diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..0a72520
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": true
+}
diff --git a/README.md b/README.md
index 82b92ad..031a4e4 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,45 @@
-# 서론
-
-안녕하세요 🙌🏻 19기 프론트 운영진 배성준입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
+# 미션
-이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
+## 배포링크
-동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다.
+- [배포링크](https://react-messenger-19th-tawny.vercel.app/)
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
+## 피그마 링크
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
+- [피그마 링크](https://www.figma.com/file/W7XzAbakNIOQxwiza0wLIo/CEOS-Messenger-Redesign?type=design&node-id=5-3617&mode=design&t=ic7kPkFW1UIT5FiD-11)
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
+
-그럼 이번 미션도 파이팅입니다!!🎉
+## 디자이너 분께 받은 QA
-# 미션
+- 입력창 폼 안에 양옆으로 10px 패딩 추가
+- 한줄짜리 채팅 버블 추가
+- 챗을 보내면 생성되는 ‘날짜 인디케이터’의 상하너비 수정
+- contact-info 페이지에서 이름 축약
## Key Questions
-- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요?
-- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본
-- 컴포넌트를 분리한 기준은 무엇인가요?
-- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요?
-- 디자이너와 소통하며 느낀점은 무엇인가요?
-
-## 미션 목표
+**1. Routing이란?**
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다.
+Routing이란 경로를 선택하면 웹의 URL을 변경하여 사용자가 선택한 해당 경로로 이동하게 해주며 새로 페이지를 로드하지 않고도 변경 가능합니다. React 자체에는 Routing 기능이 내장되어 있지 않습니다. 따라서 React 애플리케이션에서 Routing을 구현하기 위해 react-router-dom과 같은 외부 라이브러리를 사용합니다. react-router-dom은 React에서 SPA를 구현할 때 널리 사용되는 Routing 라이브러리입니다.
-## 기한
+이번 과제를 진행하며 react router에서 제공하는 여러 hook을 사용하였습니다. 가장 기본 경로를 채팅 목록 chats page로 지정하였고 이외에도 다른 페이지들은 각각의 URL에 맞는 컴포넌트를 정의하였습니다. URL 이동을 해야할 때에는 useNavigate을 사용해서 경로 이동을 하였습니다. useParams를 사용해서 url에서 채팅방 ID를 추출하기도 하고 useLocation을 사용하여 이전 페이지로부터 전달받은 userId 정보를 컴포넌트 내에서 사용할 수 있게 하기도 하였습니다.
-2024년 3월 29일 금요일
+**2. SPA란?**
-## 필수 구현 기능
+SPA는 Single Page Application의 약자로 하나의 페이지로 이루어진 웹 애플리케이션을 말합니다. 새로 페이지를 로드하지 않고 필요한 부분만 수정합니다. SPA는 클라이언트 사이드 렌더링(Client Side Rendering, CSR) 방식을 취하고 있습니다. 이는 빠른 페이지 로딩과 부드러운 페이지 전환, 그리고 사용자 화면에 높은 반응성을 유지할 수 있도록 합니다. 하지만 검색 엔진 최적화에 제한이 있으며 첫 페이지 로딩 속도가 느리다는 단점이 있습니다.
-- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
+**3. 상태관리란?**
-### 추가 구현 기능
+리액트의 가장 큰 특징은 부모 요소에서 자식 요소로 데이터를 전달한다는 것입니다. 컴포넌트 간에 상태를 공유하려면 이처럼 상위 컴포넌트에서 하위 컴포넌트로 porp을 넘겨 전달합니다. 이런 prop 전달 과정이 복잡해지면 계속해서 추적하고 관리하기 어렵기 때문에 전역 상태 관리를 합니다.
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
+전역 상태 관리는 여러 방법이 있는데 그중에서도 Context API, Redux, React Query가 많이 쓰입니다.
+제가 이번 과제에서 사용한 redux toolkit에 대해 간단히 설명을 해보면 . . .
-참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
+리액트는 컴포넌트 자신이 개별적으로 상태관리를 하는데 redux를 쓰면 전용 장소 store에서 상태를 관리하고, 리액트 컴포넌트는 그걸 보여주기만 하는 용도로 쓰입니다. 리액트 컴포넌트가 store에 접근하고 싶으면 action을 발행해야합니다. 여기서 reducer는 이전 상태와 action을 합쳐서 새로운 상태를 state을 만드는 조작을 말합니다. redux 사용시 저장소 구성의 복잡성, 많은 패키지 필요성, 한 작업 시 필요한 수 많은 코드양 등의 문제점을 보완하여 redux toolkit은 redux를 더 사용하기 쉽게 만들었습니다. 여기서 createSlice는 action과 reducer를 전부 가진 함수로 기존에는 액션생성함수와 액션 타입을 선언해 사용했다면 createSlice는 액션을 선언하고 해당 액션이 dispatch되면 바로 해당 액션을 처리합니다.
## 링크 및 참고자료
-- [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에서 상태 관리하기](https://mingule.tistory.com/74)
+- [React - Router, useLocation 를 통해 상세페이지 구현](https://bmy1320.tistory.com/entry/React-Router-useLocation-%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%83%81%EC%84%B8%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%AC%ED%98%84)
+- [SPA란](https://www.elancer.co.kr/blog/view?seq=214)
diff --git a/custom.d.ts b/custom.d.ts
new file mode 100644
index 0000000..091d25e
--- /dev/null
+++ b/custom.d.ts
@@ -0,0 +1,4 @@
+declare module '*.svg' {
+ const content: any;
+ export default content;
+}
diff --git a/git b/git
new file mode 100644
index 0000000..e69de29
diff --git a/package-lock.json b/package-lock.json
index c27bbe4..b762c24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "react-messenger-19th",
"version": "0.1.0",
"dependencies": {
+ "@reduxjs/toolkit": "^2.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -17,9 +18,14 @@
"@types/react-dom": "^18.2.22",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
- "typescript": "^4.9.5",
+ "styled-reset": "^4.5.2",
"web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "typescript": "^4.9.5"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2288,6 +2294,27 @@
"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==",
+ "peer": true,
+ "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==",
+ "peer": true
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
+ "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==",
+ "peer": true
+ },
"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",
@@ -3338,6 +3365,46 @@
}
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.2.tgz",
+ "integrity": "sha512-454GZrEx3G6QSYwIx9ROaso1HR6sTH8qyZBe3KEsdWVGU3ayV8jYCwdaEJV3vl9V6+pi3GRl+7Xl7AeDna6qwQ==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz",
+ "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
+ "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
+ "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",
@@ -4290,6 +4357,12 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
},
+ "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==",
+ "peer": true
+ },
"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",
@@ -4303,6 +4376,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@@ -5750,6 +5828,15 @@
"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==",
+ "peer": true,
+ "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",
@@ -6182,6 +6269,15 @@
"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==",
+ "peer": true,
+ "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",
@@ -6372,6 +6468,17 @@
"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==",
+ "peer": true,
+ "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",
@@ -14856,6 +14963,32 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-redux": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz",
+ "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25",
+ "react": "^18.0",
+ "react-native": ">=0.69",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14864,6 +14997,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
+ "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
+ "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3",
+ "react-router": "6.22.3"
+ },
+ "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",
@@ -14991,6 +15154,19 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -15138,6 +15314,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "node_modules/reselect": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz",
+ "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg=="
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -15719,6 +15900,12 @@
"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==",
+ "peer": true
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -16260,6 +16447,85 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.8",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz",
+ "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==",
+ "peer": true,
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.1",
+ "@emotion/unitless": "0.8.0",
+ "@types/stylis": "4.2.0",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.2",
+ "postcss": "8.4.31",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.1",
+ "tslib": "2.5.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
+ "peer": true
+ },
+ "node_modules/styled-components/node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/styled-components/node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
+ "peer": true
+ },
+ "node_modules/styled-reset": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/styled-reset/-/styled-reset-4.5.2.tgz",
+ "integrity": "sha512-dbAaaVEhweBs2FGfqGBdW6oMcMK8238C2X5KCxBhUQJX92m/QyUfzRADOXhdXiXNkIPELtMCd72YY9eCdORfIw==",
+ "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",
@@ -16275,6 +16541,12 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz",
+ "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==",
+ "peer": true
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -17089,6 +17361,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index ea335d3..9b755df 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@reduxjs/toolkit": "^2.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,8 +13,10 @@
"@types/react-dom": "^18.2.22",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
- "typescript": "^4.9.5",
+ "styled-reset": "^4.5.2",
"web-vitals": "^2.1.4"
},
"scripts": {
@@ -39,5 +42,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "typescript": "^4.9.5"
}
}
diff --git a/public/img/CharileMoore.svg b/public/img/CharileMoore.svg
new file mode 100644
index 0000000..be0ce2b
--- /dev/null
+++ b/public/img/CharileMoore.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/ColinBearman.svg b/public/img/ColinBearman.svg
new file mode 100644
index 0000000..a2129bb
--- /dev/null
+++ b/public/img/ColinBearman.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/DainPark.svg b/public/img/DainPark.svg
new file mode 100644
index 0000000..e0eb5a3
--- /dev/null
+++ b/public/img/DainPark.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/DeclanWalker.svg b/public/img/DeclanWalker.svg
new file mode 100644
index 0000000..3a3e224
--- /dev/null
+++ b/public/img/DeclanWalker.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/img/HaileyStephenson.svg b/public/img/HaileyStephenson.svg
new file mode 100644
index 0000000..4f08117
--- /dev/null
+++ b/public/img/HaileyStephenson.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/JoshuaLawrence.svg b/public/img/JoshuaLawrence.svg
new file mode 100644
index 0000000..f5c6cec
--- /dev/null
+++ b/public/img/JoshuaLawrence.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/LeahStimptson.svg b/public/img/LeahStimptson.svg
new file mode 100644
index 0000000..62683b4
--- /dev/null
+++ b/public/img/LeahStimptson.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/MyStatus.svg b/public/img/MyStatus.svg
new file mode 100644
index 0000000..832316a
--- /dev/null
+++ b/public/img/MyStatus.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/SenaAdams.svg b/public/img/SenaAdams.svg
new file mode 100644
index 0000000..daad4ec
--- /dev/null
+++ b/public/img/SenaAdams.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/img/SiennaHwang.svg b/public/img/SiennaHwang.svg
new file mode 100644
index 0000000..eb0ca48
--- /dev/null
+++ b/public/img/SiennaHwang.svg
@@ -0,0 +1,19 @@
+
diff --git a/public/index.html b/public/index.html
index aa069f2..4c9fa54 100644
--- a/public/index.html
+++ b/public/index.html
@@ -24,7 +24,11 @@
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
+
+ react-messenger
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.tsx b/src/App.tsx
index bd79c18..07f426a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,55 @@
+import { useEffect, useState, ReactNode } from 'react';
+import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
+import styled from 'styled-components';
+import GlobalStyles from './style/GlobalStyles';
+import ChattingPage from './pages/ChattingPage';
+import ContactInfoPage from './pages/ContactInfoPage';
+import ChatsPage from './pages/ChatsPage';
+import StatusPage from './pages/StatusPage';
+import EditContactPage from './pages/EditContactPage';
+import StatusBar from './components/iphone/StatusBar';
+import HomeIndicator from './components/iphone/HomeIndicator';
+
+const Container = styled.div<{ $bgColor: string }>`
+ width: 23.4375rem;
+ height: 50.75rem;
+ background: ${({ $bgColor }) => $bgColor};
+ position: relative;
+`;
+
+// useLocation을 사용하기 위한 Wrapper 컴포넌트
+function AppContainer({ children }: { children: ReactNode }) {
+ const location = useLocation();
+ const [bgColor, setBgColor] = useState('#f6f6f6');
+
+ useEffect(() => {
+ if (location.pathname === '/edit-contact') {
+ setBgColor('#fff'); // edit-contact 경로일 때만 배경색을 #fff로 설정
+ } else {
+ setBgColor('#f6f6f6');
+ }
+ }, [location]);
+
+ return {children};
+}
+
function App() {
- return (
-
-
19기 프론트엔드 파이팅!!! 디자인과 사이좋게 지내요~~~
-
- );
+ return (
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ );
}
export default App;
diff --git a/src/assets/data/initialChatData.json b/src/assets/data/initialChatData.json
new file mode 100644
index 0000000..c48830a
--- /dev/null
+++ b/src/assets/data/initialChatData.json
@@ -0,0 +1,157 @@
+{
+ "chattings": [
+ {
+ "chatRoomId": 0,
+ "userList": [0, 1],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 0,
+ "content": "Lovely you on the way? I had other plans after the meet-up, I just gave ur IG to Charlie just in case, hope you don’t mind.",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ },
+ {
+ "id": 1,
+ "senderId": 1,
+ "content": "Gosh I’m a bit under the weather, so I just came back to my flat. Would you pls let him know I’m gonna drop dm? So sorry, and thx for that. I don’t mind at all.",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ },
+ {
+ "id": 2,
+ "senderId": 0,
+ "content": "Oooo sorry to hear that. Could you give just for a moment to see me? Cos I’m on the way to see you to bring your clothes that you’d left.",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ },
+
+ {
+ "id": 3,
+ "senderId": 1,
+ "content": "Aww sweet, tysm! let me know when you arrive the ground floor!",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ },
+ {
+ "id": 4,
+ "senderId": 0,
+ "content": "Hey lovey don’t forget to tell me when you are okay! Tmrw is the 24th and you leave soon!",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ },
+ {
+ "id": 5,
+ "senderId": 0,
+ "content": "I couldn’t get to see Curtis or Senna so I should see you bfr you go!",
+ "time": "2024-03-28T09:41:00",
+ "isRead": true
+ }
+ ]
+ },
+ {
+ "chatRoomId": 1,
+ "userList": [0, 2],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 0,
+ "content": "Is that yours?",
+ "time": "2024-03-05T09:41:00",
+ "isRead": true
+ }
+ ]
+ },
+ {
+ "chatRoomId": 2,
+ "userList": [0, 3],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 3,
+ "content": "I just want you to know that if it is another choice I could change, yes I ...",
+ "time": "2024-02-27T09:41:00",
+ "isRead": false
+ }
+ ]
+ },
+ {
+ "chatRoomId": 3,
+ "userList": [0, 4],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 0,
+ "content": "Mate where???",
+ "time": "2024-02-15T09:41:00",
+ "isRead": true
+ }
+ ]
+ },
+ {
+ "chatRoomId": 4,
+ "userList": [0, 5],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 5,
+ "content": "Photo",
+ "time": "2024-02-10T09:41:00",
+ "isRead": false
+ }
+ ]
+ },
+ {
+ "chatRoomId": 5,
+ "userList": [0, 6],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 6,
+ "content": "You reacted 🥺 to “Now I arrived in Seoul! It’s insane. You have to see...",
+ "time": "2024-01-25T09:41:00",
+ "isRead": false
+ }
+ ]
+ },
+ {
+ "chatRoomId": 6,
+ "userList": [0, 7],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 0,
+ "content": "언니 이거 봄?",
+ "time": "2024-01-15T09:41:00",
+ "isRead": true
+ }
+ ]
+ },
+ {
+ "chatRoomId": 7,
+ "userList": [0, 8],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 8,
+ "content": "You reacted ❤️ to “Really honoured to be there with you, thank you for..",
+ "time": "2024-01-09T09:41:00",
+ "isRead": false
+ }
+ ]
+ },
+ {
+ "chatRoomId": 8,
+ "userList": [0, 9],
+ "chatList": [
+ {
+ "id": 0,
+ "senderId": 0,
+ "content": "Do you like WhatsApp UI?",
+ "time": "2019-10-20T09:41:00",
+ "isRead": true
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/assets/data/userData.json b/src/assets/data/userData.json
new file mode 100644
index 0000000..d404369
--- /dev/null
+++ b/src/assets/data/userData.json
@@ -0,0 +1,64 @@
+{
+ "users": [
+ {
+ "id": 0,
+ "name": "My Status",
+ "profileImg": "/img/MyStatus.svg",
+ "isActive": true
+ },
+ {
+ "id": 1,
+ "name": "Dain Park",
+ "profileImg": "/img/DainPark.svg",
+ "isActive": true
+ },
+ {
+ "id": 2,
+ "name": "Leah Stimptson",
+ "profileImg": "/img/LeahStimptson.svg",
+ "isActive": true
+ },
+ {
+ "id": 3,
+ "name": "Colin Bearman",
+ "profileImg": "/img/ColinBearman.svg",
+ "isActive": false
+ },
+ {
+ "id": 4,
+ "name": "Charile Moore",
+ "profileImg": "/img/CharileMoore.svg",
+ "isActive": true
+ },
+ {
+ "id": 5,
+ "name": "Hailey Stephenson",
+ "profileImg": "/img/HaileyStephenson.svg",
+ "isActive": false
+ },
+ {
+ "id": 6,
+ "name": "Sena Adams",
+ "profileImg": "/img/SenaAdams.svg",
+ "isActive": false
+ },
+ {
+ "id": 7,
+ "name": "Sienna Hwang",
+ "profileImg": "/img/SiennaHwang.svg",
+ "isActive": true
+ },
+ {
+ "id": 8,
+ "name": "Declan Walker",
+ "profileImg": "/img/DeclanWalker.svg",
+ "isActive": false
+ },
+ {
+ "id": 9,
+ "name": "Joshua Lawrence",
+ "profileImg": "/img/JoshuaLawrence.svg",
+ "isActive": true
+ }
+ ]
+}
diff --git a/src/assets/img/attachment.svg b/src/assets/img/attachment.svg
new file mode 100644
index 0000000..c7bd49d
--- /dev/null
+++ b/src/assets/img/attachment.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/audio-call.svg b/src/assets/img/audio-call.svg
new file mode 100644
index 0000000..be1ea2c
--- /dev/null
+++ b/src/assets/img/audio-call.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/bubble-gray.svg b/src/assets/img/bubble-gray.svg
new file mode 100644
index 0000000..e030af8
--- /dev/null
+++ b/src/assets/img/bubble-gray.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/img/bubble-green.svg b/src/assets/img/bubble-green.svg
new file mode 100644
index 0000000..30cae03
--- /dev/null
+++ b/src/assets/img/bubble-green.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/img/call.svg b/src/assets/img/call.svg
new file mode 100644
index 0000000..bc7ee06
--- /dev/null
+++ b/src/assets/img/call.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/calls-gray.svg b/src/assets/img/calls-gray.svg
new file mode 100644
index 0000000..b094369
--- /dev/null
+++ b/src/assets/img/calls-gray.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/camera-gray.svg b/src/assets/img/camera-gray.svg
new file mode 100644
index 0000000..d51b58e
--- /dev/null
+++ b/src/assets/img/camera-gray.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/camera.svg b/src/assets/img/camera.svg
new file mode 100644
index 0000000..6137ebe
--- /dev/null
+++ b/src/assets/img/camera.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/chats-gray.svg b/src/assets/img/chats-gray.svg
new file mode 100644
index 0000000..ca3c898
--- /dev/null
+++ b/src/assets/img/chats-gray.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/chats-green.svg b/src/assets/img/chats-green.svg
new file mode 100644
index 0000000..a0688c4
--- /dev/null
+++ b/src/assets/img/chats-green.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/checkmark.svg b/src/assets/img/checkmark.svg
new file mode 100644
index 0000000..817ef99
--- /dev/null
+++ b/src/assets/img/checkmark.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/img/contact-call.svg b/src/assets/img/contact-call.svg
new file mode 100644
index 0000000..6d5bbfe
--- /dev/null
+++ b/src/assets/img/contact-call.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/contact-github.svg b/src/assets/img/contact-github.svg
new file mode 100644
index 0000000..7d3c750
--- /dev/null
+++ b/src/assets/img/contact-github.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/contact-instagram.svg b/src/assets/img/contact-instagram.svg
new file mode 100644
index 0000000..3810213
--- /dev/null
+++ b/src/assets/img/contact-instagram.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/img/contact-message.svg b/src/assets/img/contact-message.svg
new file mode 100644
index 0000000..f28623a
--- /dev/null
+++ b/src/assets/img/contact-message.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/edit-arrow.svg b/src/assets/img/edit-arrow.svg
new file mode 100644
index 0000000..d74e65f
--- /dev/null
+++ b/src/assets/img/edit-arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/edit.svg b/src/assets/img/edit.svg
new file mode 100644
index 0000000..6a0c561
--- /dev/null
+++ b/src/assets/img/edit.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/home-indicator.svg b/src/assets/img/home-indicator.svg
new file mode 100644
index 0000000..2822346
--- /dev/null
+++ b/src/assets/img/home-indicator.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/left.svg b/src/assets/img/left.svg
new file mode 100644
index 0000000..fe8eea4
--- /dev/null
+++ b/src/assets/img/left.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/media.svg b/src/assets/img/media.svg
new file mode 100644
index 0000000..225ffe8
--- /dev/null
+++ b/src/assets/img/media.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/microphone.svg b/src/assets/img/microphone.svg
new file mode 100644
index 0000000..b4356dd
--- /dev/null
+++ b/src/assets/img/microphone.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/mute.svg b/src/assets/img/mute.svg
new file mode 100644
index 0000000..34eaaf8
--- /dev/null
+++ b/src/assets/img/mute.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/plus.svg b/src/assets/img/plus.svg
new file mode 100644
index 0000000..c2db4a7
--- /dev/null
+++ b/src/assets/img/plus.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/right-side.svg b/src/assets/img/right-side.svg
new file mode 100644
index 0000000..1a10768
--- /dev/null
+++ b/src/assets/img/right-side.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/img/search.svg b/src/assets/img/search.svg
new file mode 100644
index 0000000..58dafe3
--- /dev/null
+++ b/src/assets/img/search.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/settings-gray.svg b/src/assets/img/settings-gray.svg
new file mode 100644
index 0000000..c047ce4
--- /dev/null
+++ b/src/assets/img/settings-gray.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/star.svg b/src/assets/img/star.svg
new file mode 100644
index 0000000..6f64956
--- /dev/null
+++ b/src/assets/img/star.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/status-add.svg b/src/assets/img/status-add.svg
new file mode 100644
index 0000000..a68b6e8
--- /dev/null
+++ b/src/assets/img/status-add.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/status-camera.svg b/src/assets/img/status-camera.svg
new file mode 100644
index 0000000..b046af5
--- /dev/null
+++ b/src/assets/img/status-camera.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/status-edit.svg b/src/assets/img/status-edit.svg
new file mode 100644
index 0000000..c32c615
--- /dev/null
+++ b/src/assets/img/status-edit.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/status-gray.svg b/src/assets/img/status-gray.svg
new file mode 100644
index 0000000..ca58b51
--- /dev/null
+++ b/src/assets/img/status-gray.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/status-green.svg b/src/assets/img/status-green.svg
new file mode 100644
index 0000000..bf91df6
--- /dev/null
+++ b/src/assets/img/status-green.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/img/tone.svg b/src/assets/img/tone.svg
new file mode 100644
index 0000000..c187d78
--- /dev/null
+++ b/src/assets/img/tone.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/img/video-call.svg b/src/assets/img/video-call.svg
new file mode 100644
index 0000000..7873299
--- /dev/null
+++ b/src/assets/img/video-call.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/BottomTabBar/BottomTabBar.tsx b/src/components/BottomTabBar/BottomTabBar.tsx
new file mode 100644
index 0000000..3fb6c86
--- /dev/null
+++ b/src/components/BottomTabBar/BottomTabBar.tsx
@@ -0,0 +1,98 @@
+import styled from 'styled-components';
+import { useNavigate, useLocation } from 'react-router-dom';
+import StatusGray from '../../assets/img/status-gray.svg';
+import StatusGreen from '../../assets/img/status-green.svg';
+import Calls from '../../assets/img/calls-gray.svg';
+import Camera from '../../assets/img/camera-gray.svg';
+import ChatsGray from '../../assets/img/chats-gray.svg';
+import ChatsGreen from '../../assets/img/chats-green.svg';
+import Settings from '../../assets/img/settings-gray.svg';
+
+const TabBarContainer = styled.div`
+ width: 23.4375rem;
+ height: 3.06rem;
+ border-top: 0.03125rem solid #a4a39e;
+ position: relative;
+ background: #f6f6f6;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ margin-bottom: 2.125rem;
+`;
+
+const IconContainer = styled.div`
+ width: 4.6875rem;
+ height: 3.0625rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+`;
+
+const IconImg = styled.img`
+ width: auto;
+ height: auto;
+ position: absolute;
+`;
+
+const Text = styled.div`
+ color: ${(props) => (props.$active ? '#1BD742' : 'rgba(84, 84, 88, 0.65)')};
+ text-align: center;
+ font-family: 'SF Pro Text';
+ font-size: 0.625rem;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ letter-spacing: 0.00625rem;
+
+ position: absolute;
+ top: 2.19rem;
+`;
+
+interface TextProps {
+ $active?: boolean;
+}
+
+export default function BotttomTabBar() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const isActive = (path: string) => location.pathname === path;
+
+ return (
+
+ navigate('/status')}>
+
+ Status
+
+
+
+ Calls
+
+
+
+ Camera
+
+ navigate('/')}>
+
+ Chats
+
+
+
+ Settings
+
+
+ );
+}
diff --git a/src/components/Chats/Chat.tsx b/src/components/Chats/Chat.tsx
new file mode 100644
index 0000000..c450cea
--- /dev/null
+++ b/src/components/Chats/Chat.tsx
@@ -0,0 +1,109 @@
+import styled from 'styled-components';
+import { UserProps } from '../../types/interface';
+import { ChatProps } from '../../types/interface';
+import FormatDateToDMY from '../../utils/formatDateToDMY';
+import checkmark from '../../assets/img/checkmark.svg';
+
+// 각각의 채팅
+const ChatContainer = styled.div`
+ width: 21.4375rem;
+ height: 3.435rem;
+ padding: 0.5rem 1rem 0.69rem 1rem;
+ display: flex;
+`;
+
+const ProfileImg = styled.img`
+ width: 3.25rem;
+ height: 3.25rem;
+ border-radius: 3.25rem;
+ margin-top: 0.19rem;
+`;
+
+const TextContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ padding-left: 0.75rem;
+`;
+
+// 유저 이름과 채팅 시간 컨테이너
+const NameTimeContainer = styled.div`
+ width: 17.4325rem;
+ display: flex;
+ justify-content: space-between;
+`;
+
+const UserName = styled.div`
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 1rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 1.3125rem;
+ letter-spacing: -0.02063rem;
+`;
+
+const TimeText = styled.div`
+ margin-top: 0.13rem;
+ color: #8e8e93;
+ text-align: right;
+ font-family: 'SF Pro Text';
+ font-size: 0.875rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.00938rem;
+`;
+
+const ContentContainer = styled.div`
+ display: block;
+ align-items: center;
+`;
+
+// 체크 이미지를 ChatText 컴포넌트 내부의 ::before 가상 요소로 삽입
+// 체크 이미지가 텍스트의 일부로 취급되어 텍스트가 줄바꿈될 때 체크 이미지는 첫 번째 줄에 그대로 유지되고, 텍스트는 체크 이미지 아래부터 시작
+const ChatText = styled.div<{ $isRead: boolean }>`
+ color: #8e8e93;
+ font-family: 'SF Pro Text';
+ font-size: 0.875rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.0125rem;
+ text-align: left;
+ width: 16.25rem;
+
+ ${({ $isRead }) =>
+ $isRead &&
+ `
+ &::before {
+ content: url(${checkmark});
+ display: inline-block;
+ width: 0.875rem;
+ height: 0.875rem;
+ margin-right: 0.28rem; // 이미지와 텍스트 사이 공간
+ vertical-align: middle;
+ }
+ `}
+`;
+
+// 두 타입을 결합하여 새로운 타입 정의
+type CombinedProps = UserProps & ChatProps;
+
+export default function Chat(props: CombinedProps) {
+ const { name, profileImg, lastChatTime, lastChatContent, $isRead } = props;
+
+ return (
+
+
+
+
+ {name}
+ {FormatDateToDMY(lastChatTime)}
+
+
+ {lastChatContent}
+
+
+
+ );
+}
diff --git a/src/components/Chats/ChatsList.tsx b/src/components/Chats/ChatsList.tsx
new file mode 100644
index 0000000..211ed1f
--- /dev/null
+++ b/src/components/Chats/ChatsList.tsx
@@ -0,0 +1,96 @@
+import styled from 'styled-components';
+import { useNavigate } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../store';
+import Chat from '../Chats/Chat';
+
+const ChatsListPageContainer = styled.div`
+ width: 23.4375rem;
+ height: 40.06rem;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ scrollbar-width: none; // Firefox
+ -ms-overflow-style: none; // Internet Explorer/Edge
+ &::-webkit-scrollbar {
+ display: none; // Chrome, Safari
+ }
+`;
+
+const TitleContainer = styled.div`
+ width: 21.4375rem;
+ height: 1.25rem;
+ padding: 0.75rem 1rem;
+ display: flex;
+ justify-content: space-between;
+`;
+
+const TitleText = styled.div`
+ color: #1bd742;
+ font-family: 'SF Pro Text';
+ font-size: 17px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.4px;
+`;
+
+export default function ChatsList() {
+ const navigate = useNavigate();
+ const nowUser = useSelector((state: RootState) => state.user.nowUser);
+ const userList = useSelector((state: RootState) => state.user.userList);
+ const chattings = useSelector((state: RootState) => state.chat.chattings);
+
+ // 현재 nowUser가 포함된 채팅방만 필터링
+ const filteredChattings = chattings.filter((chatRoom) =>
+ chatRoom.userList.includes(nowUser)
+ );
+
+ // 채팅방 클릭 핸들러 함수
+ const handleChatClick = (chatRoomId: number) => {
+ navigate(`/chatroom/${chatRoomId}`); // `/chatroom/{id}` 경로로 이동
+ };
+
+ return (
+
+
+ Broadcast Lists
+ New Group
+
+
+ {filteredChattings.map((chatRoom) => {
+ // 현재 채팅방에서 현재 사용자를 제외한 상대방의 정보를 찾기
+ const partner =
+ userList.find(
+ (user) =>
+ chatRoom.userList.includes(user.id!) && user.id !== nowUser
+ ) ?? null;
+
+ // 마지막 채팅 정보를 추출
+ const lastChat =
+ chatRoom.chatList[chatRoom.chatList.length - 1] || null;
+
+ // 각 채팅방에 대한 상대방의 프로필을 출력
+ return (
+ handleChatClick(chatRoom.chatRoomId)}
+ >
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Chatting/ChatBubble.tsx b/src/components/Chatting/ChatBubble.tsx
new file mode 100644
index 0000000..39219ef
--- /dev/null
+++ b/src/components/Chatting/ChatBubble.tsx
@@ -0,0 +1,154 @@
+import { useState, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+import CheckMark from '../../assets/img/checkmark.svg';
+import BubbleGreen from '../../assets/img/bubble-green.svg';
+import BubbleGray from '../../assets/img/bubble-gray.svg';
+
+const ChatBubbleContainer = styled.div`
+ display: flex;
+ justify-content: ${(props) =>
+ props.$isSentByMe
+ ? 'flex-end'
+ : 'flex-start'}; // 나와 상대방의 채팅 버블을 각자 정렬
+ margin-bottom: 0.31rem;
+ margin-left: ${(props) => (props.$isSentByMe ? 'auto' : '0.44rem')};
+ margin-right: ${(props) => (props.$isSentByMe ? '0.44rem' : 'auto')};
+ position: relative;
+`;
+
+const BubbleWrapper = styled.div`
+ display: flex;
+ flex-direction: ${(props) =>
+ props.$isSentByMe ? 'row-reverse' : 'row'}; // 말풍선과 버블의 순서
+ align-items: flex-end;
+`;
+
+const BubbleImg = styled.img`
+ width: 0.5rem;
+ height: 0.9375rem;
+`;
+
+const BubbleRectangle = styled.div`
+ display: inline-flex;
+ min-width: 5.5625rem;
+ max-width: 17.125rem;
+ padding: 0.5rem;
+ flex-direction: column;
+ gap: 0.625rem;
+ border-radius: 0.375rem;
+ background: ${(props) => (props.$isSentByMe ? '#E1F3D2' : '#eaeaea')};
+`;
+
+const TextContainer = styled.div`
+ display: inline-flex;
+ flex-direction: ${(props) => props.direction};
+ align-items: flex-end;
+ position: relative;
+`;
+
+const ChatText = styled.div`
+ width: 100%;
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 1.0625rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.375rem;
+ letter-spacing: -0.02563rem;
+ margin-top: 0;
+ margin-left: 0;
+`;
+
+const MessageInfoContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-top: 0.25rem;
+ margin-left: ${(props) => (props.direction === 'row' ? '0.75rem' : '0')};
+`;
+
+const TimeText = styled.div`
+ color: rgba(0, 0, 0, 0.15);
+ font-family: 'SF Pro Display';
+ font-size: 0.75rem;
+ font-style: normal;
+ font-weight: 500;
+ margin-bottom: 0;
+ margin-right: ${(props) => (props.$isSentByMe ? '0.25rem' : '0')};
+`;
+
+const CheckImg = styled.img`
+ width: 0.9375rem;
+ height: 0.875rem;
+ position: relative;
+ margin-bottom: 0;
+ margin-right: 0;
+ display: ${(props) =>
+ props.$isRead && props.$isSentByMe ? 'inline' : 'none'};
+`;
+
+interface ChatBubbleBaseProps {
+ $isSentByMe?: boolean;
+ $isRead?: boolean;
+}
+
+interface ChatBubbleProps extends ChatBubbleBaseProps {
+ content: string;
+ time: string;
+}
+
+interface TextContainerProps {
+ direction: string;
+}
+
+export default function ChatBubble(props: ChatBubbleProps) {
+ const chatTextRef = useRef(null);
+ const [flexDirection, setFlexDirection] = useState('column');
+ const { $isSentByMe, content, time, $isRead } = props;
+
+ // TextContainer의 flex-direction을 텍스트의 길이에 따라 결정
+ useEffect(() => {
+ const checkTextWidth = () => {
+ if (chatTextRef.current) {
+ const width = chatTextRef.current.offsetWidth;
+ const maxWidth = 17 * 16; // rem을 px로 변환 (가정: 1rem = 16px)
+ if (width > maxWidth) {
+ setFlexDirection('column');
+ } else {
+ setFlexDirection('row');
+ }
+ }
+ };
+
+ checkTextWidth();
+
+ // 창 크기 변경에 따라 다시 검사
+ window.addEventListener('resize', checkTextWidth);
+ return () => window.removeEventListener('resize', checkTextWidth);
+ }, []);
+
+ return (
+
+
+
+
+
+ {content}
+
+ {time}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Chatting/ChatInput.tsx b/src/components/Chatting/ChatInput.tsx
new file mode 100644
index 0000000..63908dd
--- /dev/null
+++ b/src/components/Chatting/ChatInput.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react';
+import styled from 'styled-components';
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../../store';
+import { addChat } from '../../features/chatSlice';
+import { Chats } from '../../types/interface';
+import Plus from '../../assets/img/plus.svg';
+import Attachment from '../../assets/img/attachment.svg';
+import Camera from '../../assets/img/camera.svg';
+import Microphone from '../../assets/img/microphone.svg';
+
+const InputContainer = styled.div`
+ width: 23.4375rem;
+ height: 2.87rem;
+ border-top: 0.03125rem solid #a4a39e;
+ position: relative;
+ margin-bottom: 2.125rem;
+`;
+
+const IconImg = styled.img`
+ width: 1.5rem;
+ height: 1.5rem;
+ position: absolute;
+ bottom: 0.5rem;
+`;
+
+const InputBox = styled.div`
+ display: flex;
+ align-items: center;
+ width: 13.115rem;
+ height: 1.375rem;
+ padding: 0.25rem 0.63rem;
+ border-radius: 0.9375rem;
+ border: 0.6px solid #dedfe0;
+ background: #fff;
+ position: absolute;
+ top: 0.62rem;
+ left: 2.88rem;
+`;
+
+const Input = styled.input`
+ width: 11.615rem;
+ border: none;
+ outline: none;
+ font-family: 'SF Pro Text';
+ font-size: 1.0625rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.375rem;
+ letter-spacing: -0.02563rem;
+ background: transparent;
+`;
+
+export default function ChatInput({ chatRoomId }: Chats) {
+ const nowUser = useSelector((state: RootState) => state.user.nowUser); // 현재 사용자 ID 가져오기
+ const [value, setValue] = useState('');
+ const dispatch = useDispatch();
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ if (!value.trim()) {
+ alert('공백 없이 입력해주세요.');
+ return;
+ }
+
+ const senderId = nowUser;
+ const time = new Date().toISOString(); // 현재 시각을 string으로 변환
+ const isRead = true; // 상황에 따라 설정
+
+ dispatch(addChat({ chatRoomId, senderId, content: value, time, isRead }));
+ setValue('');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Chatting/ChattingRoom.tsx b/src/components/Chatting/ChattingRoom.tsx
new file mode 100644
index 0000000..d215cf0
--- /dev/null
+++ b/src/components/Chatting/ChattingRoom.tsx
@@ -0,0 +1,96 @@
+import React, { useRef, useEffect } from 'react';
+import styled from 'styled-components';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../store';
+import { Chats } from '../../types/interface';
+import ChatBubble from './ChatBubble';
+import FormatTimeToAMPM from '../../utils/formatTimeToAMPM';
+import FormatDateToDMY from '../../utils/formatDateToDMY';
+
+const ChattingPageContainer = styled.div`
+ width: 23.4375rem;
+ height: 40.25rem;
+ display: block; // DateContainer와 ChatBubble 사이 마진 겹침 현상을 위함
+ flex-direction: column;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ scrollbar-width: none; // Firefox
+ -ms-overflow-style: none; // Internet Explorer/Edge
+ &::-webkit-scrollbar {
+ display: none; // Chrome, Safari
+ }
+`;
+
+const DateContainer = styled.div`
+ width: fit-content;
+ height: 0.875rem;
+ display: block;
+ text-align: center;
+ padding: 0.1875rem 1rem;
+ border-radius: 0.375rem;
+ background: #dbdfeb;
+ margin: 0.63rem auto;
+`;
+
+const DateText = styled.div`
+ color: #414350;
+ text-align: center;
+ font-family: 'SF Pro Text';
+ font-size: 0.75rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+`;
+
+// 오늘 날짜를 "dd/mm/yy" 형식의 문자열로 반환하는 함수
+function getTodayDateStringDMY() {
+ return FormatDateToDMY(new Date());
+}
+
+export default function ChattingRoom({ chatList }: Chats) {
+ const nowUser = useSelector((state: RootState) => state.user.nowUser); // 현재 사용자 상태 가져오기
+ const ChattingRoomRef = useRef(null);
+
+ useEffect(() => {
+ if (ChattingRoomRef.current) {
+ ChattingRoomRef.current.scrollTop = ChattingRoomRef.current.scrollHeight;
+ }
+ }, [chatList]); // chatList가 변경될 때마다 useEffect를 다시 실행
+
+ let lastDate = '';
+
+ return (
+
+ {chatList.map((chat) => {
+ const chatDateDMY = FormatDateToDMY(chat.time); // 현재 채팅의 날짜를 "dd/mm/yy" 형식으로 변환
+ let showDateText = false;
+
+ if (chatDateDMY !== lastDate) {
+ lastDate = chatDateDMY;
+ showDateText = true; // 날짜가 변경되었으므로 텍스트 보여주기
+ }
+
+ const todayDateStringDMY = getTodayDateStringDMY();
+ const isToday = chatDateDMY === todayDateStringDMY; // 현재 채팅 날짜가 오늘인지 확인
+ const isSentByMe = chat.senderId === nowUser;
+
+ return (
+
+ {showDateText && (
+
+ {isToday ? 'Today' : chatDateDMY}
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/Chatting/TitleBar.tsx b/src/components/Chatting/TitleBar.tsx
new file mode 100644
index 0000000..ac06cf6
--- /dev/null
+++ b/src/components/Chatting/TitleBar.tsx
@@ -0,0 +1,114 @@
+import { useState, useEffect } from 'react';
+import styled from 'styled-components';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useSelector, useDispatch } from 'react-redux';
+import { changeUser } from '../../features/userSlice';
+import { RootState } from '../../store';
+import { UserProps } from '../../types/interface';
+import { Chats } from '../../types/interface';
+import TopNavBar from '../TopNavBar/TopNavBar';
+import Left from '../../assets/img/left.svg';
+import Call from '../../assets/img/call.svg';
+
+const ProfileContainer = styled.div`
+ display: inline-flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ position: absolute;
+ top: 0.13rem;
+ left: 3.81rem;
+`;
+
+const ProfileImg = styled.img`
+ width: 2.25rem;
+ height: 2.25rem;
+ border-radius: 2.25rem;
+`;
+
+const ProfileInnerContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+`;
+
+const ProfileName = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+
+ color: #000;
+ font-family: 'SF Pro Display';
+ font-size: 1.125rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+`;
+
+const OnlineText = styled.div`
+ color: #898989;
+ font-family: 'SF Pro Text';
+ font-size: 0.75rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.0015rem;
+`;
+
+export default function TitleBar(props: UserProps & { userId: number }) {
+ // 파트너 유저 아이디도 전달
+ const { name, profileImg, isActive } = props;
+ const { chatRoomId } = useParams();
+ const nowUser = useSelector((state: RootState) => state.user.nowUser);
+ const chattings = useSelector((state: RootState) => state.chat.chattings);
+ const [chatRoom, setChatRoom] = useState(null);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ // chatRoomId를 사용하여 해당 채팅방 찾기 및 설정
+ const foundChatRoom = chattings.find(
+ (chat) => chat.chatRoomId.toString() === chatRoomId
+ );
+ setChatRoom(foundChatRoom ?? null);
+ }, [chatRoomId, chattings]); // chattings 또는 chatRoomId 변경 시 effect 실행
+
+ const handleChangeUser = () => {
+ if (chatRoom && chatRoom.userList && chatRoom.userList.length > 0) {
+ const currentChatRoomUserList = chatRoom.userList;
+ const currentIndex = currentChatRoomUserList.findIndex(
+ (userId) => userId === nowUser
+ );
+ const nextUserIndex = (currentIndex + 1) % currentChatRoomUserList.length;
+ const nextUserId = currentChatRoomUserList[nextUserIndex];
+
+ dispatch(changeUser(nextUserId));
+ }
+ };
+
+ return (
+ navigate('/')}
+ >
+
+ navigate('/contact-info', { state: { userId: props.userId } }) // 클릭된 이미지에 해당하는 유저 아이디 넘김
+ }
+ />
+
+ {name}
+ {isActive ? (
+ online
+ ) : (
+ offline
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ContactInfo/ContactInfo.tsx b/src/components/ContactInfo/ContactInfo.tsx
new file mode 100644
index 0000000..511d463
--- /dev/null
+++ b/src/components/ContactInfo/ContactInfo.tsx
@@ -0,0 +1,218 @@
+import styled from 'styled-components';
+import { UserProps } from '../../types/interface';
+import Instagram from '../../assets/img/contact-instagram.svg';
+import Github from '../../assets/img/contact-github.svg';
+import Message from '../../assets/img/contact-message.svg';
+import Call from '../../assets/img/contact-call.svg';
+import Media from '../../assets/img/media.svg';
+import Star from '../../assets/img/star.svg';
+import Search from '../../assets/img/search.svg';
+import Mute from '../../assets/img/mute.svg';
+import Tone from '../../assets/img/tone.svg';
+import RightArrow from '../../assets/img/edit-arrow.svg';
+
+const ContactPageContainer = styled.div`
+ width: 23.4375rem;
+ height: 45.245rem;
+ background: #efeff4;
+ position: relative;
+
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ scrollbar-width: none; // Firefox
+ -ms-overflow-style: none; // Internet Explorer/Edge
+ &::-webkit-scrollbar {
+ display: none; // Chrome, Safari
+ }
+`;
+
+const ProfileImg = styled.img`
+ width: 23.4375rem;
+ height: 23.4375rem;
+`;
+
+const InfoContainer = styled.div`
+ width: 22.4975rem;
+ height: 7.875rem;
+ box-shadow: 0px 0.33px 0px 0px rgba(60, 60, 67, 0.29);
+ padding-left: 0.94rem;
+ background: #fff;
+`;
+
+const InfoBox = styled.div`
+ width: 21.5575rem;
+ padding: 0.69rem 0.94rem 0rem 0rem;
+`;
+
+const NameText = styled.div`
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 1.125rem;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 1.4375rem;
+ letter-spacing: -0.0265rem;
+`;
+
+const SubText = styled.div`
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 0.875rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1rem;
+ letter-spacing: -0.0125rem;
+`;
+
+const InfoText = styled.div`
+ color: #8e8e93;
+ font-family: 'SF Pro Text';
+ font-size: 0.75rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.0015rem;
+`;
+
+const Seperator = styled.div`
+ height: 1px;
+ background: rgba(60, 60, 67, 0.29);
+ margin-left: auto;
+`;
+
+const IconContainer = styled.div`
+ display: flex;
+ align-items: flex-start;
+ gap: 0.625rem;
+ position: absolute;
+ right: 0.9375rem;
+ top: 24.38rem;
+`;
+
+const ContactIconImg = styled.img`
+ display: flex;
+ width: 2.25rem;
+ height: 2.25rem;
+ justify-content: center;
+ align-items: center;
+`;
+
+const ExtraContainer = styled.div`
+ width: 23.4375rem;
+ background: #fff;
+ box-shadow: 0px 0.33px 0px 0px rgba(60, 60, 67, 0.29),
+ 0px -0.33px 0px 0px rgba(60, 60, 67, 0.29);
+ margin-top: 1.22rem;
+`;
+
+const DetailContainer = styled.div`
+ width: 21.4375rem;
+ height: 1.8075rem;
+ padding: 0.5rem 1.06rem 0.63rem 0.94rem;
+ display: flex;
+ align-items: center;
+`;
+
+const ExtraIconImg = styled.img`
+ width: 1.8125rem;
+ height: 1.8125rem;
+ margin-right: 0.94rem;
+`;
+
+const ExtraText = styled.div`
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 1rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.375rem;
+ letter-spacing: -0.02063rem;
+`;
+
+const ExtraDetailText = styled.div`
+ color: rgba(60, 60, 67, 0.6);
+ text-align: right;
+ font-family: 'SF Pro Text';
+ font-size: 1rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.375rem;
+ letter-spacing: -0.0235rem;
+ margin-left: auto;
+`;
+
+const RightArrowImg = styled.img`
+ width: 0.4375rem;
+ height: 0.75rem;
+ margin-left: 0.75rem;
+`;
+
+export default function ContactInfo(props: UserProps) {
+ const { name, profileImg } = props;
+
+ return (
+
+
+
+
+ {name}
+ +44 7496 0000000
+
+
+
+
+
+
+
+
+
+ I’m in Seoul Now!
+ Mar 20, 2024
+
+
+
+
+
+
+ Media, Links, and Docs
+ 12
+
+
+
+
+
+ Starred Messages
+ None
+
+
+
+
+
+ Chat Search
+
+
+
+
+
+
+
+ Mute
+ No
+
+
+
+
+
+ Custom Tone
+ Default (Note)
+
+
+
+
+
+ );
+}
diff --git a/src/components/EditContact/EditContact.tsx b/src/components/EditContact/EditContact.tsx
new file mode 100644
index 0000000..ddb6145
--- /dev/null
+++ b/src/components/EditContact/EditContact.tsx
@@ -0,0 +1,123 @@
+import styled from 'styled-components';
+import RightArrow from '../../assets/img/edit-arrow.svg';
+
+const EditPageContainer = styled.div`
+ width: 22.4375rem;
+ height: 40.93rem;
+ padding: 2.19rem 0rem 0rem 1rem;
+`;
+
+const TextContainer = styled.div`
+ width: 22.4375rem;
+ min-height: 3.125rem;
+ display: flex;
+ justify-content: space-between;
+`;
+
+const SubTextContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`;
+
+const SubTextBox = styled.div`
+ width: 15.4275rem;
+ height: 1.3125rem;
+ padding: 0.88rem 1.01rem 0.94rem 0rem;
+ border-bottom: 0.0625rem solid rgba(60, 60, 67, 0.29);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+`;
+
+const EditText = styled.div`
+ font-family: 'SF Pro Text';
+ font-size: 1rem;
+ font-style: normal;
+ line-height: 1.3125rem;
+ letter-spacing: -0.02063rem;
+`;
+
+const MobileContainer = styled.div`
+ width: 4.92rem;
+ height: 1.305rem;
+ gap: 0.36rem;
+ display: flex;
+ align-items: center;
+ padding: 0.88rem 1.08rem 0.94rem 0rem;
+`;
+
+const RightArrowImg = styled.img`
+ width: 0.4985rem;
+ height: 0.82025rem;
+`;
+
+export default function EditContact() {
+ return (
+
+
+
+ Name
+
+
+
+ Timothée
+
+
+ Chalamet
+
+
+
+
+
+ Phone
+
+
+ United Kingdom
+
+
+
+
+
+ Link
+
+
+ Instagram
+
+
+
+
+
+
+ mobile
+
+
+
+
+ +44 1234 567890
+
+
+
+
+ more fields
+
+
+
+
+ Delete Contact
+
+
+
+ );
+}
diff --git a/src/components/Status/MyStatus.tsx b/src/components/Status/MyStatus.tsx
new file mode 100644
index 0000000..18d4daa
--- /dev/null
+++ b/src/components/Status/MyStatus.tsx
@@ -0,0 +1,127 @@
+import styled from 'styled-components';
+import { useNavigate } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../store';
+import add from '../../assets/img/status-add.svg';
+import camera from '../../assets/img/status-camera.svg';
+import edit from '../../assets/img/status-edit.svg';
+
+const StatusPageContainer = styled.div`
+ width: 23.4375rem;
+ height: 40.06rem;
+ background: #efeff4;
+`;
+
+const MyStatusContainer = styled.div`
+ width: 23.4375rem;
+ height: 4.75rem;
+ background: #fff;
+ box-shadow: 0px 0.33px 0px 0px rgba(60, 60, 67, 0.29),
+ 0px -0.33px 0px 0px rgba(60, 60, 67, 0.29);
+ position: relative;
+ top: 2.19rem;
+`;
+
+const ProfileImg = styled.img`
+ width: 3.625rem;
+ height: 3.625rem;
+ border-radius: 3.625rem;
+ position: absolute;
+ left: 0.81rem;
+ top: 0.56rem;
+`;
+
+const AddImg = styled.img`
+ width: 1.25rem;
+ height: 1.25rem;
+ position: absolute;
+ left: 3.19rem;
+ top: 2.97rem;
+`;
+
+const MyStatusText = styled.div`
+ color: #000;
+ font-family: 'SF Pro Text';
+ font-size: 1rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 1.3125rem; /* 131.25% */
+ letter-spacing: -0.02063rem;
+
+ position: absolute;
+ top: 1.06rem;
+ left: 5rem;
+`;
+
+const SubText = styled.div`
+ color: #8e8e93;
+ font-family: 'SF Pro Text';
+ font-size: 0.875rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1rem; /* 114.286% */
+ letter-spacing: -0.0125rem;
+
+ position: absolute;
+`;
+
+const IconContainer = styled.div`
+ display: flex;
+ width: 5.5rem;
+ height: 2.25rem;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 1rem;
+ position: absolute;
+ top: 1.25rem;
+ left: 16.94rem;
+`;
+
+const IconImg = styled.img`
+ width: auto;
+ height: auto;
+`;
+
+const TipContainer = styled.div`
+ width: 23.4375rem;
+ height: 2.6875rem;
+ background: #fff;
+ box-shadow: 0px 0.33px 0px 0px rgba(60, 60, 67, 0.29),
+ 0px -0.33px 0px 0px rgba(60, 60, 67, 0.29);
+ position: relative;
+ top: 4.75rem;
+`;
+
+export default function MyStatus() {
+ const navigate = useNavigate();
+ const nowUser = useSelector((state: RootState) => state.user.nowUser); // 현재 사용자 ID
+ const userList = useSelector((state: RootState) => state.user.userList);
+ const currentUser = userList.find((user) => user.id === nowUser); // 현재 사용자 정보 찾기
+
+ return (
+
+
+
+
+ {currentUser?.name}
+
+ Give me magarita
+
+
+
+ navigate('/edit-contact')}
+ src={edit}
+ alt="수정 버튼 이미지"
+ />
+
+
+
+
+ No recent updates to show right now.
+
+
+
+ );
+}
diff --git a/src/components/TopNavBar/TopNavBar.tsx b/src/components/TopNavBar/TopNavBar.tsx
new file mode 100644
index 0000000..9113041
--- /dev/null
+++ b/src/components/TopNavBar/TopNavBar.tsx
@@ -0,0 +1,114 @@
+import styled from 'styled-components';
+
+const NavBarContainer = styled.div<{ $noBorderBottom?: boolean }>`
+ width: 23.4375rem;
+ height: 2.12rem;
+ border-bottom: ${(props) =>
+ props.$noBorderBottom ? 'none' : '0.03125rem solid #a4a39e'};
+ position: relative;
+ padding-top: 0.63rem;
+`;
+
+const LeftContainer = styled.div`
+ display: inline-flex;
+ padding: 0.625rem 0.5625rem;
+ align-items: flex-start;
+ gap: 0.3125rem;
+ position: absolute;
+ bottom: 0.13rem;
+ cursor: pointer;
+`;
+
+const LeftImg = styled.img`
+ width: 0.75rem;
+ height: 1.25rem;
+`;
+
+const Text = styled.div`
+ white-space: nowrap;
+ color: ${(props) => (props.$isEditPage ? '#D1D1D6' : '#1bd742')};
+ font-family: 'SF Pro Text';
+ font-size: 1.0625rem;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.375rem;
+ letter-spacing: -0.0255rem;
+ ${(props) =>
+ !props.$hasLeftImg &&
+ `
+ margin-left: 0.44rem; // leftImgSrc가 없을 때 적용될 스타일
+ `}
+`;
+
+const TitleText = styled.div`
+ color: #000;
+ text-align: center;
+ font-family: 'SF Pro Text';
+ font-size: 1.0625rem;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 1.375rem;
+ letter-spacing: -0.025rem;
+`;
+
+const RightContainer = styled.div`
+ display: inline-flex;
+ align-items: flex-start;
+ gap: 1.375rem;
+ position: absolute;
+ top: 0.63rem;
+ right: 0.94rem;
+`;
+
+const IconImg = styled.img`
+ height: 1.5rem;
+`;
+
+interface TextProps {
+ $hasLeftImg: boolean;
+ $isEditPage?: boolean;
+}
+
+interface TopNavBarProps {
+ leftImgSrc?: string;
+ leftText?: string;
+ rightImgSrc?: string;
+ rightText?: string;
+ children?: React.ReactNode;
+ title?: string;
+ leftTextOnClick?: () => void; // 클릭을 통해 이전 페이지로 돌아가기 위함
+ $noBorderBottom?: boolean; // edit-contact page에서는 테두리 없애기 위함
+ $isEditPage?: boolean; // edit-contact page에서 오른쪽 텍스트를 회색으로 지정하기 위함
+}
+
+export default function TopNavBar(props: TopNavBarProps) {
+ const {
+ leftImgSrc,
+ leftText,
+ rightImgSrc,
+ rightText,
+ children,
+ title,
+ leftTextOnClick,
+ $noBorderBottom,
+ $isEditPage = false,
+ } = props;
+ return (
+
+
+ {leftImgSrc && }
+ {leftText}
+
+ {children}
+ {title && {title}}{' '}
+
+ {rightImgSrc && }
+ {rightText && (
+
+ {rightText}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/iphone/HomeIndicator.tsx b/src/components/iphone/HomeIndicator.tsx
new file mode 100644
index 0000000..fcacfd5
--- /dev/null
+++ b/src/components/iphone/HomeIndicator.tsx
@@ -0,0 +1,14 @@
+import styled from 'styled-components';
+import HomeBar from '../../assets/img/home-indicator.svg';
+
+const HomeBarImg = styled.img`
+ width: 8.375rem;
+ height: 0.3125rem;
+ position: absolute;
+ bottom: 0.56rem;
+ left: 7.56rem;
+`;
+
+export default function HomeIndicator() {
+ return ;
+}
diff --git a/src/components/iphone/StatusBar.tsx b/src/components/iphone/StatusBar.tsx
new file mode 100644
index 0000000..5eaac01
--- /dev/null
+++ b/src/components/iphone/StatusBar.tsx
@@ -0,0 +1,60 @@
+import { useState, useEffect } from 'react';
+import styled from 'styled-components';
+import rightSide from '../../assets/img/right-side.svg';
+
+const StatusBarContainer = styled.div`
+ width: 23.4375rem;
+ height: 2.75rem;
+ position: relative;
+`;
+
+const RightSideImg = styled.img`
+ width: 4.16631rem;
+ height: 0.7085rem;
+ position: absolute;
+ top: 1.08rem;
+ right: 0.92rem;
+`;
+
+const TextContainer = styled.div`
+ position: absolute;
+ top: 1rem;
+ left: 1.75rem;
+`;
+
+const ClockText = styled.div`
+ color: #000;
+ text-align: center;
+ font-family: 'SF Pro Text';
+ font-size: 0.875rem;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 100%; /* 0.875rem */
+`;
+
+export default function StatusBar() {
+ const [currentTime, setCurrentTime] = useState('');
+
+ useEffect(() => {
+ function getCurrentTime() {
+ const now = new Date();
+ const hours = String(now.getHours()).padStart(2, '0');
+ const minutes = String(now.getMinutes()).padStart(2, '0');
+ setCurrentTime(`${hours}:${minutes}`);
+ }
+
+ getCurrentTime();
+ const intervalId = setInterval(getCurrentTime, 1000); // 1초마다 getCurrentTime을 실행
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return (
+
+
+ {currentTime}
+
+
+
+ );
+}
diff --git a/src/features/chatSlice.ts b/src/features/chatSlice.ts
new file mode 100644
index 0000000..d535961
--- /dev/null
+++ b/src/features/chatSlice.ts
@@ -0,0 +1,45 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import initialChatData from '../assets/data/initialChatData.json';
+
+const initialState = initialChatData;
+
+export const chatSlice = createSlice({
+ name: 'chat',
+ initialState,
+ reducers: {
+ addChat: (
+ state,
+ action: PayloadAction<{
+ chatRoomId: number;
+ senderId: number;
+ content: string;
+ time: string;
+ isRead: boolean;
+ }>
+ ) => {
+ const { chatRoomId, senderId, content, time, isRead } = action.payload;
+ const chatRoom = state.chattings.find(
+ (room) => room.chatRoomId === chatRoomId
+ );
+ if (chatRoom) {
+ const newChatId =
+ chatRoom.chatList.length > 0
+ ? chatRoom.chatList[chatRoom.chatList.length - 1].id + 1 // 마지막 항목의 id+1로 id 설정
+ : 0;
+ chatRoom.chatList.push({
+ id: newChatId,
+ senderId,
+ content,
+ time,
+ isRead,
+ });
+ }
+ },
+ },
+});
+
+// Actions export
+export const { addChat } = chatSlice.actions;
+
+// Reducer export
+export default chatSlice.reducer;
diff --git a/src/features/userSlice.ts b/src/features/userSlice.ts
new file mode 100644
index 0000000..3ada918
--- /dev/null
+++ b/src/features/userSlice.ts
@@ -0,0 +1,27 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { UserProps } from '../types/interface';
+import UserData from '../assets/data/userData.json';
+
+interface UserState {
+ nowUser: number;
+ userList: UserProps[];
+}
+
+const initialState: UserState = {
+ nowUser: 0,
+ userList: UserData.users,
+};
+
+export const userSlice = createSlice({
+ name: 'user',
+ initialState,
+ reducers: {
+ changeUser: (state, action: PayloadAction) => {
+ state.nowUser = action.payload;
+ },
+ },
+});
+
+export const { changeUser } = userSlice.actions;
+
+export default userSlice.reducer;
diff --git a/src/index.css b/src/index.css
index ec2585e..e7719be 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,13 +1,34 @@
-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;
+@font-face {
+ font-family: 'SF Pro Text';
+ font-style: normal;
+ font-weight: 400;
+ src: url('https://raw.githubusercontent.com/blaisck/sfwin/master/SFPro/TrueType/SFProText-Regular.ttf');
+}
+
+@font-face {
+ font-family: 'SF Pro Text';
+ font-style: normal;
+ font-weight: 500;
+ src: url('https://raw.githubusercontent.com/blaisck/sfwin/master/SFPro/TrueType/SFProText-Medium.ttf');
+}
+
+@font-face {
+ font-family: 'SF Pro Text';
+ font-style: normal;
+ font-weight: 600;
+ src: url('https://raw.githubusercontent.com/blaisck/sfwin/master/SFPro/TrueType/SFProText-SemiBold.ttf');
+}
+
+@font-face {
+ font-family: 'SF Pro Text';
+ font-style: normal;
+ font-weight: 700;
+ src: url('https://raw.githubusercontent.com/blaisck/sfwin/master/SFPro/TrueType/SFProText-Bold.ttf');
+}
+
+@font-face {
+ font-family: 'SF Pro Display';
+ font-style: normal;
+ font-weight: 500;
+ src: url('https://raw.githubusercontent.com/blaisck/sfwin/master/SFPro/TrueType/SFProDisplay-Medium.ttf');
}
diff --git a/src/index.tsx b/src/index.tsx
index d10be77..3be48dc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,13 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
-import './index.css';
+import { Provider } from 'react-redux';
+import { store } from './store';
import App from './App';
+import './index.css';
const root = ReactDOM.createRoot(
- document.getElementById('root') as HTMLElement
+ document.getElementById('root') as HTMLElement
);
root.render(
-
-
-
+
+
+
+
+
);
diff --git a/src/pages/ChatsPage.tsx b/src/pages/ChatsPage.tsx
new file mode 100644
index 0000000..9fbfc44
--- /dev/null
+++ b/src/pages/ChatsPage.tsx
@@ -0,0 +1,14 @@
+import BottomTabBar from '../components/BottomTabBar/BottomTabBar';
+import ChatsList from '../components/Chats/ChatsList';
+import TopNavBar from '../components/TopNavBar/TopNavBar';
+import Edit from '../assets/img/edit.svg';
+
+export default function ChatsPage() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/pages/ChattingPage.tsx b/src/pages/ChattingPage.tsx
new file mode 100644
index 0000000..aa973cb
--- /dev/null
+++ b/src/pages/ChattingPage.tsx
@@ -0,0 +1,62 @@
+import { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { RootState } from '../store';
+import { UserProps } from '../types/interface';
+import { Chats } from '../types/interface';
+import TitleBar from '../components/Chatting/TitleBar';
+import ChattingRoom from '../components/Chatting/ChattingRoom';
+import ChatInput from '../components/Chatting/ChatInput';
+
+export default function ChattingPage() {
+ const { chatRoomId } = useParams(); // URL에서 채팅방 ID를 추출
+ const nowUser = useSelector((state: RootState) => state.user.nowUser);
+ const userList = useSelector((state: RootState) => state.user.userList);
+ const chattings = useSelector((state: RootState) => state.chat.chattings);
+ const [partner, setPartner] = useState(null);
+ const [chatRoom, setChatRoom] = useState(null);
+
+ useEffect(() => {
+ // chatRoomId를 사용하여 해당 채팅방의 userList를 찾기
+ const foundChatRoom = chattings.find(
+ (chat) => chat.chatRoomId.toString() === chatRoomId
+ );
+ if (foundChatRoom) {
+ setChatRoom(foundChatRoom); // 찾은 채팅방을 상태로 설정
+ // 현재 유저 제외한 상대 유저 찾기
+ const nextUser =
+ userList.find(
+ (user) =>
+ foundChatRoom.userList.includes(user.id!) && user.id !== nowUser
+ ) ?? null;
+ setPartner(nextUser);
+ } else {
+ setChatRoom(null); // 찾을 수 없으면 null로 설정
+ }
+ }, [chatRoomId, nowUser, userList, chattings]); // 넷 중 하나라도 변경될 때마다 effect를 실행
+
+ if (chatRoom && partner !== null) {
+ return (
+ <>
+
+
+
+ >
+ );
+ } else {
+ return 파트너 정보가 없습니다.
;
+ }
+}
diff --git a/src/pages/ContactInfoPage.tsx b/src/pages/ContactInfoPage.tsx
new file mode 100644
index 0000000..db70be0
--- /dev/null
+++ b/src/pages/ContactInfoPage.tsx
@@ -0,0 +1,47 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { RootState } from '../store';
+import { UserProps } from '../types/interface';
+import FormatName from '../utils/formatName';
+import TopNavBar from '../components/TopNavBar/TopNavBar';
+import ContactInfo from '../components/ContactInfo/ContactInfo';
+import Left from '../assets/img/left.svg';
+
+export default function ContactInfoPage() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const userList = useSelector((state: RootState) => state.user.userList);
+ const [partner, setPartner] = useState(null);
+ const { userId } = location.state || {}; // 전달받은 userId
+
+ // 이전 페이지로 돌아가는 함수
+ const goBack = () => {
+ navigate(-1);
+ };
+
+ useEffect(() => {
+ // 전달받은 userId에 해당하는 사용자 정보 찾기
+ const nextUser = userList.find((user) => user.id === userId) ?? null;
+ setPartner(nextUser);
+ }, [userId, userList]);
+
+ return (
+ <>
+ {partner !== null ? (
+ <>
+
+
+ >
+ ) : (
+ 파트너 정보가 없습니다.
+ )}
+ >
+ );
+}
diff --git a/src/pages/EditContactPage.tsx b/src/pages/EditContactPage.tsx
new file mode 100644
index 0000000..c6b8a85
--- /dev/null
+++ b/src/pages/EditContactPage.tsx
@@ -0,0 +1,26 @@
+import { useNavigate } from 'react-router-dom';
+import TopNavBar from '../components/TopNavBar/TopNavBar';
+import EditContact from '../components/EditContact/EditContact';
+
+export default function EditContactPage() {
+ const navigate = useNavigate();
+
+ // 이전 페이지로 돌아가는 함수
+ const goBack = () => {
+ navigate(-1);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/pages/StatusPage.tsx b/src/pages/StatusPage.tsx
new file mode 100644
index 0000000..687afb8
--- /dev/null
+++ b/src/pages/StatusPage.tsx
@@ -0,0 +1,13 @@
+import TopNavBar from '../components/TopNavBar/TopNavBar';
+import BottomTabBar from '../components/BottomTabBar/BottomTabBar';
+import MyStatus from '../components/Status/MyStatus';
+
+export default function StatusPage() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..dde7770
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,16 @@
+import { configureStore } from '@reduxjs/toolkit';
+import chatReducer from './features/chatSlice';
+import userReducer from './features/userSlice';
+
+export const store = configureStore({
+ reducer: {
+ chat: chatReducer,
+ user: userReducer,
+ },
+});
+
+// 스토어의 상태 타입을 추론하기 위한 타입
+export type RootState = ReturnType;
+
+// App 디스패치 함수의 타입을 추론하기 위한 타입
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/style/GlobalStyles.tsx b/src/style/GlobalStyles.tsx
new file mode 100644
index 0000000..3278efa
--- /dev/null
+++ b/src/style/GlobalStyles.tsx
@@ -0,0 +1,32 @@
+import { createGlobalStyle } from 'styled-components';
+import reset from 'styled-reset';
+
+const GlobalStyle = createGlobalStyle`
+ ${reset}
+ html {
+ height: 100dvh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #A7A7A7;
+ }
+
+ body {
+ width: 23.4375rem;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ #root {
+ height: 100dvh;
+ width: 100%;
+ }
+
+ input {
+ padding: 0;
+ }
+`;
+
+export default GlobalStyle;
diff --git a/src/types/interface.ts b/src/types/interface.ts
new file mode 100644
index 0000000..35faf1a
--- /dev/null
+++ b/src/types/interface.ts
@@ -0,0 +1,25 @@
+// 재사용하는 경우 분리해놓음
+export interface UserProps {
+ id?: number;
+ name: string;
+ profileImg: string;
+ isActive?: boolean;
+}
+
+export interface Chats {
+ chatRoomId: number;
+ userList: number[];
+ chatList: {
+ id: number;
+ senderId: number;
+ content: string;
+ time: string;
+ isRead: boolean;
+ }[];
+}
+
+export interface ChatProps {
+ lastChatContent: string;
+ lastChatTime: string;
+ $isRead: boolean;
+}
diff --git a/src/utils/formatDateToDMY.ts b/src/utils/formatDateToDMY.ts
new file mode 100644
index 0000000..f120b9b
--- /dev/null
+++ b/src/utils/formatDateToDMY.ts
@@ -0,0 +1,11 @@
+// 채팅 시간을 "dd/mm/yy" 형식의 문자열로 변환하는 함수
+export default function formatDateToDMY(dateString: Date | string) {
+ const date = new Date(dateString);
+ const day = date.getDate();
+ const month = date.getMonth() + 1; // 월은 0부터 시작하므로 1을 더해줌
+ const year = date.getFullYear() % 100; // 년도의 마지막 두 자리만 가져옴
+
+ return `${day < 10 ? `0${day}` : day}/${
+ month < 10 ? `0${month}` : month
+ }/${year}`;
+}
diff --git a/src/utils/formatName.ts b/src/utils/formatName.ts
new file mode 100644
index 0000000..c085314
--- /dev/null
+++ b/src/utils/formatName.ts
@@ -0,0 +1,9 @@
+// 이름 짧게 변환하는 로직
+export default function formatName(name: string) {
+ const parts = name.split(' ');
+ if (parts.length > 1) {
+ const lastInitial = parts.pop()!.charAt(0); // 띄어쓰기 다음 첫 글자 구하기. parts.pop()의 결과가 undefined가 아니라는 것을 명시하기 위해 !를 사용함
+ return `${parts.join(' ')} ${lastInitial}.`;
+ }
+ return name; // 이름에 공백이 없는 경우 변환 없이 반환
+}
diff --git a/src/utils/formatTimeToAMPM.ts b/src/utils/formatTimeToAMPM.ts
new file mode 100644
index 0000000..f0d1a9f
--- /dev/null
+++ b/src/utils/formatTimeToAMPM.ts
@@ -0,0 +1,13 @@
+// 채팅 시간을 "09:41am" 형식의 문자열로 변환하는 함수
+export default function formatTimeToAMPM(dateInput: Date | string): string {
+ const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
+
+ let hours = date.getHours();
+ let minutes = date.getMinutes();
+ const ampm = hours >= 12 ? 'pm' : 'am';
+ hours = hours % 12 || 12; // 시간이 0이면 12로 변환
+ const minutesStr = `${minutes < 10 ? '0' : ''}${minutes}`;
+ const strTime = `${hours}:${minutesStr}${ampm}`;
+
+ return strTime;
+}
diff --git a/tsconfig.json b/tsconfig.json
index a273b0c..573e866 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -20,7 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
- "include": [
- "src"
- ]
+ "include": ["src/**/*", "custom.d.ts"]
}