diff --git a/.env b/.env
new file mode 100644
index 0000000..515caf7
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+REACT_APP_VERSION = 1.1.0
\ No newline at end of file
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 6ab8e98..843890a 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,17 @@
-# 서론
-
-안녕하세요 🙌🏻 18기 프론트 운영진 김문기입니다. 이번 미션에서는 드디어 투두리스트에서 벗어나 새로운 프로젝트인 **messenger** 만들기를 진행합니다.
-
-이번주는 특별히 **디자이너와의 협업**으로 진행되는 미션입니다. 디자이너분께서 열심히 리디자인 한 메신저 프로젝트를 여러분들께서 구현해주시면 됩니다.
-
-동시에, 이번주부터는 새로 **TypeScript**를 적용해보려고 합니다.
-
-프로젝트의 규모가 커지게 될 수록, 컴포넌트가 가지는 props의 종류 또한 다양해지게 됩니다. 무지성 코딩을 하다보면 가끔 이 props의 구성과 이름, 어떤 타입이 들어가야 하는지 헷갈리기 마련이죠. 보통 그럴 때 다시 컴포넌트 정의 부분으로 돌아가 props의 구성을 보고 오곤 합니다.
-
-하지만 이럴 때, typescript를 적용한다면 컴포넌트의 구성과 그 이름, 심지어 타입까지 한번에 자동완성으로 편리하게 관리할 수 있고, 생산성이 증대되겠죠.
-
-또한, **React Hooks**에 조금 더 익숙해지는 것을 목표로 합니다. 여러 Hook들이 있지만 그 중에서도 `useState`, `useEffect`, `useRef`를 중점적으로 사용해 보는 미션인데요, React를 사용하면서 굉장히 자주 쓰이는 Hook들이기 때문에 이 부분을 집중적으로 공부해 보아요!
-
-그럼 이번 미션도 파이팅입니다!!🎉
-
-# 미션
-
-## Key Questions
-
-- JavaScript를 사용할때에 비해 TypeScript를 사용할 때의 장점은 무엇인가요?
-- 디자이너로부터 전달받은 피그마 링크 혹은, 피그마 캡처본
-- 컴포넌트를 분리한 기준은 무엇인가요?
-- 디자인 시스템을 적용하면서 느낀 점은 무엇인가요?
-- 디자이너와 소통하며 느낀점은 무엇인가요?
-
-## 미션 목표
-
-- TypeScript를 사용해봅시다.
-- useState로 컴포넌트의 상태를 관리합니다.
-- useEffect와 useRef의 사용법을 이해합니다.
-- styled-components를 통한 CSS-in-JS 및 CSS Preprocessor의 사용법에 익숙해집니다.
-
-## 기한
-
-2023년 9월 29일 금요일
-
-## 필수 구현 기능
-
-- 피그마를 보고 [결과화면](https://3th-fb-messenger.netlify.app)과 같이 구현합니다.
-- 디자인 시스템을 구축합니다.
-- 채팅방 상단의 프로필을 클릭하면 사용자를 변경할 수 있습니다.
-- 메세지를 보내면 채팅방 하단으로 스크롤을 이동시킵니다.
-- 메세지에 유저 정보(프로필 사진, 이름)를 표시합니다.
-- user와 message 데이터를 json 파일에 저장합니다.
-- UI는 **반응형을 제외**하고 피그마파일을 따라서 진행합니다.
-
-### 추가 구현 기능
-
-- 더블 클릭 하면 감정표현을 추가합니다.
-- 그 외 추가하고 싶은 기능이 있다면 마음껏 추가해 주세요!
-
-참고로 이번 과제는 다음주까지 이어지는 과제이므로 **확장성**을 충분히 고려해 주세요. 참고로 **4주차 과제에서는 유저 및 기능 추가와 Routing을** 진행합니다. 이를 위해 [recoil](https://recoiljs.org/ko/)이나 [redux](https://ko.redux.js.org/introduction/getting-started/)를 이용한 상태관리를 미리 해보시는 것을 추천합니다!! 모두 공식문서 많이 읽어보시고 자신만의 상태관리 조합도 찾아보면 재밌을 거에요 XD
-
-## 링크 및 참고자료
-
-- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html)
-- [React의 Hooks 완벽 정복하기](https://velog.io/@velopert/react-hooks#1-usestate)
-- [useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/)
-- [코딩 컨벤션](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION)
-- [타입스크립트 핸드북](https://joshua1988.github.io/ts/intro.html)
-- [리액트 프로젝트에서 타입스크립트 사용하기 (시리즈)](https://velog.io/@velopert/series/react-with-typescript)
-- [디자인 시스템 구축기](https://yozm.wishket.com/magazine/detail/1830/)
-- [[영상] : 컴포넌트에 대한 이해](https://www.youtube.com/watch?v=21eiJc90ggo)
-- [Styled Component로 디자인 시스템 구축하기](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/)
-- [ts 절대경로 설정하기](https://tesseractjh.tistory.com/232)
+# 배포
+
+[오대균의 Todo List](https://ceos-line-plus.vercel.app/)
+
+# Key Features
+
+- 모바일과 데스크탑 환경 모두 대응
+- 모바일과 데스크탑의 작동 방식 차별화
+- react router dom을 활용한 라우팅
+- useState(local)와 zustand(global)를 활용한 상태 관리
+- styled components를 활용한 스타일링
+- 읽음 기능
+- 메시지 좋아요 기능
+- 홈화면에서 원하는 유저를 더블 클릭하면 main 유저 변경 기능
+- 새로운 대화 시작 기능
+- 홈, 채팅목록, 새로운 채팅 페이지에서 검색 기능
+- 유저 프로필 변경 기능
diff --git a/package-lock.json b/package-lock.json
index 82a715f..e54519f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,13 +8,24 @@
"name": "react-messenger-18th",
"version": "0.1.0",
"dependencies": {
- "@testing-library/jest-dom": "^5.17.0",
- "@testing-library/react": "^13.4.0",
- "@testing-library/user-event": "^13.5.0",
+ "browser-image-compression": "^2.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
- "web-vitals": "^2.1.4"
+ "react-spinners": "^0.13.8",
+ "styled-components": "^6.0.8",
+ "styled-reset": "^4.5.1",
+ "zustand": "^4.4.1"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.5",
+ "@types/node": "^20.6.5",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-router-dom": "^5.3.3",
+ "@types/styled-components": "^5.1.27",
+ "typescript": "^5.2.2"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -25,11 +36,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/@adobe/css-tools": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
- "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg=="
- },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -53,6 +59,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 +613,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 +693,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 +2386,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",
@@ -2680,6 +2814,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
"integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
"dependencies": {
"jest-get-type": "^29.6.3"
},
@@ -2691,6 +2826,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
@@ -3132,6 +3268,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 +3383,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,308 +3709,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",
- "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==",
- "dependencies": {
- "@adobe/css-tools": "^4.0.1",
- "@babel/runtime": "^7.9.2",
- "@types/testing-library__jest-dom": "^5.9.1",
- "aria-query": "^5.0.0",
- "chalk": "^3.0.0",
- "css.escape": "^1.5.1",
- "dom-accessibility-api": "^0.5.6",
- "lodash": "^4.17.15",
- "redent": "^3.0.0"
- },
- "engines": {
- "node": ">=8",
- "npm": ">=6",
- "yarn": ">=1"
- }
- },
- "node_modules/@testing-library/jest-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==",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@testing-library/jest-dom/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/jest-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==",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@testing-library/jest-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=="
- },
- "node_modules/@testing-library/jest-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==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/jest-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==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/react": {
- "version": "13.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz",
- "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==",
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "@testing-library/dom": "^8.5.0",
- "@types/react-dom": "^18.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- }
- },
- "node_modules/@testing-library/react/node_modules/@testing-library/dom": {
- "version": "8.20.1",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
- "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==",
- "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": ">=12"
- }
- },
- "node_modules/@testing-library/react/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==",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@testing-library/react/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==",
- "dependencies": {
- "deep-equal": "^2.0.5"
- }
- },
- "node_modules/@testing-library/react/node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "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/react/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==",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/@testing-library/react/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=="
- },
- "node_modules/@testing-library/react/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==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/react/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==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/user-event": {
- "version": "13.5.0",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
- "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
- "dependencies": {
- "@babel/runtime": "^7.12.5"
- },
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- },
- "peerDependencies": {
- "@testing-library/dom": ">=7.21.4"
- }
- },
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -3877,11 +3725,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/@types/aria-query": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz",
- "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ=="
- },
"node_modules/@types/babel__core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz",
@@ -4006,6 +3849,22 @@
"@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==",
+ "dev": true
+ },
+ "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==",
+ "dev": true,
+ "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",
@@ -4049,6 +3908,7 @@
"version": "29.5.5",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz",
"integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==",
+ "dev": true,
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
@@ -4058,6 +3918,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
"dependencies": {
"@sinclair/typebox": "^0.27.8"
},
@@ -4069,6 +3930,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
+ "dev": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -4084,12 +3946,14 @@
"node_modules/@types/jest/node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
- "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true
},
"node_modules/@types/jest/node_modules/@types/yargs": {
"version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
"integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
+ "dev": true,
"dependencies": {
"@types/yargs-parser": "*"
}
@@ -4098,6 +3962,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -4112,6 +3977,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -4127,6 +3993,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -4137,12 +4004,14 @@
"node_modules/@types/jest/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=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/@types/jest/node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+ "dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
@@ -4151,6 +4020,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
"integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
"dependencies": {
"@jest/expect-utils": "^29.7.0",
"jest-get-type": "^29.6.3",
@@ -4166,6 +4036,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -4174,6 +4045,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
"integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+ "dev": true,
"dependencies": {
"chalk": "^4.0.0",
"diff-sequences": "^29.6.3",
@@ -4188,6 +4060,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+ "dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
@@ -4196,6 +4069,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
"integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+ "dev": true,
"dependencies": {
"chalk": "^4.0.0",
"jest-diff": "^29.7.0",
@@ -4210,6 +4084,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
+ "dev": true,
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.6.3",
@@ -4229,6 +4104,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
+ "dev": true,
"dependencies": {
"@jest/types": "^29.6.3",
"@types/node": "*",
@@ -4245,6 +4121,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
@@ -4258,6 +4135,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -4268,12 +4146,14 @@
"node_modules/@types/jest/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
},
"node_modules/@types/jest/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==",
+ "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -4297,9 +4177,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.6.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.5.tgz",
+ "integrity": "sha512-2qGq5LAOTh9izcc0+F+dToFigBWiK1phKPt7rNhOqJSr35y8rlIBjDwGtFSgAI6MGIhjwOVNSQZVdJsZJ2uR1w=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -4314,7 +4194,8 @@
"node_modules/@types/prop-types": {
"version": "15.7.7",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.7.tgz",
- "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog=="
+ "integrity": "sha512-FbtmBWCcSa2J4zL781Zf1p5YUBXQomPEcep9QZCfRfQgTxz3pJWiDFLebohZ9fFntX5ibzOkSsrJ0TEew8cAog==",
+ "devOptional": true
},
"node_modules/@types/q": {
"version": "1.5.6",
@@ -4335,6 +4216,7 @@
"version": "18.2.22",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.22.tgz",
"integrity": "sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==",
+ "devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4345,10 +4227,32 @@
"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==",
+ "dev": true,
"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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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",
@@ -4365,7 +4269,8 @@
"node_modules/@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
+ "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
+ "devOptional": true
},
"node_modules/@types/semver": {
"version": "7.5.2",
@@ -4412,14 +4317,22 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
- "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",
- "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==",
+ "node_modules/@types/styled-components": {
+ "version": "5.1.27",
+ "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.27.tgz",
+ "integrity": "sha512-oY9c1SdztRRF0QDQdwXEenfAjGN4WGUkaMpx5hvdTbYYqw01qoY2GrHi+kAR6SVofynzD6KbGoF5ITP0zh5pvg==",
+ "dev": true,
"dependencies": {
- "@types/jest": "*"
+ "@types/hoist-non-react-statics": "*",
+ "@types/react": "*",
+ "csstype": "^3.0.2"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw=="
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz",
@@ -5710,6 +5623,14 @@
"node": ">=8"
}
},
+ "node_modules/browser-image-compression": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
+ "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
+ "dependencies": {
+ "uzip": "0.20201231.0"
+ }
+ },
"node_modules/browser-process-hrtime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
@@ -5826,6 +5747,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 +6190,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 +6379,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",
@@ -6473,11 +6420,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/css.escape": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
- },
"node_modules/cssdb": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.2.tgz",
@@ -6683,34 +6625,6 @@
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="
},
- "node_modules/deep-equal": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz",
- "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==",
- "dependencies": {
- "array-buffer-byte-length": "^1.0.0",
- "call-bind": "^1.0.2",
- "es-get-iterator": "^1.1.3",
- "get-intrinsic": "^1.2.1",
- "is-arguments": "^1.1.1",
- "is-array-buffer": "^3.0.2",
- "is-date-object": "^1.0.5",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.2",
- "isarray": "^2.0.5",
- "object-is": "^1.1.5",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.4",
- "regexp.prototype.flags": "^1.5.0",
- "side-channel": "^1.0.4",
- "which-boxed-primitive": "^1.0.2",
- "which-collection": "^1.0.1",
- "which-typed-array": "^1.1.9"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6903,11 +6817,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/dom-accessibility-api": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
- },
"node_modules/dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -7162,25 +7071,6 @@
"resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="
},
- "node_modules/es-get-iterator": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
- "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.3",
- "has-symbols": "^1.0.3",
- "is-arguments": "^1.1.1",
- "is-map": "^2.0.2",
- "is-set": "^2.0.2",
- "is-string": "^1.0.7",
- "isarray": "^2.0.5",
- "stop-iteration-iterator": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/es-iterator-helpers": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz",
@@ -8549,6 +8439,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 +8799,21 @@
"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==",
+ "dev": true,
+ "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==",
+ "dev": true
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -9244,14 +9154,6 @@
"node": ">=0.8.19"
}
},
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -9292,21 +9194,6 @@
"node": ">= 10"
}
},
- "node_modules/is-arguments": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
- "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -12155,14 +12042,6 @@
"yallist": "^3.0.2"
}
},
- "node_modules/lz-string": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "bin": {
- "lz-string": "bin/bin.js"
- }
- },
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@@ -12301,14 +12180,6 @@
"node": ">=6"
}
},
- "node_modules/min-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
- "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/mini-css-extract-plugin": {
"version": "2.7.6",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz",
@@ -12583,21 +12454,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/object-is": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
- "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -14686,6 +14542,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",
@@ -14758,6 +14644,15 @@
}
}
},
+ "node_modules/react-spinners": {
+ "version": "0.13.8",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
+ "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14801,18 +14696,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/redent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
- "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
- "dependencies": {
- "indent-string": "^4.0.0",
- "strip-indent": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -15513,6 +15396,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",
@@ -15809,17 +15697,6 @@
"node": ">= 0.8"
}
},
- "node_modules/stop-iteration-iterator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
- "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
- "dependencies": {
- "internal-slot": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -15972,17 +15849,6 @@
"node": ">=6"
}
},
- "node_modules/strip-indent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
- "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
- "dependencies": {
- "min-indent": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -16009,6 +15875,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 +15944,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 +16578,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": {
@@ -16807,6 +16731,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",
@@ -16847,6 +16779,11 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/uzip": {
+ "version": "0.20201231.0",
+ "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
+ "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="
+ },
"node_modules/v8-to-istanbul": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
@@ -16916,11 +16853,6 @@
"minimalistic-assert": "^1.0.0"
}
},
- "node_modules/web-vitals": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
- "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="
- },
"node_modules/webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
@@ -17836,6 +17768,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
+ "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 49b3308..8103134 100644
--- a/package.json
+++ b/package.json
@@ -3,13 +3,15 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@testing-library/jest-dom": "^5.17.0",
- "@testing-library/react": "^13.4.0",
- "@testing-library/user-event": "^13.5.0",
+ "browser-image-compression": "^2.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
- "web-vitals": "^2.1.4"
+ "react-spinners": "^0.13.8",
+ "styled-components": "^6.0.8",
+ "styled-reset": "^4.5.1",
+ "zustand": "^4.4.1"
},
"scripts": {
"start": "react-scripts start",
@@ -34,5 +36,17 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "@types/jest": "^29.5.5",
+ "@types/node": "^20.6.5",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-router-dom": "^5.3.3",
+ "@types/styled-components": "^5.1.27",
+ "typescript": "^5.2.2"
+ },
+ "overrides": {
+ "typescript": "^5.2.2"
}
}
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index a11777c..0000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/favicon/android-icon-144x144.png b/public/favicon/android-icon-144x144.png
new file mode 100644
index 0000000..3cfbed0
Binary files /dev/null and b/public/favicon/android-icon-144x144.png differ
diff --git a/public/favicon/android-icon-192x192.png b/public/favicon/android-icon-192x192.png
new file mode 100644
index 0000000..58ef396
Binary files /dev/null and b/public/favicon/android-icon-192x192.png differ
diff --git a/public/favicon/android-icon-36x36.png b/public/favicon/android-icon-36x36.png
new file mode 100644
index 0000000..b1185d5
Binary files /dev/null and b/public/favicon/android-icon-36x36.png differ
diff --git a/public/favicon/android-icon-48x48.png b/public/favicon/android-icon-48x48.png
new file mode 100644
index 0000000..bc167b1
Binary files /dev/null and b/public/favicon/android-icon-48x48.png differ
diff --git a/public/favicon/android-icon-72x72.png b/public/favicon/android-icon-72x72.png
new file mode 100644
index 0000000..9d1aefb
Binary files /dev/null and b/public/favicon/android-icon-72x72.png differ
diff --git a/public/favicon/android-icon-96x96.png b/public/favicon/android-icon-96x96.png
new file mode 100644
index 0000000..5319174
Binary files /dev/null and b/public/favicon/android-icon-96x96.png differ
diff --git a/public/favicon/apple-icon-114x114.png b/public/favicon/apple-icon-114x114.png
new file mode 100644
index 0000000..f0d440c
Binary files /dev/null and b/public/favicon/apple-icon-114x114.png differ
diff --git a/public/favicon/apple-icon-120x120.png b/public/favicon/apple-icon-120x120.png
new file mode 100644
index 0000000..74e6fff
Binary files /dev/null and b/public/favicon/apple-icon-120x120.png differ
diff --git a/public/favicon/apple-icon-144x144.png b/public/favicon/apple-icon-144x144.png
new file mode 100644
index 0000000..3cfbed0
Binary files /dev/null and b/public/favicon/apple-icon-144x144.png differ
diff --git a/public/favicon/apple-icon-152x152.png b/public/favicon/apple-icon-152x152.png
new file mode 100644
index 0000000..e505921
Binary files /dev/null and b/public/favicon/apple-icon-152x152.png differ
diff --git a/public/favicon/apple-icon-180x180.png b/public/favicon/apple-icon-180x180.png
new file mode 100644
index 0000000..1c02315
Binary files /dev/null and b/public/favicon/apple-icon-180x180.png differ
diff --git a/public/favicon/apple-icon-57x57.png b/public/favicon/apple-icon-57x57.png
new file mode 100644
index 0000000..fa5d3da
Binary files /dev/null and b/public/favicon/apple-icon-57x57.png differ
diff --git a/public/favicon/apple-icon-60x60.png b/public/favicon/apple-icon-60x60.png
new file mode 100644
index 0000000..9d87654
Binary files /dev/null and b/public/favicon/apple-icon-60x60.png differ
diff --git a/public/favicon/apple-icon-72x72.png b/public/favicon/apple-icon-72x72.png
new file mode 100644
index 0000000..9d1aefb
Binary files /dev/null and b/public/favicon/apple-icon-72x72.png differ
diff --git a/public/favicon/apple-icon-76x76.png b/public/favicon/apple-icon-76x76.png
new file mode 100644
index 0000000..1461c3b
Binary files /dev/null and b/public/favicon/apple-icon-76x76.png differ
diff --git a/public/favicon/apple-icon-precomposed.png b/public/favicon/apple-icon-precomposed.png
new file mode 100644
index 0000000..4275361
Binary files /dev/null and b/public/favicon/apple-icon-precomposed.png differ
diff --git a/public/favicon/apple-icon.png b/public/favicon/apple-icon.png
new file mode 100644
index 0000000..4275361
Binary files /dev/null and b/public/favicon/apple-icon.png differ
diff --git a/public/favicon/browserconfig.xml b/public/favicon/browserconfig.xml
new file mode 100644
index 0000000..c554148
--- /dev/null
+++ b/public/favicon/browserconfig.xml
@@ -0,0 +1,2 @@
+
+#ffffff
\ No newline at end of file
diff --git a/public/favicon/favicon-16x16.png b/public/favicon/favicon-16x16.png
new file mode 100644
index 0000000..410fa10
Binary files /dev/null and b/public/favicon/favicon-16x16.png differ
diff --git a/public/favicon/favicon-32x32.png b/public/favicon/favicon-32x32.png
new file mode 100644
index 0000000..650d0c6
Binary files /dev/null and b/public/favicon/favicon-32x32.png differ
diff --git a/public/favicon/favicon-96x96.png b/public/favicon/favicon-96x96.png
new file mode 100644
index 0000000..5319174
Binary files /dev/null and b/public/favicon/favicon-96x96.png differ
diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico
new file mode 100644
index 0000000..ec6666d
Binary files /dev/null and b/public/favicon/favicon.ico differ
diff --git a/public/favicon/manifest.json b/public/favicon/manifest.json
new file mode 100644
index 0000000..013d4a6
--- /dev/null
+++ b/public/favicon/manifest.json
@@ -0,0 +1,41 @@
+{
+ "name": "App",
+ "icons": [
+ {
+ "src": "\/android-icon-36x36.png",
+ "sizes": "36x36",
+ "type": "image\/png",
+ "density": "0.75"
+ },
+ {
+ "src": "\/android-icon-48x48.png",
+ "sizes": "48x48",
+ "type": "image\/png",
+ "density": "1.0"
+ },
+ {
+ "src": "\/android-icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image\/png",
+ "density": "1.5"
+ },
+ {
+ "src": "\/android-icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image\/png",
+ "density": "2.0"
+ },
+ {
+ "src": "\/android-icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image\/png",
+ "density": "3.0"
+ },
+ {
+ "src": "\/android-icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image\/png",
+ "density": "4.0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/favicon/ms-icon-144x144.png b/public/favicon/ms-icon-144x144.png
new file mode 100644
index 0000000..3cfbed0
Binary files /dev/null and b/public/favicon/ms-icon-144x144.png differ
diff --git a/public/favicon/ms-icon-150x150.png b/public/favicon/ms-icon-150x150.png
new file mode 100644
index 0000000..f35ad06
Binary files /dev/null and b/public/favicon/ms-icon-150x150.png differ
diff --git a/public/favicon/ms-icon-310x310.png b/public/favicon/ms-icon-310x310.png
new file mode 100644
index 0000000..a2aa61e
Binary files /dev/null and b/public/favicon/ms-icon-310x310.png differ
diff --git a/public/favicon/ms-icon-70x70.png b/public/favicon/ms-icon-70x70.png
new file mode 100644
index 0000000..098c572
Binary files /dev/null and b/public/favicon/ms-icon-70x70.png differ
diff --git a/public/index.html b/public/index.html
index aa069f2..3fdd44d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,19 +2,90 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
+
- React App
+ CEOS LINE PLUS
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a..0000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a6..0000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/manifest.json b/public/manifest.json
deleted file mode 100644
index 080d6c7..0000000
--- a/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ],
- "start_url": ".",
- "display": "standalone",
- "theme_color": "#000000",
- "background_color": "#ffffff"
-}
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 3784575..0000000
--- a/src/App.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
- return (
-
- );
-}
-
-export default App;
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 1f03afe..0000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..ae6436c
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,62 @@
+import { Route, Routes, useLocation } from 'react-router-dom';
+import { useEffect } from 'react';
+import { useMessageStore } from 'stores/messageStore';
+import { useUserStore } from 'stores/userStore';
+import Home from 'pages/home/Home';
+import Profile from 'pages/profile/Profile';
+import ChatList from 'pages/chat/ChatList';
+import ChatRoom from 'pages/chatRoom/ChatRoom';
+import NewChat from 'pages/newChat/NewChat';
+import Layout from 'pages/common/Layout';
+import { BackgroundColor, ChatRoomBackgroundColor } from 'styles/global.style';
+
+function App() {
+ const messages = useMessageStore((state) => state.messages);
+ const user = useUserStore((state) => state.user);
+ const location = useLocation();
+
+ // localStorage에 저장되어 있는 데이터 버전이 일치하지 않을 때 초기화
+ useEffect(() => {
+ const storedVersion = localStorage.getItem('version');
+ if (!storedVersion || storedVersion < process.env.REACT_APP_VERSION!) {
+ localStorage.clear();
+ localStorage.setItem('version', process.env.REACT_APP_VERSION!);
+ // return;
+ }
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem('messages', JSON.stringify(messages));
+ }, [messages]);
+
+ useEffect(() => {
+ localStorage.setItem(`user`, JSON.stringify(user));
+ localStorage.setItem(`user_${user.id}`, JSON.stringify(user));
+ }, [user]);
+
+ useEffect(() => {
+ // 모바일로 접속시 페이지 최상단 부분 색상 적용
+ if (/\/chat\//.test(location.pathname))
+ document
+ .querySelector('meta[name="theme-color"]')
+ ?.setAttribute('content', ChatRoomBackgroundColor);
+ else
+ document
+ .querySelector('meta[name="theme-color"]')
+ ?.setAttribute('content', BackgroundColor);
+ }, [location.pathname]);
+
+ return (
+
+ }>
+ } />
+ } />
+
+ } />
+ } />
+ } />
+
+ );
+}
+
+export default App;
diff --git a/src/custom.d.ts b/src/custom.d.ts
new file mode 100644
index 0000000..615b5c2
--- /dev/null
+++ b/src/custom.d.ts
@@ -0,0 +1,7 @@
+declare module '*.svg' {
+ export const ReactComponent: React.FC>;
+ const content: string;
+ export default content;
+}
+
+declare module '*.woff2';
diff --git a/src/data/chatData.json b/src/data/chatData.json
new file mode 100644
index 0000000..e990522
--- /dev/null
+++ b/src/data/chatData.json
@@ -0,0 +1,94 @@
+{
+ "data": [
+ {
+ "id": 0,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "짧은 메시지",
+ "time": "2023-09-26T01:07:37.672Z",
+ "isRead": true,
+ "likeCount": 2
+ },
+ {
+ "id": 1,
+ "fromUserId": 2,
+ "toUserId": 1,
+ "text": "되게 긴글인데 주저리주저리\n줄바꿈도 하고 사우나도가고 다했다고 주저리주저리주저리주저리주저리\n\n주절스",
+ "time": "2023-09-26T02:10:04.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 2,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "날짜바뀌는거보이지ABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEFABCDAEF영어도 대응을한다고 ABCDAEF영어도 대응을한다고ABCDAEF영어도 대응을한다고ABCDAEF영어도 대응을한다고ABCDAEF영어도 대응을한다고",
+ "time": "2023-09-27T09:00:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 3,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "한글자",
+ "time": "2023-09-27T10:23:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 4,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "배고픈 사람 손",
+ "time": "2023-09-27T10:28:07.672Z",
+ "isRead": true,
+ "likeCount": 1
+ },
+ {
+ "id": 5,
+ "fromUserId": 2,
+ "toUserId": 1,
+ "text": "손",
+ "time": "2023-09-27T10:29:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 6,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "안녕하세요",
+ "time": "2023-09-27T10:42:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 7,
+ "fromUserId": 2,
+ "toUserId": 1,
+ "text": "넵넵",
+ "time": "2023-09-27T10:42:07.673Z",
+ "isRead": true,
+ "likeCount": 1
+ },
+ {
+ "id": 8,
+ "fromUserId": 1,
+ "toUserId": 2,
+ "text": "피자먹고싶어요",
+ "time": "2023-09-27T10:43:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ },
+ {
+ "id": 9,
+ "fromUserId": 1,
+ "toUserId": 3,
+ "text": "세오스야 안녕",
+ "time": "2023-10-04T03:31:07.672Z",
+ "isRead": true,
+ "likeCount": 0
+ }
+ ]
+}
diff --git a/src/data/userData.json b/src/data/userData.json
new file mode 100644
index 0000000..539b076
--- /dev/null
+++ b/src/data/userData.json
@@ -0,0 +1,84 @@
+{
+ "data": [
+ {
+ "id": 1,
+ "name": "오대균",
+ "profileImage": null,
+ "statusMessage": "긴 상태메시지 Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,",
+ "likedMessages": [0, 4],
+ "github": "https://github.com/oooppq/",
+ "behance": null,
+ "instagram": "https://www.instagram.com/daegyunii/"
+ },
+ {
+ "id": 2,
+ "name": "배수연",
+ "profileImage": null,
+ "statusMessage": "짧은 상태메시지",
+ "likedMessages": [0, 7],
+ "github": null,
+ "behance": "https://www.behance.net/baesy07017a2a",
+ "instagram": null
+ },
+ {
+ "id": 3,
+ "name": "세오스",
+ "profileImage": null,
+ "statusMessage": "안녕하세요 세오스입니다. 저는 세오스입니다.",
+ "likedMessages": [],
+ "github": "https://github.com/CEOS-Developers",
+ "behance": null,
+ "instagram": "https://www.instagram.com/ceos.sinchon/"
+ },
+ {
+ "id": 4,
+ "name": "일론머스크",
+ "profileImage": null,
+ "statusMessage": "안녕하세요. 저는 자동차를 사랑하는 남자 일론 머스크입니다.",
+ "likedMessages": [],
+ "github": null,
+ "behance": null,
+ "instagram": null
+ },
+ {
+ "id": 5,
+ "name": "오소균",
+ "profileImage": null,
+ "statusMessage": "끼잉",
+ "likedMessages": [],
+ "github": null,
+ "behance": null,
+ "instagram": null
+ },
+ {
+ "id": 6,
+ "name": "2023년",
+ "profileImage": null,
+ "statusMessage": "거의 끝나간다..",
+ "likedMessages": [],
+ "github": null,
+ "behance": null,
+ "instagram": null
+ },
+ {
+ "id": 7,
+ "name": "호랑이",
+ "profileImage": null,
+ "statusMessage": "지금부터는 엑스트라에요.",
+ "likedMessages": [],
+ "github": null,
+ "behance": null,
+ "instagram": null
+ },
+ {
+ "id": 8,
+ "name": "고양이",
+ "profileImage": null,
+ "statusMessage": "지금부터는 엑스트라에요.",
+ "likedMessages": [],
+ "github": null,
+ "behance": null,
+ "instagram": null
+ }
+ ]
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index d563c0f..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..f429fe1
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import { BrowserRouter } from 'react-router-dom';
+import { GlobalStyle } from 'styles/global.style';
+
+const root = ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement
+);
+root.render(
+
+
+
+
+
+
+);
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/chat/ChatList.tsx b/src/pages/chat/ChatList.tsx
new file mode 100644
index 0000000..eedc069
--- /dev/null
+++ b/src/pages/chat/ChatList.tsx
@@ -0,0 +1,50 @@
+import ChatListBody from 'pages/chat/ChatListBody';
+import ChatListHeader from 'pages/chat/ChatListHeader';
+import { useEffect, useState } from 'react';
+import { ClipLoader } from 'react-spinners';
+import styled from 'styled-components';
+
+const ChatList = () => {
+ const [query, setQuery] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ // 탐색중 효과(그냥 기분만 내봄)
+ useEffect(() => {
+ if (query) {
+ setIsLoading(true);
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 700);
+ }
+ }, [query]);
+
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const ChatListContainer = styled.div`
+ flex: 1;
+ overflow: hidden;
+ width: 100%;
+ background-color: var(--Background-White);
+ display: flex;
+ flex-direction: column;
+`;
+
+export default ChatList;
diff --git a/src/pages/chat/ChatListBody.tsx b/src/pages/chat/ChatListBody.tsx
new file mode 100644
index 0000000..a53657f
--- /dev/null
+++ b/src/pages/chat/ChatListBody.tsx
@@ -0,0 +1,54 @@
+import ChatListElement from 'pages/chat/ChatListElement';
+import { useMessageStore } from 'stores/messageStore';
+import { useUserStore } from 'stores/userStore';
+import styled from 'styled-components';
+import { getLastMessages } from 'utils';
+import userData from 'data/userData.json';
+import { include } from 'utils/search';
+
+interface ChatListBodyProps {
+ query: string;
+}
+
+const ChatListBody = ({ query }: ChatListBodyProps) => {
+ const user = useUserStore((state) => state.user);
+ const messages = useMessageStore((state) => state.messages);
+ // 각 유저와의 마지막 메시지
+ const lastMessages = getLastMessages(user.id, messages);
+
+ return (
+
+ {lastMessages.map((message) => {
+ // 해당 대화의 상대방 찾기
+ const opponentId =
+ user.id === message.fromUserId
+ ? message.toUserId
+ : message.fromUserId;
+ const storedOpponent = localStorage.getItem(`user_${opponentId}`);
+ const opponentUser = storedOpponent
+ ? JSON.parse(storedOpponent)
+ : userData.data.find((userToCheck) => userToCheck.id === opponentId)!;
+ //만약 검색어가 있다면 검색어에 해당하는 채팅방만 display
+ if (!include(opponentUser.name, query)) return null;
+ return (
+
+ );
+ })}
+
+ );
+};
+
+const ChatListBodyContainer = styled.div`
+ flex: 1;
+ overflow-y: auto;
+`;
+export default ChatListBody;
diff --git a/src/pages/chat/ChatListElement.tsx b/src/pages/chat/ChatListElement.tsx
new file mode 100644
index 0000000..99d5e27
--- /dev/null
+++ b/src/pages/chat/ChatListElement.tsx
@@ -0,0 +1,100 @@
+import styled from 'styled-components';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+import { TChatRoomInfo } from 'types';
+import { useNavigate } from 'react-router-dom';
+import { convertTimeFormatForChatRoom } from 'utils';
+
+interface ChatListElementProps {
+ chatRoomInfo: TChatRoomInfo;
+}
+const ChatListElement = ({ chatRoomInfo }: ChatListElementProps) => {
+ const navigate = useNavigate();
+
+ return (
+ {
+ navigate(`./${chatRoomInfo.id}`);
+ }}
+ >
+
+ {chatRoomInfo.profileImage ? (
+
+ ) : (
+
+ )}
+
+
+ {chatRoomInfo.userName}
+ {chatRoomInfo.message}
+
+
+ {convertTimeFormatForChatRoom(chatRoomInfo.time)}
+
+
+ );
+};
+
+const ChatListElementContainer = styled.button`
+ text-align: start;
+ width: 100%;
+ // height: 56px;
+ padding: 6px 20px 6px 14.4px;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+`;
+
+const ProfileImageConatiner = styled.div`
+ width: 44px;
+ height: 44px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-right: 14.6px;
+ img,
+ svg {
+ width: 44px;
+ }
+`;
+
+const ChatRoomInfo = styled.div`
+ // padding-top: 3px;
+ flex: 1;
+ .username {
+ height: 19px;
+ color: var(--Gray-3);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 160%;
+ margin-bottom: 4px;
+ }
+ .thumb-message {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre-wrap;
+ word-break: break-all;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+
+ color: var(--Gray-2);
+ font-size: 14px;
+ font-weight: 400;
+ // line-height: 160%;
+ }
+`;
+
+const ChatRoomDate = styled.div`
+ align-self: start;
+ padding-top: 4px;
+ margin-right: 4px;
+ color: var(--Gray-2);
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 120%;
+ white-space: nowrap;
+`;
+
+export default ChatListElement;
diff --git a/src/pages/chat/ChatListHeader.tsx b/src/pages/chat/ChatListHeader.tsx
new file mode 100644
index 0000000..0cbb3fa
--- /dev/null
+++ b/src/pages/chat/ChatListHeader.tsx
@@ -0,0 +1,57 @@
+import ButtonWithIcon from 'pages/common/ButtonWithIcon';
+import styled from 'styled-components';
+import { ReactComponent as StartChatIcon } from 'static/images/start-chat-icon.svg';
+import { ReactComponent as MoreIcon } from 'static/images/more-icon.svg';
+import { useNavigate } from 'react-router-dom';
+import SearchBar from 'pages/common/SearchBar';
+
+interface ChatListHeaderProps {
+ query: string;
+ setQuery: React.Dispatch>;
+}
+
+const ChatListHeader = ({ query, setQuery }: ChatListHeaderProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ Chats
+ }
+ handleClickButton={() => {
+ navigate('/new-chat');
+ }}
+ size={32}
+ />
+ } size={32} />
+
+ {
+ setQuery(e.target.value);
+ }}
+ />
+
+ );
+};
+
+const ChatListHeaderContainer = styled.div`
+ height: 113px;
+ width: 100%;
+ padding: 26px 12px 20px 12px;
+`;
+
+const ChatListHeaderTop = styled.div`
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ .title {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 160%;
+ margin-right: auto;
+ }
+`;
+
+export default ChatListHeader;
diff --git a/src/pages/chatRoom/ChatRoom.tsx b/src/pages/chatRoom/ChatRoom.tsx
new file mode 100644
index 0000000..98b184f
--- /dev/null
+++ b/src/pages/chatRoom/ChatRoom.tsx
@@ -0,0 +1,86 @@
+import { useEffect, useRef } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useMessageStore } from 'stores/messageStore';
+import { useUserStore } from 'stores/userStore';
+import ChatRoomBody from 'pages/chatRoom/ChatRoomBody';
+import ChatRoomFooter from 'pages/chatRoom/ChatRoomFooter';
+import ChatRoomHeader from 'pages/chatRoom/ChatRoomHeader';
+import styled from 'styled-components';
+import { TMessage } from 'types';
+
+const ChatRoom = () => {
+ const { id }: { id?: string } = useParams(); // 채팅의 대상
+ const user = useUserStore((state) => state.user); // 채팅앱의 주체
+
+ const messages = useMessageStore((state) => state.messages);
+
+ const setMessages = useMessageStore((state) => state.setMessages);
+ const toggleIsRead = useMessageStore((state) => state.toggleIsRead);
+
+ const navigate = useNavigate();
+
+ const headerRef = useRef(null);
+ const bodyRef = useRef(null);
+
+ useEffect(() => {
+ if (user.id === Number(id)) {
+ navigate('/chat');
+ }
+ }, [id, navigate, user.id]);
+
+ // 유저 페이지 전환될 때 메시지에에 대한 읽음처리하도록
+ useEffect(() => {
+ if (user.id !== Number(id)) {
+ messages.forEach((message, idx) => {
+ if (
+ message.fromUserId === Number(id) &&
+ message.toUserId === user.id &&
+ !message.isRead // 읽지 않은 메시지가 있으면 읽음표시 하도록
+ )
+ toggleIsRead(idx);
+ });
+ }
+ }, [id, messages, toggleIsRead, user.id]);
+
+ return (
+
+
+
+ (message.fromUserId === user.id &&
+ message.toUserId === Number(id)) ||
+ (message.fromUserId === Number(id) && message.toUserId === user.id)
+ )}
+ bodyRef={bodyRef}
+ />
+ {
+ const newMessage: TMessage = {
+ id: messages.length,
+ toUserId: Number(id),
+ fromUserId: user.id,
+ text: message,
+ time: new Date().toISOString(),
+ isRead: false,
+ likeCount: 0,
+ };
+ setMessages([...messages, newMessage]);
+ }}
+ />
+
+ );
+};
+
+export default ChatRoom;
+
+// ############### 디자인 ###############
+
+const ChatRoomContainer = styled.div`
+ height: 100%;
+ background-color: var(--Blue);
+ display: flex;
+ flex-direction: column;
+`;
diff --git a/src/pages/chatRoom/ChatRoomBody.tsx b/src/pages/chatRoom/ChatRoomBody.tsx
new file mode 100644
index 0000000..b8b28c8
--- /dev/null
+++ b/src/pages/chatRoom/ChatRoomBody.tsx
@@ -0,0 +1,112 @@
+import EachMessage from 'pages/chatRoom/EachMessage';
+import { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { useMessageStore } from 'stores/messageStore';
+import { useUserStore } from 'stores/userStore';
+import styled from 'styled-components';
+import { ChatRoomBackgroundColor } from 'styles/global.style';
+import { TMessage } from 'types';
+import { checkIsNextDay } from 'utils';
+import userData from 'data/userData.json';
+
+interface ChatRoomBodyProps {
+ // dummy에 타입 적용
+ messages: TMessage[];
+ bodyRef: React.RefObject;
+}
+
+const ChatRoomBody = ({ messages, bodyRef }: ChatRoomBodyProps) => {
+ const { id }: { id?: string } = useParams();
+
+ const setMessages = useMessageStore((state) => state.setMessages);
+ const wholeMessages = useMessageStore((state) => state.messages);
+
+ const user = useUserStore((state) => state.user);
+ const setUser = useUserStore((state) => state.setUser);
+
+ const opponent = localStorage.getItem(`user_${id}`)
+ ? JSON.parse(localStorage.getItem(`user_${id}`)!)
+ : userData.data.find((e) => e.id === Number(id));
+
+ let before = '';
+
+ // 더블클리 이벤트 헨들러
+ const handleDoubleClickMessage = (messageId: number) => {
+ // const newMessages = [...messages];
+ const newMessages = [...wholeMessages];
+ // 더블 클릭된 메시지의 index 찾기
+ const clickedIdx = newMessages.findIndex(
+ (message) => message.id === messageId
+ );
+ try {
+ // 혹시 localStorage에 저장된 data가 변경되는 상황을 대비
+ if (user.likedMessages.includes(messageId)) {
+ // 현재 유저가 이미 좋아한 메시지라면 좋아요 취소
+ newMessages[clickedIdx] = {
+ ...newMessages[clickedIdx],
+ likeCount: newMessages[clickedIdx].likeCount - 1,
+ };
+ setMessages(newMessages);
+ setUser({
+ ...user,
+ likedMessages: user.likedMessages.filter((id) => id !== messageId),
+ });
+ } else {
+ // 이미 좋아요한 메시지가 아니라면 좋아요
+ newMessages[clickedIdx] = {
+ ...newMessages[clickedIdx],
+ likeCount: newMessages[clickedIdx].likeCount + 1,
+ };
+ setMessages(newMessages);
+ setUser({ ...user, likedMessages: [...user.likedMessages, messageId] });
+ }
+ } catch {
+ localStorage.clear();
+ window.location.reload();
+ }
+ };
+
+ // 채팅방 스크롤 아래로
+ useEffect(() => {
+ if (bodyRef.current) {
+ bodyRef.current.scrollTop = bodyRef.current?.scrollHeight;
+ }
+ }, [bodyRef, messages.length]);
+
+ return (
+
+ {messages.map((message) => {
+ const isNextDay = checkIsNextDay(before, message.time);
+ before = message.time;
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default ChatRoomBody;
+
+// ############### 디자인 ###############
+
+const ChatRoomBodyContainer = styled.div`
+ background-color: ${ChatRoomBackgroundColor};
+ padding: 0 8px 56px 8px;
+ flex-grow: 1;
+ max-height: 100%;
+ overflow-y: auto;
+`;
diff --git a/src/pages/chatRoom/ChatRoomFooter.tsx b/src/pages/chatRoom/ChatRoomFooter.tsx
new file mode 100644
index 0000000..727fbe2
--- /dev/null
+++ b/src/pages/chatRoom/ChatRoomFooter.tsx
@@ -0,0 +1,166 @@
+import { useEffect, useRef, useState } from 'react';
+import styled from 'styled-components';
+import ButtonWithIcon from 'pages/common/ButtonWithIcon';
+import { ReactComponent as PlusIcon } from 'static/images/plus-icon.svg';
+import { ReactComponent as CameraIcon } from 'static/images/camera-icon.svg';
+import { ReactComponent as PicIcon } from 'static/images/pic-icon.svg';
+import { ReactComponent as MicIcon } from 'static/images/mic-icon.svg';
+import { ReactComponent as SendIcon } from 'static/images/send-icon.svg';
+import { ReactComponent as BackIcon } from 'static/images/back-arrow-icon.svg';
+
+interface ChatRoomFooterProps {
+ headerRef: React.RefObject;
+ bodyRef: React.RefObject;
+ sendMessage: (message: string) => void;
+}
+
+const ChatRoomFooter = ({
+ headerRef,
+ bodyRef,
+ sendMessage,
+}: ChatRoomFooterProps) => {
+ const inputRef = useRef(null);
+ const hiddenInputRef = useRef(null);
+ const [content, setContent] = useState('');
+ const [isMenuSpread, setIsMenuSpread] = useState(true);
+ const [isInputFocused, setIsInputFocused] = useState(false);
+
+ // textarea 높이 동적 조절
+ const handleChangeContent = (e: React.ChangeEvent) => {
+ setContent(e.target.value);
+ e.target.style.height = '36px';
+ e.target.style.height = e.target.scrollHeight + 'px';
+ };
+
+ // footer 부분 이외 클릭시 textarea focus out
+ const handleOnClickOutOfFooter = (e: React.MouseEvent | MouseEvent) => {
+ if (
+ headerRef.current?.contains(e.target as Node) ||
+ bodyRef.current?.contains(e.target as Node)
+ ) {
+ setIsInputFocused(false);
+ setIsMenuSpread(true);
+ }
+ };
+
+ // submit handler
+ const handleSubmitMessage = () => {
+ if (content.trim()) {
+ sendMessage(content);
+ setContent('');
+ // focus 상태에서 전송을 누르면, 계속 focus 유지되도록
+ if (isInputFocused) {
+ // hiddenInput에 focus를 옮기고, 다시 input으로 옮기는 방식을 사용하여
+ // ios 환경에서 한글(받침없는 글자) 입력시 buffer가 남아있는 문제를 해결했음
+ hiddenInputRef.current?.focus();
+ inputRef.current?.focus();
+ }
+ if (inputRef.current) inputRef.current.style.height = '36px';
+ }
+ };
+
+ // 전체 화면에 클릭이벤트 적용
+ useEffect(() => {
+ document.addEventListener('click', handleOnClickOutOfFooter);
+ return () =>
+ document.removeEventListener('click', handleOnClickOutOfFooter);
+ });
+
+ return (
+
+
+ : }
+ handleClickButton={() => {
+ setIsMenuSpread(true);
+ }}
+ size={28}
+ />
+ } size={28} />
+ } size={28} />
+
+
+ {
+ setIsMenuSpread(false);
+ setIsInputFocused(true);
+ }}
+ onKeyDown={(e) => {
+ if (e.nativeEvent.isComposing) return; //key 조합 감지
+ // 모바일 환경이 아닐 때에는 enter로 전송, shift + enter로 줄바꿈
+ if (!/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
+ if (e.key === 'Enter' && e.shiftKey) return;
+ else if (e.key === 'Enter') {
+ handleSubmitMessage();
+ e.preventDefault();
+ }
+ }
+ }}
+ value={content}
+ />
+
+ : }
+ handleClickButton={handleSubmitMessage}
+ size={28}
+ />
+
+ );
+};
+
+export default ChatRoomFooter;
+
+// ############### 디자인 ###############
+
+const ChatRoomFooterContainer = styled.div`
+ position: fixed;
+ bottom: 0;
+ display: flex;
+ align-items: end;
+ background-color: white;
+ width: 100%;
+ padding: 6px 8px 14px 11px;
+`;
+
+const LeftSideButtonsOuter = styled.div<{ $isMenuSpread: boolean }>`
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+ width: ${(props) => (props.$isMenuSpread ? '108px' : '36px')};
+ display: flex;
+ overflow: hidden;
+`;
+
+const LeftSideButton = styled(ButtonWithIcon)`
+ margin-bottom: 8px;
+ margin-right: 8px;
+`;
+
+const RightSideButton = styled(ButtonWithIcon)`
+ margin: 0 0 8px 8px;
+`;
+
+const ChatInput = styled.textarea.attrs({
+ rows: 1,
+})`
+ max-height: 86px;
+ border: none;
+ border-radius: 16px;
+ background: #f5f5f5;
+ padding: 9px 17px;
+ flex: 1;
+ resize: none;
+ font-size: 14px;
+ font-family: 'Pretendard Variable';
+`;
+
+const HiddenInput = styled.input`
+ position: absolute;
+ border: 0;
+ background-color: transparent;
+ pointer-events: none;
+ width: 0;
+ height: 0;
+`;
diff --git a/src/pages/chatRoom/ChatRoomHeader.tsx b/src/pages/chatRoom/ChatRoomHeader.tsx
new file mode 100644
index 0000000..5a690e7
--- /dev/null
+++ b/src/pages/chatRoom/ChatRoomHeader.tsx
@@ -0,0 +1,54 @@
+import { useNavigate, useParams } from 'react-router-dom';
+import ButtonWithIcon from 'pages/common/ButtonWithIcon';
+import { ReactComponent as BackIcon } from 'static/images/back-arrow-icon.svg';
+import styled from 'styled-components';
+import userData from 'data/userData.json';
+import { TUser } from 'types';
+
+interface ChatRoomHeaderProps {
+ headerRef: React.RefObject;
+}
+
+const typedUserData: TUser[] = userData.data;
+
+const ChatRoomHeader = ({ headerRef }: ChatRoomHeaderProps) => {
+ const navigate = useNavigate();
+ const { id }: { id?: string } = useParams();
+
+ const storedUser = localStorage.getItem(`user_${id}`);
+ const roomOwner = storedUser // 해당 방이 누구와의 대화방인지
+ ? JSON.parse(storedUser)
+ : typedUserData.find((user) => user.id === Number(id));
+
+ return (
+
+ }
+ handleClickButton={() => {
+ navigate('/chat');
+ }}
+ size={28}
+ />
+ {roomOwner!.name}
+
+ );
+};
+
+export default ChatRoomHeader;
+
+// ############### 디자인 ###############
+
+const ChatRoomHeaderContainer = styled.div`
+ display: flex;
+ align-items: center;
+ height: 53px;
+ width: 100%;
+ padding: 8px 0 14px 16px;
+ background-color: var(--Blue);
+`;
+
+const UserNameDiv = styled.div`
+ font-size: 18px;
+ font-weight: 600;
+ margin-left: 9px;
+`;
diff --git a/src/pages/chatRoom/EachMessage.tsx b/src/pages/chatRoom/EachMessage.tsx
new file mode 100644
index 0000000..e5f2152
--- /dev/null
+++ b/src/pages/chatRoom/EachMessage.tsx
@@ -0,0 +1,159 @@
+import styled from 'styled-components';
+import { TMessage } from 'types';
+import { ReactComponent as HeartIcon } from 'static/images/heart-icon.svg';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+import { convertDayDateFormat, convertTimeFormat } from 'utils';
+
+interface EachMessageProps {
+ message: TMessage & { profileImage?: string };
+ isOwnMessage: boolean;
+ isNextDay: boolean;
+ handleDoubleClickMessage: () => void;
+}
+
+const EachMessage = ({
+ message,
+ isOwnMessage,
+ isNextDay,
+ handleDoubleClickMessage,
+}: EachMessageProps) => {
+ // 나와 상대방을 구분하여 display하도록 order를 정한 후, flex를 통해 나열
+ const orders = isOwnMessage ? [1, 0] : [0, 1];
+
+ return (
+
+ {isNextDay && (
+
+ {convertDayDateFormat(message.time)}
+
+ )}
+
+
+ {!isOwnMessage &&
+ (message.profileImage ? (
+
+ ) : (
+
+ ))}
+
+
+
+
+ {message.text}
+
+ {message.likeCount > 0 && (
+
+
+ {message.likeCount}
+
+ )}
+
+
+ 0}>
+ {isOwnMessage && message.isRead && Read
}
+ {convertTimeFormat(message.time)}
+
+
+
+ );
+};
+
+export default EachMessage;
+
+// ############### 디자인 ###############
+
+const EachMessageContainer = styled.div`
+ margin-bottom: 8px;
+`;
+
+const MessageBody = styled.div<{ $isOwnMessage: boolean }>`
+ display: flex;
+ justify-content: ${(props) => (props.$isOwnMessage ? 'end' : 'start')};
+ .profile-image-outer {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-right: 11px;
+ img,
+ svg {
+ width: 100%;
+ object-fit: cover;
+ }
+ }
+`;
+
+const TextAndLikeOuter = styled.div<{ $order: number; $isOwnMessage: boolean }>`
+ order: ${(props) => props.$order};
+ display: flex;
+ flex-direction: column;
+ align-items: ${(props) => (props.$isOwnMessage ? 'end' : 'start')};
+`;
+
+const MessageText = styled.div<{ $isOwnMessage: boolean }>`
+ border-radius: 16px;
+ background-color: ${(props) =>
+ props.$isOwnMessage ? 'var(--Green)' : 'white'};
+ padding: 8px 12px;
+ font-size: 14px;
+ line-height: 160%;
+ max-width: 244px;
+ word-break: break-all;
+ white-space: break-spaces;
+`;
+
+const MessageExtraInfo = styled.div<{ $order: number; $isLiked: boolean }>`
+ order: ${(props) => props.$order};
+ margin: auto 10px 0 10px;
+ margin-bottom: ${(props) => (props.$isLiked ? '31px' : '3px')};
+ font-size: 10px;
+ font-weight: 300;
+ line-height: 160%;
+ color: var(--Gray-3);
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+`;
+
+const LikeContainer = styled.div`
+ width: 40px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 16px;
+ background-color: var(--gray-30);
+ margin: 6px 0 2px 0;
+ svg {
+ margin-right: 3px;
+ }
+ .like-count {
+ margin-left: 3px;
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 120%;
+ color: white;
+ }
+`;
+
+const DayDateContainer = styled.div`
+ width: fit-content;
+ margin: 4px 0 12px 0;
+ margin-left: 50%;
+ transform: translate(-50%, 0%);
+ padding: 3px 8px;
+
+ height: 20px;
+ border-radius: 16px;
+ background-color: var(--gray-30);
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 120%;
+ color: white;
+`;
diff --git a/src/pages/common/ButtonWithIcon.tsx b/src/pages/common/ButtonWithIcon.tsx
new file mode 100644
index 0000000..1755595
--- /dev/null
+++ b/src/pages/common/ButtonWithIcon.tsx
@@ -0,0 +1,36 @@
+import { ReactNode } from 'react';
+import styled from 'styled-components';
+
+interface ButtonWithIconProps {
+ className?: string;
+ children: ReactNode;
+ handleClickButton?: (e?: React.MouseEvent | MouseEvent) => void;
+ size?: number;
+}
+const ButtonWithIcon = ({
+ className,
+ children,
+ handleClickButton,
+ size,
+}: ButtonWithIconProps) => {
+ return (
+
+ );
+};
+
+const Button = styled.button<{ $size: number | undefined }>`
+ width: ${(props) => props.$size}px;
+ height: ${(props) => props.$size}px;
+ display: inline-block;
+ font-size: 0;
+ line-height: 0;
+ display: flex;
+ svg,
+ img {
+ width: ${(props) => props.$size}px;
+ height: ${(props) => props.$size}px;
+ }
+`;
+export default ButtonWithIcon;
diff --git a/src/pages/common/Layout.tsx b/src/pages/common/Layout.tsx
new file mode 100644
index 0000000..b2e87db
--- /dev/null
+++ b/src/pages/common/Layout.tsx
@@ -0,0 +1,21 @@
+import NavBar from 'pages/common/NavBar';
+import { Outlet } from 'react-router-dom';
+import styled from 'styled-components';
+
+const Layout = () => {
+ return (
+
+
+
+
+ );
+};
+
+const LayOutContainer = styled.div`
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+export default Layout;
diff --git a/src/pages/common/NavBar.tsx b/src/pages/common/NavBar.tsx
new file mode 100644
index 0000000..3bab3ca
--- /dev/null
+++ b/src/pages/common/NavBar.tsx
@@ -0,0 +1,52 @@
+import styled from 'styled-components';
+import { ReactComponent as HomeOnIcon } from 'static/images/home-on-icon.svg';
+import { ReactComponent as HomeOffIcon } from 'static/images/home-off-icon.svg';
+import { ReactComponent as ChatOnIcon } from 'static/images/chat-on-icon.svg';
+import { ReactComponent as ChatOffIcon } from 'static/images/chat-off-icon.svg';
+import { ReactComponent as CallOffIcon } from 'static/images/call-off-icon.svg';
+import NavBarButton from 'pages/common/NavBarButton';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+const NavBar = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ return (
+
+ : }
+ buttonName="Home"
+ handleClickButton={() => {
+ navigate('/');
+ }}
+ />
+ :
+ }
+ buttonName="Chat"
+ handleClickButton={() => {
+ navigate('/chat');
+ }}
+ />
+ }
+ buttonName="Call"
+ handleClickButton={() => {}}
+ />
+
+ );
+};
+
+const NavBarContainer = styled.div`
+ position: fixed;
+ bottom: 0;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ height: 80px;
+ padding: 24px 52px 0 52px;
+ background-color: var(--Background-White);
+`;
+
+export default NavBar;
diff --git a/src/pages/common/NavBarButton.tsx b/src/pages/common/NavBarButton.tsx
new file mode 100644
index 0000000..c0e3640
--- /dev/null
+++ b/src/pages/common/NavBarButton.tsx
@@ -0,0 +1,41 @@
+import { ReactNode } from 'react';
+import styled from 'styled-components';
+
+interface NavBarButtonProps {
+ children: ReactNode;
+ buttonName: string;
+ handleClickButton?: (e?: React.MouseEvent | MouseEvent) => void;
+}
+
+const NavBarButton = ({
+ children,
+ buttonName,
+ handleClickButton,
+}: NavBarButtonProps) => {
+ return (
+
+ {children}
+ {buttonName}
+
+ );
+};
+
+const NavBarButtonContainer = styled.button`
+ svg {
+ width: 24px;
+ height: 24px;
+ }
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+const ButtonName = styled.div`
+ color: black;
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 120%;
+ font-style: normal;
+`;
+
+export default NavBarButton;
diff --git a/src/pages/common/SearchBar.tsx b/src/pages/common/SearchBar.tsx
new file mode 100644
index 0000000..72dd0aa
--- /dev/null
+++ b/src/pages/common/SearchBar.tsx
@@ -0,0 +1,49 @@
+import { ReactComponent as SearchIcon } from 'static/images/search-icon.svg';
+import styled from 'styled-components';
+
+interface SearchBarProps {
+ query: string;
+ handleChange: (e: React.ChangeEvent) => void;
+ customStyle?: string;
+}
+
+// 홈, 채팅목록, 새로운 채팅 페이지에서의 search bar
+const SearchBar = ({ query, handleChange, customStyle }: SearchBarProps) => {
+ return (
+
+
+
+
+ );
+};
+
+const SearchBarContainer = styled.div<{ $style: string | undefined }>`
+ ${(props) => props.$style}
+ height: 32px;
+ border-radius: 4px;
+ background: var(--Gray-1);
+ display: flex;
+ align-items: center;
+ padding: 0 12px;
+`;
+
+const SearchBarInput = styled.input`
+ font-size: 12px;
+ font-style: normal;
+ width: 100%;
+ font-weight: 300;
+ line-height: 120%;
+ color: var(--Gray-2);
+ border: none;
+ background: transparent;
+ margin-left: 10px;
+
+ &::placeholder {
+ color: var(--Gray-2);
+ }
+`;
+export default SearchBar;
diff --git a/src/pages/home/FriendListElement.tsx b/src/pages/home/FriendListElement.tsx
new file mode 100644
index 0000000..a76f9eb
--- /dev/null
+++ b/src/pages/home/FriendListElement.tsx
@@ -0,0 +1,55 @@
+import styled from 'styled-components';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+import { TUser } from 'types';
+
+interface FriendListElementProps {
+ user: TUser;
+ handleDoubleClickUser: () => void;
+}
+
+const FriendListElement = ({
+ user,
+ handleDoubleClickUser,
+}: FriendListElementProps) => {
+ return (
+
+
+ {user.profileImage ? (
+
+ ) : (
+
+ )}
+
+ {user.name}
+
+ );
+};
+
+const FriendListElementContainer = styled.button`
+ display: flex;
+ width: 100%;
+ align-items: center;
+ padding: 8px 0;
+ margin-bottom: 8px;
+ .username {
+ color: black;
+ font-size: 14px;
+ }
+`;
+
+const ProfileImageConatiner = styled.div`
+ width: 36px;
+ height: 36px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-right: 14px;
+ img,
+ svg {
+ width: 36px;
+ }
+`;
+
+export default FriendListElement;
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
new file mode 100644
index 0000000..99e0c7c
--- /dev/null
+++ b/src/pages/home/Home.tsx
@@ -0,0 +1,49 @@
+import HomeBody from 'pages/home/HomeBody';
+import HomeHeader from 'pages/home/HomeHeader';
+import { useEffect, useState } from 'react';
+import { ClipLoader } from 'react-spinners';
+import styled from 'styled-components';
+
+const Home = () => {
+ const [query, setQuery] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ // 탐색중 효과(그냥 기분만 내봄)
+ useEffect(() => {
+ if (query) {
+ setIsLoading(true);
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 700);
+ }
+ }, [query]);
+
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const HomeContainer = styled.div`
+ flex: 1;
+ overflow-y: auto;
+ width: 100%;
+ background-color: var(--Background-White);
+ // padding: 0 12px;
+ // position: relative;
+`;
+export default Home;
diff --git a/src/pages/home/HomeBody.tsx b/src/pages/home/HomeBody.tsx
new file mode 100644
index 0000000..46a7748
--- /dev/null
+++ b/src/pages/home/HomeBody.tsx
@@ -0,0 +1,58 @@
+import FriendListElement from 'pages/home/FriendListElement';
+import styled from 'styled-components';
+import { useUserStore } from 'stores/userStore';
+import { getSearchedUsers } from 'utils';
+
+interface HomeBodyProps {
+ query: string;
+}
+
+const HomeBody = ({ query }: HomeBodyProps) => {
+ const user = useUserStore((state) => state.user);
+ const setUser = useUserStore((state) => state.setUser);
+
+ const users = getSearchedUsers(user.id, query);
+
+ return (
+
+
+
Friends
+
{users.length}
+
+
+ {users.map((e) => (
+ {
+ setUser(e);
+ }}
+ />
+ ))}
+
+
+ );
+};
+
+const HomeBodyContainer = styled.div`
+ margin-top: 17px;
+ padding: 0 12px 80px 12px;
+ .title-outer {
+ display: flex;
+ margin-bottom: 4px;
+ .title {
+ height: 22px;
+ font-size: 14px;
+ line-height: 160%;
+ margin-right: 8px;
+ }
+ .friend-number {
+ height: 22px;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 160%; /* 22.4px */
+ }
+ }
+`;
+
+export default HomeBody;
diff --git a/src/pages/home/HomeHeader.tsx b/src/pages/home/HomeHeader.tsx
new file mode 100644
index 0000000..dc4872f
--- /dev/null
+++ b/src/pages/home/HomeHeader.tsx
@@ -0,0 +1,51 @@
+import Services from 'pages/home/Services';
+import UserProfile from 'pages/home/UserProfile';
+import styled from 'styled-components';
+import { useState } from 'react';
+import SearchBar from 'pages/common/SearchBar';
+
+interface HomeHeaderProps {
+ query: string;
+ setQuery: React.Dispatch>;
+}
+
+const HomeHeader = ({ query, setQuery }: HomeHeaderProps) => {
+ // status message가 펼쳐졌는지에 대한 flag
+ const [isStatusMessageSpread, setIsStatusMessageSpread] =
+ useState(false);
+
+ return (
+
+ {
+ setIsStatusMessageSpread(true);
+ }}
+ foldStatusMessage={() => {
+ setIsStatusMessageSpread(false);
+ }}
+ />
+ {
+ setQuery(e.target.value);
+ }}
+ customStyle="margin: 0 12px;"
+ />
+
+
+ );
+};
+
+const HomeHeaderContainer = styled.div<{
+ $isStatusMessageSpread: boolean;
+}>`
+ margin-top: 26px;
+ margin-top: ${(props) => (props.$isStatusMessageSpread ? '102px' : '26px')};
+`;
+
+const StyledSearchBar = styled(SearchBar)`
+ margin: 0 12px;
+`;
+
+export default HomeHeader;
diff --git a/src/pages/home/Services.tsx b/src/pages/home/Services.tsx
new file mode 100644
index 0000000..53be854
--- /dev/null
+++ b/src/pages/home/Services.tsx
@@ -0,0 +1,67 @@
+import styled from 'styled-components';
+import { ReactComponent as StickersIcon } from 'static/images/stickers-icon.svg';
+import { ReactComponent as PencilIcon } from 'static/images/pencil-icon.svg';
+import { ReactComponent as OfficialIcon } from 'static/images/official-icon.svg';
+import { ReactComponent as PointsIcon } from 'static/images/line-point-icon.svg';
+
+const Services = () => {
+ return (
+
+ Services
+
+
+
+ Stickers
+
+
+
+ Themes
+
+
+
+
+ Official
+
+ Accounts
+
+
+
+
+
+ LINE
+
+ Points
+
+
+
+
+ );
+};
+
+const ServicesContainer = styled.div`
+ margin-top: 20px;
+ padding: 0 12px;
+ .title {
+ font-size: 14px;
+ line-height: 160%; /* 22.4px */
+ }
+ .btns {
+ margin-top: 12px;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: start;
+ }
+`;
+
+const ServiceButton = styled.button`
+ .button-name {
+ height: 36px;
+ text-align: center;
+ font-size: 12px;
+ font-weight: 300;
+ color: var(--Gray-3);
+ line-height: 120%;
+ }
+`;
+
+export default Services;
diff --git a/src/pages/home/UserProfile.tsx b/src/pages/home/UserProfile.tsx
new file mode 100644
index 0000000..81e62f0
--- /dev/null
+++ b/src/pages/home/UserProfile.tsx
@@ -0,0 +1,141 @@
+import styled from 'styled-components';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+import { ReactComponent as MoreOnIcon } from 'static/images/more-on-icon.svg';
+import { ReactComponent as MoreOffIcon } from 'static/images/more-off-icon.svg';
+import ButtonWithIcon from 'pages/common/ButtonWithIcon';
+import { useUserStore } from 'stores/userStore';
+import { useNavigate } from 'react-router-dom';
+
+interface UserProfileProps {
+ isStatusMessageSpread: boolean;
+ spreadStatusMessage: () => void;
+ foldStatusMessage: () => void;
+}
+const UserProfile = ({
+ isStatusMessageSpread,
+ spreadStatusMessage,
+ foldStatusMessage,
+}: UserProfileProps) => {
+ const user = useUserStore((state) => state.user);
+ const navigate = useNavigate();
+
+ return (
+ {
+ navigate('/profile');
+ }}
+ >
+
+
+
{user.name}
+
+ {user.statusMessage}
+ {
+ foldStatusMessage();
+ e.stopPropagation();
+ }}
+ />
+ ) : (
+ {
+ spreadStatusMessage();
+ e.stopPropagation();
+ }}
+ />
+ )
+ }
+ size={28}
+ />
+
+
+
+ {user.profileImage ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const UserProfileContainer = styled.div<{
+ $isStatusMessageSpread: boolean;
+}>`
+ width: 100%;
+ ${(props) =>
+ props.$isStatusMessageSpread
+ ? 'position: fixed; top: 0px; bottom: 0;'
+ : 'height: 76px;'};
+
+ background-color: var(--gray-30, rgba(0, 0, 0, 0.3));
+
+ .profile-body {
+ padding: 0 12px 12px 12px;
+ padding-top: ${(props) => (props.$isStatusMessageSpread ? '26px' : '0px')};
+ background-color: var(--Background-White);
+ display: flex;
+ .profile-info {
+ margin-top: 4px;
+ flex: 1;
+ .username {
+ height: 32px;
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 160%;
+ }
+ }
+ }
+`;
+
+const StatusMessage = styled.div<{
+ $isStatusMessageSpread: boolean;
+}>`
+ display: flex;
+ .message {
+ color: var(--Gray-2);
+ margin-top: 4px;
+ margin-right: 8px;
+ font-size: 14px;
+ line-height: 160%;
+ flex: 1;
+ ${(props) =>
+ props.$isStatusMessageSpread
+ ? ''
+ : `text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre-wrap;
+ word-break: break-all;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;`};
+ }
+ button {
+ margin-right: 17px;
+ }
+`;
+
+const ProfileImageConatiner = styled.div`
+ width: 44px;
+ height: 44px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-top: 9px;
+ margin-right: 4px;
+ img,
+ svg {
+ width: 44px;
+ // height: 44px;
+ }
+`;
+
+export default UserProfile;
diff --git a/src/pages/newChat/NewChat.tsx b/src/pages/newChat/NewChat.tsx
new file mode 100644
index 0000000..9dea239
--- /dev/null
+++ b/src/pages/newChat/NewChat.tsx
@@ -0,0 +1,59 @@
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import NewChatHeader from 'pages/newChat/NewChatHeader';
+import NewChatBody from 'pages/newChat/NewChatBody';
+import { ClipLoader } from 'react-spinners';
+
+const NewChat = () => {
+ const [query, setQuery] = useState(''); // 검색어
+ const [selected, setSelected] = useState(null); // 선택된 유저
+ const [isLoading, setIsLoading] = useState(false); // 로딩 flag
+
+ // 탐색중 효과(그냥 기분만 내봄)
+ useEffect(() => {
+ if (query) {
+ setIsLoading(true);
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 700);
+ }
+ }, [query]);
+
+ return (
+
+ {
+ setQuery(e.target.value);
+ }}
+ />
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const NewChatContainer = styled.div`
+ height: 100%;
+ width: 100%;
+ padding: 20px 12px 0 12px;
+ background-color: var(--Background-White);
+`;
+
+export default NewChat;
diff --git a/src/pages/newChat/NewChatBody.tsx b/src/pages/newChat/NewChatBody.tsx
new file mode 100644
index 0000000..78b3556
--- /dev/null
+++ b/src/pages/newChat/NewChatBody.tsx
@@ -0,0 +1,34 @@
+import { useUserStore } from 'stores/userStore';
+import styled from 'styled-components';
+import NewChatElement from 'pages/newChat/NewChatElement';
+import { getSearchedUsers } from 'utils';
+
+interface NewChatBodyProps {
+ query: string;
+ selected: number | null;
+ setSelected: React.Dispatch>;
+}
+const NewChatBody = ({ query, selected, setSelected }: NewChatBodyProps) => {
+ const user = useUserStore((state) => state.user);
+ const users = getSearchedUsers(user.id, query);
+
+ return (
+
+ {users.map((e) => (
+ {
+ setSelected(e.id);
+ }}
+ />
+ ))}
+
+ );
+};
+
+const NewChatBodyConatiner = styled.div`
+ margin-top: 20px;
+`;
+export default NewChatBody;
diff --git a/src/pages/newChat/NewChatElement.tsx b/src/pages/newChat/NewChatElement.tsx
new file mode 100644
index 0000000..5382a7a
--- /dev/null
+++ b/src/pages/newChat/NewChatElement.tsx
@@ -0,0 +1,65 @@
+import styled from 'styled-components';
+import { TUser } from 'types';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+
+interface NewChatElementProps {
+ user: TUser;
+ checked: boolean;
+ handleChange: () => void;
+}
+
+const NewChatElement = ({
+ user,
+ checked,
+ handleChange,
+}: NewChatElementProps) => {
+ return (
+
+
+ {user.profileImage ? (
+
+ ) : (
+
+ )}
+
+ {user.name}
+
+
+ );
+};
+
+const NewChatElementContainer = styled.button`
+ display: flex;
+ width: 100%;
+ align-items: center;
+ text-align: start;
+ padding: 8px 0;
+ margin-bottom: 8px;
+ .username {
+ color: black;
+ font-size: 14px;
+ flex: 1;
+ }
+`;
+
+const ProfileImageConatiner = styled.div`
+ width: 36px;
+ height: 36px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ margin-right: 14px;
+ img,
+ svg {
+ width: 36px;
+ }
+`;
+
+export default NewChatElement;
diff --git a/src/pages/newChat/NewChatHeader.tsx b/src/pages/newChat/NewChatHeader.tsx
new file mode 100644
index 0000000..a658479
--- /dev/null
+++ b/src/pages/newChat/NewChatHeader.tsx
@@ -0,0 +1,46 @@
+import SearchBar from 'pages/common/SearchBar';
+import { useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+
+interface NewChatHeaderProps {
+ query: string;
+ selected: number | null;
+ handleChange: (e: React.ChangeEvent) => void;
+}
+const NewChatHeader = ({
+ query,
+ selected,
+ handleChange,
+}: NewChatHeaderProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ {
+ // 선택된 유저가 있으면 해당 유저와의 채팅방으로 이동
+ if (selected) navigate(`/chat/${selected}`);
+ else navigate(-1);
+ }}
+ >
+ {selected ? '확인' : '취소'}
+
+
+ );
+};
+
+const NewChatHeaderContainer = styled.div`
+ display: flex;
+`;
+
+const CancelButton = styled.button`
+ margin-left: 10px;
+ color: var(--Gray-3);
+ font-size: 14px;
+`;
+export default NewChatHeader;
diff --git a/src/pages/profile/Profile.tsx b/src/pages/profile/Profile.tsx
new file mode 100644
index 0000000..93f0e10
--- /dev/null
+++ b/src/pages/profile/Profile.tsx
@@ -0,0 +1,56 @@
+import UserLink from 'pages/profile/UserLink';
+import UserProfile from 'pages/profile/UserProfile';
+import styled from 'styled-components';
+import { ReactComponent as InstagramIcon } from 'static/images/instagram-icon.svg';
+import { ReactComponent as GithubIcon } from 'static/images/github-icon.svg';
+import { ReactComponent as BehanceIcon } from 'static/images/behance-icon.svg';
+import { useUserStore } from 'stores/userStore';
+import { useState } from 'react';
+import ProfileHeader from 'pages/profile/ProfileHeader';
+
+const Profile = () => {
+ const user = useUserStore((state) => state.user);
+ // 프로필 변경 모드 flag
+ const [isProfileChanging, setIsProfileChanging] = useState(false);
+
+ return (
+
+ {
+ setIsProfileChanging((state) => !state);
+ }}
+ />
+
+ }
+ linkName="Instagram"
+ href={user.instagram}
+ />
+ }
+ linkName="Github"
+ href={user.github}
+ />
+ }
+ linkName="Behance"
+ href={user.behance}
+ />
+
+ );
+};
+
+const ProfileContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ padding: 20px 16px;
+ background-color: var(--Background-White);
+`;
+
+export default Profile;
diff --git a/src/pages/profile/ProfileHeader.tsx b/src/pages/profile/ProfileHeader.tsx
new file mode 100644
index 0000000..2fcd4d9
--- /dev/null
+++ b/src/pages/profile/ProfileHeader.tsx
@@ -0,0 +1,52 @@
+import styled from 'styled-components';
+import { ReactComponent as BackIcon } from 'static/images/back-arrow-icon.svg';
+import { ReactComponent as PencilIcon } from 'static/images/pencil-bold-icon.svg';
+import ButtonWithIcon from 'pages/common/ButtonWithIcon';
+import { useNavigate } from 'react-router-dom';
+
+interface ProfileHeaderProps {
+ isProfileChanging: boolean;
+ handleClickProfileChangeButton: () => void;
+}
+const ProfileHeader = ({
+ isProfileChanging,
+ handleClickProfileChangeButton,
+}: ProfileHeaderProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+ }
+ handleClickButton={() => {
+ navigate('/');
+ }}
+ size={28}
+ />
+ {isProfileChanging ? (
+
+ ) : (
+ }
+ handleClickButton={handleClickProfileChangeButton}
+ size={28}
+ />
+ )}
+
+ );
+};
+
+const ProfileHeaderContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ .okButton {
+ height: 28px;
+ font-size: 16px;
+ color: black;
+ }
+`;
+
+export default ProfileHeader;
diff --git a/src/pages/profile/ProfileImage.tsx b/src/pages/profile/ProfileImage.tsx
new file mode 100644
index 0000000..b0394b2
--- /dev/null
+++ b/src/pages/profile/ProfileImage.tsx
@@ -0,0 +1,98 @@
+import styled from 'styled-components';
+import { ReactComponent as DefaultProfileIcon } from 'static/images/default-profile-icon.svg';
+import { ReactComponent as CameraIcon } from 'static/images/camera-icon.svg';
+import { CompressImage } from 'utils/fileCompression';
+import { memo } from 'react';
+
+interface ProfileImageProps {
+ isProfileChanging: boolean;
+ newProfileImage: string | null;
+ setNewProfileImage: React.Dispatch>;
+}
+
+const ProfileImage = ({
+ isProfileChanging,
+ newProfileImage,
+ setNewProfileImage,
+}: ProfileImageProps) => {
+ return (
+
+
+ {newProfileImage ? (
+
+ ) : (
+
+ )}
+
+ {isProfileChanging ? (
+
+
+ {
+ if (e.target.files && e.target.files.length) {
+ // 압축 진행
+ const compressedFile = await CompressImage(e.target.files[0]);
+ if (compressedFile) {
+ // file reader로 이미지 encoding
+ const reader = new FileReader();
+ reader.readAsDataURL(compressedFile);
+ reader.onloadend = () => {
+ if (reader.result) {
+ // string 형태로 이미지 저장
+ setNewProfileImage(reader.result.toString());
+ }
+ };
+ }
+ }
+ }}
+ />
+
+ ) : null}
+
+ );
+};
+
+const ProfileImageOuter = styled.div`
+ position: relative;
+`;
+
+const ProfileImageConatiner = styled.div`
+ width: 108px;
+ height: 108px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ overflow: hidden;
+ img,
+ svg {
+ width: 100%;
+ object-fit: cover;
+ }
+`;
+
+const ProfileImageChangeButton = styled.label`
+ position: absolute;
+ bottom: 5px;
+ right: 5px;
+ background: var(--Gray-1);
+ border-radius: 50%;
+ z-index: 100;
+ border: 1px solid black;
+ height: 22px;
+ width: 22px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+ input {
+ display: none;
+ }
+`;
+
+export default memo(ProfileImage);
diff --git a/src/pages/profile/StatusMessage.tsx b/src/pages/profile/StatusMessage.tsx
new file mode 100644
index 0000000..9fc0319
--- /dev/null
+++ b/src/pages/profile/StatusMessage.tsx
@@ -0,0 +1,79 @@
+import { memo, useEffect, useRef } from 'react';
+import styled from 'styled-components';
+
+interface StatusMessageProps {
+ isProfileChanging: boolean;
+ statusMessage: string | null;
+ newStatusMessage: string | null;
+ setNewStatusMessage: React.Dispatch>;
+}
+
+const StatusMessage = ({
+ isProfileChanging,
+ statusMessage,
+ newStatusMessage,
+ setNewStatusMessage,
+}: StatusMessageProps) => {
+ const statusMessageRef = useRef(null);
+
+ // textarea의 높이 초기 설정
+ useEffect(() => {
+ if (statusMessageRef.current) {
+ statusMessageRef.current.style.height =
+ statusMessageRef.current.scrollHeight + 'px';
+ }
+ }, [isProfileChanging]);
+
+ return (
+
+ {isProfileChanging ? (
+
+ ) : (
+ {statusMessage}
+ )}
+
+ );
+};
+
+const StatusMessageOuter = styled.div`
+ position: relative;
+ padding: 0 12px 12px 12px;
+ margin-top: 4px;
+ width: 100%;
+ border-bottom: 1px solid var(--Gray-2);
+ .status-message {
+ text-align: center;
+ white-space: pre-wrap;
+ color: var(--Gray-2);
+ font-size: 14px;
+ line-height: 160%;
+ }
+ .status-message-textarea {
+ margin-top: 10px;
+ text-align: center;
+ width: 100%;
+ color: var(--Gray-2);
+ padding: 12px;
+ border: none;
+ resize: none;
+ border-radius: 4px;
+ font-size: 14px;
+ background: var(--Gray-1);
+ // line-height: 160%;
+ }
+`;
+
+export default memo(StatusMessage);
diff --git a/src/pages/profile/UserLink.tsx b/src/pages/profile/UserLink.tsx
new file mode 100644
index 0000000..43d5e23
--- /dev/null
+++ b/src/pages/profile/UserLink.tsx
@@ -0,0 +1,43 @@
+import { ReactNode } from 'react';
+import { ReactComponent as RightArrowIcon } from 'static/images/right-arrow-icon.svg';
+import styled from 'styled-components';
+
+interface UserLinkProps {
+ children: ReactNode;
+ linkName: string;
+ href: string | null;
+}
+const UserLink = ({ children, linkName, href }: UserLinkProps) => {
+ return (
+
+ {children}
+ {linkName}
+
+
+ );
+};
+
+const UserLinkContainer = styled.a`
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ width: 100%;
+ height: 40px;
+ padding: 0 28px 0 32px;
+ margin-bottom: 4px;
+ svg {
+ width: 28px;
+ height: 28px;
+ margin-right: 9px;
+ }
+ .link-name {
+ flex: 1;
+ color: black;
+ font-family: Pretendard Variable;
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 120%;
+ }
+`;
+
+export default UserLink;
diff --git a/src/pages/profile/UserProfile.tsx b/src/pages/profile/UserProfile.tsx
new file mode 100644
index 0000000..cc238ac
--- /dev/null
+++ b/src/pages/profile/UserProfile.tsx
@@ -0,0 +1,83 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import styled from 'styled-components';
+import { useUserStore } from 'stores/userStore';
+import { useEffect, useState } from 'react';
+import ProfileImage from 'pages/profile/ProfileImage';
+import Username from 'pages/profile/Username';
+import StatusMessage from 'pages/profile/StatusMessage';
+
+interface UserProfileProps {
+ username: string;
+ isProfileChanging: boolean;
+ profileImage: string | null;
+ statusMessage: string | null;
+}
+
+const UserProfile = ({
+ username,
+ isProfileChanging,
+ profileImage,
+ statusMessage,
+}: UserProfileProps) => {
+ const user = useUserStore((state) => state.user);
+ const setUser = useUserStore((state) => state.setUser);
+
+ const [newName, setNewName] = useState(username);
+ const [newStatusMessage, setNewStatusMessage] = useState(
+ statusMessage
+ );
+ const [newProfileImage, setNewProfileImage] = useState(
+ profileImage
+ );
+
+ // 프로필 변경 모드가 바뀔 때 프로필에 변경사항이 있다면 전역 user state 업데이트
+ useEffect(() => {
+ // console.log(username, newName);
+ if (
+ username !== newName ||
+ statusMessage !== newStatusMessage ||
+ profileImage !== newProfileImage
+ ) {
+ setUser({
+ ...user,
+ name: newName,
+ statusMessage: newStatusMessage,
+ profileImage: newProfileImage,
+ });
+ }
+ }, [isProfileChanging]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+const UserProfileContainer = styled.div`
+ margin-top: 66px;
+ margin-bottom: 8px;
+ width: 100%;
+ padding: 0 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+export default UserProfile;
diff --git a/src/pages/profile/Username.tsx b/src/pages/profile/Username.tsx
new file mode 100644
index 0000000..37703a2
--- /dev/null
+++ b/src/pages/profile/Username.tsx
@@ -0,0 +1,56 @@
+import { memo } from 'react';
+import styled from 'styled-components';
+
+interface UserNameProps {
+ isProfileChanging: boolean;
+ username: string;
+ newName: string;
+ setNewName: React.Dispatch>;
+}
+
+const UserName = ({
+ isProfileChanging,
+ username,
+ newName,
+ setNewName,
+}: UserNameProps) => {
+ return (
+
+ {isProfileChanging ? (
+ {
+ // 닉네임 10글자 제한
+ if (e.target.value.length <= 10) setNewName(e.target.value);
+ }}
+ />
+ ) : (
+ {username}
+ )}
+
+ );
+};
+
+const UsernameOuter = styled.div`
+ display: flex;
+ position: relative;
+ margin-top: 20px;
+ .username {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 160%;
+ }
+ .username-input {
+ text-align: center;
+ border: none;
+ color: var(--Gray-2);
+ padding: 6px 12px;
+ border-radius: 4px;
+ background: var(--Gray-1);
+ font-size: 14px;
+ }
+`;
+
+export default memo(UserName);
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
deleted file mode 100644
index 5253d3a..0000000
--- a/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 8f2609b..0000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/static/fonts/PretendardVariable.woff2 b/src/static/fonts/PretendardVariable.woff2
new file mode 100644
index 0000000..a8d0637
Binary files /dev/null and b/src/static/fonts/PretendardVariable.woff2 differ
diff --git a/src/static/images/back-arrow-icon.svg b/src/static/images/back-arrow-icon.svg
new file mode 100644
index 0000000..fd9329c
--- /dev/null
+++ b/src/static/images/back-arrow-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/behance-icon.svg b/src/static/images/behance-icon.svg
new file mode 100644
index 0000000..c14d81d
--- /dev/null
+++ b/src/static/images/behance-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/static/images/call-off-icon.svg b/src/static/images/call-off-icon.svg
new file mode 100644
index 0000000..b009faf
--- /dev/null
+++ b/src/static/images/call-off-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/camera-icon.svg b/src/static/images/camera-icon.svg
new file mode 100644
index 0000000..a3dbf52
--- /dev/null
+++ b/src/static/images/camera-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/static/images/chat-off-icon.svg b/src/static/images/chat-off-icon.svg
new file mode 100644
index 0000000..a7b4502
--- /dev/null
+++ b/src/static/images/chat-off-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/static/images/chat-on-icon.svg b/src/static/images/chat-on-icon.svg
new file mode 100644
index 0000000..53200c1
--- /dev/null
+++ b/src/static/images/chat-on-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/close-icon.svg b/src/static/images/close-icon.svg
new file mode 100644
index 0000000..e3639df
--- /dev/null
+++ b/src/static/images/close-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/default-profile-icon.svg b/src/static/images/default-profile-icon.svg
new file mode 100644
index 0000000..64cc7e9
--- /dev/null
+++ b/src/static/images/default-profile-icon.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/static/images/github-icon.svg b/src/static/images/github-icon.svg
new file mode 100644
index 0000000..37d2a22
--- /dev/null
+++ b/src/static/images/github-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/heart-icon.svg b/src/static/images/heart-icon.svg
new file mode 100644
index 0000000..dd7fffa
--- /dev/null
+++ b/src/static/images/heart-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/home-off-icon.svg b/src/static/images/home-off-icon.svg
new file mode 100644
index 0000000..5f3dfd3
--- /dev/null
+++ b/src/static/images/home-off-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/home-on-icon.svg b/src/static/images/home-on-icon.svg
new file mode 100644
index 0000000..11503d4
--- /dev/null
+++ b/src/static/images/home-on-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/instagram-icon.svg b/src/static/images/instagram-icon.svg
new file mode 100644
index 0000000..2702db9
--- /dev/null
+++ b/src/static/images/instagram-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/static/images/line-point-icon.svg b/src/static/images/line-point-icon.svg
new file mode 100644
index 0000000..4749776
--- /dev/null
+++ b/src/static/images/line-point-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/static/images/mic-icon.svg b/src/static/images/mic-icon.svg
new file mode 100644
index 0000000..f441b1b
--- /dev/null
+++ b/src/static/images/mic-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/more-icon.svg b/src/static/images/more-icon.svg
new file mode 100644
index 0000000..05f7aec
--- /dev/null
+++ b/src/static/images/more-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/static/images/more-off-icon.svg b/src/static/images/more-off-icon.svg
new file mode 100644
index 0000000..fc98d06
--- /dev/null
+++ b/src/static/images/more-off-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/more-on-icon.svg b/src/static/images/more-on-icon.svg
new file mode 100644
index 0000000..c41cb90
--- /dev/null
+++ b/src/static/images/more-on-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/static/images/official-icon.svg b/src/static/images/official-icon.svg
new file mode 100644
index 0000000..9963bbb
--- /dev/null
+++ b/src/static/images/official-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/pencil-bold-icon.svg b/src/static/images/pencil-bold-icon.svg
new file mode 100644
index 0000000..ff2c0d8
--- /dev/null
+++ b/src/static/images/pencil-bold-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/pencil-icon.svg b/src/static/images/pencil-icon.svg
new file mode 100644
index 0000000..38a14f1
--- /dev/null
+++ b/src/static/images/pencil-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/pic-icon.svg b/src/static/images/pic-icon.svg
new file mode 100644
index 0000000..9dd1023
--- /dev/null
+++ b/src/static/images/pic-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/plus-icon.svg b/src/static/images/plus-icon.svg
new file mode 100644
index 0000000..2a1ccb2
--- /dev/null
+++ b/src/static/images/plus-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/right-arrow-icon.svg b/src/static/images/right-arrow-icon.svg
new file mode 100644
index 0000000..a152750
--- /dev/null
+++ b/src/static/images/right-arrow-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/search-icon.svg b/src/static/images/search-icon.svg
new file mode 100644
index 0000000..851f9af
--- /dev/null
+++ b/src/static/images/search-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/images/send-icon.svg b/src/static/images/send-icon.svg
new file mode 100644
index 0000000..6f6dee8
--- /dev/null
+++ b/src/static/images/send-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/images/smile-icon.svg b/src/static/images/smile-icon.svg
new file mode 100644
index 0000000..c9dde81
--- /dev/null
+++ b/src/static/images/smile-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/static/images/start-chat-icon.svg b/src/static/images/start-chat-icon.svg
new file mode 100644
index 0000000..6add522
--- /dev/null
+++ b/src/static/images/start-chat-icon.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/static/images/stickers-icon.svg b/src/static/images/stickers-icon.svg
new file mode 100644
index 0000000..c9dde81
--- /dev/null
+++ b/src/static/images/stickers-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/stores/messageStore.ts b/src/stores/messageStore.ts
new file mode 100644
index 0000000..6a728a1
--- /dev/null
+++ b/src/stores/messageStore.ts
@@ -0,0 +1,35 @@
+import { devtools } from 'zustand/middleware';
+import { immer } from 'zustand/middleware/immer';
+import { TMessage } from 'types';
+import chatData from 'data/chatData.json';
+import { create } from 'zustand';
+
+interface TMessageStore {
+ messages: TMessage[];
+ setMessages: (messages: TMessage[]) => void;
+ toggleIsRead: (idx: number) => void;
+}
+
+const storedMessages: string | null = localStorage.getItem('messages');
+const storedVersion = localStorage.getItem('version');
+// localStorage에 data가 있으면 그것을 사용하고, 아니면 dummy 사용
+const initialMessageState: TMessage[] =
+ storedMessages &&
+ storedVersion &&
+ storedVersion === process.env.REACT_APP_VERSION
+ ? JSON.parse(storedMessages)
+ : chatData.data;
+
+export const useMessageStore = create(
+ devtools(
+ // immer을 통해 messages 내부의 객체 수정 시 불필요한 리렌더링 방지
+ immer((set) => ({
+ messages: initialMessageState,
+ toggleIsRead: (idx: number) =>
+ set((state) => {
+ state.messages[idx].isRead = !state.messages[idx].isRead;
+ }),
+ setMessages: (newMessages: TMessage[]) => set({ messages: newMessages }),
+ }))
+ )
+);
diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts
new file mode 100644
index 0000000..6087710
--- /dev/null
+++ b/src/stores/userStore.ts
@@ -0,0 +1,23 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { TUser } from 'types';
+import userData from 'data/userData.json';
+
+interface TUserStore {
+ user: TUser;
+ setUser: (user: TUser) => void;
+}
+
+const user: string | null = localStorage.getItem('user');
+const storedVersion = localStorage.getItem('version');
+const initialUserState: TUser =
+ user && storedVersion && storedVersion === process.env.REACT_APP_VERSION
+ ? JSON.parse(user)
+ : userData.data[0]; // default로 설정되는 유저는 user_1
+
+export const useUserStore = create(
+ devtools((set) => ({
+ user: initialUserState,
+ setUser: (newUser: TUser) => set({ user: newUser }),
+ }))
+);
diff --git a/src/styles/global.style.ts b/src/styles/global.style.ts
new file mode 100644
index 0000000..597e41f
--- /dev/null
+++ b/src/styles/global.style.ts
@@ -0,0 +1,62 @@
+import * as styled from 'styled-components';
+import reset from 'styled-reset';
+import PretendardVariable from 'static/fonts/PretendardVariable.woff2';
+
+export const ChatRoomBackgroundColor = '#93aad4';
+export const BackgroundColor = '#fafafa';
+
+export const GlobalStyle = styled.createGlobalStyle`
+ ${reset}
+
+ #root {
+ --Background-White: #fafafa;
+ --Blue: ${ChatRoomBackgroundColor};
+ --Green: #ace49b;
+ --Gray-1: #f5f5f5;
+ --Gray-2: #8f8f8f;
+ --Gray-3: #5a5a5a;
+ --gray-30: rgba(0, 0, 0, 0.3);
+ }
+
+ html,
+ body,
+ #root {
+ font-family: 'Pretendard Variable', sans-serif;
+ width: 100%;
+ height: 100%;
+ }
+
+ * {
+ // 이유는 모르겠지만, 위랑 아래 이렇게 둘 다 폰트 지정을 해줘야 데스크탑 모바일 환경에서 모두 정상적으로 폰트 적용됨
+ font-family: 'Pretendard Variable', sans-serif;
+ font-weight: 400;
+ box-sizing: border-box;
+ }
+
+ button {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ input:focus,
+ textarea:focus {
+ outline: none;
+ }
+ input::placeholder,
+ textarea::placeholder {
+ font-family: 'Pretendard Variable';
+ }
+
+ @font-face {
+ font-family: 'Pretendard Variable';
+ font-weight: 45 920;
+ font-style: normal;
+ font-display: block;
+ src: url(${PretendardVariable}) format('woff2-variations');
+ }
+`;
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..4b53c78
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,28 @@
+export interface TMessage {
+ id: number;
+ fromUserId: number;
+ toUserId: number;
+ text: string;
+ time: string;
+ isRead: boolean;
+ likeCount: number;
+}
+
+export interface TUser {
+ id: number;
+ name: string;
+ profileImage: string | null;
+ statusMessage: string | null;
+ likedMessages: number[];
+ github: string | null;
+ behance: string | null;
+ instagram: string | null;
+}
+
+export interface TChatRoomInfo {
+ id: number;
+ profileImage: string | null;
+ userName: string;
+ message: string;
+ time: string;
+}
diff --git a/src/utils/fileCompression.ts b/src/utils/fileCompression.ts
new file mode 100644
index 0000000..afb2609
--- /dev/null
+++ b/src/utils/fileCompression.ts
@@ -0,0 +1,13 @@
+// 프로필 사진의 크기가 너무 커서, localStorage에 overflow가 발생하는 것을 방지하기 위한 압축 기능
+
+import imageCompression from 'browser-image-compression';
+
+export const CompressImage = async (image: File) => {
+ const options = {
+ maxSizeMB: 0.01,
+ maxWidthOrHeight: 1920,
+ useWebWorker: true,
+ };
+ const compressedFile = await imageCompression(image, options);
+ return compressedFile;
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..3554d98
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,127 @@
+import { TMessage } from 'types';
+import userData from 'data/userData.json';
+import { include } from 'utils/search';
+
+const daySelector = ['Sun', 'Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat'];
+const monthSelector = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+];
+
+// 채팅버블 시간 formatting
+export const convertTimeFormat = (date: string) => {
+ const dateObj = new Date(date);
+ let amPm = 'AM';
+ let hour = dateObj.getHours();
+ if (hour > 12) {
+ hour -= 12;
+ amPm = 'PM';
+ }
+ if (hour === 0) hour = 12;
+ return `${hour}:${String(dateObj.getMinutes()).padStart(2, '0')} ${amPm}`;
+};
+
+// 채팅목록 시간 formatting
+export const convertTimeFormatForChatRoom = (date: string) => {
+ const dateObj = new Date(date);
+
+ let converted = '';
+ if (
+ new Date().getFullYear() === dateObj.getFullYear() &&
+ new Date().getMonth() === dateObj.getMonth() &&
+ new Date().getDate() === dateObj.getDate()
+ ) {
+ let hour = dateObj.getHours() % 12;
+ const amFlag = dateObj.getHours() < 12;
+ if (hour === 0) {
+ hour = 12;
+ }
+
+ converted = `${amFlag ? '오전' : '오후'} ${hour}:${String(
+ dateObj.getMinutes()
+ ).padStart(2, '0')}`;
+ return converted;
+ }
+ if (new Date().getFullYear() > dateObj.getFullYear()) {
+ converted = converted.concat(`${dateObj.getFullYear()}`);
+ }
+ converted = converted.concat(
+ `${dateObj.getMonth() + 1}/${String(dateObj.getDate()).padStart(2, '0')}`
+ );
+
+ return converted;
+};
+
+// 채팅방 날짜 formatting
+export const convertDayDateFormat = (originalDate: string) => {
+ const dateObj = new Date(originalDate);
+ const day = daySelector[dateObj.getDay()];
+ const month = monthSelector[dateObj.getMonth()];
+ const date = dateObj.getDate();
+
+ return `${day}, ${month} ${date}`;
+};
+
+export const checkIsNextDay = (date1: string, date2: string) => {
+ const dateObj1 = new Date(date1);
+ const dateObj2 = new Date(date2);
+ if (
+ dateObj1.getFullYear() === dateObj2.getFullYear() &&
+ dateObj1.getMonth() === dateObj2.getMonth() &&
+ dateObj1.getDate() === dateObj2.getDate()
+ )
+ return false;
+ return true;
+};
+
+// 채팅목록 구성을 위한 각 유저와의 마지막 메시지 구하기
+export const getLastMessages = (id: number, messages: TMessage[]) => {
+ const lastMessages: TMessage[] = [];
+ const checkFirst = Array(userData.data.length + 1).fill(0);
+
+ [...messages].reverse().forEach((message) => {
+ if (message.fromUserId === id || message.toUserId === id) {
+ const opponent =
+ message.fromUserId === id ? message.toUserId : message.fromUserId;
+
+ if (!checkFirst[opponent]) {
+ lastMessages.push(message);
+ checkFirst[opponent] = 1;
+ }
+ }
+ });
+
+ return lastMessages;
+};
+
+// 검색된 유저 목록
+export const getSearchedUsers = (userId: number, query: string) => {
+ const storedUserData = userData.data.filter(
+ (e) => e.id !== userId && include(e.name, query)
+ );
+ // 유저 데이터가 초기 상태에서 변경되어 localStorage에 저장되어 있다면 해당 데이터로 교체
+ for (let i = 0; i < storedUserData.length; i += 1) {
+ const data = localStorage.getItem(`user_${storedUserData[i].id}`);
+ if (data) {
+ storedUserData[i] = JSON.parse(data);
+ }
+ }
+
+ // 사전순 정렬
+ storedUserData.sort((a, b) => {
+ if (a.name < b.name) return -1;
+ else return 1;
+ });
+
+ return storedUserData;
+};
diff --git a/src/utils/search.ts b/src/utils/search.ts
new file mode 100644
index 0000000..228a946
--- /dev/null
+++ b/src/utils/search.ts
@@ -0,0 +1,54 @@
+// 한글 초성 검색을 가능하게 하는 기능, 그냥 귀찮아서 구글링해서 찾음
+
+const CHO_HANGUL = [
+ 'ㄱ',
+ 'ㄲ',
+ 'ㄴ',
+ 'ㄷ',
+ 'ㄸ',
+ 'ㄹ',
+ 'ㅁ',
+ 'ㅂ',
+ 'ㅃ',
+ 'ㅅ',
+ 'ㅆ',
+ 'ㅇ',
+ 'ㅈ',
+ 'ㅉ',
+ 'ㅊ',
+ 'ㅋ',
+ 'ㅌ',
+ 'ㅍ',
+ 'ㅎ',
+];
+
+const HANGUL_START_CHARCODE = '가'.charCodeAt(0);
+
+const CHO_PERIOD = Math.floor('까'.charCodeAt(0) - '가'.charCodeAt(0));
+const JUNG_PERIOD = Math.floor('개'.charCodeAt(0) - '가'.charCodeAt(0));
+
+function combine(cho: number, jung: number, jong: number) {
+ return String.fromCharCode(
+ HANGUL_START_CHARCODE + cho * CHO_PERIOD + jung * JUNG_PERIOD + jong
+ );
+}
+
+function makeRegexByCho(search = '') {
+ const regex = CHO_HANGUL.reduce(
+ (acc, cho, index) =>
+ acc.replace(
+ new RegExp(cho, 'g'),
+ `[${combine(index, 0, 0)}-${combine(index + 1, 0, -1)}]`
+ ),
+ search
+ );
+
+ return new RegExp(`(${regex})`, 'g');
+}
+
+export function include(target: string, query: string) {
+ if (!query) return target;
+ if (/[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(query))
+ return makeRegexByCho(query).test(target);
+ else return target.toLowerCase().includes(query.toLowerCase());
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5211c0a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2016",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "baseUrl": "src",
+ "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", "src/custom.d.ts"]
+}