diff --git a/.gitignore b/.gitignore
index 4d29575d..e85f4eb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+node_modules
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 00000000..8b0bc4ef
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,3 @@
+{
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
\ No newline at end of file
diff --git a/README copy.md b/README copy.md
deleted file mode 100644
index 82b92ada..00000000
--- a/README copy.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# 서론
-
-안녕하세요 🙌🏻 19기 프론트 운영진 배성준입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
-
-이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
-
-동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다.
-
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
-
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
-
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
-
-그럼 이번 미션도 파이팅입니다!!🎉
-
-# 미션
-
-## Key Questions
-
-- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요?
-- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본
-- 컴포넌트를 분리한 기준은 무엇인가요?
-- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요?
-- 디자이너와 소통하며 느낀점은 무엇인가요?
-
-## 미션 목표
-
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다.
-
-## 기한
-
-2024년 3월 29일 금요일
-
-## 필수 구현 기능
-
-- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
-
-### 추가 구현 기능
-
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
-
-참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
-
-## 링크 및 참고자료
-
-- [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)
diff --git a/package-lock.json b/package-lock.json
index c27bbe4e..3a78dee2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,11 +15,23 @@
"@types/node": "^16.18.91",
"@types/react": "^18.2.69",
"@types/react-dom": "^18.2.22",
+ "animate.css": "^4.1.1",
+ "eslint-plugin-react": "^7.36.1",
"react": "^18.2.0",
+ "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.7",
+ "recoil-persist": "^5.1.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.47",
+ "prettier-plugin-tailwindcss": "^0.6.6",
+ "tailwindcss": "^3.4.13"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -3338,6 +3350,15 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
+ "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
+ "license": "MIT",
+ "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",
@@ -4851,6 +4872,12 @@
"ajv": "^6.9.1"
}
},
+ "node_modules/animate.css": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
+ "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
+ "license": "MIT"
+ },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -5072,27 +5099,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/array.prototype.toreversed": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
- "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- }
- },
"node_modules/array.prototype.tosorted": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
- "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.5",
+ "call-bind": "^1.0.7",
"define-properties": "^1.2.1",
- "es-abstract": "^1.22.3",
- "es-errors": "^1.1.0",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
"es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
}
},
"node_modules/arraybuffer.prototype.slice": {
@@ -5145,9 +5165,9 @@
}
},
"node_modules/autoprefixer": {
- "version": "10.4.19",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
- "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"funding": [
{
"type": "opencollective",
@@ -5162,12 +5182,13 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "browserslist": "^4.23.0",
- "caniuse-lite": "^1.0.30001599",
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
- "picocolors": "^1.0.0",
+ "picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -5634,9 +5655,9 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"node_modules/browserslist": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
- "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+ "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"funding": [
{
"type": "opencollective",
@@ -5651,11 +5672,12 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001587",
- "electron-to-chromium": "^1.4.668",
- "node-releases": "^2.0.14",
- "update-browserslist-db": "^1.0.13"
+ "caniuse-lite": "^1.0.30001646",
+ "electron-to-chromium": "^1.5.4",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -5762,9 +5784,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001600",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
- "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
+ "version": "1.0.30001663",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz",
+ "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==",
"funding": [
{
"type": "opencollective",
@@ -5778,7 +5800,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
@@ -7018,9 +7041,10 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.715",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz",
- "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
+ "version": "1.5.27",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz",
+ "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==",
+ "license": "ISC"
},
"node_modules/emittery": {
"version": "0.8.1",
@@ -7091,9 +7115,10 @@
}
},
"node_modules/es-abstract": {
- "version": "1.23.2",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz",
- "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==",
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
+ "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
+ "license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.1",
"arraybuffer.prototype.slice": "^1.0.3",
@@ -7134,11 +7159,11 @@
"safe-regex-test": "^1.0.3",
"string.prototype.trim": "^1.2.9",
"string.prototype.trimend": "^1.0.8",
- "string.prototype.trimstart": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.8",
"typed-array-buffer": "^1.0.2",
"typed-array-byte-length": "^1.0.1",
"typed-array-byte-offset": "^1.0.2",
- "typed-array-length": "^1.0.5",
+ "typed-array-length": "^1.0.6",
"unbox-primitive": "^1.0.2",
"which-typed-array": "^1.1.15"
},
@@ -7193,13 +7218,14 @@
}
},
"node_modules/es-iterator-helpers": {
- "version": "1.0.18",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
- "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz",
+ "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==",
+ "license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
- "es-abstract": "^1.23.0",
+ "es-abstract": "^1.23.3",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.0.3",
"function-bind": "^1.1.2",
@@ -7569,34 +7595,35 @@
}
},
"node_modules/eslint-plugin-react": {
- "version": "7.34.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
- "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
+ "version": "7.36.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
+ "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
+ "license": "MIT",
"dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlast": "^1.2.4",
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
"array.prototype.flatmap": "^1.3.2",
- "array.prototype.toreversed": "^1.1.2",
- "array.prototype.tosorted": "^1.1.3",
+ "array.prototype.tosorted": "^1.1.4",
"doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.0.17",
+ "es-iterator-helpers": "^1.0.19",
"estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7",
- "object.hasown": "^1.1.3",
- "object.values": "^1.1.7",
+ "object.entries": "^1.1.8",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.0",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.5",
"semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.10"
+ "string.prototype.matchall": "^4.0.11",
+ "string.prototype.repeat": "^1.0.0"
},
"engines": {
"node": ">=4"
},
"peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
"node_modules/eslint-plugin-react-hooks": {
@@ -8861,6 +8888,12 @@
"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==",
+ "license": "MIT"
+ },
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -12619,9 +12652,10 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="
},
"node_modules/node-releases": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
- "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -12804,22 +12838,6 @@
"node": ">= 0.4"
}
},
- "node_modules/object.hasown": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
- "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
- "dependencies": {
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/object.values": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
@@ -13092,9 +13110,10 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -13258,9 +13277,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.38",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
- "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -13275,10 +13294,11 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -14464,6 +14484,102 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
+ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.6.6",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz",
+ "integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig-melody": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-import-sort": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-style-order": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig-melody": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-import-sort": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-style-order": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -14834,6 +14950,19 @@
"node": ">=8"
}
},
+ "node_modules/react-device-detect": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz",
+ "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==",
+ "license": "MIT",
+ "dependencies": {
+ "ua-parser-js": "^1.0.33"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0",
+ "react-dom": ">= 0.14.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -14864,6 +14993,38 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
+ "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.19.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
+ "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.19.2",
+ "react-router": "6.26.2"
+ },
+ "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",
@@ -14968,6 +15129,35 @@
"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==",
+ "license": "MIT",
+ "dependencies": {
+ "hamt_plus": "1.0.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recoil-persist": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/recoil-persist/-/recoil-persist-5.1.0.tgz",
+ "integrity": "sha512-sew4k3uBVJjRWKCSFuBw07Y1p1pBOb0UxLJPxn4G2bX/9xNj+r2xlqYy/BRfyofR/ANfqBU04MIvulppU4ZC0w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "recoil": "^0.7.2"
+ }
+ },
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -15805,9 +15995,10 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -16117,6 +16308,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
"node_modules/string.prototype.trim": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
@@ -16491,9 +16692,10 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"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==",
+ "license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -16503,7 +16705,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",
@@ -16947,6 +17149,32 @@
"node": ">=4.2.0"
}
},
+ "node_modules/ua-parser-js": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz",
+ "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -17044,9 +17272,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.13",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
- "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
+ "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"funding": [
{
"type": "opencollective",
@@ -17061,9 +17289,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "escalade": "^3.1.1",
- "picocolors": "^1.0.0"
+ "escalade": "^3.1.2",
+ "picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
diff --git a/package.json b/package.json
index ea335d36..937a3ef5 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,15 @@
"@types/node": "^16.18.91",
"@types/react": "^18.2.69",
"@types/react-dom": "^18.2.22",
+ "animate.css": "^4.1.1",
+ "eslint-plugin-react": "^7.36.1",
"react": "^18.2.0",
+ "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
+ "recoil": "^0.7.7",
+ "recoil-persist": "^5.1.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
@@ -25,8 +31,13 @@
"eslintConfig": {
"extends": [
"react-app",
- "react-app/jest"
- ]
+ "react-app/jest",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "rules": {
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": "warn"
+ }
},
"browserslist": {
"production": [
@@ -39,5 +50,11 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.47",
+ "prettier-plugin-tailwindcss": "^0.6.6",
+ "tailwindcss": "^3.4.13"
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/public/favicon-messenger.png b/public/favicon-messenger.png
new file mode 100644
index 00000000..02dbed7f
Binary files /dev/null and b/public/favicon-messenger.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index a11777cc..00000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/index.html b/public/index.html
index aa069f27..03cbafd7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,43 +1,12 @@
-
+
-
+
-
-
-
-
-
-
- React App
+ Direct Message
-
-
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a3..00000000
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 a4e47a65..00000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 080d6c77..00000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ],
- "start_url": ".",
- "display": "standalone",
- "theme_color": "#000000",
- "background_color": "#ffffff"
-}
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index e9e57dc4..00000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053..00000000
--- 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.tsx b/src/App.tsx
index 5381007b..110c400c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,8 +1,76 @@
+import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
+import { RecoilRoot } from "recoil";
+import { BrowserView, MobileView } from "react-device-detect";
+
+// common
+import { ReactComponent as HomeIndicator } from "./assets/images/home_indicator.svg";
+import StatusBar from "./components/common/StatusBar";
+
+// pages
+import MyProfilePage from "./pages/MyProfilePage";
+import FollowListPage from "./pages/FollowListPage";
+import ChatListPage from "./pages/ChatListPage";
+import ChatRoomPage from "./pages/ChatRoomPage";
+
+//hooks
+import useHandleHeight from "./hooks/useHandleHeight";
+
function App() {
+ const height = useHandleHeight();
+
return (
-
-
20기 프론트엔드 파이팅!!! 디자인과 사이좋게 지내요~~~
-
+ <>
+
+ {/*브라우저 뷰 (상하단 바 포함)*/}
+
+
+
+
+
+
+ }>
+ }
+ >
+ }>
+ }
+ >
+
+
+
+
+
+
+ {/*모바일 뷰*/}
+
+
+
+
+ }>
+ }
+ >
+ }>
+ }
+ >
+
+
+
+
+
+ >
);
}
diff --git a/src/assets/fonts/SF-Pro-Text-Bold.otf b/src/assets/fonts/SF-Pro-Text-Bold.otf
new file mode 100644
index 00000000..93a89484
Binary files /dev/null and b/src/assets/fonts/SF-Pro-Text-Bold.otf differ
diff --git a/src/assets/fonts/SF-Pro-Text-Regular.otf b/src/assets/fonts/SF-Pro-Text-Regular.otf
new file mode 100644
index 00000000..e6301485
Binary files /dev/null and b/src/assets/fonts/SF-Pro-Text-Regular.otf differ
diff --git a/src/assets/fonts/SF-Pro.ttf b/src/assets/fonts/SF-Pro.ttf
new file mode 100644
index 00000000..e051a82c
Binary files /dev/null and b/src/assets/fonts/SF-Pro.ttf differ
diff --git a/src/assets/icons/arrow.svg b/src/assets/icons/arrow.svg
new file mode 100644
index 00000000..ba5193d3
--- /dev/null
+++ b/src/assets/icons/arrow.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/back.svg b/src/assets/icons/back.svg
new file mode 100644
index 00000000..60505d7a
--- /dev/null
+++ b/src/assets/icons/back.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/camera.svg b/src/assets/icons/camera.svg
new file mode 100644
index 00000000..ca033379
--- /dev/null
+++ b/src/assets/icons/camera.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/icons/camera_gray.svg b/src/assets/icons/camera_gray.svg
new file mode 100644
index 00000000..32275952
--- /dev/null
+++ b/src/assets/icons/camera_gray.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg
new file mode 100644
index 00000000..7e8d3519
--- /dev/null
+++ b/src/assets/icons/edit.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/ellipse.svg b/src/assets/icons/ellipse.svg
new file mode 100644
index 00000000..d4791cd9
--- /dev/null
+++ b/src/assets/icons/ellipse.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/icons/emotion_add.svg b/src/assets/icons/emotion_add.svg
new file mode 100644
index 00000000..75620e13
--- /dev/null
+++ b/src/assets/icons/emotion_add.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/emotion_angry.svg b/src/assets/icons/emotion_angry.svg
new file mode 100644
index 00000000..ce228170
--- /dev/null
+++ b/src/assets/icons/emotion_angry.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/emotion_heart.svg b/src/assets/icons/emotion_heart.svg
new file mode 100644
index 00000000..6172d3c6
--- /dev/null
+++ b/src/assets/icons/emotion_heart.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/emotion_joy.svg b/src/assets/icons/emotion_joy.svg
new file mode 100644
index 00000000..1e51ace3
--- /dev/null
+++ b/src/assets/icons/emotion_joy.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/emotion_surprise.svg b/src/assets/icons/emotion_surprise.svg
new file mode 100644
index 00000000..37c1ed79
--- /dev/null
+++ b/src/assets/icons/emotion_surprise.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/emotion_tear.svg b/src/assets/icons/emotion_tear.svg
new file mode 100644
index 00000000..0330d2d7
--- /dev/null
+++ b/src/assets/icons/emotion_tear.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/emotion_thumbup.svg b/src/assets/icons/emotion_thumbup.svg
new file mode 100644
index 00000000..7674981c
--- /dev/null
+++ b/src/assets/icons/emotion_thumbup.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/gif.svg b/src/assets/icons/gif.svg
new file mode 100644
index 00000000..396bd82a
--- /dev/null
+++ b/src/assets/icons/gif.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/grid.svg b/src/assets/icons/grid.svg
new file mode 100644
index 00000000..33ae68e5
--- /dev/null
+++ b/src/assets/icons/grid.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/grid2.svg b/src/assets/icons/grid2.svg
new file mode 100644
index 00000000..cf7128c8
--- /dev/null
+++ b/src/assets/icons/grid2.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/home.svg b/src/assets/icons/home.svg
new file mode 100644
index 00000000..19403d8e
--- /dev/null
+++ b/src/assets/icons/home.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg
new file mode 100644
index 00000000..46d57a00
--- /dev/null
+++ b/src/assets/icons/info.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg
new file mode 100644
index 00000000..6fd0181b
--- /dev/null
+++ b/src/assets/icons/menu.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg
new file mode 100644
index 00000000..b53fcaf8
--- /dev/null
+++ b/src/assets/icons/message.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/message_active.svg b/src/assets/icons/message_active.svg
new file mode 100644
index 00000000..91204399
--- /dev/null
+++ b/src/assets/icons/message_active.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/mic.svg b/src/assets/icons/mic.svg
new file mode 100644
index 00000000..6a20a594
--- /dev/null
+++ b/src/assets/icons/mic.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/more.svg b/src/assets/icons/more.svg
new file mode 100644
index 00000000..2dab7e10
--- /dev/null
+++ b/src/assets/icons/more.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/phone.svg b/src/assets/icons/phone.svg
new file mode 100644
index 00000000..71e458a6
--- /dev/null
+++ b/src/assets/icons/phone.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/picture.svg b/src/assets/icons/picture.svg
new file mode 100644
index 00000000..ea0f4e6d
--- /dev/null
+++ b/src/assets/icons/picture.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/pin.svg b/src/assets/icons/pin.svg
new file mode 100644
index 00000000..87797ffd
--- /dev/null
+++ b/src/assets/icons/pin.svg
@@ -0,0 +1,20 @@
+
+
diff --git a/src/assets/icons/pin2.svg b/src/assets/icons/pin2.svg
new file mode 100644
index 00000000..08a4d63e
--- /dev/null
+++ b/src/assets/icons/pin2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg
new file mode 100644
index 00000000..54f98272
--- /dev/null
+++ b/src/assets/icons/plus.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/profile.svg b/src/assets/icons/profile.svg
new file mode 100644
index 00000000..c17b88a5
--- /dev/null
+++ b/src/assets/icons/profile.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/profile_active.svg b/src/assets/icons/profile_active.svg
new file mode 100644
index 00000000..e03558b2
--- /dev/null
+++ b/src/assets/icons/profile_active.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg
new file mode 100644
index 00000000..efac557a
--- /dev/null
+++ b/src/assets/icons/search.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/send_button.svg b/src/assets/icons/send_button.svg
new file mode 100644
index 00000000..97621081
--- /dev/null
+++ b/src/assets/icons/send_button.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/tagged.svg b/src/assets/icons/tagged.svg
new file mode 100644
index 00000000..f3096eb7
--- /dev/null
+++ b/src/assets/icons/tagged.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/tagged2.svg b/src/assets/icons/tagged2.svg
new file mode 100644
index 00000000..50905d63
--- /dev/null
+++ b/src/assets/icons/tagged2.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/videocam.svg b/src/assets/icons/videocam.svg
new file mode 100644
index 00000000..f777b1e0
--- /dev/null
+++ b/src/assets/icons/videocam.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/images/ceos_logo.svg b/src/assets/images/ceos_logo.svg
new file mode 100644
index 00000000..56aed933
--- /dev/null
+++ b/src/assets/images/ceos_logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/images/content1.png b/src/assets/images/content1.png
new file mode 100644
index 00000000..64838fe6
Binary files /dev/null and b/src/assets/images/content1.png differ
diff --git a/src/assets/images/content2.png b/src/assets/images/content2.png
new file mode 100644
index 00000000..647f3d2c
Binary files /dev/null and b/src/assets/images/content2.png differ
diff --git a/src/assets/images/content3.png b/src/assets/images/content3.png
new file mode 100644
index 00000000..c6f69355
Binary files /dev/null and b/src/assets/images/content3.png differ
diff --git a/src/assets/images/ewha_logo.svg b/src/assets/images/ewha_logo.svg
new file mode 100644
index 00000000..c4325231
--- /dev/null
+++ b/src/assets/images/ewha_logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/home_indicator.svg b/src/assets/images/home_indicator.svg
new file mode 100644
index 00000000..ecb5158d
--- /dev/null
+++ b/src/assets/images/home_indicator.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/assets/images/hongik_logo.svg b/src/assets/images/hongik_logo.svg
new file mode 100644
index 00000000..968e0197
--- /dev/null
+++ b/src/assets/images/hongik_logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/newJeans_logo.svg b/src/assets/images/newJeans_logo.svg
new file mode 100644
index 00000000..1fdf7fee
--- /dev/null
+++ b/src/assets/images/newJeans_logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/none_tagged.png b/src/assets/images/none_tagged.png
new file mode 100644
index 00000000..8103d7aa
Binary files /dev/null and b/src/assets/images/none_tagged.png differ
diff --git a/src/assets/images/profile_img.svg b/src/assets/images/profile_img.svg
new file mode 100644
index 00000000..59a2592e
--- /dev/null
+++ b/src/assets/images/profile_img.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/profile_img2.svg b/src/assets/images/profile_img2.svg
new file mode 100644
index 00000000..bf902d9a
--- /dev/null
+++ b/src/assets/images/profile_img2.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/components/ChatList/ChatListContents.tsx b/src/components/ChatList/ChatListContents.tsx
new file mode 100644
index 00000000..a674e635
--- /dev/null
+++ b/src/components/ChatList/ChatListContents.tsx
@@ -0,0 +1,99 @@
+// components
+import ChatListItem from "./ChatListItem";
+
+// icons
+import { ReactComponent as SearchIcon } from "../../assets/icons/search.svg";
+
+// reoil
+import { useRecoilValue } from "recoil";
+import { chattingState, userState } from "../../recoil/atom";
+import { useEffect, useState } from "react";
+
+import userData from "../../data/UserData.json";
+
+const ChatListContents = () => {
+ const chattings = useRecoilValue(chattingState);
+ const user = useRecoilValue(userState);
+ const myId = user.me.id;
+
+ // 검색 기능
+ const [searchInput, setSearchInput] = useState("");
+ const handleInput = (e: React.ChangeEvent) => {
+ setSearchInput(e.target.value);
+ };
+
+ // 상단 고정 기능
+ const [pinnedChatIds, setPinnedChatIds] = useState(() => {
+ const storedPinnedChatIds = localStorage.getItem("pinnedChatIds");
+ return storedPinnedChatIds ? JSON.parse(storedPinnedChatIds) : [];
+ });
+
+ useEffect(() => {
+ // pinnedChatIds 변경 시 로컬 스토리지에 저장
+ localStorage.setItem("pinnedChatIds", JSON.stringify(pinnedChatIds));
+ }, [pinnedChatIds]);
+
+ const handlePinToggle = (id: number | undefined) => {
+ if (id !== undefined) {
+ setPinnedChatIds(
+ (prevIds) =>
+ prevIds.includes(id)
+ ? prevIds.filter((chatId) => chatId !== id) // 이미 고정된 경우 해제
+ : [id, ...prevIds], // 새로운 고정 항목은 가장 상단으로 추가
+ );
+ }
+ };
+
+ const filteredFollowers = chattings
+ .filter((chat) =>
+ chat.users.some((userId) => {
+ if (userId !== myId) {
+ const otherUser = userData.users.find((user) => user.id === userId); // 대화가 존재하는 사용자와의 채팅 필터
+ return otherUser?.userName.toLocaleLowerCase().includes(searchInput); // 검색어에 부합하는 사용자와의 채팅 필터
+ }
+ return false;
+ }),
+ )
+ .sort((a, b) => {
+ // 고정된 항목 상단으로 + 최신 항목이 더 위에 오도록 정렬
+ const aIdx = pinnedChatIds.indexOf(a.id!);
+ const bIdx = pinnedChatIds.indexOf(b.id!);
+ if (aIdx === -1 && bIdx === -1) return 0; // 모두 핀 되지 않은 경우 -> 순서 변경 안함
+ if (aIdx === -1) return 1; // a 핀x, b 핀o
+ if (bIdx === -1) return -1; // a 핀o, b 핀x
+ return aIdx - bIdx; // 둘다 핀 된 경우 -> 배열 순대로
+ });
+
+ return (
+
+
+
+
+
+
+ Messages
+ Requests
+
+
+ {filteredFollowers.map(
+ (chatting) =>
+ chatting.id !== undefined && ( // id가 undefined가 아닌 항목만 렌더링
+ handlePinToggle(chatting.id)}
+ />
+ ),
+ )}
+
+
+ );
+};
+
+export default ChatListContents;
diff --git a/src/components/ChatList/ChatListItem.tsx b/src/components/ChatList/ChatListItem.tsx
new file mode 100644
index 00000000..be7acdd4
--- /dev/null
+++ b/src/components/ChatList/ChatListItem.tsx
@@ -0,0 +1,105 @@
+import { useNavigate } from "react-router-dom";
+
+// icons
+import { ReactComponent as CameraIcon } from "../../assets/icons/camera_gray.svg";
+import { ReactComponent as PinnedIcon } from "../../assets/icons/pin.svg";
+import { ReactComponent as PinIcon } from "../../assets/icons/pin2.svg";
+
+// data & recoil
+import userData from "../../data/UserData.json";
+import { chattingInterface } from "../../types/interface";
+import useHandleTime from "../../hooks/useHandleTime";
+import { useState } from "react";
+
+interface ChatListItemProps {
+ chatting: chattingInterface;
+ isPinned: boolean;
+ handleTogglePin: () => void;
+}
+
+const ChatListItem: React.FC = ({
+ chatting,
+ isPinned,
+ handleTogglePin,
+}) => {
+ const followerId = chatting.users.filter((id) => id !== 0)[0];
+ const follower = userData.users.filter((user) => user.id === followerId)[0];
+
+ const lastMessageContent = chatting.chatList[chatting.chatList.length - 1];
+ const lastMessage = lastMessageContent?.message.startsWith("data:image/")
+ ? "이미지가 전송됨" // 이미지 파일일 경우 경로 대신 대체 텍스트
+ : lastMessageContent.message;
+
+ //
+ const { lastModified } = useHandleTime(lastMessageContent);
+
+ const navigate = useNavigate();
+ const gotoChatRoom = () => {
+ navigate(`/chatroom/${followerId}`);
+ };
+
+ const [isSlided, setIsSlided] = useState(false);
+ const clickCamera = () => {
+ setIsSlided(!isSlided);
+ };
+
+ const clickPin = () => {
+ handleTogglePin();
+ // console.log("Pinned!"); // 콘솔 대신 핀 기능 구현
+ setIsSlided(false);
+ };
+
+ return (
+
+
+
+

+
+ gotoChatRoom()}
+ className="flex flex-grow cursor-pointer flex-col"
+ >
+
+
+ {follower.userName}
+
+ {isPinned &&
}
+
+
+
+
+ {lastMessage}
+
+
+ {lastModified}
+
+
+
+
+
+
+
+ {isSlided && (
+
+
+
+ )}
+
+ );
+};
+
+export default ChatListItem;
diff --git a/src/components/ChatList/TopBar.tsx b/src/components/ChatList/TopBar.tsx
new file mode 100644
index 00000000..9b590a45
--- /dev/null
+++ b/src/components/ChatList/TopBar.tsx
@@ -0,0 +1,29 @@
+import { useNavigate } from "react-router-dom";
+// icons
+import { ReactComponent as BackIcon } from "../../assets/icons/back.svg";
+import { ReactComponent as ArrowIcon } from "../../assets/icons/arrow.svg";
+import { ReactComponent as EditIcon } from "../../assets/icons/edit.svg";
+
+const TopBar = () => {
+ const navigate = useNavigate();
+
+ return (
+ <>
+
+
+
+ navigate("/")}
+ />
+ jngynjng
+
+
+
+
+
+ >
+ );
+};
+
+export default TopBar;
diff --git a/src/components/ChatRoom/Chattings.tsx b/src/components/ChatRoom/Chattings.tsx
new file mode 100644
index 00000000..66c1a75a
--- /dev/null
+++ b/src/components/ChatRoom/Chattings.tsx
@@ -0,0 +1,98 @@
+// components
+import InputBox from "./InputBox";
+import MyChat from "./MyChat";
+import ReceivedChat from "./ReceivedChat";
+
+// recoil
+import { useRecoilValue } from "recoil";
+import { selectedEmotionsState, selectedMessageState } from "../../recoil/atom";
+
+// hooks
+import useAutoScroll from "../../hooks/useAutoScroll";
+import useChatSend from "../../hooks/useChatSend";
+import useEmotionBox from "../../hooks/useEmotionBox";
+
+const Chattings = () => {
+ const { users, currentChatting, sendChat } = useChatSend();
+
+ // 하단으로 자동 스크롤
+ const scrollRef = useAutoScroll([currentChatting.chatList]);
+
+ // 감정 남기기
+ const { handleLongPress, emotionBoxRef } = useEmotionBox(scrollRef);
+ const selectedMessage = useRecoilValue(selectedMessageState);
+ const selectedEmotions = useRecoilValue(selectedEmotionsState);
+
+ return (
+
+
+ {currentChatting.chatList.length > 0 ? (
+ // 이전 대화내역이 있을 때
+ currentChatting.chatList.map((chat, index) =>
+ chat.sender === users.me.id ? (
+
+ ) : (
+
handleLongPress(index, e)}
+ isSelected={selectedMessage === index}
+ selectedEmotion={selectedEmotions[index] || null}
+ emotionBoxRef={emotionBoxRef}
+ />
+ ),
+ )
+ ) : (
+ // 이전 대화내역이 없을 때
+
+

+
+
+ {users.other.userName}
+
+
{users.other.userId}
+
735 followers 174 posts
+
+ hongik_university also follows
+
+
+
+ Inquire
+
+
+ View profile
+
+
+
+ )}
+
+
+
+ );
+};
+export default Chattings;
diff --git a/src/components/ChatRoom/EmotionBox.tsx b/src/components/ChatRoom/EmotionBox.tsx
new file mode 100644
index 00000000..5db1a579
--- /dev/null
+++ b/src/components/ChatRoom/EmotionBox.tsx
@@ -0,0 +1,44 @@
+import heart from "../../assets/icons/emotion_heart.svg";
+import joy from "../../assets/icons/emotion_joy.svg";
+import surprise from "../../assets/icons/emotion_surprise.svg";
+import tear from "../../assets/icons/emotion_tear.svg";
+import angry from "../../assets/icons/emotion_angry.svg";
+import thumbup from "../../assets/icons/emotion_thumbup.svg";
+import plus from "../../assets/icons/emotion_add.svg";
+
+interface EmotionBoxProps {
+ onSelectEmotion: (emotionId: number) => void; // 선택된 감정을 처리하는 함수 prop
+}
+
+const EmotionBox: React.FC = ({ onSelectEmotion }) => {
+ const emotionList = [
+ { id: 1, src: heart },
+ { id: 2, src: joy },
+ { id: 3, src: surprise },
+ { id: 4, src: tear },
+ { id: 5, src: angry },
+ { id: 6, src: thumbup },
+ { id: 7, src: plus },
+ ];
+ return (
+ <>
+
+
+ Tap and hold to super react
+
+
+ {emotionList.map((item) => (
+
onSelectEmotion(item.id)}
+ className="cursor-pointer"
+ />
+ ))}
+
+
+ >
+ );
+};
+
+export default EmotionBox;
diff --git a/src/components/ChatRoom/EmotionRemained.tsx b/src/components/ChatRoom/EmotionRemained.tsx
new file mode 100644
index 00000000..10cc5d77
--- /dev/null
+++ b/src/components/ChatRoom/EmotionRemained.tsx
@@ -0,0 +1,39 @@
+import heart from "../../assets/icons/emotion_heart.svg";
+import joy from "../../assets/icons/emotion_joy.svg";
+import surprise from "../../assets/icons/emotion_surprise.svg";
+import tear from "../../assets/icons/emotion_tear.svg";
+import angry from "../../assets/icons/emotion_angry.svg";
+import thumbup from "../../assets/icons/emotion_thumbup.svg";
+import plus from "../../assets/icons/emotion_add.svg";
+
+interface EmotionRemainedProps {
+ emotionId: number;
+ isMine: boolean;
+}
+
+const EmotionRemained: React.FC = ({
+ emotionId,
+ isMine,
+}) => {
+ const emotionList = [
+ { id: 1, src: heart },
+ { id: 2, src: joy },
+ { id: 3, src: surprise },
+ { id: 4, src: tear },
+ { id: 5, src: angry },
+ { id: 6, src: thumbup },
+ { id: 7, src: plus },
+ ];
+
+ const emotionIcon = emotionList.find((emotion) => emotion.id === emotionId);
+
+ return (
+
+

+
+ );
+};
+
+export default EmotionRemained;
diff --git a/src/components/ChatRoom/InputBox.tsx b/src/components/ChatRoom/InputBox.tsx
new file mode 100644
index 00000000..e8e71c2e
--- /dev/null
+++ b/src/components/ChatRoom/InputBox.tsx
@@ -0,0 +1,114 @@
+import { useRef, useState } from "react";
+
+// icons
+import { ReactComponent as CameraIcon } from "../../assets/icons/camera.svg";
+import { ReactComponent as MicIcon } from "../../assets/icons/mic.svg";
+import { ReactComponent as PictureIcon } from "../../assets/icons/picture.svg";
+import { ReactComponent as GIFIcon } from "../../assets/icons/gif.svg";
+import { ReactComponent as SendButton } from "../../assets/icons/send_button.svg";
+
+interface InputBoxProps {
+ sendChat: (message: string | File, type: "text" | "image") => void;
+}
+
+const InputBox: React.FC = ({ sendChat }) => {
+ const [inputText, setInputText] = useState(""); // 입력한 텍스트
+ const heightRef = useRef(null); // 입력창 높이 지정
+ const fileInputRef = useRef(null);
+
+ // 입력 내용 반영
+ const handleChange = (e: React.ChangeEvent) => {
+ setInputText(e.target.value);
+ };
+
+ // 입력 내용 전송
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (inputText.trim() !== "") {
+ sendChat(inputText, "text");
+ setInputText(""); // 입력창 내용 초기화
+ if (heightRef.current) {
+ heightRef.current.style.height = "auto"; // 전송 후 입력창 높이 초기화
+ }
+ }
+ };
+
+ // 이미지 업로드
+ const handleImageUpload = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // setUploadedImage(file);
+ sendChat(file, "image");
+ console.log("image ", file);
+ }
+ };
+
+ // PictureIcon 클릭 시 파일 업로드 창 열기
+ const handlePictureIconClick = () => {
+ fileInputRef.current?.click();
+ console.log("click");
+ };
+
+ // 입력 내용 엔터키로 전송 (shift + Enter로 줄바꿈 가능)
+ const handleEnterSubmit = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ }
+ };
+
+ // 입력창 높이 자동 조절
+ const handleInputHeight = () => {
+ if (heightRef.current) {
+ heightRef.current.style.height = "auto"; // 기본 높이
+ heightRef.current.style.height = heightRef.current.scrollHeight + "px"; // 줄바꿈 시 변화
+ }
+ };
+
+ return (
+
+
+
+
+
+ {inputText.trim() === "" ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+export default InputBox;
diff --git a/src/components/ChatRoom/MyChat.tsx b/src/components/ChatRoom/MyChat.tsx
new file mode 100644
index 00000000..a04791c0
--- /dev/null
+++ b/src/components/ChatRoom/MyChat.tsx
@@ -0,0 +1,60 @@
+// component
+import EmotionRemained from "./EmotionRemained";
+
+interface MyChatProps {
+ message: string;
+ isLastChat: boolean;
+ /*감정 관련 */
+ selectedEmotion: number | null;
+}
+
+const MyChat: React.FC = ({
+ message,
+ isLastChat,
+ selectedEmotion,
+}) => {
+ //const imageUrl = file ? URL.createObjectURL(file) : null; // 1회성 url
+ const isImageMessage = message.startsWith("data:image/");
+
+ return (
+ <>
+
+
+ {isImageMessage ? (
+ // 이미지 메시지인 경우
+

+ ) : (
+ // 텍스트 메시지인 경우
+
+ {message}
+
+ )}
+ {selectedEmotion && (
+ <>
+
+
+
+ >
+ )}
+
+
+ >
+ );
+};
+
+export default MyChat;
diff --git a/src/components/ChatRoom/ReceivedChat.tsx b/src/components/ChatRoom/ReceivedChat.tsx
new file mode 100644
index 00000000..21f4f182
--- /dev/null
+++ b/src/components/ChatRoom/ReceivedChat.tsx
@@ -0,0 +1,104 @@
+// components
+import EmotionBox from "./EmotionBox";
+import EmotionRemained from "./EmotionRemained";
+
+// recoil
+import { useRecoilValue } from "recoil";
+import { emotionBoxState } from "../../recoil/atom";
+
+// hooks
+import useHandleEmotionSelect from "../../hooks/useHandleEmotionSelect";
+
+import "animate.css";
+
+interface ReceivedChatProps {
+ message: string;
+ profileImg: string;
+ isLastChat: boolean;
+ /* 감정 달기 관련 */
+ onMouseDown: (e: React.MouseEvent) => void;
+ isSelected: boolean;
+ selectedEmotion: number | null;
+ emotionBoxRef: React.RefObject;
+}
+
+const ReceivedChat: React.FC = ({
+ message,
+ profileImg,
+ isLastChat,
+ /*감정 달기 관련*/
+ onMouseDown,
+ isSelected,
+ selectedEmotion,
+ emotionBoxRef,
+}) => {
+ const showEmotionBox = useRecoilValue(emotionBoxState);
+ const { selectEmotion } = useHandleEmotionSelect();
+
+ const isImageMessage = message.startsWith("data:image/");
+
+ return (
+ <>
+
+ {showEmotionBox && isSelected && (
+
+
+
+ )}
+
+ {isLastChat ? (
+
+ ) : (
+
+ )}
+
+
+ {isImageMessage ? (
+ // 이미지 메시지인 경우
+

+ ) : (
+ // 텍스트 메시지인 경우
+
+ {message}
+
+ )}
+
+ {selectedEmotion && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+ >
+ );
+};
+
+export default ReceivedChat;
diff --git a/src/components/ChatRoom/TopBar.tsx b/src/components/ChatRoom/TopBar.tsx
new file mode 100644
index 00000000..86d2bd6f
--- /dev/null
+++ b/src/components/ChatRoom/TopBar.tsx
@@ -0,0 +1,70 @@
+// icons
+import { ReactComponent as ProfileCircle } from "../../assets/icons/ellipse.svg";
+import { ReactComponent as BackIcon } from "../../assets/icons/back.svg";
+import { ReactComponent as ArrowIcon } from "../../assets/icons/arrow.svg";
+import { ReactComponent as PhoneIcon } from "../../assets/icons/phone.svg";
+import { ReactComponent as VideoIcon } from "../../assets/icons/videocam.svg";
+
+// recoil
+import { userState } from "../../recoil/atom";
+import { useRecoilState } from "recoil";
+import { userInterface } from "../../types/interface";
+import { useNavigate } from "react-router-dom";
+
+const TopBar = () => {
+ const [users, setUsers] = useRecoilState<{
+ me: userInterface;
+ other: userInterface;
+ }>(userState);
+
+ // 유저 전환
+ const handleChangeUser = () => {
+ setUsers((prevState) => ({
+ me: prevState.other,
+ other: prevState.me,
+ }));
+ };
+
+ const navigate = useNavigate();
+
+ return (
+ <>
+
+
navigate(-1)}
+ className="mr-[1.19rem] h-6 w-6 cursor-pointer"
+ />
+
+
+
+

+
+
+
+
+ {users.other.userName}
+
+ {users.other.userId}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TopBar;
diff --git a/src/components/FollowList/FollowContents.tsx b/src/components/FollowList/FollowContents.tsx
new file mode 100644
index 00000000..4e5b7f6d
--- /dev/null
+++ b/src/components/FollowList/FollowContents.tsx
@@ -0,0 +1,21 @@
+import userData from "../../data/UserData.json";
+import Follower from "./Follower";
+
+const FollowContents = () => {
+ const users = userData.users.slice(1);
+
+ return (
+ <>
+
+
+ All followers
+
+ {users.map((user) => (
+
+ ))}
+
+ >
+ );
+};
+
+export default FollowContents;
diff --git a/src/components/FollowList/Follower.tsx b/src/components/FollowList/Follower.tsx
new file mode 100644
index 00000000..9c8ae531
--- /dev/null
+++ b/src/components/FollowList/Follower.tsx
@@ -0,0 +1,69 @@
+import { useNavigate } from "react-router-dom";
+import { userInterface } from "../../types/interface";
+
+// recoil
+import { useRecoilState, useSetRecoilState } from "recoil";
+import {
+ chattingState,
+ currentChattingState,
+ userState,
+} from "../../recoil/atom";
+
+import userData from "../../data/UserData.json";
+
+interface FollowerProps {
+ user: userInterface;
+}
+
+const Follower: React.FC = ({ user }) => {
+ const navigate = useNavigate();
+
+ const [chattings] = useRecoilState(chattingState);
+ const setCurrentChatting = useSetRecoilState(currentChattingState);
+ const setCurrentUsers = useSetRecoilState(userState);
+
+ const gotoChatRoom = () => {
+ const selectedChatting = chattings.find(
+ (chat) => chat.users.includes(0) && chat.users.includes(user.id),
+ );
+
+ if (selectedChatting) {
+ setCurrentChatting(selectedChatting); // 현재 채팅을 지정하고
+ } else {
+ setCurrentChatting(null);
+ }
+ setCurrentUsers(() => ({
+ me: userData.users[0],
+ other: userData.users[user.id],
+ }));
+
+ navigate(`/chatroom/${user.id}`); // 상대방(other)의 id에 따라 동적 라우팅
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {user.userName}
+
+ {user.userId}
+
+
+ gotoChatRoom()}
+ className="h-6 w-[4.4375rem] cursor-pointer rounded-[0.3125rem] bg-Gray200 px-[0.625rem] py-[0.3125rem] text-xs"
+ >
+ Message
+
+
+ >
+ );
+};
+
+export default Follower;
diff --git a/src/components/FollowList/TopBar.tsx b/src/components/FollowList/TopBar.tsx
new file mode 100644
index 00000000..71638430
--- /dev/null
+++ b/src/components/FollowList/TopBar.tsx
@@ -0,0 +1,23 @@
+const TopBar = () => {
+ return (
+ <>
+
+ jngynjng
+
+
+
+ 2.5M follower
+
+
+ 3 following
+
+
+ subscription
+
+
+
+ >
+ );
+};
+
+export default TopBar;
diff --git a/src/components/MyProfile/MyContents.tsx b/src/components/MyProfile/MyContents.tsx
new file mode 100644
index 00000000..88bba4d2
--- /dev/null
+++ b/src/components/MyProfile/MyContents.tsx
@@ -0,0 +1,89 @@
+import { useState } from "react";
+import { ReactComponent as GridIcon } from "../../assets/icons/grid.svg";
+import { ReactComponent as GridIcon2 } from "../../assets/icons/grid2.svg";
+import { ReactComponent as TaggedIcon } from "../../assets/icons/tagged.svg";
+import { ReactComponent as TaggedIcon2 } from "../../assets/icons/tagged2.svg";
+
+import content1 from "../../assets/images/content1.png";
+import content2 from "../../assets/images/content2.png";
+import content3 from "../../assets/images/content3.png";
+import none_tagged from "../../assets/images/none_tagged.png";
+
+const MyContents = () => {
+ const [currentTab, setCurrentTab] = useState(0);
+ const tab = [
+ {
+ id: 1,
+ icon: currentTab == 0 ? : , // 2가 선택되지 않았을 때의 회색 아이콘
+ content: (
+
+ ),
+ },
+ {
+ id: 2,
+ icon: currentTab == 1 ? : ,
+ content: (
+
+

+
+ 내가 태그된 사진과 동영상
+
+
+ 사람들이 회원님을 사진 및 동영상에 태그하면
+
+ 태그된 사진 및 동영상이 여기에 표시됩니다.
+
+
+ ),
+ },
+ ];
+
+ const handleTab = (index: number) => {
+ setCurrentTab(index);
+ };
+
+ return (
+ <>
+
+
+ {tab.map((tab, index) => (
+ handleTab(index)}
+ >
+ {tab.icon}
+
+ ))}
+
+
+ {/*
{tab[currentTab].content}
*/}
+
+
+
{tab[0].content}
+
{tab[1].content}
+
+
+
+ >
+ );
+};
+
+export default MyContents;
diff --git a/src/components/MyProfile/MyInfo.tsx b/src/components/MyProfile/MyInfo.tsx
new file mode 100644
index 00000000..0a802dcf
--- /dev/null
+++ b/src/components/MyProfile/MyInfo.tsx
@@ -0,0 +1,42 @@
+import { useNavigate } from "react-router-dom";
+import { ReactComponent as ProfileImg } from "../../assets/images/profile_img.svg";
+
+const MyInfo = () => {
+ const navigate = useNavigate();
+
+ return (
+ <>
+
+
+
+
+
navigate("/followlist")}
+ className="flex cursor-pointer flex-col items-center"
+ >
+
2.5M
+
Followers
+
+
+
+
+ 장윤정
+
+
+ 안녕하세요!!
+
+
+ Edit Your Profile
+
+
+ >
+ );
+};
+
+export default MyInfo;
diff --git a/src/components/MyProfile/TopBar.tsx b/src/components/MyProfile/TopBar.tsx
new file mode 100644
index 00000000..8caa2b45
--- /dev/null
+++ b/src/components/MyProfile/TopBar.tsx
@@ -0,0 +1,22 @@
+import { ReactComponent as ArrowIcon } from "../../assets/icons/arrow.svg";
+import { ReactComponent as PlusIcon } from "../../assets/icons/plus.svg";
+import { ReactComponent as MenuIcon } from "../../assets/icons/menu.svg";
+
+const TopBar = () => {
+ return (
+ <>
+
+
+ jngynjng
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TopBar;
diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx
new file mode 100644
index 00000000..da0d06d2
--- /dev/null
+++ b/src/components/common/Footer.tsx
@@ -0,0 +1,49 @@
+import { useLocation, useNavigate } from "react-router-dom";
+
+// icons
+import { ReactComponent as HomeIcon } from "../../assets/icons/home.svg";
+import { ReactComponent as SearchIcon } from "../../assets/icons/search.svg";
+import { ReactComponent as MessageIcon } from "../../assets/icons/message.svg";
+import { ReactComponent as MessageIconActive } from "../../assets/icons/message_active.svg";
+import { ReactComponent as ProfileIcon } from "../../assets/icons/profile.svg";
+import { ReactComponent as ProfileIconActive } from "../../assets/icons/profile_active.svg";
+import { ReactComponent as MoreIcon } from "../../assets/icons/more.svg";
+
+const Footer = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const isChatListActive = location.pathname === "/chatlist";
+ return (
+ <>
+
+
+
+ {isChatListActive ? (
+
navigate("/chatlist")}
+ />
+ ) : (
+ navigate("/chatlist")}
+ />
+ )}
+ {isChatListActive ? (
+ navigate("/")}
+ />
+ ) : (
+ navigate("/")}
+ />
+ )}
+
+
+ >
+ );
+};
+
+export default Footer;
diff --git a/src/components/common/StatusBar.tsx b/src/components/common/StatusBar.tsx
new file mode 100644
index 00000000..29cc8ec0
--- /dev/null
+++ b/src/components/common/StatusBar.tsx
@@ -0,0 +1,18 @@
+import { ReactComponent as Info } from "../../assets/icons/info.svg";
+
+const StatusBar = () => {
+ const now = new Date();
+ const hour = now.getHours();
+ const minute = now.getMinutes();
+
+ return (
+
+
+ {hour}:{minute}
+
+
+
+ );
+};
+
+export default StatusBar;
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 00000000..a8dc3cc3
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,12 @@
+declare module "*.svg" {
+ import React = require("react");
+
+ export const ReactComponent: REact.FC>;
+ const src: string;
+ export default src;
+}
+
+declare module "*.png" {
+ const value: string;
+ export default value;
+}
diff --git a/src/data/ChattingData.json b/src/data/ChattingData.json
new file mode 100644
index 00000000..4dbe6b02
--- /dev/null
+++ b/src/data/ChattingData.json
@@ -0,0 +1,82 @@
+{
+ "chattings": [
+ {
+ "id": 1,
+ "users": [0, 1],
+ "chatList": [
+ {
+ "message": "안녕하세요!",
+ "sender": 1,
+ "timestamp": "2024-10-26T10:15:30.000Z"
+ },
+ {
+ "message": "안녕하세용",
+ "sender": 0,
+ "timestamp": "2024-10-26T10:16:45.000Z"
+ },
+ {
+ "message": "잘 부탁드립니다!!",
+ "sender": 1,
+ "timestamp": "2024-10-27T14:20:10.000Z"
+ },
+ {
+ "message": "저는 홍익대학교 산업디자인과 21학번 장윤정이라고 합니다!!!",
+ "sender": 1,
+ "timestamp": "2024-10-28T09:45:50.000Z"
+ },
+ {
+ "message": "안녕하세요 저는 이화여자대학교 컴퓨터공학과 22학번 최지원입니다! 잘 부탁드립니다 :):)",
+ "sender": 0,
+ "timestamp": "2024-10-28T09:46:10.000Z"
+ },
+ {
+ "message": "채팅이 잘 뜨나욥",
+ "sender": 0,
+ "timestamp": "2024-10-29T12:00:05.000Z"
+ },
+ {
+ "message": "네넵 뜹니다 길이 체크용 0000011111110000000000333333300000000000222222222000000000000",
+ "sender": 1,
+ "timestamp": "2024-10-30T16:30:00.000Z"
+ },
+ {
+ "message": "요즘 날씨가 너무 좋아요 그치만 저는 하루종일 코딩해요",
+ "sender": 0,
+ "timestamp": "2024-11-02T01:15:30.000Z"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "users": [0, 2],
+ "chatList": [
+ {
+ "message": "헤어지자고?",
+ "sender": 2,
+ "timestamp": "2024-10-27T11:00:00.000Z"
+ },
+ {
+ "message": "너 누군데",
+ "sender": 2,
+ "timestamp": "2024-11-01T11:05:45.000Z"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "users": [0, 3],
+ "chatList": [
+ {
+ "message": "재료의 익힘정도가",
+ "sender": 0,
+ "timestamp": "2024-10-28T14:22:30.000Z"
+ },
+ {
+ "message": "even하게 되었네요",
+ "sender": 0,
+ "timestamp": "2024-10-28T15:30:00.000Z"
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/data/UserData.json b/src/data/UserData.json
new file mode 100644
index 00000000..e69b6b00
--- /dev/null
+++ b/src/data/UserData.json
@@ -0,0 +1,40 @@
+{
+ "users": [
+ {
+ "id": 0,
+ "userName": "장윤정",
+ "userId": "jngynjng",
+ "profileImg": "profile_img"
+ },
+ {
+ "id": 1,
+ "userName": "최지원",
+ "userId": "jiwwonn__",
+ "profileImg": "profile_img2"
+ },
+ {
+ "id": 2,
+ "userName": "CEOS",
+ "userId": "ceos.shinchon",
+ "profileImg": "ceos_logo"
+ },
+ {
+ "id": 3,
+ "userName": "홍익대학교",
+ "userId": "hongik_university",
+ "profileImg": "hongik_logo"
+ },
+ {
+ "id": 4,
+ "userName": "이화여자대학교",
+ "userId": "ewha.w.univ",
+ "profileImg": "ewha_logo"
+ },
+ {
+ "id": 5,
+ "userName": "NewJeans",
+ "userId": "newjeans_official",
+ "profileImg": "newJeans_logo"
+ }
+ ]
+}
diff --git a/src/hooks/useAutoScroll.tsx b/src/hooks/useAutoScroll.tsx
new file mode 100644
index 00000000..df9fe1aa
--- /dev/null
+++ b/src/hooks/useAutoScroll.tsx
@@ -0,0 +1,21 @@
+import { useEffect, useRef } from "react";
+
+const useAutoScroll = (dependency: any[]) => {
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTo({
+ top: scrollRef.current.scrollHeight, // ref로 전달받은 높이값만큼 top 부여
+ behavior: "smooth",
+ });
+ console.log(scrollRef.current.scrollHeight);
+ }
+ }, 100);
+ }, dependency);
+
+ return scrollRef;
+};
+
+export default useAutoScroll;
diff --git a/src/hooks/useChatSend.tsx b/src/hooks/useChatSend.tsx
new file mode 100644
index 00000000..4e0913df
--- /dev/null
+++ b/src/hooks/useChatSend.tsx
@@ -0,0 +1,79 @@
+import { useRecoilState } from "recoil";
+import { chattingState, userState } from "../recoil/atom";
+import { chattingInterface, chatInterface } from "../types/interface";
+import { useParams } from "react-router-dom";
+
+const useChatSend = () => {
+ const [users, setUsers] = useRecoilState(userState);
+ const [chatting, setChatting] = useRecoilState(chattingState);
+ const { chatroomId } = useParams();
+ const followerId: number = parseInt(chatroomId || "1", 10);
+
+ const currentChatting =
+ chatting.find((chat) => chat.users.includes(followerId)) ||
+ ({
+ id: chatroomId,
+ users: [0, followerId],
+ chatList: [],
+ } as chattingInterface);
+
+ // 파일 객체를 Base64로 변환하는 함수
+ const fileToBase64 = (file: File): Promise => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const base64String = reader.result as string;
+ resolve(base64String);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ };
+
+ // 메시지 전송 함수
+ const sendChat = async (content: string | File, type: "text" | "image") => {
+ let messageContent: string;
+
+ if (type === "image" && content instanceof File) {
+ // 이미지 파일을 Base64로 변환하여 저장
+ messageContent = await fileToBase64(content);
+ } else {
+ messageContent = content as string;
+ }
+
+ const newMessage: chatInterface = {
+ message: messageContent,
+ sender: users.me.id,
+ timestamp: new Date().toISOString(),
+ };
+
+ const updatedChatting = [...currentChatting.chatList, newMessage];
+
+ // setChatting((prevChatting: chattingInterface[]) =>
+ // prevChatting.map((chat) =>
+ // chat.id === currentChatting.id
+ // ? { ...chat, chatList: updatedChatting }
+ // : chat,
+ // ),
+ // );
+ setChatting((prevChatting: chattingInterface[]) =>
+ prevChatting.find((chat) => chat.id === currentChatting.id)
+ ? // 기존 대화 업데이트
+ prevChatting.map((chat) =>
+ chat.id === currentChatting.id
+ ? { ...chat, chatList: updatedChatting }
+ : chat,
+ )
+ : // 새 대화 추가 (새로운 대화를 chattingState에 추가)
+ [...prevChatting, { ...currentChatting, chatList: updatedChatting }],
+ );
+ };
+
+ return {
+ users,
+ currentChatting,
+ sendChat,
+ };
+};
+
+export default useChatSend;
diff --git a/src/hooks/useEmotionBox.tsx b/src/hooks/useEmotionBox.tsx
new file mode 100644
index 00000000..d8e04595
--- /dev/null
+++ b/src/hooks/useEmotionBox.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useRef } from "react";
+
+// recoil
+import { useRecoilState } from "recoil";
+import {
+ emotionBoxState,
+ selectedEmotionsState,
+ selectedMessageState,
+} from "../recoil/atom";
+
+const useEmotionBox = (scrollRef: React.RefObject) => {
+ const [selectedMessage, setSelectedMessage] =
+ useRecoilState(selectedMessageState); // 감정을 달 메시지
+ const [showEmotionBox, setShowEmotionBox] = useRecoilState(emotionBoxState); // 감정 박스 표시 상태
+ const [selectedEmotions, setSelectedEmotions] = useRecoilState(
+ selectedEmotionsState,
+ ); // 메시지별 선택된 감정들
+ const emotionBoxRef = useRef(null); // 감정 박스 ref
+
+ // 길게 눌러서 감정 박스 표시
+ const handleLongPress = (messageId: number, event: React.MouseEvent) => {
+ event.persist(); // 선택한 객체의 비동기 이벤트 처리 (null값 방지)
+
+ const timerId = setTimeout(() => {
+ setSelectedMessage(messageId);
+ setShowEmotionBox(true);
+
+ setTimeout(() => {
+ if (emotionBoxRef.current && scrollRef.current) {
+ const emotionBoxRect = emotionBoxRef.current.getBoundingClientRect();
+ const topBarHeight = 100;
+ const emotionBoxHeight = 64;
+
+ // 감정 박스 상단이 TopBar보다 위에 있으면 스크롤을 조정
+ if (emotionBoxRect.top < topBarHeight) {
+ const offset = topBarHeight - emotionBoxRect.top;
+ scrollRef.current.scrollBy({ top: -offset, behavior: "smooth" });
+ }
+
+ // 가장 상단이라 더 스크롤되지 않는 경우
+ if (emotionBoxRect.top - emotionBoxHeight < topBarHeight) {
+ emotionBoxRef.current.style.top = `${50}px`;
+ } else {
+ // 원래 위치 복귀
+ emotionBoxRef.current.style.top = "-67px";
+ emotionBoxRef.current.style.position = "absolute";
+ }
+ }
+ }, 0);
+ }, 500);
+
+ const handleMouseUp = () => {
+ clearTimeout(timerId);
+ };
+
+ document.addEventListener("mouseup", handleMouseUp, { once: true });
+ };
+
+ return {
+ handleLongPress,
+ emotionBoxRef,
+ };
+};
+
+export default useEmotionBox;
diff --git a/src/hooks/useHandleEmotionSelect.tsx b/src/hooks/useHandleEmotionSelect.tsx
new file mode 100644
index 00000000..fc43d941
--- /dev/null
+++ b/src/hooks/useHandleEmotionSelect.tsx
@@ -0,0 +1,59 @@
+import { useEffect } from "react";
+import { useRecoilState } from "recoil";
+import {
+ emotionBoxState,
+ selectedEmotionsState,
+ selectedMessageState,
+} from "../recoil/atom";
+
+const useHandleEmotionSelect = () => {
+ const [selectedMessage, setSelectedMessage] =
+ useRecoilState(selectedMessageState);
+ const [selectedEmotions, setSelectedEmotions] = useRecoilState(
+ selectedEmotionsState,
+ );
+ const [showEmotionBox, setShowEmotionBox] = useRecoilState(emotionBoxState);
+
+ // 감정 선택
+ const selectEmotion = (emotionId: number) => {
+ if (selectedMessage !== null) {
+ setSelectedEmotions((prevEmotions) => {
+ // 이미 선택된 감정이 동일한 경우 해제
+ if (prevEmotions[selectedMessage] === emotionId) {
+ const updatedEmotions = { ...prevEmotions };
+ delete updatedEmotions[selectedMessage];
+ return updatedEmotions;
+ }
+
+ return {
+ ...prevEmotions,
+ [selectedMessage]: emotionId,
+ };
+ });
+ setShowEmotionBox(false);
+ }
+ };
+
+ // 감정 박스 닫기
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const emotionBoxRef =
+ document.querySelector("#emotionBoxRef");
+
+ if (emotionBoxRef && !emotionBoxRef.contains(event.target as Node)) {
+ setShowEmotionBox(false);
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [setShowEmotionBox]);
+
+ return {
+ selectEmotion,
+ };
+};
+
+export default useHandleEmotionSelect;
diff --git a/src/hooks/useHandleHeight.tsx b/src/hooks/useHandleHeight.tsx
new file mode 100644
index 00000000..ff124218
--- /dev/null
+++ b/src/hooks/useHandleHeight.tsx
@@ -0,0 +1,29 @@
+import { useState, useEffect } from "react";
+import { isMobile } from "react-device-detect";
+
+const useHandleHeight = () => {
+ const [height, setHeight] = useState(isMobile ? "100vh" : "733px");
+
+ useEffect(() => {
+ const handleHeight = () => {
+ if (isMobile) {
+ setHeight(`${window.innerHeight}px`);
+ } else {
+ setHeight(
+ window.innerHeight > 813 ? "733px" : `${window.innerHeight - 80}px`,
+ );
+ }
+ };
+
+ handleHeight(); // 처음 로드 시 높이 설정
+ window.addEventListener("resize", handleHeight); // 창 크기 변경 시 높이 업데이트
+
+ return () => {
+ window.removeEventListener("resize", handleHeight);
+ };
+ }, []);
+
+ return height;
+};
+
+export default useHandleHeight;
diff --git a/src/hooks/useHandleTime.tsx b/src/hooks/useHandleTime.tsx
new file mode 100644
index 00000000..5f49871a
--- /dev/null
+++ b/src/hooks/useHandleTime.tsx
@@ -0,0 +1,40 @@
+interface LastMessageContent {
+ timestamp: string;
+}
+
+const useHandleTime = (lastMessageContent: LastMessageContent) => {
+ const today = new Date();
+ const date = new Date(lastMessageContent.timestamp);
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+
+ // 현재와 비교하여 lastModified 값 설정
+ const isToday =
+ date.getFullYear() === today.getFullYear() &&
+ date.getMonth() === today.getMonth() &&
+ date.getDate() === today.getDate();
+
+ let lastModified;
+
+ if (isToday) {
+ // 현재 시각과의 차이 계산
+ const milliDiff = today.getTime() - date.getTime();
+ const minuteDiff = Math.floor(milliDiff / (1000 * 60));
+ const hourDiff = Math.floor(minuteDiff / 60);
+
+ if (minuteDiff === 0) {
+ lastModified = "now";
+ } else if (minuteDiff < 60) {
+ lastModified = `${minuteDiff}분 전`;
+ } else {
+ lastModified = `${hourDiff}시간 전`;
+ }
+ } else {
+ lastModified = `${month}/${day}`;
+ }
+
+ return { lastModified };
+};
+
+export default useHandleTime;
diff --git a/src/index.css b/src/index.css
index ec2585e8..14356346 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,13 +1,88 @@
-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;
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@font-face {
+ font-family: "SF-Pro";
+ src: url("./assets/fonts/SF-Pro.ttf");
+}
+@font-face {
+ font-family: "SF-Pro-Text-Regular";
+ src: url("./assets/fonts/SF-Pro-Text-Regular.otf");
+}
+@font-face {
+ font-family: "SF-Pro-Text-Bold";
+ src: url("./assets/fonts/SF-Pro-Text-Bold.otf");
+}
+
+@layer base {
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ /* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif; */
+ font-family: "SF-Pro";
+ display: flex;
+ justify-content: center;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ max-width: 375px;
+ margin: auto;
+ background-color: #f2f2f2;
+ }
+
+ button {
+ border: none;
+ background-color: transparent;
+ }
+
+ input {
+ border: transparent;
+ background: transparent;
+ }
+
+ input:focus {
+ outline: none;
+ }
+
+ img {
+ -webkit-user-drag: none;
+ -khtml-user-drag: none;
+ -moz-user-drag: none;
+ -o-user-drag: none;
+ user-drag: none;
+ }
+
+ /* width */
+ ::-webkit-scrollbar {
+ width: 5px;
+ }
+
+ /* Track */
+ ::-webkit-scrollbar-track {
+ background: #fff;
+ border-radius: 5px;
+ }
+
+ /* Handle */
+ ::-webkit-scrollbar-thumb {
+ background: #fff;
+ border-radius: 5px;
+ }
+
+ /* Handle on hover */
+ ::-webkit-scrollbar-thumb:hover {
+ background: #dfe2e5;
+ }
+
+ /* code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
+ monospace;
+ } */
}
diff --git a/src/pages/ChatListPage.tsx b/src/pages/ChatListPage.tsx
new file mode 100644
index 00000000..653ee851
--- /dev/null
+++ b/src/pages/ChatListPage.tsx
@@ -0,0 +1,17 @@
+import TopBar from "../components/ChatList/TopBar";
+import ChatListContents from "../components/ChatList/ChatListContents";
+import Footer from "../components/common/Footer";
+
+const ChatListPage = () => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default ChatListPage;
diff --git a/src/pages/ChatRoomPage.tsx b/src/pages/ChatRoomPage.tsx
new file mode 100644
index 00000000..af832c02
--- /dev/null
+++ b/src/pages/ChatRoomPage.tsx
@@ -0,0 +1,43 @@
+import { useEffect } from "react";
+import { useParams } from "react-router-dom";
+
+// components
+import TopBar from "../components/ChatRoom/TopBar";
+import Chattings from "../components/ChatRoom/Chattings";
+
+import userData from "../data/UserData.json";
+
+// recoil
+import { useRecoilValue, useSetRecoilState } from "recoil";
+import { emotionBoxState, userState } from "../recoil/atom";
+
+const ChatRoomPage = () => {
+ const { chatroomId } = useParams(); // 경로로부터 chatroom id
+ const followerId: number = parseInt(chatroomId || "1", 10);
+ const showEmotionBox = useRecoilValue(emotionBoxState); // 배경 블러처리를 위한 감정 박스 표시 상태
+
+ const setCurrentUsers = useSetRecoilState(userState);
+
+ useEffect(() => {
+ setCurrentUsers(() => ({
+ me: userData.users[0],
+ other: userData.users[followerId],
+ }));
+ }, [followerId, setCurrentUsers]);
+
+ return (
+ <>
+
+
+
+
+
+ {showEmotionBox && ( // 블러 배경
+
+ )}
+
+ >
+ );
+};
+
+export default ChatRoomPage;
diff --git a/src/pages/FollowListPage.tsx b/src/pages/FollowListPage.tsx
new file mode 100644
index 00000000..5a114ba8
--- /dev/null
+++ b/src/pages/FollowListPage.tsx
@@ -0,0 +1,17 @@
+import TopBar from "../components/FollowList/TopBar";
+import FollowContents from "../components/FollowList/FollowContents";
+import Footer from "../components/common/Footer";
+
+const FollowListPage = () => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default FollowListPage;
diff --git a/src/pages/MyProfilePage.tsx b/src/pages/MyProfilePage.tsx
new file mode 100644
index 00000000..2974f121
--- /dev/null
+++ b/src/pages/MyProfilePage.tsx
@@ -0,0 +1,19 @@
+import TopBar from "../components/MyProfile/TopBar";
+import MyInfo from "../components/MyProfile/MyInfo";
+import MyContents from "../components/MyProfile/MyContents";
+import Footer from "../components/common/Footer";
+
+const MyProfilePage = () => {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export default MyProfilePage;
diff --git a/src/recoil/atom.ts b/src/recoil/atom.ts
new file mode 100644
index 00000000..dc14b20b
--- /dev/null
+++ b/src/recoil/atom.ts
@@ -0,0 +1,48 @@
+import { atom } from "recoil";
+import userData from "../data/UserData.json";
+import chattingData from "../data/ChattingData.json";
+import { recoilPersist } from "recoil-persist";
+import type { chattingInterface, userInterface } from "../types/interface";
+
+const { persistAtom } = recoilPersist({
+ key: "chattings",
+ storage: localStorage,
+});
+
+export const userState = atom<{ me: userInterface; other: userInterface }>({
+ key: "userState",
+ default: {
+ me: userData.users[0],
+ other: userData.users[1],
+ },
+});
+
+export const chattingState = atom({
+ key: "chattingState",
+ default: chattingData.chattings,
+ effects_UNSTABLE: [persistAtom],
+});
+
+export const currentChattingState = atom({
+ key: "currentChattingState",
+ default: null,
+ effects_UNSTABLE: [persistAtom],
+});
+
+export const emotionBoxState = atom({
+ // 감정 박스 보이는지 상태
+ key: "emotionBoxState",
+ default: false,
+});
+
+// useEmotionBox
+export const selectedMessageState = atom({
+ key: "selectedMessageState",
+ default: null,
+});
+
+export const selectedEmotionsState = atom<{ [key: number]: number }>({
+ key: "selectedEmotionsState",
+ default: {},
+ effects_UNSTABLE: [persistAtom],
+});
diff --git a/src/types/interface.ts b/src/types/interface.ts
new file mode 100644
index 00000000..b2d66f6d
--- /dev/null
+++ b/src/types/interface.ts
@@ -0,0 +1,18 @@
+export interface userInterface {
+ id: number;
+ userName: string;
+ userId: string;
+ profileImg: string;
+}
+
+export interface chatInterface {
+ message: string;
+ sender: number;
+ timestamp: string;
+}
+
+export interface chattingInterface {
+ id: number | undefined;
+ users: number[];
+ chatList: chatInterface[];
+}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..2a98b446
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,27 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["./src/**/*.{js,jsx,ts,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ Chat_BG: "#378DEF",
+ Alert: "#ff0000",
+ Gray100: "#F9FAFB",
+ Gray200: "#F2F4F6",
+ Gray300: "#DFE2E5",
+ Gray400: "#B0B8C1",
+ Gray500: "#8B95A1",
+ Gray600: "#6B7684",
+ Gray700: "#4E5968",
+ Gray800: "#333D48",
+ Gray900: "#121212",
+ },
+ fontFamily: {
+ "SF-Pro": ["SF-Pro"],
+ "SF-Pro-Text-Regular": ["SF-Pro-Text-Regular"],
+ "SF-Pro-Text-Bold": ["SF-Pro-Text-Bold"],
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/tsconfig.json b/tsconfig.json
index a273b0cf..6f1f4321 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -18,9 +14,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
- "jsx": "react-jsx"
+ "jsx": "react-jsx",
+ "noUnusedLocals": false
},
- "include": [
- "src"
- ]
+ "include": ["src/**/*"]
}