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 ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} - -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