diff --git a/README.md b/README.md index 6ab8e98..3582bf2 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,56 @@ -# 서론 +# 4주차 미션: React-Messenger 💌 -안녕하세요 🙌🏻 18기 프론트 운영진 김문기입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다. +## 서론 -이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다. +안녕하세요 🙌🏻 프론트엔드 18기 신동현입니다. -동시에, 이번주부터는 새로 **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..3d83b6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,19 @@ "@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", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.28", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", + "styled-components": "^6.0.8", + "styled-reset": "^4.5.1", + "typescript": "^5.2.2", "web-vitals": "^2.1.4" } }, @@ -53,6 +63,83 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/cli": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.0.tgz", + "integrity": "sha512-17E1oSkGk2IwNILM4jtfAvgjt+ohmpfBky8aLerUfYZhiPNg7ca+CRCxZn8QDxwNhV/upsc2VHBCqGFIR+iBfA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "commander": "^4.0.1", + "convert-source-map": "^2.0.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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "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 +617,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 +697,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 +2390,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 +3270,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 +3385,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", @@ -3559,104 +3711,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@testing-library/dom": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", - "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "peer": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "peer": true - }, - "node_modules/@testing-library/dom/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4006,6 +4060,20 @@ "@types/node": "*" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4297,9 +4365,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 +4400,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,13 +4410,32 @@ } }, "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": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", "dependencies": { + "@types/history": "^4.7.11", "@types/react": "*" } }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4412,6 +4499,21 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "node_modules/@types/styled-components": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.28.tgz", + "integrity": "sha512-nu0VKNybkjvUqJAXWtRqKd7j3iRUl8GbYSTvZNuIBJcw/HUp1Y4QUXNLlj7gcnRV/t784JnHAlvRnSnE3nPbJA==", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stylis": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.1.tgz", + "integrity": "sha512-OSaMrXUKxVigGlKRrET39V2xdhzlztQ9Aqumn1WbCBKHOi9ry7jKSd7rkyj0GzmWaU960Rd+LpOFpLfx5bMQAg==" + }, "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 +5928,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 +6371,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 +6560,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", @@ -8549,6 +8677,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", @@ -8904,6 +9037,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -14686,6 +14832,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", @@ -15513,6 +15689,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", @@ -16009,6 +16190,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 +16259,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 +16893,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": { diff --git a/package.json b/package.json index 49b3308..3cb8e36 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,19 @@ "@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", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.28", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", + "styled-components": "^6.0.8", + "styled-reset": "^4.5.1", + "typescript": "^5.2.2", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/public/index.html b/public/index.html index aa069f2..9aaf4d4 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + CEOS Messenger service 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.tsx similarity index 100% rename from src/App.test.js rename to src/App.test.tsx diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..06e1004 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,29 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import Chat from "./pages/chat/chat"; +import ChatList from "./pages/chatlist/chatList"; +import Profile from "./pages/profile/profile"; +import Friends from "./pages/friends/friends"; +import StatusBar from "./components/StatusBar/statusbar"; + +import chatData from "./assets/datas/chatdata.json"; +import userData from "./assets/datas/userdata.json"; + +function App() { + return ( +
+ + {/* */} + + } /> + } /> + } /> + } /> + } /> + + +
+ ); +} + +export default App; diff --git a/src/assets/datas/chatdata.json b/src/assets/datas/chatdata.json new file mode 100644 index 0000000..be26c84 --- /dev/null +++ b/src/assets/datas/chatdata.json @@ -0,0 +1,118 @@ +{"2":[ + { + "id": 0, + "sender": "신동현", + "content": "ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + + + }, + { + "id": 1, + "sender": "신동현", + "content": "아니 나 졸작", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 2, + "sender": "신동현", + "content": "작업하는데 천이 부족해서", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 3, + "sender": "신동현", + "content": "동대문 들렀다가 집 감", + "showIcon": true, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 4, + "sender": "이예진", + "content": "그것 참 불쌍하게 되었군", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 5, + "sender": "이예진", + "content": "동대문에서 집까지", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 6, + "sender": "이예진", + "content": "한 시간 넘게 걸리지 않아?", + "showIcon": true, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 7, + "sender": "신동현", + "content": "한 시간 반? 정도", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 8, + "sender": "신동현", + "content": "그렇게 멀지는 않은데", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 9, + "sender": "신동현", + "content": "짐이 많아서 힘든 겨", + "showIcon": true, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 10, + "sender": "이예진", + "content": "이따가 갈 때", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 11, + "sender": "이예진", + "content": "심심하면 영통하자", + "showIcon": true, + "timestamp": "2023-11-01T04:29:17.011Z" + }, + { + "id": 12, + "sender": "신동현", + "content": "오키 알겠음\n그럼 이따가 연락할게", + "showIcon": false, + "timestamp": "2023-11-01T04:29:17.011Z" + }], +"5":[ + { + "id": 0, + "sender": "신동현", + "content": "안녕하세여", + "showIcon": false, + "timestamp": "2023-11-03T07:44:53.152Z" + }, + { + "id": 1, + "sender": "신동현", + "content": "ㅎㅎㅎ", + "showIcon": false, + "timestamp": "2023-11-03T07:44:55.011Z" + }, + { + "id": 2, + "sender": "고세희 선배님", + "content": "ㅎㅇㅎㅇ", + "showIcon": false, + "timestamp": "2023-11-03T07:45:45.536Z" + } +] +} \ No newline at end of file diff --git a/src/assets/datas/userdata.json b/src/assets/datas/userdata.json new file mode 100644 index 0000000..6b86792 --- /dev/null +++ b/src/assets/datas/userdata.json @@ -0,0 +1,74 @@ +{ + "users": [ + { + "id": 1, + "name": "신동현", + "instagram":"@s_d0nghyun", + "status":"" + + }, + { + "id": 2, + "name": "이예진", + "instagram":"@leebyvae", + "status":"" + + }, + { + "id": 3, + "name": "강은정", + "instagram":"@0o0go", + "status":"👍" + + },{ + "id": 4, + "name": "강은비", + "instagram":"@siverrainy_", + "status":"날씨 완전 가을🍁" + + },{ + "id": 5, + "name": "고세희 선배님", + "instagram":"@saysehi", + "status":"" + + },{ + "id": 6, + "name": "곽수연", + "instagram":"@jusdjkl", + "status":"우하하하" + + },{ + "id": 7, + "name": "권은수", + "instagram":"@jusdjkl", + "status":"" + + },{ + "id": 8, + "name": "김진솔", + "instagram":"@jusdjkl", + "status":"가을가을가을" + + },{ + "id": 9, + "name": "김가영", + "instagram":"@odeegayo", + "status":"" + + },{ + "id": 10, + "name": "김하은 오빠", + "instagram":"@ggksenvl", + "status":"" + + },{ + "id":11, + "name": "문지영 언니", + "instagram":"@m00nzi0", + "status":"공부?" + + } + ] + } + diff --git a/src/assets/images/App-Store.svg b/src/assets/images/App-Store.svg new file mode 100644 index 0000000..c0855ad --- /dev/null +++ b/src/assets/images/App-Store.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Arrow.svg b/src/assets/images/Arrow.svg new file mode 100644 index 0000000..41cece8 --- /dev/null +++ b/src/assets/images/Arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/BackIcon.svg b/src/assets/images/BackIcon.svg new file mode 100644 index 0000000..01ab4ed --- /dev/null +++ b/src/assets/images/BackIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Battery.svg b/src/assets/images/Battery.svg new file mode 100644 index 0000000..4a45d70 --- /dev/null +++ b/src/assets/images/Battery.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/BigIcon.svg b/src/assets/images/BigIcon.svg new file mode 100644 index 0000000..06ac6c6 --- /dev/null +++ b/src/assets/images/BigIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/BigIconLogo.svg b/src/assets/images/BigIconLogo.svg new file mode 100644 index 0000000..668378c --- /dev/null +++ b/src/assets/images/BigIconLogo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/Camera.svg b/src/assets/images/Camera.svg new file mode 100644 index 0000000..916dbf3 --- /dev/null +++ b/src/assets/images/Camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Cellular Connection.svg b/src/assets/images/Cellular Connection.svg new file mode 100644 index 0000000..ebe9186 --- /dev/null +++ b/src/assets/images/Cellular Connection.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Dictation.svg b/src/assets/images/Dictation.svg new file mode 100644 index 0000000..7e30da1 --- /dev/null +++ b/src/assets/images/Dictation.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Edit.svg b/src/assets/images/Edit.svg new file mode 100644 index 0000000..ed9701c --- /dev/null +++ b/src/assets/images/Edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Facetime.svg b/src/assets/images/Facetime.svg new file mode 100644 index 0000000..3a6feb9 --- /dev/null +++ b/src/assets/images/Facetime.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/Friends.svg b/src/assets/images/Friends.svg new file mode 100644 index 0000000..dc73793 --- /dev/null +++ b/src/assets/images/Friends.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/images/Group.svg b/src/assets/images/Group.svg new file mode 100644 index 0000000..f04ec64 --- /dev/null +++ b/src/assets/images/Group.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/LeftArrow.svg b/src/assets/images/LeftArrow.svg new file mode 100644 index 0000000..0c1506b --- /dev/null +++ b/src/assets/images/LeftArrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/LightBottomBar.svg b/src/assets/images/LightBottomBar.svg new file mode 100644 index 0000000..2553c20 --- /dev/null +++ b/src/assets/images/LightBottomBar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/LightStatusBar.svg b/src/assets/images/LightStatusBar.svg new file mode 100644 index 0000000..24e6034 --- /dev/null +++ b/src/assets/images/LightStatusBar.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/Message Numbers.svg b/src/assets/images/Message Numbers.svg new file mode 100644 index 0000000..6fe3f42 --- /dev/null +++ b/src/assets/images/Message Numbers.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/SearchIcon.svg b/src/assets/images/SearchIcon.svg new file mode 100644 index 0000000..a8b2328 --- /dev/null +++ b/src/assets/images/SearchIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Searchbars.svg b/src/assets/images/Searchbars.svg new file mode 100644 index 0000000..e1f0272 --- /dev/null +++ b/src/assets/images/Searchbars.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/images/Time Style.svg b/src/assets/images/Time Style.svg new file mode 100644 index 0000000..4f4cd9d --- /dev/null +++ b/src/assets/images/Time Style.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Time.svg b/src/assets/images/Time.svg new file mode 100644 index 0000000..4c781ef --- /dev/null +++ b/src/assets/images/Time.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/User.svg b/src/assets/images/User.svg new file mode 100644 index 0000000..f04ec64 --- /dev/null +++ b/src/assets/images/User.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Vector (1).svg b/src/assets/images/Vector (1).svg new file mode 100644 index 0000000..d41faaf --- /dev/null +++ b/src/assets/images/Vector (1).svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Vector.svg b/src/assets/images/Vector.svg new file mode 100644 index 0000000..225d492 --- /dev/null +++ b/src/assets/images/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/Wifi.svg b/src/assets/images/Wifi.svg new file mode 100644 index 0000000..c00f1c7 --- /dev/null +++ b/src/assets/images/Wifi.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/back.svg b/src/assets/images/back.svg new file mode 100644 index 0000000..cd3366b --- /dev/null +++ b/src/assets/images/back.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/backArrow.svg b/src/assets/images/backArrow.svg new file mode 100644 index 0000000..6ec2b83 --- /dev/null +++ b/src/assets/images/backArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/bubbleStatusImg.svg b/src/assets/images/bubbleStatusImg.svg new file mode 100644 index 0000000..c0575ca --- /dev/null +++ b/src/assets/images/bubbleStatusImg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/chatBackArrow.svg b/src/assets/images/chatBackArrow.svg new file mode 100644 index 0000000..40a5259 --- /dev/null +++ b/src/assets/images/chatBackArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/github.svg b/src/assets/images/github.svg new file mode 100644 index 0000000..dd1591b --- /dev/null +++ b/src/assets/images/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/instagram.svg b/src/assets/images/instagram.svg new file mode 100644 index 0000000..4a05ea4 --- /dev/null +++ b/src/assets/images/instagram.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/record.svg b/src/assets/images/record.svg new file mode 100644 index 0000000..ea6718a --- /dev/null +++ b/src/assets/images/record.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/rightarrowIcon.svg b/src/assets/images/rightarrowIcon.svg new file mode 100644 index 0000000..41cece8 --- /dev/null +++ b/src/assets/images/rightarrowIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/sendMessage.svg b/src/assets/images/sendMessage.svg new file mode 100644 index 0000000..aff31f1 --- /dev/null +++ b/src/assets/images/sendMessage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/video.svg b/src/assets/images/video.svg new file mode 100644 index 0000000..0071623 --- /dev/null +++ b/src/assets/images/video.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/write.svg b/src/assets/images/write.svg new file mode 100644 index 0000000..2f6a79a --- /dev/null +++ b/src/assets/images/write.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/ChatInput/chatinput.tsx b/src/components/ChatInput/chatinput.tsx new file mode 100644 index 0000000..d31b715 --- /dev/null +++ b/src/components/ChatInput/chatinput.tsx @@ -0,0 +1,153 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +//images +import cameraIcon from "../../assets/images/Camera.svg"; +import appStoreIcon from "../../assets/images/App-Store.svg"; +import dictationIcon from "../../assets/images/Dictation.svg"; +import sendIcon from "../../assets/images/sendMessage.svg"; +//채팅 입력받는 component + +interface ChatInputProps { + onSend: (message: string) => void; +} + +const ChatInput: React.FC = ({ onSend }) => { + const [inputValue, setInputValue] = useState(""); + const [isInputFocused, setIsInputFocused] = useState(false); + + //여러줄 입력 위해 input=> textarea로 변경 + const handleInputEnter = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (inputValue.trim() !== "") { + // 입력값이 공백인 경우 예외 처리 + onSend(inputValue); + } + // 입력값 초기화 + setIsInputFocused(false); + setInputValue(""); + } + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + setInputValue((prev) => prev + "\n"); + } + }; + + const handleSendClick = () => { + console.log("Send button clicked"); // Add this line for debugging + if (inputValue.trim() !== "") { + onSend(inputValue); + } + setInputValue(""); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + setIsInputFocused(e.target.value !== ""); // isInputFocused true로 설정 + }; + + return ( + + {!isInputFocused && } + {!isInputFocused && } + + + setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + required + /> + {isInputFocused ? ( + //handleSendClick 작동?? click 작동 + + ) : ( + + )} + + + ); +}; + +const BottomBarContainer = styled.div` + display: flex; + align-items: center; + gap: 1.38rem; + position: relative; + width: 100%; + height: 2.5rem; + margin-left: 1.38rem; + /* margin-bottom: 0.81rem; */ +`; + +const Camera = styled.img` + width: 1.92713rem; + height: 1.51313rem; + /* margin-left: 1.06rem; */ +`; + +const AppStore = styled.img` + width: 2.18363rem; + height: 1.58331rem; +`; + +const Input = styled.textarea` + width: 100%; + font-family: "SF Pro Text"; + font-size: 0.9375rem; + margin-right: 1.03rem; + height: 2.125rem; + border: 1px solid var(--gray-3); + outline: none; + padding-left: 0.84rem; + border-radius: 1.875rem; + resize: none; + word-break: break-all; + overflow-y: auto; //스크롤바 + padding-top: 0.5rem; + &:focus { + box-shadow: none; + } + &::placeholder { + display: flex; + align-items: center; + } + + padding-right: 2rem; //input 길어질때 방지 하기 위해 +`; + +const InputContainer = styled.span` + display: flex; + align-items: center; + flex-grow: 1; + width: 90%; +`; + +const Dictation = styled.img` + width: 1.6875rem; + height: 1.6875rem; + position: absolute; //input fiield 안에 위치 + right: 1.25rem; +`; + +const Send = styled.button` + width: 1.6875rem; + height: 1.6875rem; + background: url(${sendIcon}) no-repeat center center; + border: none; + position: absolute; //input fiield 안에 위치 + z-index: 10; + right: 1.25rem; + cursor: pointer; + + &:focus { + outline: none; + } +`; + +export default ChatInput; diff --git a/src/components/ChatTitle/chatTitle.tsx b/src/components/ChatTitle/chatTitle.tsx new file mode 100644 index 0000000..848ca1d --- /dev/null +++ b/src/components/ChatTitle/chatTitle.tsx @@ -0,0 +1,84 @@ +import styled from "styled-components"; +import arrowIcon from "../../assets/images/chatBackArrow.svg"; +import userIcon from "../../assets/images/User.svg"; +import facetime from "../../assets/images/Facetime.svg"; +import { useNavigate } from "react-router-dom"; + +interface ChatHeaderProps { + chatName: string; + chatID: string; + changeUser?: () => void; +} +const ChatTitle: React.FC = ({ + chatName, + chatID, + changeUser, +}) => { + const navigate = useNavigate(); + return ( + + navigate("/chatlist")} + /> + + + {chatName} + {chatID} + + + + ); +}; + +const ChatTitleContainer = styled.div` + display: flex; + align-items: center; + position: relative; + width: 100%; + height: 2.5rem; + padding-bottom: 0.7rem; + border-bottom: solid rgba(144, 144, 147, 0.5); +`; +const UserSection = styled.div` + display: flex; + flex-direction: column; + width: 100%; + &:hover { + cursor: pointer; + } +`; +const UserName = styled.div` + color: var(--black); + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: normal; + word-wrap: break-word; +`; +const InstagramID = styled.div` + color: var(--gray-1); + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 125%; /* 0.9375rem */ +`; +const ArrowIcon = styled.img` + width: 2.5rem; + height: 2.5rem; +`; + +const UserIcon = styled.img` + width: 2.06244rem; + height: 2.06238rem; + margin-right: 0.64rem; + &:hover { + cursor: pointer; + } +`; +const Facetime = styled.img` + margin-right: 1.12rem; +`; + +export default ChatTitle; diff --git a/src/components/ChatlistItem/chatlistitem.tsx b/src/components/ChatlistItem/chatlistitem.tsx new file mode 100644 index 0000000..7fdf814 --- /dev/null +++ b/src/components/ChatlistItem/chatlistitem.tsx @@ -0,0 +1,149 @@ +import styled from "styled-components"; +//images +import groupIcon from "../../assets/images/Group.svg"; +import rightarrowIcon from "../../assets/images/rightarrowIcon.svg"; +interface Message { + id: number; + sender: string; + content: string; + showIcon: boolean; + timestamp: string; //메시지 보내는 시간 정보 추가 + unread?: boolean; // 읽지 않은 메시지인지 여부를 나타내는 속성 추가 +} +interface User { + id: number; + name: string; +} + +interface ChatListItemProps { + key: number; + user: User; + lastMessage: Message | null; + onClick: () => void; +} +function formatTimestamp(isoString?: string) { + if (!isoString) return "No last message"; + + const date = new Date(isoString); + date.setHours(0, 0, 0, 0); + + const now = new Date(); + now.setHours(0, 0, 0, 0); + + const diffTime = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + // 날짜 차이에 따른 문자열 반환 + //오늘은 시간 정보, 전날은 "어제", 그 전날은 '~일전'으로 반환해줌 + if (diffDays === 0) { + const date = new Date(isoString); + let hours = date.getHours(); + const minutes = date.getMinutes(); + const ampm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12; + hours = hours ? hours : 12; + const strHours = hours < 10 ? `0${hours}` : `${hours}`; + const strMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`; + return `${strHours}:${strMinutes} ${ampm}`; + } else if (diffDays === 1) { + return "어제"; + } else { + return `${diffDays}일 전`; + } +} + +const ChatListItem: React.FC = ({ + user, + lastMessage, + onClick, +}) => { + return ( + + {lastMessage?.unread && } + + + + {user.name} + + + + + + {lastMessage?.content || ""} + + + ); +}; + +const ChatlistItem = styled.div` + width: 100%; + height: 5.25rem; + display: flex; + padding: 0.81rem 1.56rem; + position: relative; + + &::after { + content: ""; + position: absolute; + left: 5.1rem; + right: 0; + bottom: 0; + border-bottom: 1px solid var(--gray-1); + } +`; + +const Time = styled.div` + font-family: "SF Pro Text"; + font-size: 0.9375rem; + font-style: normal; + font-weight: 400; + line-height: normal; + color: var(--gray-1); +`; +const ChatlistData = styled.div` + width: 100%; + display: flex; + flex-direction: column; + padding-left: 0.87rem; +`; +const ChatlistDataTitle = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.25rem; +`; + +const TimeContainer = styled.div` + display: flex; +`; +const UnreadIndicator = styled.div` + position: absolute; + top: 50%; + left: 0.8rem; + width: 0.56rem; + height: 0.56rem; + border-radius: 50%; + background-color: var(--blue); +`; +const GroupIcon = styled.img` + margin-left: 0.2rem; +`; +const Name = styled.span` + font-family: "SF Pro Text"; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: normal; +`; +const LastMessage = styled.div` + /* width: 17.25rem; */ + height: 2.25rem; + color: var(--gray-1); + font-family: "SF Pro Text"; + font-size: 0.9375rem; + font-style: normal; + font-weight: 400; + line-height: normal; +`; +export default ChatListItem; diff --git a/src/components/Message/message.tsx b/src/components/Message/message.tsx new file mode 100644 index 0000000..d9f189c --- /dev/null +++ b/src/components/Message/message.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import styled from "styled-components"; +import userIcon from "../../assets/images/User.svg"; + +interface MessageProps { + sender: string; + content: string; + nowUser: string; + showIcon: boolean; +} + +const Message: React.FC = ({ + sender, + content, + nowUser, + showIcon, +}) => { + return ( + + {showIcon && } + + {content} + + + ); +}; + +const MessageContainer = styled.div<{ sender: string; nowUser: string }>` + display: flex; + align-items: center; + margin: 0.44rem 0; + max-width: 100%; + flex-direction: ${(props) => + props.sender === props.nowUser ? "row-reverse" : "row"}; + //왼, 오른쪽 기준 나누기 +`; + +const MessageItem = styled.div<{ + sender: string; + content: string; + nowUser: string; + showIcon: boolean; +}>` + /* width: 100%; */ + padding: 0.7rem; + border-radius: 1.25rem; + margin-right: 0.69rem; + background-color: ${({ sender, nowUser }) => + sender === nowUser ? "var(--blue)" : "rgba(118, 118, 128, 0.12)"}; + color: ${({ sender, nowUser }) => + sender === nowUser ? "#FFFFFF" : "#000000"}; + // showIcon가 false인 경우 margin-left를 한방에 설정 + margin-left: ${(msg) => + !msg.showIcon && msg.sender !== msg.nowUser ? "3.75rem" : "0"}; + max-width: 90%; // 데이터가 길어질 경우 + font-size: 0.9375rem; + font-weight: 400; + line-height: normal; + box-sizing: border-box; + word-break: break-all; + white-space: pre-line; //띄어쓰기도 인식할 수 있게 +`; + +const UserIconShow = styled.img` + width: 2.75rem; + height: 2.06238rem; + margin-left: 1rem; +`; + +export default Message; diff --git a/src/components/Message/messageList.tsx b/src/components/Message/messageList.tsx new file mode 100644 index 0000000..778639d --- /dev/null +++ b/src/components/Message/messageList.tsx @@ -0,0 +1,34 @@ +import React, { useRef, useEffect } from "react"; +import styled from "styled-components"; + +interface MessageListProps { + children: React.ReactNode; +} + +//메시지 리스트 스크롤 관리 + +const MessageList: React.FC = ({ children }) => { + const scrollRef = useRef(null); + + useEffect(() => { + // 현재 스크롤 위치 :scrollRef.current.scrollTop + // 스크롤 길이 : scrollRef.current.scrollHeight + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [children]); + + return ( + {children} + ); +}; + +const MessageListContainer = styled.div` + overflow-y: auto; + width: 100%; + max-height: 100%; + flex: 1; + scrollbar-width: none; +`; + +export default MessageList; diff --git a/src/components/SearchBar/serachbar.tsx b/src/components/SearchBar/serachbar.tsx new file mode 100644 index 0000000..1f75925 --- /dev/null +++ b/src/components/SearchBar/serachbar.tsx @@ -0,0 +1,68 @@ +// searchIcon & recording + +import styled from "styled-components"; +import { ChangeEvent } from "react"; +import searchIcon from "../../assets/images/SearchIcon.svg"; +import recordIcon from "../../assets/images/record.svg"; + +interface SearchBarProps { + value: string; + onChange: (event: ChangeEvent) => void; +} +const SearchBar: React.FC = ({ value, onChange }) => { + return ( + + + + + + ); +}; + +const InputContainer = styled.span` + display: flex; + align-items: center; + width: 100%; + /* height: 2.25rem; */ + padding: 0 1rem; + font-family: "SF Pro Text"; +`; +const Input = styled.input` + width: 100%; + font-family: "SF Pro Text"; + height: 2.25rem; + padding-left: 2.87rem; + background: var(--gray-2); + &:focus { + box-shadow: none; + } + outline: none; + border: none; + border-radius: 0.5rem; +`; + +const Search = styled.img` + position: absolute; + font-size: 1.0625rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.017rem; + left: 1.5rem; +`; + +const Record = styled.img` + position: absolute; + font-size: 1.0625rem; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.017rem; + right: 1.63rem; +`; +export default SearchBar; diff --git a/src/components/StatusBar/statusbar.tsx b/src/components/StatusBar/statusbar.tsx new file mode 100644 index 0000000..15d2296 --- /dev/null +++ b/src/components/StatusBar/statusbar.tsx @@ -0,0 +1,58 @@ +import styled from "styled-components"; +import timeIcon from "../../assets/images/Time Style.svg"; +import cellularIcon from "../../assets/images/Cellular Connection.svg"; +import wifiIcon from "../../assets/images/Wifi.svg"; +import batteryIcon from "../../assets/images/Battery.svg"; + +const StatusBar = () => { + return ( + + + + + + + + + ); +}; + +const StatusBarContainer = styled.div` + width: 100%; + height: 2.75rem; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; +`; +const TimeIcon = styled.img` + width: 3.375rem; + height: 2.3125rem; + padding-bottom: 0.6rem; + margin-left: 0.4rem; +`; + +const CellularIcon = styled.img` + width: 1.0625rem; + height: 0.66669rem; +`; +const WifiIcon = styled.img` + width: 0.95831rem; + height: 0.6875rem; +`; +const BatteryIcon = styled.img` + width: 1.5205rem; + height: 0.70831rem; + margin-right: 1.12rem; +`; + +const StatusBarRight = styled.div` + display: inline-flex; + align-items: center; + gap: 0.31rem; + left: 18.37rem; + right: 0.9rem; + /* top: 1.08rem; + bottom: 0.96rem; */ +`; +export default StatusBar; diff --git a/src/components/TopBar/topbar.tsx b/src/components/TopBar/topbar.tsx new file mode 100644 index 0000000..4f3a437 --- /dev/null +++ b/src/components/TopBar/topbar.tsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; +import StatusBar from "../StatusBar/statusbar"; +import ChatTitle from "../ChatTitle/chatTitle"; +import backArrow from "../../assets/images/backArrow.svg"; +import backText from "../../assets/images/BackIcon.svg"; +import editIcon from "../../assets/images/Edit.svg"; +import { useNavigate } from "react-router-dom"; +const TopBar = () => { + const navigate = useNavigate(); + const handleBackClick = () => { + navigate("/profile"); + }; + return ( + +
+ + +
+ +
+ ); +}; +const TopBarContainer = styled.div` + width: 100%; + height: 1.25rem; + padding: 0rem 1.12rem; + margin-top: 0.81rem; + margin-bottom: 1rem; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; +`; +const BackArrow = styled.img` + width: 0.56088rem; + height: 0.9745rem; + margin-right: 0.31rem; +`; +const BackText = styled.img` + width: 2.1875rem; + height: 1.125rem; +`; +const EditIcon = styled.img` + width: 1.3125rem; + height: 1.25rem; +`; + +export default TopBar; diff --git a/src/custom.d.tsx b/src/custom.d.tsx new file mode 100644 index 0000000..b528b80 --- /dev/null +++ b/src/custom.d.tsx @@ -0,0 +1,6 @@ +// 이미지 import 위한 확장자 설정 +declare module "*.jpg"; +declare module "*.png"; +declare module "*.jpeg"; +declare module "*.gif"; +declare module "*.svg"; diff --git a/src/index.css b/src/index.css index ec2585e..206ffdc 100644 --- a/src/index.css +++ b/src/index.css @@ -1,10 +1,12 @@ +@import url('https://fonts.cdnfonts.com/css/sf-pro-display'); body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + font-family: "SF Pro Text",-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; + font-size: 1.125rem; } code { 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..e302672 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; +import GlobalStyle from "./styles/globalStyle"; + +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement +); + +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/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/chat.tsx b/src/pages/chat/chat.tsx new file mode 100644 index 0000000..7b7d6dd --- /dev/null +++ b/src/pages/chat/chat.tsx @@ -0,0 +1,197 @@ +import React, { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import ChatInput from "../../components/ChatInput/chatinput"; +import userData from "../../assets/datas/userdata.json"; +import chatData from "../../assets/datas/chatdata.json"; +import ChatTitle from "../../components/ChatTitle/chatTitle"; +import MessageList from "../../components/Message/messageList"; +import Message from "../../components/Message/message"; +import "../../styles/colors.css"; +import bottomBar from "../../assets/images/LightBottomBar.svg"; +import StatusBar from "../../components/StatusBar/statusbar"; +import { useParams } from "react-router-dom"; +interface Message { + id: number; + sender: string; + content: string; + showIcon: boolean; + timestamp: Date; // message list 페이지에 시간 추가 필요헤서 필드 추가 +} + +interface ChatData { + [key: string]: Message[]; +} + +function Chat() { + // URL에서 채팅 ID를 추출, 예: /chat/1 + const { chatId } = useParams<{ chatId: string }>(); + const [messages, setMessages] = useState([]); + const [messageId, setMessageId] = useState(0); + const [nowUser, setNowUser] = useState(userData.users[0]); + + useEffect(() => { + if (typeof chatId !== "undefined") { + const savedChats = JSON.parse( + localStorage.getItem("chatMessages") || JSON.stringify(chatData) + ) as ChatData; + setMessages(savedChats[chatId] || []); + setMessageId(savedChats[chatId]?.length || 0); + } + }, [chatId]); + + // 사용자가 바뀔 때마다 실행 + useEffect(() => { + setMessages((prevMessages) => { + const newMessages = [...prevMessages]; + const lastMessageIndex = newMessages.length - 1; + + if (lastMessageIndex >= 0) { + // 모든 메시지를 순회하며 showIcon을 설정 + for (let i = 0; i < lastMessageIndex; i++) { + const currentSender = newMessages[i].sender; + const nextSender = newMessages[i + 1].sender; + newMessages[i].showIcon = currentSender !== nextSender; + } + // 마지막 메시지는 항상 showIcon을 true 로 설정 + newMessages[lastMessageIndex].showIcon = true; + } + + return newMessages; + }); + }, [nowUser]); + + if (!chatId) { + return
없는 친구 ID입니다.
; + } + + const handleSendMessage = (content: string) => { + const newMessage: Message = { + id: messageId, + sender: nowUser.name, + content, + showIcon: false, + timestamp: new Date(), // 현재 시간 추가 + }; + + // 메세지가 추가됐을 때 가장 마지막 index의 메세지와 비교하여 showIcon 업데이트 + if ( + messages.length > 0 && + messages[messages.length - 1].sender !== nowUser.name + ) { + messages[messages.length - 1].showIcon = true; + } + + const updatedMessages = [...messages, newMessage]; + setMessages(updatedMessages); + + // 다음 메시지를 위해 messageId 업데이트 + setMessageId(messageId + 1); + // 로컬 스토리지에 채팅 데이터 저장 + const savedChats = JSON.parse( + localStorage.getItem("chatMessages") || JSON.stringify(chatData) + ) as ChatData; + + savedChats[chatId] = updatedMessages; + localStorage.setItem("chatMessages", JSON.stringify(savedChats)); + }; + const changeUser = () => { + setNowUser((prev) => + prev.name === userData.users[0].name + ? userData.users[chatIdNumber - 1] + : userData.users[0] + ); + + setMessages((prevMessages) => { + const newMessages = [...prevMessages]; + const lastMessageIndex = newMessages.length - 1; + //userChange 동시에 여러번 누르면 아이콘 동시에 뜨는것 방지하기 위해 + if (lastMessageIndex >= 1) { + const lastMessageSender = newMessages[lastMessageIndex].sender; + const secondLastMessageSender = + newMessages[lastMessageIndex - 1].sender; + + // 직전 메시지와 비교하여 showIcon을 설정합니다. + if ( + secondLastMessageSender === lastMessageSender && + newMessages[lastMessageIndex - 1].showIcon + ) { + newMessages[lastMessageIndex - 1].showIcon = false; + } + newMessages[lastMessageIndex].showIcon = true; + } + + return newMessages; + }); + }; + + //chatId를 integer 값으로 바꿔준 결과 + const chatIdNumber = +chatId; + const partner = + nowUser.name === userData.users[0].name + ? userData.users[chatIdNumber - 1] + : userData.users[0]; + + const messageContainers = messages.map((message: Message, index) => { + const isCurrentUser = message.sender === nowUser.name; + + return ( + + ); + }); + + return ( +
+ + + + + {messageContainers} + + + + +
+ ); +} + +const ChatContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + border-radius: 1.5rem; + height: 100vh; + width: 100vw; + border: solid var(--gray-1); +`; + +const MessageContainer = styled.div` + flex: 1; + overflow-y: auto; + width: 100%; + height: 100%; + /* padding: 2.75rem 0; //Topbar랑 안겹치게 + margin-bottom: 2.5rem; //아래 부분이랑 안겹치게 */ + display: flex; + flex-direction: column; + align-items: center; +`; + +const BottomBarIcon = styled.img` + width: 100%; + height: 2.125rem; + margin-bottom: 0; + position: relative; +`; + +export default Chat; diff --git a/src/pages/chatlist/chatList.tsx b/src/pages/chatlist/chatList.tsx new file mode 100644 index 0000000..7035590 --- /dev/null +++ b/src/pages/chatlist/chatList.tsx @@ -0,0 +1,164 @@ +import styled from "styled-components"; +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +//components +import ChatListItem from "../../components/ChatlistItem/chatlistitem"; +import StatusBar from "../../components/StatusBar/statusbar"; +import TopBar from "../../components/TopBar/topbar"; +import SearchBar from "../../components/SearchBar/serachbar"; +//images +import friendsIcon from "../../assets/images/Friends.svg"; +import userData from "../../assets/datas/userdata.json"; +import chatData from "../../assets/datas/chatdata.json"; +import bottomBar from "../../assets/images/LightBottomBar.svg"; +interface Message { + id: number; + sender: string; + content: string; + showIcon: boolean; + timestamp: string; //메시지 보내는 시간 정보 추가 + unread?: boolean; // 읽지 않은 메시지인지 여부를 나타내는 속성 추가 +} +interface User { + id: number; + name: string; +} +interface ChatData { + [key: string]: Message[]; +} + +export default function ChatList() { + const navigate = useNavigate(); + + const [lastMessages, setLastMessages] = useState< + Record + >({}); + + //마지막 메시지가 최근인 순서로 sorting + const [sortedUsers, setSortedUsers] = useState(userData.users); + + useEffect(() => { + // localStorage에서 chatData 가져오기 + const savedChats = JSON.parse( + localStorage.getItem("chatMessages") || JSON.stringify(chatData) + ) as ChatData; + + // 모든 사용자의 마지막 메시지와 시간 가져오기 + const lastMessage: Record = {}; + + //userMessage: Message기록이 있는 유저들 필터링 + 마지막 대화 시간 가져옴 + const userMessage = userData.users + .filter((user) => savedChats[user.id.toString()]?.length > 0) // chatData가 있는 사용자만 가져옴 + .map((user) => { + const userChat = savedChats[user.id.toString()]; + const lastChat = userChat ? userChat[userChat.length - 1] : null; + lastMessage[user.id] = lastChat; + return { + ...user, + lastMessageTimestamp: lastChat + ? new Date(lastChat.timestamp) // 마지막 메시지가 있으면 해당 시간을 가져옴 + : new Date(0), // 마지막 timestamp 없으면 기본 시간으로 예외처리 + }; + }); + + // 타임스탬프 기준으로 사용자 정렬: 가장 최근 메시지가 위에 오게 함 + const sortedUsers = userMessage.sort( + (a, b) => + b.lastMessageTimestamp.getTime() - a.lastMessageTimestamp.getTime() + ); + setSortedUsers(sortedUsers); + + const lastMessageUnread: Record = {}; + // 모든 사용자의 마지막 메시지에 대한 unread 상태 업데이트 : + // 마지막 sender가 내(신동현)가 아닌 경우 unread 로 처리 + userData.users.forEach((user) => { + const lastMsg = lastMessage[user.id]; + if (lastMsg) { + lastMessageUnread[user.id] = { + ...lastMsg, + unread: lastMsg.sender !== "신동현", + }; + } + }); + //unread 업데이트 저장 + setLastMessages(lastMessageUnread); + }, []); + + //searchBar에서 검색하면, 필터링된 사용자만 뜨게 하는 부분 + const [searchTerm, setSearchTerm] = useState(""); + function getFilteredUsers(users: User[], searchTerm: string) { + if (!searchTerm) return users; + return users.filter((user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + const filteredUsers = getFilteredUsers(sortedUsers, searchTerm); + + return ( +
+ + + + + Chat + <img src={friendsIcon} onClick={() => navigate("/friends")} /> + + setSearchTerm(e.target.value)} + /> + + {filteredUsers.map((user) => ( + navigate(`/chat/${user.id}`)} + /> + ))} + + + +
+ ); +} + +const ChatRoomContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + border-radius: 1.5rem; + height: 100vh; + width: 100vw; + border: solid rgba(144, 144, 147, 0.5); +`; + +const Title = styled.span` + font-family: "SF Pro Text"; + font-size: 2.125rem; + font-style: normal; + font-weight: 700; + line-height: 80%; + width: 100%; + padding-left: 1.37rem; + margin-bottom: 1.75rem; + display: flex; + justify-content: space-between; + padding-right: 1.12rem; +`; +const BottomBarIcon = styled.img` + width: 100%; + height: 2.125rem; + margin-bottom: 0; + /* position: relative; */ + position: absolute; + bottom: 0; +`; + +const ChatlistContainer = styled.div` + overflow-y: auto; // 세로 스크롤 + width: 100%; + height: 100%; +`; diff --git a/src/pages/friends/friends.tsx b/src/pages/friends/friends.tsx new file mode 100644 index 0000000..0433756 --- /dev/null +++ b/src/pages/friends/friends.tsx @@ -0,0 +1,192 @@ +import styled from "styled-components"; +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import userData from "../../assets/datas/userdata.json"; +//component +import SearchBar from "../../components/SearchBar/serachbar"; +//이미지들 +import bigIcon from "../../assets/images/BigIcon.svg"; +import bottomBar from "../../assets/images/LightBottomBar.svg"; +import StatusBar from "../../components/StatusBar/statusbar"; +import TopBar from "../../components/TopBar/topbar"; +import bubbleImg from "../../assets/images/bubbleStatusImg.svg"; + +interface User { + id: number; + name: string; + instagram: string; + status: string; +} +//친구 목록에서 status가 존재하는지 여부 +interface FriendsListItemProps { + statusExist: boolean; +} + +export default function Friends() { + const navigate = useNavigate(); + const [searchTerm, setSearchTerm] = useState(""); + const [friends, setFriends] = useState(userData.users); + + function getFilteredUsers(users: User[], searchTerm: string) { + if (!searchTerm) return users; + const filteredUsers = users.filter((user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return filteredUsers; + } + const filteredUsers = getFilteredUsers(friends, searchTerm); + + return ( +
+ + + + Friends + { + setSearchTerm(e.target.value); + }} + /> + + {filteredUsers.map((user, index) => ( + navigate(`/chat/${user.id}`)} + statusExist={!!user.status} // user.status boolean 가져와서 + > + {user.status && ( + //상메가 설정된 경우 버블 안에 보여줌 + + Status bubble + {user.status} + + )} + + + {user.name} + {user.instagram} + + ))} + + + +
+ ); +} + +const ProfileContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + border-radius: 1.5rem; + height: 100%; + width: 100vw; + border: solid var(--gray-1); +`; + +const Title = styled.span` + font-family: "SF Pro Text"; + font-size: 2.125rem; + font-style: normal; + font-weight: 700; + line-height: 80%; + width: 100%; + padding-left: 1.37rem; + margin-bottom: 1.75rem; + display: flex; + justify-content: space-between; + padding-right: 1.12rem; +`; +const BottomBarIcon = styled.img` + width: 100%; + height: 2.125rem; + margin-bottom: 0; + /* position: relative; */ + position: absolute; + bottom: 0; +`; + +const FriendsContainer = styled.div` + display: flex; + flex-flow: wrap; //여러줄에 아이템 표시 + overflow-y: auto; + width: 100%; + padding: 0 1rem; + justify-content: flex-start; +`; + +const FriendsListItem = styled.div` + position: relative; // 상메 올라가는 기준점 + display: flex; + flex-direction: column; + align-items: center; + + padding-top: ${(props) => + props.statusExist + ? "1.1rem" + : "2.1rem"}; // 상메 있는지 여부에 따라 margin-top을 변경 + + //default: 노트북 환경 + width: 100%; + max-width: 10%; // item이 한줄에 10개씩 오도록 + + @media (max-width: 850px) { + // 화면 너비가 850px 이하인 경우 (ipad 환경) + width: 100%; + max-width: 20%; // item이 한줄에 5개씩 오도록 + } + + @media (max-width: 480px) { + // 화면 너비가 480px 이하인 경우 (모바일 환경) + width: 100%; + max-width: 33%; // item이 한줄에 3개씩 오도록 + margin-right: 0; + } +`; + +const StatusBubble = styled.div` + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 10px; + + img { + position: absolute; + border-radius: inherit; + } + + span { + position: relative; + z-index: 2; // 이미지 위에 텍스트 + overflow: hidden; + white-space: nowrap; + color: var(--black); + font-family: "SF Pro Text"; + font-size: 0.625rem; + font-style: normal; + font-weight: 400; + line-height: 150%; + } +`; +const Name = styled.div` + margin-top: 0.5rem; + color: var(--black); + text-align: center; + font-family: "SF Pro Text"; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: normal; +`; +const Instagram = styled.div` + color: var(--gray-1); + text-align: center; + font-family: "SF Pro Text"; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 125%; +`; diff --git a/src/pages/profile/profile.tsx b/src/pages/profile/profile.tsx new file mode 100644 index 0000000..f22e25c --- /dev/null +++ b/src/pages/profile/profile.tsx @@ -0,0 +1,168 @@ +import styled from "styled-components"; +import { useNavigate } from "react-router-dom"; +//이미지 & datas +import bottomBar from "../../assets/images/LightBottomBar.svg"; +import StatusBar from "../../components/StatusBar/statusbar"; +import BigIconLogo from "../../assets/images/BigIconLogo.svg"; +import Github from "../../assets/images/github.svg"; +import RightArrow from "../../assets/images/Arrow.svg"; +import Instagram from "../../assets/images/instagram.svg"; +import userData from "../../assets/datas/userdata.json"; + +export default function Profile() { + const navigate = useNavigate(); + + return ( +
+ + + + My Profile + + + {userData.users[0].name} + donghyun98@gmail.com + + window.open("https://github.com/dhshin98", "_blank")} + > + + + GitHub + https://github.com/dhshin98 + + + + + + + + Instagram + @인스타 아이디 + + + + + navigate("/chatlist")}> + + + Instagram + {userData.users[0].instagram} + + + + + +
+ ); +} +const ProfileContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + border-radius: 1.5rem; + height: 100vh; + width: 100vw; + border: solid var(--gray-1); +`; + +const Title = styled.text` + font-family: "SF Pro Text"; + font-size: 2.125rem; + font-style: normal; + font-weight: 700; + line-height: 80%; + width: 100%; + padding-left: 1.37rem; + padding-top: 2.94rem; +`; +const BigIcon = styled.img` + width: 5.9375rem; + height: 5.93725rem; + margin-top: 2.62rem; +`; +const Rectangle = styled.div` + width: 90%; + height: 4.1875rem; + border-radius: 1.25rem; + display: flex; + align-items: center; + margin: 0rem 1rem; + box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.1); + margin-bottom: 0.69rem; +`; +const Icon = styled.img` + /* width: ${(props) => + props.src === "Instagram" ? "2.1875rem" : "2.77813rem"}; + height: ${(props) => + props.src === "Instagram" ? "2.1875rem" : "2.77813rem"}; + */ + margin: 0.69rem 0.35rem 0.72rem 0.75rem; + width: ${(props) => props.width}; + height: ${(props) => props.width}; +`; +const RectangleInfo = styled.div` + display: flex; + flex-direction: column; + gap: 0.06rem; + height: 100%; + width: 100%; + padding-left: 1rem; + padding-right: 0.44rem; + padding-bottom: 1.12rem; + padding-top: 0.94rem; +`; + +const RightArrowIcon = styled.img` + width: 1rem; + height: 1rem; + position: relative; + margin-right: 1.44rem; +`; +const Subtitle = styled.text` + font-family: "SF Pro Text"; + font-size: 0.9375rem; + font-style: normal; + font-weight: 400; + line-height: normal; +`; +const SubtitleInfo = styled.text` + color: var(--gray-1); + font-family: "SF Pro Text"; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 125%; +`; + +const BottomBarIcon = styled.img` + width: 100%; + height: 2.125rem; + margin-bottom: 0; + /* position: relative; */ + position: absolute; + bottom: 0; +`; + +const Name = styled.text` + color: var(--black); + font-family: "SF Pro Text"; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: normal; + margin-top: 0.69rem; +`; + +const Email = styled.text` + color: var(--gray-1); + text-align: right; + font-family: "SF Pro Text"; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 0.9375rem; + text-decoration-line: underline; + margin-bottom: 3.25rem; +`; diff --git a/src/styles/colors.css b/src/styles/colors.css new file mode 100644 index 0000000..8e4ea72 --- /dev/null +++ b/src/styles/colors.css @@ -0,0 +1,7 @@ +:root{ + --black : #000; + --gray-1 : #909093; + --gray-2: rgba(118, 118, 128, 0.12); + --gray-3: rgba(60, 60, 67, 0.30); + --blue : #3478F6; +} \ No newline at end of file diff --git a/src/styles/globalStyle.ts b/src/styles/globalStyle.ts new file mode 100644 index 0000000..175472f --- /dev/null +++ b/src/styles/globalStyle.ts @@ -0,0 +1,37 @@ +import { createGlobalStyle } from "styled-components"; +import reset from "styled-reset"; +import "../styles/colors.css" +const GlobalStyle = createGlobalStyle` + ${reset} + *{ + box-sizing:border-box; + padding: 0; + margin: 0; + font-family: "SF Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + + } + html,body{ + height:100%; + display: flex; + justify-content: center; + align-items: center; + + } + .pageWrapper{ + margin: 0; + padding:0; + display: flex; + flex-direction:column; + justify-content: center; + height:100vh; + width:100vw; + + } + :hover { + cursor: pointer; + } + + +`; + +export default GlobalStyle; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..18d86a1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] + } \ No newline at end of file