diff --git a/README.md b/README.md
index 6ab8e98..8d6c55d 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,56 @@
-# 서론
+# 4주차 미션: React-Messenger 💌
-안녕하세요 🙌🏻 18기 프론트 운영진 김문기입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
+## 서론
-이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
+안녕하세요 🙌🏻 프론트엔드 운영진 김문기입니다.
-동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다.
+다들 저번주 미션은 어떠셨나요? 이번주에는 저번 과제를 확장하여 보다 더 완성도 높은 메신저 서비스를 만들어 봅시다.
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
+이번주 과제의 목표는 React에서 **Routing**을 구현하는 방법과 **상태를 관리**하는 방법에 대해 익숙해지는 것입니다. 해당 부분을 잘 고려하시면서 미션을 수행해 주세요!
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
+또한, 이번주에는 디자이너 측에서 QA를 전달해주실 예정입니다. 전달받은 QA에 대해 디자이너와 소통 후, 이를 고쳐보시는 과정도 수행해주세요!
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
+그럼 이번주도 파이팅입니다 😤
-그럼 이번 미션도 파이팅입니다!!🎉
+## 미션
-# 미션
+### 미션 목표
-## Key Questions
-
-- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요?
-- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본
-- 컴포넌트를 분리한 기준은 무엇인가요?
-- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요?
-- 디자이너와 소통하며 느낀점은 무엇인가요?
+- SPA의 개념을 이해하고, 그에 따른 라우팅을 구현합니다.
+- 디자이너로부터 QA를 전달받고, 이에 대한 대응합니다.
+- React에서 사용하는 상태 관리 방법에 익숙해집니다.
+- UI 컴포넌트의 중복을 줄여 봅니다.
+- 코드를 확장/재사용/리팩토링하는 방법을 이해합니다.
-## 미션 목표
+### 기한
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다.
+2023년 11월 3일 금요일 (기한 엄수!)
-## 기한
+### 필수 요건
-2023년 9월 29일 금요일
+- 친구 목록 페이지, 채팅 목록 페이지, 설정 페이지 세 부분으로 구성합니다.
+- 채팅 목록을 누르면 3주차 미션에서 구현했던 채팅방으로 이동합니다.
+- 검색 기능을 추가하여 검색한 내용과 일치하는 이름을 가진 사용자만 화면에 표시합니다.
+- (선택) 각자 메신저에 추가하고 싶거나, 구현하고 싶은 기능 마음껏 구현합니다. ✨
+- Custom hooks를 통해 중복되는 로직을 줄입니다.
-## 필수 구현 기능
+### 선택 사항
-- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
+- Recoil, Redux 등의 상태 관리 라이브러리를 적용해 봅니다.
+- Base UI component system을 통해 UI 컴포넌트의 코드 재사용성을 높입니다.
-### 추가 구현 기능
-
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
+## Key Questions
-참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
+- 디자이너로부터 받은 QA 목록
+- QA 반영한 커밋(or 브랜치) 링크 (커밋 분리 필수!!!)
+- Routing
+- SPA
+- 상태관리
## 링크 및 참고자료
-- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html)
-- [React의 Hooks 완벽 정복하기](https://velog.io/@velopert/react-hooks#1-usestate)
-- [useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/)
-- [코딩 컨벤션](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION)
-- [타입스크립트 핸드북](https://joshua1988.github.io/ts/intro.html)
-- [리액트 프로젝트에서 타입스크립트 사용하기 (시리즈)](https://velog.io/@velopert/series/react-with-typescript)
-- [디자인 시스템 구축기](https://yozm.wishket.com/magazine/detail/1830/)
-- [[영상] : 컴포넌트에 대한 이해](https://www.youtube.com/watch?v=21eiJc90ggo)
-- [Styled Component로 디자인 시스템 구축하기](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/)
-- [ts 절대경로 설정하기](https://tesseractjh.tistory.com/232)
+- [React Router v6 튜토리얼](https://velog.io/@velopert/react-router-v6-tutorial)
+- [(선택) react-router v6에서는 어떤 것들이 변했을까?](https://blog.woolta.com/categories/1/posts/211)
+- [React 상태 관리 가이드](https://www.stevy.dev/react-state-management-guide/)
+- [Flux 패턴이란?](https://velog.io/@huurray/React%EC%9D%98-%ED%83%84%EC%83%9D%EA%B3%BC-Flux-%ED%8C%A8%ED%84%B4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC)
+- [useReducer](https://www.daleseo.com/react-hooks-use-reducer/)
diff --git a/package-lock.json b/package-lock.json
index 82a715f..7e60806 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,9 +11,21 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^29.5.5",
+ "@types/node": "^20.7.1",
+ "@types/react": "^18.2.23",
+ "@types/react-dom": "^18.2.8",
+ "datejs": "^1.0.0-rc3",
+ "dayjs": "^1.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.7",
+ "styled-components": "^6.0.8",
+ "styled-reset": "^4.5.1",
+ "typescript": "^5.2.2",
+ "uuid": "^9.0.1",
"web-vitals": "^2.1.4"
}
},
@@ -53,6 +65,78 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@babel/cli": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.15.tgz",
+ "integrity": "sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "commander": "^4.0.1",
+ "convert-source-map": "^1.1.0",
+ "fs-readdir-recursive": "^1.1.0",
+ "glob": "^7.2.0",
+ "make-dir": "^2.1.0",
+ "slash": "^2.0.0"
+ },
+ "bin": {
+ "babel": "bin/babel.js",
+ "babel-external-helpers": "bin/babel-external-helpers.js"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "optionalDependencies": {
+ "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
+ "chokidar": "^3.4.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/@babel/cli/node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
@@ -530,6 +614,20 @@
"@babel/core": "^7.13.0"
}
},
+ "node_modules/@babel/plugin-external-helpers": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.22.5.tgz",
+ "integrity": "sha512-ngnNEWxmykPk82mH4ajZT0qTztr3Je6hrMuKAslZVM8G1YZTENJSYwrIGtt6KOtznug3exmAtF4so/nPqJuA4A==",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-proposal-class-properties": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
@@ -596,6 +694,25 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/plugin-proposal-object-rest-spread": {
+ "version": "7.20.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz",
+ "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.",
+ "dependencies": {
+ "@babel/compat-data": "^7.20.5",
+ "@babel/helper-compilation-targets": "^7.20.7",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.20.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/plugin-proposal-optional-chaining": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
@@ -2270,6 +2387,24 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+ "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -3132,6 +3267,12 @@
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
},
+ "node_modules/@nicolo-ribaudo/chokidar-2": {
+ "version": "2.1.8-no-fsevents.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
+ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==",
+ "optional": true
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3241,6 +3382,14 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz",
+ "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==",
+ "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",
@@ -4297,9 +4446,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"node_modules/@types/node": {
- "version": "20.6.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.4.tgz",
- "integrity": "sha512-nU6d9MPY0NBUMiE/nXd2IIoC4OLvsLpwAjheoAeuzgvDZA1Cb10QYg+91AF6zQiKWRN5i1m07x6sMe0niBznoQ=="
+ "version": "20.7.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz",
+ "integrity": "sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -4332,9 +4481,9 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/react": {
- "version": "18.2.22",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz",
- "integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==",
+ "version": "18.2.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.23.tgz",
+ "integrity": "sha512-qHLW6n1q2+7KyBEYnrZpcsAmU/iiCh9WGCKgXvMxx89+TYdJWRjZohVIo9XTcoLhfX3+/hP0Pbulu3bCZQ9PSA==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4342,9 +4491,9 @@
}
},
"node_modules/@types/react-dom": {
- "version": "18.2.7",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
- "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
+ "version": "18.2.8",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz",
+ "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==",
"dependencies": {
"@types/react": "*"
}
@@ -4412,6 +4561,11 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw=="
+ },
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.9",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz",
@@ -5826,6 +5980,14 @@
"node": ">= 6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -6261,6 +6423,14 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-declaration-sorter": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",
@@ -6442,6 +6612,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -6657,6 +6837,19 @@
"node": ">=10"
}
},
+ "node_modules/datejs": {
+ "version": "1.0.0-rc3",
+ "resolved": "https://registry.npmjs.org/datejs/-/datejs-1.0.0-rc3.tgz",
+ "integrity": "sha512-c6kzWmGUKLrdT+8LjUvtAObOgsecqnkpOYOuSDzYdNhdCk3+WALxtBuSMRJDmOfS6JVs/+N2yTGHb0D8oNqB/Q==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.10",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+ },
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -8549,6 +8742,11 @@
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz",
"integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ=="
},
+ "node_modules/fs-readdir-recursive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+ "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA=="
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -8812,6 +9010,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hamt_plus": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
+ "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
+ },
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -14686,6 +14889,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz",
+ "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==",
+ "dependencies": {
+ "@remix-run/router": "1.9.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz",
+ "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==",
+ "dependencies": {
+ "@remix-run/router": "1.9.0",
+ "react-router": "6.16.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",
@@ -14790,6 +15023,25 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recoil": {
+ "version": "0.7.7",
+ "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
+ "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
+ "dependencies": {
+ "hamt_plus": "1.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -15513,6 +15765,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -15581,6 +15838,14 @@
"websocket-driver": "^0.7.4"
}
},
+ "node_modules/sockjs/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -16009,6 +16274,60 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.0.8.tgz",
+ "integrity": "sha512-AwI02MTWZwqjzfXgR5QcbmcSn5xVjY4N2TLjSuYnmuBGF3y7GicHz3ysbpUq2EMJP5M8/Nc22vcmF3V3WNZDFA==",
+ "dependencies": {
+ "@babel/cli": "^7.21.0",
+ "@babel/core": "^7.21.0",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/plugin-external-helpers": "^7.18.6",
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
+ "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
+ "@babel/preset-env": "^7.20.2",
+ "@babel/preset-react": "^7.18.6",
+ "@babel/preset-typescript": "^7.21.0",
+ "@babel/traverse": "^7.21.2",
+ "@emotion/is-prop-valid": "^1.2.1",
+ "@emotion/unitless": "^0.8.0",
+ "@types/stylis": "^4.0.2",
+ "css-to-react-native": "^3.2.0",
+ "csstype": "^3.1.2",
+ "postcss": "^8.4.23",
+ "shallowequal": "^1.1.0",
+ "stylis": "^4.3.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "babel-plugin-styled-components": ">= 2",
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-styled-components": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/styled-reset": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/styled-reset/-/styled-reset-4.5.1.tgz",
+ "integrity": "sha512-6EvFWZRwaFRFxiPYMwmnzOe33rDkw5r9jIU0eEi49bkt6VSrvjeMp2ZOw/YFbw5SVs81llIY+5fzHtR2/VBZfQ==",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "styled-components": ">=4.0.0 || >=5.0.0 || >=6.0.0"
+ }
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -16024,6 +16343,11 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz",
+ "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ=="
+ },
"node_modules/sucrase": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
@@ -16653,16 +16977,15 @@
}
},
"node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "peer": true,
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
@@ -16840,9 +17163,13 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
diff --git a/package.json b/package.json
index 49b3308..be03213 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,21 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^29.5.5",
+ "@types/node": "^20.7.1",
+ "@types/react": "^18.2.23",
+ "@types/react-dom": "^18.2.8",
+ "datejs": "^1.0.0-rc3",
+ "dayjs": "^1.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.7",
+ "styled-components": "^6.0.8",
+ "styled-reset": "^4.5.1",
+ "typescript": "^5.2.2",
+ "uuid": "^9.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/public/_redirects b/public/_redirects
new file mode 100644
index 0000000..7797f7c
--- /dev/null
+++ b/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
diff --git a/public/index.html b/public/index.html
index aa069f2..c166716 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,7 +9,7 @@
name="description"
content="Web site created using create-react-app"
/>
-
+
-
React App
+
+ Chatting UI
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a..0000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a6..0000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 3784575..0000000
--- a/src/App.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
- return (
-
- );
-}
-
-export default App;
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 1f03afe..0000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..104f322
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,37 @@
+import { BrowserRouter } from "react-router-dom";
+import Router from "./Router";
+import { GlobalStyles } from "./style/GloblalStyles";
+import { ThemeProvider } from "styled-components";
+import theme from "./style/theme";
+import { styled } from "styled-components";
+import { RecoilRoot } from "recoil";
+import React from "react";
+
+function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+// 고정형
+const AppContainer = styled.div`
+ width: 375px;
+ height: 812px;
+ margin: 0 auto;
+ border-radius: 24px;
+ background-color: ${theme.colors.white};
+ overflow: hidden;
+ position: relative;
+`;
+
+export default App;
diff --git a/src/Router.tsx b/src/Router.tsx
new file mode 100644
index 0000000..c420a16
--- /dev/null
+++ b/src/Router.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { Route, Routes } from "react-router-dom";
+import ChatRoom from "./pages/ChatRoom";
+import Contact from "./pages/Contact";
+import Profile from "./pages/Profile";
+import Chat from "./pages/Chat";
+import { Navigate } from "react-router-dom";
+// 라우팅은 여기에
+function Router() {
+ return (
+ <>
+
+ }>
+ }>
+ }>
+ }>
+ {/* 다른 경로 접속시 chat으로 이동*/}
+ } />
+
+ >
+ );
+}
+
+export default Router;
diff --git "a/src/assets/\bdummyList.tsx" "b/src/assets/\bdummyList.tsx"
new file mode 100644
index 0000000..69b7507
--- /dev/null
+++ "b/src/assets/\bdummyList.tsx"
@@ -0,0 +1,25 @@
+// 채팅방, 연락처에 대한 더미리스트 (기능 구현상으로 채팅방 추가, 연락처 추가 불가능)
+export const dummyContactList = [
+ {
+ name: "이현진",
+ introduction: "좋은 하루!",
+ },
+ {
+ name: "정인영",
+ introduction: "CEOS 프론트엔드",
+ },
+ {
+ name: "김종완",
+ introduction: "CEOS 백엔드",
+ },
+ {
+ name: "나서강",
+ introduction: "서강대학교 학생",
+ },
+ {
+ name: "나오스",
+ introduction: "CEOS 디자인",
+ },
+];
+
+export const dummyChatList = ["이현진", "김종완"];
diff --git a/src/assets/fonts/Lato-Light.woff b/src/assets/fonts/Lato-Light.woff
new file mode 100644
index 0000000..83203f6
Binary files /dev/null and b/src/assets/fonts/Lato-Light.woff differ
diff --git a/src/assets/fonts/Lato-Medium.woff b/src/assets/fonts/Lato-Medium.woff
new file mode 100644
index 0000000..cfa53c4
Binary files /dev/null and b/src/assets/fonts/Lato-Medium.woff differ
diff --git a/src/assets/fonts/Lato-Regular.woff b/src/assets/fonts/Lato-Regular.woff
new file mode 100644
index 0000000..89722fb
Binary files /dev/null and b/src/assets/fonts/Lato-Regular.woff differ
diff --git a/src/assets/fonts/Lato-Thin.ttf b/src/assets/fonts/Lato-Thin.ttf
new file mode 100644
index 0000000..0f84dc1
Binary files /dev/null and b/src/assets/fonts/Lato-Thin.ttf differ
diff --git a/src/assets/fonts/Lato-Thin.woff b/src/assets/fonts/Lato-Thin.woff
new file mode 100644
index 0000000..1b5870a
Binary files /dev/null and b/src/assets/fonts/Lato-Thin.woff differ
diff --git a/src/assets/fonts/Pretendard-Black.woff b/src/assets/fonts/Pretendard-Black.woff
new file mode 100644
index 0000000..83c411c
Binary files /dev/null and b/src/assets/fonts/Pretendard-Black.woff differ
diff --git a/src/assets/fonts/Pretendard-Bold.woff b/src/assets/fonts/Pretendard-Bold.woff
new file mode 100644
index 0000000..53470ba
Binary files /dev/null and b/src/assets/fonts/Pretendard-Bold.woff differ
diff --git a/src/assets/fonts/Pretendard-ExtraBold.woff b/src/assets/fonts/Pretendard-ExtraBold.woff
new file mode 100644
index 0000000..6b78d50
Binary files /dev/null and b/src/assets/fonts/Pretendard-ExtraBold.woff differ
diff --git a/src/assets/fonts/Pretendard-ExtraLight.woff b/src/assets/fonts/Pretendard-ExtraLight.woff
new file mode 100644
index 0000000..b0ada01
Binary files /dev/null and b/src/assets/fonts/Pretendard-ExtraLight.woff differ
diff --git a/src/assets/fonts/Pretendard-Light.woff b/src/assets/fonts/Pretendard-Light.woff
new file mode 100644
index 0000000..bc0ad69
Binary files /dev/null and b/src/assets/fonts/Pretendard-Light.woff differ
diff --git a/src/assets/fonts/Pretendard-Medium.woff b/src/assets/fonts/Pretendard-Medium.woff
new file mode 100644
index 0000000..92ca0c3
Binary files /dev/null and b/src/assets/fonts/Pretendard-Medium.woff differ
diff --git a/src/assets/fonts/Pretendard-Regular.woff b/src/assets/fonts/Pretendard-Regular.woff
new file mode 100644
index 0000000..d560808
Binary files /dev/null and b/src/assets/fonts/Pretendard-Regular.woff differ
diff --git a/src/assets/fonts/Pretendard-SemiBold.woff b/src/assets/fonts/Pretendard-SemiBold.woff
new file mode 100644
index 0000000..c6bd2ff
Binary files /dev/null and b/src/assets/fonts/Pretendard-SemiBold.woff differ
diff --git a/src/assets/fonts/Pretendard-Thin.woff b/src/assets/fonts/Pretendard-Thin.woff
new file mode 100644
index 0000000..a92526d
Binary files /dev/null and b/src/assets/fonts/Pretendard-Thin.woff differ
diff --git a/src/assets/fonts/Segoe-Bold.woff b/src/assets/fonts/Segoe-Bold.woff
new file mode 100644
index 0000000..a6b0a97
Binary files /dev/null and b/src/assets/fonts/Segoe-Bold.woff differ
diff --git a/src/assets/fonts/Segoe.woff b/src/assets/fonts/Segoe.woff
new file mode 100644
index 0000000..a45efa3
Binary files /dev/null and b/src/assets/fonts/Segoe.woff differ
diff --git a/src/assets/fonts/SegoePro-Semibold.woff b/src/assets/fonts/SegoePro-Semibold.woff
new file mode 100644
index 0000000..958e4c1
Binary files /dev/null and b/src/assets/fonts/SegoePro-Semibold.woff differ
diff --git a/src/assets/images/backIcon.svg b/src/assets/images/backIcon.svg
new file mode 100644
index 0000000..cc16d47
--- /dev/null
+++ b/src/assets/images/backIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/bellIcon.svg b/src/assets/images/bellIcon.svg
new file mode 100644
index 0000000..84f8465
--- /dev/null
+++ b/src/assets/images/bellIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/chatAddIcon.svg b/src/assets/images/chatAddIcon.svg
new file mode 100644
index 0000000..c5dc4e2
--- /dev/null
+++ b/src/assets/images/chatAddIcon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/images/goIcon.svg b/src/assets/images/goIcon.svg
new file mode 100644
index 0000000..15014cd
--- /dev/null
+++ b/src/assets/images/goIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/groupIcon.svg b/src/assets/images/groupIcon.svg
new file mode 100644
index 0000000..9eeb900
--- /dev/null
+++ b/src/assets/images/groupIcon.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/groupIconBlue.svg b/src/assets/images/groupIconBlue.svg
new file mode 100644
index 0000000..319e981
--- /dev/null
+++ b/src/assets/images/groupIconBlue.svg
@@ -0,0 +1,40 @@
+
diff --git a/src/assets/images/helpIcon.svg b/src/assets/images/helpIcon.svg
new file mode 100644
index 0000000..eba7613
--- /dev/null
+++ b/src/assets/images/helpIcon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/horizontalIcon.svg b/src/assets/images/horizontalIcon.svg
new file mode 100644
index 0000000..34f9df2
--- /dev/null
+++ b/src/assets/images/horizontalIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/horizontalIconBlue.svg b/src/assets/images/horizontalIconBlue.svg
new file mode 100644
index 0000000..f4faf41
--- /dev/null
+++ b/src/assets/images/horizontalIconBlue.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/instagramIcon.svg b/src/assets/images/instagramIcon.svg
new file mode 100644
index 0000000..a95068b
--- /dev/null
+++ b/src/assets/images/instagramIcon.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/mediaAddIcon.svg b/src/assets/images/mediaAddIcon.svg
new file mode 100644
index 0000000..83b6165
--- /dev/null
+++ b/src/assets/images/mediaAddIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/messageIcon.svg b/src/assets/images/messageIcon.svg
new file mode 100644
index 0000000..fac9936
--- /dev/null
+++ b/src/assets/images/messageIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/messageIconBlue.svg b/src/assets/images/messageIconBlue.svg
new file mode 100644
index 0000000..2f084ac
--- /dev/null
+++ b/src/assets/images/messageIconBlue.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/mushroom 2.png b/src/assets/images/mushroom 2.png
new file mode 100644
index 0000000..7817fb4
Binary files /dev/null and b/src/assets/images/mushroom 2.png differ
diff --git a/src/assets/images/naverIcon.svg b/src/assets/images/naverIcon.svg
new file mode 100644
index 0000000..6db6e50
--- /dev/null
+++ b/src/assets/images/naverIcon.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/personIcon.svg b/src/assets/images/personIcon.svg
new file mode 100644
index 0000000..f05a7ed
--- /dev/null
+++ b/src/assets/images/personIcon.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/rightIcon.svg b/src/assets/images/rightIcon.svg
new file mode 100644
index 0000000..1e5b679
--- /dev/null
+++ b/src/assets/images/rightIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/searchGrayIcon.svg b/src/assets/images/searchGrayIcon.svg
new file mode 100644
index 0000000..26734f9
--- /dev/null
+++ b/src/assets/images/searchGrayIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/securityIcon.svg b/src/assets/images/securityIcon.svg
new file mode 100644
index 0000000..5eab8dd
--- /dev/null
+++ b/src/assets/images/securityIcon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/serachIcon.svg b/src/assets/images/serachIcon.svg
new file mode 100644
index 0000000..7e3fbf4
--- /dev/null
+++ b/src/assets/images/serachIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/images/statusIcon.svg b/src/assets/images/statusIcon.svg
new file mode 100644
index 0000000..969d9b3
--- /dev/null
+++ b/src/assets/images/statusIcon.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/images/voiceAddIcon.svg b/src/assets/images/voiceAddIcon.svg
new file mode 100644
index 0000000..bd5828d
--- /dev/null
+++ b/src/assets/images/voiceAddIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/atom/Flex.tsx b/src/components/atom/Flex.tsx
new file mode 100644
index 0000000..d81d795
--- /dev/null
+++ b/src/components/atom/Flex.tsx
@@ -0,0 +1,66 @@
+import React, { ReactNode, RefObject } from "react";
+import styled from "styled-components";
+
+interface FlexProps {
+ children?: ReactNode;
+ width?: string;
+ height?: string;
+ direction?: string;
+ justify?: string;
+ align?: string;
+ gap?: string;
+ wrap?: string;
+ color?: string;
+ overflow?: string;
+ padding?: string;
+ maxwidth?: string;
+ radius?: string;
+ bordercolor?: string;
+ margin?: string;
+ self?: string;
+ shadow?: string;
+ grow?: string;
+ position?: string;
+ bottom?: string;
+ onClick?: any;
+ cursor?: string;
+ inputRef?: RefObject;
+}
+
+const FlexBase = styled.div`
+ display: flex;
+ height: ${({ height }) => height};
+ width: ${({ width }) => width};
+ flex-direction: ${({ direction }) => direction};
+ justify-content: ${({ justify }) => justify};
+ align-items: ${({ align }) => align};
+ align-self: ${({ self }) => self};
+ gap: ${({ gap }) => `${gap}px`};
+ flex-wrap: ${({ wrap }) => wrap};
+ overflow: ${({ overflow }) => overflow};
+ background-color: ${({ color, theme }) =>
+ color ? theme.colors[color] : "inherit"};
+ border-radius: ${({ radius }) => radius};
+ padding: ${({ padding }) => padding};
+ max-width: ${({ maxwidth }) => maxwidth};
+ border-bottom: ${({ bordercolor, theme }) =>
+ bordercolor ? "1px solid " + theme.colors[bordercolor] : "none"};
+ margin: ${({ margin }) => margin};
+ box-shadow: ${({ shadow }) => shadow};
+ flex-grow: ${({ grow }) => grow};
+ position: ${({ position }) => position};
+ bottom: ${({ bottom }) => bottom};
+ cursor: ${({ cursor }) => cursor};
+`;
+
+export const Flex: React.FC = ({
+ children,
+ inputRef,
+ ...rest
+}: FlexProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/atom/Icon.tsx b/src/components/atom/Icon.tsx
new file mode 100644
index 0000000..cb79c76
--- /dev/null
+++ b/src/components/atom/Icon.tsx
@@ -0,0 +1,7 @@
+import { styled } from "styled-components";
+
+// 아이콘은 24 x 24 그리드
+export const Icon = styled.img`
+ width: 24px;
+ height: 24px;
+`;
diff --git a/src/components/atom/Input.tsx b/src/components/atom/Input.tsx
new file mode 100644
index 0000000..45039b8
--- /dev/null
+++ b/src/components/atom/Input.tsx
@@ -0,0 +1,70 @@
+import React, { ChangeEvent, KeyboardEvent, RefObject } from "react";
+import styled from "styled-components";
+
+interface InputProps {
+ placeholder?: string;
+ width?: string;
+ height?: string;
+ bgcolor?: string;
+ color?: string;
+ weight?: string;
+ lineheight?: string;
+ fontSize?: string;
+ padding?: string;
+ onChange?: (e: ChangeEvent) => void;
+ value?: string;
+ onKeyDown?: (e: KeyboardEvent) => void;
+ inputRef?: RefObject;
+}
+
+const InputBase = styled.input`
+ width: ${({ width }) => width};
+ height: ${({ height }) => height};
+ padding: 6px 8px;
+ background-color: ${({ bgcolor, theme }) =>
+ bgcolor ? theme.colors[bgcolor] : "inherit"};
+ color: ${({ theme }) => theme.colors["mainBlack"]};
+ line-height: ${({ lineheight }) => lineheight};
+ font-size: ${({ fontSize }) => fontSize};
+ font-weight: ${({ weight }) => weight};
+ padding: ${({ padding }) => padding};
+ &::placeholder {
+ color: ${({ theme }) => theme.colors["gray"]};
+ font-weight: 400;
+ }
+`;
+
+export const Input: React.FC = ({
+ placeholder = "메시지를 입력해주세요",
+ width,
+ height,
+ bgcolor,
+ color,
+ weight = "600",
+ lineheight = "24px",
+ fontSize = "14px",
+ onChange,
+ value,
+ onKeyDown,
+ inputRef,
+ padding,
+}: InputProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/atom/Space.tsx b/src/components/atom/Space.tsx
new file mode 100644
index 0000000..5f74c0d
--- /dev/null
+++ b/src/components/atom/Space.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import styled from "styled-components";
+// 스타일링할때 margin 대신 Space 사용
+interface SpaceProps {
+ width?: string;
+ height?: string;
+}
+
+const SpaceBase =
+ styled.div <
+ SpaceProps >
+ `
+ width: ${({ width }) => width};
+ height: ${({ height }) => height};
+`;
+
+export const Space: React.FC = ({
+ width = "auto",
+ height = "auto",
+}) => {
+ return ;
+};
diff --git a/src/components/atom/Text.tsx b/src/components/atom/Text.tsx
new file mode 100644
index 0000000..765e31b
--- /dev/null
+++ b/src/components/atom/Text.tsx
@@ -0,0 +1,66 @@
+import React, { ChangeEvent } from "react";
+import styled from "styled-components";
+
+interface TextProps {
+ size?: string;
+ weight?: string;
+ color?: string;
+ children: React.ReactNode;
+ spacing?: string;
+ font?: string;
+ cursor?: string;
+ lineheight?: string;
+ align?: string;
+ width?: string;
+ self?: string;
+ padding?: string;
+ onClick?: (e: ChangeEvent) => void;
+}
+
+const StyledText = styled.div`
+ word-break: break-word;
+ text-align: ${({ align }) => align};
+ font-size: ${({ size }) => size};
+ font-weight: ${({ weight }) => weight};
+ color: ${({ color, theme }) => (color ? theme.colors[color] : color)};
+ width: ${({ width }) => width};
+ letter-spacing: ${({ spacing }) => spacing};
+ line-height: ${({ lineheight }) => lineheight};
+ font-family: ${({ font }) => font};
+ cursor: ${({ cursor }) => cursor};
+ align-self: ${({ self }) => self};
+ padding: ${({ padding }) => padding};
+`;
+
+export const Text: React.FC = ({
+ size = "inherit",
+ weight = "inherit",
+ color = "inherit",
+ children,
+ spacing = "-0px",
+ font = "Pretendard",
+ cursor = "inherit",
+ lineheight = "120%",
+ align = "center",
+ width = "fit-content",
+ self = "auto",
+ ...rest
+}: TextProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/moleclues/BottomLine.tsx b/src/components/moleclues/BottomLine.tsx
new file mode 100644
index 0000000..23bddae
--- /dev/null
+++ b/src/components/moleclues/BottomLine.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { Flex } from "../atom/Flex";
+
+function BottomLine() {
+ return (
+
+
+
+ );
+}
+
+export default BottomLine;
diff --git a/src/components/moleclues/BottomNavigation.tsx b/src/components/moleclues/BottomNavigation.tsx
new file mode 100644
index 0000000..6a5a6ed
--- /dev/null
+++ b/src/components/moleclues/BottomNavigation.tsx
@@ -0,0 +1,69 @@
+import React, { useEffect, useState } from "react";
+import { Flex } from "../atom/Flex";
+import { Icon } from "../atom/Icon";
+import groupIcon from "../../assets/images/groupIcon.svg";
+import groupIconBlue from "../../assets/images/groupIconBlue.svg";
+import messageIcon from "../../assets/images/messageIcon.svg";
+import messageIconBlue from "../../assets/images/messageIconBlue.svg";
+import horizontalIcon from "../../assets/images/horizontalIcon.svg";
+import horizontalIconBlue from "../../assets/images/horizontalIconBlue.svg";
+import { Link, useLocation } from "react-router-dom";
+import { useSetRecoilState } from "recoil";
+import { isSearchState } from "../../recoil/atom";
+
+function BottomNavigation() {
+ const [isBlue, setIsBlue] = useState({
+ contact: false,
+ chat: false,
+ profile: false,
+ });
+ const setIsSearch = useSetRecoilState(isSearchState);
+ const location = useLocation();
+
+ useEffect(() => {
+ setIsSearch(false);
+ const path = location.pathname;
+ setIsBlue((prevState) => ({
+ ...prevState,
+ contact: path === "/contact",
+ chat: path === "/chat",
+ profile: path === "/profile",
+ }));
+ }, [location]);
+
+ return (
+
+
+
+
+ {isBlue.contact ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isBlue.chat ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isBlue.profile ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+
+export default BottomNavigation;
diff --git a/src/components/moleclues/HomeNav.tsx b/src/components/moleclues/HomeNav.tsx
new file mode 100644
index 0000000..6bf8f4b
--- /dev/null
+++ b/src/components/moleclues/HomeNav.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Flex } from "../atom/Flex";
+import { Text } from "../atom/Text";
+import { Icon } from "../atom/Icon";
+import chatAddIcon from "../../assets/images/chatAddIcon.svg";
+import searchIcon from "../../assets/images/serachIcon.svg";
+import searchGrayIcon from "../../assets/images/searchGrayIcon.svg";
+import { Input } from "../atom/Input";
+import { useRecoilState } from "recoil";
+import { userInputState, isSearchState } from "../../recoil/atom";
+
+function HomeNav({ title }) {
+ const [isSearch, setIsSearch] = useRecoilState(isSearchState);
+ const [userInput, setUserInput] = useRecoilState(userInputState);
+ const inputRef = useRef(null);
+ const toggleState = () => {
+ setIsSearch(!isSearch);
+ setUserInput("");
+ };
+ const onTextChange = (e) => {
+ setUserInput(e.target.value);
+ };
+ useEffect(() => {
+ inputRef?.current?.focus();
+ }, [isSearch]);
+
+ return (
+ <>
+
+ {isSearch ? (
+
+
+
+
+ 취소
+
+
+ ) : (
+ <>
+ {" "}
+
+
+ {title}
+
+
+
+ {" "}
+ >
+ )}
+
+ >
+ );
+}
+
+export default HomeNav;
diff --git a/src/components/moleclues/SearchContainer.tsx b/src/components/moleclues/SearchContainer.tsx
new file mode 100644
index 0000000..2715f6c
--- /dev/null
+++ b/src/components/moleclues/SearchContainer.tsx
@@ -0,0 +1,87 @@
+import React, { useEffect } from "react";
+import { useRecoilValue } from "recoil";
+import {
+ userInputState,
+ isSearchState,
+ lastMessage1State,
+ lastMessage2State,
+ unReadCount1State,
+ unReadCount2State,
+} from "../../recoil/atom";
+import { dummyChatList, dummyContactList } from "../../assets/dummyList";
+import { Flex } from "../atom/Flex";
+import { Text } from "../atom/Text";
+import { Space } from "../atom/Space";
+import ChatItem from "./chat/ChatItem";
+function SearchContainer() {
+ const userInput = useRecoilValue(userInputState);
+ const filterChatList = dummyChatList.filter((item) =>
+ item.includes(userInput)
+ );
+ const filterContactList = dummyContactList.filter((item) =>
+ item.name.includes(userInput)
+ );
+ const lastMessageRoom1 = useRecoilValue(lastMessage1State);
+ const lastMessageRoom2 = useRecoilValue(lastMessage2State);
+ const unReadCountRoom1 = useRecoilValue(unReadCount1State);
+ const unReadCountRoom2 = useRecoilValue(unReadCount2State);
+ return (
+ <>
+
+
+
+ {filterContactList.length === 0 ? (
+ 결과가 없습니다.
+ ) : (
+ <>
+ {" "}
+
+ 친구
+
+
+ {filterContactList.map((item) => (
+
+
+
+ {item.name[0]}
+
+
+ {item.name}
+
+ ))}
+
+ >
+ )}
+
+ {filterChatList.length === 0 ? (
+ ""
+ ) : (
+
+ 채팅방
+ {filterChatList.map((item) => (
+
+ ))}
+
+ )}
+
+ >
+ );
+}
+
+export default SearchContainer;
diff --git a/src/components/moleclues/StatusBar.tsx b/src/components/moleclues/StatusBar.tsx
new file mode 100644
index 0000000..3e8fae5
--- /dev/null
+++ b/src/components/moleclues/StatusBar.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { Flex } from "../atom/Flex";
+import { Space } from "../atom/Space";
+import { Text } from "../atom/Text";
+import statusIcon from "../../assets/images/statusIcon.svg";
+import { getTime } from "../../hooks/getTime";
+
+function StatusBar() {
+ return (
+ <>
+
+
+
+
+ {getTime("HH:mm")}
+
+
+
+
+
+ >
+ );
+}
+
+export default StatusBar;
diff --git a/src/components/moleclues/chat/ChatItem.tsx b/src/components/moleclues/chat/ChatItem.tsx
new file mode 100644
index 0000000..3e51998
--- /dev/null
+++ b/src/components/moleclues/chat/ChatItem.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { Flex } from "../../atom/Flex";
+import { Text } from "../../atom/Text";
+import { useNavigate } from "react-router-dom";
+
+interface ChatItemProps {
+ id: number;
+ name: string;
+ lastMessage: string;
+ count: number;
+}
+
+function ChatItem({ id, name, lastMessage, count }: ChatItemProps) {
+ const navigate = useNavigate();
+ const onClick = () => {
+ navigate(`/chat/${id}`);
+ };
+ return (
+ <>
+
+
+
+ {name.substr(0, 1)}
+
+
+
+
+
+ {name}
+
+
+ 오늘
+
+
+
+
+ {lastMessage}
+
+ {count == 0 ? (
+ ""
+ ) : (
+
+
+ {count}
+
+
+ )}
+
+
+
+ >
+ );
+}
+
+export default ChatItem;
diff --git a/src/components/moleclues/chatroom/ChatBubbleBlue.tsx b/src/components/moleclues/chatroom/ChatBubbleBlue.tsx
new file mode 100644
index 0000000..816401a
--- /dev/null
+++ b/src/components/moleclues/chatroom/ChatBubbleBlue.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { Text } from "../../atom/Text";
+import { Flex } from "../../atom/Flex";
+
+interface ChatBubbleBlueProps {
+ text: string;
+ time: string;
+ isRead: boolean;
+}
+
+function ChatBubbleBlue({ text, time, isRead }: ChatBubbleBlueProps) {
+ return (
+
+
+ {text}
+
+
+ {time} {isRead ? "· 읽음" : ""}
+
+
+ );
+}
+
+export default ChatBubbleBlue;
diff --git a/src/components/moleclues/chatroom/ChatBubbleWhite.tsx b/src/components/moleclues/chatroom/ChatBubbleWhite.tsx
new file mode 100644
index 0000000..561aeae
--- /dev/null
+++ b/src/components/moleclues/chatroom/ChatBubbleWhite.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { Text } from "../../atom/Text";
+import { Flex } from "../../atom/Flex";
+
+interface ChatBubbleWhiteProps {
+ text: string;
+ time: string;
+}
+
+function ChatBubbleWhite({ text, time }: ChatBubbleWhiteProps) {
+ return (
+
+
+
+ {text}
+
+
+
+ {time}
+
+
+ );
+}
+
+export default ChatBubbleWhite;
diff --git a/src/components/moleclues/chatroom/ChatInput.tsx b/src/components/moleclues/chatroom/ChatInput.tsx
new file mode 100644
index 0000000..73dfaaf
--- /dev/null
+++ b/src/components/moleclues/chatroom/ChatInput.tsx
@@ -0,0 +1,87 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Flex } from "../../atom/Flex";
+import { Icon } from "../../atom/Icon";
+import { Input } from "../../atom/Input";
+import mediaAddIcon from "../../../assets/images/mediaAddIcon.svg";
+import voiceAddIcon from "../../../assets/images/voiceAddIcon.svg";
+import { useRecoilState, useRecoilValue } from "recoil";
+import {
+ firstRoomState,
+ secondRoomState,
+ userAMessageState,
+ userBMesasgeState,
+ userCMessageState,
+ userDMesasgeState,
+} from "../../../recoil/atom";
+
+import { handleKeyDown } from "../../../hooks/handleKeyDown";
+import { useParams } from "react-router-dom";
+
+function ChatInput() {
+ const params = useParams();
+ const [inputMessage, setInputMessage] = useState("");
+ const isUser1InFirstRoom = useRecoilValue(firstRoomState);
+ const isUser1InSecondRoom = useRecoilValue(secondRoomState);
+ const [userAMessage, setUserAMessage] = useRecoilState(userAMessageState);
+ const [userBMessage, setUserBMessage] = useRecoilState(userBMesasgeState);
+ const [userCMessage, setUserCMessage] = useRecoilState(userCMessageState);
+ const [userDMessage, setUserDMessage] = useRecoilState(userDMesasgeState);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setInputMessage(e.target.value);
+ };
+
+ const onKeyDown = (event: React.KeyboardEvent) => {
+ if (params.roomID === "1") {
+ handleKeyDown(
+ event,
+ inputMessage,
+ isUser1InFirstRoom,
+ setUserAMessage,
+ userAMessage,
+ setUserBMessage,
+ userBMessage,
+ setInputMessage
+ );
+ } else if (params.roomID === "2") {
+ handleKeyDown(
+ event,
+ inputMessage,
+ isUser1InSecondRoom,
+ setUserCMessage,
+ userCMessage,
+ setUserDMessage,
+ userDMessage,
+ setInputMessage
+ );
+ }
+ };
+
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default ChatInput;
diff --git a/src/components/moleclues/chatroom/ChatNav.tsx b/src/components/moleclues/chatroom/ChatNav.tsx
new file mode 100644
index 0000000..59dca53
--- /dev/null
+++ b/src/components/moleclues/chatroom/ChatNav.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from "react";
+import { Flex } from "../../atom/Flex";
+import { Icon } from "../../atom/Icon";
+import { Text } from "../../atom/Text";
+import chatAddIcon from "../../../assets/images/chatAddIcon.svg";
+import searchIcon from "../../../assets/images/serachIcon.svg";
+import backIcon from "../../../assets/images/backIcon.svg";
+import { useRecoilState } from "recoil";
+import { useNavigate, useParams } from "react-router-dom";
+import {
+ firstRoomState,
+ secondRoomState,
+ userAMessageState,
+ userBMesasgeState,
+ userCMessageState,
+ userDMesasgeState,
+} from "../../../recoil/atom";
+import { ChatMessages } from "../../organism/chatroom/ChatArea";
+function ChatNav() {
+ const [isUser1InFirstRoom, setIsUser1InFirstRoom] =
+ useRecoilState(firstRoomState);
+ const [isUser1InSecondRoom, setIsUser1InSecondRoom] =
+ useRecoilState(secondRoomState);
+ const [userAMessage, setUserAMessage] = useRecoilState(userAMessageState);
+ const [userBMessage, setUserBMessage] = useRecoilState(userBMesasgeState);
+ const [userCMessage, setUserCMessage] = useRecoilState(userCMessageState);
+ const [userDMessage, setUserDMessage] = useRecoilState(userDMesasgeState);
+ const params = useParams();
+
+ // 현재는 채팅방이 2개 밖에 없기 때문에 삼항 연산자를 통해 채팅방 내비게이션의 모습을 결정함
+ const partnerName = params.roomID === "1" ? "이현진" : "김종완";
+ const toggleUser = () => {
+ // 유저 전환
+ params.roomID === "1"
+ ? setIsUser1InFirstRoom(!isUser1InFirstRoom)
+ : setIsUser1InSecondRoom(!isUser1InSecondRoom);
+ // 메시지읽음 표시
+ if (params.roomID === "1") {
+ setUserAMessage(userAMessage.map((obj) => ({ ...obj, isRead: true })));
+ setUserBMessage(userBMessage.map((obj) => ({ ...obj, isRead: true })));
+ } else if (params.roomID === "2") {
+ setUserCMessage(userCMessage.map((obj) => ({ ...obj, isRead: true })));
+ setUserDMessage(userDMessage.map((obj) => ({ ...obj, isRead: true })));
+ }
+ };
+ const navigate = useNavigate();
+ const goChat = () => {
+ navigate("/chat");
+ };
+ return (
+
+
+
+
+ {params.roomID === "1"
+ ? isUser1InFirstRoom
+ ? `${partnerName}`
+ : "정인영"
+ : isUser1InSecondRoom
+ ? `${partnerName}`
+ : "정인영"}
+
+
+
+
+
+ );
+}
+
+export default ChatNav;
diff --git a/src/components/moleclues/contact/ContactItem.tsx b/src/components/moleclues/contact/ContactItem.tsx
new file mode 100644
index 0000000..cb5d3cf
--- /dev/null
+++ b/src/components/moleclues/contact/ContactItem.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { Flex } from "../../atom/Flex";
+import { Text } from "../../atom/Text";
+function ContactItem({ name, introduction }) {
+ return (
+
+
+
+ {name[0]}
+
+
+
+
+ {name}
+
+
+ {introduction}
+
+
+
+ );
+}
+
+export default ContactItem;
diff --git a/src/components/moleclues/profile/DivideLine.tsx b/src/components/moleclues/profile/DivideLine.tsx
new file mode 100644
index 0000000..58eec5e
--- /dev/null
+++ b/src/components/moleclues/profile/DivideLine.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { Flex } from "../../atom/Flex";
+function DivideLine() {
+ return (
+
+
+
+ );
+}
+
+export default DivideLine;
diff --git a/src/components/moleclues/profile/MyProfileLink.tsx b/src/components/moleclues/profile/MyProfileLink.tsx
new file mode 100644
index 0000000..1b32501
--- /dev/null
+++ b/src/components/moleclues/profile/MyProfileLink.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Space } from "../../atom/Space";
+import { Flex } from "../../atom/Flex";
+import { Icon } from "../../atom/Icon";
+import { Text } from "../../atom/Text";
+
+interface MyProfileLinkProps {
+ icon: string;
+ name: string;
+}
+
+function MyProfileLink({ icon, name }: MyProfileLinkProps) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+ >
+ );
+}
+
+export default MyProfileLink;
diff --git a/src/components/moleclues/profile/MyProfileMain.tsx b/src/components/moleclues/profile/MyProfileMain.tsx
new file mode 100644
index 0000000..836a630
--- /dev/null
+++ b/src/components/moleclues/profile/MyProfileMain.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Flex } from "../../atom/Flex";
+import { Icon } from "../../atom/Icon";
+import { Text } from "../../atom/Text";
+import { Space } from "../../atom/Space";
+import personIcon from "../../../assets/images/personIcon.svg";
+import rightIcon from "../../../assets/images/rightIcon.svg";
+function ProfileItem() {
+ return (
+ <>
+
+
+
+
+
+
+
+ 정인영
+
+
+
+ 010-8324-0112
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default ProfileItem;
diff --git a/src/components/moleclues/profile/MyProfileMenu.tsx b/src/components/moleclues/profile/MyProfileMenu.tsx
new file mode 100644
index 0000000..2bf2050
--- /dev/null
+++ b/src/components/moleclues/profile/MyProfileMenu.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { Space } from "../../atom/Space";
+import { Flex } from "../../atom/Flex";
+import { Icon } from "../../atom/Icon";
+import { Text } from "../../atom/Text";
+import personIcon from "../../../assets/images/personIcon.svg";
+import rightIcon from "../../../assets/images/rightIcon.svg";
+
+interface MyProfileMenuProps {
+ icon: string;
+ name: string;
+}
+
+function MyProfileMenu({ icon, name }: MyProfileMenuProps) {
+ return (
+ <>
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+
+ >
+ );
+}
+
+export default MyProfileMenu;
diff --git a/src/components/organism/HomeFooter.tsx b/src/components/organism/HomeFooter.tsx
new file mode 100644
index 0000000..22c372a
--- /dev/null
+++ b/src/components/organism/HomeFooter.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import BottomLine from "../moleclues/BottomLine";
+import BottomNavigation from "../moleclues/BottomNavigation";
+import { Flex } from "../atom/Flex";
+import { Space } from "../atom/Space";
+
+function HomeFooter() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default HomeFooter;
diff --git a/src/components/organism/HomeHeader.tsx b/src/components/organism/HomeHeader.tsx
new file mode 100644
index 0000000..e20bdac
--- /dev/null
+++ b/src/components/organism/HomeHeader.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import StatusBar from "../moleclues/StatusBar";
+import HomeNav from "../moleclues/HomeNav";
+import { Flex } from "../atom/Flex";
+
+interface HomeHeaderProps {
+ title: string;
+ isBorder: boolean;
+}
+
+function HomeHeader({ title, isBorder }: HomeHeaderProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default HomeHeader;
diff --git a/src/components/organism/chat/ChatList.tsx b/src/components/organism/chat/ChatList.tsx
new file mode 100644
index 0000000..6e92011
--- /dev/null
+++ b/src/components/organism/chat/ChatList.tsx
@@ -0,0 +1,103 @@
+import React, { useEffect } from "react";
+import ChatItem from "../../moleclues/chat/ChatItem";
+import { Flex } from "../../atom/Flex";
+import { Text } from "../../atom/Text";
+import {
+ firstRoomState,
+ lastMessage1State,
+ lastMessage2State,
+ secondRoomState,
+ unReadCount1State,
+ unReadCount2State,
+ userAMessageState,
+ userBMesasgeState,
+ userCMessageState,
+ userDMesasgeState,
+} from "../../../recoil/atom";
+import { sortMessagesByTime } from "../../../hooks/sortMessageByTime";
+import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
+import { userInputState, isSearchState } from "../../../recoil/atom";
+import { dummyChatList, dummyContactList } from "../../../assets/\bdummyList";
+import { Space } from "../../atom/Space";
+import SearchContainer from "../../moleclues/SearchContainer";
+import { ChatMessages } from "../chatroom/ChatArea";
+
+function ChatList() {
+ // userA~userD 채팅방 1 , 채팅방 2의 주체들
+ const userAMessage: ChatMessages = useRecoilValue(userAMessageState);
+ const userBMessage: ChatMessages = useRecoilValue(userBMesasgeState);
+ const userCMessage : ChatMessages= useRecoilValue(userCMessageState);
+ const userDMessage: ChatMessages = useRecoilValue(userDMesasgeState);
+ // 각 채팅방마다 마지막 메시지 세팅
+ const setLastMessage1 = useSetRecoilState(lastMessage1State);
+ const setLastMessage2 = useSetRecoilState(lastMessage2State);
+ // 각 채팅방마다 읽지 않은 메시지 개수 갱신
+ const setUnReadCountRoom1 = useSetRecoilState(unReadCount1State);
+ const setUnReadCountRoom2 = useSetRecoilState(unReadCount2State);
+ // 채팅방의 주체가 자신 (정인영) 인지 여부
+ const isUser1InFirstRoom : boolean= useRecoilValue(firstRoomState);
+ const isUser1InSecondRoom : boolean= useRecoilValue(secondRoomState);
+ // 각각의 방마다 시간대별로 정렬된 메시지
+ const room1SortedMessages : ChatMessages = sortMessagesByTime([
+ ...userAMessage,
+ ...userBMessage,
+ ]);
+ const room2SortedMessages : ChatMessages= sortMessagesByTime([
+ ...userCMessage,
+ ...userDMessage,
+ ]);
+ // 각각의 방마다의 마지막 메시지
+ const lastMessageRoom1 =
+ room1SortedMessages[room1SortedMessages.length - 1]?.text;
+
+ const lastMessageRoom2 =
+ room2SortedMessages[room2SortedMessages.length - 1]?.text;
+
+ // 채팅방의 주체 (정인영) 입장에서 각각의 채팅방에서 읽지 않은 메시지의 개수 count1, count2
+ let count1 = 0;
+ let count2 = 0;
+ // 마지막 채팅방에서 자신의 메시지가 마지막일 경우에는 읽지 않은 카운트에서 제외
+ if (!isUser1InFirstRoom) {
+ room1SortedMessages.forEach((obj) => {
+ if (!obj.isRead) count1 += 1;
+ });
+ }
+ if (!isUser1InSecondRoom) {
+ room2SortedMessages.forEach((obj) => {
+ if (!obj.isRead) count2 += 1;
+ });
+ }
+
+ const isSearch = useRecoilValue(isSearchState);
+ useEffect(()=>{
+ setLastMessage1(lastMessageRoom1);
+ setLastMessage2(lastMessageRoom2);
+ setUnReadCountRoom1(count1);
+ setUnReadCountRoom2(count2);
+ },[]);
+
+ return (
+ <>
+ {isSearch ? (
+
+ ) : (
+
+
+
+
+ )}
+ >
+ );
+}
+
+export default ChatList;
diff --git a/src/components/organism/chatroom/ChatArea.tsx b/src/components/organism/chatroom/ChatArea.tsx
new file mode 100644
index 0000000..e664d56
--- /dev/null
+++ b/src/components/organism/chatroom/ChatArea.tsx
@@ -0,0 +1,133 @@
+import React, { useEffect, useRef } from "react";
+import { Flex } from "../../atom/Flex";
+import ChatBubbleBlue from "../../moleclues/chatroom/ChatBubbleBlue";
+import ChatBubbleWhite from "../../moleclues/chatroom/ChatBubbleWhite";
+import { Space } from "../../atom/Space";
+import { useRecoilValue } from "recoil";
+import {
+ firstRoomState,
+ secondRoomState,
+ userAMessageState,
+ userBMesasgeState,
+ userCMessageState,
+ userDMesasgeState,
+} from "../../../recoil/atom";
+import dayjs from "dayjs";
+import { sortMessagesByTime } from "../../../hooks/sortMessageByTime";
+import { useParams } from "react-router-dom";
+
+export interface ChatMessage {
+ time: string;
+ id: string;
+ text: string;
+ isRead: boolean;
+}
+interface UserSortedChatMessage{
+ user: string;
+ time: string;
+ id: string;
+ text: string;
+ isRead: boolean;
+}
+
+export type ChatMessages = ChatMessage[];
+
+function ChatArea() {
+ const params = useParams();
+ // useRecoilValue가 하나의 훅이어서 곧바로 조건부 할당이 불가능하기 때문에 전역변수를 모두 구독해줘야함
+ const isUser1InFirstRoom: boolean = useRecoilValue(firstRoomState);
+ const isUser1InSecondRoom: boolean = useRecoilValue(secondRoomState);
+ const userA: ChatMessages = useRecoilValue(userAMessageState);
+ const userB: ChatMessages = useRecoilValue(userBMesasgeState);
+ const userC: ChatMessages = useRecoilValue(userCMessageState);
+ const userD: ChatMessages = useRecoilValue(userDMesasgeState);
+ // roomID에 따라 isUser1 (메시지 보내는 주체의 본인 여부) , user1Message (본인이 보낸 메시지), user2Message (상대가 보낸 메시지)에 할당하는 전역변수의 값을 다르게 할당
+ const isUser1 =
+ params.roomID === "1" ? isUser1InFirstRoom : isUser1InSecondRoom;
+ const user1Message = params.roomID === "1" ? userA : userC;
+ const user2Message = params.roomID === "1" ? userB : userD;
+
+ // user1Message와 user2Message를 합친 후 시간을 기준으로 정렬
+ const combinedMessages = sortMessagesByTime([
+ ...user1Message,
+ ...user2Message,
+ ]);
+
+ // 정렬된 메시지 리스트에 user 속성 추가
+ const sortedMessagesWithUser = combinedMessages.map((message) => {
+ const user = user1Message.find(
+ (userMessage) => userMessage.id === message.id
+ )
+ ? "User 1"
+ : "User 2";
+ return {
+ time: message.time,
+ text: message.text,
+ id: message.id,
+ user: user,
+ isRead: message.isRead,
+ };
+ });
+
+ const chatContainerRef = useRef(null);
+ useEffect(() => {
+ // 메시지가 추가될 때마다 스크롤을 아래로 이동
+ if (chatContainerRef.current) {
+ chatContainerRef.current.scrollTop =
+ chatContainerRef.current.scrollHeight;
+ }
+ }, [sortedMessagesWithUser]);
+
+ return (
+
+
+
+
+ {sortedMessagesWithUser.map((el: UserSortedChatMessage) => {
+ if (isUser1)
+ return el.user === "User 1" ? (
+
+ ) : (
+
+ );
+ else
+ return el.user === "User 1" ? (
+
+ ) : (
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export default ChatArea;
diff --git a/src/components/organism/chatroom/ChatHeader.tsx b/src/components/organism/chatroom/ChatHeader.tsx
new file mode 100644
index 0000000..ff5ee88
--- /dev/null
+++ b/src/components/organism/chatroom/ChatHeader.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import StatusBar from "../../moleclues/StatusBar";
+import ChatNav from "../../moleclues/chatroom/ChatNav";
+import { Flex } from "../../atom/Flex";
+
+function ChatHeader() {
+ return (
+
+
+
+
+ );
+}
+
+export default ChatHeader;
diff --git a/src/components/organism/chatroom/ChatMessageInput.tsx b/src/components/organism/chatroom/ChatMessageInput.tsx
new file mode 100644
index 0000000..8738ca6
--- /dev/null
+++ b/src/components/organism/chatroom/ChatMessageInput.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import ChatInput from "../../moleclues/chatroom/ChatInput";
+import BottomLine from "../../moleclues/BottomLine";
+import { Space } from "../../atom/Space";
+
+function ChatMessageInput() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default ChatMessageInput;
diff --git a/src/components/organism/contact/ContactList.tsx b/src/components/organism/contact/ContactList.tsx
new file mode 100644
index 0000000..a48f240
--- /dev/null
+++ b/src/components/organism/contact/ContactList.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import ContactItem from "../../moleclues/contact/ContactItem";
+import { Flex } from "../../atom/Flex";
+import { dummyContactList } from "../../../assets/\bdummyList";
+import { isSearchState } from "../../../recoil/atom";
+import { useRecoilValue } from "recoil";
+import SearchContainer from "../../moleclues/SearchContainer";
+interface ContactItem {
+ name: string;
+ introduction: string;
+}
+function ContactList() {
+ const isSearch:boolean = useRecoilValue(isSearchState);
+ return (
+ <>
+ {isSearch ? (
+
+ ) : (
+
+ {dummyContactList.map((item: ContactItem) => (
+
+ ))}
+
+ )}
+ >
+ );
+}
+
+export default ContactList;
diff --git a/src/components/organism/profile/MyLinkContainer.tsx b/src/components/organism/profile/MyLinkContainer.tsx
new file mode 100644
index 0000000..5ab7e8a
--- /dev/null
+++ b/src/components/organism/profile/MyLinkContainer.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import MyProfileLink from "../../moleclues/profile/MyProfileLink";
+import instagramIcon from "../../../assets/images/instagramIcon.svg";
+import naverIcon from "../../../assets/images/naverIcon.svg";
+import { Flex } from "../../atom/Flex";
+import { Space } from "../../atom/Space";
+function MyLinkContainer() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default MyLinkContainer;
diff --git a/src/components/organism/profile/MyProfileContainer.tsx b/src/components/organism/profile/MyProfileContainer.tsx
new file mode 100644
index 0000000..db291b5
--- /dev/null
+++ b/src/components/organism/profile/MyProfileContainer.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import MyProfileMain from "../../moleclues/profile/MyProfileMain";
+import MyProfileMenu from "../../moleclues/profile/MyProfileMenu";
+import personIcon from "../../../assets/images/personIcon.svg";
+import { Space } from "../../atom/Space";
+import { Flex } from "../../atom/Flex";
+function MyProfileContainer() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default MyProfileContainer;
diff --git a/src/components/organism/profile/MyUtilContainer.tsx b/src/components/organism/profile/MyUtilContainer.tsx
new file mode 100644
index 0000000..844edb2
--- /dev/null
+++ b/src/components/organism/profile/MyUtilContainer.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import MyProfileMenu from "../../moleclues/profile/MyProfileMenu";
+import bellIcon from "../../../assets/images/bellIcon.svg";
+import securityIcon from "../../../assets/images/securityIcon.svg";
+import helpIcon from "../../../assets/images/helpIcon.svg";
+import { Space } from "../../atom/Space";
+import { Flex } from "../../atom/Flex";
+import DivideLine from "../../moleclues/profile/DivideLine";
+function MyUtilContainer() {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default MyUtilContainer;
diff --git a/src/components/template/ChatRoomTemplate.tsx b/src/components/template/ChatRoomTemplate.tsx
new file mode 100644
index 0000000..cca7309
--- /dev/null
+++ b/src/components/template/ChatRoomTemplate.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import ChatHeader from "../organism/chatroom/ChatHeader";
+import ChatArea from "../organism/chatroom/ChatArea";
+import ChatMessageInput from "../organism/chatroom/ChatMessageInput";
+
+function ChatRoomTemplate() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default ChatRoomTemplate;
diff --git a/src/components/template/ChatTemplate.tsx b/src/components/template/ChatTemplate.tsx
new file mode 100644
index 0000000..50f63f4
--- /dev/null
+++ b/src/components/template/ChatTemplate.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+import HomeHeader from '../organism/HomeHeader'
+import ChatList from '../organism/chat/ChatList'
+import HomeFooter from '../organism/HomeFooter'
+
+function ChatTemplate() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+export default ChatTemplate
\ No newline at end of file
diff --git a/src/components/template/ContactTemplate.tsx b/src/components/template/ContactTemplate.tsx
new file mode 100644
index 0000000..0a07fbf
--- /dev/null
+++ b/src/components/template/ContactTemplate.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import HomeHeader from "../organism/HomeHeader";
+import HomeFooter from "../organism/HomeFooter";
+import ContactList from "../organism/contact/ContactList";
+
+function ContactTemplate() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default ContactTemplate;
diff --git a/src/components/template/ProfileTemplate.tsx b/src/components/template/ProfileTemplate.tsx
new file mode 100644
index 0000000..0a57346
--- /dev/null
+++ b/src/components/template/ProfileTemplate.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import HomeHeader from "../organism/HomeHeader";
+import ContactList from "../organism/contact/ContactList";
+import HomeFooter from "../organism/HomeFooter";
+import MyProfileContainer from "../organism/profile/MyProfileContainer";
+import MyUtilContainer from "../organism/profile/MyUtilContainer";
+import MyLinkContainer from "../organism/profile/MyLinkContainer";
+import { useRecoilValue } from "recoil";
+import { isSearchState } from "../../recoil/atom";
+import SearchContainer from "../moleclues/SearchContainer";
+
+function ProfileTemplate() {
+ const isSearch: boolean = useRecoilValue(isSearchState);
+
+ return (
+ <>
+
+ {isSearch ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ >
+ );
+}
+
+export default ProfileTemplate;
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 0000000..1a3dd3c
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,4 @@
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
diff --git a/src/hooks/getTime.tsx b/src/hooks/getTime.tsx
new file mode 100644
index 0000000..aa2e85a
--- /dev/null
+++ b/src/hooks/getTime.tsx
@@ -0,0 +1,6 @@
+import dayjs from "dayjs";
+// 원하는 포맷에 따라 현재 시간을 return
+export function getTime(format: string) {
+ const currentTime = dayjs().format(format);
+ return currentTime;
+}
diff --git a/src/hooks/handleKeyDown.tsx b/src/hooks/handleKeyDown.tsx
new file mode 100644
index 0000000..7c1572e
--- /dev/null
+++ b/src/hooks/handleKeyDown.tsx
@@ -0,0 +1,35 @@
+import { v4 as uuidv4 } from "uuid";
+import { getTime } from "./getTime";
+export function handleKeyDown(
+ event: any,
+ inputMessage: string,
+ isUser1: boolean,
+ setUser1Message: any,
+ user1Message: any,
+ setUser2Message: any,
+ user2Message: any,
+ setInputMessage: any
+) {
+ // 한글 입력시 두 번 입력 방지
+ if (event.nativeEvent.isComposing) {
+ return;
+ }
+ if (event.key === "Enter") {
+ if (inputMessage.trim().length > 0) {
+ const newMessage = {
+ time: getTime("YYYY-MM-DD HH:mm:ss"),
+ id: uuidv4(), // uuid4 generator를 통한 메시지 고유 id 생성
+ text: inputMessage,
+ isRead: false, // 상대가 읽었는지 여부 -> ChatNav에서 사용자 toggle했을시 읽음처리
+ };
+ if (isUser1) {
+ setUser1Message([...user1Message, newMessage]);
+ localStorage.setItem("user1Message", JSON.stringify(user1Message));
+ } else {
+ setUser2Message([...user2Message, newMessage]);
+ localStorage.setItem("user2Message", JSON.stringify(user2Message));
+ }
+ setInputMessage("");
+ }
+ }
+}
diff --git a/src/hooks/sortMessageByTime.tsx b/src/hooks/sortMessageByTime.tsx
new file mode 100644
index 0000000..1c7b60a
--- /dev/null
+++ b/src/hooks/sortMessageByTime.tsx
@@ -0,0 +1,12 @@
+import { ChatMessages } from "../components/organism/chatroom/ChatArea";
+
+// 메시지들을 시간순에 따라 정렬
+export function sortMessagesByTime(messages: ChatMessages) {
+ return messages.sort((a, b) => {
+ const timeA = a.time;
+ const timeB = b.time;
+ if (timeA < timeB) return -1;
+ if (timeA > timeB) return 1;
+ return 0;
+ });
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- 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',
- monospace;
-}
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index d563c0f..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..0b9f60a
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./style/font.css";
+
+const root = ReactDOM.createRoot(
+ document.getElementById("root") as HTMLElement
+);
+
+root.render(
+
+
+
+);
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/Chat.tsx b/src/pages/Chat.tsx
new file mode 100644
index 0000000..2ee8113
--- /dev/null
+++ b/src/pages/Chat.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ChatTemplate from "../components/template/ChatTemplate";
+
+function Chat() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default Chat;
diff --git a/src/pages/ChatRoom.tsx b/src/pages/ChatRoom.tsx
new file mode 100644
index 0000000..1c8a278
--- /dev/null
+++ b/src/pages/ChatRoom.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ChatRoomTemplate from "../components/template/ChatRoomTemplate";
+
+function ChatRoom() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default ChatRoom;
diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx
new file mode 100644
index 0000000..a5800e6
--- /dev/null
+++ b/src/pages/Contact.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import ContactTemplate from "../components/template/ContactTemplate";
+
+function Contact() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default Contact;
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx
new file mode 100644
index 0000000..15a4cc3
--- /dev/null
+++ b/src/pages/Profile.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import ProfileTemplate from "../components/template/ProfileTemplate";
+function Profile() {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default Profile;
diff --git a/src/recoil/atom.ts b/src/recoil/atom.ts
new file mode 100644
index 0000000..82aaac2
--- /dev/null
+++ b/src/recoil/atom.ts
@@ -0,0 +1,108 @@
+import { atom } from "recoil";
+import { ChatMessages } from "../components/organism/chatroom/ChatArea";
+
+// 첫번째 채팅방의 상태
+export const firstRoomState = atom({ key: "firstRoomState", default: true });
+
+export const userAMessageState = atom({
+ key: "userAMessageState",
+ default: [],
+ // localStorage와 atom을 연동
+ effects: [
+ ({ setSelf, onSet }) => {
+ const savedData = localStorage.getItem("userAMessage");
+ if (savedData) setSelf(JSON.parse(savedData));
+ onSet((newValue, _, isReset) => {
+ isReset
+ ? localStorage.removeItem("userAMessage")
+ : localStorage.setItem("userAMessage", JSON.stringify(newValue));
+ });
+ },
+ ],
+});
+
+export const userBMesasgeState = atom({
+ key: "userBMessageState",
+ default: [],
+ // localStorage와 atom을 연동
+ effects: [
+ ({ setSelf, onSet }) => {
+ const savedData = localStorage.getItem("userBMessage");
+ if (savedData) setSelf(JSON.parse(savedData));
+ onSet((newValue, _, isReset) => {
+ isReset
+ ? localStorage.removeItem("userBMessage")
+ : localStorage.setItem("userBMessage", JSON.stringify(newValue));
+ });
+ },
+ ],
+});
+
+// 두번째 채팅방의 상태
+
+export const secondRoomState = atom({ key: "secondRoomState", default: true });
+
+export const userCMessageState = atom({
+ key: "userCMessageState",
+ default: [],
+ // localStorage와 atom을 연동
+ effects: [
+ ({ setSelf, onSet }) => {
+ const savedData = localStorage.getItem("userCMessage");
+ if (savedData) setSelf(JSON.parse(savedData));
+ onSet((newValue, _, isReset) => {
+ isReset
+ ? localStorage.removeItem("userCMessage")
+ : localStorage.setItem("userCMessage", JSON.stringify(newValue));
+ });
+ },
+ ],
+});
+
+export const userDMesasgeState = atom({
+ key: "userDMessageState",
+ default: [],
+ // localStorage와 atom을 연동
+ effects: [
+ ({ setSelf, onSet }) => {
+ const savedData = localStorage.getItem("userDMessage");
+ if (savedData) setSelf(JSON.parse(savedData));
+ onSet((newValue, _, isReset) => {
+ isReset
+ ? localStorage.removeItem("userDMessage")
+ : localStorage.setItem("userDMessage", JSON.stringify(newValue));
+ });
+ },
+ ],
+});
+
+// 사용자 검색창의 상태
+
+export const isSearchState = atom({
+ key: "isSearchState",
+ default: false,
+});
+
+export const userInputState = atom({
+ key: "userInputState",
+ default: "",
+});
+
+export const lastMessage1State = atom({
+ key: "lastMessage1State",
+ default: "",
+});
+
+export const lastMessage2State = atom({
+ key: "lastMessage2State",
+ default: "",
+});
+
+export const unReadCount1State = atom({
+ key: "unreadCount1State",
+ default: 0,
+});
+export const unReadCount2State = atom({
+ key: "unreadCount2State",
+ default: 0,
+});
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
deleted file mode 100644
index 5253d3a..0000000
--- a/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 8f2609b..0000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/style/GloblalStyles.tsx b/src/style/GloblalStyles.tsx
new file mode 100644
index 0000000..b4d36cf
--- /dev/null
+++ b/src/style/GloblalStyles.tsx
@@ -0,0 +1,24 @@
+import { createGlobalStyle } from "styled-components";
+import reset from "styled-reset";
+
+export const GlobalStyles = createGlobalStyle`
+ ${reset};
+ *{
+ word-break: keep-all;
+ box-sizing: border-box;
+ }
+
+ *::-webkit-scrollbar {
+ display: none;
+ }
+
+ body{
+ font-family: "Pretendard";
+ background-color: #F5F5F5;
+ }
+ input{
+ outline: none;
+ border: none;
+ }
+
+`;
diff --git a/src/style/font.css b/src/style/font.css
new file mode 100644
index 0000000..cd43764
--- /dev/null
+++ b/src/style/font.css
@@ -0,0 +1,93 @@
+/* Pretendard, Lato */
+/* Pretendard */
+/* Segoe */
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Black.woff) format("woff");
+ font-weight: 900;
+}
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-ExtraBold.woff) format("woff2");
+ font-weight: 800;
+}
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Bold.woff) format("woff2");
+ font-weight: 700;
+}
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-SemiBold.woff) format("woff2");
+ font-weight: 600;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Medium.woff) format("woff2");
+ font-weight: 500;
+}
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Regular.woff) format("woff2");
+ font-weight: 400;
+}
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Thin.woff) format("woff2");
+ font-weight: 300;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-Light.woff) format("woff2");
+ font-weight: 200;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ src: url(../assets/fonts/Pretendard-ExtraLight.woff) format("woff2");
+ font-weight: 100;
+}
+
+/* Lato */
+
+@font-face {
+ font-family: "Lato";
+ src: url(../assets/fonts/Lato-Regular.woff) format("woff");
+ font-weight: 500;
+}
+@font-face {
+ font-family: "Lato";
+ src: url(../assets/fonts/Lato-Medium.woff) format("woff");
+ font-weight: 400;
+}
+@font-face {
+ font-family: "Lato";
+ src: url(../assets/fonts/Lato-Thin.woff) format("woff");
+ font-weight: 300;
+}
+
+@font-face {
+ font-family: "Lato";
+ src: url(../assets/fonts/Lato-Light.woff) format("woff");
+ font-weigth: 200;
+}
+
+/* Segoe */
+
+@font-face {
+ font-family: "Segoe";
+ src: url(../assets/fonts/Segoe.woff) format("woff");
+ font-weight: 400;
+}
+@font-face {
+ font-family: "Segoe";
+ src: url(../assets/fonts/Segoe-Bold.woff) format("woff");
+ font-weight: 600;
+}
+@font-face {
+ font-family: "Segoe";
+ src: url(../assets/fonts/SegoePro-Semibold.woff) format("woff");
+ font-weight: 500;
+}
diff --git a/src/style/theme.ts b/src/style/theme.ts
new file mode 100644
index 0000000..2ea5db4
--- /dev/null
+++ b/src/style/theme.ts
@@ -0,0 +1,15 @@
+const theme = {
+ // 사용할 색깔 모음
+ colors: {
+ mainBlack: "#0F1828",
+ mainBlue: "#166FF6",
+ lightBlue: "#D2D5F9",
+ white: "#FFFFFF",
+ gray: "#ADB5BD",
+ offWhite: "#EDEDED",
+ chatBlue: "#002DE3",
+ chatBackground: "#F7F7FC",
+ },
+};
+
+export default theme;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..63080cd
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "es5", // 'es3', 'es5', 'es2015', 'es2016', 'es2017','es2018', 'esnext' 가능
+ "module": "commonjs", //무슨 import 문법 쓸건지 'commonjs', 'amd', 'es2015', 'esnext'
+ "allowJs": true, // js 파일들 ts에서 import해서 쓸 수 있는지
+ "checkJs": true, // 일반 js 파일에서도 에러체크 여부
+ "jsx": "preserve", // tsx 파일을 jsx로 어떻게 컴파일할 것인지 'preserve', 'react-native', 'react'
+ "declaration": true, //컴파일시 .d.ts 파일도 자동으로 함께생성 (현재쓰는 모든 타입이 정의된 파일)
+ "outDir": "./", //js파일 아웃풋 경로바꾸기
+ "removeComments": true, //컴파일시 주석제거
+ "strict": true, //strict 관련, noimplicit 어쩌구 관련 모드 전부 켜기
+ "noImplicitAny": true, //any타입 금지 여부
+ "strictNullChecks": true, //null, undefined 타입에 이상한 짓 할시 에러내기
+ "strictFunctionTypes": true, //함수파라미터 타입체크 강하게
+ "strictPropertyInitialization": true, //class constructor 작성시 타입체크 강하게
+ "noImplicitThis": true, //this 키워드가 any 타입일 경우 에러내기
+ "alwaysStrict": true, //자바스크립트 "use strict" 모드 켜기
+
+ "noUnusedLocals": true, //쓰지않는 지역변수 있으면 에러내기
+ "noUnusedParameters": true, //쓰지않는 파라미터 있으면 에러내기
+ "noImplicitReturns": true, //함수에서 return 빼먹으면 에러내기
+ "noFallthroughCasesInSwitch": true //switch문 이상하면 에러내기
+ }
+}
diff --git "a/\354\261\204\355\214\205.jpg" "b/\354\261\204\355\214\205.jpg"
new file mode 100644
index 0000000..e1a9886
Binary files /dev/null and "b/\354\261\204\355\214\205.jpg" differ