diff --git a/package-lock.json b/package-lock.json
index c27bbe4..2f683f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "react-messenger-19th",
"version": "0.1.0",
"dependencies": {
+ "@tailwindcss/line-clamp": "^0.4.4",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -17,9 +18,14 @@
"@types/react-dom": "^18.2.22",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.27.0",
"react-scripts": "5.0.1",
+ "tailwind-scrollbar-hide": "^1.1.7",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.4.13"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -3338,6 +3344,14 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
+ "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3656,6 +3670,14 @@
"url": "https://github.com/sponsors/gregberge"
}
},
+ "node_modules/@tailwindcss/line-clamp": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
+ "integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
+ "peerDependencies": {
+ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
@@ -14864,6 +14886,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.27.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz",
+ "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==",
+ "dependencies": {
+ "@remix-run/router": "1.20.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.27.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz",
+ "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==",
+ "dependencies": {
+ "@remix-run/router": "1.20.0",
+ "react-router": "6.27.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -16490,10 +16542,15 @@
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
+ "node_modules/tailwind-scrollbar-hide": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz",
+ "integrity": "sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA=="
+ },
"node_modules/tailwindcss": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
- "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
+ "version": "3.4.13",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
+ "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -16503,7 +16560,7 @@
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
- "jiti": "^1.19.1",
+ "jiti": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
diff --git a/package.json b/package.json
index ea335d3..3776c08 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@tailwindcss/line-clamp": "^0.4.4",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,7 +13,9 @@
"@types/react-dom": "^18.2.22",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.27.0",
"react-scripts": "5.0.1",
+ "tailwind-scrollbar-hide": "^1.1.7",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
@@ -39,5 +42,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.4.13"
}
}
diff --git a/src/App.tsx b/src/App.tsx
index 5381007..d70c7c2 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,33 @@
+import ChatRoomPage from './pages/ChatRoomPage';
+import ChatRoomListPage from './pages/ChatRoomListPage';
+import FriendListPage from './pages/FriendListPage';
+import { Route, Routes } from 'react-router-dom';
+import MyProfilePage from './pages/MyProfilePage';
+import NotYetPage from './pages/NotYetPage';
+import { UserProvider } from './contexts/UserContext';
+import { UnreadProvider } from './contexts/UnreadContext';
+
function App() {
return (
-
-
20기 프론트엔드 파이팅!!! 디자인과 사이좋게 지내요~~~
-
+
+
+
+
+
+ }>
+ }>
+ }>
+
+ }>
+ }>
+ }>
+ }>
+ }>
+
+
+
+
+
);
}
diff --git a/src/assets/ChatRoom/addMedia.svg b/src/assets/ChatRoom/addMedia.svg
new file mode 100644
index 0000000..41f4f68
--- /dev/null
+++ b/src/assets/ChatRoom/addMedia.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/ChatRoom/back.svg b/src/assets/ChatRoom/back.svg
new file mode 100644
index 0000000..18eacf2
--- /dev/null
+++ b/src/assets/ChatRoom/back.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/ChatRoom/emoticonbtn.svg b/src/assets/ChatRoom/emoticonbtn.svg
new file mode 100644
index 0000000..ff6969f
--- /dev/null
+++ b/src/assets/ChatRoom/emoticonbtn.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/ChatRoom/menu.svg b/src/assets/ChatRoom/menu.svg
new file mode 100644
index 0000000..674a0c6
--- /dev/null
+++ b/src/assets/ChatRoom/menu.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/ChatRoom/profile.svg b/src/assets/ChatRoom/profile.svg
new file mode 100644
index 0000000..33c587a
--- /dev/null
+++ b/src/assets/ChatRoom/profile.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/ChatRoom/search.svg b/src/assets/ChatRoom/search.svg
new file mode 100644
index 0000000..7fe66cc
--- /dev/null
+++ b/src/assets/ChatRoom/search.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/ChatRoom/sendbtn.svg b/src/assets/ChatRoom/sendbtn.svg
new file mode 100644
index 0000000..03248de
--- /dev/null
+++ b/src/assets/ChatRoom/sendbtn.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/ChatRoomList/chatting_icon.svg b/src/assets/ChatRoomList/chatting_icon.svg
new file mode 100644
index 0000000..4788288
--- /dev/null
+++ b/src/assets/ChatRoomList/chatting_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/ChatRoomList/friend_icon.svg b/src/assets/ChatRoomList/friend_icon.svg
new file mode 100644
index 0000000..60f73b5
--- /dev/null
+++ b/src/assets/ChatRoomList/friend_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/ChatRoomList/more_icon.svg b/src/assets/ChatRoomList/more_icon.svg
new file mode 100644
index 0000000..e97e941
--- /dev/null
+++ b/src/assets/ChatRoomList/more_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/ChatRoomList/newChat_icon.svg b/src/assets/ChatRoomList/newChat_icon.svg
new file mode 100644
index 0000000..59338f6
--- /dev/null
+++ b/src/assets/ChatRoomList/newChat_icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/ChatRoomList/openedChat_icon.svg b/src/assets/ChatRoomList/openedChat_icon.svg
new file mode 100644
index 0000000..a43f2f6
--- /dev/null
+++ b/src/assets/ChatRoomList/openedChat_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/ChatRoomList/setting_icon.svg b/src/assets/ChatRoomList/setting_icon.svg
new file mode 100644
index 0000000..e0b5409
--- /dev/null
+++ b/src/assets/ChatRoomList/setting_icon.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/ChatRoomList/shopping_icon.svg b/src/assets/ChatRoomList/shopping_icon.svg
new file mode 100644
index 0000000..f79754c
--- /dev/null
+++ b/src/assets/ChatRoomList/shopping_icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/Common/HomeIndicator.svg b/src/assets/Common/HomeIndicator.svg
new file mode 100644
index 0000000..039e582
--- /dev/null
+++ b/src/assets/Common/HomeIndicator.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Common/shield_warning.svg b/src/assets/Common/shield_warning.svg
new file mode 100644
index 0000000..75bb670
--- /dev/null
+++ b/src/assets/Common/shield_warning.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/FriendList/down_arrow.svg b/src/assets/FriendList/down_arrow.svg
new file mode 100644
index 0000000..e85132c
--- /dev/null
+++ b/src/assets/FriendList/down_arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/FriendList/make_peong.svg b/src/assets/FriendList/make_peong.svg
new file mode 100644
index 0000000..d19d2f5
--- /dev/null
+++ b/src/assets/FriendList/make_peong.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/FriendList/peong_bg.svg b/src/assets/FriendList/peong_bg.svg
new file mode 100644
index 0000000..019a36a
--- /dev/null
+++ b/src/assets/FriendList/peong_bg.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/FriendList/redDot.svg b/src/assets/FriendList/redDot.svg
new file mode 100644
index 0000000..f691dab
--- /dev/null
+++ b/src/assets/FriendList/redDot.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/FriendList/up_arrow.svg b/src/assets/FriendList/up_arrow.svg
new file mode 100644
index 0000000..f3d9906
--- /dev/null
+++ b/src/assets/FriendList/up_arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/MyProfile/add_friend.svg b/src/assets/MyProfile/add_friend.svg
new file mode 100644
index 0000000..c081114
--- /dev/null
+++ b/src/assets/MyProfile/add_friend.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/MyProfile/multiProfile.svg b/src/assets/MyProfile/multiProfile.svg
new file mode 100644
index 0000000..83635ac
--- /dev/null
+++ b/src/assets/MyProfile/multiProfile.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/MyProfile/music.svg b/src/assets/MyProfile/music.svg
new file mode 100644
index 0000000..8d0fb6c
--- /dev/null
+++ b/src/assets/MyProfile/music.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/MyProfile/my_profile.svg b/src/assets/MyProfile/my_profile.svg
new file mode 100644
index 0000000..b8403fe
--- /dev/null
+++ b/src/assets/MyProfile/my_profile.svg
@@ -0,0 +1,19 @@
+
diff --git a/src/assets/MyProfile/right_arrow.svg b/src/assets/MyProfile/right_arrow.svg
new file mode 100644
index 0000000..d84c817
--- /dev/null
+++ b/src/assets/MyProfile/right_arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/Reactions/angry.svg b/src/assets/Reactions/angry.svg
new file mode 100644
index 0000000..78033c9
--- /dev/null
+++ b/src/assets/Reactions/angry.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/Reactions/heart.svg b/src/assets/Reactions/heart.svg
new file mode 100644
index 0000000..3496d30
--- /dev/null
+++ b/src/assets/Reactions/heart.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/Reactions/oh.svg b/src/assets/Reactions/oh.svg
new file mode 100644
index 0000000..c92037b
--- /dev/null
+++ b/src/assets/Reactions/oh.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/Reactions/plus.svg b/src/assets/Reactions/plus.svg
new file mode 100644
index 0000000..80c523f
--- /dev/null
+++ b/src/assets/Reactions/plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/Reactions/smile.svg b/src/assets/Reactions/smile.svg
new file mode 100644
index 0000000..277eead
--- /dev/null
+++ b/src/assets/Reactions/smile.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/Reactions/tear.svg b/src/assets/Reactions/tear.svg
new file mode 100644
index 0000000..eafd494
--- /dev/null
+++ b/src/assets/Reactions/tear.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/Reactions/thumb.svg b/src/assets/Reactions/thumb.svg
new file mode 100644
index 0000000..99122a7
--- /dev/null
+++ b/src/assets/Reactions/thumb.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/TopBar/Battery.svg b/src/assets/TopBar/Battery.svg
new file mode 100644
index 0000000..2096fbf
--- /dev/null
+++ b/src/assets/TopBar/Battery.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/TopBar/CellularConnection.svg b/src/assets/TopBar/CellularConnection.svg
new file mode 100644
index 0000000..71c8333
--- /dev/null
+++ b/src/assets/TopBar/CellularConnection.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/TopBar/Wifi.svg b/src/assets/TopBar/Wifi.svg
new file mode 100644
index 0000000..8fb9f26
--- /dev/null
+++ b/src/assets/TopBar/Wifi.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/ChatRoomListPage/ChatRoomComponent.tsx b/src/components/ChatRoomListPage/ChatRoomComponent.tsx
new file mode 100644
index 0000000..d0f0b28
--- /dev/null
+++ b/src/components/ChatRoomListPage/ChatRoomComponent.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import { UserData } from '../../lib/UserData';
+import { formatTimeForChatList } from '../../utils/ClockUtils';
+
+interface LastMessage {
+ text: string;
+ timestamp: Date;
+}
+
+interface ComponentProps {
+ userId: number;
+ lastMessage?: LastMessage;
+ onClick: () => void;
+ unread: number;
+}
+
+const ChatRoomComponent: React.FC = ({ userId, lastMessage, onClick, unread }) => {
+ const user = UserData.find(user => user.userId === userId);
+
+ return (
+
+
+
+ {user?.userName || '(알 수 없음)'}
+ {lastMessage?.text}
+
+
+ {lastMessage ? formatTimeForChatList(lastMessage.timestamp) : ''}
+ {unread > 0 && (
+
+ {unread}
+
+ )}
+
+
+ );
+}
+
+export default ChatRoomComponent;
\ No newline at end of file
diff --git a/src/components/ChatRoomListPage/Header.tsx b/src/components/ChatRoomListPage/Header.tsx
new file mode 100644
index 0000000..7edbc05
--- /dev/null
+++ b/src/components/ChatRoomListPage/Header.tsx
@@ -0,0 +1,25 @@
+import searchIcon from "../../assets/ChatRoom/search.svg";
+import menuIcon from "../../assets/ChatRoom/menu.svg";
+import newChatIcon from "../../assets/ChatRoomList/newChat_icon.svg";
+import settingIcon from "../../assets/ChatRoomList/setting_icon.svg";
+
+const Header = () => {
+
+ return (
+
+ );
+}
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/ChatRoomPage/ChatBar.tsx b/src/components/ChatRoomPage/ChatBar.tsx
new file mode 100644
index 0000000..5c5da20
--- /dev/null
+++ b/src/components/ChatRoomPage/ChatBar.tsx
@@ -0,0 +1,73 @@
+import React, { useState } from 'react';
+import addIcon from "../../assets/ChatRoom/addMedia.svg";
+import emoticon from "../../assets/ChatRoom/emoticonbtn.svg";
+import sendIcon from "../../assets/ChatRoom/sendbtn.svg";
+
+const ChatBar = ({ onSendMessage }: { onSendMessage: (message: string) => void }) => {
+ const [message, setMessage] = useState('');
+ const [isInputFocused, setIsInputFocused] = useState(false);
+
+ // 메시지 전송 핸들러
+ const handleSendMessage = () => {
+ if (message.trim() !== '') {
+ onSendMessage(message);
+ setMessage('');
+ setIsInputFocused(false);
+ }
+ };
+
+ // 엔터로 메시지 전송
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleSendMessage();
+ }
+ };
+
+ return (
+
+
+
+
setMessage(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onFocus={() => setIsInputFocused(true)}
+ onBlur={() => {
+ if (!message.trim()) setIsInputFocused(false);
+ }}
+ placeholder="메시지 입력하기"
+ style={{
+ transition: 'width 0.3s ease-in-out',
+ width: isInputFocused ? '284px' : '308px',
+ }}
+ className={`transition-all duration-300 w-inputWidth h-inputHeight rounded-full placeholder-Gray/4 font-['Pretendard'] pl-[12px] pr-[40px] bg-Gray/5`}
+ />
+
+ {/* 이모티콘 버튼 */}
+
+
+
+
+
+ {/* 전송 아이콘 */}
+
+
+ );
+}
+
+export default ChatBar;
diff --git a/src/components/ChatRoomPage/Header.tsx b/src/components/ChatRoomPage/Header.tsx
new file mode 100644
index 0000000..2517b25
--- /dev/null
+++ b/src/components/ChatRoomPage/Header.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import backIcon from "../../assets/ChatRoom/back.svg";
+import searchIcon from "../../assets/ChatRoom/search.svg";
+import menuIcon from "../../assets/ChatRoom/menu.svg";
+
+interface HeaderProps {
+ opponentUser: { userId: number, userName: string };
+}
+
+const Header: React.FC = ({ opponentUser }) => {
+
+ const navigate = useNavigate();
+
+ const handleBackClick = () => {
+ navigate('/chatlist');
+ }
+ return (
+
+
+
+
+ {opponentUser.userName}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/ChatRoomPage/MessageList.tsx b/src/components/ChatRoomPage/MessageList.tsx
new file mode 100644
index 0000000..ea58771
--- /dev/null
+++ b/src/components/ChatRoomPage/MessageList.tsx
@@ -0,0 +1,185 @@
+import React, { useEffect, useRef, useState } from 'react';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import { formatTime } from '../../utils/ClockUtils';
+import { useUser } from '../../contexts/UserContext';
+import heartIcon from "../../assets/Reactions/heart.svg";
+import smileIcon from "../../assets/Reactions/smile.svg";
+import ohIcon from "../../assets/Reactions/oh.svg";
+import tearIcon from "../../assets/Reactions/tear.svg";
+import angerIcon from "../../assets/Reactions/angry.svg";
+import thumbIcon from "../../assets/Reactions/thumb.svg";
+import plusIcon from "../../assets/Reactions/plus.svg";
+
+interface Message {
+ senderId: number;
+ text: string;
+ timestamp: Date;
+ emoji?: string;
+}
+
+interface MessageListProps {
+ messages: Message[];
+}
+
+// 메시지 그룹화 함수
+const groupMessages = (messages: Message[]) => {
+ return messages.reduce((arr, message, index) => {
+ const timeKey = formatTime(message.timestamp);
+ const prevMessage = messages[index - 1];
+ const isSameSender = prevMessage && prevMessage?.senderId === message.senderId;
+ const isSameMinute = prevMessage && formatTime(prevMessage.timestamp) === timeKey;
+
+ if (!isSameSender || !isSameMinute) {
+ arr.push({ timeKey, senderId: message.senderId, messages: [message] });
+ } else {
+ arr[arr.length - 1].messages.push(message);
+ }
+
+ return arr;
+ }, [] as { timeKey: string; senderId: number; messages: Message[] }[]);
+};
+
+const MessageList: React.FC = ({ messages }) => {
+ const { currentUser, opponentUser } = useUser();
+ const scrollRef = useRef(null);
+ const [reactions, setReactions] = useState<{ [key: number]: string | undefined }>({});
+ const [showReactions, setShowReactions] = useState(null);
+
+ // 렌더링 시 로컬스토리지에서 리액션 데이터 가져오기
+ useEffect(() => {
+ const storedReactions = localStorage.getItem('messageReactions');
+ if (storedReactions) {
+ setReactions(JSON.parse(storedReactions));
+ }
+ }, []);
+
+ // 로컬스토리지에 리액션 저장
+ const saveReactionsToLocalStorage = (updatedReactions: { [key: number]: string | undefined }) => {
+ localStorage.setItem('messageReactions', JSON.stringify(updatedReactions));
+ };
+
+
+ const handleDoubleClick = (index: number, event: React.MouseEvent) => {
+ event.preventDefault(); // 드래그 방지
+ setShowReactions(index === showReactions ? null : index);
+ };
+
+ const handleSelectEmoji = (index: number, emoji: string) => {
+ const currentReaction = reactions[index];
+ const updatedReactions = { ...reactions, [index]: currentReaction === emoji ? undefined : emoji };
+ setReactions(updatedReactions);
+ saveReactionsToLocalStorage(updatedReactions);
+ setShowReactions(null);
+ };
+
+ const groupedMessages = groupMessages(messages);
+
+ let lastTimeDisplayed: string | null = null;
+
+ useEffect(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, [messages]);
+
+ return (
+
+ {groupedMessages.map((group, groupIndex) => {
+ const shouldDisplayTime = group.timeKey !== lastTimeDisplayed;
+ lastTimeDisplayed = group.timeKey;
+
+ return (
+
+ {shouldDisplayTime && (
+
+ {group.timeKey}
+
+ )}
+
+{group.messages.map((message, idx) => {
+ const messageIndex = groupIndex * 100 + idx;
+ const isCurrentUser = message.senderId === currentUser?.userId;
+
+ return (
+
handleDoubleClick(messageIndex, e)}
+ style={{ userSelect: 'none'}}
+ >
+ {/* 상대방 메시지 렌더링 */}
+ {!isCurrentUser && (
+
+ {idx === 0 && (
+
+
+
+ {opponentUser?.userName || '(알 수 없음)'}
+
+
+ )}
+
+
+ {message.text}
+
+ {reactions[messageIndex] && (
+
+
+
+
+
+ )}
+
+ {/* 리액션 바 */}
+ {showReactions === messageIndex && (
+
+ {[heartIcon, smileIcon, ohIcon, tearIcon, angerIcon, thumbIcon].map((emoji, i) => (
+
handleSelectEmoji(messageIndex, emoji)}
+ >
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ {/* 본인 메시지 렌더링 */}
+ {isCurrentUser && (
+
+
+ {message.text}
+
+ {reactions[messageIndex] && (
+
+
+
+
+
+ )}
+
+ )}
+
+ );
+})}
+
+ );
+ })}
+
+
+ );
+};
+
+export default MessageList;
\ No newline at end of file
diff --git a/src/components/FriendListPage/Birthday.tsx b/src/components/FriendListPage/Birthday.tsx
new file mode 100644
index 0000000..f1e371e
--- /dev/null
+++ b/src/components/FriendListPage/Birthday.tsx
@@ -0,0 +1,28 @@
+import React, { useState } from 'react';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import foldIcon from "../../assets/FriendList/up_arrow.svg";
+import unfoldIcon from "../../assets/FriendList/down_arrow.svg";
+
+const Birthday = () => {
+ const [isFolded, setIsFolded] = useState(false);
+
+ const toggleFold = () => {
+ setIsFolded(!isFolded);
+ };
+
+ return (
+
+
+
생일인 친구
+
+
+
+ );
+}
+
+export default Birthday;
\ No newline at end of file
diff --git a/src/components/FriendListPage/FriendList.tsx b/src/components/FriendListPage/FriendList.tsx
new file mode 100644
index 0000000..03e9ff7
--- /dev/null
+++ b/src/components/FriendListPage/FriendList.tsx
@@ -0,0 +1,59 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import foldIcon from "../../assets/FriendList/up_arrow.svg";
+import unfoldIcon from "../../assets/FriendList/down_arrow.svg";
+import { UserData } from '../../lib/UserData';
+import { useUser } from '../../contexts/UserContext';
+
+const FriendList: React.FC = () => {
+ const [isFolded, setIsFolded] = useState(false);
+ const navigate = useNavigate();
+ const { currentUser } = useUser();
+
+ const toggleFold = () => {
+ setIsFolded(!isFolded);
+ };
+
+ const handleProfileClick = (userId: number) => {
+ if (!currentUser) return;
+
+ // chatKey 생성
+ const chatKey = `${Math.min(currentUser.userId, userId)}_${Math.max(currentUser.userId, userId)}`;
+
+ // 클릭한 채팅방으로 이동
+ navigate(`/chat/${chatKey}`);
+ };
+
+ return (
+
+
+
친구
+
+
+ {!isFolded && (
+
+ {UserData.filter(user => user.userId !== currentUser?.userId).map((user) => (
+
handleProfileClick(user.userId)}
+ >
+
+
+ {user.userName}
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default FriendList;
\ No newline at end of file
diff --git a/src/components/FriendListPage/Header.tsx b/src/components/FriendListPage/Header.tsx
new file mode 100644
index 0000000..868d5cd
--- /dev/null
+++ b/src/components/FriendListPage/Header.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import searchIcon from "../../assets/ChatRoom/search.svg";
+import addIcon from "../../assets/MyProfile/add_friend.svg";
+import musicIcon from "../../assets/MyProfile/music.svg";
+import settingIcon from "../../assets/ChatRoomList/setting_icon.svg";
+
+const Header = () => {
+ return (
+
+ );
+}
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/FriendListPage/Peong.tsx b/src/components/FriendListPage/Peong.tsx
new file mode 100644
index 0000000..a7fa3c5
--- /dev/null
+++ b/src/components/FriendListPage/Peong.tsx
@@ -0,0 +1,44 @@
+import React, { useState } from 'react';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import foldIcon from "../../assets/FriendList/up_arrow.svg";
+import unfoldIcon from "../../assets/FriendList/down_arrow.svg";
+import makeIcon from "../../assets/FriendList/make_peong.svg";
+import peongIcon from "../../assets/FriendList/peong_bg.svg";
+
+const Peong = () => {
+ const [isFolded, setIsFolded] = useState(false);
+
+ const toggleFold = () => {
+ setIsFolded(!isFolded);
+ };
+
+ return (
+
+
+
펑 1
+
+
+ {!isFolded && (
+
+
+
+
만들기
+
+
+
+
+
+ CEOS
+
+
+ )}
+
+ );
+}
+
+export default Peong;
\ No newline at end of file
diff --git a/src/components/FriendListPage/UpdatedProfile.tsx b/src/components/FriendListPage/UpdatedProfile.tsx
new file mode 100644
index 0000000..157b77a
--- /dev/null
+++ b/src/components/FriendListPage/UpdatedProfile.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import profileIcon from "../../assets/ChatRoom/profile.svg";
+import foldIcon from "../../assets/FriendList/up_arrow.svg";
+import unfoldIcon from "../../assets/FriendList/down_arrow.svg";
+import redDotIcon from "../../assets/FriendList/redDot.svg";
+
+const UpdatedProfile = () => {
+ const [isFolded, setIsFolded] = useState(false);
+
+ const toggleFold = () => {
+ setIsFolded(!isFolded);
+ };
+
+ return (
+
+
+
업데이트한 프로필 3
+
+
+ {!isFolded && (
+
+
+
+
+
+
CEOS
+
+
+
+
+
+
+
CEOS
+
+
+
+
+
+
CEOS
+
+
+
+
+
CEOS
+
+
+
+
CEOS
+
+
+
+
CEOS
+
+
+ )}
+
+ );
+}
+
+export default UpdatedProfile;
\ No newline at end of file
diff --git a/src/components/MyProfilePage/Header.tsx b/src/components/MyProfilePage/Header.tsx
new file mode 100644
index 0000000..c8b716a
--- /dev/null
+++ b/src/components/MyProfilePage/Header.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import backIcon from "../../assets/ChatRoom/back.svg";
+import searchIcon from "../../assets/ChatRoom/search.svg";
+import addIcon from "../../assets/MyProfile/add_friend.svg";
+import musicIcon from "../../assets/MyProfile/music.svg";
+import settingIcon from "../../assets/ChatRoomList/setting_icon.svg";
+
+const Header = () => {
+
+ const navigate = useNavigate();
+
+ const handleBackClick = () => {
+ navigate(-1);
+ }
+ return (
+
+
+
+
+ 프로필
+
+
+
+
+
+ );
+}
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/MyProfilePage/ProfileInfo.tsx b/src/components/MyProfilePage/ProfileInfo.tsx
new file mode 100644
index 0000000..d5173ff
--- /dev/null
+++ b/src/components/MyProfilePage/ProfileInfo.tsx
@@ -0,0 +1,25 @@
+import arrowIcon from "../../assets/MyProfile/right_arrow.svg";
+
+interface ProfileInfoProps {
+ title: string;
+ content: string;
+}
+
+const ProfileInfo: React.FC = ({ title, content }) => {
+
+ return (
+
+
+
+ {title}
+
+
+ {content}
+
+
+
+
+ );
+}
+
+export default ProfileInfo;
\ No newline at end of file
diff --git a/src/components/MyProfilePage/UserProfile.tsx b/src/components/MyProfilePage/UserProfile.tsx
new file mode 100644
index 0000000..92fc03f
--- /dev/null
+++ b/src/components/MyProfilePage/UserProfile.tsx
@@ -0,0 +1,38 @@
+import { useNavigate } from 'react-router-dom';
+import profileIcon from "../../assets/MyProfile/my_profile.svg";
+import multiProfileIcon from "../../assets/MyProfile/multiProfile.svg";
+import { useUser } from '../../contexts/UserContext';
+
+const UserProfile = () => {
+ const navigate = useNavigate();
+ const { currentUser, toggleUser } = useUser();
+
+ const handleProfileClick = () => {
+ navigate('/my');
+ };
+
+ return (
+
+
+
+
+ {currentUser?.userName}
+
+
+
{
+ e.stopPropagation(); // 부모 클릭 이벤트 방지
+ toggleUser(); // 전역 상태에서 유저 토글
+ }}
+ className='cursor-pointer'
+ />
+
+ );
+}
+
+export default UserProfile;
\ No newline at end of file
diff --git a/src/components/common/HomeIndicatior.tsx b/src/components/common/HomeIndicatior.tsx
new file mode 100644
index 0000000..73a1bdf
--- /dev/null
+++ b/src/components/common/HomeIndicatior.tsx
@@ -0,0 +1,13 @@
+import indicator from "../../assets/Common/HomeIndicator.svg";
+
+const HomeIndicator = () => {
+ return (
+
+
+
+
+
+ );
+}
+
+export default HomeIndicator;
diff --git a/src/components/common/Line.tsx b/src/components/common/Line.tsx
new file mode 100644
index 0000000..8fdcaf7
--- /dev/null
+++ b/src/components/common/Line.tsx
@@ -0,0 +1,8 @@
+const Line = () => {
+ return (
+
+
+ );
+}
+
+export default Line;
diff --git a/src/components/common/NavBar.tsx b/src/components/common/NavBar.tsx
new file mode 100644
index 0000000..ea2079f
--- /dev/null
+++ b/src/components/common/NavBar.tsx
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { ReactComponent as FriendIcon } from "../../assets/ChatRoomList/friend_icon.svg";
+import { ReactComponent as ChatIcon } from "../../assets/ChatRoomList/chatting_icon.svg";
+import { ReactComponent as OpenedChatIcon } from "../../assets/ChatRoomList/openedChat_icon.svg";
+import { ReactComponent as ShoppingIcon } from "../../assets/ChatRoomList/shopping_icon.svg";
+import { ReactComponent as MoreIcon } from "../../assets/ChatRoomList/more_icon.svg";
+import { useUnread } from '../../contexts/UnreadContext';
+
+const NavBar: React.FC = () => {
+ const basicBtnStyle = "text-[9px] font-['Pretendard'] flex flex-col items-center cursor-pointer";
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [activePath, setActivePath] = useState(location.pathname);
+ const { totalUnread } = useUnread();
+
+ useEffect(() => {
+ setActivePath(location.pathname);
+ }, [location]);
+
+ const onBtnClick = (path: string) => {
+ setActivePath(path);
+ navigate(path);
+ }
+
+ const getBtnStyle = (path: string) => {
+ return activePath === path
+ ? `text-Purple/1 ${basicBtnStyle}`
+ : `text-Gray/2 ${basicBtnStyle}`;
+ }
+
+ const getIconStyle = (path: string) => {
+ return activePath === path ? "#AB78FF" : "#666666";
+ }
+
+ return (
+
+
onBtnClick("/friends")} className={getBtnStyle("/friends")} style={{color: getIconStyle("/friends")}}>
+ 친구
+
+
onBtnClick("/chatlist")} className={`${getBtnStyle("/chatlist")} relative`} style={{color: getIconStyle("/chatlist")}}>
+
+ {totalUnread > 0 && (
+
+ {totalUnread}
+
+ )}
+ 채팅
+
+
onBtnClick("/openedChat")} className={getBtnStyle("/openedChat")} style={{color: getIconStyle("/openedChat")}}>
+ 오픈채팅
+
+
onBtnClick("/shopping")} className={getBtnStyle("/shopping")} style={{color: getIconStyle("/shopping")}}>
+ 쇼핑
+
+
onBtnClick("/more")} className={getBtnStyle("/more")} style={{color: getIconStyle("/more")}}>
+ 더보기
+
+
+ );
+}
+
+export default NavBar;
diff --git a/src/components/common/TopBar.tsx b/src/components/common/TopBar.tsx
new file mode 100644
index 0000000..7bea561
--- /dev/null
+++ b/src/components/common/TopBar.tsx
@@ -0,0 +1,34 @@
+import React, { useState, useEffect } from 'react';
+import { currentTime } from '../../utils/ClockUtils';
+import BatteryIcon from '../../assets/TopBar/Battery.svg';
+import WifiIcon from '../../assets/TopBar/Wifi.svg';
+import NetworkIcon from '../../assets/TopBar/CellularConnection.svg';
+
+const TopBar = () => {
+ const [time, setTime] = useState(currentTime());
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTime(currentTime());
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+ {/* 현재 시간 표시 */}
+
+ {time}
+
+ {/* 상단바 아이콘 */}
+
+
+ );
+}
+
+export default TopBar;
diff --git a/src/contexts/UnreadContext.tsx b/src/contexts/UnreadContext.tsx
new file mode 100644
index 0000000..9af8edd
--- /dev/null
+++ b/src/contexts/UnreadContext.tsx
@@ -0,0 +1,49 @@
+import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
+import { useUser } from './UserContext';
+import { UserData } from '../lib/UserData';
+
+interface UnreadContextType {
+ totalUnread: number;
+ calculateUnread: () => void;
+}
+
+const UnreadContext = createContext(undefined);
+
+export const UnreadProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ const { currentUser } = useUser();
+ const [totalUnread, setTotalUnread] = useState(0);
+
+ const calculateUnread = () => {
+ if (!currentUser) return;
+ let cnt = 0;
+ UserData.forEach(user => {
+ if (user.userId !== currentUser.userId) {
+ const chatKey = `messages_${Math.min(currentUser.userId, user.userId)}_${Math.max(currentUser.userId, user.userId)}`;
+ const savedMessages = localStorage.getItem(chatKey);
+ if (savedMessages) {
+ const parsedMessages = JSON.parse(savedMessages);
+ cnt += parsedMessages.filter((msg: any) => !msg.read && msg.senderId !== currentUser.userId).length;
+ }
+ }
+ });
+ setTotalUnread(cnt);
+ };
+
+ useEffect(() => {
+ calculateUnread();
+ }, [currentUser]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useUnread = (): UnreadContextType => {
+ const context = useContext(UnreadContext);
+ if (!context) {
+ throw new Error("error");
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx
new file mode 100644
index 0000000..71fdbcd
--- /dev/null
+++ b/src/contexts/UserContext.tsx
@@ -0,0 +1,63 @@
+import React, { createContext, useState, useContext, ReactNode, useEffect } from 'react';
+import { UserData } from '../lib/UserData';
+
+interface User {
+ userId: number;
+ userName: string;
+ phoneNumber: string;
+ email: string;
+ sns: string;
+}
+
+interface UserContextType {
+ currentUser: User | null;
+ opponentUser: User | null;
+ toggleUser: () => void;
+}
+
+const UserContext = createContext(undefined);
+
+interface UserProviderProps {
+ children: ReactNode;
+}
+
+export const UserProvider = ({ children }: UserProviderProps) => {
+ // 로컬스토리지에서 currentUser 가져오기
+ const storedUser = localStorage.getItem('currentUser');
+ const initialUser = storedUser ? JSON.parse(storedUser) : UserData.find(user => user.userId === 0) || null;
+
+ const [currentUser, setCurrentUser] = useState(initialUser);
+
+ // currentUser가 변경될 때마다 로컬스토리지에 저장
+ useEffect(() => {
+ if (currentUser) {
+ localStorage.setItem('currentUser', JSON.stringify(currentUser));
+ }
+ }, [currentUser]);
+
+ // currentUser의 userId에 따라 opponentUser 설정
+ const opponentUser: User | null = currentUser?.userId === 0
+ ? UserData.find(user => user.userId === 1) || null
+ : UserData.find(user => user.userId === 0) || null;
+
+ const toggleUser = () => {
+ const newUser = currentUser?.userId === 0
+ ? UserData.find(user => user.userId === 1)
+ : UserData.find(user => user.userId === 0);
+ if (newUser) setCurrentUser(newUser);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useUser = (): UserContextType => {
+ const context = useContext(UserContext);
+ if (!context) {
+ throw new Error("error");
+ }
+ return context;
+};
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 0000000..4ec4690
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,4 @@
+declare module '*.svg' {
+ const content: any;
+ export default content;
+}
\ No newline at end of file
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
new file mode 100644
index 0000000..d7d1bdc
--- /dev/null
+++ b/src/declaration.d.ts
@@ -0,0 +1,6 @@
+declare module "*.svg" {
+ import * as React from 'react';
+ export const ReactComponent: React.FunctionComponent>;
+ const src: string;
+ export default src;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index ec2585e..b155dce 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,13 +1,25 @@
-body {
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@font-face {
+ font-family: "Pretendard-Regular";
+ src: url("https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff")
+ format("woff");
+ font-weight: 400;
+ font-style: normal;
+}
+
+/* body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
-}
+} */
diff --git a/src/index.tsx b/src/index.tsx
index d10be77..5e97bd5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,12 +2,15 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
+import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
-
+
+
+
);
diff --git a/src/lib/MessageData.tsx b/src/lib/MessageData.tsx
new file mode 100644
index 0000000..4cf8d9e
--- /dev/null
+++ b/src/lib/MessageData.tsx
@@ -0,0 +1,26 @@
+export const MessageData = [
+ {
+ senderId: 0,
+ text: "안녕하세요!",
+ timestamp: new Date('2024-09-25T10:30:00'),
+ read: true,
+ },
+ {
+ senderId: 0,
+ text: "1주차 세션 팀구성이 궁금합니다!",
+ timestamp: new Date('2024-09-25T10:30:00'),
+ read: true,
+ },
+ {
+ senderId: 1,
+ text: "안녕하세요!",
+ timestamp: new Date('2024-09-25T10:35:00'),
+ read: true,
+ },
+ {
+ senderId: 1,
+ text: "세오스 공개용 노션에 비상관제시어 관련하여 공지 업로드 했습니다. 참고해주세요!",
+ timestamp: new Date('2024-09-25T10:35:00'),
+ read: true,
+ },
+];
diff --git a/src/lib/UserData.tsx b/src/lib/UserData.tsx
new file mode 100644
index 0000000..9ee2f74
--- /dev/null
+++ b/src/lib/UserData.tsx
@@ -0,0 +1,51 @@
+export const UserData = [
+ {
+ userId: 0,
+ userName: '김태양',
+ phoneNumber: '2052-8337',
+ email: 'tangerin2601@naver.com',
+ sns: '@taeyaaaaaaang',
+ },
+ {
+ userId: 1,
+ userName: '이가빈',
+ phoneNumber: '1886-1029',
+ email: 'billy@gmail.com',
+ sns: '@billyboo',
+ },
+ {
+ userId: 2,
+ userName: '김민지',
+ phoneNumber: '2004-0507',
+ email: 'minji@gmail.com',
+ sns: '@newjeans_official',
+ },
+ {
+ userId: 3,
+ userName: '팜하니',
+ phoneNumber: '2004-1006',
+ email: 'hanni@gmail.com',
+ sns: '@newjeans_official',
+ },
+ {
+ userId: 4,
+ userName: '모지혜',
+ phoneNumber: '2005-0411',
+ email: 'danielle@gmail.com',
+ sns: '@newjeans_official',
+ },
+ {
+ userId: 5,
+ userName: '강해린',
+ phoneNumber: '2006-0515',
+ email: 'haerin@gmail.com',
+ sns: '@newjeans_official',
+ },
+ {
+ userId: 6,
+ userName: '이혜인',
+ phoneNumber: '2008-0421',
+ email: 'hyein@gmail.com',
+ sns: '@newjeans_official',
+ },
+];
\ No newline at end of file
diff --git a/src/pages/ChatRoomListPage.tsx b/src/pages/ChatRoomListPage.tsx
new file mode 100644
index 0000000..4894a98
--- /dev/null
+++ b/src/pages/ChatRoomListPage.tsx
@@ -0,0 +1,107 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import HomeIndicator from '../components/common/HomeIndicatior';
+import TopBar from '../components/common/TopBar';
+import NavBar from '../components/common/NavBar';
+import Header from '../components/ChatRoomListPage/Header';
+import Line from '../components/common/Line';
+import ChatRoomComponent from '../components/ChatRoomListPage/ChatRoomComponent';
+import { UserData } from '../lib/UserData';
+import { useUser } from '../contexts/UserContext';
+import { useUnread } from '../contexts/UnreadContext';
+
+interface Message {
+ senderId: number;
+ text: string;
+ timestamp: Date;
+ read: boolean;
+}
+
+const ChatRoomListPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { currentUser } = useUser();
+ const [lastMessages, setLastMessages] = useState<{ [userId: number]: Message | null }>({});
+ const [unread, setUnread] = useState<{ [userId: number]: number }>({});
+ const { calculateUnread } = useUnread();
+
+ useEffect(() => {
+ //localStorage.clear();
+ const newUnread: { [userId: number]: number } = {};
+ const lastMessagesByUser: { [userId: number]: Message | null } = {};
+
+ UserData.forEach(user => {
+ // 각 유저별 메시지 불러오기
+ if (currentUser && user.userId !== currentUser.userId) {
+ // currentUser와 user 간의 공통 대화 키 생성
+ const chatKey = `messages_${Math.min(currentUser.userId, user.userId)}_${Math.max(currentUser.userId, user.userId)}`;
+ const savedMessages = localStorage.getItem(chatKey);
+ if (savedMessages) {
+ const parsedMessages: Message[] = JSON.parse(savedMessages).map((msg: any) => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp),
+ }));
+
+ lastMessagesByUser[user.userId] = parsedMessages.length > 0 ? parsedMessages[parsedMessages.length - 1] : null;
+ newUnread[user.userId] = parsedMessages.filter(msg => !msg.read && msg.senderId !== currentUser.userId).length;
+ } else {
+ lastMessagesByUser[user.userId] = null;
+ newUnread[user.userId] = 0;
+ }
+ }
+ });
+
+ setLastMessages(lastMessagesByUser);
+ setUnread(newUnread);
+ }, [currentUser]);
+
+ const handleChatRoomClick = (userId: number) => {
+ if (!currentUser) return;
+
+ // chatKey 생성
+ const chatKey = `messages_${Math.min(currentUser.userId, userId)}_${Math.max(currentUser.userId, userId)}`;
+
+ // 해당 채팅방의 안 읽은 메시지 개수 0으로 초기화
+ setUnread(prevUnread => ({
+ ...prevUnread,
+ [userId]: 0
+ }));
+
+ // 로컬스토리지에서 해당 채팅방의 모든 메시지를 읽음으로 표시
+ const savedMessages = localStorage.getItem(chatKey);
+ if (savedMessages) {
+ const messages: Message[] = (JSON.parse(savedMessages) as Message[]).map((msg: Message) =>
+ msg.senderId !== currentUser.userId ? { ...msg, read: true } : msg
+ );
+ localStorage.setItem(chatKey, JSON.stringify(messages));
+ calculateUnread();
+ }
+
+ // 클릭한 채팅방으로 이동
+ navigate(`/chat/${Math.min(currentUser.userId, userId)}_${Math.max(currentUser.userId, userId)}`);
+ };
+
+ return (
+
+
+
+
+ {UserData.filter(user => currentUser && user.userId !== currentUser.userId).map(user => (
+ lastMessages[user.userId] && (
+
handleChatRoomClick(user.userId)}
+ unread={unread[user.userId] || 0}
+ />
+ )
+ ))}
+
+
+
+
+
+ );
+}
+
+export default ChatRoomListPage;
diff --git a/src/pages/ChatRoomPage.tsx b/src/pages/ChatRoomPage.tsx
new file mode 100644
index 0000000..33ff25a
--- /dev/null
+++ b/src/pages/ChatRoomPage.tsx
@@ -0,0 +1,90 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import ChatBar from '../components/ChatRoomPage/ChatBar';
+import Header from '../components/ChatRoomPage/Header';
+import HomeIndicator from '../components/common/HomeIndicatior';
+import TopBar from '../components/common/TopBar';
+import MessageList from '../components/ChatRoomPage/MessageList';
+import { MessageData } from '../lib/MessageData';
+import { UserData } from '../lib/UserData';
+import { useUser } from '../contexts/UserContext';
+
+interface Message {
+ senderId: number;
+ text: string;
+ timestamp: Date;
+}
+
+const ChatRoomPage: React.FC = () => {
+ const { chatKey } = useParams<{ chatKey: string }>();
+ const { currentUser } = useUser();
+ const [messages, setMessages] = useState([]);
+
+ // chatKey에서 userId 추출
+ const [user1Id, user2Id] = chatKey?.split('_').map(Number) || [];
+ const opponentUser = UserData.find(user => user.userId === (currentUser?.userId === user1Id ? user2Id : user1Id));
+
+ // 초기 메시지 및 로컬스토리지 메시지 불러오기
+ useEffect(() => {
+ if (!chatKey) return;
+
+ const isDefaultChatKey = chatKey === '0_1' || chatKey === '1_0';
+
+ const savedMessages = localStorage.getItem(`messages_${chatKey}`);
+ if (savedMessages) {
+ const parsedMessages: Message[] = JSON.parse(savedMessages).map((msg: any) => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp),
+ }));
+ setMessages(parsedMessages);
+ } else if (isDefaultChatKey) {
+ // 기본 메시지 설정
+ const initialMessages: Message[] = MessageData.filter(
+ msg => msg.senderId === opponentUser?.userId || msg.senderId === currentUser?.userId
+ ).map(msg => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp),
+ }));
+ setMessages(initialMessages);
+
+ // 초기 메시지 로컬스토리지에 저장
+ localStorage.setItem(`messages_${chatKey}`, JSON.stringify(initialMessages));
+
+ }
+ }, [chatKey, opponentUser, currentUser]);
+
+ // 메시지 전송 핸들러
+ const handleSendMessage = (message: string) => {
+ if (!chatKey || !currentUser) return;
+
+ const newMessage: Message = {
+ senderId: currentUser.userId,
+ text: message,
+ timestamp: new Date()
+ };
+
+ setMessages((prevMessages) => {
+ const updatedMessages = [...prevMessages, newMessage];
+ // 로컬 스토리지에 저장
+ localStorage.setItem(`messages_${chatKey}`, JSON.stringify(updatedMessages));
+
+ return updatedMessages;
+ });
+ };
+
+ if (!opponentUser) return 상대방 정보를 찾을 수 없습니다.
;
+
+ return (
+
+ );
+}
+
+export default ChatRoomPage;
diff --git a/src/pages/FriendListPage.tsx b/src/pages/FriendListPage.tsx
new file mode 100644
index 0000000..2a57344
--- /dev/null
+++ b/src/pages/FriendListPage.tsx
@@ -0,0 +1,37 @@
+import React, { useState, useEffect } from 'react';
+import HomeIndicator from '../components/common/HomeIndicatior';
+import TopBar from '../components/common/TopBar';
+import NavBar from '../components/common/NavBar';
+import Header from '../components/FriendListPage/Header';
+import UserProfile from '../components/MyProfilePage/UserProfile';
+import UpdatedProfile from '../components/FriendListPage/UpdatedProfile';
+import Peong from '../components/FriendListPage/Peong';
+import Birthday from '../components/FriendListPage/Birthday';
+import FriendList from '../components/FriendListPage/FriendList';
+import Line from '../components/common/Line';
+
+const FriendListPage = () => {
+ return (
+
+ );
+}
+
+export default FriendListPage;
diff --git a/src/pages/MyProfilePage.tsx b/src/pages/MyProfilePage.tsx
new file mode 100644
index 0000000..db7726e
--- /dev/null
+++ b/src/pages/MyProfilePage.tsx
@@ -0,0 +1,35 @@
+import React, { useState, useEffect } from 'react';
+import HomeIndicator from '../components/common/HomeIndicatior';
+import TopBar from '../components/common/TopBar';
+import NavBar from '../components/common/NavBar';
+import Header from '../components/MyProfilePage/Header';
+import UserProfile from '../components/MyProfilePage/UserProfile';
+import Line from '../components/common/Line';
+import ProfileInfo from '../components/MyProfilePage/ProfileInfo';
+import { useUser } from '../contexts/UserContext';
+
+const MyProfilePage = () => {
+ const { currentUser } = useUser();
+
+ return (
+
+
+
+
+
+ {currentUser && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+ );
+}
+
+export default MyProfilePage;
diff --git a/src/pages/NotYetPage.tsx b/src/pages/NotYetPage.tsx
new file mode 100644
index 0000000..7714aff
--- /dev/null
+++ b/src/pages/NotYetPage.tsx
@@ -0,0 +1,27 @@
+import HomeIndicator from '../components/common/HomeIndicatior';
+import TopBar from '../components/common/TopBar';
+import NavBar from '../components/common/NavBar';
+import Icon from "../assets/Common/shield_warning.svg";
+
+const NotYetPage = () => {
+ return (
+
+
+
+
+
+ 서비스 준비중입니다.
+
+ 보다 나은 서비스 제공을 위하여 페이지 준비중에 있습니다.
+
+ 빠른 시일 내에 준비하여 찾아뵙겠습니다.
+
+
+
+
+
+
+ );
+}
+
+export default NotYetPage;
diff --git a/src/utils/ClockUtils.tsx b/src/utils/ClockUtils.tsx
new file mode 100644
index 0000000..667a939
--- /dev/null
+++ b/src/utils/ClockUtils.tsx
@@ -0,0 +1,72 @@
+// 현재 시간 포맷 (상단바)
+export const currentTime = () => {
+ return new Date().toLocaleTimeString('ko-KR', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+};
+
+// 타임스탬프 포맷
+export const formatTime = (date: any): string => {
+ // Date 객체가 아닌 경우
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
+ return 'Invalid date';
+ }
+
+ const now = new Date();
+ const diffInTime = now.getTime() - date.getTime();
+ const diffInDays = Math.floor(diffInTime / (1000 * 60 * 60 * 24));
+
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? '오후' : '오전';
+ const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
+ const timeString = `${ampm} ${formattedHours}:${minutes < 10 ? '0' : ''}${minutes}`;
+
+ if (diffInDays === 0) {
+ return timeString;
+ } else if (diffInDays === 1) {
+ return `(어제) ${timeString}`;
+ } else if (diffInDays === 2) {
+ return `(그저께) ${timeString}`;
+ } else if (diffInDays <= 3) {
+ return `(${diffInDays}일 전) ${timeString}`;
+ } else {
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `(${month}/${day}) ${timeString}`;
+ }
+};
+
+// 채팅 리스트용 타임스탬프
+export const formatTimeForChatList = (date: any): string => {
+ // Date 객체가 아닌 경우
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
+ return 'Invalid date';
+ }
+
+ const now = new Date();
+ const diffInTime = now.getTime() - date.getTime();
+ const diffInDays = Math.floor(diffInTime / (1000 * 60 * 60 * 24));
+
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? '오후' : '오전';
+ const formattedHours = hours % 12 === 0 ? 12 : hours % 12;
+ const timeString = `${ampm} ${formattedHours}:${minutes < 10 ? '0' : ''}${minutes}`;
+
+ if (diffInDays === 0) {
+ return timeString;
+ } else if (diffInDays === 1) {
+ return `어제`;
+ } else if (diffInDays === 2) {
+ return `그저께`;
+ } else if (diffInDays <= 3) {
+ return `${diffInDays}일 전)`;
+ } else {
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${month}/${day}`;
+ }
+};
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..f3a28fe
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,42 @@
+/** @type {import('tailwindcss').Config} */
+
+module.exports = {
+ content: [ "./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
+ theme: {
+ colors: {
+ 'Purple/1': '#AB78FF',
+ 'Purple/2': '#B98FFF',
+ 'Purple/3': '#D3B8FF',
+ 'White': '#FFFFFF',
+ 'Gray/2': '#666666',
+ 'Gray/3': '#999999',
+ 'Gray/4' : "#CCCCCC",
+ 'Gray/5': '#EDEDED',
+ 'Black': '#000000',
+ },
+ extend: {
+ width: {
+ 'width': '375px',
+ 'inputWidth':'308px',
+ },
+ height: {
+ 'height': '812px',
+ 'topBarHeight': '52px',
+ 'headerHeight': '48px',
+ 'messageAreaHeight':'631px',
+ 'chatBarHeight': '68px',
+ 'inputHeight': '40px',
+ 'indicatorHeight': '13px',
+ 'navBarHeight': '70px',
+ 'ChatRoomComponentHeight': '76px',
+ 'profileTitleHeight': '86px',
+ },
+ }
+ },
+ plugins: [
+ require("tailwind-scrollbar-hide"),
+ require('@tailwindcss/line-clamp'),
+ ],
+}
+
+
diff --git a/tsconfig.json b/tsconfig.json
index a273b0c..1152a63 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,7 @@
"jsx": "react-jsx"
},
"include": [
- "src"
+ "src",
+ "src/custom.d.ts",
]
}