From 2e4b5781252d244dc412ad16b61c730ec5a6d6af Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sat, 6 Apr 2024 17:34:56 +0900 Subject: [PATCH 001/130] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=98=B5=EC=85=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/profile-page.tsx | 22 +-- src/app/pages/setting-page.tsx | 6 +- src/app/pages/user-input-page.tsx | 8 +- src/app/pages/writing-post-page.tsx | 4 +- src/components/FloatingChatting.tsx | 6 +- src/components/OptionSection.tsx | 83 +++++---- src/components/VitalSection.tsx | 114 +++++++----- src/components/options/CleanTest.tsx | 216 +++++++++++++++++++++++ src/components/options/MajorSelector.tsx | 48 +++++ 9 files changed, 402 insertions(+), 105 deletions(-) create mode 100644 src/components/options/CleanTest.tsx create mode 100644 src/components/options/MajorSelector.tsx diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index 4ddf10146d..dd992ba261 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -28,7 +28,7 @@ const styles = { height: 8.3125rem; justify-content: center; align-items: center; - border-radius: 6.25rem; + border-radius: 100px; border: 1px solid #dcddea; background: #c4c4c4; @@ -99,7 +99,7 @@ const styles = { background-color: #bebebe; -webkit-transition: 0.4s; transition: 0.4s; - border-radius: 1.5rem; + border-radius: 24px; `, sliderDot: styled.span` position: absolute; @@ -126,7 +126,7 @@ const styles = { authContainer: styled.div` height: 2rem; width: 5.3125rem; - border-radius: 1.625rem; + border-radius: 26px; background: #5c6eb4; margin: 1rem 1.4375rem 0 1.5625rem; cursor: pointer; @@ -196,7 +196,7 @@ const styles = { justify-content: center; align-items: center; gap: 0.25rem; - border-radius: 0.5rem; + border-radius: 8px; border: 1px solid var(--Gray-5, #828282); background: var(--White, #fff); @@ -223,7 +223,7 @@ const styles = { width: 15rem; height: 15rem; flex-shrink: 0; - border-radius: 1.25rem; + border-radius: 20px; border: 1pxs olid var(--background, #f7f6f9); box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); background: var(--grey-100, #fff); @@ -249,7 +249,7 @@ const styles = { width: 6.0625rem; height: 2.5625rem; flex-shrink: 0; - border-radius: 1.25rem 0rem 0rem 1.25rem; + border-radius: 20px 0 0 20px; background: var(--Main-1, #e15637); position: absolute; @@ -298,7 +298,7 @@ const styles = { height: 15.625rem; padding: 1.5rem 0; flex-shrink: 0; - border-radius: 1.25rem; + border-radius: 20px; background: var(--background, #f7f6f9); justify-content: center; @@ -338,7 +338,7 @@ const styles = { justify-content: center; align-items: center; gap: 0.5rem; - border-radius: 1.625rem; + border-radius: 26px; color: #fff; text-align: center; font-family: 'Noto Sans KR'; @@ -371,7 +371,7 @@ const styles = { padding: 0.63rem 1.13rem; justify-content: center; align-items: center; - border-radius: 1rem; + border-radius: 16px; border: 1px solid var(--Main-1, #e15637); background: var(--White, #fff); color: var(--Main-1, #e15637); @@ -384,7 +384,7 @@ const styles = { rulesContent: styled.div` width: 100%; height: 21.625rem; - border-radius: 1rem; + border-radius: 16px; background: #f7f6f9; `, @@ -400,7 +400,7 @@ const styles = { accountContent: styled.div` width: 64.5rem; height: 12.4375rem; - border-radius: 1rem; + border-radius: 16px; background: #f7f6f9; `, }; diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 25da50c8b1..d220131a3a 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -30,7 +30,7 @@ const styles = { width: 23.0625rem; height: 17.5rem; flex-shrink: 0; - border-radius: 1.875rem; + border-radius: 30px; background: #f7f6f9; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); padding: 1.62rem 1.44rem; @@ -57,7 +57,7 @@ const styles = { justify-content: center; align-items: center; gap: 0.5rem; - border-radius: 1.625rem; + border-radius: 26px; border: 2px solid var(--Main-1, #e15637); background: #fff; @@ -73,7 +73,7 @@ const styles = { width: 51.0625rem; height: 95.8125rem; flex-shrink: 0; - border-radius: 1.875rem; + border-radius: 30px; background: var(--background, #f7f6f9); padding: 3.56rem 0 0 1.56rem; margin-bottom: 7.5rem; diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 4b7e632e25..8e12fc3406 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -38,7 +38,7 @@ const styles = { width: 23.0625rem; height: 17.5rem; flex-shrink: 0; - border-radius: 1.875rem; + border-radius: 30px; padding: 1.62rem 1.44rem; display: flex; flex-direction: column; @@ -78,7 +78,7 @@ const styles = { justify-content: center; align-items: center; gap: 0.5rem; - border-radius: 1.625rem; + border-radius: 26px; border: 2px solid var(--Main-1, #e15637); background: #fff; @@ -99,7 +99,7 @@ const styles = { width: 51.0625rem; height: 95.8125rem; flex-shrink: 0; - border-radius: 1.875rem; + border-radius: 30px; background: var(--background, #f7f6f9); padding: 3.56rem 0 0 1.56rem; margin-bottom: 7.5rem; @@ -129,7 +129,7 @@ const styles = { justify-content: center; align-items: center; gap: 0.25rem; - border-radius: 0.5rem; + border-radius: 8px; background: var(--Main-1, #e15637); margin: 4.06rem 31rem 9.06rem 31rem; `, diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 9cc1e02f4c..cb1760e549 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -25,7 +25,7 @@ const styles = { display: flex; flex-direction: column; background-color: #fff; - border-radius: 2rem; + border-radius: 32px; margin: 3rem 7.5rem; padding: 3.69rem 0 0 4.19rem; `, @@ -171,7 +171,7 @@ const styles = { padding: 0.5rem 1rem 3.5rem 1rem; border: none; - border-radius: 0.5rem; + border-radius: 8px; background: var(--Gray-6, #efefef); color: #9a95a3; diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 4017a98dcf..af34ecebd4 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -13,7 +13,7 @@ const styles = { padding: 1rem; align-items: flex-start; gap: 0.5rem; - border-radius: 6.25rem; + border-radius: 100px; background: #e15637; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); z-index: 100; @@ -34,7 +34,7 @@ const styles = { flex-direction: column; align-items: flex-start; flex-shrink: 0; - border-radius: 1.25rem; + border-radius: 20px; background: #fff; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); z-index: 100; @@ -46,7 +46,7 @@ const styles = { width: 100%; height: 3.25rem; flex-shrink: 0; - border-radius: 1.25rem 1.25rem 0rem 0rem; + border-radius: 20px 20px 0 0; background: var(--background, #f7f6f9); `, }; diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index ee79ea7f1c..56b5a59b0d 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -3,6 +3,9 @@ import React, { useState } from 'react'; import styled from 'styled-components'; +import { CleanTest } from './options/CleanTest'; +import { MajorSelector } from './options/MajorSelector'; + const styles = { optionContainer: styled.div` display: flex; @@ -19,6 +22,7 @@ const styles = { line-height: normal; `, optionList: styled.ul` + position: relative; width: 100%; margin-top: 2.25rem; `, @@ -80,7 +84,7 @@ const styles = { width: 25rem; height: 0.3125rem; flex-shrink: 0; - border-radius: 1.25rem; + border-radius: 20px; background: #d9d9d9; margin-bottom: 0.63rem; `, @@ -131,7 +135,7 @@ const styles = { background-color: #bebebe; -webkit-transition: 0.4s; transition: 0.4s; - border-radius: 1.5rem; + border-radius: 24px; `, sliderDot: styled.span` position: absolute; @@ -188,7 +192,7 @@ const CheckItem = styled.div` justify-content: center; align-items: center; gap: 0.5rem; - border-radius: 1.625rem; + border-radius: 26px; border: 2px solid #dfdfdf; background: #fff; cursor: pointer; @@ -222,7 +226,6 @@ const HearingOptions = [ '게임 소음 허용', ]; const WeatherOptions = ['더위 많이 타요', '추위 많이 타요']; -const CleanOptions = ['상', '평범보통', '천하태평']; const PersonalOptions = [ '반려동물', '차량 보유', @@ -233,7 +236,6 @@ const PersonalOptions = [ '엠비티아이', '전공', ]; -const BudgetOptions = ['보증금', '월세']; export function OptionSection() { type SelectedOptions = Record; @@ -261,6 +263,17 @@ export function OptionSection() { })); }; + const [isTestVisible, setIsTestVisible] = useState(false); + const [score, setScore] = useState(-1); + + const toggleTestVisibility = () => { + setIsTestVisible(prev => !prev); + }; + + const handleTestCompletion = (cleanScore: number) => { + setScore(cleanScore); + }; + return ( 선택 @@ -333,23 +346,37 @@ export function OptionSection() { - + 테스트 하기 - {CleanOptions.map(option => ( - { - handleOptionClick(option); - }} - > - {option} - - ))} + = 0 && score < 5.34} + onClick={() => { + handleOptionClick('상'); + }} + > + 상 + + 5.34 && score < 10.67} + onClick={() => { + handleOptionClick('중'); + }} + > + 평범보통 + + 10.67} + onClick={() => { + handleOptionClick('하'); + }} + > + 천하태평 + + {isTestVisible && } @@ -364,6 +391,7 @@ export function OptionSection() { {option === '엠비티아이' ? <>MBTI : option} ))} + {selectedOptions['전공'] ? : null} {selectedOptions['엠비티아이'] ? ( @@ -407,27 +435,16 @@ export function OptionSection() { J - ) : ( - <> - )} + ) : null} - {BudgetOptions.map(option => ( - - { - handleOptionClick(option); - }} - > - {option} - - - - ))} + + 금액 + + diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 9987b30b2d..4c0d316ca2 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -17,19 +17,20 @@ const styles = { font-weight: 500; line-height: normal; `, - vitalListContainer: styled.div` + vitalListContainer: styled.ul` display: flex; + flex-direction: column; margin-top: 2.62rem; - gap: 3.12rem; + gap: 1.5rem; `, - vitalList: styled.ul` + vitalList: styled.li` display: inline-flex; - flex-direction: column; - align-items: flex-start; + align-items: center; gap: 2.5rem; flex-shrink: 0; `, - vitalListItem: styled.li` + vitalListItemDescription: styled.p` + width: 5.125rem; color: #000; font-family: 'Noto Sans KR'; @@ -40,15 +41,10 @@ const styles = { list-style-type: none; `, - vitalCheckList: styled.ul` + vitalCheckListContainer: styled.div` display: inline-flex; - flex-direction: column; align-items: flex-start; - gap: 1.5rem; - `, - vitalCheckListItem: styled.li` - display: flex; - list-style-type: none; + gap: 0.5rem; `, birthYear: styled.select` @@ -56,7 +52,7 @@ const styles = { -moz-appearance: none; appearance: none; - width: 6.125rem; + width: 6.7rem; height: 3.125rem; display: inline-flex; padding: 0.75rem 1rem; @@ -112,7 +108,7 @@ const CheckItem = styled.div` justify-content: center; align-items: center; gap: 0.5rem; - border-radius: 1.625rem; + border-radius: 26px; border: 2px solid #dfdfdf; background: #fff; cursor: pointer; @@ -172,14 +168,10 @@ export function VitalSection() { 필수 - 성별 - 출생 연도 - 흡연 여부 - 메이트와 - 희망 지역 - - - + + 성별 + + { @@ -189,28 +181,28 @@ export function VitalSection() { 남성 { - handleOptionClick('gender', '여성'); + handleOptionClick('gender', '남성'); }} > 여성 - - - - - {years.map(year => ( - - ))} - - - + + + + + 희망 지역 + + + + + + + + 흡연 여부 + + { @@ -227,8 +219,21 @@ export function VitalSection() { > 비흡연 - - + { + handleOptionClick('smoking', '상관없어요'); + }} + > + 상관없어요 + + + + + + 메이트와 + + { @@ -253,13 +258,24 @@ export function VitalSection() { > 상관없어요 - - - - - - - + + + + + 출생 연도 + + + + {years.map(year => ( + + ))} + + ); diff --git a/src/components/options/CleanTest.tsx b/src/components/options/CleanTest.tsx new file mode 100644 index 0000000000..c3749a0064 --- /dev/null +++ b/src/components/options/CleanTest.tsx @@ -0,0 +1,216 @@ +'use client'; + +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const styles = { + testSection: styled.ul` + position: absolute; + display: inline-flex; + flex-direction: column; + height: 31.25rem; + padding: 2.6875rem 3.4375rem; + border-radius: 20px; + background: #fff; + gap: 2rem; + z-index: 5; + + /* button */ + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + `, + testListContainer: styled.li` + display: flex; + align-items: center; + gap: 4.31rem; + `, + testDescription: styled.p` + width: 6.9rem; + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 1.25rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + testItemContainer: styled.div` + display: flex; + align-items: flex-end; + gap: 0.5rem; + `, +}; + +interface CheckItemProps { + $isSelected: boolean; +} + +const CheckItem = styled.div` + display: flex; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 26px; + border: 2px solid #dfdfdf; + background: #fff; + cursor: pointer; + + color: #888; + + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + + ${props => + props.$isSelected + ? { + color: 'var(--Main-1, #E15637)', + border: '2px solid var(--Main-1, #E15637)', + } + : { + backgroundColor: '#FFF', + color: '#888', + }}; +`; + +const room = ['매일', '주 2회 이상', '주 1회', '가끔']; +const bathRoom = ['매일', '주 2회 이상', '주 1회', '가끔']; +const washingDishes = ['식사 직후', '매일', '가끔']; +const laundry = ['매일', '주 2회 이상', '주 1회', '가끔']; +const wasteSorting = ['매일', '주 2회 이상', '주 1회', '가끔']; +const currentState = ['깔끔', '정리 필요', '대청소 필요', '카오스']; + +export function CleanTest({ + onComplete, +}: { + onComplete: (score: number) => void; +}) { + const [selectedOptions, setSelectedOptions] = useState({ + room: 0, + bathRoom: 0, + washingDishes: 0, + laundry: 0, + wasteSorting: 0, + currentState: 0, + }); + + const handleOptionClick = (option: string, idx: number) => { + setSelectedOptions(prevSelectedOptions => ({ + ...prevSelectedOptions, + [option]: idx, + })); + + onComplete(calculateScore()); + }; + + const calculateScore = () => { + let score = -5; + Object.values(selectedOptions).forEach(option => { + score += option + 1; + }); + return score; + }; + + return ( + + + 방 청소 + + {room.map((option, idx) => ( + { + handleOptionClick('room', idx); + }} + > + {option} + + ))} + + + + 욕실 청소 + + {bathRoom.map((option, idx) => ( + { + handleOptionClick('bathRoom', idx); + }} + > + {option} + + ))} + + + + 설거지 + + {washingDishes.map((option, idx) => ( + { + handleOptionClick('washingDishes', idx); + }} + > + {option} + + ))} + + + + 세탁 + + {laundry.map((option, idx) => ( + { + handleOptionClick('laundry', idx); + }} + > + {option} + + ))} + + + + 분리수거 + + {wasteSorting.map((option, idx) => ( + { + handleOptionClick('wasteSorting', idx); + }} + > + {option} + + ))} + + + + 현재 집 상태 + + {currentState.map((option, idx) => ( + { + handleOptionClick('currentState', idx); + }} + > + {option} + + ))} + + + + ); +} diff --git a/src/components/options/MajorSelector.tsx b/src/components/options/MajorSelector.tsx new file mode 100644 index 0000000000..2148ea7d69 --- /dev/null +++ b/src/components/options/MajorSelector.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const styles = { + majorSelector: styled.select` + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + display: flex; + width: 5.875rem; + padding: 0.575rem 0; + align-items: center; + border: 2px solid var(--Gray-4, #dfdfdf); + background: var(--White, #fff); + + color: var(--Gray-3, #888); + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + text-align: center; + `, +}; + +const majorOptions = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; + +export function MajorSelector() { + const [selectedMajor, setSelectedMajor] = useState(''); + const handleMajorChange = (event: React.ChangeEvent) => { + setSelectedMajor(event.target.value); + }; + return ( + + + {majorOptions.map(major => ( + + ))} + + ); +} From 90972f58315a3a153e9ca7c16945c51becbe2f1d Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 10 Apr 2024 18:06:25 +0900 Subject: [PATCH 002/130] fix: miniCard Design (#52) --- next.config.mjs | 9 +- package.json | 1 + public/option-img/location_on.svg | 10 + public/option-img/meeting_room.svg | 10 + public/option-img/person.svg | 2 +- public/option-img/visibility.svg | 2 +- src/app/pages/user-input-page.tsx | 143 +- src/components/OptionSection.tsx | 4 +- .../{options => card}/CleanTest.tsx | 0 .../{options => card}/MajorSelector.tsx | 0 yarn.lock | 1623 ++++++++++++++++- 11 files changed, 1765 insertions(+), 39 deletions(-) create mode 100644 public/option-img/location_on.svg create mode 100644 public/option-img/meeting_room.svg rename src/components/{options => card}/CleanTest.tsx (100%) rename src/components/{options => card}/MajorSelector.tsx (100%) diff --git a/next.config.mjs b/next.config.mjs index ede628e888..e67a37e5d9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - // reactStrictMode: false, reactStrictMode: true, compiler: { styledComponents: true, @@ -14,6 +13,14 @@ const nextConfig = { ]; }, trailingSlash: false /* 만약, 후행 슬래시가 필요하다면 true, default → false */, + webpack: config => { + config.module.rules.push({ + test: /\.svg$/, + use: ['@svgr/webpack'], + }); + + return config; + }, }; export default nextConfig; diff --git a/package.json b/package.json index 704661dda7..31df737c2c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "styled-components": "^6.1.8" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "@tanstack/eslint-plugin-query": "^5.20.1", "@types/navermaps": "^3.7.4", "@types/node": "^20", diff --git a/public/option-img/location_on.svg b/public/option-img/location_on.svg new file mode 100644 index 0000000000..3c2b4915a1 --- /dev/null +++ b/public/option-img/location_on.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/option-img/meeting_room.svg b/public/option-img/meeting_room.svg new file mode 100644 index 0000000000..1ff5c1e47a --- /dev/null +++ b/public/option-img/meeting_room.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/option-img/person.svg b/public/option-img/person.svg index f5f0c2428f..50cd89a34c 100644 --- a/public/option-img/person.svg +++ b/public/option-img/person.svg @@ -1,4 +1,4 @@ - + diff --git a/public/option-img/visibility.svg b/public/option-img/visibility.svg index 8c78ae026d..ebf2fa343e 100644 --- a/public/option-img/visibility.svg +++ b/public/option-img/visibility.svg @@ -1,4 +1,4 @@ - + diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 8e12fc3406..e8a9d7860f 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -3,6 +3,11 @@ import React, { useState } from 'react'; import styled from 'styled-components'; +import Location from '../../../public/option-img/location_on.svg'; +import Meeting from '../../../public/option-img/meeting_room.svg'; +import Person from '../../../public/option-img/person.svg'; +import Visibility from '../../../public/option-img/visibility.svg'; + import { VitalSection, OptionSection } from '@/components'; const styles = { @@ -67,28 +72,72 @@ const styles = { line-height: normal; margin-bottom: 1.69rem; `, - miniCardKeywordsContainer: styled.div` - width: 17.5625rem; - height: 5.6875rem; - position: relative; + miniCardKeywordsContainer: styled.ul` + display: flex; + width: 18.375rem; + flex-direction: column; + align-items: flex-start; + gap: 1rem; `, - miniCardKeyword: styled.div` - display: inline-flex; - padding: 0.5rem 1.5rem; - justify-content: center; + miniCardList: styled.li` + display: flex; align-items: center; - gap: 0.5rem; - border-radius: 26px; - border: 2px solid var(--Main-1, #e15637); - background: #fff; - - color: var(--Main-1, #e15637); + gap: 2rem; + align-self: stretch; + `, + miniCardPerson: styled(Person)` + width: 1.5rem; + height: 1.5rem; + path { + fill: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Main-1, #e15637)' + : 'var(--Main-2, #767D86)'}; + } + `, + miniCardLocation: styled(Location)` + width: 1.5rem; + height: 1.5rem; + path { + fill: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Main-1, #e15637)' + : 'var(--Main-2, #767D86)'}; + } + `, + miniCardMeeting: styled(Meeting)` + width: 1.5rem; + height: 1.5rem; + path { + fill: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Main-1, #e15637)' + : 'var(--Main-2, #767D86)'}; + } + `, + miniCardVisibility: styled(Visibility)` + width: 1.5rem; + height: 1.5rem; + path { + fill: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Main-1, #e15637)' + : 'var(--Main-2, #767D86)'}; + } + `, + miniCardText: styled.p` + flex: 1 0 0; + height: 1.5rem; font-family: 'Noto Sans KR'; font-size: 1rem; font-style: normal; font-weight: 500; line-height: normal; - position: absolute; + + color: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Main-1, #e15637)' + : 'var(--Main-2, #767D86)'}; `, checkSection: styled.div` width: calc(100% - 23.0625rem); @@ -178,13 +227,30 @@ export function UserInputPage() { 내카드 - 여성 - - 비흡연 - - - 아침형 - + + + + 여성 · 00년생 · 비흡연 + + + + + + 서울특별시 성북구 정릉동 + + + + + + 메이트와 다른 방 + + + + + + 아침형 + + - 여성 - - 비흡연 - - - 아침형 - + + + + 여성 · 00년생 · 비흡연 + + + + + + 서울특별시 성북구 정릉동 + + + + + + 메이트와 다른 방 + + + + + + 아침형 + + diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index 56b5a59b0d..4bc30197d6 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -3,8 +3,8 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import { CleanTest } from './options/CleanTest'; -import { MajorSelector } from './options/MajorSelector'; +import { CleanTest } from './card/CleanTest'; +import { MajorSelector } from './card/MajorSelector'; const styles = { optionContainer: styled.div` diff --git a/src/components/options/CleanTest.tsx b/src/components/card/CleanTest.tsx similarity index 100% rename from src/components/options/CleanTest.tsx rename to src/components/card/CleanTest.tsx diff --git a/src/components/options/MajorSelector.tsx b/src/components/card/MajorSelector.tsx similarity index 100% rename from src/components/options/MajorSelector.tsx rename to src/components/card/MajorSelector.tsx diff --git a/yarn.lock b/yarn.lock index 5c6a2a3b4e..4ca086b1c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,1001 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== + dependencies: + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" + integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== + +"@babel/core@^7.21.3": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" + integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.24.4" + "@babel/parser" "^7.24.4" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.1", "@babel/generator@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498" + integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw== + dependencies: + "@babel/types" "^7.24.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3" + integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-member-expression-to-functions" "^7.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.24.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" + integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.24.1": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" + integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== + dependencies: + "@babel/types" "^7.24.0" + +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a" + integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w== + +"@babel/helper-remap-async-to-generator@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" + +"@babel/helper-replace-supers@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" + integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.23.4": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" + +"@babel/helpers@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" + integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== + dependencies: + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" + integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" + integrity sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz#da8261f2697f0f41b0855b91d3a20a1fbfd271d3" + integrity sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz#1181d9685984c91d657b8ddf14f0487a6bab2988" + integrity sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz#db3aad724153a00eaac115a3fb898de544e34971" + integrity sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-import-attributes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz#c66b966c63b714c4eec508fcf5763b1f2d381093" + integrity sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.23.3", "@babel/plugin-syntax-jsx@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" + integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" + integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz#2bf263617060c9cc45bcdbf492b8cc805082bf27" + integrity sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-async-generator-functions@^7.24.3": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz#8fa7ae481b100768cc9842c8617808c5352b8b89" + integrity sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz#0e220703b89f2216800ce7b1c53cb0cf521c37f4" + integrity sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw== + dependencies: + "@babel/helper-module-imports" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" + +"@babel/plugin-transform-block-scoped-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz#1c94799e20fcd5c4d4589523bbc57b7692979380" + integrity sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-block-scoping@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012" + integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-class-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz#bcbf1aef6ba6085cfddec9fc8d58871cf011fc29" + integrity sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-class-static-block@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4" + integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz#5bc8fc160ed96378184bc10042af47f50884dcb1" + integrity sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz#bc7e787f8e021eccfb677af5f13c29a9934ed8a7" + integrity sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/template" "^7.24.0" + +"@babel/plugin-transform-destructuring@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz#b1e8243af4a0206841973786292b8c8dd8447345" + integrity sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-dotall-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz#d56913d2f12795cc9930801b84c6f8c47513ac13" + integrity sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-duplicate-keys@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz#5347a797fe82b8d09749d10e9f5b83665adbca88" + integrity sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-dynamic-import@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz#2a5a49959201970dd09a5fca856cb651e44439dd" + integrity sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz#6650ebeb5bd5c012d5f5f90a26613a08162e8ba4" + integrity sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-export-namespace-from@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz#f033541fc036e3efb2dcb58eedafd4f6b8078acd" + integrity sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz#67448446b67ab6c091360ce3717e7d3a59e202fd" + integrity sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz#8cba6f7730626cc4dfe4ca2fa516215a0592b361" + integrity sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA== + dependencies: + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-json-strings@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz#08e6369b62ab3e8a7b61089151b161180c8299f7" + integrity sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz#0a1982297af83e6b3c94972686067df588c5c096" + integrity sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz#719d8aded1aa94b8fb34e3a785ae8518e24cfa40" + integrity sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz#896d23601c92f437af8b01371ad34beb75df4489" + integrity sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-modules-amd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz#b6d829ed15258536977e9c7cc6437814871ffa39" + integrity sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-modules-commonjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz#e71ba1d0d69e049a22bf90b3867e263823d3f1b9" + integrity sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" + integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/plugin-transform-modules-umd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz#69220c66653a19cf2c0872b9c762b9a48b8bebef" + integrity sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz#29c59988fa3d0157de1c871a28cd83096363cc34" + integrity sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz#0cd494bb97cb07d428bd651632cb9d4140513988" + integrity sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz#5bc019ce5b3435c1cadf37215e55e433d674d4e8" + integrity sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" + integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== + dependencies: + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.24.1" + +"@babel/plugin-transform-object-super@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz#e71d6ab13483cca89ed95a474f542bbfc20a0520" + integrity sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" + +"@babel/plugin-transform-optional-catch-binding@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz#92a3d0efe847ba722f1a4508669b23134669e2da" + integrity sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz#26e588acbedce1ab3519ac40cc748e380c5291e6" + integrity sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" + integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-private-methods@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz#a0faa1ae87eff077e1e47a5ec81c3aef383dc15a" + integrity sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-private-property-in-object@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz#756443d400274f8fb7896742962cc1b9f25c1f6a" + integrity sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz#d6a9aeab96f03749f4eebeb0b6ea8e90ec958825" + integrity sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-react-constant-elements@^7.21.3": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.1.tgz#d493a0918b9fdad7540f5afd9b5eb5c52500d18d" + integrity sha512-QXp1U9x0R7tkiGB0FOk8o74jhnap0FlZ5gNkRIWdG3eP+SvMFg118e1zaWewDzgABb106QSKpVsD3Wgd8t6ifA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-react-display-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz#554e3e1a25d181f040cf698b93fd289a03bfdcdb" + integrity sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-react-jsx-development@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + +"@babel/plugin-transform-react-jsx@^7.22.5", "@babel/plugin-transform-react-jsx@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" + integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/types" "^7.23.4" + +"@babel/plugin-transform-react-pure-annotations@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz#c86bce22a53956331210d268e49a0ff06e392470" + integrity sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-regenerator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz#625b7545bae52363bdc1fbbdc7252b5046409c8c" + integrity sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz#8de729f5ecbaaf5cf83b67de13bad38a21be57c1" + integrity sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-shorthand-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz#ba9a09144cf55d35ec6b93a32253becad8ee5b55" + integrity sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz#a1acf9152cbf690e4da0ba10790b3ac7d2b2b391" + integrity sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz#f03e672912c6e203ed8d6e0271d9c2113dc031b9" + integrity sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-template-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz#15e2166873a30d8617e3e2ccadb86643d327aab7" + integrity sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-typeof-symbol@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz#6831f78647080dec044f7e9f68003d99424f94c7" + integrity sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-typescript@^7.24.1": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz#03e0492537a4b953e491f53f2bc88245574ebd15" + integrity sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-typescript" "^7.24.1" + +"@babel/plugin-transform-unicode-escapes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz#fb3fa16676549ac7c7449db9b342614985c2a3a4" + integrity sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-property-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz#56704fd4d99da81e5e9f0c0c93cabd91dbc4889e" + integrity sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz#57c3c191d68f998ac46b708380c1ce4d13536385" + integrity sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-sets-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz#c1ea175b02afcffc9cf57a9c4658326625165b7f" + integrity sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/preset-env@^7.20.2": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b" + integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A== + dependencies: + "@babel/compat-data" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.24.1" + "@babel/plugin-syntax-import-attributes" "^7.24.1" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.24.1" + "@babel/plugin-transform-async-generator-functions" "^7.24.3" + "@babel/plugin-transform-async-to-generator" "^7.24.1" + "@babel/plugin-transform-block-scoped-functions" "^7.24.1" + "@babel/plugin-transform-block-scoping" "^7.24.4" + "@babel/plugin-transform-class-properties" "^7.24.1" + "@babel/plugin-transform-class-static-block" "^7.24.4" + "@babel/plugin-transform-classes" "^7.24.1" + "@babel/plugin-transform-computed-properties" "^7.24.1" + "@babel/plugin-transform-destructuring" "^7.24.1" + "@babel/plugin-transform-dotall-regex" "^7.24.1" + "@babel/plugin-transform-duplicate-keys" "^7.24.1" + "@babel/plugin-transform-dynamic-import" "^7.24.1" + "@babel/plugin-transform-exponentiation-operator" "^7.24.1" + "@babel/plugin-transform-export-namespace-from" "^7.24.1" + "@babel/plugin-transform-for-of" "^7.24.1" + "@babel/plugin-transform-function-name" "^7.24.1" + "@babel/plugin-transform-json-strings" "^7.24.1" + "@babel/plugin-transform-literals" "^7.24.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.1" + "@babel/plugin-transform-member-expression-literals" "^7.24.1" + "@babel/plugin-transform-modules-amd" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-modules-systemjs" "^7.24.1" + "@babel/plugin-transform-modules-umd" "^7.24.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.24.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.1" + "@babel/plugin-transform-numeric-separator" "^7.24.1" + "@babel/plugin-transform-object-rest-spread" "^7.24.1" + "@babel/plugin-transform-object-super" "^7.24.1" + "@babel/plugin-transform-optional-catch-binding" "^7.24.1" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-private-methods" "^7.24.1" + "@babel/plugin-transform-private-property-in-object" "^7.24.1" + "@babel/plugin-transform-property-literals" "^7.24.1" + "@babel/plugin-transform-regenerator" "^7.24.1" + "@babel/plugin-transform-reserved-words" "^7.24.1" + "@babel/plugin-transform-shorthand-properties" "^7.24.1" + "@babel/plugin-transform-spread" "^7.24.1" + "@babel/plugin-transform-sticky-regex" "^7.24.1" + "@babel/plugin-transform-template-literals" "^7.24.1" + "@babel/plugin-transform-typeof-symbol" "^7.24.1" + "@babel/plugin-transform-unicode-escapes" "^7.24.1" + "@babel/plugin-transform-unicode-property-regex" "^7.24.1" + "@babel/plugin-transform-unicode-regex" "^7.24.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.18.6": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.1.tgz#2450c2ac5cc498ef6101a6ca5474de251e33aa95" + integrity sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-transform-react-display-name" "^7.24.1" + "@babel/plugin-transform-react-jsx" "^7.23.4" + "@babel/plugin-transform-react-jsx-development" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations" "^7.24.1" + +"@babel/preset-typescript@^7.21.0": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz#89bdf13a3149a17b3b2a2c9c62547f06db8845ec" + integrity sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-syntax-jsx" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-typescript" "^7.24.1" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + "@babel/runtime@^7.23.2": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" @@ -14,6 +1009,47 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.8.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" + integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" + +"@babel/traverse@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" + integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== + dependencies: + "@babel/code-frame" "^7.24.1" + "@babel/generator" "^7.24.1" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.24.1" + "@babel/types" "^7.24.0" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.21.3", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.24.0", "@babel/types@^7.4.4": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@emotion/is-prop-valid@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" @@ -94,6 +1130,38 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@next/env@14.1.0": version "14.1.0" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" @@ -182,6 +1250,112 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/core@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + +"@svgr/plugin-jsx@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + +"@svgr/plugin-svgo@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== + dependencies: + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" + +"@svgr/webpack@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== + dependencies: + "@babel/core" "^7.21.3" + "@babel/plugin-transform-react-constant-elements" "^7.21.3" + "@babel/preset-env" "^7.20.2" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.21.0" + "@svgr/core" "8.1.0" + "@svgr/plugin-jsx" "8.1.0" + "@svgr/plugin-svgo" "8.1.0" + "@swc/helpers@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" @@ -220,6 +1394,11 @@ dependencies: "@tanstack/query-core" "5.25.0" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@types/geojson@*": version "7946.0.14" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" @@ -457,6 +1636,13 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -625,6 +1811,30 @@ axobject-query@^3.2.1: dependencies: dequal "^2.0.3" +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" + integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.1" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" + integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -635,6 +1845,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -657,6 +1872,16 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +browserslist@^4.22.2, browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -692,6 +1917,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelize@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" @@ -702,6 +1932,20 @@ caniuse-lite@^1.0.30001579: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz#16745e50263edc9f395895a7cd468b9f3767cf33" integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== +caniuse-lite@^1.0.30001587: + version "1.0.30001608" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz#7ae6e92ffb300e4b4ec2f795e0abab456ec06cc0" + integrity sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -735,6 +1979,13 @@ clsx@^2.1.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -742,6 +1993,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" @@ -754,6 +2010,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -764,6 +2025,28 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.31.0, core-js-compat@^3.36.1: + version "3.36.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.1.tgz#1818695d72c99c25d621dca94e6883e190cea3c8" + integrity sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA== + dependencies: + browserslist "^4.23.0" + +cosmiconfig@^8.1.3: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -778,6 +2061,17 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" @@ -787,6 +2081,34 @@ css-to-react-native@3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + csstype@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" @@ -809,7 +2131,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -821,6 +2143,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + define-data-property@^1.0.1, define-data-property@^1.1.2, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -870,11 +2197,54 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +electron-to-chromium@^1.4.668: + version "1.4.731" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.731.tgz#d3dc19f359045b750a1fb0bc42315a502d950187" + integrity sha512-+TqVfZjpRz2V/5SPpmJxq9qK620SC5SqCnxQIOi7i/U08ZDcTpKbT7Xjj9FU5CbXTMUb4fywbIr8C7cGv4hcjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -893,6 +2263,18 @@ enhanced-resolve@^5.12.0: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22.4: version "1.22.5" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.5.tgz#1417df4e97cc55f09bf7e58d1e614bc61cb8df46" @@ -1003,6 +2385,16 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -1422,6 +2814,11 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -1486,6 +2883,11 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + globals@^13.19.0, globals@^13.24.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -1539,6 +2941,11 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -1585,7 +2992,7 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -1628,6 +3035,11 @@ is-array-buffer@^3.0.4: call-bind "^1.0.2" get-intrinsic "^1.2.1" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-async-function@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" @@ -1832,7 +3244,7 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -"js-tokens@^3.0.0 || ^4.0.0": +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1844,11 +3256,26 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1866,6 +3293,11 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -1903,6 +3335,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -1910,6 +3347,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1922,6 +3364,20 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1934,6 +3390,16 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -2026,11 +3492,31 @@ next@14.1.0: "@next/swc-win32-ia32-msvc" "14.1.0" "@next/swc-win32-x64-msvc" "14.1.0" +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2142,6 +3628,16 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2300,11 +3796,30 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -2315,6 +3830,25 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.2: es-errors "^1.3.0" set-function-name "^2.0.1" +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2325,7 +3859,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.14.2, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -2468,11 +4002,24 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -2591,6 +4138,13 @@ stylis@4.3.1: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.1.tgz#ed8a9ebf9f76fe1e12d462f5cc3c4c980b23a7eb" integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ== +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2603,6 +4157,24 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.2.0.tgz#7a5dff2938d8c6096e00295c2390e8e652fa805d" + integrity sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.3.1" + css-what "^6.1.0" + csso "^5.0.5" + picocolors "^1.0.0" + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -2613,6 +4185,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2640,7 +4217,7 @@ tslib@2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tslib@^2.4.0: +tslib@^2.0.3, tslib@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -2721,6 +4298,37 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2813,6 +4421,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 8310bb13078d8a97bf945dc5d35f804296cb67ab Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 10 Apr 2024 18:26:08 +0900 Subject: [PATCH 003/130] =?UTF-8?q?fix:=20setting-page=20=EC=9E=90?= =?UTF-8?q?=EA=B8=B0=EC=86=8C=EA=B0=9C=EB=9E=80=20=EC=B6=94=EA=B0=80=20(#5?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/setting-page.tsx | 90 ++++++++++++++++++------ src/components/card/SelfIntroduction.tsx | 31 ++++++++ src/components/card/index.ts | 1 + 3 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 src/components/card/SelfIntroduction.tsx create mode 100644 src/components/card/index.ts diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index d220131a3a..ba1c7fe8e9 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -2,7 +2,13 @@ import styled from 'styled-components'; +import Location from '../../../public/option-img/location_on.svg'; +import Meeting from '../../../public/option-img/meeting_room.svg'; +import Person from '../../../public/option-img/person.svg'; +import Visibility from '../../../public/option-img/visibility.svg'; + import { VitalSection, OptionSection } from '@/components'; +import { SelfIntroduction } from '@/components/card'; const styles = { pageContainer: styled.div` @@ -46,28 +52,56 @@ const styles = { line-height: normal; margin-bottom: 1.69rem; `, - miniCardKeywordsContainer: styled.div` - width: 17.5625rem; - height: 5.6875rem; - position: relative; + miniCardKeywordsContainer: styled.ul` + display: flex; + width: 18.375rem; + flex-direction: column; + align-items: flex-start; + gap: 1rem; `, - miniCardKeyword: styled.div` - display: inline-flex; - padding: 0.5rem 1.5rem; - justify-content: center; + miniCardList: styled.li` + display: flex; align-items: center; - gap: 0.5rem; - border-radius: 26px; - border: 2px solid var(--Main-1, #e15637); - background: #fff; - - color: var(--Main-1, #e15637); + gap: 2rem; + align-self: stretch; + `, + miniCardPerson: styled(Person)` + width: 1.5rem; + height: 1.5rem; + path { + fill: var(--Main-1, #e15637); + } + `, + miniCardLocation: styled(Location)` + width: 1.5rem; + height: 1.5rem; + path { + fill: var(--Main-1, #e15637); + } + `, + miniCardMeeting: styled(Meeting)` + width: 1.5rem; + height: 1.5rem; + path { + fill: var(--Main-1, #e15637); + } + `, + miniCardVisibility: styled(Visibility)` + width: 1.5rem; + height: 1.5rem; + path { + fill: var(--Main-1, #e15637); + } + `, + miniCardText: styled.p` + flex: 1 0 0; + height: 1.5rem; font-family: 'Noto Sans KR'; font-size: 1rem; font-style: normal; font-weight: 500; line-height: normal; - position: absolute; + color: var(--Main-1, #e15637); `, checkContainer: styled.div` width: 51.0625rem; @@ -107,13 +141,24 @@ export function SettingPage({ type, name }: SettingPageProps) { {type}카드 - 여성 - - 비흡연 - - - 아침형 - + + + 여성 · 00년생 · 비흡연 + + + + + 서울특별시 성북구 정릉동 + + + + + 메이트와 다른 방 + + + + 아침형 + @@ -122,6 +167,7 @@ export function SettingPage({ type, name }: SettingPageProps) { + diff --git a/src/components/card/SelfIntroduction.tsx b/src/components/card/SelfIntroduction.tsx new file mode 100644 index 0000000000..d526f9f9cd --- /dev/null +++ b/src/components/card/SelfIntroduction.tsx @@ -0,0 +1,31 @@ +'use client'; + +import styled from 'styled-components'; + +const styles = { + introduction: styled.input` + display: flex; + margin-top: 4.13rem; + width: 41.9375rem; + height: 5.4375rem; + padding: 0.5rem 1rem 3.5rem 1rem; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 8px; + border: none; + background: #fff; + + color: #9a95a3; + + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, +}; + +export function SelfIntroduction() { + return ; +} diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 0000000000..e5198e9461 --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1 @@ +export * from './SelfIntroduction'; From dddeff7a1c5c27eee0fedd6bdcdfefee11fd15b0 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Thu, 11 Apr 2024 23:48:19 +0900 Subject: [PATCH 004/130] fix: miniCard data (#52) --- src/app/pages/profile-page.tsx | 4 +- src/app/pages/setting-page.tsx | 22 +++++- src/app/pages/user-input-page.tsx | 28 +++++-- src/components/VitalSection.tsx | 67 ++++++++++++----- src/components/card/Slider.tsx | 117 ++++++++++++++++++++++++++++++ src/components/card/index.ts | 1 + 6 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 src/components/card/Slider.tsx diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index a526dac7bc..5cddd7c2a0 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -499,7 +499,7 @@ function Card({ 내 카드 {name} @@ -510,7 +510,7 @@ function Card({ 메이트 카드 메이트 diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 683f1a0837..4e291b4dae 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -153,6 +153,7 @@ export function SettingPage({ cardId }: { cardId: number }) { const memberId = memberIdParams ?? ''; const isMySelfStr = params.get('isMySelf'); const isMySelf = isMySelfStr === 'true'; + const type = params.get('type') ?? ''; const user = useProfileData(memberId); const [userData, setUserData] = useState(null); @@ -295,21 +296,32 @@ export function SettingPage({ cardId }: { cardId: number }) { - 여성 · 00년생 · 비흡연 + + {userData?.gender === 'MALE' ? '남성' : '여성'} ·{' '} + {userData?.birthYear?.slice(2)}년생 · {selectedState.smoking} + - 서울특별시 성북구 정릉동 + {card.data?.data.location} - 메이트와 다른 방 + + 메이트와 {selectedState.room} + - 아침형 + + {selectedOptions['아침형'] ? '아침형' : null} + {selectedOptions['아침형'] && selectedOptions['올빼미형'] + ? ' · ' + : null} + {selectedOptions['올빼미형'] ? '올빼미형' : null} + @@ -317,10 +329,12 @@ export function SettingPage({ cardId }: { cardId: number }) { diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 40489b385b..24e8ebdcf2 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -331,7 +331,6 @@ export function UserInputPage() { const handleMateCardClick = () => { setActiveContainer('mate'); }; - return ( @@ -350,7 +349,8 @@ export function UserInputPage() { - 여성 · 00년생 · 비흡연 + {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} + {user?.birthYear?.slice(2)}년생 · {selectedState.smoking} @@ -362,13 +362,17 @@ export function UserInputPage() { - 메이트와 다른 방 + 메이트와 {selectedState.room} - 아침형 + {selectedOptions['아침형'] ? '아침형' : null} + {selectedOptions['아침형'] && selectedOptions['올빼미형'] + ? ' · ' + : null} + {selectedOptions['올빼미형'] ? '올빼미형' : null} @@ -384,7 +388,8 @@ export function UserInputPage() { - 여성 · 00년생 · 비흡연 + {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} + {user?.birthYear?.slice(2)}년생 · {selectedMateState.smoking} @@ -396,7 +401,7 @@ export function UserInputPage() { - 메이트와 다른 방 + 메이트와 {selectedMateState.room} @@ -404,7 +409,12 @@ export function UserInputPage() { $active={activeContainer === 'mate'} /> - 아침형 + {selectedMateOptions['아침형'] ? '아침형' : null} + {selectedMateOptions['아침형'] && + selectedMateOptions['올빼미형'] + ? ' · ' + : null} + {selectedMateOptions['올빼미형'] ? '올빼미형' : null} @@ -415,10 +425,12 @@ export function UserInputPage() { @@ -433,10 +445,12 @@ export function UserInputPage() { diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 0ae0d8f52c..24a17217da 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -3,6 +3,8 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; +import { Slider } from './card/Slider'; + const styles = { vitalContainer: styled.div` display: flex; @@ -143,13 +145,16 @@ interface SelectedState { export function VitalSection({ gender, birthYear, + location, smoking, room, onFeatureChange, isMySelf, + type, }: { gender: string | undefined; birthYear: string | undefined; + location: string | undefined; smoking: string | undefined; room: string | undefined; onFeatureChange: ( @@ -157,6 +162,7 @@ export function VitalSection({ item: string | number, ) => void; isMySelf: boolean; + type: string; }) { const [selectedYear, setSelectedYear] = useState(null); const [selectedState, setSelectedState] = useState({ @@ -186,6 +192,8 @@ export function VitalSection({ const yearValue = parseInt(event.target.value, 10); setSelectedYear(Number.isNaN(yearValue) ? null : yearValue); }; + + const handleAgeChange = (value: number) => {}; return ( 필수 @@ -221,12 +229,25 @@ export function VitalSection({ 희망 지역 - - - + {type === 'myCard' ? ( + + + + ) : ( + + {location} + + )} @@ -306,18 +327,28 @@ export function VitalSection({ 출생 연도 - - - {years.map(year => ( - - ))} - + {type === 'myCard' ? ( + + + {years.map(year => ( + + ))} + + ) : ( + + )} diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx new file mode 100644 index 0000000000..2036851414 --- /dev/null +++ b/src/components/card/Slider.tsx @@ -0,0 +1,117 @@ +'use client'; + +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const styles = { + container: styled.div` + display: flex; + width: 30rem; + align-items: center; + `, + sliderContainer: styled.div` + width: 25rem; + height: 1.875rem; + position: relative; + `, + sliderTrack: styled.div` + width: 100%; + height: 0.3125rem; + border-radius: 20px; + background: #d9d9d9; + position: absolute; + top: calc(50% - 2px); + `, + sliderFillTrack: styled.div` + width: ${props => props.$fill}; + height: 0.3125rem; + border-radius: 2px; + background: var(--Main-1, #e15637); + position: absolute; + top: calc(50% - 2px); + `, + slider: styled.input` + width: 25rem; + height: 0.3125rem; + border-radius: 1.25rem; + -webkit-appearance: none; + background: transparent; + + &:active { + cursor: grabbing; + } + + &:focus { + outline: none; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + pointer-events: all; + width: 1.875rem; + height: 1.875rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px #c6c6c6; + cursor: pointer; + position: relative; + z-index: 1; + } + `, + value: styled.span` + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + margin-left: 1rem; + `, +}; + +interface SliderProps { + min: number; + max: number; + step: number; + initialValue: number; + onChange: (value: number) => void; +} + +interface FillProps { + $fill: string; +} + +export function Slider({ + min, + max, + step, + initialValue, + onChange, +}: SliderProps) { + const [value, setValue] = useState(0); + + const handleChange = (event: React.ChangeEvent) => { + const newValue = parseFloat(event.target.value); + setValue(newValue); + onChange(newValue); + }; + + return ( + + + + + + + {value > 9 ? '무제한' : `±${value}세`} + + ); +} diff --git a/src/components/card/index.ts b/src/components/card/index.ts index e5198e9461..6fbb77b6ef 100644 --- a/src/components/card/index.ts +++ b/src/components/card/index.ts @@ -1 +1,2 @@ export * from './SelfIntroduction'; +export * from './Slider'; From 9d0ca61d194af2311fe29f13e34908b8e9a6e624 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sun, 14 Apr 2024 11:54:51 +0900 Subject: [PATCH 005/130] fix: mate age, budget slider (#52) --- src/app/pages/setting-page.tsx | 3 +- src/app/pages/user-input-page.tsx | 4 + src/components/OptionSection.tsx | 255 +++++++++----------------- src/components/VitalSection.tsx | 31 +++- src/components/card/MajorSelector.tsx | 14 +- src/components/card/MbtiToggle.tsx | 167 +++++++++++++++++ src/components/card/Slider.tsx | 80 ++++---- 7 files changed, 342 insertions(+), 212 deletions(-) create mode 100644 src/components/card/MbtiToggle.tsx diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 4e291b4dae..ac44e50142 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -343,8 +343,9 @@ export function SettingPage({ cardId }: { cardId: number }) { optionFeatures={features} onFeatureChange={handleOptionClick} isMySelf={isMySelf} + type={type} /> - + {type === 'myCard' ? : null} diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 24e8ebdcf2..78495a323d 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -10,6 +10,7 @@ import Person from '../../../public/option-img/person.svg'; import Visibility from '../../../public/option-img/visibility.svg'; import { VitalSection, OptionSection } from '@/components'; +import { SelfIntroduction } from '@/components/card'; import { useAuthValue, useUserData } from '@/features/auth'; import { usePutUserCard } from '@/features/profile'; @@ -439,7 +440,9 @@ export function UserInputPage() { optionFeatures={null} onFeatureChange={handleOptionClick} isMySelf + type="myCard" /> + diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index e716bd7fe7..3db02db9e1 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -5,6 +5,8 @@ import styled from 'styled-components'; import { CleanTest } from './card/CleanTest'; import { MajorSelector } from './card/MajorSelector'; +import { MbtiToggle } from './card/MbtiToggle'; +import { Slider } from './card/Slider'; const styles = { optionContainer: styled.div` @@ -48,6 +50,7 @@ const styles = { personalContainer: styled.div` width: 22rem; display: flex; + position: relative; align-items: flex-start; align-content: flex-start; gap: 0.5rem; @@ -76,7 +79,7 @@ const styles = { `, budgetContainer: styled.div` display: flex; - align-items: flex-end; + align-items: center; gap: 1.75rem; width: 100%; `, @@ -88,100 +91,17 @@ const styles = { background: #d9d9d9; margin-bottom: 0.63rem; `, - mbtiSection: styled.div` - display: inline-flex; - align-items: flex-end; - gap: 2rem; - margin: 0.5rem 0; - `, - mbtiToggleContainer: styled.div` - display: inline-flex; - align-items: flex-end; - gap: 1rem; - + value: styled.span` color: #000; font-family: 'Noto Sans KR'; - font-size: 1.125rem; + font-size: 1rem; font-style: normal; font-weight: 500; line-height: normal; `, - - switchContainer: styled.div` - display: inline-flex; - justify-content: center; - align-items: flex-end; - gap: 0.375rem; - `, - switchWrapper: styled.label` - position: relative; - display: inline-block; - width: 2.5rem; - height: 1.5rem; - `, - switchInput: styled.input` - opacity: 0; - width: 0; - height: 0; - `, - slider: styled.span` - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #bebebe; - -webkit-transition: 0.4s; - transition: 0.4s; - border-radius: 24px; - `, - sliderDot: styled.span` - position: absolute; - cursor: pointer; - top: 0.25rem; - left: 0.25rem; - bottom: 0.25rem; - background-color: white; - -webkit-transition: 0.4s; - transition: 0.4s; - border-radius: 50%; - width: 1rem; - height: 1rem; - `, }; -interface ToggleSwitchProps { - isChecked: boolean; - onToggle: () => void; -} - -function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { - return ( - - - - - - - - - ); -} - interface CheckItemProps { $isSelected: boolean; } @@ -236,15 +156,18 @@ const PersonalOptions = [ '엠비티아이', '전공', ]; +const CleaningOptions = ['상', '평범보통', '천하태평']; export function OptionSection({ optionFeatures, onFeatureChange, isMySelf, + type, }: { optionFeatures: string[] | null; onFeatureChange: (option: string) => void; isMySelf: boolean; + type: string; }) { type SelectedOptions = Record; const [selectedOptions, setSelectedOptions] = useState({}); @@ -259,13 +182,6 @@ export function OptionSection({ } }, [optionFeatures]); - const [toggleStates, setToggleStates] = useState({ - toggle1: false, - toggle2: false, - toggle3: false, - toggle4: false, - }); - const handleOptionClick = (option: string) => { setSelectedOptions(prevSelectedOptions => ({ ...prevSelectedOptions, @@ -274,24 +190,43 @@ export function OptionSection({ onFeatureChange(option); }; - const toggleSwitch = (toggleName: keyof typeof toggleStates) => { - setToggleStates(prevState => ({ - ...prevState, - [toggleName]: !prevState[toggleName], - })); - }; - const [isTestVisible, setIsTestVisible] = useState(false); const [score, setScore] = useState(-1); const toggleTestVisibility = () => { setIsTestVisible(prev => !prev); + setIsMajorSelected(false); + setIsTestSelected(true); + setIsMbtiSelected(false); }; const handleTestCompletion = (cleanScore: number) => { setScore(cleanScore); }; + const [budgetMin, setBudgetMin] = useState(0); + const [budgetMax, setBudgetMax] = useState(355); + const handleBudgetChange = (min: number, max: number) => { + setBudgetMin(min); + setBudgetMax(max); + }; + + const [isTestSelected, setIsTestSelected] = useState(false); + const [isMajorSelected, setIsMajorSelected] = useState(false); + const [isMbtiSelected, setIsMbtiSelected] = useState(false); + + const handleMajorSelect = () => { + setIsMajorSelected(true); + setIsTestSelected(false); + setIsMbtiSelected(false); + }; + + const handleMbtiSelect = () => { + setIsMajorSelected(false); + setIsTestSelected(false); + setIsMbtiSelected(true); + }; + return ( 선택 @@ -370,20 +305,44 @@ export function OptionSection({ - - - - 테스트 하기 - - - = 0 && score < 5.34}>상 - 5.34 && score < 10.67}> - 평범보통 - - 10.67}>천하태평 - + {type === 'myCard' ? ( + + + { + toggleTestVisibility(); + }} + > + 테스트 하기 + + + = 0 && score < 5.34}>상 + 5.34 && score < 10.67}> + 평범보통 + + 10.67}>천하태평 + + ) : ( + + {CleaningOptions.map(option => ( + { + if (isMySelf) { + handleOptionClick(option); + } + }} + > + {option} + + ))} + + )} - {isTestVisible && } + {isTestVisible && isTestSelected && ( + + )} @@ -395,63 +354,18 @@ export function OptionSection({ if (isMySelf) { handleOptionClick(option); } + if (option === '엠비티아이') handleMbtiSelect(); + if (option === '전공') handleMajorSelect(); }} > {option === '엠비티아이' ? <>MBTI : option} ))} - {selectedOptions['전공'] ? : null} - {selectedOptions['엠비티아이'] ? ( - - - E - { - if (isMySelf) { - toggleSwitch('toggle1'); - } - }} - /> - I - - - N - { - if (isMySelf) { - toggleSwitch('toggle2'); - } - }} - /> - S - - - F - { - if (isMySelf) { - toggleSwitch('toggle3'); - } - }} - /> - T - - - P - { - if (isMySelf) { - toggleSwitch('toggle4'); - } - }} - /> - J - - + {selectedOptions['전공'] && isMajorSelected ? ( + + ) : null} + {selectedOptions['엠비티아이'] && isMbtiSelected ? ( + ) : null} @@ -460,7 +374,16 @@ export function OptionSection({ 금액 - + + + {`${budgetMin === 0 ? '0원' : `${budgetMin}만원`}`} ~{' '} + {`${budgetMax === 355 ? '무제한' : `${budgetMax}만원`}`} + diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 24a17217da..30c56ec667 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -97,6 +97,15 @@ const styles = { outline: none; } `, + value: styled.span` + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, }; interface CheckItemProps { @@ -193,7 +202,13 @@ export function VitalSection({ setSelectedYear(Number.isNaN(yearValue) ? null : yearValue); }; - const handleAgeChange = (value: number) => {}; + const [mateMinAge, setMateMinAge] = useState(0); + const [mateMaxAge, setMateMaxAge] = useState(11); + const handleAgeChange = (min: number, max: number) => { + setMateMinAge(min); + setMateMaxAge(max); + }; + return ( 필수 @@ -341,13 +356,13 @@ export function VitalSection({ ))} ) : ( - + <> + + + {`${mateMinAge === 0 ? '동갑' : `±${mateMinAge}세`}`} ~{' '} + {`${mateMaxAge === 11 ? '무제한' : `±${mateMaxAge}세`}`} + + )} diff --git a/src/components/card/MajorSelector.tsx b/src/components/card/MajorSelector.tsx index 2148ea7d69..c648d5fefc 100644 --- a/src/components/card/MajorSelector.tsx +++ b/src/components/card/MajorSelector.tsx @@ -7,13 +7,16 @@ const styles = { majorSelector: styled.select` -webkit-appearance: none; -moz-appearance: none; + appearance: none; display: flex; width: 5.875rem; padding: 0.575rem 0; align-items: center; - border: 2px solid var(--Gray-4, #dfdfdf); background: var(--White, #fff); + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + border: none; + border-radius: 16px; color: var(--Gray-3, #888); font-family: 'Noto Sans KR'; @@ -22,6 +25,15 @@ const styles = { font-weight: 400; line-height: normal; text-align: center; + + position: absolute; + bottom: 0; + left: 12rem; + z-index: 5; + + &:focus { + outline: none; + } `, }; diff --git a/src/components/card/MbtiToggle.tsx b/src/components/card/MbtiToggle.tsx new file mode 100644 index 0000000000..e1a1755df9 --- /dev/null +++ b/src/components/card/MbtiToggle.tsx @@ -0,0 +1,167 @@ +'use client'; + +import React, { useState } from 'react'; +import styled from 'styled-components'; + +const styles = { + mbtiSection: styled.div` + position: absolute; + bottom: -18rem; + padding: 2rem; + border-radius: 16px; + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 2rem; + margin: 0.5rem 0; + background: #fff; + z-index: 5; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + `, + mbtiToggleContainer: styled.div` + display: inline-flex; + align-items: flex-end; + gap: 1rem; + + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 1.125rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + + switchContainer: styled.div` + display: inline-flex; + justify-content: center; + align-items: flex-end; + gap: 0.375rem; + `, + switchWrapper: styled.label` + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.5rem; + `, + switchInput: styled.input` + opacity: 0; + width: 0; + height: 0; + `, + slider: styled.span` + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #bebebe; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 24px; + `, + sliderDot: styled.span` + position: absolute; + cursor: pointer; + top: 0.25rem; + left: 0.25rem; + bottom: 0.25rem; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; + width: 1rem; + height: 1rem; + `, +}; + +interface ToggleSwitchProps { + isChecked: boolean; + onToggle: () => void; +} + +function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { + return ( + + + + + + + + + ); +} + +export function MbtiToggle() { + const [toggleStates, setToggleStates] = useState({ + toggle1: false, + toggle2: false, + toggle3: false, + toggle4: false, + }); + + const toggleSwitch = (toggleName: keyof typeof toggleStates) => { + setToggleStates(prevState => ({ + ...prevState, + [toggleName]: !prevState[toggleName], + })); + }; + return ( + + + E + { + toggleSwitch('toggle1'); + }} + /> + I + + + N + { + toggleSwitch('toggle2'); + }} + /> + S + + + F + { + toggleSwitch('toggle3'); + }} + /> + T + + + P + { + toggleSwitch('toggle4'); + }} + /> + J + + + ); +} diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index 2036851414..25264a5747 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -1,12 +1,11 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; const styles = { container: styled.div` - display: flex; - width: 30rem; + display: inline-flex; align-items: center; `, sliderContainer: styled.div` @@ -29,17 +28,19 @@ const styles = { background: var(--Main-1, #e15637); position: absolute; top: calc(50% - 2px); + left: ${props => props.$left}; + right: ${props => props.$right}; `, slider: styled.input` - width: 25rem; + position: absolute; + width: 100%; height: 0.3125rem; border-radius: 1.25rem; -webkit-appearance: none; + -moz-appearance: none; + appearance: none; background: transparent; - - &:active { - cursor: grabbing; - } + top: calc(50% - 2px); &:focus { outline: none; @@ -58,60 +59,67 @@ const styles = { z-index: 1; } `, - value: styled.span` - color: #000; - - font-family: 'Noto Sans KR'; - font-size: 1rem; - font-style: normal; - font-weight: 500; - line-height: normal; - margin-left: 1rem; - `, }; interface SliderProps { min: number; max: number; step: number; - initialValue: number; - onChange: (value: number) => void; + onChange: (min: number, max: number) => void; } interface FillProps { $fill: string; + $left: string; + $right: string; } -export function Slider({ - min, - max, - step, - initialValue, - onChange, -}: SliderProps) { - const [value, setValue] = useState(0); +export function Slider({ min, max, step, onChange }: SliderProps) { + const [minValue, setMinValue] = useState(min); + const [maxValue, setMaxValue] = useState(max); + + const handleMinChange = (e: React.ChangeEvent) => { + const newLow = Number(e.target.value); + if (newLow > maxValue) setMaxValue(newLow); + setMinValue(newLow); + }; - const handleChange = (event: React.ChangeEvent) => { - const newValue = parseFloat(event.target.value); - setValue(newValue); - onChange(newValue); + const handleMaxChange = (e: React.ChangeEvent) => { + const newHigh = Number(e.target.value); + if (newHigh < minValue) setMinValue(newHigh); + setMaxValue(newHigh); }; + useEffect(() => { + onChange(minValue, maxValue); + }, [onChange, minValue, maxValue]); + return ( - + + - {value > 9 ? '무제한' : `±${value}세`} ); } From 9178e559c0dda9be0a0c6b0326a00d37571ccc88 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sun, 14 Apr 2024 17:33:33 +0900 Subject: [PATCH 006/130] fix: livingPattern, cleanOptions multiple -> single (#52) --- public/option-img/location_on.svg | 2 +- public/option-img/meeting_room.svg | 2 +- public/option-img/person.svg | 2 +- public/option-img/visibility.svg | 2 +- src/components/OptionSection.tsx | 91 +++++++++++++++++++++--------- src/components/VitalSection.tsx | 9 +-- 6 files changed, 68 insertions(+), 40 deletions(-) diff --git a/public/option-img/location_on.svg b/public/option-img/location_on.svg index 3c2b4915a1..2f056fe9ef 100644 --- a/public/option-img/location_on.svg +++ b/public/option-img/location_on.svg @@ -1,4 +1,4 @@ - + diff --git a/public/option-img/meeting_room.svg b/public/option-img/meeting_room.svg index 1ff5c1e47a..dfe2d9a3f8 100644 --- a/public/option-img/meeting_room.svg +++ b/public/option-img/meeting_room.svg @@ -1,4 +1,4 @@ - + diff --git a/public/option-img/person.svg b/public/option-img/person.svg index 50cd89a34c..23986bda12 100644 --- a/public/option-img/person.svg +++ b/public/option-img/person.svg @@ -1,4 +1,4 @@ - + diff --git a/public/option-img/visibility.svg b/public/option-img/visibility.svg index ebf2fa343e..053e404413 100644 --- a/public/option-img/visibility.svg +++ b/public/option-img/visibility.svg @@ -1,4 +1,4 @@ - + diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index 3db02db9e1..c66d5e24a1 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -137,7 +137,6 @@ const CheckItem = styled.div` }}; `; -const LivingPatternOptions = ['아침형', '올빼미형']; const EatingOptions = ['실내취식 싫어요', '야식 안먹어요', '음주']; const HearingOptions = [ '잠버릇 있어요', @@ -156,7 +155,6 @@ const PersonalOptions = [ '엠비티아이', '전공', ]; -const CleaningOptions = ['상', '평범보통', '천하태평']; export function OptionSection({ optionFeatures, @@ -234,19 +232,29 @@ export function OptionSection({ - {LivingPatternOptions.map(option => ( - { - if (isMySelf) { - handleOptionClick(option); - } - }} - > - {option} - - ))} + { + if (isMySelf) { + handleOptionClick('아침형'); + if (selectedOptions['올빼미형']) + handleOptionClick('올빼미형'); + } + }} + > + 아침형 + + { + if (isMySelf) { + handleOptionClick('올빼미형'); + if (selectedOptions['아침형']) handleOptionClick('아침형'); + } + }} + > + 올빼미형 + @@ -324,19 +332,46 @@ export function OptionSection({ ) : ( - {CleaningOptions.map(option => ( - { - if (isMySelf) { - handleOptionClick(option); - } - }} - > - {option} - - ))} + { + if (isMySelf) { + handleOptionClick('상'); + if (selectedOptions['평범보통']) + handleOptionClick('평범보통'); + if (selectedOptions['천하태평']) + handleOptionClick('천하태평'); + } + }} + > + 상 + + { + if (isMySelf) { + handleOptionClick('평범보통'); + if (selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['천하태평']) + handleOptionClick('천하태평'); + } + }} + > + 평범보통 + + { + if (isMySelf) { + handleOptionClick('천하태평'); + if (selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['평범보통']) + handleOptionClick('평범보통'); + } + }} + > + 천하태평 + )} diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 30c56ec667..6d3cc79334 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -173,7 +173,6 @@ export function VitalSection({ isMySelf: boolean; type: string; }) { - const [selectedYear, setSelectedYear] = useState(null); const [selectedState, setSelectedState] = useState({ smoking: undefined, room: undefined, @@ -197,11 +196,6 @@ export function VitalSection({ onFeatureChange(optionName, item); } - const handleYearChange = (event: React.ChangeEvent) => { - const yearValue = parseInt(event.target.value, 10); - setSelectedYear(Number.isNaN(yearValue) ? null : yearValue); - }; - const [mateMinAge, setMateMinAge] = useState(0); const [mateMaxAge, setMateMaxAge] = useState(11); const handleAgeChange = (min: number, max: number) => { @@ -344,8 +338,7 @@ export function VitalSection({ {type === 'myCard' ? ( From f8d5da7e5acf687f5be028e7dd2f159ab7995545 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 15 Apr 2024 16:30:37 +0900 Subject: [PATCH 007/130] feat: chatting-component(#52) --- public/Bubble tip R.svg | 6 ++ public/Bubble tip.svg | 6 ++ public/__avatar_url.png | Bin 0 -> 1491 bytes public/bottom-curve-vector R.svg | 3 + public/bottom-curve-vector.svg | 3 + public/kebab-horizontal.svg | 9 ++ public/read.svg | 8 ++ src/components/FloatingChatting.tsx | 55 ++++++++-- src/components/chat/ChattingList.tsx | 102 ++++++++++++++++++ src/components/chat/ChattingRoom.tsx | 132 +++++++++++++++++++++++ src/components/chat/ReceiverMessage.tsx | 137 ++++++++++++++++++++++++ src/components/chat/SenderMessage.tsx | 110 +++++++++++++++++++ 12 files changed, 562 insertions(+), 9 deletions(-) create mode 100644 public/Bubble tip R.svg create mode 100644 public/Bubble tip.svg create mode 100644 public/__avatar_url.png create mode 100644 public/bottom-curve-vector R.svg create mode 100644 public/bottom-curve-vector.svg create mode 100644 public/kebab-horizontal.svg create mode 100644 public/read.svg create mode 100644 src/components/chat/ChattingList.tsx create mode 100644 src/components/chat/ChattingRoom.tsx create mode 100644 src/components/chat/ReceiverMessage.tsx create mode 100644 src/components/chat/SenderMessage.tsx diff --git a/public/Bubble tip R.svg b/public/Bubble tip R.svg new file mode 100644 index 0000000000..ccfdbebbe2 --- /dev/null +++ b/public/Bubble tip R.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/Bubble tip.svg b/public/Bubble tip.svg new file mode 100644 index 0000000000..21dcb388b4 --- /dev/null +++ b/public/Bubble tip.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/__avatar_url.png b/public/__avatar_url.png new file mode 100644 index 0000000000000000000000000000000000000000..c64b037aa7cd190a8da60c491344fb6d0e09f1d6 GIT binary patch literal 1491 zcmV;^1uXiBP)?B>v zNfL)lQXq@F=Rpuhtu{!v69~K%B)JAP(|{yd%Zgft57!NuF}cy#^Curuuw?luImRQC z>LQ0>Kjv=iL%EhjbJ0OSN#H0SN&05eY)77GbY&OI zNo1-J!z7D23rkOCFTaXv-A29Dgl1|eHp-}I_bC)DO0G*u_M>~Qo57FgUxZ=CqdmhQ zC)FjAfLpSDPN8I`6W!40x{NQso`4-V;3*LtePS^Y9nXUz%U~z6BExsPd&Z?#_WX^f zHdUhc4J(M2FQAX-MI2>^j4eGmdu|I9MZti&9!gw>D2b#Rs2ZkaJmHVnGxI0_o=CM?%Pt#KDJEc|t^f&qm?rJ>=H_Me5-N)poe!?S5D3!^FwUUWUWN9=bSn7a!mq>_1ADd((M6&k#XAv? zAx5q`lwhml1bq$W&q2eL# z^FmYu>{%q1=ynJBOV_Y*bQoWJd;bAA;kjMRGkhQ->KP_=MJ7`-7RvXU)CxkP8jlOJ zMHDM{iMETBY=`{uz=s}cS{vtP=869{8nrsAS_UQZnu+Cft!1-<-+U;~(w}DvnC7V& z1NveMGVv^vP^iZ!n7U))@~t`?$AL(+m~Yzn<$9H@WNhJ4xN!>`$42nSbUVVZy9!P( z_abUc$H^V7Ww5Fwc2J;Mp~PqgrSOp0M$I!=nXX!Z4H;8CcGHw#eH z5+aN+YIae}jubFWY*@H7pUl|r2%R`)G9o@|3(s@97|!}0UbME)^PZsiCx>I z$eZDj4WyLXgP>#kL;?(hVfCilat*WoJ^xcy9or~RPHgEzsl0#>j|}01_r@@g7Gbt@ z+^hM}XfCwO5N{t)@cgp@ZWQY%@d0-2NV2JhN2l6~CP6>su6(m!qoy{Q`+TqZ((W;` zj0bPDL5`rmKY$by5I3odCOxFmJ_gr&6n75)+&2DyWcH+d^%(tw$mHOJA92JI-qGQh zZJNIZwP72&=|i{th8Fn83ffv2{{mxdzkG|7;k5t&002ovPDHLkV1jdZt)Bn@ literal 0 HcmV?d00001 diff --git a/public/bottom-curve-vector R.svg b/public/bottom-curve-vector R.svg new file mode 100644 index 0000000000..f83802871b --- /dev/null +++ b/public/bottom-curve-vector R.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/bottom-curve-vector.svg b/public/bottom-curve-vector.svg new file mode 100644 index 0000000000..d77dd9ec64 --- /dev/null +++ b/public/bottom-curve-vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/kebab-horizontal.svg b/public/kebab-horizontal.svg new file mode 100644 index 0000000000..10ddf8d0cf --- /dev/null +++ b/public/kebab-horizontal.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/read.svg b/public/read.svg new file mode 100644 index 0000000000..58abe91797 --- /dev/null +++ b/public/read.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index af34ecebd4..704845cce0 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -1,9 +1,10 @@ 'use client'; -import Image from 'next/image'; import React, { useState } from 'react'; import styled from 'styled-components'; +import { ChattingList } from './chat/ChattingList'; + const styles = { chattingButton: styled.div` position: fixed; @@ -24,10 +25,15 @@ const styles = { transform: scale(1.1); } `, - chattingContainer: styled.div` + buttonIcon: styled.div` + width: 2.60419rem; + height: 2.60419rem; + background: url('chatting.svg') no-repeat center; + `, + container: styled.div` position: fixed; - bottom: 5rem; - right: 6.5rem; + bottom: 6rem; + right: 7.5rem; display: flex; width: 25%; height: 70%; @@ -38,17 +44,35 @@ const styles = { background: #fff; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); z-index: 100; - overflow: 'auto'; -webkit-transition: 0.4s; transition: 0.4s; + overflow: hidden; `, chattingHeader: styled.div` + display: inline-flex; + align-items: center; + padding-left: 1rem; + gap: 0.3rem; width: 100%; height: 3.25rem; flex-shrink: 0; border-radius: 20px 20px 0 0; background: var(--background, #f7f6f9); `, + title: styled.span` + font-family: 'Baloo 2'; + font-size: 1.575rem; + font-style: normal; + font-weight: 700; + line-height: normal; + `, + chattingSection: styled.div` + width: 100%; + height: calc(100% - 3.25rem); + display: flex; + flex-direction: column; + overflow-y: hidden; + `, }; export function FloatingChatting() { @@ -60,12 +84,25 @@ export function FloatingChatting() { return ( <> - + {isChatOpen && ( - - - + + + + maru{' '} + + + chat + + + + + + + + + )} ); diff --git a/src/components/chat/ChattingList.tsx b/src/components/chat/ChattingList.tsx new file mode 100644 index 0000000000..a35fba84ce --- /dev/null +++ b/src/components/chat/ChattingList.tsx @@ -0,0 +1,102 @@ +'use client'; + +import styled from 'styled-components'; + +const styles = { + chattingRoom: styled.div` + display: flex; + width: 100%; + height: 7.6875rem; + padding: 1.5rem; + align-items: center; + gap: 1rem; + flex-shrink: 0; + border-bottom: 1px solid var(--Gray-4, #dfdfdf); + background: #fff; + cursor: pointer; + `, + infoSection: styled.div` + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex: 1 0 0; + position: relative; + `, + textSection: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + flex: 1 0 0; + `, + userProfile: styled.div` + width: 3rem; + height: 3rem; + border-radius: 50%; + background: url('__avatar_url.png') lightgray 50% / cover no-repeat; + `, + activeCircle: styled.div` + width: 0.6rem; + height: 0.6rem; + position: absolute; + left: 2.375rem; + bottom: 0.125rem; + border-radius: 50%; + border: 2px solid #fff; + background-color: #27da4e; + `, + roomName: styled.p` + color: #000; + + font-family: Pretendard; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: 1.5rem; + `, + message: styled.p` + color: #666; + + text-overflow: ellipsis; + white-space: nowrap; + font-family: Pretendard; + font-size: 0.875rem; + font-style: normal; + font-weight: 700; + line-height: 1.5rem; + `, + newMessageCount: styled.div` + display: flex; + width: 2rem; + height: 2rem; + padding: 0.5rem 0.46875rem; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; + border-radius: 100px; + background: var(--Main-1, #e15637); + color: #fff; + font-family: Pretendard; + font-size: 0.75rem; + font-style: normal; + font-weight: 700; + line-height: 1rem; + `, +}; + +export function ChattingList() { + return ( + + + + + + room1 + hi + + + 2 + + ); +} diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx new file mode 100644 index 0000000000..012ad7daa5 --- /dev/null +++ b/src/components/chat/ChattingRoom.tsx @@ -0,0 +1,132 @@ +'use client'; + +import styled from 'styled-components'; + +import { ReceiverMessage } from './ReceiverMessage'; +import { SenderMessage } from './SenderMessage'; + +const styles = { + container: styled.div` + position: fixed; + bottom: 5rem; + left: 6.5rem; + display: flex; + width: 25%; + height: 70%; + flex-direction: column; + align-items: flex-start; + flex-shrink: 0; + border-radius: 20px; + background: #fff; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + z-index: 100; + -webkit-transition: 0.4s; + transition: 0.4s; + overflow: hidden; + `, + header: styled.div` + width: 100%; + height: 3.625rem; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.8rem; + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + `, + users: styled.div` + display: inline-flex; + width: 2rem; + align-items: center; + flex-shrink: 0; + position: relative; + `, + user: styled.div` + position: absolute; + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + border-radius: 150px; + border: 1.5px solid #fff; + background: url('__avatar_url.png') lightgray 50% / cover no-repeat; + `, + roomInfo: styled.div` + display: flex; + flex-direction: column; + align-items: center; + `, + roomName: styled.p` + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + `, + latestTime: styled.p` + color: var(--Text-gray, #666668); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + menu: styled.div` + width: 1rem; + height: 1rem; + flex-shrink: 0; + background: url('kebab-horizontal.svg') no-repeat; + `, + messageContainer: styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: calc(100% - 7.5rem); + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + position: relative; + `, + senderFrame: styled.div` + display: flex; + justify-content: flex-end; + padding-right: 0.8rem; + margin: 0.8rem 0; + `, + receiverFrame: styled.div` + display: flex; + justify-content: flex-start; + padding-left: 0.8rem; + margin: 0.8rem 0; + `, + messageInput: styled.div` + width: 100%; + height: 3rem; + `, +}; + +export function ChattingRoom() { + return ( + + + + + + + + + + 정릉 기숙사 405호 + 45분전 + + + + + + + + + + + + + + ); +} diff --git a/src/components/chat/ReceiverMessage.tsx b/src/components/chat/ReceiverMessage.tsx new file mode 100644 index 0000000000..f13050a4c9 --- /dev/null +++ b/src/components/chat/ReceiverMessage.tsx @@ -0,0 +1,137 @@ +'use client'; + +import styled from 'styled-components'; + +const styles = { + container: styled.div` + display: flex; + align-items: flex-start; + gap: 0.25rem; + flex-shrink: 0; + `, + profileSection: styled.div` + display: flex; + flex-direction: column; + width: 3rem; + height: 3rem; + justify-content: center; + align-items: center; + flex-shrink: 0; + `, + profilePic: styled.div` + width: 2rem; + height: 2rem; + border-radius: 50%; + flex-shrink: 0; + background: url('__avatar_url.png') center no-repeat; + `, + userName: styled.p` + color: var(--Text-gray, #666668); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + messageContainer: styled.div` + display: flex; + align-items: center; + `, + left: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + align-self: stretch; + `, + leftTop: styled.div` + width: 0.9375rem; + height: 0.75rem; + background: url('Bubble tip R.svg') no-repeat; + `, + leftMiddle: styled.div` + width: 0.375rem; + flex: 1 0 0; + background: var(--gray-gray-1, #f2f2f7); + `, + leftBottom: styled.div` + width: 0.375rem; + height: 0.4375rem; + background: url('bottom-curve-vector R.svg') no-repeat; + `, + right: styled.div` + display: flex; + padding: 0.25rem 0.5rem 0.25rem 0rem; + align-items: center; + border-radius: 0px 6px 6px 0px; + background: var(--gray-gray-1, #f2f2f7); + `, + messageFrame: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + gap: 0.25rem; + align-self: stretch; + `, + messageBody: styled.div` + display: flex; + align-items: flex-start; + gap: 0.5rem; + `, + message: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.625rem; + color: var(--Text-gray, #666668); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.25rem; + `, + time: styled.p` + color: var(--Text-gray, #666668); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 300; + line-height: normal; + `, + messageInfo: styled.div` + display: flex; + margin-top: 2rem; + align-items: flex-end; + gap: 0.25rem; + align-self: stretch; + `, +}; + +export function ReceiverMessage() { + return ( + + + + 김마루 + + + + + + + + + + + hi + + 11:31 AM + + + + + + + ); +} diff --git a/src/components/chat/SenderMessage.tsx b/src/components/chat/SenderMessage.tsx new file mode 100644 index 0000000000..405b5bdcea --- /dev/null +++ b/src/components/chat/SenderMessage.tsx @@ -0,0 +1,110 @@ +'use client'; + +import styled from 'styled-components'; + +const styles = { + container: styled.div` + display: inline-flex; + align-items: center; + `, + right: styled.div` + display: flex; + padding: 0.25rem 0rem 0.25rem 0.5rem; + align-items: center; + border-radius: 6px 0 0 6px; + background: var(--Gray-5, #828282); + `, + messageFrame: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + gap: 0.25rem; + align-self: stretch; + `, + messageBody: styled.div` + display: flex; + align-items: flex-start; + gap: 0.5rem; + `, + message: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.625rem; + color: var(--White, #fff); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.25rem; + `, + messageInfo: styled.div` + display: flex; + margin-top: 2rem; + align-items: flex-end; + gap: 0.25rem; + align-self: stretch; + `, + time: styled.span` + color: var(--White, #fff); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 300; + line-height: normal; + `, + readState: styled.div` + width: 1rem; + height: 0.5rem; + background: url('read.svg') no-repeat; + `, + left: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + align-self: stretch; + `, + leftTop: styled.div` + width: 0.9375rem; + height: 0.75rem; + background: url('Bubble tip.svg') no-repeat; + `, + leftMiddle: styled.div` + width: 0.375rem; + flex: 1 0 0; + background: var(--Gray-5, #828282); + `, + leftBottom: styled.div` + width: 0.375rem; + height: 0.4375rem; + fill: var(--Gray-5, #828282); + background: url('bottom-curve-vector.svg') no-repeat; + `, +}; + +export function SenderMessage() { + return ( + + + + + + 다음주 입소까지 생활 규칙 기입해주세요 + + + 11:31 AM + + + + + + + + + + + + ); +} From e58d5f490e9a28043aa8b531833a68b29c873cb0 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Tue, 16 Apr 2024 19:13:59 +0900 Subject: [PATCH 008/130] =?UTF-8?q?feat:=20age,=20budget,=20mbti,=20?= =?UTF-8?q?=EC=A0=84=EA=B3=B5=20state=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/profile-page.tsx | 208 ++++-------- src/app/pages/setting-page.tsx | 91 ++--- src/app/pages/user-input-page.tsx | 374 +++++++++++---------- src/components/FloatingChatting.tsx | 9 +- src/components/OptionSection.tsx | 401 ++++++++++++----------- src/components/VitalSection.tsx | 43 ++- src/components/card/CleanTest.tsx | 18 +- src/components/card/MajorSelector.tsx | 11 +- src/components/card/MbtiToggle.tsx | 35 +- src/components/card/SelfIntroduction.tsx | 31 -- src/components/card/Slider.tsx | 1 + src/components/card/index.ts | 1 - 12 files changed, 623 insertions(+), 600 deletions(-) delete mode 100644 src/components/card/SelfIntroduction.tsx diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index 5cddd7c2a0..24831999ae 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -10,22 +10,28 @@ import { useProfileData } from '@/features/profile'; const styles = { pageContainer: styled.div` display: flex; + height: 98rem; + padding: 0rem 10rem 10rem 10rem; flex-direction: column; - padding: 0 1.5rem; + justify-content: center; + align-items: center; + gap: 3rem; + align-self: stretch; `, userProfileContainer: styled.div` display: inline-flex; - align-items: flex-start; + width: 100%; + align-items: center; flex-shrink: 0; - gap: 2.62rem; + gap: 3rem; margin-top: 5.12rem; `, userProfileWithoutInfo: styled.div` display: inline-flex; flex-direction: column; align-items: center; - gap: 1.75rem; + gap: 1rem; `, userPicContainer: styled.div` display: flex; @@ -83,7 +89,7 @@ const styles = { justify-content: center; align-items: flex-end; gap: 0.375rem; - margin-left: 3.31rem; + margin-left: 2rem; `, switchWrapper: styled.label` position: relative; @@ -158,25 +164,16 @@ const styles = { cardSection: styled.div` display: inline-flex; - gap: 11.5rem; - margin: 4.75rem 0 0 0; + width: 100%; + gap: 8rem; `, cardWrapper: styled.div` display: flex; flex-direction: column; justify-content: center; align-items: flex-start; - gap: 2.25rem; - flex-shrink: 0; - `, - cardDescriptionSection: styled.div` - margin: 0 1.125rem 0 4.9375rem; - width: 36.9375rem; - height: 2.875rem; + gap: 1rem; flex-shrink: 0; - display: flex; - align-items: center; - gap: 20rem; `, description32px: styled.p` color: #000; @@ -194,11 +191,6 @@ const styles = { font-weight: 700; line-height: normal; `, - mateCardsContainer: styled.div` - display: flex; - gap: 1.69rem; - align-items: center; - `, mateCards: styled.div` display: flex; width: 35.6rem; @@ -228,41 +220,20 @@ const styles = { font-weight: 700; line-height: normal; `, - cardDefault: styled.div` - color: #fff; - text-align: center; - font-family: 'Noto Sans KR'; - font-size: 1rem; - font-style: normal; - font-weight: 700; - line-height: 2.5625rem; - - width: 6.0625rem; - height: 2.5625rem; - flex-shrink: 0; - border-radius: 20px 0 0 20px; - background: var(--Main-1, #e15637); - - position: absolute; - right: 0; - bottom: 1.5rem; - `, maruContainer: styled.div` - margin-top: 9.5625rem; display: flex; flex-direction: column; + gap: 3rem; `, weekContainer: styled.div` display: flex; - width: 70.25rem; height: 15.625rem; - justify-content: center; + justify-content: flex-end; align-items: flex-start; gap: 1.5rem; - flex-shrink: 0; - margin: 3.1875rem 0 4.0625rem 1.6875rem; + align-self: stretch; `, dayContainer: styled.div` width: 8.75rem; @@ -323,28 +294,30 @@ const styles = { rulesContainer: styled.div` display: flex; - width: 64.5rem; flex-direction: column; justify-content: center; - align-items: flex-start; - gap: 1.375rem; - margin: 0 3rem 3.0625rem 4.4375rem; + align-items: center; + gap: 3rem; + flex: 1 0 0; + align-self: stretch; `, rulesDescriptionContainer: styled.div` display: flex; - height: 2.6875rem; - justify-content: center; - align-items: flex-start; - gap: 51.6875rem; + justify-content: space-between; + align-items: center; + align-self: stretch; `, editButton: styled.button` display: flex; - padding: 0.63rem 1.13rem; + padding: 0.625rem 5.9375rem; justify-content: center; align-items: center; - border-radius: 16px; + gap: 0.5rem; + border-radius: 1rem; border: 1px solid var(--Main-1, #e15637); background: var(--White, #fff); + cursor: pointer; + color: var(--Main-1, #e15637); font-family: 'Noto Sans KR'; font-size: 1rem; @@ -358,60 +331,16 @@ const styles = { border-radius: 16px; background: #f7f6f9; `, - - accountContainer: styled.div` - display: flex; - width: 63.9375rem; - flex-direction: column; - justify-content: center; - align-items: flex-start; - gap: 1.375rem; - margin: 0 3rem 9.375rem 4.4375rem; - `, - accountContent: styled.div` - width: 64.5rem; - height: 12.4375rem; - border-radius: 16px; - background: #f7f6f9; - `, - - introductionContainer: styled.div` - display: inline-flex; - min-width: 41.125rem; - height: 5.4375rem; - padding: 1.25rem 0.5rem 2.75rem 1.5rem; - align-items: center; - flex-shrink: 0; - border-radius: 8px; - background: #f7f6f9; - - color: #000; - - font-family: 'Noto Sans KR'; - font-size: 1rem; - font-style: normal; - font-weight: 400; - line-height: normal; - `, }; interface UserProfileInfoProps { name: string | undefined; email: string | undefined; phoneNum: string | undefined; - selfIntroduction: string | undefined; src: string | undefined; - isMySelf: boolean | undefined; } -function UserInfo({ - name, - email, - phoneNum, - selfIntroduction, - src, - isMySelf, -}: UserProfileInfoProps) { +function UserInfo({ name, email, phoneNum, src }: UserProfileInfoProps) { const [isChecked, setIsChecked] = useState(false); const toggleSwitch = () => { @@ -424,17 +353,14 @@ function UserInfo({ - + {name} - {email} {phoneNum} - - {selfIntroduction} - + {email} @@ -472,7 +398,7 @@ function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { ); } -function Auth({ isMySelf }: { isMySelf: boolean | undefined }) { +function Auth() { return ( @@ -503,7 +429,6 @@ function Card({ > {name} - 기본 @@ -514,7 +439,6 @@ function Card({ > 메이트 - 기본 @@ -551,34 +475,36 @@ function Sun() { function Maru() { return ( - - 마이 마루 - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + 마이 마루 + + + + + + + + + + + + + + + + + + + + + + + + + + + 생활 규칙 @@ -586,11 +512,7 @@ function Maru() { - - 공용 계좌 - - - + ); } @@ -652,9 +574,7 @@ export function ProfilePage({ memberId }: { memberId: string }) { name={userData?.name ?? ''} email={userData?.email ?? ''} phoneNum={userData?.phoneNumber ?? ''} - selfIntroduction="집에서 요리해 먹는 것을 좋아하고 애니매이션을 즐겨봅니다." src={user.data?.data.profileImage} - isMySelf={isMySelf} /> (''); + const { mutate } = usePutUserCard(cardId); const router = useRouter(); const saveData = () => { const array = Object.keys(selectedOptions).filter( - key => selectedOptions[key], + key => key !== '전공' && key !== '엠비티아이' && selectedOptions[key], ); - const location = '성북 길음동'; - const myFeatures = [selectedState.smoking, selectedState.room, ...array]; + const location = locationInput ?? ''; + const myFeatures = [ + selectedState.smoking, + selectedState.room, + mateAge, + ...array, + mbti, + major, + budget, + ]; mutate({ location: location, features: myFeatures }); }; @@ -333,19 +346,21 @@ export function SettingPage({ cardId }: { cardId: number }) { smoking={features?.[0]} room={features?.[1]} onFeatureChange={handleFeatureChange} + onLocationChange={setLocation} + onMateAgeChange={setMateAge} isMySelf={isMySelf} type={type} /> - - - + - {type === 'myCard' ? : null} diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 78495a323d..7378805d21 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -10,18 +10,23 @@ import Person from '../../../public/option-img/person.svg'; import Visibility from '../../../public/option-img/visibility.svg'; import { VitalSection, OptionSection } from '@/components'; -import { SelfIntroduction } from '@/components/card'; import { useAuthValue, useUserData } from '@/features/auth'; import { usePutUserCard } from '@/features/profile'; const styles = { pageContainer: styled.div` display: flex; + width: 90rem; + height: 90rem; + padding: 2rem 10rem; flex-direction: column; + justify-content: center; + align-items: center; + gap: 2rem; + background: #fff; `, pageDescription: styled.p` - margin: 48px 0; - width: 100%; + align-self: stretch; color: var(--Black, #35373a); font-family: 'Noto Sans KR'; font-size: 2rem; @@ -31,26 +36,35 @@ const styles = { `, cardContainer: styled.div` display: flex; - gap: 0.88rem; + align-items: flex-start; + gap: 1rem; + `, + miniCardContainer: styled.div` + display: flex; + width: 22.5rem; + flex-direction: column; + align-items: flex-start; + gap: 0.625rem; `, cardNameSection: styled.div` display: flex; - width: 23.0625rem; - height: 36.125rem; flex-direction: column; - justify-content: center; align-items: flex-start; - gap: 1.125rem; - flex-shrink: 0; + gap: 1rem; + align-self: stretch; `, miniCard: styled.div` - width: 23.0625rem; - height: 17.5rem; - flex-shrink: 0; - border-radius: 30px; - padding: 1.62rem 1.44rem; display: flex; + padding: 2rem; flex-direction: column; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + border-radius: 1.875rem; + background: #f7f6f9; + + /* button */ + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); background: ${props => props.$active !== undefined && props.$active !== null && props.$active ? 'var(--background, #f7f6f9)' @@ -74,7 +88,6 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; - margin-bottom: 1.69rem; `, miniCardKeywordsContainer: styled.ul` display: flex; @@ -85,7 +98,7 @@ const styles = { `, miniCardList: styled.li` display: flex; - align-items: center; + align-items: flex-start; gap: 2rem; align-self: stretch; `, @@ -144,47 +157,45 @@ const styles = { : 'var(--Main-2, #767D86)'}; `, checkSection: styled.div` - width: calc(100% - 23.0625rem); - height: 95.8125rem; + display: flex; + width: 50rem; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + border-radius: 1.875rem; + background: var(--background, #f7f6f9); position: relative; `, checkContainer: styled.div` - width: 51.0625rem; - height: 95.8125rem; - flex-shrink: 0; - border-radius: 30px; + display: flex; + width: 50rem; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + border-radius: 1.875rem; background: var(--background, #f7f6f9); - padding: 3.56rem 0 0 1.56rem; - margin-bottom: 7.5rem; - position: absolute; + position: relative; display: ${props => props.$active !== undefined && props.$active !== null && props.$active ? '' : 'none'}; `, - - lineContainer: styled.div` - margin: 2.69rem 0; - padding: 0 4.37rem 0 1.38rem; - `, - horizontalLine: styled.hr` - width: 100%; - height: 0rem; - flex-shrink: 0; - stroke-width: 1px; - stroke: #d3d0d7; + horizontalLine: styled.div` + width: 43.75rem; + height: 0.0625rem; + background: var(--Gray-9, #d3d0d7); `, mateButtonContainer: styled.div` - display: inline-flex; - width: 13.5625rem; + display: flex; padding: 0.75rem 1.5rem; justify-content: center; align-items: center; gap: 0.25rem; - border-radius: 8px; + border-radius: 0.5rem; background: var(--Main-1, #e15637); - margin: 4.06rem 31rem 9.06rem 31rem; `, mateButtonDescription: styled.p` color: #fff; @@ -288,25 +299,41 @@ export function UserInputPage() { const { mutate: mutateMyCard } = usePutUserCard(myCardId); const { mutate: mutateMateCard } = usePutUserCard(mateCardId); + const [locationInput, setLocation] = useState(''); + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + const [budget, setBudget] = useState(''); + const [mateMbti, setMateMbti] = useState(''); + const [mateMajor, setMateMajor] = useState(''); + const [mateBudget, setMateBudget] = useState(''); + const [mateAge, setMateAge] = useState(''); + const handleButtonClick = () => { const myOptions = Object.keys(selectedOptions).filter( - key => selectedOptions[key], + key => key !== '전공' && key !== '엠비티아이' && selectedOptions[key], ); const mateOptions = Object.keys(selectedMateOptions).filter( - key => selectedMateOptions[key], + key => key !== '전공' && key !== '엠비티아이' && selectedMateOptions[key], ); - const location = '성북 길음동'; + const location = locationInput ?? ''; const myFeatures = [ selectedState.smoking, selectedState.room, ...myOptions, + mbti, + major, + budget, ]; const mateFeatures = [ selectedMateState.smoking, selectedMateState.room, + mateAge, ...mateOptions, + mateMbti, + mateMajor, + mateBudget, ]; try { @@ -338,134 +365,139 @@ export function UserInputPage() { {user?.name} 님과 희망 메이트에 대해서 알려주세요 - - - - 내카드 - - - - - - {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} - {user?.birthYear?.slice(2)}년생 · {selectedState.smoking} - - - - - - 서울특별시 성북구 정릉동 - - - - - - 메이트와 {selectedState.room} - - - - - - {selectedOptions['아침형'] ? '아침형' : null} - {selectedOptions['아침형'] && selectedOptions['올빼미형'] - ? ' · ' - : null} - {selectedOptions['올빼미형'] ? '올빼미형' : null} - - - - - - - 메이트카드 - - - - - - {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} - {user?.birthYear?.slice(2)}년생 · {selectedMateState.smoking} - - - - - - 서울특별시 성북구 정릉동 - - - - - - 메이트와 {selectedMateState.room} - - - - - - {selectedMateOptions['아침형'] ? '아침형' : null} - {selectedMateOptions['아침형'] && - selectedMateOptions['올빼미형'] - ? ' · ' - : null} - {selectedMateOptions['올빼미형'] ? '올빼미형' : null} - - - - - - - - - - - - - - - - - - - - - - + + + + + 내카드 + + + + + + {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} + {user?.birthYear?.slice(2)}년생 · {selectedState.smoking} + + + + + + {locationInput} + + + + + + 메이트와 {selectedState.room} + + + + + + {selectedOptions['아침형'] ? '아침형' : null} + {selectedOptions['올빼미형'] ? '올빼미형' : null} + + + + + + + 메이트카드 + + + + + + {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} + {user?.birthYear?.slice(2)}년생 ·{' '} + {selectedMateState.smoking} + + + + + + {locationInput} + + + + + + 메이트와 {selectedMateState.room} + + + + + + {selectedMateOptions['아침형'] ? '아침형' : null} + {selectedMateOptions['올빼미형'] ? '올빼미형' : null} + + + + + + + + {}} + isMySelf + type="myCard" + /> + + + + + + + + diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 704845cce0..3958517f98 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -25,10 +25,9 @@ const styles = { transform: scale(1.1); } `, - buttonIcon: styled.div` - width: 2.60419rem; - height: 2.60419rem; - background: url('chatting.svg') no-repeat center; + buttonIcon: styled.img` + width: 2rem; + height: 2rem; `, container: styled.div` position: fixed; @@ -84,7 +83,7 @@ export function FloatingChatting() { return ( <> - + {isChatOpen && ( diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index c66d5e24a1..efc32fdf22 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -9,10 +9,22 @@ import { MbtiToggle } from './card/MbtiToggle'; import { Slider } from './card/Slider'; const styles = { + container: styled.div` + width: 46rem; + height: 43.875rem; + display: flex; + flex-direction: column; + gap: 1rem; + `, optionContainer: styled.div` display: flex; + width: 46rem; + height: 41.4375rem; + padding-bottom: 6rem; flex-direction: column; - width: 100%; + align-items: flex-start; + gap: 1rem; + flex-shrink: 0; `, optionDescription: styled.p` color: #9a95a3; @@ -26,12 +38,11 @@ const styles = { optionList: styled.ul` position: relative; width: 100%; - margin-top: 2.25rem; `, optionListItem: styled.li` display: flex; align-items: center; - gap: 2rem; + gap: 4rem; margin-bottom: 1.25rem; `, optionListImg: styled.img` @@ -80,17 +91,9 @@ const styles = { budgetContainer: styled.div` display: flex; align-items: center; - gap: 1.75rem; + gap: 4rem; width: 100%; `, - budgetBar: styled.div` - width: 25rem; - height: 0.3125rem; - flex-shrink: 0; - border-radius: 20px; - background: #d9d9d9; - margin-bottom: 0.63rem; - `, value: styled.span` color: #000; @@ -159,11 +162,17 @@ const PersonalOptions = [ export function OptionSection({ optionFeatures, onFeatureChange, + onMbtiChange, + onMajorChange, + onBudgetChange, isMySelf, type, }: { optionFeatures: string[] | null; onFeatureChange: (option: string) => void; + onMbtiChange: React.Dispatch>; + onMajorChange: React.Dispatch>; + onBudgetChange: React.Dispatch>; isMySelf: boolean; type: string; }) { @@ -198,10 +207,6 @@ export function OptionSection({ setIsMbtiSelected(false); }; - const handleTestCompletion = (cleanScore: number) => { - setScore(cleanScore); - }; - const [budgetMin, setBudgetMin] = useState(0); const [budgetMax, setBudgetMax] = useState(355); const handleBudgetChange = (min: number, max: number) => { @@ -213,6 +218,22 @@ export function OptionSection({ const [isMajorSelected, setIsMajorSelected] = useState(false); const [isMbtiSelected, setIsMbtiSelected] = useState(false); + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + + useEffect(() => { + onMbtiChange(mbti); + }, [mbti]); + + useEffect(() => { + onMajorChange(major); + }, [major]); + + useEffect(() => { + const budgetString = `${budgetMin},${budgetMax}만원`; + onBudgetChange(budgetString); + }, [budgetMin, budgetMax]); + const handleMajorSelect = () => { setIsMajorSelected(true); setIsTestSelected(false); @@ -226,203 +247,211 @@ export function OptionSection({ }; return ( - + 선택 - - - - - { - if (isMySelf) { - handleOptionClick('아침형'); - if (selectedOptions['올빼미형']) - handleOptionClick('올빼미형'); - } - }} - > - 아침형 - - { - if (isMySelf) { - handleOptionClick('올빼미형'); - if (selectedOptions['아침형']) handleOptionClick('아침형'); - } - }} - > - 올빼미형 - - - - - - - {EatingOptions.map(option => ( - { - if (isMySelf) { - handleOptionClick(option); - } - }} - > - {option} - - ))} - - - - - - {HearingOptions.map(option => ( + + + + + { if (isMySelf) { - handleOptionClick(option); + handleOptionClick('아침형'); + if (selectedOptions['올빼미형']) + handleOptionClick('올빼미형'); } }} > - {option} + 아침형 - ))} - - - - - - {WeatherOptions.map(option => ( { if (isMySelf) { - handleOptionClick(option); + handleOptionClick('올빼미형'); + if (selectedOptions['아침형']) handleOptionClick('아침형'); } }} > - {option} + 올빼미형 - ))} - - - - - {type === 'myCard' ? ( + + + + - - ( + { - toggleTestVisibility(); + if (isMySelf) { + handleOptionClick(option); + } }} > - 테스트 하기 - - - = 0 && score < 5.34}>상 - 5.34 && score < 10.67}> - 평범보통 - - 10.67}>천하태평 + {option} + + ))} - ) : ( + + + - { - if (isMySelf) { - handleOptionClick('상'); - if (selectedOptions['평범보통']) + {HearingOptions.map(option => ( + { + if (isMySelf) { + handleOptionClick(option); + } + }} + > + {option} + + ))} + + + + + + {WeatherOptions.map(option => ( + { + if (isMySelf) { + handleOptionClick(option); + } + }} + > + {option} + + ))} + + + + + {type === 'myCard' ? ( + + + { + toggleTestVisibility(); + }} + > + {isTestVisible ? '결과 확인하기' : '테스트 하기'} + + + = 0 && score < 5.34}> + 상 + + 5.34 && score < 10.67}> + 평범보통 + + 10.67}>천하태평 + + ) : ( + + { + if (isMySelf) { + handleOptionClick('상'); + if (selectedOptions['평범보통']) + handleOptionClick('평범보통'); + if (selectedOptions['천하태평']) + handleOptionClick('천하태평'); + } + }} + > + 상 + + { + if (isMySelf) { handleOptionClick('평범보통'); - if (selectedOptions['천하태평']) - handleOptionClick('천하태평'); - } - }} - > - 상 - - { - if (isMySelf) { - handleOptionClick('평범보통'); - if (selectedOptions['상']) handleOptionClick('상'); - if (selectedOptions['천하태평']) + if (selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['천하태평']) + handleOptionClick('천하태평'); + } + }} + > + 평범보통 + + { + if (isMySelf) { handleOptionClick('천하태평'); - } - }} - > - 평범보통 - - { - if (isMySelf) { - handleOptionClick('천하태평'); - if (selectedOptions['상']) handleOptionClick('상'); - if (selectedOptions['평범보통']) - handleOptionClick('평범보통'); - } - }} - > - 천하태평 - - + if (selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['평범보통']) + handleOptionClick('평범보통'); + } + }} + > + 천하태평 + + + )} + + {isTestVisible && isTestSelected && ( + )} - - {isTestVisible && isTestSelected && ( - - )} - - - - {PersonalOptions.map(option => ( - { - if (isMySelf) { - handleOptionClick(option); - } - if (option === '엠비티아이') handleMbtiSelect(); - if (option === '전공') handleMajorSelect(); - }} - > - {option === '엠비티아이' ? <>MBTI : option} - - ))} - {selectedOptions['전공'] && isMajorSelected ? ( - - ) : null} - {selectedOptions['엠비티아이'] && isMbtiSelected ? ( - - ) : null} - - - - - - - 금액 - + + + + {PersonalOptions.map(option => ( + { + if (isMySelf) { + handleOptionClick(option); + } + if (option === '엠비티아이') { + handleMbtiSelect(); + } + if (option === '전공') { + handleMajorSelect(); + } + }} + > + {option === '엠비티아이' ? <>MBTI : option} + + ))} + {selectedOptions['전공'] && isMajorSelected ? ( + + ) : null} + {selectedOptions['엠비티아이'] && isMbtiSelected ? ( + + ) : null} + + + + + + + 금액 + + {`${budgetMin === 0 ? '0원' : `${budgetMin}만원`}`} ~{' '} {`${budgetMax === 355 ? '무제한' : `${budgetMax}만원`}`} - - - - - + + + + + ); } diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 6d3cc79334..4d2202f65c 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -9,9 +9,12 @@ const styles = { vitalContainer: styled.div` display: flex; flex-direction: column; - width: 100%; + align-items: flex-start; + gap: 1rem; + align-self: stretch; `, vitalDescription: styled.p` + width: 2.6875rem; color: var(--Main-1, #e15637); font-family: 'Noto Sans KR'; font-size: 1rem; @@ -22,21 +25,21 @@ const styles = { vitalListContainer: styled.ul` display: flex; flex-direction: column; - margin-top: 2.62rem; - gap: 1.5rem; + align-items: flex-start; + gap: 1rem; + align-self: stretch; `, vitalList: styled.li` - display: inline-flex; + display: flex; align-items: center; - gap: 2.5rem; - flex-shrink: 0; + gap: 2rem; + align-self: stretch; `, vitalListItemDescription: styled.p` - width: 5.125rem; - color: #000; - + width: 5rem; + color: var(--Main-2, #767d86); font-family: 'Noto Sans KR'; - font-size: 1.25rem; + font-size: 1.125rem; font-style: normal; font-weight: 500; line-height: normal; @@ -158,6 +161,8 @@ export function VitalSection({ smoking, room, onFeatureChange, + onLocationChange, + onMateAgeChange, isMySelf, type, }: { @@ -170,6 +175,8 @@ export function VitalSection({ optionName: keyof SelectedState, item: string | number, ) => void; + onLocationChange: React.Dispatch>; + onMateAgeChange: React.Dispatch>; isMySelf: boolean; type: string; }) { @@ -196,6 +203,15 @@ export function VitalSection({ onFeatureChange(optionName, item); } + const [locationInput, setLocation] = useState(location); + const handleLocationChange = (event: React.ChangeEvent) => { + setLocation(event.target.value); + }; + + useEffect(() => { + onLocationChange(locationInput); + }, [locationInput]); + const [mateMinAge, setMateMinAge] = useState(0); const [mateMaxAge, setMateMaxAge] = useState(11); const handleAgeChange = (min: number, max: number) => { @@ -203,6 +219,11 @@ export function VitalSection({ setMateMaxAge(max); }; + useEffect(() => { + const ageString = `±${mateMinAge}~${mateMaxAge}세`; + onMateAgeChange(ageString); + }, [mateMinAge, mateMaxAge]); + return ( 필수 @@ -243,6 +264,8 @@ export function VitalSection({ ) : ( diff --git a/src/components/card/CleanTest.tsx b/src/components/card/CleanTest.tsx index c3749a0064..ff6d2d03c9 100644 --- a/src/components/card/CleanTest.tsx +++ b/src/components/card/CleanTest.tsx @@ -6,14 +6,15 @@ import styled from 'styled-components'; const styles = { testSection: styled.ul` position: absolute; + z-index: 2; + right: 0; display: inline-flex; + padding: 2rem; flex-direction: column; - height: 31.25rem; - padding: 2.6875rem 3.4375rem; - border-radius: 20px; + align-items: flex-start; + gap: 1rem; + border-radius: 1.25rem; background: #fff; - gap: 2rem; - z-index: 5; /* button */ box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); @@ -21,10 +22,11 @@ const styles = { testListContainer: styled.li` display: flex; align-items: center; - gap: 4.31rem; + gap: 2rem; + align-self: stretch; `, testDescription: styled.p` - width: 6.9rem; + width: 7rem; color: #000; font-family: 'Noto Sans KR'; @@ -85,7 +87,7 @@ const currentState = ['깔끔', '정리 필요', '대청소 필요', '카오스' export function CleanTest({ onComplete, }: { - onComplete: (score: number) => void; + onComplete: React.Dispatch>; }) { const [selectedOptions, setSelectedOptions] = useState({ room: 0, diff --git a/src/components/card/MajorSelector.tsx b/src/components/card/MajorSelector.tsx index c648d5fefc..68cc56821a 100644 --- a/src/components/card/MajorSelector.tsx +++ b/src/components/card/MajorSelector.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; const styles = { @@ -39,11 +39,18 @@ const styles = { const majorOptions = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; -export function MajorSelector() { +export function MajorSelector({ + onChange, +}: { + onChange: React.Dispatch>; +}) { const [selectedMajor, setSelectedMajor] = useState(''); const handleMajorChange = (event: React.ChangeEvent) => { setSelectedMajor(event.target.value); }; + useEffect(() => { + onChange(selectedMajor); + }, [selectedMajor]); return ( >; +}) { const [toggleStates, setToggleStates] = useState({ toggle1: false, toggle2: false, @@ -114,14 +127,28 @@ export function MbtiToggle() { toggle4: false, }); + const [result, setResult] = useState(''); + const toggleSwitch = (toggleName: keyof typeof toggleStates) => { setToggleStates(prevState => ({ ...prevState, [toggleName]: !prevState[toggleName], })); }; + useEffect(() => { + const mbtiString: string[] = ['E', 'N', 'F', 'P']; + if (toggleStates.toggle1) mbtiString[0] = 'I'; + if (toggleStates.toggle2) mbtiString[1] = 'S'; + if (toggleStates.toggle3) mbtiString[2] = 'T'; + if (toggleStates.toggle4) mbtiString[3] = 'J'; + const newResult = mbtiString.join(''); + setResult(newResult); + onChange(newResult); + }, [toggleStates, onChange]); + return ( + {result} E ; -} diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index 25264a5747..8dd808ee2e 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -57,6 +57,7 @@ const styles = { cursor: pointer; position: relative; z-index: 1; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); } `, }; diff --git a/src/components/card/index.ts b/src/components/card/index.ts index 6fbb77b6ef..f48a854158 100644 --- a/src/components/card/index.ts +++ b/src/components/card/index.ts @@ -1,2 +1 @@ -export * from './SelfIntroduction'; export * from './Slider'; From 03cca1595628aa33659ddd113ee56bfd48bab00a Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 17 Apr 2024 01:19:17 +0900 Subject: [PATCH 009/130] fix: age, budget, mbti, major PUT (#52) --- src/app/pages/setting-page.tsx | 37 ++++++++++++++++++++----------- src/app/pages/user-input-page.tsx | 30 +++++++++++++++---------- src/components/OptionSection.tsx | 31 +++++++++++++++++--------- src/components/VitalSection.tsx | 29 ++++++++++++++++++++---- src/components/card/Slider.tsx | 23 ++++++++++++++++--- 5 files changed, 108 insertions(+), 42 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 37d3450914..b0521d9cd1 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -198,18 +198,35 @@ export function SettingPage({ cardId }: { cardId: number }) { const [selectedOptions, setSelectedOptions] = useState({}); + const majorArray = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; + const cleanScoreArray = ['상', '평범보통', '천하태평']; + useEffect(() => { if (isMySelf) { if (features !== null) { const initialOptions: SelectedOptions = {}; features.slice(2).forEach(option => { - initialOptions[option] = true; + if (option.includes(',')) setBudget(option); + else if (option.includes('~')) setMateAge(option); + else if (option.includes('E') || option.includes('I')) + setMbti(option); + else if (majorArray.includes(option)) setMajor(option); + else if (cleanScoreArray.includes(option)) + initialOptions[option] = true; }); setSelectedOptions(initialOptions); } } }, [features, isMySelf]); + const [locationInput, setLocation] = useState(card.data?.data.location); + + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + const [budget, setBudget] = useState(''); + const [mateAge, setMateAge] = useState(''); + const [cleanScore, setCleanScore] = useState(''); + const handleFeatureChange = ( optionName: keyof SelectedState, item: string | number, @@ -230,18 +247,12 @@ export function SettingPage({ cardId }: { cardId: number }) { } }; - const [locationInput, setLocation] = useState(card.data?.data.location); - const [mbti, setMbti] = useState(''); - const [major, setMajor] = useState(''); - const [budget, setBudget] = useState(''); - const [mateAge, setMateAge] = useState(''); - const { mutate } = usePutUserCard(cardId); const router = useRouter(); const saveData = () => { const array = Object.keys(selectedOptions).filter( - key => key !== '전공' && key !== '엠비티아이' && selectedOptions[key], + key => selectedOptions[key], ); const location = locationInput ?? ''; @@ -250,8 +261,9 @@ export function SettingPage({ cardId }: { cardId: number }) { selectedState.room, mateAge, ...array, - mbti, - major, + ...(cleanScore !== null && cleanScore !== undefined ? [cleanScore] : []), + ...(mbti !== null && mbti !== undefined ? [mbti] : []), + ...(major !== null && major !== undefined ? [major] : []), budget, ]; @@ -330,9 +342,6 @@ export function SettingPage({ cardId }: { cardId: number }) { {selectedOptions['아침형'] ? '아침형' : null} - {selectedOptions['아침형'] && selectedOptions['올빼미형'] - ? ' · ' - : null} {selectedOptions['올빼미형'] ? '올빼미형' : null} @@ -345,6 +354,7 @@ export function SettingPage({ cardId }: { cardId: number }) { location={card.data?.data.location} smoking={features?.[0]} room={features?.[1]} + mateAge={mateAge} onFeatureChange={handleFeatureChange} onLocationChange={setLocation} onMateAgeChange={setMateAge} @@ -358,6 +368,7 @@ export function SettingPage({ cardId }: { cardId: number }) { onMbtiChange={setMbti} onMajorChange={setMajor} onBudgetChange={setBudget} + onCleanTestChange={setCleanScore} isMySelf={isMySelf} type={type} /> diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 7378805d21..2e7dc24853 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -300,20 +300,21 @@ export function UserInputPage() { const { mutate: mutateMateCard } = usePutUserCard(mateCardId); const [locationInput, setLocation] = useState(''); - const [mbti, setMbti] = useState(''); - const [major, setMajor] = useState(''); - const [budget, setBudget] = useState(''); - const [mateMbti, setMateMbti] = useState(''); - const [mateMajor, setMateMajor] = useState(''); - const [mateBudget, setMateBudget] = useState(''); + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + const [budget, setBudget] = useState(''); + const [mateMbti, setMateMbti] = useState(''); + const [mateMajor, setMateMajor] = useState(''); + const [mateBudget, setMateBudget] = useState(''); const [mateAge, setMateAge] = useState(''); + const [cleanScore, setCleanScore] = useState(''); const handleButtonClick = () => { const myOptions = Object.keys(selectedOptions).filter( - key => key !== '전공' && key !== '엠비티아이' && selectedOptions[key], + key => selectedOptions[key], ); const mateOptions = Object.keys(selectedMateOptions).filter( - key => key !== '전공' && key !== '엠비티아이' && selectedMateOptions[key], + key => selectedMateOptions[key], ); const location = locationInput ?? ''; @@ -321,8 +322,9 @@ export function UserInputPage() { selectedState.smoking, selectedState.room, ...myOptions, - mbti, - major, + ...(cleanScore !== null && cleanScore !== undefined ? [cleanScore] : []), + ...(mbti !== null && mbti !== undefined ? [mbti] : []), + ...(major !== null && major !== undefined ? [major] : []), budget, ]; @@ -331,8 +333,8 @@ export function UserInputPage() { selectedMateState.room, mateAge, ...mateOptions, - mateMbti, - mateMajor, + ...(mateMbti !== null && mateMbti !== undefined ? [mateMbti] : []), + ...(mateMajor !== null && mateMajor !== undefined ? [mateMajor] : []), mateBudget, ]; @@ -457,6 +459,7 @@ export function UserInputPage() { location={undefined} smoking={undefined} room={undefined} + mateAge={undefined} onFeatureChange={handleFeatureChange} onLocationChange={setLocation} onMateAgeChange={() => {}} @@ -470,6 +473,7 @@ export function UserInputPage() { onMbtiChange={setMbti} onMajorChange={setMajor} onBudgetChange={setBudget} + onCleanTestChange={setCleanScore} isMySelf type="myCard" /> @@ -481,6 +485,7 @@ export function UserInputPage() { location={locationInput ?? undefined} smoking={undefined} room={undefined} + mateAge={undefined} onFeatureChange={handleMateFeatureChange} onLocationChange={setLocation} onMateAgeChange={setMateAge} @@ -494,6 +499,7 @@ export function UserInputPage() { onMbtiChange={setMateMbti} onMajorChange={setMateMajor} onBudgetChange={setMateBudget} + onCleanTestChange={() => {}} isMySelf type="mateCard" /> diff --git a/src/components/OptionSection.tsx b/src/components/OptionSection.tsx index efc32fdf22..2307d837f3 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/OptionSection.tsx @@ -165,14 +165,16 @@ export function OptionSection({ onMbtiChange, onMajorChange, onBudgetChange, + onCleanTestChange, isMySelf, type, }: { optionFeatures: string[] | null; onFeatureChange: (option: string) => void; - onMbtiChange: React.Dispatch>; - onMajorChange: React.Dispatch>; - onBudgetChange: React.Dispatch>; + onMbtiChange: React.Dispatch>; + onMajorChange: React.Dispatch>; + onBudgetChange: React.Dispatch>; + onCleanTestChange: React.Dispatch>; isMySelf: boolean; type: string; }) { @@ -207,8 +209,15 @@ export function OptionSection({ setIsMbtiSelected(false); }; + useEffect(() => { + if (score < 5.34 && score > 0) onCleanTestChange('상'); + if (score > 5.34 && score < 10.67) onCleanTestChange('평범보통'); + if (score > 10.67) onCleanTestChange('천하태평'); + }, [score]); + const [budgetMin, setBudgetMin] = useState(0); const [budgetMax, setBudgetMax] = useState(355); + const handleBudgetChange = (min: number, max: number) => { setBudgetMin(min); setBudgetMax(max); @@ -218,19 +227,19 @@ export function OptionSection({ const [isMajorSelected, setIsMajorSelected] = useState(false); const [isMbtiSelected, setIsMbtiSelected] = useState(false); - const [mbti, setMbti] = useState(''); - const [major, setMajor] = useState(''); + const [selectedMbti, setMbti] = useState(''); + const [selectedMajor, setMajor] = useState(''); useEffect(() => { - onMbtiChange(mbti); - }, [mbti]); + onMbtiChange(selectedMbti); + }, [selectedMbti]); useEffect(() => { - onMajorChange(major); - }, [major]); + onMajorChange(selectedMajor); + }, [selectedMajor]); useEffect(() => { - const budgetString = `${budgetMin},${budgetMax}만원`; + const budgetString = `${budgetMin},${budgetMax}`; onBudgetChange(budgetString); }, [budgetMin, budgetMax]); @@ -441,6 +450,8 @@ export function OptionSection({ min={0} max={355} step={5} + initialMin={budgetMin} + initialMax={budgetMax} onChange={handleBudgetChange} /> diff --git a/src/components/VitalSection.tsx b/src/components/VitalSection.tsx index 4d2202f65c..4d0f391868 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/VitalSection.tsx @@ -160,6 +160,7 @@ export function VitalSection({ location, smoking, room, + mateAge, onFeatureChange, onLocationChange, onMateAgeChange, @@ -171,6 +172,7 @@ export function VitalSection({ location: string | undefined; smoking: string | undefined; room: string | undefined; + mateAge: string | undefined; onFeatureChange: ( optionName: keyof SelectedState, item: string | number, @@ -212,15 +214,27 @@ export function VitalSection({ onLocationChange(locationInput); }, [locationInput]); - const [mateMinAge, setMateMinAge] = useState(0); - const [mateMaxAge, setMateMaxAge] = useState(11); + const [initialMateMinAge, setInitialMin] = useState(0); + const [initialMateMaxAge, setInitialMax] = useState(11); + + useEffect(() => { + if (mateAge !== undefined) { + const [min, max] = mateAge.split('~'); + setInitialMin(Number(min)); + setInitialMax(Number(max)); + } + }, [mateAge]); + + const [mateMinAge, setMateMinAge] = useState(initialMateMinAge); + const [mateMaxAge, setMateMaxAge] = useState(initialMateMaxAge); + const handleAgeChange = (min: number, max: number) => { setMateMinAge(min); setMateMaxAge(max); }; useEffect(() => { - const ageString = `±${mateMinAge}~${mateMaxAge}세`; + const ageString = `${mateMinAge}~${mateMaxAge}`; onMateAgeChange(ageString); }, [mateMinAge, mateMaxAge]); @@ -373,7 +387,14 @@ export function VitalSection({ ) : ( <> - + {`${mateMinAge === 0 ? '동갑' : `±${mateMinAge}세`}`} ~{' '} {`${mateMaxAge === 11 ? '무제한' : `±${mateMaxAge}세`}`} diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index 8dd808ee2e..e4ef6929aa 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -66,6 +66,8 @@ interface SliderProps { min: number; max: number; step: number; + initialMin: number; + initialMax: number; onChange: (min: number, max: number) => void; } @@ -75,9 +77,24 @@ interface FillProps { $right: string; } -export function Slider({ min, max, step, onChange }: SliderProps) { - const [minValue, setMinValue] = useState(min); - const [maxValue, setMaxValue] = useState(max); +export function Slider({ + min, + max, + step, + initialMin, + initialMax, + onChange, +}: SliderProps) { + const [initialMinValue, setInitialMin] = useState(initialMin); + const [initialMaxValue, setInitialMax] = useState(initialMax); + + useEffect(() => { + setInitialMin(initialMin); + setInitialMax(initialMax); + }, []); + + const [minValue, setMinValue] = useState(initialMinValue); + const [maxValue, setMaxValue] = useState(initialMaxValue); const handleMinChange = (e: React.ChangeEvent) => { const newLow = Number(e.target.value); From 763dbd1148df151030bdc4931903d1eaf7488325 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 17 Apr 2024 15:12:00 +0900 Subject: [PATCH 010/130] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/chat.api.ts | 8 ++++++++ src/features/chat/chat.dto.ts | 8 ++++++++ src/features/chat/chat.hook.ts | 10 ++++++++++ src/features/chat/index.ts | 1 + 4 files changed, 27 insertions(+) create mode 100644 src/features/chat/chat.api.ts create mode 100644 src/features/chat/chat.dto.ts create mode 100644 src/features/chat/chat.hook.ts create mode 100644 src/features/chat/index.ts diff --git a/src/features/chat/chat.api.ts b/src/features/chat/chat.api.ts new file mode 100644 index 0000000000..628b27ae03 --- /dev/null +++ b/src/features/chat/chat.api.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; + +import { type GetChatRoomDTO } from './chat.dto'; + +export const getChatRoom = async (roomName: string) => + await axios + .get(`/maru-api/chatRoom/${roomName}`) + .then(res => res.data); diff --git a/src/features/chat/chat.dto.ts b/src/features/chat/chat.dto.ts new file mode 100644 index 0000000000..5486787d6e --- /dev/null +++ b/src/features/chat/chat.dto.ts @@ -0,0 +1,8 @@ +import { type SuccessBaseDTO } from '@/shared/types'; + +export interface GetChatRoomDTO extends SuccessBaseDTO { + data: { + id: number; + name: string; + }; +} diff --git a/src/features/chat/chat.hook.ts b/src/features/chat/chat.hook.ts new file mode 100644 index 0000000000..9cba08e70f --- /dev/null +++ b/src/features/chat/chat.hook.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getChatRoom } from './chat.api'; + +export const useChatRoomData = (roomName: string) => + useQuery({ + queryKey: [`/api/chatRoom/${roomName}`], + queryFn: async () => await getChatRoom(roomName), + enabled: roomName !== undefined, + }); diff --git a/src/features/chat/index.ts b/src/features/chat/index.ts new file mode 100644 index 0000000000..721c4e668a --- /dev/null +++ b/src/features/chat/index.ts @@ -0,0 +1 @@ +export * from './chat.hook'; From cb45dd7bfacd750c1063478acd1c769b78fb7390 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 17 Apr 2024 17:09:37 +0900 Subject: [PATCH 011/130] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=EC=99=80=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/components/FloatingChatting.tsx | 36 ++++++++++++++++++++++++++--- yarn.lock | 5 ++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 704661dda7..93708cb071 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.25.0", "@tanstack/react-query-devtools": "^5.25.0", "axios": "^1.6.7", diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index af34ecebd4..3569ce02c0 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -1,9 +1,12 @@ 'use client'; +import { Client } from '@stomp/stompjs'; import Image from 'next/image'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; +import { useAuthValue } from '@/features/auth'; + const styles = { chattingButton: styled.div` position: fixed; @@ -29,8 +32,8 @@ const styles = { bottom: 5rem; right: 6.5rem; display: flex; - width: 25%; - height: 70%; + width: 25rem; + height: 35rem; flex-direction: column; align-items: flex-start; flex-shrink: 0; @@ -57,6 +60,33 @@ export function FloatingChatting() { const toggleChat = () => { setIsChatOpen(prevState => !prevState); }; + + // const [stompClient, setStompClient] = useState(null); + + const auth = useAuthValue(); + + useEffect(() => { + const stomp = new Client({ + brokerURL: `ws://ec2-13-124-240-68.ap-northeast-2.compute.amazonaws.com:8080/ws`, + connectHeaders: { + Authorization: `Bearer ${auth?.accessToken}`, + }, + debug: (str: string) => { + console.log(str); + }, + reconnectDelay: 5000, + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + }); + // setStompClient(stomp); + + stomp.onConnect = () => { + console.log('WebSocket 연결이 열렸습니다.'); + }; + + stomp.activate(); + }, []); + return ( <> diff --git a/yarn.lock b/yarn.lock index 43e53f84a9..f4b3b4e6dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,6 +182,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== +"@stomp/stompjs@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.0.0.tgz#46b5c454a9dc8262e0b20f3b3dbacaa113993077" + integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== + "@swc/helpers@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" From 422469c23b83423e27a1df33491c6dfb4f37b916 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 03:09:31 +0900 Subject: [PATCH 012/130] fix: modify eslint rule --- .eslintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.json b/.eslintrc.json index dd21620a3a..141b7d3134 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,7 @@ ], "no-empty-pattern": ["warn", { "allowObjectPatternsAsParameters": true }], "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-empty-function": ["warn", {}], "react/no-array-index-key": "warn", From d789287e65eaa0cbacbe101ac947fabbe48d701e Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 03:11:28 +0900 Subject: [PATCH 013/130] fix: apply changed rule --- src/app/pages/writing-post-page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 0f31064bd8..5c93a705c5 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -494,9 +494,7 @@ export function WritingPostPage() { console.error(error); } } - })().catch((error: Error) => { - console.error(error); - }); + })(); }; useEffect( From bb478c4fdae3f59c1bff3e97f123befd8fb7908d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 11:59:34 +0900 Subject: [PATCH 014/130] =?UTF-8?q?fix:=20AuthProvider=20Refresh=20Token?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/lib/providers/AuthProvider.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index 8114cef1c8..98ba841ef1 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -1,5 +1,6 @@ 'use client'; +import { isAxiosError } from 'axios'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect } from 'react'; @@ -8,7 +9,7 @@ import { useAuthActions, useAuthValue, } from '@/features/auth'; -import { load } from '@/shared/storage'; +import { load, remove } from '@/shared/storage'; export function AuthProvider({ children }: { children: React.ReactNode }) { const auth = useAuthValue(); @@ -33,9 +34,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { expiresIn: data.expiresIn, }); }) - .catch(err => { - console.error(err); - router.replace('/'); + .catch((err: Error) => { + if (isAxiosError(err) && err.status === 500) { + remove({ type: 'local', key: 'refreshToken' }); + router.replace('/'); + } }); } else { router.replace('/'); From d8b27b810d7fc2fdb3fd670c12e521e0a0f6a1c0 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 17:04:11 +0900 Subject: [PATCH 015/130] fix: apply changes to create shared post api (#63) --- src/app/pages/writing-post-page.tsx | 22 +++++++++++++++++++++- src/features/shared/shared.api.ts | 24 ++---------------------- src/features/shared/shared.type.ts | 4 ++++ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 5c93a705c5..938ba45040 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -11,6 +11,7 @@ import { import { type NaverAddress } from '@/features/geocoding'; import { getImageURL, putImage } from '@/features/image'; import { useCreateSharedPost } from '@/features/shared'; +import { useToast } from '@/features/toast'; const styles = { pageContainer: styled.div` @@ -348,6 +349,8 @@ export function WritingPostPage() { const { mutate } = useCreateSharedPost(); + const { createToast } = useToast(); + const handleExtraOptionClick = (option: string) => { setSelectedExtraOptions(prevSelectedOptions => ({ ...prevSelectedOptions, @@ -482,12 +485,29 @@ export function WritingPostPage() { schoolTime: 20, convenienceStoreTime: 2, }, + roomMateCardData: { + location: '솔샘로 44', + features: ['특징1', '특징2', '특징3'], + }, }, { onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); router.back(); }, - onError: () => {}, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, }, ); } catch (error) { diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index 3741de2273..445248ea4a 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -56,28 +56,8 @@ export const getSharedPosts = async ({ return await axios.get(getURI()); }; -export const createSharedPost = async ({ - imageFilesData, - postData, - transactionData, - roomDetailData, - locationData, -}: CreateSharedPostProps) => { - const formData = new FormData(); - formData.append('imageFilesData', JSON.stringify(imageFilesData)); - formData.append('postData', JSON.stringify(postData)); - formData.append('transactionData', JSON.stringify(transactionData)); - formData.append('roomDetailData', JSON.stringify(roomDetailData)); - formData.append('locationData', JSON.stringify(locationData)); - - return await axios.post( - `/maru-api/shared/posts/studio`, - formData, - { - headers: { 'Content-Type': 'multipart/form-data' }, - }, - ); -}; +export const createSharedPost = async (postData: CreateSharedPostProps) => + await axios.post(`/maru-api/shared/posts/studio`, postData); export const getSharedPost = async (postId: number) => await axios.get(`/maru-api/shared/posts/studio/${postId}`); diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 870077f82c..5e0d6ea161 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -44,4 +44,8 @@ export interface CreateSharedPostProps { schoolTime: number; convenienceStoreTime: number; }; + roomMateCardData: { + location: string; + features: string[]; + }; } From 63efa8eee09be768e836919fb005841a2be8b2c8 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 17:05:59 +0900 Subject: [PATCH 016/130] fix: change the condition for removing refresh token (#63) --- src/app/lib/providers/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index 98ba841ef1..609ebf1ee8 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -35,7 +35,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }); }) .catch((err: Error) => { - if (isAxiosError(err) && err.status === 500) { + if (isAxiosError(err) && err.code === 'ETIMEOUT') { remove({ type: 'local', key: 'refreshToken' }); router.replace('/'); } From dc6add1e4b632a4bb2cc90e230c77139aa7f22ad Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 22:21:50 +0900 Subject: [PATCH 017/130] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EC=8B=9C,=20=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EC=9A=94=EC=B2=AD=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 1 + src/app/lib/axios-interceptor.ts | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/app/lib/axios-interceptor.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index edc0121cdb..b934776ae0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import React from 'react'; import './globals.scss'; +import '@/app/lib/axios-interceptor'; import { AuthProvider, diff --git a/src/app/lib/axios-interceptor.ts b/src/app/lib/axios-interceptor.ts new file mode 100644 index 0000000000..4b0e94edb9 --- /dev/null +++ b/src/app/lib/axios-interceptor.ts @@ -0,0 +1,54 @@ +import axios, { type AxiosRequestConfig, isAxiosError } from 'axios'; + +import { postTokenRefresh } from '@/features/auth'; +import { load, save } from '@/shared/storage'; + +interface AxiosRequestConfigWithRetryCount extends AxiosRequestConfig { + retryCount?: number; +} + +axios.interceptors.response.use( + response => response, + async error => { + if (!isAxiosError(error)) return await Promise.reject(error); + + const refreshToken = load({ type: 'local', key: 'refreshToken' }); + const config = error.config as AxiosRequestConfigWithRetryCount; + if ( + error.response?.status === 401 && + refreshToken != null && + (config?.retryCount ?? 0) < 3 + ) { + config.retryCount = (config?.retryCount ?? 0) + 1; + try { + const response = await postTokenRefresh(refreshToken); + + axios.defaults.headers.common.Authorization = `Bearer ${response.data.accessToken}`; + save({ + type: 'local', + key: 'refreshToken', + value: response.data.refreshToken, + }); + save({ + type: 'local', + key: 'expiresIn', + value: `${response.data.expiresIn}`, + }); + + const token = response.data.accessToken; + const newConfig = { + ...config, + headers: { + ...config.headers, + Authorization: `Bearer ${token}`, + }, + }; + + return await axios(newConfig); + } catch (refreshError) { + return await Promise.reject(refreshError); + } + } + return await Promise.reject(error); + }, +); From 100993b8a751a098717c9834cc1ea4fbefe21ec7 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 22:23:48 +0900 Subject: [PATCH 018/130] =?UTF-8?q?feat:=20axios-interceptor=20'use=20clie?= =?UTF-8?q?nt'=20=EC=A7=80=EC=8B=9C=EC=96=B4=20=EC=B6=94=EA=B0=80=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/lib/axios-interceptor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/lib/axios-interceptor.ts b/src/app/lib/axios-interceptor.ts index 4b0e94edb9..e15039eede 100644 --- a/src/app/lib/axios-interceptor.ts +++ b/src/app/lib/axios-interceptor.ts @@ -1,3 +1,5 @@ +'use client'; + import axios, { type AxiosRequestConfig, isAxiosError } from 'axios'; import { postTokenRefresh } from '@/features/auth'; From 39b34731c0fdad761943b1acc86923109a512f3c Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 18 Apr 2024 23:32:41 +0900 Subject: [PATCH 019/130] =?UTF-8?q?fix:=20SharedPost=20Entity=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/shared-post/shared-post.type.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index 4f1d0201b9..c5ceb6f806 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -55,6 +55,7 @@ export interface SharedPost { id: number; title: string; content: string; + roomMateFeatures: string[]; roomImages: Set<{ fileName: string; isThumbnail: boolean; From f1cd6b6f66189c9120e082471b8b4b8dc4399e9a Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 19 Apr 2024 00:47:49 +0900 Subject: [PATCH 020/130] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/writing-post-page.tsx | 88 ++++++++++---------------- src/features/shared/shared.hook.ts | 96 +++++++++++++++++++++++++++++ src/features/shared/shared.type.ts | 17 +++++ 3 files changed, 145 insertions(+), 56 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 938ba45040..56ecb6e89e 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -8,9 +8,12 @@ import { LocationSearchBox, MateSearchBox, } from '@/components/writing-post-page'; -import { type NaverAddress } from '@/features/geocoding'; import { getImageURL, putImage } from '@/features/image'; -import { useCreateSharedPost } from '@/features/shared'; +import { + type ImageFile, + useCreateSharedPost, + useCreateSharedPostProps, +} from '@/features/shared'; import { useToast } from '@/features/toast'; const styles = { @@ -310,64 +313,37 @@ interface ButtonActiveProps { $isSelected: boolean; } -interface SelectedOptions { - budget?: string; - roomType?: string; - livingRoom?: string; - roomCount?: string; - restRoomCount?: string; - floorType?: string; -} - -type SelectedExtraOptions = Record; - -interface ImageFile { - url: string; - file: File; - extension: string; -} - export function WritingPostPage() { const router = useRouter(); - const [images, setImages] = useState([]); const imageInputRef = useRef(null); - const [selectedExtraOptions, setSelectedExtraOptions] = - useState({}); - const [showMateSearchBox, setShowMateSearchBox] = useState(false); - const [selectedOptions, setSelectedOptions] = useState({}); - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); - const [mateLimit, setMateLimit] = useState(0); - const [expectedMonthlyFee, setExpectedMonthlyFee] = useState(0); - const [houseSize, setHouseSize] = useState(0); - - const [address, setAddress] = useState(null); const [showLocationSearchBox, setShowLocationSearchBox] = useState(false); + const { + title, + setTitle, + content, + setContent, + images, + setImages, + mateLimit, + setMateLimit, + houseSize, + setHouseSize, + address, + setAddress, + expectedMonthlyFee, + setExpectedMonthlyFee, + handleOptionClick, + handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, + } = useCreateSharedPostProps(); const { mutate } = useCreateSharedPost(); - const { createToast } = useToast(); - const handleExtraOptionClick = (option: string) => { - setSelectedExtraOptions(prevSelectedOptions => ({ - ...prevSelectedOptions, - [option]: !prevSelectedOptions[option], - })); - }; - - const handleOptionClick = ( - optionName: keyof SelectedOptions, - item: string, - ) => { - setSelectedOptions(prevState => ({ - ...prevState, - [optionName]: prevState[optionName] === item ? null : item, - })); - }; - const handleTitleInputChanged = ( event: React.ChangeEvent, ) => { @@ -605,7 +581,7 @@ export function WritingPostPage() { {DealOptions.map(option => ( { handleOptionClick('budget', option); }} @@ -633,7 +609,7 @@ export function WritingPostPage() { {FloorOptions.map(option => ( { handleOptionClick('floorType', option); }} @@ -647,7 +623,7 @@ export function WritingPostPage() { {AdditionalOptions.map(option => ( { handleExtraOptionClick(option); }} @@ -661,7 +637,7 @@ export function WritingPostPage() { {RoomOptions.map(option => ( { handleOptionClick('roomType', option); }} @@ -675,7 +651,7 @@ export function WritingPostPage() { {LivingRoomOptions.map(option => ( { handleOptionClick('livingRoom', option); }} @@ -689,7 +665,7 @@ export function WritingPostPage() { {RoomCountOptions.map(option => ( { handleOptionClick('roomCount', option); }} @@ -703,7 +679,7 @@ export function WritingPostPage() { {RestRoomCountOptions.map(option => ( { handleOptionClick('restRoomCount', option); }} diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 1efb729887..b59fb0abbc 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -10,10 +10,14 @@ import { scrapPost, } from './shared.api'; import { + type ImageFile, type CreateSharedPostProps, type GetSharedPostsProps, + type SelectedExtraOptions, + type SelectedOptions, } from './shared.type'; +import { type NaverAddress } from '@/features/geocoding'; import { type FailureDTO, type SuccessBaseDTO } from '@/shared/types'; export const usePaging = ({ @@ -83,6 +87,98 @@ export const usePaging = ({ ); }; +export const useCreateSharedPostProps = () => { + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [images, setImages] = useState([]); + const [address, setAddress] = useState(null); + + const [mateLimit, setMateLimit] = useState(0); + const [expectedMonthlyFee, setExpectedMonthlyFee] = useState(0); + + const [houseSize, setHouseSize] = useState(0); + const [selectedExtraOptions, setSelectedExtraOptions] = + useState({}); + const [selectedOptions, setSelectedOptions] = useState({}); + + const handleExtraOptionClick = useCallback((option: string) => { + setSelectedExtraOptions(prevSelectedOptions => ({ + ...prevSelectedOptions, + [option]: !prevSelectedOptions[option], + })); + }, []); + + const handleOptionClick = useCallback( + (optionName: keyof SelectedOptions, item: string) => { + setSelectedOptions(prevState => ({ + ...prevState, + [optionName]: prevState[optionName] === item ? null : item, + })); + }, + [], + ); + + const isOptionSelected = useCallback( + (optionName: keyof SelectedOptions, item: string) => + selectedOptions[optionName] === item, + [selectedOptions], + ); + + const isExtraOptionSelected = useCallback( + (item: string) => selectedExtraOptions[item], + [selectedExtraOptions], + ); + + return useMemo( + () => ({ + title, + setTitle, + content, + setContent, + images, + setImages, + address, + setAddress, + mateLimit, + setMateLimit, + expectedMonthlyFee, + setExpectedMonthlyFee, + houseSize, + setHouseSize, + selectedExtraOptions, + setSelectedExtraOptions, + selectedOptions, + setSelectedOptions, + handleOptionClick, + handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, + }), + [ + title, + setTitle, + content, + setContent, + images, + setImages, + address, + setAddress, + mateLimit, + setMateLimit, + expectedMonthlyFee, + setExpectedMonthlyFee, + houseSize, + setHouseSize, + selectedExtraOptions, + setSelectedExtraOptions, + selectedOptions, + setSelectedOptions, + handleOptionClick, + handleExtraOptionClick, + ], + ); +}; + export const useCreateSharedPost = () => useMutation, FailureDTO, CreateSharedPostProps>( { diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 5e0d6ea161..ac9962f968 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -11,6 +11,23 @@ export interface GetSharedPostsProps { page: number; } +export interface SelectedOptions { + budget?: string; + roomType?: string; + livingRoom?: string; + roomCount?: string; + restRoomCount?: string; + floorType?: string; +} + +export type SelectedExtraOptions = Record; + +export interface ImageFile { + url: string; + file: File; + extension: string; +} + export interface CreateSharedPostProps { imageFilesData: Array<{ fileName: string; From 2c0270872d22a9a6ff354c8458548a7d6684b50f Mon Sep 17 00:00:00 2001 From: he2e2 Date: Fri, 19 Apr 2024 16:08:07 +0900 Subject: [PATCH 021/130] fix: Card PUT (#52) --- src/app/pages/setting-page.tsx | 52 ++++--- src/app/pages/user-input-page.tsx | 24 ++-- src/components/UserInputSection.tsx | 100 ++++++++++++++ src/components/card/MajorSelector.tsx | 5 + src/components/card/MbtiToggle.tsx | 52 +++++++ src/components/{ => card}/OptionSection.tsx | 134 +++++++++++++----- src/components/card/Slider.tsx | 46 +++++-- src/components/{ => card}/VitalSection.tsx | 143 ++++++++++++++------ src/components/index.ts | 5 +- 9 files changed, 438 insertions(+), 123 deletions(-) create mode 100644 src/components/UserInputSection.tsx rename src/components/{ => card}/OptionSection.tsx (78%) rename src/components/{ => card}/VitalSection.tsx (77%) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index b0521d9cd1..08fbc87b5a 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -197,35 +197,50 @@ export function SettingPage({ cardId }: { cardId: number }) { }, [features, isMySelf]); const [selectedOptions, setSelectedOptions] = useState({}); + const [initialMbti, setInitialMbti] = useState(''); + const [initialMajor, setInitialMajor] = useState(''); + const [initialBudget, setInitialBudget] = useState(''); const majorArray = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; - const cleanScoreArray = ['상', '평범보통', '천하태평']; useEffect(() => { if (isMySelf) { if (features !== null) { const initialOptions: SelectedOptions = {}; features.slice(2).forEach(option => { - if (option.includes(',')) setBudget(option); - else if (option.includes('~')) setMateAge(option); - else if (option.includes('E') || option.includes('I')) - setMbti(option); - else if (majorArray.includes(option)) setMajor(option); - else if (cleanScoreArray.includes(option)) - initialOptions[option] = true; + if ( + option.includes(',') || + option.includes('±') || + option.includes('E') || + option.includes('I') || + majorArray.includes(option) + ) { + } else initialOptions[option] = true; + + if (option.includes('E') || option.includes('I')) + setInitialMbti(option); + if (majorArray.includes(option)) setInitialMajor(option); + if (option.includes(',')) setInitialBudget(option); }); setSelectedOptions(initialOptions); } } }, [features, isMySelf]); - const [locationInput, setLocation] = useState(card.data?.data.location); + const [locationInput, setLocation] = useState( + card.data?.data.location, + ); + + useEffect(() => { + if (card.data?.data.location) { + setLocation(card.data.data.location); + } + }, [card.data?.data.location]); const [mbti, setMbti] = useState(''); const [major, setMajor] = useState(''); const [budget, setBudget] = useState(''); const [mateAge, setMateAge] = useState(''); - const [cleanScore, setCleanScore] = useState(''); const handleFeatureChange = ( optionName: keyof SelectedState, @@ -252,16 +267,15 @@ export function SettingPage({ cardId }: { cardId: number }) { const saveData = () => { const array = Object.keys(selectedOptions).filter( - key => selectedOptions[key], + key => selectedOptions[key] && key !== '전공' && key !== '엠비티아이', ); const location = locationInput ?? ''; const myFeatures = [ selectedState.smoking, selectedState.room, - mateAge, + mateAge !== '' ? mateAge : undefined, ...array, - ...(cleanScore !== null && cleanScore !== undefined ? [cleanScore] : []), ...(mbti !== null && mbti !== undefined ? [mbti] : []), ...(major !== null && major !== undefined ? [major] : []), budget, @@ -328,9 +342,7 @@ export function SettingPage({ cardId }: { cardId: number }) { - - {card.data?.data.location} - + {locationInput} @@ -352,9 +364,7 @@ export function SettingPage({ cardId }: { cardId: number }) { gender={userData?.gender} birthYear={userData?.birthYear} location={card.data?.data.location} - smoking={features?.[0]} - room={features?.[1]} - mateAge={mateAge} + vitalFeatures={features} onFeatureChange={handleFeatureChange} onLocationChange={setLocation} onMateAgeChange={setMateAge} @@ -363,12 +373,14 @@ export function SettingPage({ cardId }: { cardId: number }) { /> diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 2e7dc24853..dc8c0ee07f 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -311,18 +311,18 @@ export function UserInputPage() { const handleButtonClick = () => { const myOptions = Object.keys(selectedOptions).filter( - key => selectedOptions[key], + key => selectedOptions[key] && key !== '전공' && key !== '엠비티아이', ); const mateOptions = Object.keys(selectedMateOptions).filter( - key => selectedMateOptions[key], + key => selectedMateOptions[key] && key !== '전공' && key !== '엠비티아이', ); const location = locationInput ?? ''; const myFeatures = [ selectedState.smoking, selectedState.room, + mateAge, ...myOptions, - ...(cleanScore !== null && cleanScore !== undefined ? [cleanScore] : []), ...(mbti !== null && mbti !== undefined ? [mbti] : []), ...(major !== null && major !== undefined ? [major] : []), budget, @@ -457,9 +457,7 @@ export function UserInputPage() { gender={user?.gender} birthYear={user?.birthYear} location={undefined} - smoking={undefined} - room={undefined} - mateAge={undefined} + vitalFeatures={null} onFeatureChange={handleFeatureChange} onLocationChange={setLocation} onMateAgeChange={() => {}} @@ -468,12 +466,14 @@ export function UserInputPage() { /> @@ -482,10 +482,8 @@ export function UserInputPage() { {}} isMySelf type="mateCard" /> diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx new file mode 100644 index 0000000000..c42bf84f37 --- /dev/null +++ b/src/components/UserInputSection.tsx @@ -0,0 +1,100 @@ +'use client'; + +import styled from 'styled-components'; + +import { OptionSection } from './card/OptionSection'; +import { VitalSection } from './card/VitalSection'; + +const styles = { + checkContainer: styled.div` + display: flex; + width: 50rem; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + border-radius: 1.875rem; + background: var(--background, #f7f6f9); + `, + horizontalLine: styled.div` + width: 43.75rem; + height: 0.0625rem; + background: var(--Gray-9, #d3d0d7); + `, +}; + +interface SelectedState { + smoking: string | undefined; + room: string | undefined; +} + +interface UserInputProps { + gender: string | undefined; + birthYear: string | undefined; + location: string | undefined; + features: string[] | null; + isMySelf: boolean; // 본인 여부 + type: string; // 카드 타입 + mbti: string | undefined; + major: string | undefined; + budget: string | undefined; + onVitalChange: ( + // 흡연 여부, 방 공유 여부 + optionName: keyof SelectedState, + item: string | number, + ) => void; + onOptionChange: (option: string) => void; // 선택 사항 선택 여부 + onLocationChange: React.Dispatch>; // 희망 지역 + onMateAgeChange: React.Dispatch>; // 메이트 나이 '±age' + onMbtiChange: React.Dispatch>; // mbti + onMajorChange: React.Dispatch>; // major + onBudgetChange: React.Dispatch>; // 금액 'min,max' +} + +export function UserInputSection({ + gender, + birthYear, + location, + features, + isMySelf, + type, + mbti, + major, + budget, + onVitalChange, + onOptionChange, + onLocationChange, + onMateAgeChange, + onMbtiChange, + onMajorChange, + onBudgetChange, +}: UserInputProps) { + return ( + + + + + + ); +} diff --git a/src/components/card/MajorSelector.tsx b/src/components/card/MajorSelector.tsx index 68cc56821a..65446d175c 100644 --- a/src/components/card/MajorSelector.tsx +++ b/src/components/card/MajorSelector.tsx @@ -40,11 +40,16 @@ const styles = { const majorOptions = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; export function MajorSelector({ + major, onChange, }: { + major: string | undefined; onChange: React.Dispatch>; }) { const [selectedMajor, setSelectedMajor] = useState(''); + useEffect(() => { + if (major !== undefined) setSelectedMajor(major); + }, [major]); const handleMajorChange = (event: React.ChangeEvent) => { setSelectedMajor(event.target.value); }; diff --git a/src/components/card/MbtiToggle.tsx b/src/components/card/MbtiToggle.tsx index 3a76b188e4..7f54b04f74 100644 --- a/src/components/card/MbtiToggle.tsx +++ b/src/components/card/MbtiToggle.tsx @@ -116,8 +116,10 @@ function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { } export function MbtiToggle({ + mbti, onChange, }: { + mbti: string | undefined; onChange: React.Dispatch>; }) { const [toggleStates, setToggleStates] = useState({ @@ -128,6 +130,56 @@ export function MbtiToggle({ }); const [result, setResult] = useState(''); + useEffect(() => { + if (mbti !== undefined) setResult(mbti); + }, [mbti]); + + useEffect(() => { + if (mbti !== undefined && mbti.charAt(0) === 'I') { + setToggleStates(prevState => ({ + ...prevState, + toggle1: true, + })); + } else { + setToggleStates(prevState => ({ + ...prevState, + toggle1: false, + })); + } + if (mbti !== undefined && mbti.charAt(1) === 'S') { + setToggleStates(prevState => ({ + ...prevState, + toggle2: true, + })); + } else { + setToggleStates(prevState => ({ + ...prevState, + toggle2: false, + })); + } + if (mbti !== undefined && mbti.charAt(2) === 'T') { + setToggleStates(prevState => ({ + ...prevState, + toggle3: true, + })); + } else { + setToggleStates(prevState => ({ + ...prevState, + toggle3: false, + })); + } + if (mbti !== undefined && mbti.charAt(3) === 'J') { + setToggleStates(prevState => ({ + ...prevState, + toggle4: true, + })); + } else { + setToggleStates(prevState => ({ + ...prevState, + toggle4: false, + })); + } + }, [mbti]); const toggleSwitch = (toggleName: keyof typeof toggleStates) => { setToggleStates(prevState => ({ diff --git a/src/components/OptionSection.tsx b/src/components/card/OptionSection.tsx similarity index 78% rename from src/components/OptionSection.tsx rename to src/components/card/OptionSection.tsx index 2307d837f3..469e54846e 100644 --- a/src/components/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -3,10 +3,10 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { CleanTest } from './card/CleanTest'; -import { MajorSelector } from './card/MajorSelector'; -import { MbtiToggle } from './card/MbtiToggle'; -import { Slider } from './card/Slider'; +import { CleanTest } from './CleanTest'; +import { MajorSelector } from './MajorSelector'; +import { MbtiToggle } from './MbtiToggle'; +import { Slider } from './Slider'; const styles = { container: styled.div` @@ -155,37 +155,48 @@ const PersonalOptions = [ '밖에서 활동', '친구초대 허용', '취미 같이 즐겨요', - '엠비티아이', - '전공', ]; export function OptionSection({ + mbti, + major, + budget, optionFeatures, onFeatureChange, onMbtiChange, onMajorChange, onBudgetChange, - onCleanTestChange, isMySelf, type, }: { + mbti: string | undefined; + major: string | undefined; + budget: string | undefined; optionFeatures: string[] | null; onFeatureChange: (option: string) => void; onMbtiChange: React.Dispatch>; onMajorChange: React.Dispatch>; onBudgetChange: React.Dispatch>; - onCleanTestChange: React.Dispatch>; isMySelf: boolean; type: string; }) { type SelectedOptions = Record; const [selectedOptions, setSelectedOptions] = useState({}); + const majorArray = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; + useEffect(() => { if (optionFeatures !== null) { const initialOptions: SelectedOptions = {}; - optionFeatures.forEach(option => { - initialOptions[option] = true; + optionFeatures.slice(2).forEach(option => { + if ( + option.includes(',') || + option.includes('±') || + option.includes('E') || + option.includes('I') || + majorArray.includes(option) + ) { + } else initialOptions[option] = true; }); setSelectedOptions(initialOptions); } @@ -205,18 +216,44 @@ export function OptionSection({ const toggleTestVisibility = () => { setIsTestVisible(prev => !prev); setIsMajorSelected(false); - setIsTestSelected(true); + setIsTestSelected(!isTestSelected); setIsMbtiSelected(false); }; useEffect(() => { - if (score < 5.34 && score > 0) onCleanTestChange('상'); - if (score > 5.34 && score < 10.67) onCleanTestChange('평범보통'); - if (score > 10.67) onCleanTestChange('천하태평'); + if (score < 5.34 && score > 0) { + if (!selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['평범보통']) handleOptionClick('평범보통'); + if (selectedOptions['천하태평']) handleOptionClick('천하태평'); + } + if (score > 5.34 && score < 10.67) { + if (!selectedOptions['평범보통']) handleOptionClick('평범보통'); + if (selectedOptions['상']) handleOptionClick('상'); + if (selectedOptions['천하태평']) handleOptionClick('천하태평'); + } + if (score > 10.67) { + if (!selectedOptions['천하태평']) handleOptionClick('천하태평'); + if (selectedOptions['평범보통']) handleOptionClick('평범보통'); + if (selectedOptions['상']) handleOptionClick('상'); + } }, [score]); + const [initialMin, setInitialMin] = useState(0); + const [initialMax, setInitialMax] = useState(355); + useEffect(() => { + if (budget !== undefined) { + const [min, max] = budget.split(',').map(Number); + setInitialMin(min); + setInitialMax(max); + } + }, [budget]); + const [budgetMin, setBudgetMin] = useState(0); const [budgetMax, setBudgetMax] = useState(355); + useEffect(() => { + setBudgetMin(initialMin); + setBudgetMax(initialMax); + }, [initialMin, initialMax]); const handleBudgetChange = (min: number, max: number) => { setBudgetMin(min); @@ -228,7 +265,18 @@ export function OptionSection({ const [isMbtiSelected, setIsMbtiSelected] = useState(false); const [selectedMbti, setMbti] = useState(''); + useEffect(() => { + if (mbti !== undefined) { + setMbti(mbti); + } + }, [mbti]); + const [selectedMajor, setMajor] = useState(''); + useEffect(() => { + if (major !== undefined) { + setMajor(major); + } + }, [major]); useEffect(() => { onMbtiChange(selectedMbti); @@ -244,7 +292,7 @@ export function OptionSection({ }, [budgetMin, budgetMax]); const handleMajorSelect = () => { - setIsMajorSelected(true); + setIsMajorSelected(!isMajorSelected); setIsTestSelected(false); setIsMbtiSelected(false); }; @@ -252,7 +300,7 @@ export function OptionSection({ const handleMbtiSelect = () => { setIsMajorSelected(false); setIsTestSelected(false); - setIsMbtiSelected(true); + setIsMbtiSelected(!isMbtiSelected); }; return ( @@ -355,13 +403,13 @@ export function OptionSection({ {isTestVisible ? '결과 확인하기' : '테스트 하기'} - = 0 && score < 5.34}> - 상 - - 5.34 && score < 10.67}> + + 평범보통 - 10.67}>천하태평 + + 천하태평 + ) : ( @@ -422,22 +470,36 @@ export function OptionSection({ if (isMySelf) { handleOptionClick(option); } - if (option === '엠비티아이') { - handleMbtiSelect(); - } - if (option === '전공') { - handleMajorSelect(); - } }} > - {option === '엠비티아이' ? <>MBTI : option} + {option} ))} - {selectedOptions['전공'] && isMajorSelected ? ( - + { + if (isMySelf) { + handleMbtiSelect(); + } + }} + > + MBTI + + { + if (isMySelf) { + handleMajorSelect(); + } + }} + > + 전공 + + {isMajorSelected ? ( + ) : null} - {selectedOptions['엠비티아이'] && isMbtiSelected ? ( - + {isMbtiSelected ? ( + ) : null} @@ -450,14 +512,14 @@ export function OptionSection({ min={0} max={355} step={5} - initialMin={budgetMin} - initialMax={budgetMax} + initialMin={initialMin} + initialMax={initialMax} onChange={handleBudgetChange} /> - {`${budgetMin === 0 ? '0원' : `${budgetMin}만원`}`} ~{' '} - {`${budgetMax === 355 ? '무제한' : `${budgetMax}만원`}`} + {`${budgetMin === 0 ? '0원' : `${budgetMin ?? ''}만원`}`} ~{' '} + {`${budgetMax === 355 ? '무제한' : `${budgetMax ?? ''}만원`}`} diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index e4ef6929aa..1a10e66aee 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; const styles = { @@ -77,6 +77,15 @@ interface FillProps { $right: string; } +interface SliderProps { + min: number; + max: number; + step: number; + initialMin: number; + initialMax: number; + onChange: (minValue: number, maxValue: number) => void; +} + export function Slider({ min, max, @@ -85,26 +94,37 @@ export function Slider({ initialMax, onChange, }: SliderProps) { - const [initialMinValue, setInitialMin] = useState(initialMin); - const [initialMaxValue, setInitialMax] = useState(initialMax); + const inputMinRef = useRef(null); + const inputMaxRef = useRef(null); + const [minValue, setMinValue] = useState(initialMin); + const [maxValue, setMaxValue] = useState(initialMax); useEffect(() => { - setInitialMin(initialMin); - setInitialMax(initialMax); - }, []); - - const [minValue, setMinValue] = useState(initialMinValue); - const [maxValue, setMaxValue] = useState(initialMaxValue); + setMinValue(initialMin); + setMaxValue(initialMax); + }, [initialMin, initialMax]); const handleMinChange = (e: React.ChangeEvent) => { const newLow = Number(e.target.value); - if (newLow > maxValue) setMaxValue(newLow); + const currentMax = inputMaxRef.current + ? Number(inputMaxRef.current.value) + : initialMin; + if (newLow > currentMax) { + setMaxValue(newLow); + inputMaxRef.current && (inputMaxRef.current.value = newLow.toString()); + } setMinValue(newLow); }; const handleMaxChange = (e: React.ChangeEvent) => { const newHigh = Number(e.target.value); - if (newHigh < minValue) setMinValue(newHigh); + const currentMin = inputMinRef.current + ? Number(inputMinRef.current.value) + : initialMax; + if (newHigh < currentMin) { + setMinValue(newHigh); + inputMinRef.current && (inputMinRef.current.value = newHigh.toString()); + } setMaxValue(newHigh); }; @@ -123,18 +143,22 @@ export function Slider({ /> diff --git a/src/components/VitalSection.tsx b/src/components/card/VitalSection.tsx similarity index 77% rename from src/components/VitalSection.tsx rename to src/components/card/VitalSection.tsx index 4d0f391868..0074055132 100644 --- a/src/components/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Slider } from './card/Slider'; +import { Slider } from './Slider'; const styles = { vitalContainer: styled.div` @@ -109,12 +109,67 @@ const styles = { font-weight: 500; line-height: normal; `, + + sliderContainer: styled.div` + width: 25rem; + height: 1.875rem; + position: relative; + `, + sliderTrack: styled.div` + width: 100%; + height: 0.3125rem; + border-radius: 20px; + background: #d9d9d9; + position: absolute; + top: calc(50% - 2px); + `, + sliderFillTrack: styled.div` + width: ${props => props.$fill}; + height: 0.3125rem; + border-radius: 2px; + background: var(--Main-1, #e15637); + position: absolute; + top: calc(50% - 2px); + `, + slider: styled.input` + position: absolute; + width: 100%; + height: 0.3125rem; + border-radius: 1.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: transparent; + top: calc(50% - 2px); + + &:focus { + outline: none; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + pointer-events: all; + width: 1.875rem; + height: 1.875rem; + background-color: #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px #c6c6c6; + cursor: pointer; + position: relative; + z-index: 1; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); + } + `, }; interface CheckItemProps { $isSelected: boolean; } +interface FillProps { + $fill: string; +} + const CheckItem = styled.div` margin-right: 0.5rem; display: flex; @@ -158,9 +213,7 @@ export function VitalSection({ gender, birthYear, location, - smoking, - room, - mateAge, + vitalFeatures, onFeatureChange, onLocationChange, onMateAgeChange, @@ -170,9 +223,7 @@ export function VitalSection({ gender: string | undefined; birthYear: string | undefined; location: string | undefined; - smoking: string | undefined; - room: string | undefined; - mateAge: string | undefined; + vitalFeatures: string[] | null; onFeatureChange: ( optionName: keyof SelectedState, item: string | number, @@ -189,10 +240,10 @@ export function VitalSection({ useEffect(() => { setSelectedState({ ...selectedState, - smoking: smoking, - room: room, + smoking: vitalFeatures?.[0], + room: vitalFeatures?.[1], }); - }, [smoking, room]); + }, [vitalFeatures]); function handleOptionClick( optionName: keyof SelectedState, @@ -205,38 +256,43 @@ export function VitalSection({ onFeatureChange(optionName, item); } - const [locationInput, setLocation] = useState(location); + const [initialLocation, setInitialLocation] = useState(''); + useEffect(() => { + if (location !== undefined && type === 'myCard') { + setInitialLocation(location); + } + }, [location]); + + const [locationInput, setLocation] = useState(''); + useEffect(() => { + setLocation(initialLocation); + }, [initialLocation]); + const handleLocationChange = (event: React.ChangeEvent) => { setLocation(event.target.value); }; - useEffect(() => { onLocationChange(locationInput); }, [locationInput]); - const [initialMateMinAge, setInitialMin] = useState(0); - const [initialMateMaxAge, setInitialMax] = useState(11); - + const [initialAge, setInitialAge] = useState(0); useEffect(() => { - if (mateAge !== undefined) { - const [min, max] = mateAge.split('~'); - setInitialMin(Number(min)); - setInitialMax(Number(max)); - } - }, [mateAge]); + if (vitalFeatures !== null) + setInitialAge(Number(vitalFeatures?.[2].slice(1))); + }, [vitalFeatures?.[2]]); - const [mateMinAge, setMateMinAge] = useState(initialMateMinAge); - const [mateMaxAge, setMateMaxAge] = useState(initialMateMaxAge); + const [ageValue, setAgeValue] = useState(0); + useEffect(() => { + if (initialAge !== undefined) setAgeValue(initialAge); + }, [initialAge]); - const handleAgeChange = (min: number, max: number) => { - setMateMinAge(min); - setMateMaxAge(max); + const handleAgeChange = (e: React.ChangeEvent) => { + setAgeValue(Number(e.target.value)); }; - useEffect(() => { - const ageString = `${mateMinAge}~${mateMaxAge}`; + const ageString = `±${ageValue}`; onMateAgeChange(ageString); - }, [mateMinAge, mateMaxAge]); + }, [ageValue]); return ( @@ -286,9 +342,9 @@ export function VitalSection({ {location} @@ -387,17 +443,20 @@ export function VitalSection({ ) : ( <> - + + + + + - {`${mateMinAge === 0 ? '동갑' : `±${mateMinAge}세`}`} ~{' '} - {`${mateMaxAge === 11 ? '무제한' : `±${mateMaxAge}세`}`} + {`${ageValue === 11 ? '동갑 ~ 무제한' : `동갑 ~ ±${ageValue}세`}`} )} diff --git a/src/components/index.ts b/src/components/index.ts index f48ef934bc..804a32b078 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,10 +2,11 @@ export * from './NavigationBar'; export * from './ToggleSwitch'; export * from './Bookmark'; export * from './CircularProgressBar'; -export * from './OptionSection'; -export * from './VitalSection'; +export * from './card/OptionSection'; +export * from './card/VitalSection'; export * from './FloatingChatting'; export * from './HorizontalDivider'; export * from './CircularProfileImage'; export * from './CircularButton'; export * from './RangeSlider'; +export * from './UserInputSection'; From a082e145f995df0d3426d2f31c1535c1bd824be2 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Fri, 19 Apr 2024 16:42:09 +0900 Subject: [PATCH 022/130] feat: add authorization header (#57) --- src/components/chat/ChattingList.tsx | 1 + src/components/chat/ChattingRoom.tsx | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/chat/ChattingList.tsx b/src/components/chat/ChattingList.tsx index a35fba84ce..62ae87fc0a 100644 --- a/src/components/chat/ChattingList.tsx +++ b/src/components/chat/ChattingList.tsx @@ -14,6 +14,7 @@ const styles = { border-bottom: 1px solid var(--Gray-4, #dfdfdf); background: #fff; cursor: pointer; + overflow-y: auto; `, infoSection: styled.div` display: flex; diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 829caeef25..8f3e73a422 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -7,6 +7,8 @@ import styled from 'styled-components'; import { ReceiverMessage } from './ReceiverMessage'; import { SenderMessage } from './SenderMessage'; +import { useAuthValue } from '@/features/auth'; + const styles = { container: styled.div` position: fixed; @@ -142,11 +144,16 @@ export function ChattingRoom({ const [stompClient, setStompClient] = useState(null); const user = userName; + const auth = useAuthValue(); + useEffect(() => { const initializeChat = async () => { try { const stomp = new Client({ brokerURL: `ws://ec2-13-125-228-9.ap-northeast-2.compute.amazonaws.com:8080/ws`, + connectHeaders: { + Authorization: `Bearer ${auth?.accessToken}`, + }, debug: (str: string) => { console.log(str); }, @@ -173,14 +180,16 @@ export function ChattingRoom({ } }; - void initializeChat(); + if (auth?.accessToken != null) { + void initializeChat(); + } return () => { if (stompClient !== null && stompClient.connected) { void stompClient.deactivate(); } }; - }, []); + }, [auth?.accessToken]); const sendMessage = () => { if (stompClient !== null && stompClient.connected) { @@ -189,7 +198,7 @@ export function ChattingRoom({ stompClient.publish({ destination, body: JSON.stringify({ - roomId: 1, + roomId: roomId, sender: user, message: inputMessage, }), From 7f66a93d242274ca1fbde2307d9d580e89dc973a Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sat, 20 Apr 2024 16:52:49 +0900 Subject: [PATCH 023/130] fix: Seperate UserInputSection Component (#52) --- src/app/pages/setting-page.tsx | 89 +++++++++++++++------------ src/app/pages/user-input-page.tsx | 17 ++++- src/components/UserInputSection.tsx | 25 ++++---- src/components/card/MajorSelector.tsx | 6 +- src/components/card/OptionSection.tsx | 61 +++++++++++++----- src/components/card/Slider.tsx | 28 +++++---- src/components/card/VitalSection.tsx | 41 +++++++++--- 7 files changed, 173 insertions(+), 94 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 08fbc87b5a..a322e34220 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -10,7 +10,7 @@ import Meeting from '../../../public/option-img/meeting_room.svg'; import Person from '../../../public/option-img/person.svg'; import Visibility from '../../../public/option-img/visibility.svg'; -import { VitalSection, OptionSection } from '@/components'; +import { UserInputSection } from '@/components'; import { useProfileData, usePutUserCard, @@ -209,13 +209,14 @@ export function SettingPage({ cardId }: { cardId: number }) { const initialOptions: SelectedOptions = {}; features.slice(2).forEach(option => { if ( - option.includes(',') || - option.includes('±') || - option.includes('E') || - option.includes('I') || - majorArray.includes(option) + !option.includes(',') && + !option.includes('±') && + !option.includes('E') && + !option.includes('I') && + !majorArray.includes(option) ) { - } else initialOptions[option] = true; + initialOptions[option] = true; + } if (option.includes('E') || option.includes('I')) setInitialMbti(option); @@ -232,7 +233,7 @@ export function SettingPage({ cardId }: { cardId: number }) { ); useEffect(() => { - if (card.data?.data.location) { + if (card.data?.data.location !== undefined) { setLocation(card.data.data.location); } }, [card.data?.data.location]); @@ -326,18 +327,38 @@ export function SettingPage({ cardId }: { cardId: number }) { }; }, [handlePopState]); + let ageString; + if (type === 'myCard') { + ageString = `${userData?.birthYear.slice(2)}년생`; + } else { + switch (mateAge) { + case '±0': + ageString = '동갑'; + break; + case '±11': + ageString = '상관없어요'; + break; + default: + ageString = `${mateAge}년생`; + } + } + return ( - 내 카드 > {userData?.name} + + {type === 'myCard' ? `내 카드 > ${userData?.name}` : '메이트 카드'} + - 내카드 + + {type === 'myCard' ? '내카드' : '메이트카드'} + - {userData?.gender === 'MALE' ? '남성' : '여성'} ·{' '} - {userData?.birthYear?.slice(2)}년생 · {selectedState.smoking} + {userData?.gender === 'MALE' ? '남성' : '여성'} · {ageString} ·{' '} + {selectedState.smoking} @@ -359,32 +380,24 @@ export function SettingPage({ cardId }: { cardId: number }) { - - - - - + ); diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index dc8c0ee07f..bfc164f16f 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -307,7 +307,6 @@ export function UserInputPage() { const [mateMajor, setMateMajor] = useState(''); const [mateBudget, setMateBudget] = useState(''); const [mateAge, setMateAge] = useState(''); - const [cleanScore, setCleanScore] = useState(''); const handleButtonClick = () => { const myOptions = Object.keys(selectedOptions).filter( @@ -361,6 +360,19 @@ export function UserInputPage() { const handleMateCardClick = () => { setActiveContainer('mate'); }; + + let ageString; + switch (mateAge) { + case '±0': + ageString = '동갑'; + break; + case '±11': + ageString = '상관없어요'; + break; + default: + ageString = `${mateAge}년생`; + } + return ( @@ -418,8 +430,7 @@ export function UserInputPage() { - {user?.gender === 'MALE' ? '남성' : '여성'} ·{' '} - {user?.birthYear?.slice(2)}년생 ·{' '} + {user?.gender === 'MALE' ? '남성' : '여성'} · {ageString} ·{' '} {selectedMateState.smoking} diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index c42bf84f37..f6322c0003 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -32,35 +32,34 @@ interface UserInputProps { gender: string | undefined; birthYear: string | undefined; location: string | undefined; - features: string[] | null; - isMySelf: boolean; // 본인 여부 - type: string; // 카드 타입 mbti: string | undefined; major: string | undefined; budget: string | undefined; + features: string[] | null; + isMySelf: boolean; + type: string; onVitalChange: ( - // 흡연 여부, 방 공유 여부 optionName: keyof SelectedState, item: string | number, ) => void; - onOptionChange: (option: string) => void; // 선택 사항 선택 여부 - onLocationChange: React.Dispatch>; // 희망 지역 - onMateAgeChange: React.Dispatch>; // 메이트 나이 '±age' - onMbtiChange: React.Dispatch>; // mbti - onMajorChange: React.Dispatch>; // major - onBudgetChange: React.Dispatch>; // 금액 'min,max' + onOptionChange: (option: string) => void; + onLocationChange: React.Dispatch>; + onMateAgeChange: React.Dispatch>; + onMbtiChange: React.Dispatch>; + onMajorChange: React.Dispatch>; + onBudgetChange: React.Dispatch>; } export function UserInputSection({ gender, birthYear, location, - features, - isMySelf, - type, mbti, major, budget, + features, + isMySelf, + type, onVitalChange, onOptionChange, onLocationChange, diff --git a/src/components/card/MajorSelector.tsx b/src/components/card/MajorSelector.tsx index 65446d175c..6104e2b726 100644 --- a/src/components/card/MajorSelector.tsx +++ b/src/components/card/MajorSelector.tsx @@ -62,9 +62,9 @@ export function MajorSelector({ onChange={handleMajorChange} > - {majorOptions.map(major => ( - ))} diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index 469e54846e..0b0fcf9769 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -91,11 +91,21 @@ const styles = { budgetContainer: styled.div` display: flex; align-items: center; - gap: 4rem; + gap: 0.5rem; width: 100%; `, - value: styled.span` - color: #000; + value: styled.div` + display: flex; + width: 6.8rem; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 10px; + border: 2px solid #dfdfdf; + background: #fff; + + color: #888; font-family: 'Noto Sans KR'; font-size: 1rem; @@ -190,13 +200,14 @@ export function OptionSection({ const initialOptions: SelectedOptions = {}; optionFeatures.slice(2).forEach(option => { if ( - option.includes(',') || - option.includes('±') || - option.includes('E') || - option.includes('I') || - majorArray.includes(option) + !option.includes(',') && + !option.includes('±') && + !option.includes('E') && + !option.includes('I') && + !majorArray.includes(option) ) { - } else initialOptions[option] = true; + initialOptions[option] = true; + } }); setSelectedOptions(initialOptions); } @@ -504,10 +515,31 @@ export function OptionSection({ - +
+ + + 금액 + +
- 금액 + {`${budgetMin === 0 ? '0원' : `${budgetMin ?? ''}만원`}`} + + {`${budgetMax === 355 ? '무제한' : `${budgetMax ?? ''}만원`}`} + - - {`${budgetMin === 0 ? '0원' : `${budgetMin ?? ''}만원`}`} ~{' '} - {`${budgetMax === 355 ? '무제한' : `${budgetMax ?? ''}만원`}`} -
diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index 1a10e66aee..7cad3eb552 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -9,7 +9,7 @@ const styles = { align-items: center; `, sliderContainer: styled.div` - width: 25rem; + width: 22rem; height: 1.875rem; position: relative; `, @@ -106,24 +106,28 @@ export function Slider({ const handleMinChange = (e: React.ChangeEvent) => { const newLow = Number(e.target.value); - const currentMax = inputMaxRef.current - ? Number(inputMaxRef.current.value) - : initialMin; + const currentMax = + inputMaxRef.current !== null + ? Number(inputMaxRef.current.value) + : initialMin; if (newLow > currentMax) { setMaxValue(newLow); - inputMaxRef.current && (inputMaxRef.current.value = newLow.toString()); + inputMaxRef.current !== null && + (inputMaxRef.current.value = newLow.toString()); } setMinValue(newLow); }; const handleMaxChange = (e: React.ChangeEvent) => { const newHigh = Number(e.target.value); - const currentMin = inputMinRef.current - ? Number(inputMinRef.current.value) - : initialMax; + const currentMin = + inputMinRef.current !== null + ? Number(inputMinRef.current.value) + : initialMax; if (newHigh < currentMin) { setMinValue(newHigh); - inputMinRef.current && (inputMinRef.current.value = newHigh.toString()); + inputMinRef.current !== null && + (inputMinRef.current.value = newHigh.toString()); } setMaxValue(newHigh); }; @@ -147,8 +151,7 @@ export function Slider({ min={min} max={max} step={step} - value={minValue} - defaultValue={initialMin} + value={minValue ?? min} onChange={handleMinChange} /> diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 0074055132..8710ff6781 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -3,8 +3,6 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { Slider } from './Slider'; - const styles = { vitalContainer: styled.div` display: flex; @@ -101,7 +99,16 @@ const styles = { } `, value: styled.span` - color: #000; + display: flex; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border-radius: 10px; + border: 2px solid #dfdfdf; + background: #fff; + + color: #888; font-family: 'Noto Sans KR'; font-size: 1rem; @@ -111,7 +118,7 @@ const styles = { `, sliderContainer: styled.div` - width: 25rem; + width: 22rem; height: 1.875rem; position: relative; `, @@ -294,6 +301,18 @@ export function VitalSection({ onMateAgeChange(ageString); }, [ageValue]); + let ageValueString; + switch (ageValue) { + case 0: + ageValueString = '동갑'; + break; + case 11: + ageValueString = '상관없어요'; + break; + default: + ageValueString = `±${ageValue}년생`; + } + return ( 필수 @@ -442,7 +461,13 @@ export function VitalSection({ ))} ) : ( - <> +
@@ -455,10 +480,8 @@ export function VitalSection({ onChange={handleAgeChange} /> - - {`${ageValue === 11 ? '동갑 ~ 무제한' : `동갑 ~ ±${ageValue}세`}`} - - + {ageValueString} +
)} From 65bbe1263b689841f820e20346a62f915cccb496 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 20 Apr 2024 18:30:09 +0900 Subject: [PATCH 024/130] =?UTF-8?q?fix:=20GetSharedPostsDTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/shared-post/shared-post.type.ts | 10 +--------- src/features/shared/shared.dto.ts | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index c5ceb6f806..c1e89f5915 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -28,21 +28,13 @@ export interface SharedPostListItem { oldAddress: string; roadAddress: string; detailAddress?: string; - stationName: string; - stationTime: number; - busStopTime: number; - schoolName: string; - schoolTime: number; - convenienceStortTime: number; }; roomType: string; + floorType: string; size: number; numberOfRoom: number; rentalType: string; - price: number; - managementFee: number; expectedPayment: number; - monthlyFee: number; }; isScrapped: boolean; createdAt: Date; diff --git a/src/features/shared/shared.dto.ts b/src/features/shared/shared.dto.ts index 663f7f6d70..65997e9b3e 100644 --- a/src/features/shared/shared.dto.ts +++ b/src/features/shared/shared.dto.ts @@ -5,7 +5,6 @@ import { import { type SuccessBaseDTO } from '@/shared/types'; export interface GetSharedPostsDTO extends SuccessBaseDTO { - message: string; data: { content: SharedPostListItem[]; pageable: { From 64a4da6b893c9c54f3c31adf2a25c874a502cc69 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 20 Apr 2024 18:35:23 +0900 Subject: [PATCH 025/130] =?UTF-8?q?fix:=20GetSharedPostDTO=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/shared-post/shared-post.type.ts | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index c1e89f5915..630dd8c9e5 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -48,6 +48,7 @@ export interface SharedPost { title: string; content: string; roomMateFeatures: string[]; + participants: Array<{ memberId: string; profileImage: string }>; roomImages: Set<{ fileName: string; isThumbnail: boolean; @@ -74,24 +75,27 @@ export interface SharedPost { oldAddress: string; roadAddress: string; detailAddress?: string; - stationName: string; - stationTime: number; - busStopTime: number; - schoolName: string; - schoolTime: number; - convenienceStortTime: number; }; roomType: RoomType; + floorType: string; size: number; numberOfRoom: number; + numberOfBathRoom: number; + hasLivingRoom: boolean; + recruitmentCapacity: number; rentalType: RentalType; - price: number; - managementFee: number; expectedPayment: number; - monthlyFee: number; + extraOption: { + canPark: boolean; + hasAirConditioner: boolean; + hasRefrigerator: boolean; + hasWasher: boolean; + hasTerrace: boolean; + }; }; isScrapped: boolean; scrapCount: number; + viewCount: number; createdAt: Date; createdBy: string; modifiedAt: Date; From 7ce149b577fd9516aeec9d6689d75fd4af1909d2 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 22 Apr 2024 15:03:06 +0900 Subject: [PATCH 026/130] fix: modify asset path --- src/app/pages/setting-page.tsx | 11 +++++------ src/app/pages/user-input-page.tsx | 9 ++++----- tsconfig.json | 3 ++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 2a65186987..663e8df8d6 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -1,15 +1,14 @@ 'use client'; +import Location from '@public/option-img/location_on.svg'; +import Meeting from '@public/option-img/meeting_room.svg'; +import Person from '@public/option-img/person.svg'; +import Visibility from '@public/option-img/visibility.svg'; import { type NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'; import { useSearchParams, useRouter } from 'next/navigation'; import { useEffect, useState, useRef } from 'react'; import styled from 'styled-components'; -import Location from '../../../public/option-img/location_on.svg'; -import Meeting from '../../../public/option-img/meeting_room.svg'; -import Person from '../../../public/option-img/person.svg'; -import Visibility from '../../../public/option-img/visibility.svg'; - import { UserInputSection } from '@/components'; import { useProfileData, @@ -282,7 +281,7 @@ export function SettingPage({ cardId }: { cardId: number }) { budget, ]; - mutate({ location: location, features: myFeatures }); + mutate({ location, features: myFeatures }); }; const handleBeforeUnload = () => { diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 80dbbb2290..29d05d23be 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -4,14 +4,13 @@ import Link from 'next/link'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import Location from '../../../public/option-img/location_on.svg'; -import Meeting from '../../../public/option-img/meeting_room.svg'; -import Person from '../../../public/option-img/person.svg'; -import Visibility from '../../../public/option-img/visibility.svg'; - import { VitalSection, OptionSection } from '@/components'; import { useAuthValue, useUserData } from '@/features/auth'; import { usePutUserCard } from '@/features/profile'; +import Location from '@/public/option-img/location_on.svg'; +import Meeting from '@/public/option-img/meeting_room.svg'; +import Person from '@/public/option-img/person.svg'; +import Visibility from '@/public/option-img/visibility.svg'; const styles = { pageContainer: styled.div` diff --git a/tsconfig.json b/tsconfig.json index debad160d3..3a2134b3bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@/public/*": ["./public/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], From 738ecae65a2f283926fdfdd8ee0847a87dcf8da2 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 22 Apr 2024 15:04:55 +0900 Subject: [PATCH 027/130] fix: fix build error --- src/app/pages/setting-page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 663e8df8d6..46fe6c202f 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -1,9 +1,5 @@ 'use client'; -import Location from '@public/option-img/location_on.svg'; -import Meeting from '@public/option-img/meeting_room.svg'; -import Person from '@public/option-img/person.svg'; -import Visibility from '@public/option-img/visibility.svg'; import { type NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'; import { useSearchParams, useRouter } from 'next/navigation'; import { useEffect, useState, useRef } from 'react'; @@ -15,6 +11,10 @@ import { usePutUserCard, useUserCard, } from '@/features/profile'; +import Location from '@/public/option-img/location_on.svg'; +import Meeting from '@/public/option-img/meeting_room.svg'; +import Person from '@/public/option-img/person.svg'; +import Visibility from '@/public/option-img/visibility.svg'; const styles = { pageContainer: styled.div` From 290503daf791f8f802e4e603560ed4c0e27d3751 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 22 Apr 2024 16:45:35 +0900 Subject: [PATCH 028/130] feat: add ChatMenu Component (#57) --- public/Close.svg | 5 + public/backward-arrow.png | Bin 0 -> 233 bytes src/components/FloatingChatting.tsx | 70 +++++++++--- src/components/chat/ChatMenu.tsx | 165 +++++++++++++++++++++++++++ src/components/chat/ChattingList.tsx | 4 +- src/components/chat/ChattingRoom.tsx | 54 ++++----- 6 files changed, 257 insertions(+), 41 deletions(-) create mode 100644 public/Close.svg create mode 100644 public/backward-arrow.png create mode 100644 src/components/chat/ChatMenu.tsx diff --git a/public/Close.svg b/public/Close.svg new file mode 100644 index 0000000000..219bd0c26b --- /dev/null +++ b/public/Close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/backward-arrow.png b/public/backward-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..ed27d86cd69f73e7dfc5dc396c096c19fed5fd61 GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFmmtT}V`<;yxP?2eXHg P)W+cH>gTe~DWM4fMU_2c literal 0 HcmV?d00001 diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 763352ee00..8676daa0f3 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -52,9 +52,10 @@ const styles = { `, chattingHeader: styled.div` display: inline-flex; + justify-content: space-between; align-items: center; - padding-left: 1rem; - gap: 0.3rem; + padding: 0 1rem; + gap: 1rem; width: 100%; height: 3.25rem; flex-shrink: 0; @@ -75,10 +76,36 @@ const styles = { flex-direction: column; overflow-y: hidden; `, + searchButton: styled.img` + width: 1.2rem; + height: 1.2rem; + cursor: pointer; + `, + searchInput: styled.input` + flex: 1; + font-size: 1.25rem; + padding: 0.8rem; + height: 2rem; + width: 8rem; + background-color: transparent; + border: #bdbdbd solid 1px; + border-radius: 1.2rem; + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + + &:focus { + outline: none; + } + `, }; export function FloatingChatting() { const [isChatOpen, setIsChatOpen] = useState(false); + const [isChatRoomOpen, setIsChatRoomOpen] = useState(false); const toggleChat = () => { setIsChatOpen(prevState => !prevState); @@ -92,6 +119,10 @@ export function FloatingChatting() { if (data !== undefined) setName(data.name); }, [data]); + const handleChatRoomClick = () => { + setIsChatRoomOpen(prev => !prev); + }; + return ( <> @@ -100,22 +131,35 @@ export function FloatingChatting() { {isChatOpen && ( - - maru{' '} - - - chat - +
+ + maru{' '} + + + chat + +
+
+ + +
- - - - + + +
)} - + {isChatRoomOpen && ( + + )} ); } diff --git a/src/components/chat/ChatMenu.tsx b/src/components/chat/ChatMenu.tsx new file mode 100644 index 0000000000..4c21461076 --- /dev/null +++ b/src/components/chat/ChatMenu.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useState } from 'react'; +import styled from 'styled-components'; + +const styles = { + menuContainer: styled.div` + position: absolute; + top: 0; + right: 0; + display: inline-flex; + flex-direction: column; + width: 18rem; + height: 100%; + border-radius: 20px; + background: #fff; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + z-index: 20; + `, + header: styled.div` + width: 100%; + height: 3.625rem; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.8rem; + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + `, + closeButton: styled.img` + width: 1.2rem; + height: 1.2rem; + border: none; + flex-shrink: 0; + cursor: pointer; + `, + menuListContainer: styled.ul` + display: flex; + flex: 1; + flex-direction: column; + padding: 0.8rem 0.8rem; + width: 100%; + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + `, + footer: styled.div` + width: 100%; + height: 3rem; + display: flex; + justify-content: center; + align-items: center; + padding: 0 0.8rem; + `, + menuList: styled.li` + width: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.8rem 0.8rem; + gap: 0.8rem; + border-bottom: #e5e5ea solid 1px; + cursor: pointer; + + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; /* 128.571% */ + `, + userListContainer: styled.ul` + display: flex; + flex: 1; + flex-direction: column; + padding: 0 0.8rem; + width: 100%; + gap: 1rem; + `, + userList: styled.li` + display: flex; + gap: 0.4rem; + align-items: center; + list-style: none; + color: var(--Text-gray, #666668); + cursor: pointer; + `, + userImg: styled.div` + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + border-radius: 150px; + border: 1.5px solid #fff; + background: url('__avatar_url.png') lightgray 50% / cover no-repeat; + `, + searchInput: styled.input` + flex: 1; + font-size: 1.25rem; + padding: 0.8rem; + border: none; + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + + &:focus { + outline: none; + } + `, + searchButton: styled.img` + width: 1.2rem; + height: 1.2rem; + cursor: pointer; + `, +}; + +export function ChatMenu({ + onMenuClicked, +}: { + onMenuClicked: React.Dispatch>; +}) { + const [isCloseClick, setIsCloseClick] = useState(false); + + const handleCloseClick = () => { + setIsCloseClick(prev => !prev); + onMenuClicked(isCloseClick); + }; + + return ( + + + + + + + 하우스 메이트 + + + + 김마루 + + + + 김마루 + + + + 김마루 + + + + 김마루 + + + + 메이트 초대하기 + 마이 마루 + 채팅방 나가기 + + + + + + + ); +} diff --git a/src/components/chat/ChattingList.tsx b/src/components/chat/ChattingList.tsx index 62ae87fc0a..60d5d10022 100644 --- a/src/components/chat/ChattingList.tsx +++ b/src/components/chat/ChattingList.tsx @@ -86,9 +86,9 @@ const styles = { `, }; -export function ChattingList() { +export function ChattingList({ onClick }: { onClick: () => void }) { return ( - + diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 8f3e73a422..19c5d85a2f 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -4,6 +4,7 @@ import { Client } from '@stomp/stompjs'; import { useEffect, useState } from 'react'; import styled from 'styled-components'; +import { ChatMenu } from './ChatMenu'; import { ReceiverMessage } from './ReceiverMessage'; import { SenderMessage } from './SenderMessage'; @@ -12,8 +13,8 @@ import { useAuthValue } from '@/features/auth'; const styles = { container: styled.div` position: fixed; - bottom: 5rem; - left: 6.5rem; + bottom: 6rem; + right: 6.5rem; display: flex; width: 25rem; height: 35rem; @@ -37,22 +38,6 @@ const styles = { padding: 0 0.8rem; box-shadow: 0px -1px 0px 0px #e5e5ea inset; `, - users: styled.div` - display: inline-flex; - width: 2rem; - align-items: center; - flex-shrink: 0; - position: relative; - `, - user: styled.div` - position: absolute; - width: 1.5rem; - height: 1.5rem; - flex-shrink: 0; - border-radius: 150px; - border: 1.5px solid #fff; - background: url('__avatar_url.png') lightgray 50% / cover no-repeat; - `, roomInfo: styled.div` display: flex; flex-direction: column; @@ -79,6 +64,7 @@ const styles = { height: 1rem; flex-shrink: 0; background: url('kebab-horizontal.svg') no-repeat; + cursor: pointer; `, messageContainer: styled.div` display: flex; @@ -89,7 +75,6 @@ const styles = { height: calc(100% - 7.5rem); box-shadow: 0px -1px 0px 0px #e5e5ea inset; position: relative; - overflow-y: auto; `, senderFrame: styled.div` display: flex; @@ -124,6 +109,11 @@ const styles = { outline: none; } `, + backButton: styled.img` + width: 1rem; + height: 1rem; + cursor: pointer; + `, }; interface Content { @@ -135,14 +125,18 @@ interface Content { export function ChattingRoom({ userName, roomId, + onRoomClick, }: { userName: string | undefined; roomId: number; + onRoomClick: React.Dispatch>; }) { const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [stompClient, setStompClient] = useState(null); const user = userName; + const [isMenuClick, setIsMenuClick] = useState(false); + const [isBackClick, setIsBackClick] = useState(false); const auth = useAuthValue(); @@ -208,6 +202,15 @@ export function ChattingRoom({ setInputMessage(''); }; + const handleMenuClick = () => { + setIsMenuClick(prev => !prev); + }; + + const handleBackClick = () => { + setIsBackClick(prev => !prev); + onRoomClick(isBackClick); + }; + function handleKeyDown(event: React.KeyboardEvent) { if (event.keyCode === 13) { sendMessage(); @@ -217,17 +220,16 @@ export function ChattingRoom({ return ( - - - - - - + 정릉 기숙사 405호 45분전 - + + {isMenuClick && } {messages.map((message, index) => ( From d6171b4e36402016453749e9a0ae00536c9e8019 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Thu, 25 Apr 2024 13:31:36 +0900 Subject: [PATCH 029/130] feat: add chat-api (#57) --- src/components/FloatingChatting.tsx | 1 + src/features/chat/chat.api.ts | 46 +++++++++++++++++++++++++++-- src/features/chat/chat.dto.ts | 15 ++++++++++ src/features/chat/chat.hook.ts | 45 ++++++++++++++++++++++++---- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 8676daa0f3..1f8e2c73f3 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -109,6 +109,7 @@ export function FloatingChatting() { const toggleChat = () => { setIsChatOpen(prevState => !prevState); + if (isChatRoomOpen) setIsChatRoomOpen(false); }; const auth = useAuthValue(); diff --git a/src/features/chat/chat.api.ts b/src/features/chat/chat.api.ts index 628b27ae03..05a83bef78 100644 --- a/src/features/chat/chat.api.ts +++ b/src/features/chat/chat.api.ts @@ -1,8 +1,48 @@ import axios from 'axios'; -import { type GetChatRoomDTO } from './chat.dto'; +import { type GetChatRoomUserDTO, type PostChatRoomDTO } from './chat.dto'; -export const getChatRoom = async (roomName: string) => +export const getChatRoomList = async () => + await axios.get(`/maru-api/chatRoom`).then(res => { + console.log(res.data); + return res.data; + }); + +export const postChatRoom = async (roomName: string, members: string[]) => { await axios - .get(`/maru-api/chatRoom/${roomName}`) + .post(`/maru-api/chatRoom`, { + roomName: roomName, + members: members, + }) .then(res => res.data); +}; + +export const postInviteUser = async (roomId: number, members: string[]) => { + await axios + .post(`/chatRoom/${roomId}/invite`, { + members: members, + }) + .then(res => res.data); +}; + +export const postEnterChatRoom = async ( + roomId: number, + page: number, + size: number, +) => { + await axios + .post(`/chatRoom/chat`, { + roomId: roomId, + page: page, + size: size, + }) + .then(res => res.data); +}; + +export const getChatRoomUser = async (roomId: number) => + await axios + .get(`/maru-api/chatRoom/${roomId}`) + .then(res => { + console.log(res.data); + return res.data; + }); diff --git a/src/features/chat/chat.dto.ts b/src/features/chat/chat.dto.ts index 5486787d6e..091f4fe054 100644 --- a/src/features/chat/chat.dto.ts +++ b/src/features/chat/chat.dto.ts @@ -6,3 +6,18 @@ export interface GetChatRoomDTO extends SuccessBaseDTO { name: string; }; } + +export interface PostChatRoomDTO extends SuccessBaseDTO { + data: { + id: number; + name: string; + }; +} + +export interface GetChatRoomUserDTO extends SuccessBaseDTO { + data: { + memberId: string; + nickname: string; + profileImageUrl: string; + }; +} diff --git a/src/features/chat/chat.hook.ts b/src/features/chat/chat.hook.ts index 9cba08e70f..80e2930526 100644 --- a/src/features/chat/chat.hook.ts +++ b/src/features/chat/chat.hook.ts @@ -1,10 +1,43 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; -import { getChatRoom } from './chat.api'; +import { + getChatRoomList, + getChatRoomUser, + postChatRoom, + postEnterChatRoom, + postInviteUser, +} from './chat.api'; -export const useChatRoomData = (roomName: string) => +export const useChatRoomList = () => useQuery({ - queryKey: [`/api/chatRoom/${roomName}`], - queryFn: async () => await getChatRoom(roomName), - enabled: roomName !== undefined, + queryKey: [`/api/chatRoom`], + queryFn: getChatRoomList, + }); + +export const useCreateChatRoom = (roomName: string, members: string[]) => + useMutation({ + mutationFn: async () => { + await postChatRoom(roomName, members); + }, + }); + +export const useInviteUsers = (roomId: number, members: string[]) => + useMutation({ + mutationFn: async () => { + await postInviteUser(roomId, members); + }, + }); + +export const useEnterChatRoom = (roomId: number, page: number, size: number) => + useMutation({ + mutationFn: async () => { + await postEnterChatRoom(roomId, page, size); + }, + }); + +export const useChatRoomUser = (roomId: number) => + useQuery({ + queryKey: [`/api/chatRoom/${roomId}`], + queryFn: async () => await getChatRoomUser(roomId), + enabled: roomId !== undefined, }); From 0d5e492fd71a31ca849762a65e903780e253dd7e Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 27 Apr 2024 22:02:03 +0900 Subject: [PATCH 030/130] feat: apply the changed design (#62) --- src/app/pages/writing-post-page.tsx | 546 +++++++++++++++++----------- 1 file changed, 332 insertions(+), 214 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 56ecb6e89e..d147fc1d6b 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -39,6 +39,73 @@ const styles = { border-radius: 16px; background: #fff; `, + essentialInfoContainer: styled.div` + display: flex; + flex: 1 0 0; + width: 100%; + flex-direction: column; + gap: 1rem; + `, + essentialRow: styled.div` + display: flex; + width: 100%; + flex-direction: row; + gap: 1rem; + + .column { + display: flex; + flex-direction: column; + gap: 1rem; + flex: 1 0 0; + } + `, + mateCardContainer: styled.div` + display: flex; + width: 100%; + flex-direction: column; + gap: 1rem; + + button { + all: unset; + cursor: pointer; + + display: flex; + width: fit-content; + padding: 0.5rem 1rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + + border-radius: 0.5rem; + background: #ededed; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + button[class~='edit'] { + display: flex; + width: fit-content; + padding: 0.5rem 1rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + + border-radius: 0.5rem; + background: #e15637; + + color: #eee; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + `, row: styled.div` display: flex; justify-content: space-between; @@ -56,9 +123,35 @@ const styles = { `, captionRow: styled.div` display: flex; + flex-direction: row; align-items: flex-end; gap: 1rem; align-self: stretch; + + .caption { + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + dealInfoContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + `, + roomInfoContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + align-self: stretch; `, caption: styled.span` color: rgba(53, 55, 58, 0.5); @@ -118,10 +211,11 @@ const styles = { border-radius: 8px; background: #ededed; `, - contentInput: styled.input` + contentInput: styled.textarea` all: unset; display: flex; + height: 100%; padding: 1rem; flex-direction: column; justify-content: center; @@ -351,7 +445,7 @@ export function WritingPostPage() { }; const handleContentInputChanged = ( - event: React.ChangeEvent, + event: React.ChangeEvent, ) => { setContent(event.target.value); }; @@ -505,225 +599,249 @@ export function WritingPostPage() { return ( - - 기본 정보 - - 작성하기 - - - 제목 - - 상세 정보 - - - 사진 - 최소 2장 이상 업로드 - - - {images.map(image => ( - + + 기본 정보 + + 작성하기 + + + 제목 + + 위치 정보 + + { - handleRemoveImage(image); + setShowLocationSearchBox(true); + }} + > + + 위치 찾기 + + 상세 주소: + + {address?.roadAddress ?? '주소를 입력해주세요.'} + + + {showLocationSearchBox && ( + { + setAddress(selectedAddress); }} - /> - ))} - - - - - 인원 - - { - handleNumberInput(event.target.value, value => { - setMateLimit(value); - }); - }} - $width={3} - /> - - - 메이트 - - { - setShowMateSearchBox(true); - }} - /> - {showMateSearchBox && ( - { - setShowMateSearchBox(false); + setShowLocationSearchBox(false); }} /> )} - - 거래 정보 - 거래 방식 - - {DealOptions.map(option => ( - - { - handleOptionClick('budget', option); - }} - /> - {option} - - ))} - - 희망 메이트 월 분담금 - - { - handleNumberInput(event.target.value, value => { - setExpectedMonthlyFee(value); - }); - }} - $width={3} - /> - 만원 - - 방 정보 - - - {FloorOptions.map(option => ( - - { - handleOptionClick('floorType', option); - }} - /> - {option} - - ))} - - 추가 옵션 - - {AdditionalOptions.map(option => ( - - { - handleExtraOptionClick(option); - }} - /> - {option} - - ))} - - 방 종류 - - {RoomOptions.map(option => ( - - { - handleOptionClick('roomType', option); - }} - /> - {option} - - ))} - - 거실 - - {LivingRoomOptions.map(option => ( - - { - handleOptionClick('livingRoom', option); - }} - /> - {option} - - ))} - - 방 개수 - - {RoomCountOptions.map(option => ( - - { - handleOptionClick('roomCount', option); - }} + +
+ 상세 정보 + - {option} - - ))} - - 화장실 개수 - - {RestRoomCountOptions.map(option => ( - - { - handleOptionClick('restRoomCount', option); - }} - /> - {option} - - ))} - - 전체 면적 - - { - handleNumberInput(event.target.value, value => { - setHouseSize(value); - }); - }} - $width={2} - /> - - - 위치 정보 - - 상세 주소 - {address?.roadAddress} - - { - setShowLocationSearchBox(true); - }} - > - - 위치 찾기 - - {showLocationSearchBox && ( - { - setAddress(selectedAddress); - }} - setHidden={() => { - setShowLocationSearchBox(false); - }} - /> - )} +
+
+ + 사진 + 최소 2장 이상 업로드 + + + {images.map(image => ( + { + handleRemoveImage(image); + }} + /> + ))} + + + + +
+
+ +
+ 모집 할 인원 + + { + handleNumberInput(event.target.value, value => { + setMateLimit(value); + }); + }} + $width={3} + /> + + +
+
+ 메이트 + + { + setShowMateSearchBox(true); + }} + /> + {showMateSearchBox && ( + { + setShowMateSearchBox(false); + }} + /> + )} + +
+
+ + 메이트 카드 + + + + + 거래 정보 + 거래 방식 + + {DealOptions.map(option => ( + + { + handleOptionClick('budget', option); + }} + /> + {option} + + ))} + + 희망 메이트 월 분담금 + + { + handleNumberInput(event.target.value, value => { + setExpectedMonthlyFee(value); + }); + }} + $width={3} + /> + 만원 + + + + 방 정보 + + + {FloorOptions.map(option => ( + + { + handleOptionClick('floorType', option); + }} + /> + {option} + + ))} + + 추가 옵션 + + {AdditionalOptions.map(option => ( + + { + handleExtraOptionClick(option); + }} + /> + {option} + + ))} + + 방 종류 + + {RoomOptions.map(option => ( + + { + handleOptionClick('roomType', option); + }} + /> + {option} + + ))} + + 거실 + + {LivingRoomOptions.map(option => ( + + { + handleOptionClick('livingRoom', option); + }} + /> + {option} + + ))} + + 방 개수 + + {RoomCountOptions.map(option => ( + + { + handleOptionClick('roomCount', option); + }} + /> + {option} + + ))} + + 화장실 개수 + + {RestRoomCountOptions.map(option => ( + + { + handleOptionClick('restRoomCount', option); + }} + /> + {option} + + ))} + + 전체 면적 + + { + handleNumberInput(event.target.value, value => { + setHouseSize(value); + }); + }} + $width={2} + /> + + +
); From ce114edc6513d76a53509489ff5727717bad08c4 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 27 Apr 2024 22:04:03 +0900 Subject: [PATCH 031/130] =?UTF-8?q?fix:=20change=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20span=20tag=20className?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/writing-post-page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index d147fc1d6b..578af9a911 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -620,9 +620,11 @@ export function WritingPostPage() { }} > - 위치 찾기 + 위치 찾기 - 상세 주소: + + 상세 주소: + {address?.roadAddress ?? '주소를 입력해주세요.'} From caf39fd72c31eff74f1539cdc8e9e75ddbc292ec Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 27 Apr 2024 22:09:06 +0900 Subject: [PATCH 032/130] fix: add missing dependency array in useCreateSharedPostProps --- src/features/shared/shared.hook.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index b59fb0abbc..55c8ca1933 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -175,6 +175,8 @@ export const useCreateSharedPostProps = () => { setSelectedOptions, handleOptionClick, handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, ], ); }; From 0d1869ecef7f96977ed60572f44fb47ae744c211 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sun, 28 Apr 2024 15:52:51 +0900 Subject: [PATCH 033/130] feat: add enter chatting, invite user, get chatting user (#57) --- src/app/pages/shared-post-page.tsx | 38 +++++++- src/components/FloatingChatting.tsx | 82 ++++++++++++++-- src/components/chat/ChatMenu.tsx | 140 ++++++++++++++++++++++----- src/components/chat/ChattingList.tsx | 18 +++- src/components/chat/ChattingRoom.tsx | 42 ++++++-- src/features/chat/chat.api.ts | 40 +++++--- src/features/chat/chat.dto.ts | 37 +++++-- src/features/chat/chat.hook.ts | 12 +-- src/features/profile/profile.hook.ts | 4 +- 9 files changed, 335 insertions(+), 78 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index 5cf03fddb5..99356d4a9c 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -5,7 +5,9 @@ import styled from 'styled-components'; import { Bookmark, CircularProfileImage } from '@/components'; import { ImageGrid } from '@/components/shared-post-page'; -import { useAuthValue } from '@/features/auth'; +import { useAuthValue, useUserData } from '@/features/auth'; +import { useCreateChatRoom } from '@/features/chat'; +import { useFollowData } from '@/features/profile'; import { useScrapSharedPost, useSharedPost } from '@/features/shared'; import { getAge } from '@/shared'; @@ -385,6 +387,15 @@ export function SharedPostPage({ postId }: { postId: number }) { enabled: auth?.accessToken !== undefined, }); + const { data: userData } = useUserData(auth?.accessToken !== undefined); + const [userId, setUserId] = useState(''); + + useEffect(() => { + if (userData !== undefined) { + setUserId(userData.memberId); + } + }, [userData]); + const { mutate: scrapPost } = useScrapSharedPost(); useEffect(() => { @@ -398,6 +409,19 @@ export function SharedPostPage({ postId }: { postId: number }) { ); }, []); + const [roomName, setRoomName] = useState(''); + + useEffect(() => { + if (sharedPost !== undefined) { + setRoomName(sharedPost.data.publisherAccount.nickname); + } + }, [sharedPost]); + + const members = [userId]; + const { mutate: chattingMutate } = useCreateChatRoom(roomName, members); + + const { mutate: followingMutate } = useFollowData(userId); + return ( @@ -512,14 +536,22 @@ export function SharedPostPage({ postId }: { postId: number }) { - 채팅하기 + { + chattingMutate(); + }} + > + 채팅하기 +
프로필 보기 {}} + onToggle={() => { + followingMutate(); + }} />
diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 1f8e2c73f3..bbdc41958a 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -7,6 +7,11 @@ import { ChattingList } from './chat/ChattingList'; import { ChattingRoom } from './chat/ChattingRoom'; import { useAuthValue, useUserData } from '@/features/auth'; +import { + useChatRoomList, + useCreateChatRoom, + useEnterChatRoom, +} from '@/features/chat'; const styles = { chattingButton: styled.div` @@ -74,7 +79,7 @@ const styles = { height: calc(100% - 3.25rem); display: flex; flex-direction: column; - overflow-y: hidden; + overflow-y: auto; `, searchButton: styled.img` width: 1.2rem; @@ -103,9 +108,30 @@ const styles = { `, }; +interface ChatRoom { + roomId: number; + roomName: string; + unreadCount: number; + lastMessage: string; + lastMessageTime: string; +} + export function FloatingChatting() { const [isChatOpen, setIsChatOpen] = useState(false); const [isChatRoomOpen, setIsChatRoomOpen] = useState(false); + const [chatRooms, setChatRooms] = useState([]); + const [selectedRoomId, setSelectedRoomId] = useState(0); + const [roomData, setRoomData] = useState< + | [ + { + messageId: string; + sender: string; + message: string; + createdAt: string; + }, + ] + | undefined + >(); const toggleChat = () => { setIsChatOpen(prevState => !prevState); @@ -114,16 +140,38 @@ export function FloatingChatting() { const auth = useAuthValue(); const { data } = useUserData(auth?.accessToken !== undefined); - const [name, setName] = useState(''); + const [userId, setUserId] = useState(''); useEffect(() => { - if (data !== undefined) setName(data.name); + if (data !== undefined) setUserId(data.memberId); }, [data]); + const page = 0; + const size = 2; + const { mutate: enterChatting, data: chatRoomData } = useEnterChatRoom( + selectedRoomId, + page, + size, + ); + const handleChatRoomClick = () => { + enterChatting(); + setRoomData(chatRoomData?.data); setIsChatRoomOpen(prev => !prev); }; + const chatRoomList = useChatRoomList(auth?.accessToken); + useEffect(() => { + if (chatRoomList.data !== undefined) { + const chatRoomListData: ChatRoom[] = chatRoomList.data.data; + setChatRooms(chatRoomListData); + } + }, [chatRoomList.data]); + + const roomName = 'test2'; + const members = ['naver_htT4VdDRPKqGqKpnncpa71HCA4CVg5LdRC1cWZhCnF8']; + const { mutate: chattingMutate } = useCreateChatRoom(roomName, members); + return ( <> @@ -147,17 +195,35 @@ export function FloatingChatting() { + - - - + {chatRooms.map((room, index) => ( + { + handleChatRoomClick(); + setSelectedRoomId(room.roomId); + }} + /> + ))}
)} {isChatRoomOpen && ( )} diff --git a/src/components/chat/ChatMenu.tsx b/src/components/chat/ChatMenu.tsx index 4c21461076..bce5a40abe 100644 --- a/src/components/chat/ChatMenu.tsx +++ b/src/components/chat/ChatMenu.tsx @@ -1,8 +1,11 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import styled from 'styled-components'; +// import { useFollowingListData } from '@/features/profile'; +import { useChatRoomUser } from '@/features/chat'; + const styles = { menuContainer: styled.div` position: absolute; @@ -65,6 +68,8 @@ const styles = { font-style: normal; font-weight: 400; line-height: 1.125rem; /* 128.571% */ + + position: relative; `, userListContainer: styled.ul` display: flex; @@ -82,25 +87,24 @@ const styles = { color: var(--Text-gray, #666668); cursor: pointer; `, - userImg: styled.div` + userImg: styled.img` width: 1.5rem; height: 1.5rem; flex-shrink: 0; border-radius: 150px; border: 1.5px solid #fff; - background: url('__avatar_url.png') lightgray 50% / cover no-repeat; `, searchInput: styled.input` flex: 1; font-size: 1.25rem; - padding: 0.8rem; + padding: 0 0.8rem; border: none; color: var(--Text-grayDark, #2c2c2e); font-family: 'Noto Sans KR'; font-size: 0.875rem; font-style: normal; font-weight: 400; - line-height: 1.125rem; + line-height: normal; &:focus { outline: none; @@ -111,20 +115,85 @@ const styles = { height: 1.2rem; cursor: pointer; `, + dropDownContainer: styled.div` + width: 100%; + overflow: hidden; + z-index: 1; + `, + followingListContainer: styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 0.625rem; + padding: 1rem 1.5rem; + border-radius: 10px; + background: #fff; + border: 1px solid #e5e5ea; + @keyframes dropdown { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(0); + } + } + animation: dropdown 0.5s ease; + `, + followingUserContainer: styled.div` + display: flex; + padding: 0.625rem; + justify-content: center; + align-items: center; + gap: 0.625rem; + align-self: stretch; + `, + searchBox: styled.div` + display: flex; + height: 1.6875rem; + justify-content: space-between; + align-items: center; + flex: 1 0 0; + border-radius: 1.25rem; + border: 1px solid #888; + padding: 0.1rem 0.8rem; + `, }; +interface User { + memberId: string; + nickname: string; + profileImageUrl: string; +} + export function ChatMenu({ + roomId, onMenuClicked, }: { + roomId: number; onMenuClicked: React.Dispatch>; }) { const [isCloseClick, setIsCloseClick] = useState(false); + const [isInviteClick, setIsInviteClick] = useState(false); + const users = useChatRoomUser(roomId); + const [userList, setUserList] = useState([]); + // const folloingUsers = useFollowingListData(); + + useEffect(() => { + if (users.data !== undefined) { + const userListData: User[] = users.data.data; + setUserList(userListData); + } + }, [users.data]); const handleCloseClick = () => { setIsCloseClick(prev => !prev); onMenuClicked(isCloseClick); }; + // const { mutate: inviteUser } = useInviteUsers(roomId, [ + // 'naver_htT4VdDRPKqGqKpnncpa71HCA4CVg5LdRC1cWZhCnF8', + // ]); + return ( @@ -134,25 +203,52 @@ export function ChatMenu({ 하우스 메이트 - - - 김마루 - - - - 김마루 - - - - 김마루 - - - - 김마루 - + {userList.map((user, index) => ( + + + {user.nickname} + + ))} - 메이트 초대하기 + + + {isInviteClick && ( + + + + + + + + + + 김마루 + + + + 김마루 + + + + 김마루 + + + + + )} + 마이 마루 채팅방 나가기 diff --git a/src/components/chat/ChattingList.tsx b/src/components/chat/ChattingList.tsx index 60d5d10022..1e87c3b82e 100644 --- a/src/components/chat/ChattingList.tsx +++ b/src/components/chat/ChattingList.tsx @@ -86,18 +86,28 @@ const styles = { `, }; -export function ChattingList({ onClick }: { onClick: () => void }) { +export function ChattingList({ + name, + unreadCount, + lastMessage, + onClick, +}: { + name: string; + unreadCount: number; + lastMessage: string; + onClick: () => void; +}) { return ( - room1 - hi + {name} + {lastMessage} - 2 + {unreadCount} ); } diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 19c5d85a2f..bfe660dfcc 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -123,18 +123,29 @@ interface Content { } export function ChattingRoom({ - userName, + chatRoomData, + userId, roomId, onRoomClick, }: { - userName: string | undefined; + chatRoomData: + | [ + { + messageId: string; + sender: string; + message: string; + createdAt: string; + }, + ] + | undefined; + userId: string | undefined; roomId: number; onRoomClick: React.Dispatch>; }) { const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [stompClient, setStompClient] = useState(null); - const user = userName; + const user = userId; const [isMenuClick, setIsMenuClick] = useState(false); const [isBackClick, setIsBackClick] = useState(false); @@ -144,7 +155,7 @@ export function ChattingRoom({ const initializeChat = async () => { try { const stomp = new Client({ - brokerURL: `ws://ec2-13-125-228-9.ap-northeast-2.compute.amazonaws.com:8080/ws`, + brokerURL: `ws://ec2-3-35-138-168.ap-northeast-2.compute.amazonaws.com:8080/ws`, connectHeaders: { Authorization: `Bearer ${auth?.accessToken}`, }, @@ -160,7 +171,7 @@ export function ChattingRoom({ stomp.onConnect = () => { console.log('WebSocket 연결이 열렸습니다.'); - stomp.subscribe(`/room/1`, frame => { + stomp.subscribe(`/room/${roomId}`, frame => { try { const parsedMessage = JSON.parse(frame.body); setMessages(prevMessages => [...prevMessages, parsedMessage]); @@ -187,7 +198,7 @@ export function ChattingRoom({ const sendMessage = () => { if (stompClient !== null && stompClient.connected) { - const destination = `/room/1`; + const destination = `/send/${roomId}`; stompClient.publish({ destination, @@ -229,12 +240,27 @@ export function ChattingRoom({ 45분전 - {isMenuClick && } + {isMenuClick && ( + + )} + {chatRoomData?.map((message, index) => ( +
+ {message.sender === userId ? ( + + + + ) : ( + + + + )} +
+ ))} {messages.map((message, index) => (
- {message.sender === userName ? ( + {message.sender === userId ? ( diff --git a/src/features/chat/chat.api.ts b/src/features/chat/chat.api.ts index 05a83bef78..b96ceefcdd 100644 --- a/src/features/chat/chat.api.ts +++ b/src/features/chat/chat.api.ts @@ -1,12 +1,18 @@ import axios from 'axios'; -import { type GetChatRoomUserDTO, type PostChatRoomDTO } from './chat.dto'; +import { + type PostChatRoomEnterDTO, + type GetChatRoomDTO, + type GetChatRoomUserDTO, + type PostChatRoomDTO, +} from './chat.dto'; -export const getChatRoomList = async () => - await axios.get(`/maru-api/chatRoom`).then(res => { - console.log(res.data); - return res.data; - }); +export const getChatRoomList = async (token: string | undefined) => + await axios + .get(`/maru-api/chatRoom`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then(res => res.data); export const postChatRoom = async (roomName: string, members: string[]) => { await axios @@ -19,7 +25,7 @@ export const postChatRoom = async (roomName: string, members: string[]) => { export const postInviteUser = async (roomId: number, members: string[]) => { await axios - .post(`/chatRoom/${roomId}/invite`, { + .post(`/maru-api/chatRoom/${roomId}/invite`, { members: members, }) .then(res => res.data); @@ -30,19 +36,23 @@ export const postEnterChatRoom = async ( page: number, size: number, ) => { - await axios - .post(`/chatRoom/chat`, { + const res = await axios.post( + `/maru-api/chatRoom/chat`, + { roomId: roomId, page: page, size: size, - }) - .then(res => res.data); + }, + ); + + return res.data; +}; + +export const getEnterChatRoom = async () => { + await axios.get(`/maru-api/chatRoom/chat`); }; export const getChatRoomUser = async (roomId: number) => await axios .get(`/maru-api/chatRoom/${roomId}`) - .then(res => { - console.log(res.data); - return res.data; - }); + .then(res => res.data); diff --git a/src/features/chat/chat.dto.ts b/src/features/chat/chat.dto.ts index 091f4fe054..0bee2c5a59 100644 --- a/src/features/chat/chat.dto.ts +++ b/src/features/chat/chat.dto.ts @@ -1,23 +1,40 @@ import { type SuccessBaseDTO } from '@/shared/types'; export interface GetChatRoomDTO extends SuccessBaseDTO { - data: { - id: number; - name: string; - }; + data: [ + { + roomId: number; + roomName: string; + unreadCount: number; + lastMessage: string; + lastMessageTime: string; + }, + ]; } export interface PostChatRoomDTO extends SuccessBaseDTO { data: { id: number; - name: string; }; } export interface GetChatRoomUserDTO extends SuccessBaseDTO { - data: { - memberId: string; - nickname: string; - profileImageUrl: string; - }; + data: [ + { + memberId: string; + nickname: string; + profileImageUrl: string; + }, + ]; +} + +export interface PostChatRoomEnterDTO extends SuccessBaseDTO { + data: [ + { + messageId: string; + sender: string; + message: string; + createdAt: string; + }, + ]; } diff --git a/src/features/chat/chat.hook.ts b/src/features/chat/chat.hook.ts index 80e2930526..9a1eaec8b7 100644 --- a/src/features/chat/chat.hook.ts +++ b/src/features/chat/chat.hook.ts @@ -8,10 +8,11 @@ import { postInviteUser, } from './chat.api'; -export const useChatRoomList = () => +export const useChatRoomList = (token: string | undefined) => useQuery({ - queryKey: [`/api/chatRoom`], - queryFn: getChatRoomList, + queryKey: [`/api/chatRoom`, token], + queryFn: async () => await getChatRoomList(token), + enabled: token !== undefined, }); export const useCreateChatRoom = (roomName: string, members: string[]) => @@ -30,9 +31,8 @@ export const useInviteUsers = (roomId: number, members: string[]) => export const useEnterChatRoom = (roomId: number, page: number, size: number) => useMutation({ - mutationFn: async () => { - await postEnterChatRoom(roomId, page, size); - }, + mutationFn: async () => await postEnterChatRoom(roomId, page, size), + onSuccess: data => data.data, }); export const useChatRoomUser = (roomId: number) => diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index ac35c1775c..a93b1b49f3 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -36,9 +36,9 @@ export const useFollowingListData = () => queryFn: getFollowingListData, }); -export const useFollowData = () => +export const useFollowData = (memberId: string) => useMutation({ - mutationFn: async (memberId: string) => { + mutationFn: async () => { await postFollowData(memberId); }, }); From 9d3fbf4cf819133e56d144ab93ad555b830c3a44 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 29 Apr 2024 16:46:45 +0900 Subject: [PATCH 034/130] feat: enter chatting (#57) --- src/components/FloatingChatting.tsx | 11 +++++++++- src/components/chat/ChatMenu.tsx | 33 +++++++++++++++++++--------- src/components/chat/ChattingRoom.tsx | 13 +++++++---- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index bbdc41958a..24d9b9a2ca 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -121,6 +121,8 @@ export function FloatingChatting() { const [isChatRoomOpen, setIsChatRoomOpen] = useState(false); const [chatRooms, setChatRooms] = useState([]); const [selectedRoomId, setSelectedRoomId] = useState(0); + const [selectedRoomName, setSelectedRoomName] = useState(''); + const [selectedRoomLastTime, setSelectedRoomLastTime] = useState(''); const [roomData, setRoomData] = useState< | [ { @@ -156,10 +158,13 @@ export function FloatingChatting() { const handleChatRoomClick = () => { enterChatting(); - setRoomData(chatRoomData?.data); setIsChatRoomOpen(prev => !prev); }; + useEffect(() => { + setRoomData(chatRoomData?.data); + }, [chatRoomData]); + const chatRoomList = useChatRoomList(auth?.accessToken); useEffect(() => { if (chatRoomList.data !== undefined) { @@ -213,6 +218,8 @@ export function FloatingChatting() { onClick={() => { handleChatRoomClick(); setSelectedRoomId(room.roomId); + setSelectedRoomName(room.roomName); + setSelectedRoomLastTime(room.lastMessageTime); }} /> ))} @@ -224,6 +231,8 @@ export function FloatingChatting() { chatRoomData={roomData} userId={userId} roomId={selectedRoomId} + roomName={selectedRoomName} + lastTime={selectedRoomLastTime} onRoomClick={setIsChatRoomOpen} /> )} diff --git a/src/components/chat/ChatMenu.tsx b/src/components/chat/ChatMenu.tsx index bce5a40abe..f78e83d90b 100644 --- a/src/components/chat/ChatMenu.tsx +++ b/src/components/chat/ChatMenu.tsx @@ -1,5 +1,6 @@ 'use client'; +import Link from 'next/link'; import { useEffect, useState } from 'react'; import styled from 'styled-components'; @@ -71,12 +72,26 @@ const styles = { position: relative; `, + inviteButton: styled.button` + border: none; + background-color: #fff; + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; /* 128.571% */ + text-align: start; + cursor: pointer; + `, userListContainer: styled.ul` display: flex; flex: 1; flex-direction: column; padding: 0 0.8rem; width: 100%; + max-height: 12rem; + overflow-y: auto; gap: 1rem; `, userList: styled.li` @@ -204,26 +219,24 @@ export function ChatMenu({ 하우스 메이트 {userList.map((user, index) => ( - - - {user.nickname} - + + + + {user.nickname} + + ))} - + {isInviteClick && ( diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index bfe660dfcc..89e001e6e3 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -126,6 +126,8 @@ export function ChattingRoom({ chatRoomData, userId, roomId, + roomName, + lastTime, onRoomClick, }: { chatRoomData: @@ -140,16 +142,19 @@ export function ChattingRoom({ | undefined; userId: string | undefined; roomId: number; + roomName: string; + lastTime: string; onRoomClick: React.Dispatch>; }) { const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [stompClient, setStompClient] = useState(null); - const user = userId; const [isMenuClick, setIsMenuClick] = useState(false); const [isBackClick, setIsBackClick] = useState(false); const auth = useAuthValue(); + const user = userId; + const reversedChatRoomData = chatRoomData?.reverse(); useEffect(() => { const initializeChat = async () => { @@ -236,8 +241,8 @@ export function ChattingRoom({ src="/backward-arrow.png" /> - 정릉 기숙사 405호 - 45분전 + {roomName} + {lastTime} {isMenuClick && ( @@ -245,7 +250,7 @@ export function ChattingRoom({ )} - {chatRoomData?.map((message, index) => ( + {reversedChatRoomData?.map((message, index) => (
{message.sender === userId ? ( From 5e2baa8f93daf8f9b5ecbdea35c12baaf0446075 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 29 Apr 2024 22:14:45 +0900 Subject: [PATCH 035/130] feat: add MateCardForm in writing-post-page (#62) --- src/app/pages/writing-post-page.tsx | 63 ++++++++++++++++++++++++++- src/components/UserInputSection.tsx | 4 +- src/features/shared/shared.hook.ts | 66 ++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 578af9a911..6670cddfe2 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -4,15 +4,17 @@ import { useRouter } from 'next/navigation'; import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import { UserInputSection } from '@/components'; import { LocationSearchBox, MateSearchBox, } from '@/components/writing-post-page'; import { getImageURL, putImage } from '@/features/image'; import { - type ImageFile, useCreateSharedPost, useCreateSharedPostProps, + useUserInputSection, + type ImageFile, } from '@/features/shared'; import { useToast } from '@/features/toast'; @@ -387,6 +389,12 @@ const styles = { width: 2rem; height: 2rem; `, + UserInputSection: styled(UserInputSection)` + position: fixed; + left: 50%; + right: 50%; + transform: translate(-50%, -50%); + `, }; const DealOptions = ['월세', '전세']; @@ -412,6 +420,7 @@ export function WritingPostPage() { const imageInputRef = useRef(null); const [showMateSearchBox, setShowMateSearchBox] = useState(false); + const [showMateCardForm, setShowMateCardForm] = useState(false); const [showLocationSearchBox, setShowLocationSearchBox] = useState(false); @@ -435,6 +444,21 @@ export function WritingPostPage() { isOptionSelected, isExtraOptionSelected, } = useCreateSharedPostProps(); + + const { + gender, + birthYear, + mbti, + major, + budget, + isMySelf, + type, + setBirthYear, + setMbti, + setMajor, + setBudget, + } = useUserInputSection(); + const { mutate } = useCreateSharedPost(); const { createToast } = useToast(); @@ -633,6 +657,7 @@ export function WritingPostPage() { { setAddress(selectedAddress); + setShowLocationSearchBox(false); }} setHidden={() => { setShowLocationSearchBox(false); @@ -711,7 +736,41 @@ export function WritingPostPage() { 메이트 카드 - + + {showMateCardForm && ( + {}} + onOptionChange={option => {}} + onLocationChange={() => {}} + onMateAgeChange={setBirthYear} + onMbtiChange={setMbti} + onMajorChange={setMajor} + onBudgetChange={setBudget} + /> + )} diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index f6322c0003..9561cab614 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -48,6 +48,7 @@ interface UserInputProps { onMbtiChange: React.Dispatch>; onMajorChange: React.Dispatch>; onBudgetChange: React.Dispatch>; + className?: string; } export function UserInputSection({ @@ -67,9 +68,10 @@ export function UserInputSection({ onMbtiChange, onMajorChange, onBudgetChange, + className, }: UserInputProps) { return ( - + { ); }; +export const useUserInputSection = () => { + const [gender, setGender] = useState(undefined); + const [birthYear, setBirthYear] = useState(undefined); + const [location, setLocation] = useState(undefined); + const [mbti, setMbti] = useState(undefined); + const [major, setMajor] = useState(undefined); + const [budget, setBudget] = useState(undefined); + const [features, setFeatures] = useState([]); + const [isMySelf, setIsMySelf] = useState(true); + const [type, setType] = useState<'myCard' | 'mateCard'>('mateCard'); + + const auth = useAuthValue(); + + useEffect(() => { + if (auth?.user != null) { + setGender(auth.user.gender); + } + }, [auth?.user]); + + return useMemo( + () => ({ + gender, + setGender, + birthYear, + setBirthYear, + location, + setLocation, + mbti, + setMbti, + major, + setMajor, + budget, + setBudget, + features, + setFeatures, + isMySelf, + setIsMySelf, + type, + setType, + }), + [ + gender, + setGender, + birthYear, + setBirthYear, + location, + setLocation, + mbti, + setMbti, + major, + setMajor, + budget, + setBudget, + features, + setFeatures, + isMySelf, + setIsMySelf, + type, + setType, + ], + ); +}; + export const useCreateSharedPost = () => useMutation, FailureDTO, CreateSharedPostProps>( { From 0cbc86dd791668e40b0d7bb3c471dafe79d4d611 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 29 Apr 2024 22:40:04 +0900 Subject: [PATCH 036/130] fix: change operators to compare values --- src/app/lib/providers/AuthProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index 609ebf1ee8..ec7f9a7335 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -23,9 +23,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return; } - if (auth === null) { + if (auth == null) { const refreshToken = load({ type: 'local', key: 'refreshToken' }); - if (refreshToken !== null) { + if (refreshToken != null) { postTokenRefresh(refreshToken) .then(({ data }) => { login({ From 9055e40df1682d4b564278288979cdf43f9e78ce Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 29 Apr 2024 22:58:32 +0900 Subject: [PATCH 037/130] fix: apply the new refresh token from the API response result, modify error handling --- src/app/lib/providers/AuthProvider.tsx | 4 ++-- src/app/pages/writing-post-page.tsx | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index ec7f9a7335..c8c276704b 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -30,12 +30,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { .then(({ data }) => { login({ accessToken: data.accessToken, - refreshToken, + refreshToken: data.refreshToken, expiresIn: data.expiresIn, }); }) .catch((err: Error) => { - if (isAxiosError(err) && err.code === 'ETIMEOUT') { + if (isAxiosError(err)) { remove({ type: 'local', key: 'refreshToken' }); router.replace('/'); } diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 6670cddfe2..a8e6bb4bd1 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -389,12 +389,6 @@ const styles = { width: 2rem; height: 2rem; `, - UserInputSection: styled(UserInputSection)` - position: fixed; - left: 50%; - right: 50%; - transform: translate(-50%, -50%); - `, }; const DealOptions = ['월세', '전세']; From 6245c4417b91b057f29d0a99a8f712c3e94e2c97 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 29 Apr 2024 23:12:33 +0900 Subject: [PATCH 038/130] fix: add navigation bar min width --- src/components/NavigationBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index d3b6b2c578..8a00cf3aa3 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -18,10 +18,11 @@ import { load } from '@/shared/storage'; const styles = { container: styled.nav` display: flex; + flex: 1; + min-width: 1440px; width: 100%; height: 4.5rem; padding: 1rem 11.25rem; - flex-shrink: 0; align-items: center; justify-content: space-between; From 5e174b0767e8c824ad7bdb0b32a5889fa59eb830 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 29 Apr 2024 23:46:13 +0900 Subject: [PATCH 039/130] fix: features form --- src/app/pages/setting-page.tsx | 89 +++++++++++++++++---------- src/app/pages/user-input-page.tsx | 37 ++++++----- src/components/UserInputSection.tsx | 2 +- src/components/card/OptionSection.tsx | 11 ++-- src/components/card/VitalSection.tsx | 6 +- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 46fe6c202f..846c130bf2 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -188,8 +188,8 @@ export function SettingPage({ cardId }: { cardId: number }) { if (features !== null) { setSelectedState({ ...selectedState, - smoking: features[0], - room: features[1], + smoking: features[0].split(':')[1], + room: features[1].split(':')[1], }); } } @@ -206,10 +206,14 @@ export function SettingPage({ cardId }: { cardId: number }) { if (isMySelf) { if (features !== null) { const initialOptions: SelectedOptions = {}; - features.slice(2).forEach(option => { + const optionsString = features[3].split(':')[1]; + const budgetIdx = optionsString.indexOf('['); + const budget = optionsString.slice(budgetIdx); + setInitialBudget(budget.slice(1, -1)); + + const options = optionsString.slice(0, budgetIdx).split(','); + options.forEach(option => { if ( - !option.includes(',') && - !option.includes('±') && !option.includes('E') && !option.includes('I') && !majorArray.includes(option) @@ -220,7 +224,6 @@ export function SettingPage({ cardId }: { cardId: number }) { if (option.includes('E') || option.includes('I')) setInitialMbti(option); if (majorArray.includes(option)) setInitialMajor(option); - if (option.includes(',')) setInitialBudget(option); }); setSelectedOptions(initialOptions); } @@ -269,16 +272,19 @@ export function SettingPage({ cardId }: { cardId: number }) { const array = Object.keys(selectedOptions).filter( key => selectedOptions[key] && key !== '전공' && key !== '엠비티아이', ); + const options = [ + ...array, + ...(mbti != null ? [mbti] : []), + ...(major != null ? [major] : []), + ...(budget != null ? [budget] : []), + ].filter(Boolean); const location = locationInput ?? ''; const myFeatures = [ - selectedState.smoking, - selectedState.room, - mateAge !== '' ? mateAge : undefined, - ...array, - ...(mbti !== null && mbti !== undefined ? [mbti] : []), - ...(major !== null && major !== undefined ? [major] : []), - budget, + `smoking:${selectedState.smoking}`, + `room:${selectedState.room}`, + `mateAge:${mateAge !== '' ? mateAge : undefined}`, + `options:${options.join(',')}`, ]; mutate({ location, features: myFeatures }); @@ -379,24 +385,45 @@ export function SettingPage({ cardId }: { cardId: number }) { - + {type === 'myCard' ? ( + + ) : ( + + )} ); diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 29d05d23be..0a98451fa4 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -227,6 +227,7 @@ interface UserProps { interface SelectedState { smoking: string | undefined; room: string | undefined; + mateAge: string | undefined; } type SelectedOptions = Record; @@ -239,6 +240,7 @@ const useSelectedState = (): [ const [selectedState, setSelectedState] = useState({ smoking: undefined, room: undefined, + mateAge: undefined, }); const [selectedOptions, setSelectedOptions] = useState({}); @@ -315,25 +317,32 @@ export function UserInputPage() { key => selectedMateOptions[key] && key !== '전공' && key !== '엠비티아이', ); + const myOptionsString = [ + ...myOptions, + ...(mbti != null ? [mbti] : []), + ...(major != null ? [major] : []), + ...(budget != null ? [budget] : []), + ].filter(Boolean); + const mateOptionsString = [ + ...mateOptions, + ...(mateMbti != null ? [mateMbti] : []), + ...(mateMajor != null ? [mateMajor] : []), + ...(mateBudget != null ? [mateBudget] : []), + ].filter(Boolean); + const location = locationInput ?? ''; const myFeatures = [ - selectedState.smoking, - selectedState.room, - mateAge, - ...myOptions, - ...(mbti !== null && mbti !== undefined ? [mbti] : []), - ...(major !== null && major !== undefined ? [major] : []), - budget, + `smoking:${selectedState.smoking}`, + `room:${selectedState.room}`, + `mateAge:${mateAge !== '' ? mateAge : undefined}`, + `options:${myOptionsString.join(',')}`, ]; const mateFeatures = [ - selectedMateState.smoking, - selectedMateState.room, - mateAge, - ...mateOptions, - ...(mateMbti !== null && mateMbti !== undefined ? [mateMbti] : []), - ...(mateMajor !== null && mateMajor !== undefined ? [mateMajor] : []), - mateBudget, + `smoking:${selectedMateState.smoking}`, + `room:${selectedMateState.room}`, + `mateAge:${mateAge !== '' ? mateAge : undefined}`, + `options:${mateOptionsString.join(',')}`, ]; try { diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index f6322c0003..2f5b1b171a 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -37,7 +37,7 @@ interface UserInputProps { budget: string | undefined; features: string[] | null; isMySelf: boolean; - type: string; + type: 'myCard' | 'mateCard'; onVitalChange: ( optionName: keyof SelectedState, item: string | number, diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index 0b0fcf9769..bfe9f61188 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -198,10 +198,12 @@ export function OptionSection({ useEffect(() => { if (optionFeatures !== null) { const initialOptions: SelectedOptions = {}; - optionFeatures.slice(2).forEach(option => { + const optionsString = optionFeatures[3].split(':')[1]; + const budgetIdx = optionsString.indexOf('['); + + const options = optionsString.slice(0, budgetIdx).split(','); + options.forEach(option => { if ( - !option.includes(',') && - !option.includes('±') && !option.includes('E') && !option.includes('I') && !majorArray.includes(option) @@ -251,6 +253,7 @@ export function OptionSection({ const [initialMin, setInitialMin] = useState(0); const [initialMax, setInitialMax] = useState(355); + useEffect(() => { if (budget !== undefined) { const [min, max] = budget.split(',').map(Number); @@ -298,7 +301,7 @@ export function OptionSection({ }, [selectedMajor]); useEffect(() => { - const budgetString = `${budgetMin},${budgetMax}`; + const budgetString = `[${budgetMin},${budgetMax}]`; onBudgetChange(budgetString); }, [budgetMin, budgetMax]); diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 8710ff6781..905c7efd3e 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -247,8 +247,8 @@ export function VitalSection({ useEffect(() => { setSelectedState({ ...selectedState, - smoking: vitalFeatures?.[0], - room: vitalFeatures?.[1], + smoking: vitalFeatures?.[0].split(':')[1], + room: vitalFeatures?.[1].split(':')[1], }); }, [vitalFeatures]); @@ -285,7 +285,7 @@ export function VitalSection({ const [initialAge, setInitialAge] = useState(0); useEffect(() => { if (vitalFeatures !== null) - setInitialAge(Number(vitalFeatures?.[2].slice(1))); + setInitialAge(Number(vitalFeatures?.[2].split(':')[1].slice(1))); }, [vitalFeatures?.[2]]); const [ageValue, setAgeValue] = useState(0); From 252385e1dbd8e7849e5743d7e08a181b4766116c Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Tue, 30 Apr 2024 15:24:15 +0900 Subject: [PATCH 040/130] fix: change eslint rule --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 141b7d3134..7ebebe7586 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -55,7 +55,7 @@ "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-empty-function": ["warn", {}], + "@typescript-eslint/no-empty-function": ["off", {}], "react/no-array-index-key": "warn", "react/require-default-props": "off", "react/jsx-no-useless-fragment": "off", From a08b41dad683ed1428cf46fff161b3b37540b46a Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 1 May 2024 01:29:52 +0900 Subject: [PATCH 041/130] feat/refactor: add custom hook for UserInputSection, change props types in UserInputSection component (#62) --- src/app/pages/setting-page.tsx | 18 +++---- src/app/pages/writing-post-page.tsx | 22 ++++++--- src/components/UserInputSection.tsx | 24 +++++---- src/components/card/OptionSection.tsx | 10 ++-- src/components/card/VitalSection.tsx | 28 +++++------ src/features/shared/shared.hook.ts | 71 +++++++++++++++++++++------ 6 files changed, 107 insertions(+), 66 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 846c130bf2..78137fcd1d 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -133,8 +133,9 @@ const styles = { }; interface SelectedState { - smoking: string | undefined; - room: string | undefined; + smoking?: string; + room?: string; + mateAge?: string; } interface UserProps { @@ -167,25 +168,22 @@ export function SettingPage({ cardId }: { cardId: number }) { }, [user.data]); const card = useUserCard(cardId); - const [features, setFeatures] = useState(null); + const [features, setFeatures] = useState(undefined); useEffect(() => { if (isMySelf) { if (card !== undefined) { - const featuresData = card.data?.data.myFeatures ?? null; + const featuresData = card.data?.data.myFeatures ?? undefined; setFeatures(featuresData); } } }, [card, isMySelf]); - const [selectedState, setSelectedState] = useState({ - smoking: undefined, - room: undefined, - }); + const [selectedState, setSelectedState] = useState({}); useEffect(() => { if (isMySelf) { - if (features !== null) { + if (features != null) { setSelectedState({ ...selectedState, smoking: features[0].split(':')[1], @@ -204,7 +202,7 @@ export function SettingPage({ cardId }: { cardId: number }) { useEffect(() => { if (isMySelf) { - if (features !== null) { + if (features != null) { const initialOptions: SelectedOptions = {}; const optionsString = features[3].split(':')[1]; const budgetIdx = optionsString.indexOf('['); diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index a8e6bb4bd1..672e3ce381 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -445,12 +445,12 @@ export function WritingPostPage() { mbti, major, budget, - isMySelf, - type, setBirthYear, setMbti, setMajor, setBudget, + handleEssentialFeatureChange, + handleOptionalFeatureChange, } = useUserInputSection(); const { mutate } = useCreateSharedPost(); @@ -753,11 +753,19 @@ export function WritingPostPage() { mbti={mbti} major={major} budget={budget} - features={null} - isMySelf={isMySelf} - type={type} - onVitalChange={(optionName, item) => {}} - onOptionChange={option => {}} + features={undefined} + isMySelf + type="mateCard" + onVitalChange={(optionName, option) => { + if ( + optionName === 'room' || + optionName === 'smoking' || + optionName === 'mateAge' + ) { + handleEssentialFeatureChange(optionName, option); + } + }} + onOptionChange={handleOptionalFeatureChange} onLocationChange={() => {}} onMateAgeChange={setBirthYear} onMbtiChange={setMbti} diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index 20301ef392..8d2229f43a 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -24,24 +24,22 @@ const styles = { }; interface SelectedState { - smoking: string | undefined; - room: string | undefined; + smoking?: string; + room?: string; + mateAge?: string; } interface UserInputProps { - gender: string | undefined; - birthYear: string | undefined; - location: string | undefined; - mbti: string | undefined; - major: string | undefined; - budget: string | undefined; - features: string[] | null; + gender?: string; + birthYear?: string; + location?: string; + mbti?: string; + major?: string; + budget?: string; + features?: string[]; isMySelf: boolean; type: 'myCard' | 'mateCard'; - onVitalChange: ( - optionName: keyof SelectedState, - item: string | number, - ) => void; + onVitalChange: (optionName: keyof SelectedState, item: string) => void; onOptionChange: (option: string) => void; onLocationChange: React.Dispatch>; onMateAgeChange: React.Dispatch>; diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index bfe9f61188..b0ca7fa5f7 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -179,10 +179,10 @@ export function OptionSection({ isMySelf, type, }: { - mbti: string | undefined; - major: string | undefined; - budget: string | undefined; - optionFeatures: string[] | null; + mbti?: string; + major?: string; + budget?: string; + optionFeatures?: string[]; onFeatureChange: (option: string) => void; onMbtiChange: React.Dispatch>; onMajorChange: React.Dispatch>; @@ -196,7 +196,7 @@ export function OptionSection({ const majorArray = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; useEffect(() => { - if (optionFeatures !== null) { + if (optionFeatures != null) { const initialOptions: SelectedOptions = {}; const optionsString = optionFeatures[3].split(':')[1]; const budgetIdx = optionsString.indexOf('['); diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 905c7efd3e..0e1823094f 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -209,11 +209,15 @@ const CheckItem = styled.div` }}; `; -const years = Array.from({ length: 100 }, (_, index) => 2024 - index); +const years = Array.from( + { length: 100 }, + (_, index) => new Date().getFullYear() - index, +); interface SelectedState { - smoking: string | undefined; - room: string | undefined; + smoking?: string; + room?: string; + mateAge?: string; } export function VitalSection({ @@ -227,14 +231,11 @@ export function VitalSection({ isMySelf, type, }: { - gender: string | undefined; - birthYear: string | undefined; - location: string | undefined; - vitalFeatures: string[] | null; - onFeatureChange: ( - optionName: keyof SelectedState, - item: string | number, - ) => void; + gender?: string; + birthYear?: string; + location?: string; + vitalFeatures?: string[]; + onFeatureChange: (optionName: keyof SelectedState, item: string) => void; onLocationChange: React.Dispatch>; onMateAgeChange: React.Dispatch>; isMySelf: boolean; @@ -252,10 +253,7 @@ export function VitalSection({ }); }, [vitalFeatures]); - function handleOptionClick( - optionName: keyof SelectedState, - item: string | number, - ) { + function handleOptionClick(optionName: keyof SelectedState, item: string) { setSelectedState(prevState => ({ ...prevState, [optionName]: prevState[optionName] === item ? null : item, diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 7f3d42668d..99ac47b23c 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -189,12 +189,57 @@ export const useUserInputSection = () => { const [mbti, setMbti] = useState(undefined); const [major, setMajor] = useState(undefined); const [budget, setBudget] = useState(undefined); - const [features, setFeatures] = useState([]); - const [isMySelf, setIsMySelf] = useState(true); - const [type, setType] = useState<'myCard' | 'mateCard'>('mateCard'); - const auth = useAuthValue(); + const [features, setFeatures] = useState<{ + smoking?: string; + room?: string; + mateAge?: string; + options: Set; + }>({ options: new Set() }); + + const handleEssentialFeatureChange = useCallback( + (key: 'smoking' | 'room' | 'mateAge', value: string) => { + setFeatures(prev => { + if (prev[key] === value) { + const newFeatures = { ...prev }; + newFeatures[key] = undefined; + return newFeatures; + } + return { ...prev, [key]: value }; + }); + }, + [], + ); + + const handleOptionalFeatureChange = useCallback((option: string) => { + setFeatures(prev => { + const { options } = prev; + const newOptions = new Set(options); + + if (options.has(option)) newOptions.delete(option); + else newOptions.add(option); + return { ...prev, options: newOptions }; + }); + }, []); + + const derivedFeatures = useMemo(() => { + const options: string[] = []; + features.options.forEach(option => options.push(option)); + + return JSON.stringify({ + smoking: features?.smoking, + room: features?.room, + mateAge: features?.mateAge, + options, + }); + }, [features]); + + useEffect(() => { + console.log(derivedFeatures); + }, [derivedFeatures]); + + const auth = useAuthValue(); useEffect(() => { if (auth?.user != null) { setGender(auth.user.gender); @@ -215,12 +260,9 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - features, - setFeatures, - isMySelf, - setIsMySelf, - type, - setType, + derivedFeatures, + handleEssentialFeatureChange, + handleOptionalFeatureChange, }), [ gender, @@ -235,12 +277,9 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - features, - setFeatures, - isMySelf, - setIsMySelf, - type, - setType, + derivedFeatures, + handleEssentialFeatureChange, + handleOptionalFeatureChange, ], ); }; From f06aad90e1d3b1ea615821d82e6b707e79ad707d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 1 May 2024 01:39:06 +0900 Subject: [PATCH 042/130] fix: fix build error --- src/app/pages/user-input-page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 0a98451fa4..74d9d4e0de 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -476,7 +476,7 @@ export function UserInputPage() { gender={user?.gender} birthYear={user?.birthYear} location={undefined} - vitalFeatures={null} + vitalFeatures={undefined} onFeatureChange={handleFeatureChange} onLocationChange={setLocation} onMateAgeChange={() => {}} @@ -488,7 +488,7 @@ export function UserInputPage() { mbti={undefined} major={undefined} budget={undefined} - optionFeatures={null} + optionFeatures={undefined} onFeatureChange={handleOptionClick} onMbtiChange={setMbti} onMajorChange={setMajor} @@ -502,7 +502,7 @@ export function UserInputPage() { gender={user?.gender} birthYear={undefined} location={locationInput} - vitalFeatures={null} + vitalFeatures={undefined} onFeatureChange={handleMateFeatureChange} onLocationChange={setLocation} onMateAgeChange={setMateAge} @@ -514,7 +514,7 @@ export function UserInputPage() { mbti={undefined} major={undefined} budget={undefined} - optionFeatures={null} + optionFeatures={undefined} onFeatureChange={handleMateOptionClick} onMbtiChange={setMateMbti} onMajorChange={setMateMajor} From a850d53f275dc00c988c538ccadf72ae69a145f6 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 1 May 2024 16:12:42 +0900 Subject: [PATCH 043/130] feat: add follow, unfollow, search API (#71) --- src/app/pages/profile-page.tsx | 56 ++++++++++++++++++--- src/app/pages/shared-post-page.tsx | 27 +++++++++- src/components/NavigationBar.tsx | 73 +++++++++++++++++++++++++++- src/components/SearchBox.tsx | 29 +++++++++-- src/features/profile/profile.api.ts | 36 ++++++++++++-- src/features/profile/profile.dto.ts | 10 ++-- src/features/profile/profile.hook.ts | 25 ++++++++-- 7 files changed, 231 insertions(+), 25 deletions(-) diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index 4819eae9c0..924dfa193d 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -4,8 +4,14 @@ import Link from 'next/link'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; +import { Bookmark } from '@/components'; import { useAuthValue, useUserData } from '@/features/auth'; -import { useProfileData } from '@/features/profile'; +import { + useFollowUser, + useFollowingListData, + useProfileData, + useUnfollowUser, +} from '@/features/profile'; const styles = { pageContainer: styled.div` @@ -61,9 +67,8 @@ const styles = { userDetailedContainer: styled.div` display: inline-flex; width: 100%; - flex-direction: column; align-items: flex-start; - gap: 0.25rem; + gap: 2rem; `, userName: styled.div` color: #000; @@ -338,15 +343,32 @@ interface UserProfileInfoProps { email: string | undefined; phoneNum: string | undefined; src: string | undefined; + memberId: string; + isMySelf: boolean; } -function UserInfo({ name, email, phoneNum, src }: UserProfileInfoProps) { +function UserInfo({ + name, + email, + phoneNum, + src, + memberId, + isMySelf, +}: UserProfileInfoProps) { const [isChecked, setIsChecked] = useState(false); + const followList = useFollowingListData(); + const [isMarked, setIsMarked] = useState( + followList.data?.data.followingList[memberId] != null, + ); + const toggleSwitch = () => { setIsChecked(!isChecked); }; + const { mutate: follow } = useFollowUser(memberId); + const { mutate: unfollow } = useUnfollowUser(memberId); + return ( @@ -359,8 +381,28 @@ function UserInfo({ name, email, phoneNum, src }: UserProfileInfoProps) { {name} - {phoneNum} - {email} +
+ {phoneNum} + {email} +
+ {!isMySelf && ( + { + if (isMarked) unfollow(); + else follow(); + setIsMarked(prev => !prev); + }} + hasBorder + color="#888" + /> + )}
@@ -575,6 +617,8 @@ export function ProfilePage({ memberId }: { memberId: string }) { email={userData?.email ?? ''} phoneNum={userData?.phoneNumber ?? ''} src={user.data?.data.profileImage} + memberId={memberId} + isMySelf={isMySelf} /> { const center = new naver.maps.LatLng(37.6090857, 126.9966865); setMap( @@ -516,10 +535,14 @@ export function SharedPostPage({ postId }: { postId: number }) {
프로필 보기 { + if (isFollowed) unfollow(); + else follow(); + setIsFollowed(prev => !prev); + }} hasBorder color="#888" - marked={false} - onToggle={() => {}} />
diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index d3b6b2c578..f50e8e7335 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; import styled from 'styled-components'; import { SearchBox } from './SearchBox'; @@ -13,6 +14,7 @@ import { useAuthValue, useUserData, } from '@/features/auth'; +import { useSearchUser } from '@/features/profile'; import { load } from '@/shared/storage'; const styles = { @@ -67,6 +69,35 @@ const styles = { cursor: pointer; `, + searchUserBox: styled.ul` + display: flex; + flex-direction: column; + position: fixed; + top: 6rem; + left: 19rem; + background-color: #fff; + min-width: 20rem; + min-height: 10rem; + border-radius: 1rem; + box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.05); + z-index: 20000; + `, + userContainer: styled.li` + display: flex; + gap: 1.5rem; + align-items: center; + + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + userImg: styled.img` + width: 1rem; + height: 1rem; + `, }; export function NavigationBar() { @@ -92,13 +123,53 @@ export function NavigationBar() { } }; + interface user { + memberId: string; + nickname: string; + profileImageUrl: string; + } + + const [isSearchBox, setIsSearchBox] = useState(false); + const [email, setEmail] = useState(''); + const [enter, setEnter] = useState(false); + + const { mutate: search, data: searchUser } = useSearchUser(email); + const [userData, setUserData] = useState(); + + useEffect(() => { + if (enter) { + search(); + setEnter(false); + } + }, [enter]); + + useEffect(() => { + setUserData(searchUser?.data); + }, [searchUser]); + return ( maru - + + {isSearchBox && ( + + { + setIsSearchBox(false); + }} + > + + {userData?.nickname ?? ''} + + + )} 메이트찾기 diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx index b28ca569ee..22a334dc67 100644 --- a/src/components/SearchBox.tsx +++ b/src/components/SearchBox.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import styled from 'styled-components'; const styles = { @@ -23,17 +23,40 @@ const styles = { `, }; -export function SearchBox() { +export function SearchBox({ + onClick, + onContentChange, + onEnter, +}: { + onClick: React.Dispatch>; + onContentChange: React.Dispatch>; + onEnter: React.Dispatch>; +}) { const [content, setContent] = useState(''); + useEffect(() => { + onContentChange(content); + }, [content]); + + function handleKeyUp(event: React.KeyboardEvent) { + if (event.keyCode === 13) { + onEnter(true); + } + } + return ( - + { + onClick(prev => !prev); + }} + > { setContent(e.target.value); }} + onKeyDown={handleKeyUp} /> ); diff --git a/src/features/profile/profile.api.ts b/src/features/profile/profile.api.ts index cf05758b6d..9fc9d11ba3 100644 --- a/src/features/profile/profile.api.ts +++ b/src/features/profile/profile.api.ts @@ -4,13 +4,15 @@ import { type GetUserProfileDTO, type GetUserCardDTO, type PutUserCardDTO, - type PostFollowDTO, type GetFollowingListDTO, + type PostSearchDTO, } from './profile.dto'; export const getUserProfileData = async (memberId: string) => await axios - .get(`/maru-api/profile/${memberId}`) + .get(`/maru-api/profile/${memberId}`, { + // params: { memberId: memberId }, + }) .then(res => res.data); export const getUserCard = async (cardId: number) => @@ -38,8 +40,32 @@ export const getFollowingListData = async () => .get(`/maru-api/profile/follow`) .then(res => res.data); -export const postFollowData = async (memberId: string) => { +export const postFollowUser = async (memberId: string) => { await axios - .post(`/maru-api/profile/${memberId}/follow`, {}) - .then(res => res.data); + .post(`/maru-api/profile/follow`, { + memberId: memberId, + }) + .then(res => { + console.log('follow'); + return res.data; + }); +}; + +export const postUnfollowUser = async (memberId: string) => { + await axios + .post(`/maru-api/profile/unfollow`, { + memberId: memberId, + }) + .then(res => { + console.log('unfollow'); + return res.data; + }); +}; + +export const postSearchUser = async (email: string) => { + const res = await axios.post(`/maru-api/profile/search`, { + email: email, + }); + + return res.data; }; diff --git a/src/features/profile/profile.dto.ts b/src/features/profile/profile.dto.ts index 2bb2b1c755..af463e5842 100644 --- a/src/features/profile/profile.dto.ts +++ b/src/features/profile/profile.dto.ts @@ -44,10 +44,14 @@ export interface PutUserCardDTO extends SuccessBaseDTO { export interface GetFollowingListDTO extends SuccessBaseDTO { data: { - followingList: string[]; + followingList: Record; }; } -export interface PostFollowDTO extends SuccessBaseDTO { - data: null; +export interface PostSearchDTO extends SuccessBaseDTO { + data: { + memberId: string; + nickname: string; + profileImageUrl: string; + }; } diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index ac35c1775c..6c27a14033 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -4,13 +4,15 @@ import { getUserProfileData, getUserCard, getFollowingListData, - postFollowData, putUserCard, + postSearchUser, + postUnfollowUser, + postFollowUser, } from './profile.api'; export const useProfileData = (memberId: string) => useQuery({ - queryKey: [`/api/profile/${memberId}`], + queryKey: [`/api/profile`, memberId], queryFn: async () => await getUserProfileData(memberId), enabled: memberId !== undefined, }); @@ -36,9 +38,22 @@ export const useFollowingListData = () => queryFn: getFollowingListData, }); -export const useFollowData = () => +export const useFollowUser = (memberId: string) => useMutation({ - mutationFn: async (memberId: string) => { - await postFollowData(memberId); + mutationFn: async () => { + await postFollowUser(memberId); }, }); + +export const useUnfollowUser = (memberId: string) => + useMutation({ + mutationFn: async () => { + await postUnfollowUser(memberId); + }, + }); + +export const useSearchUser = (email: string) => + useMutation({ + mutationFn: async () => await postSearchUser(email), + onSuccess: data => data.data, + }); From 1388ef6128fd840c39710fb9c0cf5d2a7b1b4381 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 1 May 2024 16:44:20 +0900 Subject: [PATCH 044/130] fix: scroll bar custom (#57) --- src/components/FloatingChatting.tsx | 10 +++++++++- src/components/chat/ChatMenu.tsx | 8 ++++++++ src/components/chat/ChattingRoom.tsx | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 24d9b9a2ca..443d229013 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -79,7 +79,15 @@ const styles = { height: calc(100% - 3.25rem); display: flex; flex-direction: column; - overflow-y: auto; + overflow-y: scroll; + + &::-webkit-scrollbar { + width: 0.5rem; + } + &::-webkit-scrollbar-thumb { + background-color: #ced3da; + border-radius: 4px; + } `, searchButton: styled.img` width: 1.2rem; diff --git a/src/components/chat/ChatMenu.tsx b/src/components/chat/ChatMenu.tsx index f78e83d90b..5fedf38792 100644 --- a/src/components/chat/ChatMenu.tsx +++ b/src/components/chat/ChatMenu.tsx @@ -93,6 +93,14 @@ const styles = { max-height: 12rem; overflow-y: auto; gap: 1rem; + + &::-webkit-scrollbar { + width: 0.5rem; + } + &::-webkit-scrollbar-thumb { + background-color: #ced3da; + border-radius: 4px; + } `, userList: styled.li` display: flex; diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 89e001e6e3..87d2a63053 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -75,6 +75,14 @@ const styles = { height: calc(100% - 7.5rem); box-shadow: 0px -1px 0px 0px #e5e5ea inset; position: relative; + + &::-webkit-scrollbar { + width: 0.5rem; + } + &::-webkit-scrollbar-thumb { + background-color: #ced3da; + border-radius: 4px; + } `, senderFrame: styled.div` display: flex; From c282c231868bffdecf68ae4109db829b9b7bb194 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 2 May 2024 12:03:53 +0900 Subject: [PATCH 045/130] fix: modify AuthProvider --- src/app/lib/providers/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index c8c276704b..968f560d6e 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -44,7 +44,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { router.replace('/'); } } - }, [auth, login, router, pathName]); + }); return <>{children}; } From d0bd94b145dd73b13e392655c4517254935bff2b Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 2 May 2024 12:16:33 +0900 Subject: [PATCH 046/130] fix: change variable name in useUserInputSection hook --- src/features/shared/shared.hook.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 99ac47b23c..89e1a66e4c 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -223,7 +223,7 @@ export const useUserInputSection = () => { }); }, []); - const derivedFeatures = useMemo(() => { + const stringifyFeatures = useMemo(() => { const options: string[] = []; features.options.forEach(option => options.push(option)); @@ -235,10 +235,6 @@ export const useUserInputSection = () => { }); }, [features]); - useEffect(() => { - console.log(derivedFeatures); - }, [derivedFeatures]); - const auth = useAuthValue(); useEffect(() => { if (auth?.user != null) { @@ -260,7 +256,7 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - derivedFeatures, + stringifyFeatures, handleEssentialFeatureChange, handleOptionalFeatureChange, }), @@ -277,7 +273,7 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - derivedFeatures, + stringifyFeatures, handleEssentialFeatureChange, handleOptionalFeatureChange, ], From 3c465d71551e9d3ce898663766da4bb2c0f8ee4d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 2 May 2024 12:35:10 +0900 Subject: [PATCH 047/130] feat: add FloorType (#62) --- src/shared/types/floor-type.ts | 7 +++++++ src/shared/types/index.ts | 1 + 2 files changed, 8 insertions(+) create mode 100644 src/shared/types/floor-type.ts diff --git a/src/shared/types/floor-type.ts b/src/shared/types/floor-type.ts new file mode 100644 index 0000000000..f0598d5beb --- /dev/null +++ b/src/shared/types/floor-type.ts @@ -0,0 +1,7 @@ +export type FloorType = 'GROUND' | 'SEMI_BASEMENT' | 'PENTHOUSE'; + +export const FloorTypeValue: Record = { + GROUND: 0, + SEMI_BASEMENT: 1, + PENTHOUSE: 2, +}; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index e9322297dd..38263f7496 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,3 +1,4 @@ export * from './dto'; +export * from './floor-type'; export * from './rental-type'; export * from './room-type'; From e3fee483e7c489c162e4b380f82720c59eb6f817 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 2 May 2024 14:10:36 +0900 Subject: [PATCH 048/130] fix: modify types (#62) --- src/shared/types/floor-type.ts | 8 ++++---- src/shared/types/rental-type.ts | 6 +++--- src/shared/types/room-type.ts | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/shared/types/floor-type.ts b/src/shared/types/floor-type.ts index f0598d5beb..05fa210266 100644 --- a/src/shared/types/floor-type.ts +++ b/src/shared/types/floor-type.ts @@ -1,7 +1,7 @@ export type FloorType = 'GROUND' | 'SEMI_BASEMENT' | 'PENTHOUSE'; -export const FloorTypeValue: Record = { - GROUND: 0, - SEMI_BASEMENT: 1, - PENTHOUSE: 2, +export const FloorTypeValue: Record = { + GROUND: '0', + SEMI_BASEMENT: '1', + PENTHOUSE: '2', }; diff --git a/src/shared/types/rental-type.ts b/src/shared/types/rental-type.ts index 2e898bfc1e..03f7bcee4d 100644 --- a/src/shared/types/rental-type.ts +++ b/src/shared/types/rental-type.ts @@ -1,6 +1,6 @@ export type RentalType = 'MONTHLY' | 'JEONSE'; -export const RentalTypeValue: Record = { - MONTHLY: 0, - JEONSE: 1, +export const RentalTypeValue: Record = { + MONTHLY: '0', + JEONSE: '1', }; diff --git a/src/shared/types/room-type.ts b/src/shared/types/room-type.ts index 833bbe7fb8..dbc63d4789 100644 --- a/src/shared/types/room-type.ts +++ b/src/shared/types/room-type.ts @@ -7,12 +7,12 @@ export type RoomType = | 'OFFICE_TEL_3' | 'APT'; -export const RoomTypeValue: Record = { - VILLA_1: 0, - VILLA_2: 1, - VILLA_3: 2, - OFFICE_TEL_1: 3, - OFFICE_TEL_2: 4, - OFFICE_TEL_3: 5, - APT: 6, +export const RoomTypeValue: Record = { + VILLA_1: '0', + VILLA_2: '1', + VILLA_3: '2', + OFFICE_TEL_1: '3', + OFFICE_TEL_2: '4', + OFFICE_TEL_3: '5', + APT: '6', }; From 206800497515c6bc899af8d1ab1f3fc3d9626b11 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 2 May 2024 17:47:52 +0900 Subject: [PATCH 049/130] fix: apply api changes (#62, #63) --- src/app/pages/writing-post-page.tsx | 261 ++++++++++++++++------------ src/features/shared/shared.api.ts | 8 +- src/features/shared/shared.hook.ts | 46 ++++- src/features/shared/shared.type.ts | 22 ++- 4 files changed, 209 insertions(+), 128 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 672e3ce381..ff6e6f4dd2 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -394,16 +394,16 @@ const styles = { const DealOptions = ['월세', '전세']; const RoomOptions = ['원룸', '빌라/투룸이상', '아파트', '오피스텔']; const LivingRoomOptions = ['유', '무']; -const RoomCountOptions = ['1개', '2개', '3개 이상']; -const RestRoomCountOptions = ['1개', '2개', '3개 이상']; +const RoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; +const RestRoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; const FloorOptions = ['지상', '반지하', '옥탑']; -const AdditionalOptions = [ - '주차가능', - '에어컨', - '냉장고', - '세탁기', - '베란다/테라스', -]; +const AdditionalOptions = { + canPark: '주차가능', + hasAirConditioner: '에어컨', + hasRefrigerator: '냉장고', + hasWasher: '세탁기', + hasTerrace: '베란다/테라스', +}; interface ButtonActiveProps { $isSelected: boolean; @@ -420,18 +420,21 @@ export function WritingPostPage() { const { title, - setTitle, content, - setContent, images, - setImages, mateLimit, - setMateLimit, houseSize, - setHouseSize, address, - setAddress, + selectedOptions, + selectedExtraOptions, expectedMonthlyFee, + isPostCreatable, + setTitle, + setContent, + setImages, + setMateLimit, + setHouseSize, + setAddress, setExpectedMonthlyFee, handleOptionClick, handleExtraOptionClick, @@ -445,6 +448,7 @@ export function WritingPostPage() { mbti, major, budget, + isMateCardCreatable, setBirthYear, setMbti, setMajor, @@ -508,99 +512,136 @@ export function WritingPostPage() { }; const handleCreatePost = (event: React.MouseEvent) => { + if (!isPostCreatable || !isMateCardCreatable) return; + + const rentalType = selectedOptions.budget; + const { roomType } = selectedOptions; + const { floorType } = selectedOptions; + + if ( + rentalType == null || + roomType == null || + floorType == null || + address == null || + selectedOptions.roomCount == null || + !(selectedOptions.roomCount in RoomCountOptions) || + selectedOptions.restRoomCount == null || + !(selectedOptions.restRoomCount in RestRoomCountOptions) + ) + return; + + const numberOfRoomOption = selectedOptions.roomCount as + | '1개' + | '2개' + | '3개 이상'; + const numberOfRoom = RoomCountOptions[numberOfRoomOption]; + + const numberOfBathRoomOption = selectedOptions.restRoomCount as + | '1개' + | '2개' + | '3개 이상'; + const numberOfBathRoom = RestRoomCountOptions[numberOfBathRoomOption]; + (async () => { - if (images.length > 0) { - try { - const getResults = await Promise.allSettled( - images.map(async ({ extension, file }) => { - const result = await getImageURL(extension); - return { - ...result.data.data, - file, - }; - }), - ); - - const urls = getResults.reduce< - Array<{ file: File; fileName: string; url: string }> - >((prev, result) => { - if (result.status === 'rejected') return prev; - return prev.concat(result.value); - }, []); - - const putResults = await Promise.allSettled( - urls.map(async url => { - await putImage(url.url, url.file); - return { fileName: url.fileName }; - }), - ); - - const uploadedImages = putResults.reduce< - Array<{ fileName: string; isThumbNail: boolean; order: number }> - >((prev, result) => { - if (result.status === 'rejected') return prev; - return prev.concat({ - fileName: result.value.fileName, - isThumbNail: prev.length === 0, - order: prev.length + 1, - }); - }, []); - - mutate( - { - imageFilesData: uploadedImages, - postData: { content, title }, - transactionData: { - rentalType: '0', - price: 100000, - monthlyFee: 10000, - managementFee: 1000, - }, - roomDetailData: { - roomType: '0', - size: 5, - numberOfRoom: 1, - recruitmentCapacity: 2, - }, - locationData: { - city: 'SEOUL', - oldAddress: 'test old address', - roadAddress: 'test road address', - stationName: 'mokdong', - stationTime: 10, - busStopTime: 3, - schoolName: 'kookmin', - schoolTime: 20, - convenienceStoreTime: 2, - }, - roomMateCardData: { - location: '솔샘로 44', - features: ['특징1', '특징2', '특징3'], - }, + try { + const getResults = await Promise.allSettled( + images.map(async ({ extension, file }) => { + const result = await getImageURL(extension); + return { + ...result.data.data, + file, + }; + }), + ); + + const urls = getResults.reduce< + Array<{ file: File; fileName: string; url: string }> + >((prev, result) => { + if (result.status === 'rejected') return prev; + return prev.concat(result.value); + }, []); + + const putResults = await Promise.allSettled( + urls.map(async url => { + await putImage(url.url, url.file); + return { fileName: url.fileName }; + }), + ); + + const uploadedImages = putResults.reduce< + Array<{ fileName: string; isThumbNail: boolean; order: number }> + >((prev, result) => { + if (result.status === 'rejected') return prev; + return prev.concat({ + fileName: result.value.fileName, + isThumbNail: prev.length === 0, + order: prev.length + 1, + }); + }, []); + + mutate( + { + imageFilesData: uploadedImages, + postData: { title, content }, + transactionData: { + rentalType, + expectedPayment: expectedMonthlyFee, }, - { - onSuccess: () => { - createToast({ - message: '게시글이 정상적으로 업로드되었습니다.', - option: { - duration: 3000, - }, - }); - router.back(); - }, - onError: () => { - createToast({ - message: '게시글 업로드에 실패했습니다.', - option: { - duration: 3000, - }, - }); + roomDetailData: { + roomType, + floorType, + size: houseSize, + numberOfRoom, + numberOfBathRoom, + hasLivingRoom: selectedOptions.livingRoom === '유', + recruitmentCapacity: mateLimit, + extraOption: { + canPark: selectedExtraOptions.canPark, + hasAirConditioner: selectedExtraOptions.hasAirConditioner, + hasRefrigerator: selectedExtraOptions.hasRefrigerator, + hasWasher: selectedExtraOptions.hasWasher, + hasTerrace: selectedExtraOptions.hasTerrace, }, }, - ); - } catch (error) { - console.error(error); - } + locationData: { + city: address?.roadAddress.split(' ').slice(0, 2).join(' '), + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + detailAddress: '', + }, + roomMateCardData: { + location: address?.roadAddress, + features: [], + }, + participationMemberIds: [], + }, + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); + } catch (error) { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); } })(); }; @@ -823,15 +864,15 @@ export function WritingPostPage() { 추가 옵션 - {AdditionalOptions.map(option => ( + {Object.entries(AdditionalOptions).map(([option, value]) => ( { - handleExtraOptionClick(option); + handleExtraOptionClick(value); }} /> - {option} + {value} ))} @@ -865,7 +906,7 @@ export function WritingPostPage() { 방 개수 - {RoomCountOptions.map(option => ( + {Object.keys(RoomCountOptions).map(option => ( 화장실 개수 - {RestRoomCountOptions.map(option => ( + {Object.keys(RestRoomCountOptions).map(option => ( { const result: Partial> = {}; if (filter.roomType !== undefined) { - result.roomType = Object.values(filter.roomType).map( - value => RoomTypeValue[value], + result.roomType = Object.values(filter.roomType).map(value => + Number(RoomTypeValue[value]), ); } if (filter.rentalType !== undefined) { - result.rentalType = Object.values(filter.rentalType).map( - value => RentalTypeValue[value], + result.rentalType = Object.values(filter.rentalType).map(value => + Number(RentalTypeValue[value]), ); } diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 89e1a66e4c..5e7cebe148 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -130,6 +130,32 @@ export const useCreateSharedPostProps = () => { [selectedExtraOptions], ); + const isPostCreatable = useMemo( + () => + images.length > 0 && + title.trim().length > 0 && + content.trim().length > 0 && + selectedOptions.budget != null && + expectedMonthlyFee > 0 && + selectedOptions.roomType != null && + houseSize > 0 && + selectedOptions.roomCount != null && + selectedOptions.restRoomCount != null && + selectedOptions.livingRoom != null && + mateLimit > 0 && + address != null, + [ + images, + title, + content, + selectedOptions, + expectedMonthlyFee, + houseSize, + mateLimit, + address, + ], + ); + return useMemo( () => ({ title, @@ -154,6 +180,7 @@ export const useCreateSharedPostProps = () => { handleExtraOptionClick, isOptionSelected, isExtraOptionSelected, + isPostCreatable, }), [ title, @@ -178,6 +205,7 @@ export const useCreateSharedPostProps = () => { handleExtraOptionClick, isOptionSelected, isExtraOptionSelected, + isPostCreatable, ], ); }; @@ -223,16 +251,16 @@ export const useUserInputSection = () => { }); }, []); - const stringifyFeatures = useMemo(() => { + const derivedFeatures = useMemo(() => { const options: string[] = []; features.options.forEach(option => options.push(option)); - return JSON.stringify({ + return { smoking: features?.smoking, room: features?.room, mateAge: features?.mateAge, options, - }); + }; }, [features]); const auth = useAuthValue(); @@ -242,6 +270,12 @@ export const useUserInputSection = () => { } }, [auth?.user]); + const isMateCardCreatable = useMemo( + () => + gender != null && birthYear != null && location != null && budget != null, + [gender, birthYear, location, budget], + ); + return useMemo( () => ({ gender, @@ -256,9 +290,10 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - stringifyFeatures, + derivedFeatures, handleEssentialFeatureChange, handleOptionalFeatureChange, + isMateCardCreatable, }), [ gender, @@ -273,9 +308,10 @@ export const useUserInputSection = () => { setMajor, budget, setBudget, - stringifyFeatures, + derivedFeatures, handleEssentialFeatureChange, handleOptionalFeatureChange, + isMateCardCreatable, ], ); }; diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index ac9962f968..80e85e55ad 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -40,29 +40,33 @@ export interface CreateSharedPostProps { }; transactionData: { rentalType: string; - price: number; - monthlyFee: number; - managementFee: number; + expectedPayment: number; }; roomDetailData: { roomType: string; + floorType: string; size: number; numberOfRoom: number; + numberOfBathRoom: number; + hasLivingRoom: boolean; recruitmentCapacity: number; + extraOption: { + canPark: boolean; + hasAirConditioner: boolean; + hasRefrigerator: boolean; + hasWasher: boolean; + hasTerrace: boolean; + }; }; locationData: { city: string; oldAddress: string; roadAddress: string; - stationName: string; - stationTime: number; - busStopTime: number; - schoolName: string; - schoolTime: number; - convenienceStoreTime: number; + detailAddress: string; }; roomMateCardData: { location: string; features: string[]; }; + participationMemberIds: string[]; } From 5770ce8675593fc4b0ee9cc71a4000ef56c41cca Mon Sep 17 00:00:00 2001 From: he2e2 Date: Thu, 2 May 2024 21:35:56 +0900 Subject: [PATCH 050/130] feat: auto scroll (#57) --- src/components/FloatingChatting.tsx | 21 +++++++---- src/components/chat/ChatMenu.tsx | 26 +++++++------- src/components/chat/ChattingRoom.tsx | 52 +++++++++++++++++++--------- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 443d229013..78e69c2fdc 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -208,14 +208,23 @@ export function FloatingChatting() {
- + +
+ {chatRooms.map((room, index) => ( - - - 김마루 - - - - 김마루 - - - - 김마루 - + {/* {Object.values( + folloingUsers.data?.data.followingList as Record< + string, + string[] + >, + ).map((user, index) => ( + + + {user[0]} + + ))} */} diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 87d2a63053..58c93c7082 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -1,7 +1,7 @@ 'use client'; import { Client } from '@stomp/stompjs'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { ChatMenu } from './ChatMenu'; @@ -162,13 +162,12 @@ export function ChattingRoom({ const auth = useAuthValue(); const user = userId; - const reversedChatRoomData = chatRoomData?.reverse(); useEffect(() => { const initializeChat = async () => { try { const stomp = new Client({ - brokerURL: `ws://ec2-3-35-138-168.ap-northeast-2.compute.amazonaws.com:8080/ws`, + brokerURL: `ws://ec2-54-180-133-123.ap-northeast-2.compute.amazonaws.com:8080/ws`, connectHeaders: { Authorization: `Bearer ${auth?.accessToken}`, }, @@ -241,6 +240,22 @@ export function ChattingRoom({ } } + const messageContainerRef = useRef(null); + + useEffect(() => { + if (messageContainerRef.current != null) { + messageContainerRef.current.scrollTop = + messageContainerRef.current.scrollHeight; + } + }, [chatRoomData]); + + useEffect(() => { + if (messageContainerRef.current != null) { + messageContainerRef.current.scrollTop = + messageContainerRef.current.scrollHeight; + } + }, [messages]); + return ( @@ -257,20 +272,23 @@ export function ChattingRoom({ )} - - {reversedChatRoomData?.map((message, index) => ( -
- {message.sender === userId ? ( - - - - ) : ( - - - - )} -
- ))} + + {chatRoomData + ?.slice() + .reverse() + ?.map((message, index) => ( +
+ {message.sender === userId ? ( + + + + ) : ( + + + + )} +
+ ))} {messages.map((message, index) => (
{message.sender === userId ? ( From f89728ccbbe756ebe37ffe16fd1dc59282b71338 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 3 May 2024 12:04:37 +0900 Subject: [PATCH 051/130] fix: change to a static image file from a URL --- public/kakao-login.png | Bin 0 -> 10825 bytes public/landing-page-image.png | Bin 0 -> 62092 bytes public/naver-login.png | Bin 0 -> 10127 bytes src/app/pages/landing-page.tsx | 15 +++------------ 4 files changed, 3 insertions(+), 12 deletions(-) create mode 100644 public/kakao-login.png create mode 100644 public/landing-page-image.png create mode 100644 public/naver-login.png diff --git a/public/kakao-login.png b/public/kakao-login.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4f2c659435b68f71445afe57709f9a38c5f14d GIT binary patch literal 10825 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 literal 0 HcmV?d00001 diff --git a/public/landing-page-image.png b/public/landing-page-image.png new file mode 100644 index 0000000000000000000000000000000000000000..d83d88692ad1f2df7ea435c701d7eda2b02bdb9c GIT binary patch literal 62092 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 Zki)MCXBB)ZLk2*8G7^g7RbLDO{s+W3(P01p literal 0 HcmV?d00001 diff --git a/public/naver-login.png b/public/naver-login.png new file mode 100644 index 0000000000000000000000000000000000000000..7c727531412b60cac9578888956a65432c2b7ef9 GIT binary patch literal 10127 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 HOkeyTJcvT< literal 0 HcmV?d00001 diff --git a/src/app/pages/landing-page.tsx b/src/app/pages/landing-page.tsx index 908298d2fa..1de81ee1a7 100644 --- a/src/app/pages/landing-page.tsx +++ b/src/app/pages/landing-page.tsx @@ -169,10 +169,7 @@ export function LandingPage() { return ( - +

공동주거생활의 A to Z

@@ -183,18 +180,12 @@ export function LandingPage() { - kakao + kakao - naver + naver From 87d2840a2f764dd1bd9c59c7144b4100ab104508 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Fri, 3 May 2024 14:13:10 +0900 Subject: [PATCH 052/130] fix: formatting time (#57) --- src/components/FloatingChatting.tsx | 3 +- src/components/chat/ChattingRoom.tsx | 44 ++++++++++++++++++++----- src/components/chat/ReceiverMessage.tsx | 16 +++++++-- src/components/chat/SenderMessage.tsx | 12 +++++-- src/shared/get-local-time.ts | 7 ++++ src/shared/index.ts | 1 + 6 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/shared/get-local-time.ts diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 78e69c2fdc..58b0b5f571 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -174,8 +174,9 @@ export function FloatingChatting() { }, [chatRoomData]); const chatRoomList = useChatRoomList(auth?.accessToken); + useEffect(() => { - if (chatRoomList.data !== undefined) { + if (chatRoomList.data != null) { const chatRoomListData: ChatRoom[] = chatRoomList.data.data; setChatRooms(chatRoomListData); } diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 58c93c7082..2fe63d3ad6 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -127,7 +127,21 @@ const styles = { interface Content { roomId: number; message: string; - sender?: string; + sender: string; +} + +function calTimeDiff(time: string) { + const lastTime = new Date(time); + const currentTime = new Date(); + const timeDiff = Math.floor( + (currentTime.getTime() - lastTime.getTime()) / (1000 * 60), + ); + + if (timeDiff < 60) return `${timeDiff}분 전`; + + if (timeDiff < 60 * 24) return `${Math.floor(timeDiff / 60)}시간 전`; + + return `${Math.floor(timeDiff / (60 * 24))}일 전`; } export function ChattingRoom({ @@ -234,7 +248,7 @@ export function ChattingRoom({ onRoomClick(isBackClick); }; - function handleKeyDown(event: React.KeyboardEvent) { + function handleKeyUp(event: React.KeyboardEvent) { if (event.keyCode === 13) { sendMessage(); } @@ -265,7 +279,7 @@ export function ChattingRoom({ /> {roomName} - {lastTime} + {calTimeDiff(lastTime)} {isMenuClick && ( @@ -280,11 +294,18 @@ export function ChattingRoom({

{message.sender === userId ? ( - + ) : ( - + )}
@@ -293,11 +314,18 @@ export function ChattingRoom({
{message.sender === userId ? ( - + ) : ( - + )}
@@ -310,7 +338,7 @@ export function ChattingRoom({ onChange={e => { setInputMessage(e.target.value); }} - onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} /> diff --git a/src/components/chat/ReceiverMessage.tsx b/src/components/chat/ReceiverMessage.tsx index 4b9be4c680..c896533c3e 100644 --- a/src/components/chat/ReceiverMessage.tsx +++ b/src/components/chat/ReceiverMessage.tsx @@ -2,6 +2,8 @@ import styled from 'styled-components'; +import { getLocalTime } from '@/shared'; + const styles = { container: styled.div` display: flex; @@ -108,12 +110,20 @@ const styles = { `, }; -export function ReceiverMessage({ message }: { message: string }) { +export function ReceiverMessage({ + message, + reciever, + time, +}: { + message: string; + reciever: string; + time: string; +}) { return ( - 김마루 + {reciever} @@ -126,7 +136,7 @@ export function ReceiverMessage({ message }: { message: string }) { {message} - 11:31 AM + {getLocalTime(time)} diff --git a/src/components/chat/SenderMessage.tsx b/src/components/chat/SenderMessage.tsx index 3b567617f9..20864a1216 100644 --- a/src/components/chat/SenderMessage.tsx +++ b/src/components/chat/SenderMessage.tsx @@ -2,6 +2,8 @@ import styled from 'styled-components'; +import { getLocalTime } from '@/shared'; + const styles = { container: styled.div` display: inline-flex; @@ -84,7 +86,13 @@ const styles = { `, }; -export function SenderMessage({ message }: { message: string }) { +export function SenderMessage({ + message, + time, +}: { + message: string; + time: string; +}) { return ( @@ -92,7 +100,7 @@ export function SenderMessage({ message }: { message: string }) { {message} - 11:31 AM + {getLocalTime(time)} diff --git a/src/shared/get-local-time.ts b/src/shared/get-local-time.ts new file mode 100644 index 0000000000..85f9bf2d22 --- /dev/null +++ b/src/shared/get-local-time.ts @@ -0,0 +1,7 @@ +export function getLocalTime(isoString: string) { + return new Date(isoString).toLocaleString('en-US', { + hour12: true, + hour: 'numeric', + minute: '2-digit', + }); +} diff --git a/src/shared/index.ts b/src/shared/index.ts index b6473b97da..f38d13e3d0 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1 +1,2 @@ export * from './get-age'; +export * from './get-local-time'; From 416f97fdc191bec26e2a727b798745b9c39283ff Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 3 May 2024 15:06:15 +0900 Subject: [PATCH 053/130] fix: modify routing condition --- src/app/lib/providers/AuthProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index 968f560d6e..c9372809a8 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -37,7 +37,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { .catch((err: Error) => { if (isAxiosError(err)) { remove({ type: 'local', key: 'refreshToken' }); - router.replace('/'); + if (pathName !== '/') router.replace('/'); } }); } else { From ef67b94391cda74aed9f01c5de3b89665fb8844b Mon Sep 17 00:00:00 2001 From: he2e2 Date: Fri, 3 May 2024 15:19:49 +0900 Subject: [PATCH 054/130] fix: profile GET -> POST (#71) --- src/app/pages/profile-page.tsx | 23 ++++++++++++++------- src/app/pages/setting-page.tsx | 16 +++++++++----- src/components/NavigationBar.tsx | 31 ++++------------------------ src/components/SearchBox.tsx | 10 ++------- src/features/profile/profile.api.ts | 25 +++++++++------------- src/features/profile/profile.dto.ts | 2 +- src/features/profile/profile.hook.ts | 11 +++++----- 7 files changed, 48 insertions(+), 70 deletions(-) diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index 924dfa193d..7c46a1af4c 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -9,8 +9,8 @@ import { useAuthValue, useUserData } from '@/features/auth'; import { useFollowUser, useFollowingListData, - useProfileData, useUnfollowUser, + useUserProfile, } from '@/features/profile'; const styles = { @@ -574,15 +574,21 @@ export function ProfilePage({ memberId }: { memberId: string }) { const auth = useAuthValue(); const { data } = useUserData(auth?.accessToken !== undefined); - const id = data?.memberId; + const authId = data?.memberId; - const user = useProfileData(memberId); const [userData, setUserData] = useState(null); const [isMySelf, setIsMySelf] = useState(false); + const { mutate: profile, data: profileData } = useUserProfile(memberId); + const [profileImg, setProfileImg] = useState(''); + + useEffect(() => { + profile(); + }, [auth]); + useEffect(() => { - if (user.data !== undefined) { - const userProfileData = user.data.data.authResponse; + if (profileData?.data !== undefined) { + const userProfileData = profileData.data.authResponse; const { name, email, @@ -604,11 +610,12 @@ export function ProfilePage({ memberId }: { memberId: string }) { myCardId, mateCardId, }); - if (id === memberId) { + setProfileImg(profileData.data.profileImage); + if (authId === memberId) { setIsMySelf(true); } } - }, [user.data, memberId]); + }, [profileData, memberId]); return ( @@ -616,7 +623,7 @@ export function ProfilePage({ memberId }: { memberId: string }) { name={userData?.name ?? ''} email={userData?.email ?? ''} phoneNum={userData?.phoneNumber ?? ''} - src={user.data?.data.profileImage} + src={profileImg} memberId={memberId} isMySelf={isMySelf} /> diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 846c130bf2..57a50baddd 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -6,10 +6,11 @@ import { useEffect, useState, useRef } from 'react'; import styled from 'styled-components'; import { UserInputSection } from '@/components'; +import { useAuthValue } from '@/features/auth'; import { - useProfileData, usePutUserCard, useUserCard, + useUserProfile, } from '@/features/profile'; import Location from '@/public/option-img/location_on.svg'; import Meeting from '@/public/option-img/meeting_room.svg'; @@ -153,18 +154,23 @@ export function SettingPage({ cardId }: { cardId: number }) { const isMySelf = isMySelfStr === 'true'; const type = params.get('type') ?? ''; - const user = useProfileData(memberId); + const auth = useAuthValue(); + const { mutate: profile, data: profileData } = useUserProfile(memberId); const [userData, setUserData] = useState(null); useEffect(() => { - if (user.data !== undefined) { - const userProfileData = user.data.data.authResponse; + profile(); + }, [auth]); + + useEffect(() => { + if (profileData?.data !== undefined) { + const userProfileData = profileData.data.authResponse; if (userProfileData !== undefined) { const { name, birthYear, gender } = userProfileData; setUserData({ name, gender, birthYear }); } } - }, [user.data]); + }, [profileData]); const card = useUserCard(cardId); const [features, setFeatures] = useState(null); diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index f50e8e7335..2d22409b62 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -123,18 +123,10 @@ export function NavigationBar() { } }; - interface user { - memberId: string; - nickname: string; - profileImageUrl: string; - } - - const [isSearchBox, setIsSearchBox] = useState(false); - const [email, setEmail] = useState(''); + const [email, setEmail] = useState(''); const [enter, setEnter] = useState(false); const { mutate: search, data: searchUser } = useSearchUser(email); - const [userData, setUserData] = useState(); useEffect(() => { if (enter) { @@ -144,7 +136,8 @@ export function NavigationBar() { }, [enter]); useEffect(() => { - setUserData(searchUser?.data); + if (searchUser?.data != null) + router.replace(`/profile/${searchUser?.data.memberId}`); }, [searchUser]); return ( @@ -153,23 +146,7 @@ export function NavigationBar() { maru - - {isSearchBox && ( - - { - setIsSearchBox(false); - }} - > - - {userData?.nickname ?? ''} - - - )} + 메이트찾기 diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx index 22a334dc67..b4ed134a5d 100644 --- a/src/components/SearchBox.tsx +++ b/src/components/SearchBox.tsx @@ -24,11 +24,9 @@ const styles = { }; export function SearchBox({ - onClick, onContentChange, onEnter, }: { - onClick: React.Dispatch>; onContentChange: React.Dispatch>; onEnter: React.Dispatch>; }) { @@ -45,18 +43,14 @@ export function SearchBox({ } return ( - { - onClick(prev => !prev); - }} - > + { setContent(e.target.value); }} - onKeyDown={handleKeyUp} + onKeyUp={handleKeyUp} /> ); diff --git a/src/features/profile/profile.api.ts b/src/features/profile/profile.api.ts index 9fc9d11ba3..fe5654a2b2 100644 --- a/src/features/profile/profile.api.ts +++ b/src/features/profile/profile.api.ts @@ -1,19 +1,20 @@ import axios from 'axios'; import { - type GetUserProfileDTO, + type PostUserProfileDTO, type GetUserCardDTO, type PutUserCardDTO, type GetFollowingListDTO, type PostSearchDTO, } from './profile.dto'; -export const getUserProfileData = async (memberId: string) => - await axios - .get(`/maru-api/profile/${memberId}`, { - // params: { memberId: memberId }, - }) - .then(res => res.data); +export const postUserProfile = async (memberId: string) => { + const res = await axios.post(`/maru-api/profile`, { + memberId: memberId, + }); + + return res.data; +}; export const getUserCard = async (cardId: number) => await axios @@ -45,10 +46,7 @@ export const postFollowUser = async (memberId: string) => { .post(`/maru-api/profile/follow`, { memberId: memberId, }) - .then(res => { - console.log('follow'); - return res.data; - }); + .then(res => res.data); }; export const postUnfollowUser = async (memberId: string) => { @@ -56,10 +54,7 @@ export const postUnfollowUser = async (memberId: string) => { .post(`/maru-api/profile/unfollow`, { memberId: memberId, }) - .then(res => { - console.log('unfollow'); - return res.data; - }); + .then(res => res.data); }; export const postSearchUser = async (email: string) => { diff --git a/src/features/profile/profile.dto.ts b/src/features/profile/profile.dto.ts index af463e5842..3811ef0f7c 100644 --- a/src/features/profile/profile.dto.ts +++ b/src/features/profile/profile.dto.ts @@ -1,6 +1,6 @@ import { type SuccessBaseDTO } from '@/shared/types'; -export interface GetUserProfileDTO extends SuccessBaseDTO { +export interface PostUserProfileDTO extends SuccessBaseDTO { data: { authResponse: { memberId: string; diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index 6c27a14033..7d91a4b9a2 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -1,20 +1,19 @@ import { useQuery, useMutation } from '@tanstack/react-query'; import { - getUserProfileData, getUserCard, getFollowingListData, putUserCard, postSearchUser, postUnfollowUser, postFollowUser, + postUserProfile, } from './profile.api'; -export const useProfileData = (memberId: string) => - useQuery({ - queryKey: [`/api/profile`, memberId], - queryFn: async () => await getUserProfileData(memberId), - enabled: memberId !== undefined, +export const useUserProfile = (memberId: string) => + useMutation({ + mutationFn: async () => await postUserProfile(memberId), + onSuccess: data => data.data, }); export const useUserCard = (cardId: number) => From 02b9db6f693a92fd41ab690bc5ecddc369ded26d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 3 May 2024 17:18:23 +0900 Subject: [PATCH 055/130] fix: apply changes shared post API (#63) --- src/app/pages/writing-post-page.tsx | 74 ++++++++++++++++++----------- src/features/shared/shared.hook.ts | 21 ++++++-- src/features/shared/shared.type.ts | 15 +++--- src/shared/types/floor-type.ts | 8 ++-- src/shared/types/rental-type.ts | 6 +-- src/shared/types/room-type.ts | 22 +++------ 6 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index ff6e6f4dd2..3cf33911bb 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -17,6 +17,14 @@ import { type ImageFile, } from '@/features/shared'; import { useToast } from '@/features/toast'; +import { + type RentalType, + RoomTypeValue, + RentalTypeValue, + type RoomType, + FloorTypeValue, + type FloorType, +} from '@/shared/types'; const styles = { pageContainer: styled.div` @@ -391,12 +399,21 @@ const styles = { `, }; -const DealOptions = ['월세', '전세']; -const RoomOptions = ['원룸', '빌라/투룸이상', '아파트', '오피스텔']; +const DealOptions = { 월세: 'MONTHLY', 전세: 'JEONSE' }; +const RoomOptions = { + 원룸: 'ONE_ROOM', + '빌라/투룸이상': 'TWO_ROOM_VILLA', + 아파트: 'APT', + 오피스텔: 'OFFICE_TEL', +}; const LivingRoomOptions = ['유', '무']; const RoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; const RestRoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; -const FloorOptions = ['지상', '반지하', '옥탑']; +const FloorOptions = { + 지상: 'GROUND', + 반지하: 'SEMI_BASEMENT', + 옥탑: 'PENTHOUSE', +}; const AdditionalOptions = { canPark: '주차가능', hasAirConditioner: '에어컨', @@ -428,7 +445,6 @@ export function WritingPostPage() { selectedOptions, selectedExtraOptions, expectedMonthlyFee, - isPostCreatable, setTitle, setContent, setImages, @@ -448,7 +464,7 @@ export function WritingPostPage() { mbti, major, budget, - isMateCardCreatable, + derivedFeatures, setBirthYear, setMbti, setMajor, @@ -512,7 +528,8 @@ export function WritingPostPage() { }; const handleCreatePost = (event: React.MouseEvent) => { - if (!isPostCreatable || !isMateCardCreatable) return; + createToast({ message: '생성 버튼 클릭', option: { duration: 1000 } }); + // if (!isPostCreatable || !isMateCardCreatable) return; const rentalType = selectedOptions.budget; const { roomType } = selectedOptions; @@ -542,6 +559,10 @@ export function WritingPostPage() { | '3개 이상'; const numberOfBathRoom = RestRoomCountOptions[numberOfBathRoomOption]; + const rentalTypeValue = RentalTypeValue[rentalType as RentalType]; + const roomTypeValue = RoomTypeValue[roomType as RoomType]; + const floorTypeValue = FloorTypeValue[floorType as FloorType]; + (async () => { try { const getResults = await Promise.allSettled( @@ -584,36 +605,35 @@ export function WritingPostPage() { imageFilesData: uploadedImages, postData: { title, content }, transactionData: { - rentalType, + rentalType: rentalTypeValue, expectedPayment: expectedMonthlyFee, }, roomDetailData: { - roomType, - floorType, + roomType: roomTypeValue, + floorType: floorTypeValue, size: houseSize, numberOfRoom, numberOfBathRoom, hasLivingRoom: selectedOptions.livingRoom === '유', recruitmentCapacity: mateLimit, extraOption: { - canPark: selectedExtraOptions.canPark, - hasAirConditioner: selectedExtraOptions.hasAirConditioner, - hasRefrigerator: selectedExtraOptions.hasRefrigerator, - hasWasher: selectedExtraOptions.hasWasher, - hasTerrace: selectedExtraOptions.hasTerrace, + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: + selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, }, }, locationData: { - city: address?.roadAddress.split(' ').slice(0, 2).join(' '), oldAddress: address?.jibunAddress, roadAddress: address?.roadAddress, - detailAddress: '', }, roomMateCardData: { location: address?.roadAddress, - features: [], + features: derivedFeatures, }, - participationMemberIds: [], + participationMemberIds: ['kakao_3401909236'], }, { onSuccess: () => { @@ -820,12 +840,12 @@ export function WritingPostPage() { 거래 정보 거래 방식 - {DealOptions.map(option => ( + {Object.entries(DealOptions).map(([option, value]) => ( { - handleOptionClick('budget', option); + handleOptionClick('budget', value); }} /> {option} @@ -850,12 +870,12 @@ export function WritingPostPage() { 방 정보 - {FloorOptions.map(option => ( + {Object.entries(FloorOptions).map(([option, value]) => ( { - handleOptionClick('floorType', option); + handleOptionClick('floorType', value); }} /> {option} @@ -878,12 +898,12 @@ export function WritingPostPage() { 방 종류 - {RoomOptions.map(option => ( + {Object.entries(RoomOptions).map(([option, value]) => ( { - handleOptionClick('roomType', option); + handleOptionClick('roomType', value); }} /> {option} diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 5e7cebe148..d872c56539 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -255,11 +255,24 @@ export const useUserInputSection = () => { const options: string[] = []; features.options.forEach(option => options.push(option)); + let mateAge: number | null = null; + if (features?.mateAge != null) { + if (features.mateAge === '동갑') { + mateAge = 0; + } else if (features.mateAge === '상관없어요') { + mateAge = null; + } else { + mateAge = Number(features.mateAge.slice(1)); + } + } else { + mateAge = null; + } + return { - smoking: features?.smoking, - room: features?.room, - mateAge: features?.mateAge, - options, + smoking: features?.smoking ?? '상관없어요', + roomSharingOption: features?.room ?? '상관없어요', + mateAge: mateAge ?? null, + options: JSON.stringify(options), }; }, [features]); diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 80e85e55ad..f21568be29 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -39,12 +39,12 @@ export interface CreateSharedPostProps { content: string; }; transactionData: { - rentalType: string; + rentalType: number; expectedPayment: number; }; roomDetailData: { - roomType: string; - floorType: string; + roomType: number; + floorType: number; size: number; numberOfRoom: number; numberOfBathRoom: number; @@ -59,14 +59,17 @@ export interface CreateSharedPostProps { }; }; locationData: { - city: string; oldAddress: string; roadAddress: string; - detailAddress: string; }; roomMateCardData: { location: string; - features: string[]; + features: { + smoking: string; + roomSharingOption: string; + mateAge: number | null; // 0 ~ 10: +- 범위 값, null: 상관 없어요. + options: string; + }; }; participationMemberIds: string[]; } diff --git a/src/shared/types/floor-type.ts b/src/shared/types/floor-type.ts index 05fa210266..f0598d5beb 100644 --- a/src/shared/types/floor-type.ts +++ b/src/shared/types/floor-type.ts @@ -1,7 +1,7 @@ export type FloorType = 'GROUND' | 'SEMI_BASEMENT' | 'PENTHOUSE'; -export const FloorTypeValue: Record = { - GROUND: '0', - SEMI_BASEMENT: '1', - PENTHOUSE: '2', +export const FloorTypeValue: Record = { + GROUND: 0, + SEMI_BASEMENT: 1, + PENTHOUSE: 2, }; diff --git a/src/shared/types/rental-type.ts b/src/shared/types/rental-type.ts index 03f7bcee4d..2e898bfc1e 100644 --- a/src/shared/types/rental-type.ts +++ b/src/shared/types/rental-type.ts @@ -1,6 +1,6 @@ export type RentalType = 'MONTHLY' | 'JEONSE'; -export const RentalTypeValue: Record = { - MONTHLY: '0', - JEONSE: '1', +export const RentalTypeValue: Record = { + MONTHLY: 0, + JEONSE: 1, }; diff --git a/src/shared/types/room-type.ts b/src/shared/types/room-type.ts index dbc63d4789..cde668b511 100644 --- a/src/shared/types/room-type.ts +++ b/src/shared/types/room-type.ts @@ -1,18 +1,8 @@ -export type RoomType = - | 'VILLA_1' - | 'VILLA_2' - | 'VILLA_3' - | 'OFFICE_TEL_1' - | 'OFFICE_TEL_2' - | 'OFFICE_TEL_3' - | 'APT'; +export type RoomType = 'ONE_ROOM' | 'TWO_ROOM_VILLA' | 'APT' | 'OFFICE_TEL'; -export const RoomTypeValue: Record = { - VILLA_1: '0', - VILLA_2: '1', - VILLA_3: '2', - OFFICE_TEL_1: '3', - OFFICE_TEL_2: '4', - OFFICE_TEL_3: '5', - APT: '6', +export const RoomTypeValue: Record = { + ONE_ROOM: 0, + TWO_ROOM_VILLA: 1, + APT: 2, + OFFICE_TEL: 3, }; From 5c51b5f9ff3c8a3414b5e284b2d307f92b131730 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 3 May 2024 17:31:48 +0900 Subject: [PATCH 056/130] fix: add participation data (#63) --- src/app/pages/writing-post-page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 3cf33911bb..02cc0d13f0 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -9,6 +9,7 @@ import { LocationSearchBox, MateSearchBox, } from '@/components/writing-post-page'; +import { useAuthValue } from '@/features/auth'; import { getImageURL, putImage } from '@/features/image'; import { useCreateSharedPost, @@ -476,6 +477,8 @@ export function WritingPostPage() { const { mutate } = useCreateSharedPost(); const { createToast } = useToast(); + const auth = useAuthValue(); + const handleTitleInputChanged = ( event: React.ChangeEvent, ) => { @@ -633,7 +636,8 @@ export function WritingPostPage() { location: address?.roadAddress, features: derivedFeatures, }, - participationMemberIds: ['kakao_3401909236'], + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], }, { onSuccess: () => { From 5f537e372104d22cda010a88da4c1955df1e7164 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sun, 5 May 2024 13:54:59 +0900 Subject: [PATCH 057/130] feat: add nickname, exit room (#57) --- src/components/FloatingChatting.tsx | 161 ++++++++++-------------- src/components/chat/ChattingList.tsx | 11 -- src/components/chat/ChattingRoom.tsx | 53 +++++--- src/components/chat/ReceiverMessage.tsx | 10 +- src/components/chat/SenderMessage.tsx | 4 +- src/features/chat/chat.api.ts | 20 +-- src/features/chat/chat.dto.ts | 1 + src/features/chat/chat.hook.ts | 14 ++- src/shared/get-local-time.ts | 13 +- 9 files changed, 147 insertions(+), 140 deletions(-) diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 58b0b5f571..5d96d22c32 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -7,11 +7,7 @@ import { ChattingList } from './chat/ChattingList'; import { ChattingRoom } from './chat/ChattingRoom'; import { useAuthValue, useUserData } from '@/features/auth'; -import { - useChatRoomList, - useCreateChatRoom, - useEnterChatRoom, -} from '@/features/chat'; +import { useChatRoomList } from '@/features/chat'; const styles = { chattingButton: styled.div` @@ -124,55 +120,29 @@ interface ChatRoom { lastMessageTime: string; } -export function FloatingChatting() { - const [isChatOpen, setIsChatOpen] = useState(false); +function FloatingChattingBox() { const [isChatRoomOpen, setIsChatRoomOpen] = useState(false); const [chatRooms, setChatRooms] = useState([]); const [selectedRoomId, setSelectedRoomId] = useState(0); const [selectedRoomName, setSelectedRoomName] = useState(''); const [selectedRoomLastTime, setSelectedRoomLastTime] = useState(''); - const [roomData, setRoomData] = useState< - | [ - { - messageId: string; - sender: string; - message: string; - createdAt: string; - }, - ] - | undefined - >(); - - const toggleChat = () => { - setIsChatOpen(prevState => !prevState); - if (isChatRoomOpen) setIsChatRoomOpen(false); - }; const auth = useAuthValue(); const { data } = useUserData(auth?.accessToken !== undefined); const [userId, setUserId] = useState(''); + const [userName, setUserName] = useState(''); useEffect(() => { - if (data !== undefined) setUserId(data.memberId); + if (data !== undefined) { + setUserId(data.memberId); + setUserName(data.name); + } }, [data]); - const page = 0; - const size = 2; - const { mutate: enterChatting, data: chatRoomData } = useEnterChatRoom( - selectedRoomId, - page, - size, - ); - const handleChatRoomClick = () => { - enterChatting(); setIsChatRoomOpen(prev => !prev); }; - useEffect(() => { - setRoomData(chatRoomData?.data); - }, [chatRoomData]); - const chatRoomList = useChatRoomList(auth?.accessToken); useEffect(() => { @@ -182,72 +152,56 @@ export function FloatingChatting() { } }, [chatRoomList.data]); - const roomName = 'test2'; - const members = ['naver_htT4VdDRPKqGqKpnncpa71HCA4CVg5LdRC1cWZhCnF8']; - const { mutate: chattingMutate } = useCreateChatRoom(roomName, members); + // const roomName = 'test2'; + // const members = ['naver_htT4VdDRPKqGqKpnncpa71HCA4CVg5LdRC1cWZhCnF8']; + // const { mutate: chattingCreate } = useCreateChatRoom(roomName, members); return ( <> - - - - {isChatOpen && ( - - -
- - maru{' '} - - - chat - -
-
- - -
-
-
- */} + + + {chatRooms.map((room, index) => ( + { - chattingMutate(); + handleChatRoomClick(); + setSelectedRoomId(room.roomId); + setSelectedRoomName(room.roomName); + setSelectedRoomLastTime(room.lastMessageTime); }} - > - 채팅방 생성 - -
- - - {chatRooms.map((room, index) => ( - { - handleChatRoomClick(); - setSelectedRoomId(room.roomId); - setSelectedRoomName(room.roomName); - setSelectedRoomLastTime(room.lastMessageTime); - }} - /> - ))} - -
- )} + /> + ))} + +
{isChatRoomOpen && ( ); } + +export function FloatingChatting() { + const [isChatOpen, setIsChatOpen] = useState(false); + + const toggleChat = () => { + setIsChatOpen(prevState => !prevState); + }; + + return ( + <> + + + + {isChatOpen && } + + ); +} diff --git a/src/components/chat/ChattingList.tsx b/src/components/chat/ChattingList.tsx index 1e87c3b82e..f08723c0df 100644 --- a/src/components/chat/ChattingList.tsx +++ b/src/components/chat/ChattingList.tsx @@ -35,16 +35,6 @@ const styles = { border-radius: 50%; background: url('__avatar_url.png') lightgray 50% / cover no-repeat; `, - activeCircle: styled.div` - width: 0.6rem; - height: 0.6rem; - position: absolute; - left: 2.375rem; - bottom: 0.125rem; - border-radius: 50%; - border: 2px solid #fff; - background-color: #27da4e; - `, roomName: styled.p` color: #000; @@ -101,7 +91,6 @@ export function ChattingList({ - {name} {lastMessage} diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index 2fe63d3ad6..cde101369a 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -9,6 +9,7 @@ import { ReceiverMessage } from './ReceiverMessage'; import { SenderMessage } from './SenderMessage'; import { useAuthValue } from '@/features/auth'; +import { useEnterChatRoom, useExitChatRoom } from '@/features/chat'; const styles = { container: styled.div` @@ -128,10 +129,12 @@ interface Content { roomId: number; message: string; sender: string; + nickname: string; } function calTimeDiff(time: string) { const lastTime = new Date(time); + lastTime.setHours(lastTime.getHours() + 9); const currentTime = new Date(); const timeDiff = Math.floor( (currentTime.getTime() - lastTime.getTime()) / (1000 * 60), @@ -145,24 +148,15 @@ function calTimeDiff(time: string) { } export function ChattingRoom({ - chatRoomData, userId, + userName, roomId, roomName, lastTime, onRoomClick, }: { - chatRoomData: - | [ - { - messageId: string; - sender: string; - message: string; - createdAt: string; - }, - ] - | undefined; userId: string | undefined; + userName: string; roomId: number; roomName: string; lastTime: string; @@ -176,6 +170,25 @@ export function ChattingRoom({ const auth = useAuthValue(); const user = userId; + const [roomData, setRoomData] = useState< + | [ + { + messageId: string; + sender: string; + message: string; + createdAt: string; + nickname: string; + }, + ] + | undefined + >(); + + const chattingRoom = useEnterChatRoom(roomId, 0, 2); + useEffect(() => { + if (chattingRoom != null) { + setRoomData(chattingRoom.data?.data); + } + }, [chattingRoom]); useEffect(() => { const initializeChat = async () => { @@ -216,7 +229,7 @@ export function ChattingRoom({ } return () => { - if (stompClient !== null && stompClient.connected) { + if (stompClient != null && stompClient.connected) { void stompClient.deactivate(); } }; @@ -232,6 +245,7 @@ export function ChattingRoom({ roomId: roomId, sender: user, message: inputMessage, + nickname: userName, }), }); } @@ -243,7 +257,10 @@ export function ChattingRoom({ setIsMenuClick(prev => !prev); }; + const { mutate: exit } = useExitChatRoom(roomId); + const handleBackClick = () => { + exit(); setIsBackClick(prev => !prev); onRoomClick(isBackClick); }; @@ -261,7 +278,7 @@ export function ChattingRoom({ messageContainerRef.current.scrollTop = messageContainerRef.current.scrollHeight; } - }, [chatRoomData]); + }, [roomData]); useEffect(() => { if (messageContainerRef.current != null) { @@ -287,7 +304,7 @@ export function ChattingRoom({ )} - {chatRoomData + {roomData ?.slice() .reverse() ?.map((message, index) => ( @@ -297,14 +314,16 @@ export function ChattingRoom({ ) : ( )} @@ -317,14 +336,16 @@ export function ChattingRoom({ ) : ( )} diff --git a/src/components/chat/ReceiverMessage.tsx b/src/components/chat/ReceiverMessage.tsx index c896533c3e..f0eab0367e 100644 --- a/src/components/chat/ReceiverMessage.tsx +++ b/src/components/chat/ReceiverMessage.tsx @@ -112,18 +112,20 @@ const styles = { export function ReceiverMessage({ message, - reciever, + receiver, time, + type, }: { message: string; - reciever: string; + receiver: string; time: string; + type: string; }) { return ( - {reciever} + {receiver} @@ -136,7 +138,7 @@ export function ReceiverMessage({ {message} - {getLocalTime(time)} + {getLocalTime(time, type)} diff --git a/src/components/chat/SenderMessage.tsx b/src/components/chat/SenderMessage.tsx index 20864a1216..e26e1505a8 100644 --- a/src/components/chat/SenderMessage.tsx +++ b/src/components/chat/SenderMessage.tsx @@ -89,9 +89,11 @@ const styles = { export function SenderMessage({ message, time, + type, }: { message: string; time: string; + type: string; }) { return ( @@ -100,7 +102,7 @@ export function SenderMessage({ {message} - {getLocalTime(time)} + {getLocalTime(time, type)} diff --git a/src/features/chat/chat.api.ts b/src/features/chat/chat.api.ts index b96ceefcdd..81cb1d41be 100644 --- a/src/features/chat/chat.api.ts +++ b/src/features/chat/chat.api.ts @@ -31,25 +31,27 @@ export const postInviteUser = async (roomId: number, members: string[]) => { .then(res => res.data); }; -export const postEnterChatRoom = async ( +export const getEnterChatRoom = async ( roomId: number, page: number, size: number, ) => { - const res = await axios.post( - `/maru-api/chatRoom/chat`, + const res = await axios.get( + `/maru-api/chatRoom/${roomId}/chat`, { - roomId: roomId, - page: page, - size: size, + params: { + roomId: roomId, + page: page, + size: size, + }, }, ); - + console.log(res.data); return res.data; }; -export const getEnterChatRoom = async () => { - await axios.get(`/maru-api/chatRoom/chat`); +export const postExitChatRoom = async (roomId: number) => { + await axios.post(`/maru-api/chatRoom/${roomId}/exit`).then(res => res.data); }; export const getChatRoomUser = async (roomId: number) => diff --git a/src/features/chat/chat.dto.ts b/src/features/chat/chat.dto.ts index 0bee2c5a59..1a9ef08a9b 100644 --- a/src/features/chat/chat.dto.ts +++ b/src/features/chat/chat.dto.ts @@ -35,6 +35,7 @@ export interface PostChatRoomEnterDTO extends SuccessBaseDTO { sender: string; message: string; createdAt: string; + nickname: string; }, ]; } diff --git a/src/features/chat/chat.hook.ts b/src/features/chat/chat.hook.ts index 9a1eaec8b7..3fdd47e077 100644 --- a/src/features/chat/chat.hook.ts +++ b/src/features/chat/chat.hook.ts @@ -4,8 +4,9 @@ import { getChatRoomList, getChatRoomUser, postChatRoom, - postEnterChatRoom, + getEnterChatRoom, postInviteUser, + postExitChatRoom, } from './chat.api'; export const useChatRoomList = (token: string | undefined) => @@ -30,9 +31,16 @@ export const useInviteUsers = (roomId: number, members: string[]) => }); export const useEnterChatRoom = (roomId: number, page: number, size: number) => + useQuery({ + queryKey: [`/api/chatRoom/${roomId}/chat`, page, size], + queryFn: async () => await getEnterChatRoom(roomId, page, size), + }); + +export const useExitChatRoom = (roomId: number) => useMutation({ - mutationFn: async () => await postEnterChatRoom(roomId, page, size), - onSuccess: data => data.data, + mutationFn: async () => { + await postExitChatRoom(roomId); + }, }); export const useChatRoomUser = (roomId: number) => diff --git a/src/shared/get-local-time.ts b/src/shared/get-local-time.ts index 85f9bf2d22..a2c3fdd1a4 100644 --- a/src/shared/get-local-time.ts +++ b/src/shared/get-local-time.ts @@ -1,4 +1,15 @@ -export function getLocalTime(isoString: string) { +export function getLocalTime(isoString: string, type: string) { + if (type === 'server') { + const utcTime = new Date(isoString); + const localTime = utcTime.setHours(utcTime.getHours() + 9); + + return new Date(localTime).toLocaleString('en-US', { + hour12: true, + hour: 'numeric', + minute: '2-digit', + }); + } + return new Date(isoString).toLocaleString('en-US', { hour12: true, hour: 'numeric', From b38fa0d5b44128ee51499ab2c637832c1972d8c0 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 5 May 2024 21:22:57 +0900 Subject: [PATCH 058/130] fix: change hook name --- src/app/pages/writing-post-page.tsx | 4 ++-- src/components/UserInputSection.tsx | 4 ++-- src/components/card/VitalSection.tsx | 4 ++-- src/features/shared/shared.hook.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 02cc0d13f0..c91ddca333 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -14,7 +14,7 @@ import { getImageURL, putImage } from '@/features/image'; import { useCreateSharedPost, useCreateSharedPostProps, - useUserInputSection, + usePostMateCardInputSection, type ImageFile, } from '@/features/shared'; import { useToast } from '@/features/toast'; @@ -472,7 +472,7 @@ export function WritingPostPage() { setBudget, handleEssentialFeatureChange, handleOptionalFeatureChange, - } = useUserInputSection(); + } = usePostMateCardInputSection(); const { mutate } = useCreateSharedPost(); const { createToast } = useToast(); diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index 8d2229f43a..e693e37a77 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -30,6 +30,7 @@ interface SelectedState { } interface UserInputProps { + className?: string; gender?: string; birthYear?: string; location?: string; @@ -46,10 +47,10 @@ interface UserInputProps { onMbtiChange: React.Dispatch>; onMajorChange: React.Dispatch>; onBudgetChange: React.Dispatch>; - className?: string; } export function UserInputSection({ + className, gender, birthYear, location, @@ -66,7 +67,6 @@ export function UserInputSection({ onMbtiChange, onMajorChange, onBudgetChange, - className, }: UserInputProps) { return ( diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 0e1823094f..6c54ab0f98 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -282,13 +282,13 @@ export function VitalSection({ const [initialAge, setInitialAge] = useState(0); useEffect(() => { - if (vitalFeatures !== null) + if (vitalFeatures != null) setInitialAge(Number(vitalFeatures?.[2].split(':')[1].slice(1))); }, [vitalFeatures?.[2]]); const [ageValue, setAgeValue] = useState(0); useEffect(() => { - if (initialAge !== undefined) setAgeValue(initialAge); + if (initialAge != null) setAgeValue(initialAge); }, [initialAge]); const handleAgeChange = (e: React.ChangeEvent) => { diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index d872c56539..36ffe31147 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -210,7 +210,7 @@ export const useCreateSharedPostProps = () => { ); }; -export const useUserInputSection = () => { +export const usePostMateCardInputSection = () => { const [gender, setGender] = useState(undefined); const [birthYear, setBirthYear] = useState(undefined); const [location, setLocation] = useState(undefined); From 0378d7cdf33ae9fd0ce52c3aa10fe21d75a34487 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 5 May 2024 22:24:24 +0900 Subject: [PATCH 059/130] fix: apply changes GET /shared/posts/studio API (#63) --- src/entities/shared-post/shared-post.type.ts | 4 +- src/features/shared/shared.api.ts | 59 +++++++++++++++++--- src/features/shared/shared.type.ts | 16 +++++- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index 630dd8c9e5..deaafbff90 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -16,6 +16,7 @@ export interface SharedPostListItem { birthYear: string; gender: string; phoneNumber: string; + profileImageFileName: string; createdAt: Date; createdBy: string; modifiedAt: Date; @@ -24,15 +25,14 @@ export interface SharedPostListItem { roomInfo: { id: number; address: { - city: string; oldAddress: string; roadAddress: string; - detailAddress?: string; }; roomType: string; floorType: string; size: number; numberOfRoom: number; + numberOfBathRoom: number; rentalType: string; expectedPayment: number; }; diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index f9800c388f..40c42eea21 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -8,26 +8,69 @@ import { } from './shared.type'; import { + FloorTypeValue, RentalTypeValue, RoomTypeValue, type SuccessBaseDTO, } from '@/shared/types'; -const filterConvertToValues = (filter: GetSharedPostsFilter) => { - const result: Partial> = {}; +const filterConvertToValues = ({ + roomTypes, + rentalTypes, + expectedPaymentRange, + hasLivingRoom, + numberOfRoom, + roomSizeRange, + floorTypes, + canPark, + hasAirConditioner, + hasRefrigerator, + hasWasher, + hasTerrace, +}: GetSharedPostsFilter) => { + const result: { + roomTypes?: number[]; + rentalTypes?: number[]; + expectedPaymentRange?: { start: number; end: number }; + hasLivingRoom?: boolean; + numberOfRoom?: number; + roomSizeRange?: { start: number; end: number }; + floorTypes?: number[]; + canPark?: boolean; + hasAirConditioner?: boolean; + hasRefrigerator?: boolean; + hasWasher?: boolean; + hasTerrace?: boolean; + } = { + expectedPaymentRange, + hasLivingRoom, + numberOfRoom, + roomSizeRange, + canPark, + hasAirConditioner, + hasRefrigerator, + hasWasher, + hasTerrace, + }; - if (filter.roomType !== undefined) { - result.roomType = Object.values(filter.roomType).map(value => + if (roomTypes != null) { + result.roomTypes = Object.values(roomTypes).map(value => Number(RoomTypeValue[value]), ); } - if (filter.rentalType !== undefined) { - result.rentalType = Object.values(filter.rentalType).map(value => + if (rentalTypes != null) { + result.rentalTypes = Object.values(rentalTypes).map(value => Number(RentalTypeValue[value]), ); } + if (floorTypes != null) { + result.floorTypes = Object.values(floorTypes).map(value => + Number(FloorTypeValue[value]), + ); + } + return result; }; @@ -40,11 +83,11 @@ export const getSharedPosts = async ({ const baseURL = '/maru-api/shared/posts/studio'; let query = ''; - if (filter !== undefined) { + if (filter != null) { query += `filter=${JSON.stringify(filterConvertToValues(filter))}`; } - if (search !== undefined) { + if (search != null) { query += `&search=${search}`; } diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index f21568be29..5c603de87a 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -1,8 +1,18 @@ -import { type RentalType, type RoomType } from '@/shared/types'; +import { type FloorType, type RentalType, type RoomType } from '@/shared/types'; export interface GetSharedPostsFilter { - roomType?: RoomType[]; - rentalType?: RentalType[]; + roomTypes?: RoomType[]; + rentalTypes?: RentalType[]; + expectedPaymentRange?: { start: number; end: number }; + hasLivingRoom?: boolean; + numberOfRoom?: number; + roomSizeRange?: { start: number; end: number }; + floorTypes?: FloorType[]; + canPark?: boolean; + hasAirConditioner?: boolean; + hasRefrigerator?: boolean; + hasWasher?: boolean; + hasTerrace?: boolean; } export interface GetSharedPostsProps { From c479db68ad18992b799f2ef0a13589cce5322f4f Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 6 May 2024 01:18:18 +0900 Subject: [PATCH 060/130] feat: Add filter conditions when fetching posts (#73) --- src/app/pages/setting-page.tsx | 8 +-- src/app/pages/shared-posts-page.tsx | 11 ++-- src/app/pages/user-input-page.tsx | 12 ++-- src/app/pages/writing-post-page.tsx | 51 ++++++++++------ src/components/UserInputSection.tsx | 2 +- src/components/card/OptionSection.tsx | 5 +- src/components/card/VitalSection.tsx | 18 +++--- .../shared-posts/filter/ExtraInfoFilter.tsx | 28 ++------- .../shared-posts-filter.atom.ts | 1 + .../shared-posts-filter.hook.ts | 60 ++++++++++++++++++- .../shared-posts-filter.type.ts | 34 +++++------ .../recommendation/recommendation.api.ts | 4 +- .../recommendation/recommendation.dto.ts | 4 +- .../recommendation/recommendation.hook.ts | 4 +- src/features/shared/shared.hook.ts | 6 +- src/features/shared/shared.type.ts | 1 + 16 files changed, 155 insertions(+), 94 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 78137fcd1d..48f6eac346 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -134,7 +134,7 @@ const styles = { interface SelectedState { smoking?: string; - room?: string; + roomSharingOption?: string; mateAge?: string; } @@ -187,7 +187,7 @@ export function SettingPage({ cardId }: { cardId: number }) { setSelectedState({ ...selectedState, smoking: features[0].split(':')[1], - room: features[1].split(':')[1], + roomSharingOption: features[1].split(':')[1], }); } } @@ -280,7 +280,7 @@ export function SettingPage({ cardId }: { cardId: number }) { const location = locationInput ?? ''; const myFeatures = [ `smoking:${selectedState.smoking}`, - `room:${selectedState.room}`, + `roomSharingOption:${selectedState.roomSharingOption}`, `mateAge:${mateAge !== '' ? mateAge : undefined}`, `options:${options.join(',')}`, ]; @@ -371,7 +371,7 @@ export function SettingPage({ cardId }: { cardId: number }) { - 메이트와 {selectedState.room} + 메이트와 {selectedState.roomSharingOption} diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index df5b7656c7..f7cdac45e2 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -120,9 +120,8 @@ export function SharedPostsPage() { useState(null); const { setAuthUserData } = useAuthActions(); - const { filter, reset } = useSharedPostsFilter(); - - const { data: userData } = useUserData(auth?.accessToken !== undefined); + const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); + const { data: userData } = useUserData(auth?.accessToken != null); const { page, @@ -138,7 +137,9 @@ export function SharedPostsPage() { sliceSize: 10, }); + // TODO: 디바운싱 추가 필요 const { data: sharedPosts } = useSharedPosts({ + filter: derivedFilter, enabled: auth?.accessToken != null && selected === 'hasRoom', page: page - 1, }); @@ -150,9 +151,9 @@ export function SharedPostsPage() { }); useEffect(() => { - reset(); + resetFilter(); return () => { - reset(); + resetFilter(); }; }, []); diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index 74d9d4e0de..83639fc5f6 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -226,7 +226,7 @@ interface UserProps { interface SelectedState { smoking: string | undefined; - room: string | undefined; + roomSharingOption: string | undefined; mateAge: string | undefined; } type SelectedOptions = Record; @@ -239,7 +239,7 @@ const useSelectedState = (): [ ] => { const [selectedState, setSelectedState] = useState({ smoking: undefined, - room: undefined, + roomSharingOption: undefined, mateAge: undefined, }); const [selectedOptions, setSelectedOptions] = useState({}); @@ -333,14 +333,14 @@ export function UserInputPage() { const location = locationInput ?? ''; const myFeatures = [ `smoking:${selectedState.smoking}`, - `room:${selectedState.room}`, + `roomSharingOption:${selectedState.roomSharingOption}`, `mateAge:${mateAge !== '' ? mateAge : undefined}`, `options:${myOptionsString.join(',')}`, ]; const mateFeatures = [ `smoking:${selectedMateState.smoking}`, - `room:${selectedMateState.room}`, + `roomSharingOption:${selectedMateState.roomSharingOption}`, `mateAge:${mateAge !== '' ? mateAge : undefined}`, `options:${mateOptionsString.join(',')}`, ]; @@ -413,7 +413,7 @@ export function UserInputPage() { - 메이트와 {selectedState.room} + 메이트와 {selectedState.roomSharingOption} @@ -455,7 +455,7 @@ export function UserInputPage() { $active={activeContainer === 'mate'} /> - 메이트와 {selectedMateState.room} + 메이트와 {selectedMateState.roomSharingOption} diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index c91ddca333..ace15f22cf 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -9,6 +9,13 @@ import { LocationSearchBox, MateSearchBox, } from '@/components/writing-post-page'; +import { + type RoomTypeFilter, + type DealTypeFilter, + type RoomCountTypeFilter, + type FloorTypeFilter, + type AdditionalInfoTypeFilter, +} from '@/entities/shared-posts-filter'; import { useAuthValue } from '@/features/auth'; import { getImageURL, putImage } from '@/features/image'; import { @@ -400,27 +407,38 @@ const styles = { `, }; -const DealOptions = { 월세: 'MONTHLY', 전세: 'JEONSE' }; -const RoomOptions = { +const DealOptions: Record = { + 월세: 'MONTHLY', + 전세: 'JEONSE', +}; +const RoomOptions: Record = { 원룸: 'ONE_ROOM', '빌라/투룸이상': 'TWO_ROOM_VILLA', 아파트: 'APT', 오피스텔: 'OFFICE_TEL', }; const LivingRoomOptions = ['유', '무']; -const RoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; -const RestRoomCountOptions = { '1개': 1, '2개': 2, '3개 이상': 3 }; -const FloorOptions = { +const RoomCountOptions: Record = { + '1개': 1, + '2개': 2, + '3개 이상': 3, +}; +const RestRoomCountOptions: Record = { + '1개': 1, + '2개': 2, + '3개 이상': 3, +}; +const FloorOptions: Record = { 지상: 'GROUND', 반지하: 'SEMI_BASEMENT', 옥탑: 'PENTHOUSE', }; -const AdditionalOptions = { - canPark: '주차가능', - hasAirConditioner: '에어컨', - hasRefrigerator: '냉장고', - hasWasher: '세탁기', - hasTerrace: '베란다/테라스', +const AdditionalOptions: Record = { + 주차가능: 'canPark', + 에어컨: 'hasAirConditioner', + 냉장고: 'hasRefrigerator', + 세탁기: 'hasWasher', + '베란다/테라스': 'hasTerrace', }; interface ButtonActiveProps { @@ -531,7 +549,6 @@ export function WritingPostPage() { }; const handleCreatePost = (event: React.MouseEvent) => { - createToast({ message: '생성 버튼 클릭', option: { duration: 1000 } }); // if (!isPostCreatable || !isMateCardCreatable) return; const rentalType = selectedOptions.budget; @@ -823,7 +840,7 @@ export function WritingPostPage() { type="mateCard" onVitalChange={(optionName, option) => { if ( - optionName === 'room' || + optionName === 'roomSharingOption' || optionName === 'smoking' || optionName === 'mateAge' ) { @@ -889,14 +906,14 @@ export function WritingPostPage() { 추가 옵션 {Object.entries(AdditionalOptions).map(([option, value]) => ( - + { - handleExtraOptionClick(value); + handleExtraOptionClick(option); }} /> - {value} + {option} ))} diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index e693e37a77..2dae9f6e7b 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -25,7 +25,7 @@ const styles = { interface SelectedState { smoking?: string; - room?: string; + roomSharingOption?: string; mateAge?: string; } diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index b0ca7fa5f7..8e92dc74e8 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -256,7 +256,10 @@ export function OptionSection({ useEffect(() => { if (budget !== undefined) { - const [min, max] = budget.split(',').map(Number); + const [min, max] = budget + .slice(1, budget.length - 1) + .split(',') + .map(Number); setInitialMin(min); setInitialMax(max); } diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 6c54ab0f98..2180e202cf 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -216,7 +216,7 @@ const years = Array.from( interface SelectedState { smoking?: string; - room?: string; + roomSharingOption?: string; mateAge?: string; } @@ -243,13 +243,13 @@ export function VitalSection({ }) { const [selectedState, setSelectedState] = useState({ smoking: undefined, - room: undefined, + roomSharingOption: undefined, }); useEffect(() => { setSelectedState({ ...selectedState, smoking: vitalFeatures?.[0].split(':')[1], - room: vitalFeatures?.[1].split(':')[1], + roomSharingOption: vitalFeatures?.[1].split(':')[1], }); }, [vitalFeatures]); @@ -411,30 +411,30 @@ export function VitalSection({ { if (isMySelf) { - handleOptionClick('room', '같은 방'); + handleOptionClick('roomSharingOption', '같은 방'); } }} > 같은 방 { if (isMySelf) { - handleOptionClick('room', '다른 방'); + handleOptionClick('roomSharingOption', '다른 방'); } }} > 다른 방 { if (isMySelf) { - handleOptionClick('room', '상관없어요'); + handleOptionClick('roomSharingOption', '상관없어요'); } }} > diff --git a/src/components/shared-posts/filter/ExtraInfoFilter.tsx b/src/components/shared-posts/filter/ExtraInfoFilter.tsx index ba7f86664e..0b1bbe0265 100644 --- a/src/components/shared-posts/filter/ExtraInfoFilter.tsx +++ b/src/components/shared-posts/filter/ExtraInfoFilter.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { - type ExtraInfoType, + type AdditionalInfoTypeFilter, useSharedPostsFilter, } from '@/entities/shared-posts-filter'; @@ -71,7 +71,7 @@ const styles = { export function ExtraInfoFilter() { const { filter, setFilter } = useSharedPostsFilter(); - const handleOptionClick = (option: ExtraInfoType) => { + const handleOptionClick = (option: AdditionalInfoTypeFilter) => { setFilter(prev => { const value = prev.extraInfo[option] ?? true; return { @@ -84,7 +84,7 @@ export function ExtraInfoFilter() { }); }; - const isSelectedChecker = (option: ExtraInfoType) => + const isSelectedChecker = (option: AdditionalInfoTypeFilter) => filter.extraInfo[option] === true; return ( @@ -93,9 +93,9 @@ export function ExtraInfoFilter() {
- -
); diff --git a/src/entities/shared-posts-filter/shared-posts-filter.atom.ts b/src/entities/shared-posts-filter/shared-posts-filter.atom.ts index 2ae0165964..415767c3eb 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.atom.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.atom.ts @@ -8,6 +8,7 @@ export const sharedPostsFilterState = atom({ roomInfo: { hasLivingRoom: false, }, + dealInfo: {}, extraInfo: {}, }, }); diff --git a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts index 8926fac694..25cbe8782f 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts @@ -4,18 +4,76 @@ import { useRecoilState, useResetRecoilState } from 'recoil'; import { sharedPostsFilterState } from './shared-posts-filter.atom'; import { type SharedPostsFilter } from './shared-posts-filter.type'; +import { type FloorType, type RentalType, type RoomType } from '@/shared/types'; + export const useSharedPostsFilter = () => { const [filter, setFilter] = useRecoilState( sharedPostsFilterState, ); const reset = useResetRecoilState(sharedPostsFilterState); + + const derivedFilter = useMemo<{ + roomTypes?: RoomType[]; + rentalTypes?: RentalType[]; + expectedPaymentRange?: { start: number; end: number }; + hasLivingRoom?: boolean; + numberOfRoom?: number; + numberOfRestRoom?: number; + roomSizeRange?: { start: number; end: number }; + floorTypes?: FloorType[]; + canPark?: boolean; + hasAirConditioner?: boolean; + hasRefrigerator?: boolean; + hasWasher?: boolean; + hasTerrace?: boolean; + }>(() => { + let numberOfRoom: number | undefined; + if (filter.roomInfo.roomCount === '1개') numberOfRoom = 1; + else if (filter.roomInfo.roomCount === '2개') numberOfRoom = 2; + else if (filter.roomInfo.roomCount === '3개 이상') numberOfRoom = 3; + + let numberOfRestRoom: number | undefined; + if (filter.roomInfo.restRoomCount === '1개') numberOfRestRoom = 1; + else if (filter.roomInfo.restRoomCount === '2개') numberOfRestRoom = 2; + else if (filter.roomInfo.restRoomCount === '3개 이상') numberOfRestRoom = 3; + + return { + roomTypes: [], // TODO: 다중 선택으로 수정 필요. + rentalTypes: [], // TODO: 다중 선택으로 수정 필요. + expectedPaymentRange: + filter.dealInfo?.expectedFee != null + ? { + start: filter.dealInfo.expectedFee.low, + end: filter.dealInfo.expectedFee.high, + } + : undefined, + hasLivingRoom: filter.roomInfo.hasLivingRoom, + numberOfRoom, + numberOfRestRoom, + roomSizeRange: + filter.roomInfo.size != null + ? { + start: filter.roomInfo.size.low, + end: filter.roomInfo.size.high, + } + : undefined, + floorTypes: [], // TODO: 다중 선택으로 수정 필요. + canPark: filter.extraInfo.주차가능, + hasAirConditioner: filter.extraInfo.에어컨, + hasRefrigerator: filter.extraInfo.냉장고, + hasWasher: filter.extraInfo.세탁기, + hasTerrace: filter.extraInfo['베란다/테라스'], + }; + }, [filter]); + return useMemo( () => ({ filter, + derivedFilter, setFilter, reset, }), - [filter, setFilter, reset], + [filter, derivedFilter, setFilter, reset], ); }; diff --git a/src/entities/shared-posts-filter/shared-posts-filter.type.ts b/src/entities/shared-posts-filter/shared-posts-filter.type.ts index abc19e98f8..3a947feb1e 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.type.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.type.ts @@ -1,32 +1,30 @@ -export type CardType = 'my' | 'mate'; -export type DealType = '전세' | '월세'; -export type RoomType = '원룸' | '빌라/투룸이상' | '아파트' | '오피스텔'; -export type RoomCountType = '1개' | '2개' | '3개 이상'; -export type FloorType = '지상' | '반지하' | '옥탑'; -export type ExtraInfoType = - | '주차 가능' +export type CardTypeFilter = 'my' | 'mate'; +export type DealTypeFilter = '전세' | '월세'; +export type RoomTypeFilter = '원룸' | '빌라/투룸이상' | '아파트' | '오피스텔'; +export type FloorTypeFilter = '지상' | '반지하' | '옥탑'; +export type RoomCountTypeFilter = '1개' | '2개' | '3개 이상'; +export type AdditionalInfoTypeFilter = + | '주차가능' | '에어컨' | '냉장고' | '세탁기' - | '엘리베이터' - | '베란다/테라스' - | '복층형'; + | '베란다/테라스'; export interface SharedPostsFilter { - cardType?: CardType; + cardType?: CardTypeFilter; roomInfo: { - roomType?: RoomType; + roomType?: RoomTypeFilter; hasLivingRoom: boolean; - roomCount?: RoomCountType; - restRoomCount?: RoomCountType; + roomCount?: RoomCountTypeFilter; + restRoomCount?: RoomCountTypeFilter; size?: { low: number; high: number }; - floor?: FloorType; + floor?: FloorTypeFilter; }; - dealInfo?: { - dealType?: DealType; + dealInfo: { + dealType?: DealTypeFilter; expectedFee?: { low: number; high: number }; }; - extraInfo: Partial>; + extraInfo: Partial>; } export type SharedPostsFilterType = keyof SharedPostsFilter; diff --git a/src/features/recommendation/recommendation.api.ts b/src/features/recommendation/recommendation.api.ts index b5b6a9c7b8..f22085eb1f 100644 --- a/src/features/recommendation/recommendation.api.ts +++ b/src/features/recommendation/recommendation.api.ts @@ -2,11 +2,11 @@ import axios from 'axios'; import { type GetRecommendationMateDTO } from './recommendation.dto'; -import { type CardType } from '@/entities/shared-posts-filter'; +import { type CardTypeFilter } from '@/entities/shared-posts-filter'; export const getRecommendationMate = async ( memberId: string, - cardType: CardType, + cardType: CardTypeFilter, ) => await axios.get( `http://localhost:8000/recommendation/${memberId}/${cardType}`, diff --git a/src/features/recommendation/recommendation.dto.ts b/src/features/recommendation/recommendation.dto.ts index 076698d4a4..3adc96887c 100644 --- a/src/features/recommendation/recommendation.dto.ts +++ b/src/features/recommendation/recommendation.dto.ts @@ -1,4 +1,4 @@ -import { type CardType } from '@/entities/shared-posts-filter'; +import { type CardTypeFilter } from '@/entities/shared-posts-filter'; export interface GetRecommendationMateDTO { user: { userId: string; gender: string }; @@ -6,6 +6,6 @@ export interface GetRecommendationMateDTO { userId: string; name: string; similarity: number; - cardType: CardType; + cardType: CardTypeFilter; }>; } diff --git a/src/features/recommendation/recommendation.hook.ts b/src/features/recommendation/recommendation.hook.ts index a5a890ae4b..33feab75ee 100644 --- a/src/features/recommendation/recommendation.hook.ts +++ b/src/features/recommendation/recommendation.hook.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { getRecommendationMate } from './recommendation.api'; -import { type CardType } from '@/entities/shared-posts-filter'; +import { type CardTypeFilter } from '@/entities/shared-posts-filter'; export const useRecommendationMate = ({ memberId, @@ -10,7 +10,7 @@ export const useRecommendationMate = ({ enabled, }: { memberId: string; - cardType: CardType; + cardType: CardTypeFilter; enabled: boolean; }) => useQuery({ diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 36ffe31147..85c8204175 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -220,13 +220,13 @@ export const usePostMateCardInputSection = () => { const [features, setFeatures] = useState<{ smoking?: string; - room?: string; + roomSharingOption?: string; mateAge?: string; options: Set; }>({ options: new Set() }); const handleEssentialFeatureChange = useCallback( - (key: 'smoking' | 'room' | 'mateAge', value: string) => { + (key: 'smoking' | 'roomSharingOption' | 'mateAge', value: string) => { setFeatures(prev => { if (prev[key] === value) { const newFeatures = { ...prev }; @@ -270,7 +270,7 @@ export const usePostMateCardInputSection = () => { return { smoking: features?.smoking ?? '상관없어요', - roomSharingOption: features?.room ?? '상관없어요', + roomSharingOption: features?.roomSharingOption ?? '상관없어요', mateAge: mateAge ?? null, options: JSON.stringify(options), }; diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 5c603de87a..ba12dc3255 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -6,6 +6,7 @@ export interface GetSharedPostsFilter { expectedPaymentRange?: { start: number; end: number }; hasLivingRoom?: boolean; numberOfRoom?: number; + numberOfRestRoom?: number; roomSizeRange?: { start: number; end: number }; floorTypes?: FloorType[]; canPark?: boolean; From f018d77c7c0102f49725857de9cfde877fcda233 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 6 May 2024 02:04:38 +0900 Subject: [PATCH 061/130] fix: fix the issue with the toggle not working properly (#73) --- src/components/shared-posts/filter/ExtraInfoFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shared-posts/filter/ExtraInfoFilter.tsx b/src/components/shared-posts/filter/ExtraInfoFilter.tsx index 0b1bbe0265..462e5a769e 100644 --- a/src/components/shared-posts/filter/ExtraInfoFilter.tsx +++ b/src/components/shared-posts/filter/ExtraInfoFilter.tsx @@ -73,7 +73,7 @@ export function ExtraInfoFilter() { const handleOptionClick = (option: AdditionalInfoTypeFilter) => { setFilter(prev => { - const value = prev.extraInfo[option] ?? true; + const value = prev.extraInfo[option] ?? false; return { ...prev, extraInfo: { From 3beb649007079b31a326f298925664031b714718 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 6 May 2024 15:14:27 +0900 Subject: [PATCH 062/130] feat: modify some filtering options to allow multiple selections. (#73) --- src/app/pages/writing-post-page.tsx | 22 ++-- .../shared-posts/filter/DealTypeFilter.tsx | 47 ++++++-- .../shared-posts/filter/ExtraInfoFilter.tsx | 6 +- .../shared-posts/filter/RoomTypeFilter.tsx | 113 +++++++++++------- .../shared-posts-filter.type.ts | 30 +++-- .../recommendation/recommendation.api.ts | 4 +- .../recommendation/recommendation.dto.ts | 4 +- .../recommendation/recommendation.hook.ts | 4 +- 8 files changed, 146 insertions(+), 84 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index ace15f22cf..5e0cbe7aba 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -10,11 +10,11 @@ import { MateSearchBox, } from '@/components/writing-post-page'; import { - type RoomTypeFilter, - type DealTypeFilter, - type RoomCountTypeFilter, - type FloorTypeFilter, - type AdditionalInfoTypeFilter, + type RoomTypeFilterOptions, + type DealTypeFilterOptions, + type RoomCountTypeFilterOptions, + type FloorTypeFilterOptions, + type AdditionalInfoTypeFilterOptions, } from '@/entities/shared-posts-filter'; import { useAuthValue } from '@/features/auth'; import { getImageURL, putImage } from '@/features/image'; @@ -407,33 +407,33 @@ const styles = { `, }; -const DealOptions: Record = { +const DealOptions: Record = { 월세: 'MONTHLY', 전세: 'JEONSE', }; -const RoomOptions: Record = { +const RoomOptions: Record = { 원룸: 'ONE_ROOM', '빌라/투룸이상': 'TWO_ROOM_VILLA', 아파트: 'APT', 오피스텔: 'OFFICE_TEL', }; const LivingRoomOptions = ['유', '무']; -const RoomCountOptions: Record = { +const RoomCountOptions: Record = { '1개': 1, '2개': 2, '3개 이상': 3, }; -const RestRoomCountOptions: Record = { +const RestRoomCountOptions: Record = { '1개': 1, '2개': 2, '3개 이상': 3, }; -const FloorOptions: Record = { +const FloorOptions: Record = { 지상: 'GROUND', 반지하: 'SEMI_BASEMENT', 옥탑: 'PENTHOUSE', }; -const AdditionalOptions: Record = { +const AdditionalOptions: Record = { 주차가능: 'canPark', 에어컨: 'hasAirConditioner', 냉장고: 'hasRefrigerator', diff --git a/src/components/shared-posts/filter/DealTypeFilter.tsx b/src/components/shared-posts/filter/DealTypeFilter.tsx index 0de779d7f6..274c12971c 100644 --- a/src/components/shared-posts/filter/DealTypeFilter.tsx +++ b/src/components/shared-posts/filter/DealTypeFilter.tsx @@ -1,9 +1,13 @@ 'use client'; +import { useCallback } from 'react'; import styled from 'styled-components'; import { RangeSlider } from '@/components'; -import { useSharedPostsFilter } from '@/entities/shared-posts-filter'; +import { + type DealTypeFilterOptions, + useSharedPostsFilter, +} from '@/entities/shared-posts-filter'; const styles = { container: styled.div` @@ -77,6 +81,33 @@ const styles = { export function DealTypeFilter() { const { filter, setFilter } = useSharedPostsFilter(); + const isDealTypeSelected = useCallback( + (dealTypeOption: DealTypeFilterOptions) => { + if (filter.dealInfo?.dealType?.[dealTypeOption] === true) return true; + return false; + }, + [filter.dealInfo.dealType], + ); + + const handleDealTypeClick = useCallback( + (dealTypeOption: DealTypeFilterOptions) => { + setFilter(prev => { + const value = prev.dealInfo.dealType?.[dealTypeOption] ?? false; + return { + ...prev, + dealInfo: { + ...prev.dealInfo, + dealType: { + ...prev.dealInfo.dealType, + [dealTypeOption]: !value, + }, + }, + }; + }); + }, + [setFilter], + ); + return ( @@ -84,24 +115,18 @@ export function DealTypeFilter() {
*/} - - {chatRooms.map((room, index) => ( (); - const chattingRoom = useEnterChatRoom(roomId, 0, 2); + const chattingRoom = useEnterChatRoom(roomId, 0, 10); useEffect(() => { if (chattingRoom != null) { setRoomData(chattingRoom.data?.data); } }, [chattingRoom]); + const [time, setTime] = useState(lastTime); + const [type, setType] = useState('server'); + useEffect(() => { const initializeChat = async () => { try { @@ -212,6 +215,8 @@ export function ChattingRoom({ console.log('WebSocket 연결이 열렸습니다.'); stomp.subscribe(`/room/${roomId}`, frame => { try { + setTime(new Date().toISOString()); + setType('client'); const parsedMessage = JSON.parse(frame.body); setMessages(prevMessages => [...prevMessages, parsedMessage]); } catch (error) { @@ -296,7 +301,7 @@ export function ChattingRoom({ /> {roomName} - {calTimeDiff(lastTime)} + {calTimeDiff(time, type)} {isMenuClick && ( diff --git a/src/features/chat/index.ts b/src/features/chat/index.ts index 721c4e668a..be0e3149d5 100644 --- a/src/features/chat/index.ts +++ b/src/features/chat/index.ts @@ -1 +1,2 @@ export * from './chat.hook'; +export * from './chat.dto'; From 2a5d97d456b88be19c1859116c720513388f7fc4 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 10 May 2024 15:10:54 +0900 Subject: [PATCH 086/130] feat: dummy data --- src/app/pages/main-page.tsx | 30 +++++++++++---- src/app/pages/shared-post-page.tsx | 16 ++++---- src/app/pages/shared-posts-page.tsx | 33 ++++++++++++----- src/features/profile/profile.api.ts | 8 ++-- src/features/shared/shared.hook.ts | 57 ++++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index 6fa40ae057..c9df3f3a7b 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -9,7 +9,7 @@ import { CircularButton } from '@/components'; import { UserCard } from '@/components/main-page'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { getGeolocation } from '@/features/geocoding'; -import { useRecommendationMate } from '@/features/recommendation'; +import { useDummyUsers } from '@/features/shared'; const styles = { container: styled.div` @@ -94,11 +94,11 @@ export function MainPage() { const { data: userData } = useUserData(auth?.accessToken !== undefined); - const { data: recommendationMates } = useRecommendationMate({ - memberId: auth?.user?.memberId ?? 'undefined', - cardType: 'mate', - enabled: auth?.accessToken != null, - }); + // const { data: recommendationMates } = useRecommendationMate({ + // memberId: auth?.user?.memberId ?? 'undefined', + // cardType: 'mate', + // enabled: auth?.accessToken != null, + // }); const [map, setMap] = useState(null); @@ -147,6 +147,8 @@ export function MainPage() { } }, [userData, router, setAuthUserData]); + const users = useDummyUsers(); + return ( @@ -168,14 +170,26 @@ export function MainPage() { onClick={handleScrollLeft} /> - {recommendationMates?.map(({ name, similarity, userId }) => ( + {/* {recommendationMates?.map(({ name, similarity, userId }) => ( - ))} + ))} */} + {users?.map( + ({ + userId, + data: { + authResponse: { name }, + }, + }) => ( + + + + ), + )} (null); + const [selected, setSelected] = useState< + | { + memberId: string; + profileImage: string; + } + | undefined + >(undefined); + const { isLoading, data: sharedPost } = useSharedPost({ postId, enabled: auth?.accessToken !== undefined, @@ -397,14 +405,6 @@ export function SharedPostPage({ postId }: { postId: number }) { sharedPost?.data.publisherAccount.memberId ?? '', ); - const [selected, setSelected] = useState< - | { - memberId: string; - profileImage: string; - } - | undefined - >(undefined); - useEffect(() => { const center = new naver.maps.LatLng(37.6090857, 126.9966865); setMap( diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index 2c9f3c285f..a314707b85 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -17,8 +17,7 @@ import { type SharedPostsType, } from '@/entities/shared-posts-filter'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; -import { useRecommendationMate } from '@/features/recommendation'; -import { usePaging, useSharedPosts } from '@/features/shared'; +import { useDummyUsers, usePaging, useSharedPosts } from '@/features/shared'; import { type GetSharedPostsDTO } from '@/features/shared/'; const styles = { @@ -120,7 +119,7 @@ export function SharedPostsPage() { useState(null); const { setAuthUserData } = useAuthActions(); - const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); + const { derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); const { @@ -143,11 +142,11 @@ export function SharedPostsPage() { page: page - 1, }); - const { data: recommendationMates } = useRecommendationMate({ - memberId: auth?.user?.memberId ?? 'undefined', - cardType: filter.cardType ?? 'mate', - enabled: auth?.accessToken != null && selected === 'homeless', - }); + // const { data: recommendationMates } = useRecommendationMate({ + // memberId: auth?.user?.memberId ?? 'undefined', + // cardType: filter.cardType ?? 'mate', + // enabled: auth?.accessToken != null && selected === 'homeless', + // }); useEffect(() => { resetFilter(); @@ -172,6 +171,8 @@ export function SharedPostsPage() { } }, [userData, router, setAuthUserData]); + const users = useDummyUsers(); + return ( @@ -254,11 +255,23 @@ export function SharedPostsPage() { ) : ( - {recommendationMates?.map(({ userId, name, similarity }) => ( + {/* {recommendationMates?.map(({ userId, name, similarity }) => ( - ))} + ))} */} + {users?.map( + ({ + userId, + data: { + authResponse: { name }, + }, + }) => ( + + + + ), + )} )} diff --git a/src/features/profile/profile.api.ts b/src/features/profile/profile.api.ts index 3c5a5232a3..404ea009b3 100644 --- a/src/features/profile/profile.api.ts +++ b/src/features/profile/profile.api.ts @@ -10,7 +10,7 @@ import { export const postUserProfile = async (memberId: string) => { const res = await axios.post(`/maru-api/profile`, { - memberId: memberId, + memberId, }); return res.data; @@ -49,7 +49,7 @@ export const getFollowingListData = async () => export const postFollowUser = async (memberId: string) => { await axios .post(`/maru-api/profile/follow`, { - memberId: memberId, + memberId, }) .then(res => res.data); }; @@ -57,14 +57,14 @@ export const postFollowUser = async (memberId: string) => { export const postUnfollowUser = async (memberId: string) => { await axios .post(`/maru-api/profile/unfollow`, { - memberId: memberId, + memberId, }) .then(res => res.data); }; export const postSearchUser = async (email: string) => { const res = await axios.post(`/maru-api/profile/search`, { - email: email, + email, }); return res.data; diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index c2382f5177..6a0621b3ab 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -14,12 +14,14 @@ import { scrapPost, } from './shared.api'; import { - type ImageFile, type CreateSharedPostProps, type GetSharedPostsProps, + type ImageFile, type SelectedExtraOptions, type SelectedOptions, } from './shared.type'; +import { postUserProfile } from '../profile/profile.api'; +import { type PostUserProfileDTO } from '../profile/profile.dto'; import { useAuthValue } from '@/features/auth'; import { type NaverAddress } from '@/features/geocoding'; @@ -435,3 +437,56 @@ export const useScrapDormitorySharedPost = () => useMutation, FailureDTO, number>({ mutationFn: scrapDormitoryPost, }); + +const userIds = [ + 'naver_0', + 'kakao_1', + 'kakao_2', + 'naver_3', + 'kakao_4', + 'naver_5', + 'kakao_6', + 'kakao_7', + 'kakao_8', + 'naver_9', + 'naver_10', + 'naver_11', + 'naver_12', + 'naver_13', + 'kakao_14', + 'naver_15', + 'kakao_16', + 'naver_17', + 'naver_18', + 'kakao_19', +]; + +export const useDummyUsers = () => { + const [users, setUsers] = + useState>(); + + useEffect(() => { + (async () => { + const userData = await Promise.allSettled( + userIds.map(async userId => { + const result = await postUserProfile(userId); + return { ...result, userId }; + }), + ); + + setUsers( + userData.reduce>( + (prev, curr) => { + if (curr.status === 'fulfilled') { + prev.push({ ...curr.value, userId: curr.value.userId }); + } + return prev; + }, + [], + ), + ); + })(); + }, []); + + return users; +}; From 01e8ac5ca34f602e15c022a8d4acff66d88cb294 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Fri, 10 May 2024 18:40:19 +0900 Subject: [PATCH 087/130] fix: Apply filter value changes (#75) --- .../shared-posts-filter/shared-posts-filter.hook.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts index 5dc5dad05a..0467587afa 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts @@ -81,8 +81,8 @@ export const useSharedPostsFilter = () => { : []; return { - roomTypes, - rentalTypes, + roomTypes: roomTypes.length === 0 ? undefined : roomTypes, + rentalTypes: rentalTypes.length === 0 ? undefined : rentalTypes, expectedPaymentRange: filter.dealInfo?.expectedFee != null ? { @@ -100,7 +100,7 @@ export const useSharedPostsFilter = () => { end: filter.roomInfo.size.high, } : undefined, - floorTypes, + floorTypes: floorTypes.length === 0 ? undefined : rentalTypes, canPark: filter.extraInfo.주차가능, hasAirConditioner: filter.extraInfo.에어컨, hasRefrigerator: filter.extraInfo.냉장고, From cea6faedd5f0acf093b677170d6e02a1e265063d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 11 May 2024 01:59:16 +0900 Subject: [PATCH 088/130] fix: remove useDummyUsers hook --- src/app/pages/main-page.tsx | 30 +++++----------- src/app/pages/shared-posts-page.tsx | 33 ++++++----------- src/features/shared/shared.hook.ts | 55 ----------------------------- 3 files changed, 18 insertions(+), 100 deletions(-) diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index c9df3f3a7b..6fa40ae057 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -9,7 +9,7 @@ import { CircularButton } from '@/components'; import { UserCard } from '@/components/main-page'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { getGeolocation } from '@/features/geocoding'; -import { useDummyUsers } from '@/features/shared'; +import { useRecommendationMate } from '@/features/recommendation'; const styles = { container: styled.div` @@ -94,11 +94,11 @@ export function MainPage() { const { data: userData } = useUserData(auth?.accessToken !== undefined); - // const { data: recommendationMates } = useRecommendationMate({ - // memberId: auth?.user?.memberId ?? 'undefined', - // cardType: 'mate', - // enabled: auth?.accessToken != null, - // }); + const { data: recommendationMates } = useRecommendationMate({ + memberId: auth?.user?.memberId ?? 'undefined', + cardType: 'mate', + enabled: auth?.accessToken != null, + }); const [map, setMap] = useState(null); @@ -147,8 +147,6 @@ export function MainPage() { } }, [userData, router, setAuthUserData]); - const users = useDummyUsers(); - return ( @@ -170,26 +168,14 @@ export function MainPage() { onClick={handleScrollLeft} /> - {/* {recommendationMates?.map(({ name, similarity, userId }) => ( + {recommendationMates?.map(({ name, similarity, userId }) => ( - ))} */} - {users?.map( - ({ - userId, - data: { - authResponse: { name }, - }, - }) => ( - - - - ), - )} + ))} (null); const { setAuthUserData } = useAuthActions(); - const { derivedFilter, reset: resetFilter } = useSharedPostsFilter(); + const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); const { @@ -142,11 +143,11 @@ export function SharedPostsPage() { page: page - 1, }); - // const { data: recommendationMates } = useRecommendationMate({ - // memberId: auth?.user?.memberId ?? 'undefined', - // cardType: filter.cardType ?? 'mate', - // enabled: auth?.accessToken != null && selected === 'homeless', - // }); + const { data: recommendationMates } = useRecommendationMate({ + memberId: auth?.user?.memberId ?? 'undefined', + cardType: filter.cardType ?? 'mate', + enabled: auth?.accessToken != null && selected === 'homeless', + }); useEffect(() => { resetFilter(); @@ -171,8 +172,6 @@ export function SharedPostsPage() { } }, [userData, router, setAuthUserData]); - const users = useDummyUsers(); - return ( @@ -255,23 +254,11 @@ export function SharedPostsPage() { ) : ( - {/* {recommendationMates?.map(({ userId, name, similarity }) => ( + {recommendationMates?.map(({ userId, name, similarity }) => ( - ))} */} - {users?.map( - ({ - userId, - data: { - authResponse: { name }, - }, - }) => ( - - - - ), - )} + ))} )} diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 6a0621b3ab..9b8bd87fc4 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -20,8 +20,6 @@ import { type SelectedExtraOptions, type SelectedOptions, } from './shared.type'; -import { postUserProfile } from '../profile/profile.api'; -import { type PostUserProfileDTO } from '../profile/profile.dto'; import { useAuthValue } from '@/features/auth'; import { type NaverAddress } from '@/features/geocoding'; @@ -437,56 +435,3 @@ export const useScrapDormitorySharedPost = () => useMutation, FailureDTO, number>({ mutationFn: scrapDormitoryPost, }); - -const userIds = [ - 'naver_0', - 'kakao_1', - 'kakao_2', - 'naver_3', - 'kakao_4', - 'naver_5', - 'kakao_6', - 'kakao_7', - 'kakao_8', - 'naver_9', - 'naver_10', - 'naver_11', - 'naver_12', - 'naver_13', - 'kakao_14', - 'naver_15', - 'kakao_16', - 'naver_17', - 'naver_18', - 'kakao_19', -]; - -export const useDummyUsers = () => { - const [users, setUsers] = - useState>(); - - useEffect(() => { - (async () => { - const userData = await Promise.allSettled( - userIds.map(async userId => { - const result = await postUserProfile(userId); - return { ...result, userId }; - }), - ); - - setUsers( - userData.reduce>( - (prev, curr) => { - if (curr.status === 'fulfilled') { - prev.push({ ...curr.value, userId: curr.value.userId }); - } - return prev; - }, - [], - ), - ); - })(); - }, []); - - return users; -}; From ad0a0896b571691bf5ea6ead0b0bee5f98916688 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sat, 11 May 2024 20:54:21 +0900 Subject: [PATCH 089/130] fix: modify dormitory roommate UI --- src/app/pages/shared-posts-page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index 2c9f3c285f..73bdc2a191 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -177,13 +177,15 @@ export function SharedPostsPage() { - {selected === 'hasRoom' && ( + {selected === 'hasRoom' || selected === 'dormitory' ? ( 작성하기 + ) : ( + <> )} - {selected === 'hasRoom' ? ( + {selected === 'hasRoom' || selected === 'dormitory' ? ( <> {prevSharedPosts != null From 8a61adc65783567bffff3d522bb4f4d37c326abc Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 12 May 2024 01:21:39 +0900 Subject: [PATCH 090/130] refactor: remove fragment tag --- src/app/pages/shared-posts-page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index 73bdc2a191..bab5695161 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -177,12 +177,10 @@ export function SharedPostsPage() { - {selected === 'hasRoom' || selected === 'dormitory' ? ( + {(selected === 'hasRoom' || selected === 'dormitory') && ( 작성하기 - ) : ( - <> )} {selected === 'hasRoom' || selected === 'dormitory' ? ( From c7e904c1d9f8b8af91a94bbd76d97cee2488da4b Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 12 May 2024 01:29:17 +0900 Subject: [PATCH 091/130] feat: add post modify button (#75) --- src/app/pages/shared-post-page.tsx | 50 +++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index 16d396c9db..c63384032f 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { Bookmark, CircularProfileImage } from '@/components'; @@ -371,11 +371,36 @@ const styles = { font-weight: 600; line-height: 1.5rem; `, + modifyPostButton: styled.button` + all: unset; + cursor: pointer; + + display: flex; + width: fit-content; + height: fit-content; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + + border-radius: 8px; + border: 1px solid var(--Gray-3, #888); + background: var(--White, #fff); + + color: var(--Gray-3, #888); + font-family: Pretendard; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + + align-self: end; + `, }; export function SharedPostPage({ postId }: { postId: number }) { const auth = useAuthValue(); const [, setMap] = useState(null); + const mapRef = useRef(null); const [selected, setSelected] = useState< | { @@ -416,15 +441,17 @@ export function SharedPostPage({ postId }: { postId: number }) { ); useEffect(() => { - const center = new naver.maps.LatLng(37.6090857, 126.9966865); - setMap( - new naver.maps.Map('map', { - center, - disableKineticPan: false, - scrollWheel: false, - }), - ); - }, []); + if (mapRef.current != null) { + const center = new naver.maps.LatLng(37.6090857, 126.9966865); + setMap( + new naver.maps.Map(mapRef.current, { + center, + disableKineticPan: false, + scrollWheel: false, + }), + ); + } + }, [mapRef]); const [roomName, setRoomName] = useState(''); @@ -520,8 +547,9 @@ export function SharedPostPage({ postId }: { postId: number }) {

위치 정보

{sharedPost.data.address.roadAddress}

-
+
+ 수정하기 From 026f4b061894d08b570af9d3748325669b5bf2bc Mon Sep 17 00:00:00 2001 From: he2e2 Date: Sun, 12 May 2024 15:50:00 +0900 Subject: [PATCH 092/130] feat: NavigationBar, landing-page, main-page (#79) --- package.json | 1 + src/app/globals.scss | 4 + src/app/page.tsx | 13 +- src/app/pages/mobile/index.ts | 2 + src/app/pages/mobile/mobile-landing-page.tsx | 110 ++++++++++ src/app/pages/mobile/mobile-main-page.tsx | 213 +++++++++++++++++++ src/components/CircularButton.tsx | 6 + src/components/CircularProfileImage.tsx | 7 + src/components/NavigationBar.tsx | 188 +++++++++++----- src/components/main-page/UserCard.tsx | 38 +++- src/shared/mobile/index.ts | 1 + src/shared/mobile/mobile.hook.ts | 11 + yarn.lock | 46 +++- 13 files changed, 573 insertions(+), 67 deletions(-) create mode 100644 src/app/pages/mobile/index.ts create mode 100644 src/app/pages/mobile/mobile-landing-page.tsx create mode 100644 src/app/pages/mobile/mobile-main-page.tsx create mode 100644 src/shared/mobile/index.ts create mode 100644 src/shared/mobile/mobile.hook.ts diff --git a/package.json b/package.json index 3e3b253956..7f8e17b2d0 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@react-hook/media-query": "^1.1.1", "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.25.0", "@tanstack/react-query-devtools": "^5.25.0", diff --git a/src/app/globals.scss b/src/app/globals.scss index 2aee40f0b7..b7fff38263 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -29,6 +29,10 @@ main { margin-top: 4.5rem; display: flex; justify-content: center; + + @media (max-width: 768px) { + justify-content: flex-start; + } } a { diff --git a/src/app/page.tsx b/src/app/page.tsx index 3238ba4646..edaece2d8f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,21 @@ 'use client'; import { LandingPage, MainPage } from './pages'; +import { MobileLandingPage, MobileMainPage } from './pages/mobile'; import { useAuthIsLogin } from '@/features/auth'; +import { useIsMobile } from '@/shared/mobile'; export default function Home() { const isLogin = useAuthIsLogin(); - return <>{isLogin ? : }; + const isMobile = useIsMobile(); + return ( + <> + {isLogin ? ( + <>{isMobile ? : } + ) : ( + <>{isMobile ? : } + )} + + ); } diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts new file mode 100644 index 0000000000..f2eace5ac0 --- /dev/null +++ b/src/app/pages/mobile/index.ts @@ -0,0 +1,2 @@ +export * from './mobile-landing-page'; +export * from './mobile-main-page'; diff --git a/src/app/pages/mobile/mobile-landing-page.tsx b/src/app/pages/mobile/mobile-landing-page.tsx new file mode 100644 index 0000000000..527b563f57 --- /dev/null +++ b/src/app/pages/mobile/mobile-landing-page.tsx @@ -0,0 +1,110 @@ +'use client'; + +import styled from 'styled-components'; + +const styles = { + container: styled.div` + display: flex; + width: 100vw; + height: 47.8125rem; + padding-bottom: 2rem; + flex-direction: column; + justify-content: space-between; + align-items: center; + `, + img: styled.img` + width: 24.375rem; + height: 27.125rem; + flex-shrink: 0; + object-fit: contain; + `, + description: styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1.75rem; + width: 27.8125rem; + flex-shrink: 0; + + h1 { + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 2rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + p { + color: #000; + + text-align: center; + font-family: 'Noto Sans KR'; + font-size: 1.5rem; + font-style: normal; + font-weight: 500; + line-height: normal; + } + `, + loginButtons: styled.div` + display: flex; + flex-direction: column; + gap: 0.6875rem; + + button { + all: unset; + cursor: pointer; + display: flex; + justify-content: center; + } + + img { + width: 16.5625rem; + height: 2.6875rem; + flex-shrink: 0; + border-radius: 8px; + object-fit: cover; + } + `, + section2: styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + align-self: stretch; + `, + section3: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1rem; + `, +}; + +export function MobileLandingPage() { + return ( + + + +

공동주거생활의 A to Z

+

+ 마루에서 여러분만을 위한 +
메이트를 만나보세요 +

+
+ + + kakao + + + naver + + +
+ ); +} diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx new file mode 100644 index 0000000000..65ae8c85bc --- /dev/null +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -0,0 +1,213 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { CircularButton } from '@/components'; +import { UserCard } from '@/components/main-page'; +import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; +import { getGeolocation } from '@/features/geocoding'; +import { useDummyUsers } from '@/features/shared'; + +const styles = { + container: styled.div` + display: flex; + width: 100vw; + height: 47.8125rem; + padding-bottom: 2rem; + flex-direction: column; + align-items: center; + `, + map: styled.div` + width: 100%; + height: 50dvh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + p { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + + p[class~='caption'] { + color: #19191980; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + mateRecommendationContainer: styled.div` + width: 100vw; + display: flex; + flex-direction: column; + gap: 0.5rem; + + margin-bottom: 2.5rem; + `, + mateRecommendationTitle: styled.div` + display: flex; + padding: 2rem 1.5rem; + justify-content: flex-start; + align-items: center; + align-self: stretch; + + h1 { + color: #000; + + font-family: 'Noto Sans KR'; + font-size: 1.1875rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + `, + mateRecommendationRow: styled.div` + display: flex; + width: 100%; + padding: 0rem 1.5rem; + justify-content: space-between; + align-items: center; + gap: 1rem; + `, + mateRecommendation: styled.div` + display: flex; + width: 75%; + padding: 0.5rem 0rem; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; + overflow-x: auto; + + -ms-overflow-style: none; + scrollbar-width: none; + scrollbar ::-webkit-scrollbar { + display: none; + } + `, +}; + +export function MobileMainPage() { + const router = useRouter(); + + const auth = useAuthValue(); + const { setAuthUserData } = useAuthActions(); + + const { data: userData } = useUserData(auth?.accessToken !== undefined); + + // const { data: recommendationMates } = useRecommendationMate({ + // memberId: auth?.user?.memberId ?? 'undefined', + // cardType: 'mate', + // enabled: auth?.accessToken != null, + // }); + + const [map, setMap] = useState(null); + + const scrollRef = useRef(null); + + const handleScrollRight = () => { + if (scrollRef.current !== null) { + scrollRef.current.scrollBy({ left: 300, behavior: 'smooth' }); + } + }; + + const handleScrollLeft = () => { + if (scrollRef.current !== null) { + scrollRef.current.scrollBy({ left: -300, behavior: 'smooth' }); + } + }; + + useEffect(() => { + getGeolocation({ + onSuccess: position => { + const center = new naver.maps.LatLng( + position.coords.latitude, + position.coords.longitude, + ); + + setMap( + new naver.maps.Map('map', { + center, + disableKineticPan: false, + scrollWheel: false, + }), + ); + }, + onError: error => { + console.error(error); + }, + }); + }, []); + + useEffect(() => { + if (userData !== undefined) { + setAuthUserData(userData); + if (userData.initialized) { + // router.replace('/profile'); + } + } + }, [userData, router, setAuthUserData]); + + const users = useDummyUsers(); + return ( + + + {map == null && ( + <> +

지도를 불러오는 중입니다.

+

(위치 권한이 필요합니다)

+ + )} +
+ + +

{auth?.user?.name}님의 추천 메이트

+
+ + + + {/* {recommendationMates?.map(({ name, similarity, userId }) => ( + + + + ))} */} + {users?.map( + ({ + userId, + data: { + authResponse: { name }, + }, + }) => ( + + + + ), + )} + + + +
+
+ ); +} diff --git a/src/components/CircularButton.tsx b/src/components/CircularButton.tsx index fecabf93a8..5971b53983 100644 --- a/src/components/CircularButton.tsx +++ b/src/components/CircularButton.tsx @@ -21,6 +21,12 @@ const styles = { position: absolute; margin: auto; } + + @media (max-width: 768px) { + width: 2.25rem; + height: 2.25rem; + flex-shrink: 0; + } `, }; diff --git a/src/components/CircularProfileImage.tsx b/src/components/CircularProfileImage.tsx index 8d5f687201..ae137cc1c2 100644 --- a/src/components/CircularProfileImage.tsx +++ b/src/components/CircularProfileImage.tsx @@ -59,6 +59,13 @@ const styles = { left: 65%; z-index: 2; + + @media (max-width: 768px) { + width: 1.6875rem; + height: 1.75rem; + padding: 0.75rem 0.375rem; + font-size: 0.75rem; + } `, }; diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 0c1d9ca9cc..cb9d05e5e8 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import styled from 'styled-components'; import { UserSearchBox } from './UserSearchBox'; @@ -14,6 +15,7 @@ import { useUserData, } from '@/features/auth'; import { useToast } from '@/features/toast'; +import { useIsMobile } from '@/shared/mobile'; import { load } from '@/shared/storage'; const styles = { @@ -31,11 +33,64 @@ const styles = { background: #fff; box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.05); z-index: 2147483647; + + @media (max-width: 768px) { + min-width: 390px; + width: 100vw; + padding: 1rem 2rem; + } + `, + mobileMenuIcon: styled.div` + display: none; + + @media (max-width: 768px) { + display: block; + width: 1rem; + height: 1rem; + flex-shrink: 0; + background: url('/kebab-horizontal.svg') no-repeat; + cursor: pointer; + } + `, + menuContainer: styled.ul` + display: none; + + @media (max-width: 768px) { + display: block; + width: 40vw; + height: 100%; + background: #fff; + z-index: 20000; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + position: fixed; + right: 0; + } + `, + menuItem: styled.div` + display: flex; + padding: 0.8rem; + align-items: flex-start; + gap: 0.8rem; + align-self: stretch; + border-bottom: 1px solid #e5e5ea; + cursor: pointer; + + p { + color: #2c2c2e; + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } `, utils: styled.div` display: flex; align-items: center; gap: 3.63rem; + @media (max-width: 768px) { + display: none; + } `, title: styled.h1` color: var(--Main-1, #e15637); @@ -50,6 +105,9 @@ const styles = { justify-content: center; align-items: center; gap: 1.5rem; + @media (max-width: 768px) { + display: none; + } `, logout: styled.button` all: unset; @@ -68,35 +126,9 @@ const styles = { line-height: normal; cursor: pointer; - `, - searchUserBox: styled.ul` - display: flex; - flex-direction: column; - position: fixed; - top: 6rem; - left: 19rem; - background-color: #fff; - min-width: 20rem; - min-height: 10rem; - border-radius: 1rem; - box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.05); - z-index: 20000; - `, - userContainer: styled.li` - display: flex; - gap: 1.5rem; - align-items: center; - - color: var(--Text-grayDark, #2c2c2e); - font-family: 'Noto Sans KR'; - font-size: 0.875rem; - font-style: normal; - font-weight: 400; - line-height: normal; - `, - userImg: styled.img` - width: 1rem; - height: 1rem; + @media (max-width: 768px) { + display: none; + } `, }; @@ -104,6 +136,8 @@ export function NavigationBar() { const isLogin = useAuthIsLogin(); const router = useRouter(); + const isMobile = useIsMobile(); + const auth = useAuthValue(); const { logout } = useAuthActions(); const { createToast } = useToast(); @@ -135,28 +169,80 @@ export function NavigationBar() { } }; + const [isMenuClick, setIsMenuClick] = useState(false); + return ( - - - - maru - - - - - 메이트찾기 - 커뮤니티 - 마이페이지 - {isLogin && ( - { - handleLogout(); - }} - > - 로그아웃 - - )} - - + <> + + {isMobile ? ( + <> + { + setIsMenuClick(prev => !prev); + }} + > + maru + + { + setIsMenuClick(prev => !prev); + }} + /> + + ) : null} + + + maru + + + + + 메이트찾기 + 커뮤니티 + 마이페이지 + {isLogin && ( + { + handleLogout(); + }} + > + 로그아웃 + + )} + + + {isMenuClick && ( + + + { + setIsMenuClick(prev => !prev); + }} + > +

메이트 찾기

+
+ + + { + setIsMenuClick(prev => !prev); + }} + > +

마이페이지

+
+ + {isLogin && ( + { + setIsMenuClick(prev => !prev); + handleLogout(); + }} + > +

로그아웃

+
+ )} +
+ )} + ); } diff --git a/src/components/main-page/UserCard.tsx b/src/components/main-page/UserCard.tsx index 14b312dc27..a879d19f67 100644 --- a/src/components/main-page/UserCard.tsx +++ b/src/components/main-page/UserCard.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { CircularProfileImage } from '@/components'; +import { useIsMobile } from '@/shared/mobile'; const styles = { container: styled.div` @@ -13,12 +14,22 @@ const styles = { background: var(--Gray-1, #f7f6f9); padding: 1.88rem 1.25rem; + + @media (max-width: 768px) { + width: 11.125rem; + height: 12.5625rem; + padding: 1.5rem 0.75rem 1.0625rem 1rem; + } `, profileInfo: styled.div` display: flex; align-items: center; gap: 1.44rem; + @media (max-width: 768px) { + gap: 0.8rem; + } + margin-bottom: 1.25rem; div { @@ -28,7 +39,7 @@ const styles = { h1 { color: #000; font-family: 'Noto Sans KR'; - font-size: 1.5rem; + font-size: 1.125rem; font-style: normal; font-weight: 500; line-height: normal; @@ -38,7 +49,7 @@ const styles = { p { color: #000; font-family: 'Noto Sans KR'; - font-size: 1rem; + font-size: 0.75rem; font-style: normal; font-weight: 500; line-height: normal; @@ -49,13 +60,13 @@ const styles = { data: styled.div` display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.25rem; padding: 0 1.06rem; color: #000; font-family: 'Noto Sans KR'; - font-size: 1rem; + font-size: 0.75rem; font-style: normal; font-weight: 500; line-height: normal; @@ -73,14 +84,23 @@ export function UserCard({ name: string; percentage: number; }) { + const isMobile = useIsMobile(); return ( - + {isMobile ? ( + + ) : ( + + )}

{name}

24세

diff --git a/src/shared/mobile/index.ts b/src/shared/mobile/index.ts new file mode 100644 index 0000000000..64b43aad19 --- /dev/null +++ b/src/shared/mobile/index.ts @@ -0,0 +1 @@ +export * from './mobile.hook'; diff --git a/src/shared/mobile/mobile.hook.ts b/src/shared/mobile/mobile.hook.ts new file mode 100644 index 0000000000..9f02e864ce --- /dev/null +++ b/src/shared/mobile/mobile.hook.ts @@ -0,0 +1,11 @@ +import { useMediaQuery } from '@react-hook/media-query'; +import { useEffect, useState } from 'react'; + +export function useIsMobile() { + const [isMobile, setIsMobile] = useState(false); + const mobile = useMediaQuery('(max-width: 768px)'); + useEffect(() => { + setIsMobile(mobile); + }, [mobile]); + return isMobile; +} diff --git a/yarn.lock b/yarn.lock index 57a6c2430f..10d5eb174e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1245,11 +1245,21 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@react-hook/media-query@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@react-hook/media-query/-/media-query-1.1.1.tgz#7fc4e52591784a39be924b62b4270ae3e18ec578" + integrity sha512-VM14wDOX5CW5Dn6b2lTiMd79BFMTut9AZj2+vIRT3LCKgMCYmdqruTtzDPSnIVDQdtxdPgtOzvU9oK20LopuOw== + "@rushstack/eslint-patch@^1.3.3": version "1.7.2" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== +"@stomp/stompjs@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.0.0.tgz#46b5c454a9dc8262e0b20f3b3dbacaa113993077" + integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== + "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" @@ -1356,11 +1366,6 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@stomp/stompjs@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@stomp/stompjs/-/stompjs-7.0.0.tgz#46b5c454a9dc8262e0b20f3b3dbacaa113993077" - integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== - "@swc/helpers@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" @@ -1937,6 +1942,20 @@ caniuse-lite@^1.0.30001579: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz#fc34fad75c0c6d6d6303bdbceec2da8f203dabd6" integrity sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA== +caniuse-lite@^1.0.30001587: + version "1.0.30001617" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb" + integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1965,6 +1984,13 @@ client-only@0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -3974,12 +4000,20 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + "source-map-js@>=0.6.2 <2.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map-js@^1.0.2: +source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== From 62a6036042d8433182bc5d152c98a3214afd0b52 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 12 May 2024 23:03:22 +0900 Subject: [PATCH 093/130] feat: add modify post flow (#75) --- src/app/lib/providers/AuthProvider.tsx | 20 ++- src/app/pages/shared-post-page.tsx | 49 ++++-- src/app/pages/shared-posts-page.tsx | 19 ++- src/app/pages/writing-post-page.tsx | 103 ++++++++---- src/features/shared/shared.atom.ts | 36 +++++ src/features/shared/shared.hook.ts | 215 +++++++++++-------------- src/features/shared/shared.type.ts | 5 +- 7 files changed, 273 insertions(+), 174 deletions(-) create mode 100644 src/features/shared/shared.atom.ts diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index 8d61cc3d59..f7cb64a8bb 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -5,6 +5,7 @@ import { usePathname, useRouter } from 'next/navigation'; import { useLayoutEffect, useState, useCallback } from 'react'; import { + getUserData, postTokenRefresh, useAuthActions, useAuthValue, @@ -13,7 +14,7 @@ import { load, remove } from '@/shared/storage'; export function AuthProvider({ children }: { children: React.ReactNode }) { const auth = useAuthValue(); - const { login } = useAuthActions(); + const { setAuthUserData, login } = useAuthActions(); const router = useRouter(); const pathName = usePathname(); @@ -57,12 +58,27 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { postTokenRefresh(refreshToken) .then(({ data }) => { handleLoginSuccess(data); + getUserData() + .then(res => { + setAuthUserData(res.data); + }) + .catch(err => { + console.error(err); + }); }) .catch(handleLoginError) .finally(() => { setIsLoading(false); }); - }, [pathName, auth, isLoading, handleLoginSuccess, handleLoginError, router]); + }, [ + pathName, + auth, + isLoading, + handleLoginError, + router, + handleLoginSuccess, + setAuthUserData, + ]); useLayoutEffect(() => { checkAndRefreshToken(); diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index c63384032f..d288033853 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -7,12 +8,17 @@ import { Bookmark, CircularProfileImage } from '@/components'; import { ImageGrid } from '@/components/shared-post-page'; import { useAuthValue, useUserData } from '@/features/auth'; import { useCreateChatRoom } from '@/features/chat'; +import { fromAddrToCoord } from '@/features/geocoding'; import { useFollowUser, useFollowingListData, useUnfollowUser, } from '@/features/profile'; -import { useScrapSharedPost, useSharedPost } from '@/features/shared'; +import { + useScrapSharedPost, + useSharedPost, + useSharedPostProps, +} from '@/features/shared'; import { getAge } from '@/shared'; const styles = { @@ -399,9 +405,13 @@ const styles = { export function SharedPostPage({ postId }: { postId: number }) { const auth = useAuthValue(); + const [, setMap] = useState(null); const mapRef = useRef(null); + const router = useRouter(); + const { setStateWithPost } = useSharedPostProps(); + const [selected, setSelected] = useState< | { memberId: string; @@ -441,17 +451,24 @@ export function SharedPostPage({ postId }: { postId: number }) { ); useEffect(() => { - if (mapRef.current != null) { - const center = new naver.maps.LatLng(37.6090857, 126.9966865); - setMap( - new naver.maps.Map(mapRef.current, { - center, - disableKineticPan: false, - scrollWheel: false, - }), + if (sharedPost?.data.address.roadAddress != null) { + fromAddrToCoord({ query: sharedPost?.data.address.roadAddress }).then( + res => { + const address = res.data.addresses.shift(); + if (address != null && mapRef.current != null) { + const center = new naver.maps.LatLng(+address.y, +address.x); + setMap( + new naver.maps.Map(mapRef.current, { + center, + disableKineticPan: false, + scrollWheel: false, + }), + ); + } + }, ); } - }, [mapRef]); + }, [sharedPost]); const [roomName, setRoomName] = useState(''); @@ -549,7 +566,17 @@ export function SharedPostPage({ postId }: { postId: number }) {

{sharedPost.data.address.roadAddress}

- 수정하기 + {sharedPost.data.publisherAccount.memberId === + auth?.user?.memberId && ( + { + setStateWithPost(sharedPost); + router.push('/shared/writing'); + }} + > + 수정하기 + + )} diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index bab5695161..c885fe0279 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -18,7 +18,11 @@ import { } from '@/entities/shared-posts-filter'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { useRecommendationMate } from '@/features/recommendation'; -import { usePaging, useSharedPosts } from '@/features/shared'; +import { + usePaging, + useSharedPostProps, + useSharedPosts, +} from '@/features/shared'; import { type GetSharedPostsDTO } from '@/features/shared/'; const styles = { @@ -120,6 +124,8 @@ export function SharedPostsPage() { useState(null); const { setAuthUserData } = useAuthActions(); + const { reset: resetSharedPostProps } = useSharedPostProps(); + const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); @@ -178,9 +184,14 @@ export function SharedPostsPage() { {(selected === 'hasRoom' || selected === 'dormitory') && ( - - 작성하기 - + { + resetSharedPostProps(); + router.push('/shared/writing'); + }} + > + 작성하기 + )} {selected === 'hasRoom' || selected === 'dormitory' ? ( diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index b6b34586fb..d5077aad7b 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -10,22 +10,22 @@ import { MateSearchBox, } from '@/components/writing-post-page'; import { + AdditionalInfoTypeValue, CountTypeValue, - type DealType, DealTypeValue, - RoomTypeValue, - type RoomType, FloorTypeValue, - type FloorType, - AdditionalInfoTypeValue, LivingRoomTypeValue, + RoomTypeValue, + type DealType, + type FloorType, + type RoomType, } from '@/entities/shared-posts-filter'; import { useAuthValue } from '@/features/auth'; import { getImageURL, putImage } from '@/features/image'; import { useCreateSharedPost, - useCreateSharedPostProps, usePostMateCardInputSection, + useSharedPostProps, type ImageFile, } from '@/features/shared'; import { useToast } from '@/features/toast'; @@ -68,9 +68,11 @@ const styles = { .column { display: flex; + width: 100%; flex-direction: column; gap: 1rem; - flex: 1 0 0; + + overflow-x: auto; } `, mateCardContainer: styled.div` @@ -284,19 +286,21 @@ const styles = { `, images: styled.div` display: flex; + width: fit-content; align-items: center; align-self: stretch; gap: 1rem; overflow-x: auto; `, - image: styled.img` + image: styled.div<{ $url: string }>` width: 14.4375rem; height: 9.875rem; background: #ededed; - object-fit: cover; - object-position: center; + background-image: ${({ $url }) => `url("${$url}")`}; + background-position: center; + background-size: cover; cursor: pointer; `, @@ -417,6 +421,7 @@ export function WritingPostPage() { useState(false); const { + mode, title, content, images, @@ -426,18 +431,12 @@ export function WritingPostPage() { selectedOptions, selectedExtraOptions, expectedMonthlyFee, - setTitle, - setContent, - setImages, - setMateLimit, - setHouseSize, - setAddress, - setExpectedMonthlyFee, + setSharedPostProps, handleOptionClick, handleExtraOptionClick, isOptionSelected, isExtraOptionSelected, - } = useCreateSharedPostProps(); + } = useSharedPostProps(); const { gender, @@ -462,13 +461,19 @@ export function WritingPostPage() { const handleTitleInputChanged = ( event: React.ChangeEvent, ) => { - setTitle(event.target.value); + setSharedPostProps(prev => ({ + ...prev, + title: event.target.value, + })); }; const handleContentInputChanged = ( event: React.ChangeEvent, ) => { - setContent(event.target.value); + setSharedPostProps(prev => ({ + ...prev, + content: event.target.value, + })); }; const handleImageInputClicked = () => { @@ -482,13 +487,21 @@ export function WritingPostPage() { file, url: URL.createObjectURL(file), extension: `.${file.type.split('/')[1]}`, + uploaded: false, + })); + + setSharedPostProps(prev => ({ + ...prev, + images: [...prev.images, ...imagesArray], })); - setImages(prevImages => [...prevImages, ...imagesArray]); } }; const handleRemoveImage = (removeImage: ImageFile) => { - setImages(prev => prev.filter(image => image.url !== removeImage.url)); + setSharedPostProps(prev => ({ + ...prev, + images: prev.images.filter(image => image.url !== removeImage.url), + })); }; const convertToNumber = (value: string) => { @@ -548,33 +561,43 @@ export function WritingPostPage() { (async () => { try { const getResults = await Promise.allSettled( - images.map(async ({ extension, file }) => { + images.map(async ({ url, extension, file, uploaded }) => { + if (uploaded || extension == null) return { url, uploaded }; const result = await getImageURL(extension); return { ...result.data.data, + uploaded, file, }; }), ); const urls = getResults.reduce< - Array<{ file: File; fileName: string; url: string }> + Array<{ + file?: File; + fileName?: string; + url: string; + uploaded: boolean; + }> >((prev, result) => { if (result.status === 'rejected') return prev; return prev.concat(result.value); }, []); const putResults = await Promise.allSettled( - urls.map(async url => { - await putImage(url.url, url.file); - return { fileName: url.fileName }; + urls.map(async ({ url, fileName, file, uploaded }) => { + if (uploaded) return { fileName }; + + if (file != null) await putImage(url, file); + return { fileName }; }), ); const uploadedImages = putResults.reduce< Array<{ fileName: string; isThumbNail: boolean; order: number }> >((prev, result) => { - if (result.status === 'rejected') return prev; + if (result.status === 'rejected' || result.value.fileName == null) + return prev; return prev.concat({ fileName: result.value.fileName, isThumbNail: prev.length === 0, @@ -665,7 +688,7 @@ export function WritingPostPage() { 기본 정보 - 작성하기 + {mode === 'create' ? '작성하기' : '수정하기'} 제목 @@ -694,7 +717,10 @@ export function WritingPostPage() { {showLocationSearchBox && ( { - setAddress(selectedAddress); + setSharedPostProps(prev => ({ + ...prev, + address: selectedAddress, + })); setShowLocationSearchBox(false); }} setHidden={() => { @@ -720,7 +746,7 @@ export function WritingPostPage() { {images.map(image => ( { handleRemoveImage(image); }} @@ -746,7 +772,10 @@ export function WritingPostPage() { value={mateLimit} onChange={event => { handleNumberInput(event.target.value, value => { - setMateLimit(value); + setSharedPostProps(prev => ({ + ...prev, + mateLimit: value, + })); }); }} $width={3} @@ -833,7 +862,10 @@ export function WritingPostPage() { value={expectedMonthlyFee} onChange={event => { handleNumberInput(event.target.value, value => { - setExpectedMonthlyFee(value); + setSharedPostProps(prev => ({ + ...prev, + expectedMonthlyFee: value, + })); }); }} $width={3} @@ -933,7 +965,10 @@ export function WritingPostPage() { value={houseSize} onChange={event => { handleNumberInput(event.target.value, value => { - setHouseSize(value); + setSharedPostProps(prev => ({ + ...prev, + houseSize: value, + })); }); }} $width={2} diff --git a/src/features/shared/shared.atom.ts b/src/features/shared/shared.atom.ts new file mode 100644 index 0000000000..0a47e5a2c8 --- /dev/null +++ b/src/features/shared/shared.atom.ts @@ -0,0 +1,36 @@ +import { atom } from 'recoil'; + +import { + type SelectedExtraOptions, + type ImageFile, + type SelectedOptions, +} from './shared.type'; + +import { type NaverAddress } from '@/features/geocoding'; + +export const sharedPostPropState = atom<{ + mode: 'create' | 'modify'; + title: string; + content: string; + images: ImageFile[]; + address?: NaverAddress; + mateLimit: number; + expectedMonthlyFee: number; + houseSize: number; + selectedExtraOptions: SelectedExtraOptions; + selectedOptions: SelectedOptions; +}>({ + key: 'sharedPostPropState', + default: { + mode: 'create', + title: '', + content: '', + images: [], + address: undefined, + mateLimit: 0, + expectedMonthlyFee: 0, + houseSize: 0, + selectedExtraOptions: {}, + selectedOptions: {}, + }, +}); diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 9b8bd87fc4..d5b1e26e42 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { type AxiosResponse } from 'axios'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRecoilState, useResetRecoilState } from 'recoil'; import { createSharedPost, @@ -13,16 +14,16 @@ import { scrapDormitoryPost, scrapPost, } from './shared.api'; +import { sharedPostPropState } from './shared.atom'; +import { type GetSharedPostDTO } from './shared.dto'; import { type CreateSharedPostProps, type GetSharedPostsProps, - type ImageFile, - type SelectedExtraOptions, type SelectedOptions, } from './shared.type'; +import { fromAddrToCoord } from '../geocoding'; import { useAuthValue } from '@/features/auth'; -import { type NaverAddress } from '@/features/geocoding'; import { useDebounce } from '@/shared/debounce'; import { type FailureDTO, type SuccessBaseDTO } from '@/shared/types'; @@ -93,126 +94,98 @@ export const usePaging = ({ ); }; -export const useCreateSharedPostProps = () => { - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); - const [images, setImages] = useState([]); - const [address, setAddress] = useState(null); - - const [mateLimit, setMateLimit] = useState(0); - const [expectedMonthlyFee, setExpectedMonthlyFee] = useState(0); - - const [houseSize, setHouseSize] = useState(0); - const [selectedExtraOptions, setSelectedExtraOptions] = - useState({}); - const [selectedOptions, setSelectedOptions] = useState({}); +export const useSharedPostProps = () => { + const [state, setState] = useRecoilState(sharedPostPropState); + const reset = useResetRecoilState(sharedPostPropState); + + const setStateWithPost = ({ data }: GetSharedPostDTO) => { + fromAddrToCoord({ query: data.address.roadAddress }) + .then(res => { + const address = res.data.addresses.shift(); + if (address != null) setState(prev => ({ ...prev, address })); + }) + .catch(err => { + console.error(err); + }); - const handleExtraOptionClick = useCallback((option: string) => { - setSelectedExtraOptions(prevSelectedOptions => ({ - ...prevSelectedOptions, - [option]: !prevSelectedOptions[option], + let roomCount = '1개'; + if (data.roomInfo.numberOfRoom === 2) roomCount = '2개'; + else if (data.roomInfo.numberOfRoom === 3) roomCount = '3개 이상'; + + let restRoomCount = '1개'; + if (data.roomInfo.numberOfBathRoom === 2) restRoomCount = '2개'; + else if (data.roomInfo.numberOfBathRoom === 3) restRoomCount = '3개'; + + setState({ + mode: 'modify', + title: data.title, + content: data.content, + images: data.roomImages.map(({ fileName }) => ({ + url: fileName, + uploaded: true, + })), + mateLimit: data.roomInfo.recruitmentCapacity, + expectedMonthlyFee: data.roomInfo.expectedPayment, + houseSize: data.roomInfo.size, + selectedOptions: { + roomType: data.roomInfo.roomType, + roomCount, + budget: data.roomInfo.rentalType, + floorType: data.roomInfo.floorType, + livingRoom: data.roomInfo.hasLivingRoom ? '유' : '무', + restRoomCount, + }, + selectedExtraOptions: { + 주차가능: data.roomInfo.extraOption.canPark, + 에어컨: data.roomInfo.extraOption.hasAirConditioner, + 냉장고: data.roomInfo.extraOption.hasRefrigerator, + 세탁기: data.roomInfo.extraOption.hasWasher, + '베란다/테라스': data.roomInfo.extraOption.hasTerrace, + }, + }); + }; + + const handleOptionClick = ( + optionName: keyof SelectedOptions, + item: string, + ) => { + console.log(state.selectedOptions, optionName, item); + setState(prev => ({ + ...prev, + selectedOptions: { + ...prev.selectedOptions, + [optionName]: prev.selectedOptions[optionName] === item ? null : item, + }, })); - }, []); - - const handleOptionClick = useCallback( - (optionName: keyof SelectedOptions, item: string) => { - setSelectedOptions(prevState => ({ - ...prevState, - [optionName]: prevState[optionName] === item ? null : item, - })); - }, - [], - ); - - const isOptionSelected = useCallback( - (optionName: keyof SelectedOptions, item: string) => - selectedOptions[optionName] === item, - [selectedOptions], - ); - - const isExtraOptionSelected = useCallback( - (item: string) => selectedExtraOptions[item], - [selectedExtraOptions], - ); - - const isPostCreatable = useMemo( - () => - images.length > 0 && - title.trim().length > 0 && - content.trim().length > 0 && - selectedOptions.budget != null && - expectedMonthlyFee > 0 && - selectedOptions.roomType != null && - houseSize > 0 && - selectedOptions.roomCount != null && - selectedOptions.restRoomCount != null && - selectedOptions.livingRoom != null && - mateLimit > 0 && - address != null, - [ - images, - title, - content, - selectedOptions, - expectedMonthlyFee, - houseSize, - mateLimit, - address, - ], - ); - - return useMemo( - () => ({ - title, - setTitle, - content, - setContent, - images, - setImages, - address, - setAddress, - mateLimit, - setMateLimit, - expectedMonthlyFee, - setExpectedMonthlyFee, - houseSize, - setHouseSize, - selectedExtraOptions, - setSelectedExtraOptions, - selectedOptions, - setSelectedOptions, - handleOptionClick, - handleExtraOptionClick, - isOptionSelected, - isExtraOptionSelected, - isPostCreatable, - }), - [ - title, - setTitle, - content, - setContent, - images, - setImages, - address, - setAddress, - mateLimit, - setMateLimit, - expectedMonthlyFee, - setExpectedMonthlyFee, - houseSize, - setHouseSize, - selectedExtraOptions, - setSelectedExtraOptions, - selectedOptions, - setSelectedOptions, - handleOptionClick, - handleExtraOptionClick, - isOptionSelected, - isExtraOptionSelected, - isPostCreatable, - ], - ); + }; + + const handleExtraOptionClick = (option: string) => { + console.log(state.selectedExtraOptions, option); + setState(prev => ({ + ...prev, + selectedExtraOptions: { + ...prev.selectedExtraOptions, + [option]: !prev.selectedExtraOptions[option], + }, + })); + }; + + const isOptionSelected = (optionName: keyof SelectedOptions, item: string) => + state.selectedOptions[optionName] === item; + + const isExtraOptionSelected = (item: string) => + state.selectedExtraOptions[item]; + + return { + ...state, + setSharedPostProps: setState, + setStateWithPost, + reset, + handleOptionClick, + handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, + }; }; export const usePostMateCardInputSection = () => { diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index e809f5ba44..f8c40e5965 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -33,8 +33,9 @@ export type SelectedExtraOptions = Record; export interface ImageFile { url: string; - file: File; - extension: string; + uploaded: boolean; + file?: File; + extension?: string; } export interface CreateSharedPostProps { From 2d85bbb7a3491b339085214953c1eddc03a3cbeb Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 12 May 2024 23:51:00 +0900 Subject: [PATCH 094/130] fix: Fix the error that prevented additional options from being saved (#75) --- src/app/pages/writing-post-page.tsx | 14 ++++++-- .../shared-posts-filter.type.ts | 7 +++- src/features/shared/shared.hook.ts | 36 ++++++++++--------- src/features/shared/shared.type.ts | 8 ++++- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index d5077aad7b..1708401f92 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -605,6 +605,14 @@ export function WritingPostPage() { }); }, []); + console.log(selectedExtraOptions, { + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, + }); + mutate( { imageFilesData: uploadedImages, @@ -891,12 +899,12 @@ export function WritingPostPage() { 추가 옵션 - {Object.keys(AdditionalInfoTypeValue).map(option => ( + {Object.entries(AdditionalInfoTypeValue).map(([option, value]) => ( { - handleExtraOptionClick(option); + handleExtraOptionClick(value); }} /> {option} diff --git a/src/entities/shared-posts-filter/shared-posts-filter.type.ts b/src/entities/shared-posts-filter/shared-posts-filter.type.ts index 372201bcf7..1ec83e8775 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.type.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.type.ts @@ -1,10 +1,15 @@ +import { type SelectedExtraOptions } from '@/features/shared'; + export type AdditionalInfoType = | '주차가능' | '에어컨' | '냉장고' | '세탁기' | '베란다/테라스'; -export const AdditionalInfoTypeValue: Record = { +export const AdditionalInfoTypeValue: Record< + AdditionalInfoType, + keyof SelectedExtraOptions +> = { 주차가능: 'canPark', 에어컨: 'hasAirConditioner', 냉장고: 'hasRefrigerator', diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index d5b1e26e42..ac3e29a405 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -17,6 +17,7 @@ import { import { sharedPostPropState } from './shared.atom'; import { type GetSharedPostDTO } from './shared.dto'; import { + type SelectedExtraOptions, type CreateSharedPostProps, type GetSharedPostsProps, type SelectedOptions, @@ -136,11 +137,11 @@ export const useSharedPostProps = () => { restRoomCount, }, selectedExtraOptions: { - 주차가능: data.roomInfo.extraOption.canPark, - 에어컨: data.roomInfo.extraOption.hasAirConditioner, - 냉장고: data.roomInfo.extraOption.hasRefrigerator, - 세탁기: data.roomInfo.extraOption.hasWasher, - '베란다/테라스': data.roomInfo.extraOption.hasTerrace, + canPark: data.roomInfo.extraOption.canPark, + hasAirConditioner: data.roomInfo.extraOption.hasAirConditioner, + hasRefrigerator: data.roomInfo.extraOption.hasRefrigerator, + hasWasher: data.roomInfo.extraOption.hasWasher, + hasTerrace: data.roomInfo.extraOption.hasTerrace, }, }); }; @@ -149,7 +150,6 @@ export const useSharedPostProps = () => { optionName: keyof SelectedOptions, item: string, ) => { - console.log(state.selectedOptions, optionName, item); setState(prev => ({ ...prev, selectedOptions: { @@ -159,22 +159,24 @@ export const useSharedPostProps = () => { })); }; - const handleExtraOptionClick = (option: string) => { - console.log(state.selectedExtraOptions, option); - setState(prev => ({ - ...prev, - selectedExtraOptions: { - ...prev.selectedExtraOptions, - [option]: !prev.selectedExtraOptions[option], - }, - })); + const handleExtraOptionClick = (option: keyof SelectedExtraOptions) => { + setState(prev => { + const value = prev.selectedExtraOptions[option] ?? false; + return { + ...prev, + selectedExtraOptions: { + ...prev.selectedExtraOptions, + [option]: !value, + }, + }; + }); }; const isOptionSelected = (optionName: keyof SelectedOptions, item: string) => state.selectedOptions[optionName] === item; - const isExtraOptionSelected = (item: string) => - state.selectedExtraOptions[item]; + const isExtraOptionSelected = (item: keyof SelectedExtraOptions) => + state.selectedExtraOptions[item] === true; return { ...state, diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index f8c40e5965..e273390dd4 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -29,7 +29,13 @@ export interface SelectedOptions { floorType?: string; } -export type SelectedExtraOptions = Record; +export interface SelectedExtraOptions { + canPark?: boolean; + hasAirConditioner?: boolean; + hasRefrigerator?: boolean; + hasTerrace?: boolean; + hasWasher?: boolean; +} export interface ImageFile { url: string; From 789f46f6fe10b0926c006566287330154bfee009 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 00:18:54 +0900 Subject: [PATCH 095/130] feat: PUT /shared/posts/studio/{postId} API (#75) --- src/app/pages/writing-post-page.tsx | 183 ++++++++++++++++++---------- src/features/shared/shared.api.ts | 19 ++- src/features/shared/shared.atom.ts | 4 +- src/features/shared/shared.hook.ts | 24 ++-- src/features/shared/shared.type.ts | 2 +- 5 files changed, 155 insertions(+), 77 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 1708401f92..08ba694fc7 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -26,6 +26,7 @@ import { useCreateSharedPost, usePostMateCardInputSection, useSharedPostProps, + useUpdateSharedPost, type ImageFile, } from '@/features/shared'; import { useToast } from '@/features/toast'; @@ -421,7 +422,7 @@ export function WritingPostPage() { useState(false); const { - mode, + postId, title, content, images, @@ -453,7 +454,9 @@ export function WritingPostPage() { handleOptionalFeatureChange, } = usePostMateCardInputSection(); - const { mutate } = useCreateSharedPost(); + const { mutate: createSharedPost } = useCreateSharedPost(); + const { mutate: updateSharedPost } = useUpdateSharedPost(); + const { createToast } = useToast(); const auth = useAuthValue(); @@ -586,7 +589,7 @@ export function WritingPostPage() { const putResults = await Promise.allSettled( urls.map(async ({ url, fileName, file, uploaded }) => { - if (uploaded) return { fileName }; + if (uploaded) return { fileName: url }; if (file != null) await putImage(url, file); return { fileName }; @@ -605,70 +608,128 @@ export function WritingPostPage() { }); }, []); - console.log(selectedExtraOptions, { - canPark: selectedExtraOptions.canPark ?? false, - hasAirConditioner: selectedExtraOptions.hasAirConditioner ?? false, - hasRefrigerator: selectedExtraOptions.hasRefrigerator ?? false, - hasWasher: selectedExtraOptions.hasWasher ?? false, - hasTerrace: selectedExtraOptions.hasTerrace ?? false, - }); + console.log(uploadedImages); - mutate( - { - imageFilesData: uploadedImages, - postData: { title, content }, - transactionData: { - rentalType: dealTypeValue, - expectedPayment: expectedMonthlyFee, - }, - roomDetailData: { - roomType: roomTypeValue, - floorType: floorTypeValue, - size: houseSize, - numberOfRoom, - numberOfBathRoom, - hasLivingRoom: selectedOptions.livingRoom === '유', - recruitmentCapacity: mateLimit, - extraOption: { - canPark: selectedExtraOptions.canPark ?? false, - hasAirConditioner: - selectedExtraOptions.hasAirConditioner ?? false, - hasRefrigerator: selectedExtraOptions.hasRefrigerator ?? false, - hasWasher: selectedExtraOptions.hasWasher ?? false, - hasTerrace: selectedExtraOptions.hasTerrace ?? false, + if (postId == null) { + createSharedPost( + { + imageFilesData: uploadedImages, + postData: { title, content }, + transactionData: { + rentalType: dealTypeValue, + expectedPayment: expectedMonthlyFee, }, + roomDetailData: { + roomType: roomTypeValue, + floorType: floorTypeValue, + size: houseSize, + numberOfRoom, + numberOfBathRoom, + hasLivingRoom: selectedOptions.livingRoom === '유', + recruitmentCapacity: mateLimit, + extraOption: { + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: + selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: + selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, + }, + }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + }, + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], }, - locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, - }, - roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, }, - participationMemberIds: - auth?.user != null ? [auth.user.memberId] : [], - }, - { - onSuccess: () => { - createToast({ - message: '게시글이 정상적으로 업로드되었습니다.', - option: { - duration: 3000, + ); + } else if (postId != null) { + updateSharedPost( + { + postId, + postData: { + imageFilesData: uploadedImages, + postData: { title, content }, + transactionData: { + rentalType: dealTypeValue, + expectedPayment: expectedMonthlyFee, }, - }); - router.back(); - }, - onError: () => { - createToast({ - message: '게시글 업로드에 실패했습니다.', - option: { - duration: 3000, + roomDetailData: { + roomType: roomTypeValue, + floorType: floorTypeValue, + size: houseSize, + numberOfRoom, + numberOfBathRoom, + hasLivingRoom: selectedOptions.livingRoom === '유', + recruitmentCapacity: mateLimit, + extraOption: { + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: + selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: + selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, + }, + }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, }, - }); + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, }, - }, - ); + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 수정되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 수정에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); + } } catch (error) { createToast({ message: '게시글 업로드에 실패했습니다.', @@ -696,7 +757,7 @@ export function WritingPostPage() { 기본 정보 - {mode === 'create' ? '작성하기' : '수정하기'} + {postId == null ? '작성하기' : '수정하기'} 제목 diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index e232014448..8fb83356af 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -6,10 +6,7 @@ import { type GetSharedPostDTO, type GetSharedPostsDTO, } from './shared.dto'; -import { - type CreateSharedPostProps, - type GetSharedPostsProps, -} from './shared.type'; +import { type SharedPostProps, type GetSharedPostsProps } from './shared.type'; import { type SuccessBaseDTO } from '@/shared/types'; @@ -38,9 +35,21 @@ export const getSharedPosts = async ({ return await axios.get(getURI()); }; -export const createSharedPost = async (postData: CreateSharedPostProps) => +export const createSharedPost = async (postData: SharedPostProps) => await axios.post(`/maru-api/shared/posts/studio`, postData); +export const updateSharedPost = async ({ + postId, + postData, +}: { + postId: number; + postData: SharedPostProps; +}) => + await axios.put( + `/maru-api/shared/posts/studio/${postId}`, + postData, + ); + export const getSharedPost = async (postId: number) => await axios.get(`/maru-api/shared/posts/studio/${postId}`); diff --git a/src/features/shared/shared.atom.ts b/src/features/shared/shared.atom.ts index 0a47e5a2c8..2f0d8fac80 100644 --- a/src/features/shared/shared.atom.ts +++ b/src/features/shared/shared.atom.ts @@ -9,7 +9,7 @@ import { import { type NaverAddress } from '@/features/geocoding'; export const sharedPostPropState = atom<{ - mode: 'create' | 'modify'; + postId?: number; title: string; content: string; images: ImageFile[]; @@ -22,7 +22,7 @@ export const sharedPostPropState = atom<{ }>({ key: 'sharedPostPropState', default: { - mode: 'create', + postId: undefined, title: '', content: '', images: [], diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index ac3e29a405..b79909d085 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -13,14 +13,15 @@ import { getSharedPosts, scrapDormitoryPost, scrapPost, + updateSharedPost, } from './shared.api'; import { sharedPostPropState } from './shared.atom'; import { type GetSharedPostDTO } from './shared.dto'; import { - type SelectedExtraOptions, - type CreateSharedPostProps, type GetSharedPostsProps, + type SelectedExtraOptions, type SelectedOptions, + type SharedPostProps, } from './shared.type'; import { fromAddrToCoord } from '../geocoding'; @@ -118,7 +119,7 @@ export const useSharedPostProps = () => { else if (data.roomInfo.numberOfBathRoom === 3) restRoomCount = '3개'; setState({ - mode: 'modify', + postId: data.id, title: data.title, content: data.content, images: data.roomImages.map(({ fileName }) => ({ @@ -300,11 +301,18 @@ export const usePostMateCardInputSection = () => { }; export const useCreateSharedPost = () => - useMutation, FailureDTO, CreateSharedPostProps>( - { - mutationFn: createSharedPost, - }, - ); + useMutation, FailureDTO, SharedPostProps>({ + mutationFn: createSharedPost, + }); + +export const useUpdateSharedPost = () => + useMutation< + AxiosResponse, + FailureDTO, + { postId: number; postData: SharedPostProps } + >({ + mutationFn: updateSharedPost, + }); export const useSharedPosts = ({ filter, diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index e273390dd4..780e5551cd 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -44,7 +44,7 @@ export interface ImageFile { extension?: string; } -export interface CreateSharedPostProps { +export interface SharedPostProps { imageFilesData: Array<{ fileName: string; isThumbNail: boolean; From 02becb1ab9b0509b15234204199f0c4bdb450154 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 00:32:09 +0900 Subject: [PATCH 096/130] feat: Add delete post action (#75) --- src/app/pages/shared-post-page.tsx | 79 +++++++++++++++++++++++++---- src/app/pages/writing-post-page.tsx | 2 - src/features/shared/shared.hook.ts | 17 ++----- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index d288033853..fdf45b6bd5 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -15,10 +15,12 @@ import { useUnfollowUser, } from '@/features/profile'; import { + useDeleteSharedPost, useScrapSharedPost, useSharedPost, useSharedPostProps, } from '@/features/shared'; +import { useToast } from '@/features/toast'; import { getAge } from '@/shared'; const styles = { @@ -377,7 +379,14 @@ const styles = { font-weight: 600; line-height: 1.5rem; `, - modifyPostButton: styled.button` + rowForDeleteAndModify: styled.div` + display: flex; + flex-direction: row; + gap: 1rem; + + align-self: end; + `, + postModifyButton: styled.button` all: unset; cursor: pointer; @@ -398,8 +407,28 @@ const styles = { font-style: normal; font-weight: 600; line-height: 1.5rem; + `, + postDeleteButton: styled.div` + all: unset; + cursor: pointer; - align-self: end; + display: flex; + width: fit-content; + height: fit-content; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + + border-radius: 0.5rem; + background: #e15637; + + color: #fff; + text-align: right; + font-family: Pretendard; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; `, }; @@ -410,8 +439,11 @@ export function SharedPostPage({ postId }: { postId: number }) { const mapRef = useRef(null); const router = useRouter(); + const { createToast } = useToast(); const { setStateWithPost } = useSharedPostProps(); + const { mutate: deleteSharedPost } = useDeleteSharedPost(); + const [selected, setSelected] = useState< | { memberId: string; @@ -568,14 +600,41 @@ export function SharedPostPage({ postId }: { postId: number }) { {sharedPost.data.publisherAccount.memberId === auth?.user?.memberId && ( - { - setStateWithPost(sharedPost); - router.push('/shared/writing'); - }} - > - 수정하기 - + + { + setStateWithPost(sharedPost); + router.push('/shared/writing'); + }} + > + 수정하기 + + { + deleteSharedPost(postId, { + onSuccess: () => { + createToast({ + message: '정상적으로 삭제되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '삭제하는데 실패하였습니다.', + option: { + duration: 3000, + }, + }); + }, + }); + }} + > + 삭제하기 + + )} diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 08ba694fc7..371f287abd 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -608,8 +608,6 @@ export function WritingPostPage() { }); }, []); - console.log(uploadedImages); - if (postId == null) { createSharedPost( { diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index b79909d085..3386e3899f 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -347,20 +347,9 @@ export const useSharedPost = ({ enabled, }); -export const useDeleteSharedPost = ({ - postId, - onSuccess, - onError, -}: { - postId: number; - onSuccess: (data: SuccessBaseDTO) => void; - onError: (error: Error) => void; -}) => - useMutation({ - mutationFn: async () => - await deleteSharedPost(postId).then(response => response.data), - onSuccess, - onError, +export const useDeleteSharedPost = () => + useMutation, FailureDTO, number>({ + mutationFn: deleteSharedPost, }); export const useScrapSharedPost = () => From 0b8261189a42f10a73d2733fee08aabebb9cdab0 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 01:52:15 +0900 Subject: [PATCH 097/130] feat: Add write post page - dormitory mate (#75) --- src/app/pages/shared-posts-page.tsx | 61 +++-- src/app/pages/writing-post-page.tsx | 272 ++++++++++++----------- src/components/shared-posts/PostCard.tsx | 33 ++- src/features/shared/shared.atom.ts | 2 + src/features/shared/shared.hook.ts | 95 +++----- 5 files changed, 242 insertions(+), 221 deletions(-) diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index c885fe0279..d38dc2f019 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { CircularButton } from '@/components'; @@ -19,11 +19,13 @@ import { import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { useRecommendationMate } from '@/features/recommendation'; import { + useDormitorySharedPosts, usePaging, useSharedPostProps, useSharedPosts, + type GetDormitorySharedPostsDTO, + type GetSharedPostsDTO, } from '@/features/shared'; -import { type GetSharedPostsDTO } from '@/features/shared/'; const styles = { container: styled.div` @@ -120,11 +122,13 @@ export function SharedPostsPage() { const auth = useAuthValue(); const [selected, setSelected] = useState('hasRoom'); const [totalPageCount, setTotalPageCount] = useState(0); - const [prevSharedPosts, setPrevSharedPosts] = - useState(null); + const [prevSharedPosts, setPrevSharedPosts] = useState< + GetSharedPostsDTO | GetDormitorySharedPostsDTO | null + >(null); const { setAuthUserData } = useAuthActions(); - const { reset: resetSharedPostProps } = useSharedPostProps(); + const { setSharedPostProps, reset: resetSharedPostProps } = + useSharedPostProps(); const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); @@ -149,6 +153,17 @@ export function SharedPostsPage() { page: page - 1, }); + const { data: dormitorySharedPosts } = useDormitorySharedPosts({ + filter: derivedFilter, + enabled: auth?.accessToken != null && selected === 'dormitory', + page: page - 1, + }); + + const posts = useMemo( + () => (selected === 'hasRoom' ? sharedPosts : dormitorySharedPosts), + [selected, sharedPosts, dormitorySharedPosts], + ); + const { data: recommendationMates } = useRecommendationMate({ memberId: auth?.user?.memberId ?? 'undefined', cardType: filter.cardType ?? 'mate', @@ -160,14 +175,17 @@ export function SharedPostsPage() { return () => { resetFilter(); }; - }, [resetFilter]); + }, [selected, resetFilter]); useEffect(() => { - if (sharedPosts != null) { + if (selected === 'hasRoom' && sharedPosts != null) { setTotalPageCount(sharedPosts.data.totalPages); setPrevSharedPosts(null); + } else if (selected === 'dormitory' && dormitorySharedPosts != null) { + setTotalPageCount(dormitorySharedPosts.data.totalPages); + setPrevSharedPosts(null); } - }, [sharedPosts]); + }, [selected, dormitorySharedPosts, sharedPosts]); useEffect(() => { if (userData != null) { @@ -187,6 +205,7 @@ export function SharedPostsPage() { { resetSharedPostProps(); + setSharedPostProps(prev => ({ ...prev, type: selected })); router.push('/shared/writing'); }} > @@ -199,17 +218,27 @@ export function SharedPostsPage() { {prevSharedPosts != null ? prevSharedPosts.data.content.map(post => ( - - - + { + router.push(`/shared/${post.id}`); + setSharedPostProps(prev => ({ ...prev, type: selected })); + }} + /> )) - : sharedPosts?.data.content.map(post => ( - - - + : posts?.data.content.map(post => ( + { + router.push(`/shared/${post.id}`); + setSharedPostProps(prev => ({ ...prev, type: selected })); + }} + /> ))} - {sharedPosts?.data.content.length !== 0 && ( + {posts?.data.content.length !== 0 && ( (false); const { + type, postId, title, content, @@ -907,142 +909,148 @@ export function WritingPostPage() { )} - - 거래 정보 - 거래 방식 - - {Object.keys(DealTypeValue).map(option => ( - - { - handleOptionClick('budget', option); - }} - /> - {option} - - ))} - - 희망 메이트 월 분담금 - - { - handleNumberInput(event.target.value, value => { - setSharedPostProps(prev => ({ - ...prev, - expectedMonthlyFee: value, - })); - }); - }} - $width={3} - /> - 만원 - - - - 방 정보 - - - {Object.keys(FloorTypeValue).map(option => ( - - { - handleOptionClick('floorType', option); - }} - /> - {option} - - ))} - - 추가 옵션 - - {Object.entries(AdditionalInfoTypeValue).map(([option, value]) => ( - - { - handleExtraOptionClick(value); - }} - /> - {option} - - ))} - - 방 종류 - - {Object.keys(RoomTypeValue).map(option => ( - - { - handleOptionClick('roomType', option); - }} - /> - {option} - - ))} - - 거실 - - {Object.keys(LivingRoomTypeValue).map(option => ( - - { - handleOptionClick('livingRoom', option); - }} - /> - {option} - - ))} - - 방 개수 - - {Object.keys(CountTypeValue).map(option => ( - - { - handleOptionClick('roomCount', option); + {type === 'hasRoom' && ( + <> + + 거래 정보 + 거래 방식 + + {Object.keys(DealTypeValue).map(option => ( + + { + handleOptionClick('budget', option); + }} + /> + {option} + + ))} + + 희망 메이트 월 분담금 + + { + handleNumberInput(event.target.value, value => { + setSharedPostProps(prev => ({ + ...prev, + expectedMonthlyFee: value, + })); + }); }} + $width={3} /> - {option} - - ))} - - 화장실 개수 - - {Object.keys(CountTypeValue).map(option => ( - - { - handleOptionClick('restRoomCount', option); + 만원 + + + + 방 정보 + + + {Object.keys(FloorTypeValue).map(option => ( + + { + handleOptionClick('floorType', option); + }} + /> + {option} + + ))} + + 추가 옵션 + + {Object.entries(AdditionalInfoTypeValue).map( + ([option, value]) => ( + + { + handleExtraOptionClick(value); + }} + /> + {option} + + ), + )} + + 방 종류 + + {Object.keys(RoomTypeValue).map(option => ( + + { + handleOptionClick('roomType', option); + }} + /> + {option} + + ))} + + 거실 + + {Object.keys(LivingRoomTypeValue).map(option => ( + + { + handleOptionClick('livingRoom', option); + }} + /> + {option} + + ))} + + 방 개수 + + {Object.keys(CountTypeValue).map(option => ( + + { + handleOptionClick('roomCount', option); + }} + /> + {option} + + ))} + + 화장실 개수 + + {Object.keys(CountTypeValue).map(option => ( + + { + handleOptionClick('restRoomCount', option); + }} + /> + {option} + + ))} + + 전체 면적 + + { + handleNumberInput(event.target.value, value => { + setSharedPostProps(prev => ({ + ...prev, + houseSize: value, + })); + }); }} + $width={2} /> - {option} - - ))} - - 전체 면적 - - { - handleNumberInput(event.target.value, value => { - setSharedPostProps(prev => ({ - ...prev, - houseSize: value, - })); - }); - }} - $width={2} - /> - - - + + + + + )} ); diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index 548ae8214b..6e983995e7 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -4,7 +4,10 @@ import styled from 'styled-components'; import { HorizontalDivider } from '..'; -import { type SharedPostListItem } from '@/entities/shared-post'; +import { + type DormitorySharedPostListItem, + type SharedPostListItem, +} from '@/entities/shared-post'; const styles = { container: styled.div` @@ -120,24 +123,32 @@ const styles = { `, }; -export function PostCard({ post }: { post: SharedPostListItem }) { +export function PostCard({ + post, + onClick, +}: { + post: SharedPostListItem | DormitorySharedPostListItem; + onClick: () => void; +}) { return (
- +

{post.title}

{post.address.roadAddress}

-
-

모집 {post.roomInfo.recruitmentCapacity}명

-

- {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · - 화장실 {post.roomInfo.numberOfBathRoom} -

-

희망 월 분담금 {post.roomInfo.expectedPayment}

-
+ {'roomInfo' in post && ( +
+

모집 {post.roomInfo.recruitmentCapacity}명

+

+ {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · + 화장실 {post.roomInfo.numberOfBathRoom} +

+

희망 월 분담금 {post.roomInfo.expectedPayment}만원

+
+ )}
diff --git a/src/features/shared/shared.atom.ts b/src/features/shared/shared.atom.ts index 2f0d8fac80..3956a6579c 100644 --- a/src/features/shared/shared.atom.ts +++ b/src/features/shared/shared.atom.ts @@ -9,6 +9,7 @@ import { import { type NaverAddress } from '@/features/geocoding'; export const sharedPostPropState = atom<{ + type: 'hasRoom' | 'dormitory'; postId?: number; title: string; content: string; @@ -22,6 +23,7 @@ export const sharedPostPropState = atom<{ }>({ key: 'sharedPostPropState', default: { + type: 'hasRoom', postId: undefined, title: '', content: '', diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 3386e3899f..0a49ad9a53 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -119,6 +119,7 @@ export const useSharedPostProps = () => { else if (data.roomInfo.numberOfBathRoom === 3) restRoomCount = '3개'; setState({ + type: state.type, postId: data.id, title: data.title, content: data.content, @@ -206,24 +207,21 @@ export const usePostMateCardInputSection = () => { options: Set; }>({ options: new Set() }); - const handleEssentialFeatureChange = useCallback( - ( - key: 'smoking' | 'roomSharingOption' | 'mateAge', - value: string | number | undefined, - ) => { - setFeatures(prev => { - if (prev[key] === value) { - const newFeatures = { ...prev }; - newFeatures[key] = undefined; - return newFeatures; - } - return { ...prev, [key]: value }; - }); - }, - [], - ); + const handleEssentialFeatureChange = ( + key: 'smoking' | 'roomSharingOption' | 'mateAge', + value: string | number | undefined, + ) => { + setFeatures(prev => { + if (prev[key] === value) { + const newFeatures = { ...prev }; + newFeatures[key] = undefined; + return newFeatures; + } + return { ...prev, [key]: value }; + }); + }; - const handleOptionalFeatureChange = useCallback((option: string) => { + const handleOptionalFeatureChange = (option: string) => { setFeatures(prev => { const { options } = prev; const newOptions = new Set(options); @@ -233,7 +231,7 @@ export const usePostMateCardInputSection = () => { return { ...prev, options: newOptions }; }); - }, []); + }; const derivedFeatures = useMemo(() => { const options: string[] = []; @@ -254,50 +252,23 @@ export const usePostMateCardInputSection = () => { } }, [auth?.user]); - const isMateCardCreatable = useMemo( - () => - gender != null && birthYear != null && location != null && budget != null, - [gender, birthYear, location, budget], - ); - - return useMemo( - () => ({ - gender, - setGender, - birthYear, - setBirthYear, - location, - setLocation, - mbti, - setMbti, - major, - setMajor, - budget, - setBudget, - derivedFeatures, - handleEssentialFeatureChange, - handleOptionalFeatureChange, - isMateCardCreatable, - }), - [ - gender, - setGender, - birthYear, - setBirthYear, - location, - setLocation, - mbti, - setMbti, - major, - setMajor, - budget, - setBudget, - derivedFeatures, - handleEssentialFeatureChange, - handleOptionalFeatureChange, - isMateCardCreatable, - ], - ); + return { + gender, + setGender, + birthYear, + setBirthYear, + location, + setLocation, + mbti, + setMbti, + major, + setMajor, + budget, + setBudget, + derivedFeatures, + handleEssentialFeatureChange, + handleOptionalFeatureChange, + }; }; export const useCreateSharedPost = () => From ee3dd6181a528336f4251eff1252dc1aa75858de Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 13 May 2024 14:12:09 +0900 Subject: [PATCH 098/130] feat: profile-page (#79) --- src/app/pages/mobile/index.ts | 1 + src/app/pages/mobile/mobile-main-page.tsx | 5 +- src/app/pages/mobile/mobile-profile-page.tsx | 437 +++++++++++++++++++ src/app/profile/[memberId]/page.tsx | 15 +- src/components/NavigationBar.tsx | 25 +- 5 files changed, 472 insertions(+), 11 deletions(-) create mode 100644 src/app/pages/mobile/mobile-profile-page.tsx diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts index f2eace5ac0..bb4f015bc6 100644 --- a/src/app/pages/mobile/index.ts +++ b/src/app/pages/mobile/index.ts @@ -1,2 +1,3 @@ export * from './mobile-landing-page'; export * from './mobile-main-page'; +export * from './mobile-profile-page'; diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx index 65ae8c85bc..ad67fada07 100644 --- a/src/app/pages/mobile/mobile-main-page.tsx +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -15,6 +15,7 @@ const styles = { container: styled.div` display: flex; width: 100vw; + min-width: 390px; height: 47.8125rem; padding-bottom: 2rem; flex-direction: column; @@ -75,14 +76,14 @@ const styles = { mateRecommendationRow: styled.div` display: flex; width: 100%; - padding: 0rem 1.5rem; + padding: 0 1.5rem; justify-content: space-between; align-items: center; gap: 1rem; `, mateRecommendation: styled.div` display: flex; - width: 75%; + width: 69%; padding: 0.5rem 0rem; align-items: center; gap: 0.25rem; diff --git a/src/app/pages/mobile/mobile-profile-page.tsx b/src/app/pages/mobile/mobile-profile-page.tsx new file mode 100644 index 0000000000..e19ed58106 --- /dev/null +++ b/src/app/pages/mobile/mobile-profile-page.tsx @@ -0,0 +1,437 @@ +'use client'; + +import Link from 'next/link'; +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; + +import { Bookmark } from '@/components'; +import { useAuthValue, useUserData } from '@/features/auth'; +import { + useFollowUser, + useFollowingListData, + useUnfollowUser, + useUserProfile, +} from '@/features/profile'; + +const styles = { + container: styled.div` + display: flex; + width: 100vw; + min-width: 390px; + padding-bottom: 2rem; + flex-direction: column; + align-items: center; + padding: 2rem 1rem; + gap: 3rem; + `, + + userProfileContainer: styled.div` + display: flex; + align-items: flex-start; + gap: 1.25rem; + align-self: stretch; + `, + userProfileWithoutInfo: styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + `, + userPicContainer: styled.div` + display: flex; + width: 6.0625rem; + height: 6.0625rem; + justify-content: center; + align-items: center; + border-radius: 50%; + border: 1px solid #dcddea; + background: #c4c4c4; + `, + userPic: styled.img` + width: 100%; + height: 100%; + border-radius: inherit; + object-fit: cover; + border: 0; + `, + userInfoContainer: styled.div` + display: flex; + padding: 0.5rem 0rem; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + flex: 1 0 0; + align-self: stretch; + `, + userDetailedContainer: styled.div` + display: inline-flex; + width: 100%; + align-items: flex-start; + gap: 2rem; + `, + userName: styled.div` + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1.125rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + userDetailedInfo: styled.p` + color: #000; + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + + switchContainer: styled.div` + display: inline-flex; + justify-content: center; + align-items: center; + gap: 0.375rem; + `, + switchWrapper: styled.label` + position: relative; + display: inline-block; + width: 2.2rem; + height: 1.26rem; + `, + switchInput: styled.input` + opacity: 0; + width: 0; + height: 0; + `, + slider: styled.span` + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #bebebe; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 20px; + `, + sliderDot: styled.span` + position: absolute; + cursor: pointer; + top: 0.1624rem; + left: 0.2rem; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; + width: 0.9rem; + height: 0.9rem; + `, + switchDescription: styled.p` + color: var(--Gray-3, #888); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + + authContainer: styled.div` + height: 2rem; + width: 5.3125rem; + border-radius: 26px; + background: var(--Black, #35373a); + cursor: pointer; + display: inline-flex; + padding: 0.125rem 0.5rem; + justify-content: center; + align-items: center; + gap: 0.25rem; + `, + authDescription: styled.p` + color: #fff; + + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: 1.5rem; /* 200% */ + `, + authCheckImg: styled.img` + width: 1rem; + height: 1rem; + `, + + cardSection: styled.div` + display: flex; + width: 100%; + align-items: flex-start; + gap: 1.5rem; + align-self: stretch; + `, + cardWrapper: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + `, + description32px: styled.p` + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: normal; + `, + cardContainer: styled.div` + display: inline-flex; + align-items: center; + width: 9.8125rem; + height: 9.8125rem; + padding: 3.9375rem 5.8125rem 4.4375rem 1.1875rem; + flex-shrink: 0; + border-radius: 20px; + border: 1pxs olid var(--background, #f7f6f9); + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + background: var(--grey-100, #fff); + position: relative; + `, + cardName: styled.p` + color: var(--grey-900, #494949); + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + `, +}; + +interface UserProfileInfoProps { + name: string | undefined; + email: string | undefined; + phoneNum: string | undefined; + src: string | undefined; + memberId: string; + isMySelf: boolean; +} + +function UserInfo({ + name, + email, + phoneNum, + src, + memberId, + isMySelf, +}: UserProfileInfoProps) { + const [isChecked, setIsChecked] = useState(false); + + const followList = useFollowingListData(); + const [isMarked, setIsMarked] = useState( + followList.data?.data.followingList[memberId] != null, + ); + + const toggleSwitch = () => { + setIsChecked(!isChecked); + }; + + const { mutate: follow } = useFollowUser(memberId); + const { mutate: unfollow } = useUnfollowUser(memberId); + + return ( + + + + + + + + + {name} + +
+ {phoneNum} + {email} +
+ {!isMySelf && ( + { + if (isMarked) unfollow(); + else follow(); + setIsMarked(prev => !prev); + }} + hasBorder + color="#888" + /> + )} +
+ +
+
+ ); +} + +interface ToggleSwitchProps { + isChecked: boolean; + onToggle: () => void; +} + +function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { + return ( + + + + + + + + 메이트 찾는 중 + + ); +} + +function Auth() { + return ( + + + 학교인증 + + ); +} + +function Card({ + name, + memberId, + myCardId, + mateCardId, + isMySelf, +}: { + name: string | undefined; + memberId: string | undefined; + myCardId: number | undefined; + mateCardId: number | undefined; + isMySelf: boolean; +}) { + return ( + + + 마이 카드 + + + {name} + + + + + 메이트 카드 + + + 메이트 + + + + + ); +} + +interface UserProps { + memberId: string; + email: string; + name: string; + birthYear: string; + gender: string; + phoneNumber: string; + initialized: boolean; + myCardId: number; + mateCardId: number; +} + +export function MobileProfilePage({ memberId }: { memberId: string }) { + const auth = useAuthValue(); + const { data } = useUserData(auth?.accessToken !== undefined); + + const authId = data?.memberId; + + const [userData, setUserData] = useState(null); + const [isMySelf, setIsMySelf] = useState(false); + + const { mutate: mutateProfile, data: profileData } = useUserProfile(memberId); + const [profileImg, setProfileImg] = useState(''); + + useEffect(() => { + mutateProfile(); + }, [auth]); + + useEffect(() => { + if (profileData?.data !== undefined) { + const userProfileData = profileData.data.authResponse; + const { + name, + email, + birthYear, + gender, + phoneNumber, + initialized, + myCardId, + mateCardId, + } = userProfileData; + setUserData({ + memberId, + name, + email, + birthYear, + gender, + phoneNumber, + initialized, + myCardId, + mateCardId, + }); + setProfileImg(profileData.data.profileImage); + if (authId === memberId) { + setIsMySelf(true); + } + } + }, [profileData, memberId]); + + return ( + + + + + ); +} diff --git a/src/app/profile/[memberId]/page.tsx b/src/app/profile/[memberId]/page.tsx index e241bdfbef..c2c8183fa9 100644 --- a/src/app/profile/[memberId]/page.tsx +++ b/src/app/profile/[memberId]/page.tsx @@ -1,9 +1,22 @@ +'use client'; + import { ProfilePage } from '@/app/pages'; +import { MobileProfilePage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page({ params: { memberId }, }: { params: { memberId: string }; }) { - return ; + const isMobile = useIsMobile(); + return ( + <> + {isMobile ? ( + + ) : ( + + )} + + ); } diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index cb9d05e5e8..8f05aaabd4 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -56,14 +56,17 @@ const styles = { display: none; @media (max-width: 768px) { - display: block; - width: 40vw; - height: 100%; + display: flex; + flex-direction: column; + width: 15rem; + padding: 1rem; background: #fff; z-index: 20000; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + border-radius: 1.25rem; position: fixed; - right: 0; + top: 5rem; + right: 0.5rem; } `, menuItem: styled.div` @@ -178,7 +181,7 @@ export function NavigationBar() { <> { - setIsMenuClick(prev => !prev); + setIsMenuClick(false); }} > maru @@ -216,7 +219,7 @@ export function NavigationBar() { { - setIsMenuClick(prev => !prev); + setIsMenuClick(false); }} >

메이트 찾기

@@ -225,7 +228,10 @@ export function NavigationBar() { { - setIsMenuClick(prev => !prev); + setIsMenuClick(false); + }} + style={{ + borderBottom: !isLogin ? 'none' : '1px solid #e5e5ea', }} >

마이페이지

@@ -234,9 +240,12 @@ export function NavigationBar() { {isLogin && ( { - setIsMenuClick(prev => !prev); + setIsMenuClick(false); handleLogout(); }} + style={{ + borderBottom: 'none', + }} >

로그아웃

From b6e4299758efe9229f7217dd3cfa77f034d4ef59 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 15:25:11 +0900 Subject: [PATCH 099/130] feat: Modify PostCard when fetching Dormitory Posts (#75) --- src/app/pages/shared-post-page.tsx | 6 ++--- src/app/pages/writing-post-page.tsx | 14 +++++++--- src/components/shared-posts/PostCard.tsx | 27 ++++++++++++------- .../shared-posts/SharedPostsMenu.tsx | 21 +++++++++------ src/entities/user/user.type.ts | 5 +++- src/features/auth/auth.dto.ts | 1 + src/features/shared/shared.type.ts | 5 +++- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index fdf45b6bd5..31384465db 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -457,11 +457,11 @@ export function SharedPostPage({ postId }: { postId: number }) { enabled: auth?.accessToken !== undefined, }); - const { data: userData } = useUserData(auth?.accessToken !== undefined); + const { data: userData } = useUserData(auth?.accessToken != null); const [userId, setUserId] = useState(''); useEffect(() => { - if (userData !== undefined) { + if (userData != null) { setUserId(userData.memberId); } }, [userData]); @@ -565,7 +565,7 @@ export function SharedPostPage({ postId }: { postId: number }) {
희망 월 분담금 - {sharedPost.data.roomInfo.expectedPayment} + {sharedPost.data.roomInfo.expectedPayment}만원
diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 7b44bb763b..e548f859e9 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -645,8 +645,11 @@ export function WritingPostPage() { location: address?.roadAddress, features: derivedFeatures, }, - participationMemberIds: - auth?.user != null ? [auth.user.memberId] : [], + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, }, { onSuccess: () => { @@ -705,8 +708,11 @@ export function WritingPostPage() { location: address?.roadAddress, features: derivedFeatures, }, - participationMemberIds: - auth?.user != null ? [auth.user.memberId] : [], + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, }, }, { diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index 6e983995e7..3b346932b6 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -130,6 +130,11 @@ export function PostCard({ post: SharedPostListItem | DormitorySharedPostListItem; onClick: () => void; }) { + const recruitmentCapacity = + 'roomInfo' in post + ? post.roomInfo.expectedPayment + : post.recruitmentCapacity; + return (
@@ -139,16 +144,18 @@ export function PostCard({

{post.title}

{post.address.roadAddress}

- {'roomInfo' in post && ( -
-

모집 {post.roomInfo.recruitmentCapacity}명

-

- {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · - 화장실 {post.roomInfo.numberOfBathRoom} -

-

희망 월 분담금 {post.roomInfo.expectedPayment}만원

-
- )} +
+

모집 {recruitmentCapacity}명

+ {'roomInfo' in post && ( + <> +

+ {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · + 화장실 {post.roomInfo.numberOfBathRoom} +

+

희망 월 분담금 {post.roomInfo.expectedPayment}만원

+ + )} +
diff --git a/src/components/shared-posts/SharedPostsMenu.tsx b/src/components/shared-posts/SharedPostsMenu.tsx index da1e0cdb29..74d9ad49ac 100644 --- a/src/components/shared-posts/SharedPostsMenu.tsx +++ b/src/components/shared-posts/SharedPostsMenu.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { type SharedPostsType } from '@/entities/shared-posts-filter'; +import { useAuthValue } from '@/features/auth'; const styles = { container: styled.div` @@ -55,6 +56,8 @@ export function SharedPostsMenu({ handleSelect, className, }: Props & React.ComponentProps<'div'>) { + const auth = useAuthValue(); + return ( 방 없는 메이트 - { - handleSelect('dormitory'); - }} - className={selected === 'dormitory' ? 'selected' : ''} - > - 기숙사 메이트 - + {auth?.user?.univCertified === true ?? ( + { + handleSelect('dormitory'); + }} + className={selected === 'dormitory' ? 'selected' : ''} + > + 기숙사 메이트 + + )} ); } diff --git a/src/entities/user/user.type.ts b/src/entities/user/user.type.ts index 5ea8e1cef8..f545455006 100644 --- a/src/entities/user/user.type.ts +++ b/src/entities/user/user.type.ts @@ -1,9 +1,12 @@ export interface User { memberId: string; + email: string; name: string; birthYear: string; gender: string; - email: string; phoneNumber: string; initialized: boolean; + myCardId: number; + mateCardId: number; + univCertified: boolean; } diff --git a/src/features/auth/auth.dto.ts b/src/features/auth/auth.dto.ts index abcff99826..686221d7ba 100644 --- a/src/features/auth/auth.dto.ts +++ b/src/features/auth/auth.dto.ts @@ -11,6 +11,7 @@ export interface GetUserDataDTO extends SuccessBaseDTO { initialized: boolean; myCardId: number; mateCardId: number; + univCertified: boolean; }; } diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 780e5551cd..48266813f0 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -87,5 +87,8 @@ export interface SharedPostProps { options: string; }; }; - participationMemberIds: string[]; + participationData: { + recruitmentCapacity: number; + participationMemberIds: string[]; + }; } From 1549cfefa6cbc4a7bb784cf57daccbfa39a9c12d Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 13 May 2024 15:44:52 +0900 Subject: [PATCH 100/130] feat: apply certificate --- public/Close_white.svg | 5 + src/app/pages/profile-page.tsx | 190 ++++++++++++++++++++++++++- src/features/profile/profile.api.ts | 23 ++++ src/features/profile/profile.dto.ts | 1 + src/features/profile/profile.hook.ts | 17 +++ 5 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 public/Close_white.svg diff --git a/public/Close_white.svg b/public/Close_white.svg new file mode 100644 index 0000000000..4455ef9104 --- /dev/null +++ b/public/Close_white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/pages/profile-page.tsx b/src/app/pages/profile-page.tsx index 63c443a617..42946174d1 100644 --- a/src/app/pages/profile-page.tsx +++ b/src/app/pages/profile-page.tsx @@ -7,8 +7,10 @@ import styled from 'styled-components'; import { Bookmark } from '@/components'; import { useAuthValue, useUserData } from '@/features/auth'; import { + useCertification, useFollowUser, useFollowingListData, + useGetCode, useUnfollowUser, useUserProfile, } from '@/features/profile'; @@ -336,6 +338,89 @@ const styles = { border-radius: 16px; background: #f7f6f9; `, + + certificationContainer: styled.div` + display: inline-flex; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + justify-content: center; + width: 40rem; + height: 20rem; + gap: 3rem; + border-radius: 20px; + background: #fff; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.25); + z-index: 20000; + position: absolute; + top: 21rem; + left: 17.5rem; + + p { + color: #494949; + font-family: 'Noto Sans KR'; + font-size: 1.25rem; + font-style: normal; + font-weight: 500; + line-height: normal; + } + `, + userInputList: styled.ul` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.625rem; + `, + userInputListItem: styled.li` + display: flex; + align-items: center; + gap: 1.5rem; + + div { + display: flex; + width: 22.1875rem; + justify-content: space-between; + align-items: center; + } + + p { + color: #494949; + font-family: 'Noto Sans KR'; + font-size: 1.25rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + input { + width: 15rem; + height: 2.125rem; + flex-shrink: 0; + border-radius: 12px; + border: 1px solid #494949; + padding: 0.5rem 1rem; + } + `, + + certificationButton: styled.button` + display: flex; + width: 9.5rem; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + gap: 0.25rem; + border-radius: 8px; + background-color: white; + border: 1px solid #494949; + + color: #494949; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 1.5rem; + `, }; interface UserProfileInfoProps { @@ -345,6 +430,7 @@ interface UserProfileInfoProps { src: string | undefined; memberId: string; isMySelf: boolean; + certification?: boolean; } function UserInfo({ @@ -354,6 +440,7 @@ function UserInfo({ src, memberId, isMySelf, + certification, }: UserProfileInfoProps) { const [isChecked, setIsChecked] = useState(false); @@ -375,7 +462,7 @@ function UserInfo({ - + {name} @@ -440,12 +527,97 @@ function ToggleSwitch({ isChecked, onToggle }: ToggleSwitchProps) { ); } -function Auth() { +function Auth({ certification }: { certification?: boolean }) { + const [univName, setUnivName] = useState(); + const [email, setEmail] = useState(); + const [code, setCode] = useState(); + const [isCertification, setIsCertification] = useState(certification); + + const { mutate: getCode } = useGetCode(email ?? '', univName ?? ''); + const { mutate: postCertification, data: success } = useCertification( + email ?? '', + univName ?? '', + code ?? 0, + ); + + useEffect(() => { + setIsCertification(true); + }, [success]); + + const [isCertificationClick, setIsCertificationClick] = useState(false); return ( - - - 학교인증 - + <> + { + setIsCertificationClick(prev => !prev); + }} + > + + 학교인증 + + {isCertificationClick && ( + +

학교 인증하기

+ + +
+

대학교 명

+ { + setUnivName(e.target.value); + }} + /> +
+
+ +
+

이메일

+ { + setEmail(e.target.value); + }} + /> +
+ { + getCode(); + }} + > + 인증코드 받기 + +
+ +
+

인증코드

+ { + setCode(Number(e.target.value)); + }} + /> +
+ { + postCertification(); + }} + > + 인증하기 + +
+
+
+ )} + ); } @@ -568,6 +740,7 @@ interface UserProps { initialized: boolean; myCardId: number; mateCardId: number; + univCertified: boolean; } export function ProfilePage({ memberId }: { memberId: string }) { @@ -596,8 +769,9 @@ export function ProfilePage({ memberId }: { memberId: string }) { gender, phoneNumber, initialized, - myCardId, + univCertified, mateCardId, + myCardId, } = userProfileData; setUserData({ memberId, @@ -609,6 +783,7 @@ export function ProfilePage({ memberId }: { memberId: string }) { initialized, myCardId, mateCardId, + univCertified, }); setProfileImg(profileData.data.profileImage); if (authId === memberId) { @@ -626,6 +801,7 @@ export function ProfilePage({ memberId }: { memberId: string }) { src={profileImg} memberId={memberId} isMySelf={isMySelf} + certification={userData?.univCertified} /> { return res.data; }; + +export const postEmail = async (email: string, univName: string) => { + const res = await axios.post(`/maru-api/profile/certificate`, { + email: email, + univName: univName, + }); + + return res.data; +}; + +export const postCertificate = async ( + email: string, + univName: string, + code: number, +) => { + const res = await axios.post(`/maru-api/profile/certificate`, { + email: email, + univName: univName, + code: code, + }); + + return res.data; +}; diff --git a/src/features/profile/profile.dto.ts b/src/features/profile/profile.dto.ts index fc2bb2a1c0..6d3dca47a4 100644 --- a/src/features/profile/profile.dto.ts +++ b/src/features/profile/profile.dto.ts @@ -10,6 +10,7 @@ export interface PostUserProfileDTO extends SuccessBaseDTO { gender: string; phoneNumber: string; initialized: boolean; + univCertified: boolean; myCardId: number; mateCardId: number; }; diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index b11090a2c4..5447816352 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -8,6 +8,8 @@ import { postUnfollowUser, postFollowUser, postUserProfile, + postEmail, + postCertificate, } from './profile.api'; export const useUserProfile = (memberId: string) => @@ -62,3 +64,18 @@ export const useSearchUser = (email: string) => mutationFn: async () => await postSearchUser(email), onSuccess: data => data.data, }); + +export const useGetCode = (email: string, univName: string) => + useMutation({ + mutationFn: async () => await postEmail(email, univName), + }); + +export const useCertification = ( + email: string, + univName: string, + code: number, +) => + useMutation({ + mutationFn: async () => await postCertificate(email, univName, code), + onSuccess: data => data.data, + }); From c52f94198796b89907cd251d466ad452ee6cc8e1 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 15:52:06 +0900 Subject: [PATCH 101/130] feat: POST /shared/posts/dormitory API (#75) --- src/app/pages/writing-post-page.tsx | 192 ++++++++++-------- src/components/shared-posts/PostCard.tsx | 3 +- .../shared-posts/SharedPostsMenu.tsx | 23 +-- src/features/shared/shared.api.ts | 6 + src/features/shared/shared.hook.ts | 24 ++- src/features/shared/shared.type.ts | 4 +- 6 files changed, 143 insertions(+), 109 deletions(-) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index e548f859e9..41dc688425 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -23,6 +23,7 @@ import { import { useAuthValue } from '@/features/auth'; import { getImageURL, putImage } from '@/features/image'; import { + useCreateDormitorySharedPost, useCreateSharedPost, usePostMateCardInputSection, useSharedPostProps, @@ -458,6 +459,7 @@ export function WritingPostPage() { const { mutate: createSharedPost } = useCreateSharedPost(); const { mutate: updateSharedPost } = useUpdateSharedPost(); + const { mutate: createDormitorySharedPost } = useCreateDormitorySharedPost(); const { createToast } = useToast(); @@ -610,72 +612,10 @@ export function WritingPostPage() { }); }, []); - if (postId == null) { - createSharedPost( - { - imageFilesData: uploadedImages, - postData: { title, content }, - transactionData: { - rentalType: dealTypeValue, - expectedPayment: expectedMonthlyFee, - }, - roomDetailData: { - roomType: roomTypeValue, - floorType: floorTypeValue, - size: houseSize, - numberOfRoom, - numberOfBathRoom, - hasLivingRoom: selectedOptions.livingRoom === '유', - recruitmentCapacity: mateLimit, - extraOption: { - canPark: selectedExtraOptions.canPark ?? false, - hasAirConditioner: - selectedExtraOptions.hasAirConditioner ?? false, - hasRefrigerator: - selectedExtraOptions.hasRefrigerator ?? false, - hasWasher: selectedExtraOptions.hasWasher ?? false, - hasTerrace: selectedExtraOptions.hasTerrace ?? false, - }, - }, - locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, - }, - roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, - }, - participationData: { - recruitmentCapacity: mateLimit, - participationMemberIds: - auth?.user != null ? [auth.user.memberId] : [], - }, - }, - { - onSuccess: () => { - createToast({ - message: '게시글이 정상적으로 업로드되었습니다.', - option: { - duration: 3000, - }, - }); - router.back(); - }, - onError: () => { - createToast({ - message: '게시글 업로드에 실패했습니다.', - option: { - duration: 3000, - }, - }); - }, - }, - ); - } else if (postId != null) { - updateSharedPost( - { - postId, - postData: { + if (type === 'hasRoom') { + if (postId == null) { + createSharedPost( + { imageFilesData: uploadedImages, postData: { title, content }, transactionData: { @@ -714,27 +654,111 @@ export function WritingPostPage() { auth?.user != null ? [auth.user.memberId] : [], }, }, - }, - { - onSuccess: () => { - createToast({ - message: '게시글이 정상적으로 수정되었습니다.', - option: { - duration: 3000, - }, - }); - router.back(); + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, }, - onError: () => { - createToast({ - message: '게시글 수정에 실패했습니다.', - option: { - duration: 3000, + ); + } else if (postId != null) { + updateSharedPost( + { + postId, + postData: { + imageFilesData: uploadedImages, + postData: { title, content }, + transactionData: { + rentalType: dealTypeValue, + expectedPayment: expectedMonthlyFee, }, - }); + roomDetailData: { + roomType: roomTypeValue, + floorType: floorTypeValue, + size: houseSize, + numberOfRoom, + numberOfBathRoom, + hasLivingRoom: selectedOptions.livingRoom === '유', + recruitmentCapacity: mateLimit, + extraOption: { + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: + selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: + selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, + }, + }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + }, + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, + }, }, - }, - ); + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 수정되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 수정에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); + } + } else if (type === 'dormitory') { + if (postId == null) { + createDormitorySharedPost({ + imageFilesData: uploadedImages, + postData: { title, content }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + }, + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, + }); + } } } catch (error) { createToast({ diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index 3b346932b6..cf1da85dac 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -2,8 +2,7 @@ import styled from 'styled-components'; -import { HorizontalDivider } from '..'; - +import { HorizontalDivider } from '@/components'; import { type DormitorySharedPostListItem, type SharedPostListItem, diff --git a/src/components/shared-posts/SharedPostsMenu.tsx b/src/components/shared-posts/SharedPostsMenu.tsx index 74d9ad49ac..34ad5cec80 100644 --- a/src/components/shared-posts/SharedPostsMenu.tsx +++ b/src/components/shared-posts/SharedPostsMenu.tsx @@ -3,7 +3,6 @@ import styled from 'styled-components'; import { type SharedPostsType } from '@/entities/shared-posts-filter'; -import { useAuthValue } from '@/features/auth'; const styles = { container: styled.div` @@ -56,7 +55,7 @@ export function SharedPostsMenu({ handleSelect, className, }: Props & React.ComponentProps<'div'>) { - const auth = useAuthValue(); + // const auth = useAuthValue(); return ( @@ -76,16 +75,16 @@ export function SharedPostsMenu({ > 방 없는 메이트 - {auth?.user?.univCertified === true ?? ( - { - handleSelect('dormitory'); - }} - className={selected === 'dormitory' ? 'selected' : ''} - > - 기숙사 메이트 - - )} + {/* {auth?.user?.univCertified === true ?? ( + )} */} + { + handleSelect('dormitory'); + }} + className={selected === 'dormitory' ? 'selected' : ''} + > + 기숙사 메이트 + ); } diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index 8fb83356af..c7637fe342 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -82,6 +82,12 @@ export const getDormitorySharedPosts = async ({ return await axios.get(getURI()); }; +export const createDormitorySharedPost = async (postData: SharedPostProps) => + await axios.post( + `/maru-api/shared/posts/dormitory`, + postData, + ); + export const getDormitorySharedPost = async (postId: number) => await axios.get( `/maru-api/shared/posts/dormitory/${postId}`, diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 0a49ad9a53..688df7a5ac 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRecoilState, useResetRecoilState } from 'recoil'; import { + createDormitorySharedPost, createSharedPost, deleteDormitorySharedPost, deleteSharedPost, @@ -276,15 +277,6 @@ export const useCreateSharedPost = () => mutationFn: createSharedPost, }); -export const useUpdateSharedPost = () => - useMutation< - AxiosResponse, - FailureDTO, - { postId: number; postData: SharedPostProps } - >({ - mutationFn: updateSharedPost, - }); - export const useSharedPosts = ({ filter, search, @@ -318,6 +310,15 @@ export const useSharedPost = ({ enabled, }); +export const useUpdateSharedPost = () => + useMutation< + AxiosResponse, + FailureDTO, + { postId: number; postData: SharedPostProps } + >({ + mutationFn: updateSharedPost, + }); + export const useDeleteSharedPost = () => useMutation, FailureDTO, number>({ mutationFn: deleteSharedPost, @@ -328,6 +329,11 @@ export const useScrapSharedPost = () => mutationFn: scrapPost, }); +export const useCreateDormitorySharedPost = () => + useMutation, FailureDTO, SharedPostProps>({ + mutationFn: createDormitorySharedPost, + }); + export const useDormitorySharedPosts = ({ filter, search, diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 48266813f0..69851082a1 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -54,11 +54,11 @@ export interface SharedPostProps { title: string; content: string; }; - transactionData: { + transactionData?: { rentalType: number; expectedPayment: number; }; - roomDetailData: { + roomDetailData?: { roomType: number; floorType: number; size: number; From d8563a74fd0cea7f28dc8c724fdac04879286a90 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Mon, 13 May 2024 16:19:21 +0900 Subject: [PATCH 102/130] feat: PUT /shared/posts/dormitory/{postId} API (#75) --- src/app/pages/writing-post-page.tsx | 23 +++++++++++++++++++++++ src/features/shared/shared.api.ts | 12 ++++++++++++ src/features/shared/shared.hook.ts | 9 +++++++++ 3 files changed, 44 insertions(+) diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 41dc688425..6e86647605 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -27,6 +27,7 @@ import { useCreateSharedPost, usePostMateCardInputSection, useSharedPostProps, + useUpdateDormitorySharedPost, useUpdateSharedPost, type ImageFile, } from '@/features/shared'; @@ -460,6 +461,7 @@ export function WritingPostPage() { const { mutate: createSharedPost } = useCreateSharedPost(); const { mutate: updateSharedPost } = useUpdateSharedPost(); const { mutate: createDormitorySharedPost } = useCreateDormitorySharedPost(); + const { mutate: updateDormitorySharedPost } = useUpdateDormitorySharedPost(); const { createToast } = useToast(); @@ -758,6 +760,27 @@ export function WritingPostPage() { auth?.user != null ? [auth.user.memberId] : [], }, }); + } else if (postId != null) { + updateDormitorySharedPost({ + postId, + postData: { + imageFilesData: uploadedImages, + postData: { title, content }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + }, + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, + }, + }); } } } catch (error) { diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index c7637fe342..006b750724 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -93,6 +93,18 @@ export const getDormitorySharedPost = async (postId: number) => `/maru-api/shared/posts/dormitory/${postId}`, ); +export const updateDormitorySharedPost = async ({ + postId, + postData, +}: { + postId: number; + postData: SharedPostProps; +}) => + await axios.put( + `/maru-api/shared/posts/dormitory/${postId}`, + postData, + ); + export const deleteDormitorySharedPost = async (postId: number) => await axios.delete( `/maru-api/shared/posts/dormitory/${postId}`, diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 688df7a5ac..18bea894a6 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -364,6 +364,15 @@ export const useDormitorySharedPost = ({ enabled, }); +export const useUpdateDormitorySharedPost = () => + useMutation< + AxiosResponse, + FailureDTO, + { postId: number; postData: SharedPostProps } + >({ + mutationFn: updateSharedPost, + }); + export const useDeleteDormitorySharedPost = ({ postId, onSuccess, From cd4ad55f60d1385f633d0ca63c6b88a424b52391 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 13 May 2024 19:04:08 +0900 Subject: [PATCH 103/130] feat: user-input-page (#79) --- src/app/globals.scss | 5 + src/app/pages/mobile/index.ts | 1 + .../pages/mobile/mobile-user-input-page.tsx | 390 ++++++++++++++++++ src/app/profile/page.tsx | 7 +- src/components/UserInputSection.tsx | 6 +- src/components/card/CleanTest.tsx | 18 + src/components/card/MajorSelector.tsx | 7 + src/components/card/MbtiToggle.tsx | 30 +- src/components/card/OptionSection.tsx | 64 ++- src/components/card/Slider.tsx | 11 + src/components/card/VitalSection.tsx | 52 ++- 11 files changed, 573 insertions(+), 18 deletions(-) create mode 100644 src/app/pages/mobile/mobile-user-input-page.tsx diff --git a/src/app/globals.scss b/src/app/globals.scss index b7fff38263..9d8f9fed3d 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -7,6 +7,10 @@ body { min-height: 100dvh; min-width: 90rem; max-width: 100dvw; + + @media (max-width: 768px) { + min-width: 390px; + } } * { @@ -32,6 +36,7 @@ main { @media (max-width: 768px) { justify-content: flex-start; + main: 100vw; } } diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts index bb4f015bc6..1c0d0f7e25 100644 --- a/src/app/pages/mobile/index.ts +++ b/src/app/pages/mobile/index.ts @@ -1,3 +1,4 @@ export * from './mobile-landing-page'; export * from './mobile-main-page'; export * from './mobile-profile-page'; +export * from './mobile-user-input-page'; diff --git a/src/app/pages/mobile/mobile-user-input-page.tsx b/src/app/pages/mobile/mobile-user-input-page.tsx new file mode 100644 index 0000000000..8bd8efcfae --- /dev/null +++ b/src/app/pages/mobile/mobile-user-input-page.tsx @@ -0,0 +1,390 @@ +'use client'; + +import Link from 'next/link'; +import React, { useState, useEffect, useCallback } from 'react'; +import styled from 'styled-components'; + +import { VitalSection, OptionSection } from '@/components'; +import { useAuthValue, useUserData } from '@/features/auth'; +import { usePutUserCard } from '@/features/profile'; + +const styles = { + pageContainer: styled.div` + display: flex; + align-items: center; + width: 100vw; + min-width: 390px; + flex-direction: column; + gap: 1rem; + padding: 2rem 1rem; + `, + pageDescription: styled.p` + width: 100%; + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 1.1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + `, + cardContainer: styled.div` + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + flex: 1 0 0; + align-self: stretch; + `, + miniCardContainer: styled.div` + display: flex; + width: 100%; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; + `, + cardNameSection: styled.div` + display: flex; + width: 100%; + align-items: flex-start; + align-self: stretch; + `, + miniCard: styled.div` + display: flex; + padding: 1rem 2rem; + flex-direction: column; + align-items: flex-start; + width: 50%; + gap: 1rem; + flex: 1 0 0; + align-self: stretch; + border-radius: 1.875rem 1.875rem 0rem 0rem; + + /* button */ + background: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--background, #f7f6f9)' + : 'var(--White, #fff)'}; + border-top: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'none' + : '2px solid var(--Light-gray, #DCDDEA)'}; + border-left: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'none' + : '2px solid var(--Light-gray, #DCDDEA)'}; + border-right: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'none' + : '2px solid var(--Light-gray, #DCDDEA)'}; + `, + miniCardName: styled.p` + color: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? 'var(--Black, #35373A)' + : 'var(--Gray-3, #888)'}; + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 500; + line-height: normal; + `, + checkSection: styled.div` + display: flex; + width: 50rem; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + border-radius: 1.875rem; + background: var(--background, #f7f6f9); + position: relative; + `, + checkContainer: styled.div` + display: inline-flex; + width: 50rem; + padding: 2rem; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + border-radius: 1.875rem; + background: var(--background, #f7f6f9); + position: relative; + + @media (max-width: 768px) { + width: 100%; + border-radius: 0 0 30px 30px; + } + + display: ${props => + props.$active !== undefined && props.$active !== null && props.$active + ? '' + : 'none'}; + `, + horizontalLine: styled.div` + width: 100%; + height: 0.0625rem; + background: var(--Gray-9, #d3d0d7); + `, + mateButtonContainer: styled.div` + display: flex; + width: 10rem; + padding: 0.375rem 0.75rem; + justify-content: center; + align-items: center; + gap: 0.25rem; + border-radius: 8px; + background: var(--Main-1, #e15637); + `, + mateButtonDescription: styled.p` + color: #fff; + + font-family: Pretendard; + font-size: 0.75rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + `, + mateButtonIcon: styled.img` + width: 1rem; + height: 1rem; + `, +}; + +interface CardActiveProps { + $active?: boolean; +} + +interface UserProps { + memberId: string | undefined; + name: string | undefined; + gender: string | undefined; + birthYear: string | undefined; + myCardId: number | undefined; + mateCardId: number | undefined; +} + +const useSelectedState = (): [ + ( + | { + smoking?: string; + roomSharingOption?: string; + mateAge?: number; + options?: Set; + } + | undefined + ), + (key: 'smoking' | 'roomSharingOption' | 'mateAge', value: string) => void, + (option: string) => void, +] => { + const [features, setFeatures] = useState<{ + smoking?: string; + roomSharingOption?: string; + mateAge?: number; + options?: Set; + }>({ options: new Set() }); + + const handleEssentialFeatureChange = useCallback( + ( + key: 'smoking' | 'roomSharingOption' | 'mateAge', + value: string | number, + ) => { + setFeatures(prev => { + if (prev?.[key] === value) { + return { ...prev, [key]: undefined }; + } + return { ...prev, [key]: value }; + }); + }, + [], + ); + + const handleOptionalFeatureChange = useCallback((option: string) => { + setFeatures(prev => { + const { options } = prev; + const newOptions = new Set(options); + + if (options != null && options.has(option)) newOptions.delete(option); + else newOptions.add(option); + + return { ...prev, options: newOptions }; + }); + }, []); + + return [features, handleEssentialFeatureChange, handleOptionalFeatureChange]; +}; + +export function MobileUserInputPage() { + const auth = useAuthValue(); + const { data } = useUserData(auth?.accessToken !== undefined); + const [user, setUserData] = useState(null); + + useEffect(() => { + if (data !== undefined) { + const { name, gender, birthYear, memberId, myCardId, mateCardId } = data; + setUserData({ name, gender, birthYear, memberId, myCardId, mateCardId }); + } + }, [data]); + + const [myFeatures, handleFeatureChange, handleOptionClick] = + useSelectedState(); + const [mateFeatures, handleMateFeatureChange, handleMateOptionClick] = + useSelectedState(); + + const myCardId = user?.myCardId ?? 0; + const mateCardId = user?.mateCardId ?? 0; + + const { mutate: mutateMyCard } = usePutUserCard(myCardId); + const { mutate: mutateMateCard } = usePutUserCard(mateCardId); + + const [locationInput, setLocation] = useState(''); + + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + const [budget, setBudget] = useState(''); + + const [mateMbti, setMateMbti] = useState(''); + const [mateMajor, setMateMajor] = useState(''); + const [mateBudget, setMateBudget] = useState(''); + + const [mateAge, setMateAge] = useState(0); + + const handleButtonClick = () => { + const location = locationInput ?? ''; + const myOptions: string[] = [mbti ?? '', major ?? '', budget ?? '']; + myFeatures?.options?.forEach(option => myOptions.push(option)); + + const mutateMyfeatures = { + smoking: myFeatures?.smoking ?? '상관없어요', + roomSharingOption: myFeatures?.roomSharingOption ?? '상관없어요', + mateAge: mateAge ?? 0, + options: JSON.stringify(myOptions.filter(value => value !== '')), + }; + + const mateOptions: string[] = [ + mateMbti ?? '', + mateMajor ?? '', + mateBudget ?? '', + ]; + mateFeatures?.options?.forEach(option => mateOptions.push(option)); + + const mutateMateFeatures = { + smoking: mateFeatures?.smoking ?? '상관없어요', + roomSharingOption: mateFeatures?.roomSharingOption ?? '상관없어요', + mateAge: mateAge ?? 0, + options: JSON.stringify(mateOptions.filter(value => value !== '')), + }; + + try { + mutateMyCard({ + location, + features: mutateMyfeatures, + }); + mutateMateCard({ + location, + features: mutateMateFeatures, + }); + } catch (error) { + console.error(error); + } + }; + + const [activeContainer, setActiveContainer] = useState<'my' | 'mate'>('my'); + + const handleMyCardClick = () => { + setActiveContainer('my'); + }; + + const handleMateCardClick = () => { + setActiveContainer('mate'); + }; + + return ( + + + {user?.name} 님과 희망 메이트에 대해서 알려주세요 + + + + + + + 내카드 + + + + + 메이트카드 + + + + + + {}} + isMySelf + type="myCard" + /> + + + + + + + + + + + + + 나의 메이트 확인하기 + + + + + + ); +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 59fbe39d59..461e557c97 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,5 +1,10 @@ +'use client'; + import { UserInputPage } from '@/app/pages'; +import { MobileUserInputPage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page() { - return ; + const isMobile = useIsMobile(); + return <>{isMobile ? : }; } diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index 7e6cbd03fa..87269bdeba 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -13,8 +13,12 @@ const styles = { flex-direction: column; align-items: flex-start; gap: 2rem; - border-radius: 1.875rem; + border-radius: 30px; background: var(--background, #f7f6f9); + + @media (max-width: 768px) { + border-radius: 0 0 30px 30px; + } `, horizontalLine: styled.div` width: 43.75rem; diff --git a/src/components/card/CleanTest.tsx b/src/components/card/CleanTest.tsx index ff6d2d03c9..47de438ce0 100644 --- a/src/components/card/CleanTest.tsx +++ b/src/components/card/CleanTest.tsx @@ -24,6 +24,10 @@ const styles = { align-items: center; gap: 2rem; align-self: stretch; + + @media (max-width: 768px) { + gap: 1rem; + } `, testDescription: styled.p` width: 7rem; @@ -34,11 +38,20 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.625rem; + width: 4rem; + } `, testItemContainer: styled.div` display: flex; align-items: flex-end; gap: 0.5rem; + + @media (max-width: 768px) { + flex-wrap: wrap; + } `, }; @@ -65,6 +78,11 @@ const CheckItem = styled.div` font-weight: 500; line-height: normal; + @media (max-width: 768px) { + font-size: 0.625rem; + padding: 0.5rem 1rem; + } + ${props => props.$isSelected ? { diff --git a/src/components/card/MajorSelector.tsx b/src/components/card/MajorSelector.tsx index 6104e2b726..355032d9ab 100644 --- a/src/components/card/MajorSelector.tsx +++ b/src/components/card/MajorSelector.tsx @@ -34,6 +34,13 @@ const styles = { &:focus { outline: none; } + + @media (max-width: 768px) { + width: 3.5rem; + border-radius: 13px; + font-size: 0.625rem; + left: 9.3rem; + } `, }; diff --git a/src/components/card/MbtiToggle.tsx b/src/components/card/MbtiToggle.tsx index 7f54b04f74..e21e8de6c3 100644 --- a/src/components/card/MbtiToggle.tsx +++ b/src/components/card/MbtiToggle.tsx @@ -16,6 +16,14 @@ const styles = { background: #fff; z-index: 5; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + + @media (max-width: 768px) { + gap: 1rem; + padding: 1rem; + width: 12.5rem; + flex-wrap: wrap; + bottom: -8rem; + } `, mbtiToggleContainer: styled.div` display: inline-flex; @@ -29,6 +37,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.625rem; + } `, switchContainer: styled.div` @@ -42,6 +54,11 @@ const styles = { display: inline-block; width: 2.5rem; height: 1.5rem; + + @media (max-width: 768px) { + width: 2rem; + height: 1rem; + } `, switchInput: styled.input` opacity: 0; @@ -65,13 +82,19 @@ const styles = { cursor: pointer; top: 0.25rem; left: 0.25rem; - bottom: 0.25rem; background-color: white; -webkit-transition: 0.4s; transition: 0.4s; border-radius: 50%; width: 1rem; height: 1rem; + + @media (max-width: 768px) { + width: 0.8rem; + height: 0.8rem; + top: 0.13rem; + left: 0.13rem; + } `, value: styled.p` color: #000; @@ -82,6 +105,11 @@ const styles = { font-weight: 500; line-height: normal; align-self: stretch; + + @media (max-width: 768px) { + font-size: 0.625rem; + width: 100%; + } `, }; diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index 1a36f7f704..cb6b01164b 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -11,10 +11,14 @@ import { Slider } from './Slider'; const styles = { container: styled.div` width: 46rem; - height: 43.875rem; + /* height: 43.875rem; */ display: flex; flex-direction: column; gap: 1rem; + + @media (max-width: 768px) { + width: 100%; + } `, optionContainer: styled.div` display: flex; @@ -25,6 +29,10 @@ const styles = { align-items: flex-start; gap: 1rem; flex-shrink: 0; + + @media (max-width: 768px) { + width: 100%; + } `, optionDescription: styled.p` color: #9a95a3; @@ -34,20 +42,37 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.75rem; + } `, optionList: styled.ul` position: relative; width: 100%; + + @media (max-width: 768px) { + gap: 1srem; + } `, optionListItem: styled.li` display: flex; align-items: center; gap: 4rem; margin-bottom: 1.25rem; + + @media (max-width: 768px) { + gap: 2rem; + } `, optionListImg: styled.img` width: 3.125rem; height: 3.125rem; + + @media (max-width: 768px) { + width: 1.2rem; + height: 1.2rem; + } `, optionListCheckItemContainer: styled.div` width: 100%; @@ -68,8 +93,6 @@ const styles = { flex-wrap: wrap; `, cleanTestContainer: styled.div` - position: absolute; - left: 22.19rem; height: 2.44rem; display: flex; align-items: center; @@ -87,6 +110,10 @@ const styles = { font-weight: 500; cursor: pointer; margin-left: 2rem; + + @media (max-width: 768px) { + font-size: 0.625rem; + } `, budgetContainer: styled.div` display: flex; @@ -112,6 +139,12 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + padding: 0.5rem 0.8rem; + font-size: 0.625rem; + width: 3.8rem; + } `, }; @@ -138,6 +171,11 @@ const CheckItem = styled.div` font-weight: 500; line-height: normal; + @media (max-width: 768px) { + padding: 0.5rem 1.25rem; + font-size: 0.625rem; + } + ${props => props.$isSelected ? { @@ -417,15 +455,6 @@ export function OptionSection({ {type === 'myCard' ? ( - - { - toggleTestVisibility(); - }} - > - {isTestVisible ? '결과 확인하기' : '테스트 하기'} - - @@ -435,6 +464,15 @@ export function OptionSection({ 천하태평 + + { + toggleTestVisibility(); + }} + > + {isTestVisible ? '결과 확인하기' : '테스트 하기'} + + ) : ( @@ -544,7 +582,7 @@ export function OptionSection({ style={{ color: '#888', fontFamily: 'Noto Sans KR', - fontSize: '1rem', + fontSize: '0.475rem', fontStyle: 'normal', fontWeight: '500', lineHeight: 'normal', diff --git a/src/components/card/Slider.tsx b/src/components/card/Slider.tsx index 7cad3eb552..c745c8f2b4 100644 --- a/src/components/card/Slider.tsx +++ b/src/components/card/Slider.tsx @@ -6,12 +6,18 @@ import styled from 'styled-components'; const styles = { container: styled.div` display: inline-flex; + width: 50%; align-items: center; `, sliderContainer: styled.div` width: 22rem; height: 1.875rem; position: relative; + + @media (max-width: 768px) { + width: 100%; + height: 1.5rem; + } `, sliderTrack: styled.div` width: 100%; @@ -58,6 +64,11 @@ const styles = { position: relative; z-index: 1; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); + + @media (max-width: 768px) { + width: 1.2rem; + height: 1.2rem; + } } `, }; diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 364da6aad4..05f99958ca 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -10,6 +10,10 @@ const styles = { align-items: flex-start; gap: 1rem; align-self: stretch; + + @media (max-width: 768px) { + width: 100%; + } `, vitalDescription: styled.p` width: 2.6875rem; @@ -19,6 +23,11 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.75rem; + width: 1.5rem; + } `, vitalListContainer: styled.ul` display: flex; @@ -32,6 +41,10 @@ const styles = { align-items: center; gap: 2rem; align-self: stretch; + + @media (max-width: 768px) { + gap: 0.1rem; + } `, vitalListItemDescription: styled.p` width: 5rem; @@ -41,13 +54,22 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; - list-style-type: none; + + @media (max-width: 768px) { + font-size: 0.625rem; + width: 3.5rem; + } `, vitalCheckListContainer: styled.div` display: inline-flex; align-items: flex-start; gap: 0.5rem; + flex-wrap: wrap; + + @media (max-width: 768px) { + gap: 0.2rem; + } `, birthYear: styled.select` @@ -77,6 +99,12 @@ const styles = { background-position: calc(100% - 0.6875rem) center; padding-right: 2.75rem; + + @media (max-width: 768px) { + font-size: 0.625rem; + height: 2.8rem; + width: 6rem; + } `, searchBox: styled.div` @@ -97,6 +125,10 @@ const styles = { &:focus { outline: none; } + + @media (max-width: 768px) { + font-size: 0.625rem; + } `, value: styled.span` display: flex; @@ -115,10 +147,15 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + padding: 0.5rem 1.25rem; + font-size: 0.625rem; + } `, sliderContainer: styled.div` - width: 22rem; + width: 50%; height: 1.875rem; position: relative; `, @@ -165,6 +202,11 @@ const styles = { position: relative; z-index: 1; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); + + @media (max-width: 768px) { + width: 1.2rem; + height: 1.2rem; + } } `, }; @@ -197,6 +239,11 @@ const CheckItem = styled.div` font-weight: 500; line-height: normal; + @media (max-width: 768px) { + padding: 0.5rem 1rem; + font-size: 0.625rem; + } + ${props => props.$isSelected ? { @@ -480,6 +527,7 @@ export function VitalSection({ style={{ display: 'flex', gap: '1rem', + width: '100%', alignItems: 'center', }} > From 14718a69ac1a2dcc11bfbb4cde133c51bd444f15 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Mon, 13 May 2024 19:37:27 +0900 Subject: [PATCH 104/130] feat: card-page (#79) --- src/app/pages/main-page.tsx | 30 +- src/app/pages/mobile/index.ts | 1 + src/app/pages/mobile/mobile-main-page.tsx | 41 +-- src/app/pages/mobile/mobile-setting-page.tsx | 295 +++++++++++++++++++ src/app/pages/shared-posts-page.tsx | 33 +-- src/app/profile/card/[cardId]/page.tsx | 15 +- src/components/UserInputSection.tsx | 1 + src/components/UserSearchBox.tsx | 1 + src/features/shared/shared.hook.ts | 55 ---- 9 files changed, 344 insertions(+), 128 deletions(-) create mode 100644 src/app/pages/mobile/mobile-setting-page.tsx diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index c9df3f3a7b..6fa40ae057 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -9,7 +9,7 @@ import { CircularButton } from '@/components'; import { UserCard } from '@/components/main-page'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { getGeolocation } from '@/features/geocoding'; -import { useDummyUsers } from '@/features/shared'; +import { useRecommendationMate } from '@/features/recommendation'; const styles = { container: styled.div` @@ -94,11 +94,11 @@ export function MainPage() { const { data: userData } = useUserData(auth?.accessToken !== undefined); - // const { data: recommendationMates } = useRecommendationMate({ - // memberId: auth?.user?.memberId ?? 'undefined', - // cardType: 'mate', - // enabled: auth?.accessToken != null, - // }); + const { data: recommendationMates } = useRecommendationMate({ + memberId: auth?.user?.memberId ?? 'undefined', + cardType: 'mate', + enabled: auth?.accessToken != null, + }); const [map, setMap] = useState(null); @@ -147,8 +147,6 @@ export function MainPage() { } }, [userData, router, setAuthUserData]); - const users = useDummyUsers(); - return ( @@ -170,26 +168,14 @@ export function MainPage() { onClick={handleScrollLeft} /> - {/* {recommendationMates?.map(({ name, similarity, userId }) => ( + {recommendationMates?.map(({ name, similarity, userId }) => ( - ))} */} - {users?.map( - ({ - userId, - data: { - authResponse: { name }, - }, - }) => ( - - - - ), - )} + ))} (null); @@ -159,7 +159,6 @@ export function MobileMainPage() { } }, [userData, router, setAuthUserData]); - const users = useDummyUsers(); return ( @@ -181,26 +180,14 @@ export function MobileMainPage() { onClick={handleScrollLeft} /> - {/* {recommendationMates?.map(({ name, similarity, userId }) => ( - - - - ))} */} - {users?.map( - ({ - userId, - data: { - authResponse: { name }, - }, - }) => ( - - - - ), - )} + {recommendationMates?.map(({ name, similarity, userId }) => ( + + + + ))} (null); + + useEffect(() => { + mutateProfile(); + }, [auth]); + + useEffect(() => { + if (profileData?.data !== undefined) { + const userProfileData = profileData.data.authResponse; + if (userProfileData !== undefined) { + const { name, birthYear, gender } = userProfileData; + setUserData({ name, gender, birthYear }); + } + } + }, [profileData]); + + const card = useUserCard(cardId); + + const [locationInput, setLocation] = useState( + card.data?.data.location, + ); + + useEffect(() => { + if (card.data?.data.location !== undefined) { + setLocation(card.data.data.location); + } + }, [card.data?.data.location]); + + const [features, setFeatures] = useState<{ + smoking?: string; + roomSharingOption?: string; + mateAge?: number; + options?: Set; + }>({ options: new Set() }); + + const [initialMbti, setInitialMbti] = useState(''); + const [initialMajor, setInitialMajor] = useState(''); + const [initialBudget, setInitialBudget] = useState(''); + + const majorArray = ['공학', '교육', '인문', '사회', '자연', '예체능', '의약']; + + const [mbti, setMbti] = useState(''); + const [major, setMajor] = useState(''); + const [budget, setBudget] = useState(''); + const [mateAge, setMateAge] = useState(0); + + useEffect(() => { + if (isMySelf) { + if (card != null) { + const featuresData = card.data?.data.myFeatures; + if (featuresData != null) { + const options = JSON.parse(featuresData.options); + const optionsSet = new Set(); + options.forEach((option: string) => { + if ( + !option.includes('E') && + !option.includes('I') && + !option.includes(',') && + !majorArray.includes(option) + ) { + optionsSet.add(option); + } + if (option.includes('E') || option.includes('I')) + setInitialMbti(option); + if (option.includes(',')) { + setInitialBudget(option); + } + if (majorArray.includes(option)) setInitialMajor(option); + }); + const data = { + smoking: featuresData.smoking, + roomSharingOption: featuresData.roomSharingOption, + mateAge: featuresData.mateAge, + options: optionsSet, + }; + setMateAge(featuresData.mateAge); + setFeatures(data); + } + } + } + }, [isMySelf, card.data?.data.myFeatures]); + + const handleEssentialFeatureChange = useCallback( + ( + key: 'smoking' | 'roomSharingOption' | 'mateAge', + value: string | number, + ) => { + setFeatures(prev => { + if (prev?.[key] === value) { + return { ...prev, [key]: undefined }; + } + return { ...prev, [key]: value }; + }); + }, + [], + ); + + const handleOptionalFeatureChange = useCallback((option: string) => { + setFeatures(prev => { + const { options } = prev; + const newOptions = new Set(options); + + if (options != null && options.has(option)) { + newOptions.delete(option); + } else newOptions.add(option); + + return { ...prev, options: newOptions }; + }); + }, []); + + const { mutate } = usePutUserCard(cardId); + const router = useRouter(); + + const saveData = () => { + const location = locationInput ?? ''; + const options: string[] = [mbti ?? '', major ?? '', budget ?? '']; + features?.options?.forEach(option => options.push(option)); + + const myFeatures = { + smoking: features?.smoking ?? '상관없어요', + roomSharingOption: features?.roomSharingOption ?? '상관없어요', + mateAge: mateAge ?? 0, + options: JSON.stringify(options.filter(value => value !== '')), + }; + + mutate({ location, features: myFeatures }); + }; + + const handleBeforeUnload = () => { + saveData(); + }; + + const isClickedFirst = useRef(false); + + const handlePopState = () => { + saveData(); + history.back(); + }; + + useEffect(() => { + const originalPush = router.push.bind(router); + const newPush = ( + href: string, + options?: NavigateOptions | undefined, + ): void => { + saveData(); + originalPush(href, options); + }; + router.push = newPush; + window.onbeforeunload = handleBeforeUnload; + return () => { + router.push = originalPush; + window.onbeforeunload = null; + }; + }, [router, handleBeforeUnload]); + + useEffect(() => { + if (!isClickedFirst.current) { + history.pushState(null, '', ''); + isClickedFirst.current = true; + } + }, []); + + useEffect(() => { + window.addEventListener('popstate', handlePopState); + return () => { + window.removeEventListener('popstate', handlePopState); + }; + }, [handlePopState]); + + return ( + + + {type === 'myCard' ? `마이 카드` : '메이트 카드'} + + + {type === 'myCard' ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index a314707b85..2c9f3c285f 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -17,7 +17,8 @@ import { type SharedPostsType, } from '@/entities/shared-posts-filter'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; -import { useDummyUsers, usePaging, useSharedPosts } from '@/features/shared'; +import { useRecommendationMate } from '@/features/recommendation'; +import { usePaging, useSharedPosts } from '@/features/shared'; import { type GetSharedPostsDTO } from '@/features/shared/'; const styles = { @@ -119,7 +120,7 @@ export function SharedPostsPage() { useState(null); const { setAuthUserData } = useAuthActions(); - const { derivedFilter, reset: resetFilter } = useSharedPostsFilter(); + const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); const { @@ -142,11 +143,11 @@ export function SharedPostsPage() { page: page - 1, }); - // const { data: recommendationMates } = useRecommendationMate({ - // memberId: auth?.user?.memberId ?? 'undefined', - // cardType: filter.cardType ?? 'mate', - // enabled: auth?.accessToken != null && selected === 'homeless', - // }); + const { data: recommendationMates } = useRecommendationMate({ + memberId: auth?.user?.memberId ?? 'undefined', + cardType: filter.cardType ?? 'mate', + enabled: auth?.accessToken != null && selected === 'homeless', + }); useEffect(() => { resetFilter(); @@ -171,8 +172,6 @@ export function SharedPostsPage() { } }, [userData, router, setAuthUserData]); - const users = useDummyUsers(); - return ( @@ -255,23 +254,11 @@ export function SharedPostsPage() { ) : ( - {/* {recommendationMates?.map(({ userId, name, similarity }) => ( + {recommendationMates?.map(({ userId, name, similarity }) => ( - ))} */} - {users?.map( - ({ - userId, - data: { - authResponse: { name }, - }, - }) => ( - - - - ), - )} + ))} )} diff --git a/src/app/profile/card/[cardId]/page.tsx b/src/app/profile/card/[cardId]/page.tsx index 91fa208337..53cef69e8d 100644 --- a/src/app/profile/card/[cardId]/page.tsx +++ b/src/app/profile/card/[cardId]/page.tsx @@ -1,9 +1,22 @@ +'use client'; + import { SettingPage } from '@/app/pages'; +import { MobileSettingPage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page({ params: { cardId }, }: { params: { cardId: number }; }) { - return ; + const isMobile = useIsMobile(); + return ( + <> + {isMobile ? ( + + ) : ( + + )} + + ); } diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index 87269bdeba..e64d0e496e 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -18,6 +18,7 @@ const styles = { @media (max-width: 768px) { border-radius: 0 0 30px 30px; + width: 100%; } `, horizontalLine: styled.div` diff --git a/src/components/UserSearchBox.tsx b/src/components/UserSearchBox.tsx index a864e3dc99..530d84d2b5 100644 --- a/src/components/UserSearchBox.tsx +++ b/src/components/UserSearchBox.tsx @@ -15,6 +15,7 @@ export function UserSearchBox() { const { mutate: search, data: searchUser } = useSearchUser(email); const { createToast } = useToast(); + const { mutate: mutateSearchUserProfile, data: searchUserProfile, diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 6a0621b3ab..9b8bd87fc4 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -20,8 +20,6 @@ import { type SelectedExtraOptions, type SelectedOptions, } from './shared.type'; -import { postUserProfile } from '../profile/profile.api'; -import { type PostUserProfileDTO } from '../profile/profile.dto'; import { useAuthValue } from '@/features/auth'; import { type NaverAddress } from '@/features/geocoding'; @@ -437,56 +435,3 @@ export const useScrapDormitorySharedPost = () => useMutation, FailureDTO, number>({ mutationFn: scrapDormitoryPost, }); - -const userIds = [ - 'naver_0', - 'kakao_1', - 'kakao_2', - 'naver_3', - 'kakao_4', - 'naver_5', - 'kakao_6', - 'kakao_7', - 'kakao_8', - 'naver_9', - 'naver_10', - 'naver_11', - 'naver_12', - 'naver_13', - 'kakao_14', - 'naver_15', - 'kakao_16', - 'naver_17', - 'naver_18', - 'kakao_19', -]; - -export const useDummyUsers = () => { - const [users, setUsers] = - useState>(); - - useEffect(() => { - (async () => { - const userData = await Promise.allSettled( - userIds.map(async userId => { - const result = await postUserProfile(userId); - return { ...result, userId }; - }), - ); - - setUsers( - userData.reduce>( - (prev, curr) => { - if (curr.status === 'fulfilled') { - prev.push({ ...curr.value, userId: curr.value.userId }); - } - return prev; - }, - [], - ), - ); - })(); - }, []); - - return users; -}; From eb8f823ffe5cf78b60a451700e85458cf6189748 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Tue, 14 May 2024 18:56:45 +0900 Subject: [PATCH 105/130] feat: shared-posts-page (#79) --- src/app/pages/mobile/index.ts | 1 + src/app/pages/mobile/mobile-landing-page.tsx | 2 +- .../pages/mobile/mobile-shared-posts-page.tsx | 287 ++++++++++++++++++ src/app/shared/page.tsx | 9 +- src/components/shared-posts/PostCard.tsx | 80 ++++- .../shared-posts/SharedPostFilterItem.tsx | 21 +- .../shared-posts/SharedPostFilters.tsx | 6 + .../shared-posts/SharedPostsMenu.tsx | 13 + 8 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 src/app/pages/mobile/mobile-shared-posts-page.tsx diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts index 5bb46c5f00..dc190847db 100644 --- a/src/app/pages/mobile/index.ts +++ b/src/app/pages/mobile/index.ts @@ -3,3 +3,4 @@ export * from './mobile-main-page'; export * from './mobile-profile-page'; export * from './mobile-user-input-page'; export * from './mobile-setting-page'; +export * from './mobile-shared-posts-page'; diff --git a/src/app/pages/mobile/mobile-landing-page.tsx b/src/app/pages/mobile/mobile-landing-page.tsx index 527b563f57..29b7f0294e 100644 --- a/src/app/pages/mobile/mobile-landing-page.tsx +++ b/src/app/pages/mobile/mobile-landing-page.tsx @@ -23,7 +23,7 @@ const styles = { flex-direction: column; align-items: center; gap: 1.75rem; - width: 27.8125rem; + width: 100%; flex-shrink: 0; h1 { diff --git a/src/app/pages/mobile/mobile-shared-posts-page.tsx b/src/app/pages/mobile/mobile-shared-posts-page.tsx new file mode 100644 index 0000000000..2d931740ae --- /dev/null +++ b/src/app/pages/mobile/mobile-shared-posts-page.tsx @@ -0,0 +1,287 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { CircularButton } from '@/components'; +import { UserCard } from '@/components/main-page'; +import { + PostCard, + SharedPostFilters, + SharedPostsMenu, +} from '@/components/shared-posts'; +import { + useSharedPostsFilter, + type SharedPostsType, +} from '@/entities/shared-posts-filter'; +import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; +import { useRecommendationMate } from '@/features/recommendation'; +import { usePaging, useSharedPosts } from '@/features/shared'; +import { type GetSharedPostsDTO } from '@/features/shared/'; + +const styles = { + container: styled.div` + display: flex; + align-items: center; + width: 100vw; + min-width: 390px; + flex-direction: column; + gap: 1rem; + padding: 2rem 1rem; + `, + SharedPostsMenu: styled(SharedPostsMenu)` + margin-bottom: 1rem; + `, + createButtonRow: styled.div` + display: flex; + width: 100%; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; + + @media (max-width: 768px) { + align-items: flex-start; + } + `, + createButton: styled.button` + all: unset; + cursor: pointer; + + display: flex; + width: 7.125rem; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + gap: 0.25rem; + + border-radius: 8px; + background: var(--Black, #35373a); + + color: #fff; + font-family: Pretendard; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + + @media (max-width: 768px) { + font-size: 0.75rem; + width: 4rem; + padding: 0.25rem 1rem; + } + `, + posts: styled.div` + display: flex; + flex-direction: column; + padding-inline: 1rem; + gap: 1rem; + + @media (max-width: 768px) { + width: 100%; + } + `, + CircularButton: styled(CircularButton)` + scale: 0.9; + `, + pagingRow: styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-block: 7rem; + `, + paging: styled.div` + display: flex; + gap: 3rem; + + button { + all: unset; + cursor: pointer; + + color: #b2b2b2; + font-family: 'Spoqa Han Sans Neo'; + font-size: 1.25rem; + font-style: normal; + font-weight: 400; + line-height: 120%; + + &[class~='current'] { + color: var(--Black, #35373a); + font-family: 'Spoqa Han Sans Neo'; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: 120%; + text-decoration-line: underline; + } + } + `, + cards: styled.div` + padding-left: 2.62rem; + display: flex; + flex-wrap: wrap; + gap: 2rem 2.62rem; + + @media (max-width: 768px) { + gap: 1rem 1rem; + padding-left: 0; + } + `, +}; + +export function MobileSharedPostsPage() { + const router = useRouter(); + + const auth = useAuthValue(); + const [selected, setSelected] = useState('hasRoom'); + const [totalPageCount, setTotalPageCount] = useState(0); + const [prevSharedPosts, setPrevSharedPosts] = + useState(null); + const { setAuthUserData } = useAuthActions(); + + const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); + const { data: userData } = useUserData(auth?.accessToken != null); + + const { + page, + sliceSize, + currentSlice, + isFirstPage, + isLastPage, + handleSetPage, + handleNextPage, + handlePrevPage, + } = usePaging({ + totalPages: totalPageCount, + sliceSize: 10, + }); + + const { data: sharedPosts } = useSharedPosts({ + filter: derivedFilter, + enabled: auth?.accessToken != null && selected === 'hasRoom', + page: page - 1, + }); + + const { data: recommendationMates } = useRecommendationMate({ + memberId: auth?.user?.memberId ?? 'undefined', + cardType: filter.cardType ?? 'mate', + enabled: auth?.accessToken != null && selected === 'homeless', + }); + + useEffect(() => { + resetFilter(); + return () => { + resetFilter(); + }; + }, [resetFilter]); + + useEffect(() => { + if (sharedPosts != null) { + setTotalPageCount(sharedPosts.data.totalPages); + setPrevSharedPosts(null); + } + }, [sharedPosts]); + + useEffect(() => { + if (userData != null) { + setAuthUserData(userData); + if (userData.initialized) { + // router.replace('/profile'); + } + } + }, [userData, router, setAuthUserData]); + + return ( + + + + + {selected === 'hasRoom' && ( + + 작성하기 + + )} + + {selected === 'hasRoom' ? ( + <> + + {prevSharedPosts != null + ? prevSharedPosts.data.content.map(post => ( + + + + )) + : sharedPosts?.data.content.map(post => ( + + + + ))} + + {sharedPosts?.data.content.length !== 0 && ( + + { + if (sharedPosts != null) { + setPrevSharedPosts(sharedPosts); + } + handlePrevPage(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + /> + + {Array.from({ + length: Math.min( + totalPageCount - currentSlice * sliceSize, + sliceSize, + ), + }).map((_, index) => ( + + ))} + + { + if (sharedPosts != null) { + setPrevSharedPosts(sharedPosts); + } + handleNextPage(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + /> + + )} + + ) : ( + + {recommendationMates?.map(({ userId, name, similarity }) => ( + + + + ))} + + )} + + ); +} diff --git a/src/app/shared/page.tsx b/src/app/shared/page.tsx index b301df315a..d0317111ea 100644 --- a/src/app/shared/page.tsx +++ b/src/app/shared/page.tsx @@ -1,5 +1,10 @@ -import { SharedPostsPage } from '../pages'; +'use client'; + +import { SharedPostsPage } from '@/app/pages'; +import { MobileSharedPostsPage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page() { - return ; + const isMobile = useIsMobile(); + return <>{isMobile ? : }; } diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index 548ae8214b..1b3769d34e 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { HorizontalDivider } from '..'; import { type SharedPostListItem } from '@/entities/shared-post'; +import { useIsMobile } from '@/shared/mobile'; const styles = { container: styled.div` @@ -13,6 +14,10 @@ const styles = { display: flex; gap: 1.56rem; + + @media (max-width: 768px) { + height: 11rem; + } `, thumbnail: styled.img` width: 16.125rem; @@ -22,6 +27,11 @@ const styles = { border-radius: 16px; object-fit: cover; + + @media (max-width: 768px) { + width: 8.5625rem; + height: 8.625rem; + } `, content: styled.div` flex-grow: 1; @@ -42,6 +52,10 @@ const styles = { font-style: normal; font-weight: 700; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.875rem; + } } h2 { @@ -51,6 +65,10 @@ const styles = { font-style: normal; font-weight: 400; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.75rem; + } } p { @@ -60,6 +78,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.75rem; + } } } `, @@ -83,6 +105,11 @@ const styles = { border-radius: 50%; object-fit: cover; + + @media (max-width: 768px) { + width: 3.375rem; + height: 3.375rem; + } } p { @@ -116,11 +143,16 @@ const styles = { font-style: normal; font-weight: 600; line-height: 1.5rem; + + @media (max-width) { + font-size: 0.625rem; + } } `, }; export function PostCard({ post }: { post: SharedPostListItem }) { + const isMobile = useIsMobile(); return (
@@ -130,22 +162,42 @@ export function PostCard({ post }: { post: SharedPostListItem }) {

{post.title}

{post.address.roadAddress}

-
-

모집 {post.roomInfo.recruitmentCapacity}명

-

- {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · - 화장실 {post.roomInfo.numberOfBathRoom} -

-

희망 월 분담금 {post.roomInfo.expectedPayment}

+
+
+

모집 {post.roomInfo.recruitmentCapacity}명

+

+ {post.roomInfo.roomType} · 방 {post.roomInfo.numberOfRoom} · + 화장실 {post.roomInfo.numberOfBathRoom} +

+

희망 월 분담금 {post.roomInfo.expectedPayment}

+
+ {isMobile ? ( + + + +

50%

+
+
+ ) : null}
- - - -

50%

-
-

{post.publisherAccount.nickname}

-
+ {!isMobile ? ( + + + +

50%

+
+

{post.publisherAccount.nickname}

+
+ ) : null}
diff --git a/src/components/shared-posts/SharedPostFilterItem.tsx b/src/components/shared-posts/SharedPostFilterItem.tsx index 8af65cbc37..f7be157569 100644 --- a/src/components/shared-posts/SharedPostFilterItem.tsx +++ b/src/components/shared-posts/SharedPostFilterItem.tsx @@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import { useIsMobile } from '@/shared/mobile'; + const styles = { container: styled.div` -webkit-user-select: none; @@ -22,6 +24,10 @@ const styles = { border: 2px solid var(--Gray-4, #dfdfdf); cursor: pointer; + + @media (max-width: 768px) { + padding: 0.25rem 1rem; + } `, title: styled.div` display: flex; @@ -34,6 +40,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 0.75rem; + } `, content: styled.div<{ $hidden: boolean }>` visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')}; @@ -83,11 +93,20 @@ export function SharedPostFilterItem({ setHidden(!hidden); }; + const isMobile = useIsMobile(); + return ( {title} - drop-down-button + drop-down-button Date: Tue, 14 May 2024 20:45:16 +0900 Subject: [PATCH 106/130] feat: enhancement shared API (#75) --- src/app/pages/shared-post-page.tsx | 178 ++++++----- src/app/pages/shared-posts-page.tsx | 16 +- src/app/pages/writing-post-page.tsx | 200 +++++++----- src/app/shared/dormitory/[postId]/page.tsx | 9 + src/app/shared/{ => room}/[postId]/page.tsx | 2 +- .../writing/dormitory/[postId]/page.tsx | 9 + src/app/shared/writing/dormitory/page.tsx | 5 + src/app/shared/writing/room/[postId]/page.tsx | 9 + src/app/shared/writing/{ => room}/page.tsx | 2 +- src/components/RangeSlider.tsx | 8 +- src/components/chat/ChattingRoom.tsx | 2 +- src/components/shared-post-page/ImageGrid.tsx | 2 +- .../shared-posts/SharedPostsMenu.tsx | 16 +- .../shared-posts/filter/RoomTypeFilter.tsx | 19 +- src/entities/shared-post/shared-post.type.ts | 24 +- .../shared-posts-filter.atom.ts | 4 +- .../shared-posts-filter.type.ts | 2 +- src/features/shared/shared.api.ts | 8 +- src/features/shared/shared.atom.ts | 19 +- src/features/shared/shared.hook.ts | 301 +++++++++++------- 20 files changed, 512 insertions(+), 323 deletions(-) create mode 100644 src/app/shared/dormitory/[postId]/page.tsx rename src/app/shared/{ => room}/[postId]/page.tsx (69%) create mode 100644 src/app/shared/writing/dormitory/[postId]/page.tsx create mode 100644 src/app/shared/writing/dormitory/page.tsx create mode 100644 src/app/shared/writing/room/[postId]/page.tsx rename src/app/shared/writing/{ => room}/page.tsx (64%) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index 31384465db..15eff7f737 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { Bookmark, CircularProfileImage } from '@/components'; @@ -16,9 +16,9 @@ import { } from '@/features/profile'; import { useDeleteSharedPost, + useDormitorySharedPost, useScrapSharedPost, useSharedPost, - useSharedPostProps, } from '@/features/shared'; import { useToast } from '@/features/toast'; import { getAge } from '@/shared'; @@ -432,7 +432,13 @@ const styles = { `, }; -export function SharedPostPage({ postId }: { postId: number }) { +export function SharedPostPage({ + postId, + type, +}: { + postId: number; + type: 'hasRoom' | 'dormitory'; +}) { const auth = useAuthValue(); const [, setMap] = useState(null); @@ -440,7 +446,6 @@ export function SharedPostPage({ postId }: { postId: number }) { const router = useRouter(); const { createToast } = useToast(); - const { setStateWithPost } = useSharedPostProps(); const { mutate: deleteSharedPost } = useDeleteSharedPost(); @@ -452,11 +457,17 @@ export function SharedPostPage({ postId }: { postId: number }) { | undefined >(undefined); - const { isLoading, data: sharedPost } = useSharedPost({ + const { isLoading: isSharedPostLoading, data: sharedPost } = useSharedPost({ postId, - enabled: auth?.accessToken !== undefined, + enabled: type === 'hasRoom' && auth?.accessToken != null, }); + const { isLoading: isDormitorySharedPostLoading, data: dormitorySharedPost } = + useDormitorySharedPost({ + postId, + enabled: type === 'dormitory' && auth?.accessToken != null, + }); + const { data: userData } = useUserData(auth?.accessToken != null); const [userId, setUserId] = useState(''); @@ -513,21 +524,32 @@ export function SharedPostPage({ postId }: { postId: number }) { const members = [userId]; const { mutate: chattingMutate } = useCreateChatRoom(roomName, members); - if (isLoading || sharedPost == null) return <>; + const isLoading = useMemo( + () => + type === 'hasRoom' ? isSharedPostLoading : isDormitorySharedPostLoading, + [type, isSharedPostLoading, isDormitorySharedPostLoading], + ); + + const post = useMemo( + () => (type === 'hasRoom' ? sharedPost : dormitorySharedPost), + [type, sharedPost, dormitorySharedPost], + ); + + if (isLoading || post == null) return <>; return ( fileName)} + images={post.data.roomImages.map(({ fileName }) => fileName)} />
-

{sharedPost.data.title}

+

{post.data.title}

{ scrapPost(postId); }} @@ -535,76 +557,83 @@ export function SharedPostPage({ postId }: { postId: number }) { />
- 모집 {sharedPost.data.roomInfo.recruitmentCapacity}명 - - {sharedPost.data.roomInfo.roomType} · 방{' '} - {sharedPost.data.roomInfo.numberOfRoom} · 화장실{' '} - {sharedPost.data.roomInfo.numberOfBathRoom} - + {'roomInfo' in post.data && ( + <> + 모집 {post.data.roomInfo.recruitmentCapacity}명 + + {post.data.roomInfo.roomType} · 방{' '} + {post.data.roomInfo.numberOfRoom} · 화장실{' '} + {post.data.roomInfo.numberOfBathRoom} + + + )}
+ {'roomInfo' in post.data && ( + + 희망 월 분담금 {post.data.roomInfo.expectedPayment}만원 + + )} - 희망 월 분담금 {sharedPost.data.roomInfo.expectedPayment}만원 - - - 저장 {sharedPost.data.scrapCount} · 조회{' '} - {sharedPost.data.viewCount} + 저장 {post.data.scrapCount} · 조회 {post.data.viewCount}

상세 정보

-

{sharedPost.data.content}

+

{post.data.content}

- -

거래 정보

-
- 거래 방식 - {sharedPost.data.roomInfo.rentalType} -
-
- 희망 월 분담금 - {sharedPost.data.roomInfo.expectedPayment}만원 -
-
- -

방 정보

-
- 방 종류 - {sharedPost.data.roomInfo.roomType} -
-
- 거실 보유 - - {sharedPost.data.roomInfo.hasLivingRoom ? '유' : '무'} - -
-
- 방 개수 - {sharedPost.data.roomInfo.numberOfRoom}개 -
-
- 화장실 개수 - {sharedPost.data.roomInfo.numberOfBathRoom}개 -
-
- 평수 - {sharedPost.data.roomInfo.size}평 -
-
+ {'roomInfo' in post.data && ( + <> + +

거래 정보

+
+ 거래 방식 + {post.data.roomInfo.rentalType} +
+
+ 희망 월 분담금 + {post.data.roomInfo.expectedPayment}만원 +
+
+ +

방 정보

+
+ 방 종류 + {post.data.roomInfo.roomType} +
+
+ 거실 보유 + {post.data.roomInfo.hasLivingRoom ? '유' : '무'} +
+
+ 방 개수 + {post.data.roomInfo.numberOfRoom}개 +
+
+ 화장실 개수 + {post.data.roomInfo.numberOfBathRoom}개 +
+
+ 평수 + {post.data.roomInfo.size}평 +
+
+ + )}

위치 정보

-

{sharedPost.data.address.roadAddress}

+

{post.data.address.roadAddress}

- {sharedPost.data.publisherAccount.memberId === - auth?.user?.memberId && ( + {post.data.publisherAccount.memberId === auth?.user?.memberId && ( { - setStateWithPost(sharedPost); - router.push('/shared/writing'); + router.push( + `/shared/writing/${type === 'hasRoom' ? 'room' : 'dormitory'}/${post.data.id}`, + ); }} > 수정하기 @@ -639,15 +668,18 @@ export function SharedPostPage({ postId }: { postId: number }) { - {sharedPost.data.participants.map( - ({ memberId, profileImage }, index) => ( + {post.data.participants.map( + ({ memberId, profileImageFileName }, index) => ( { - setSelected({ memberId, profileImage }); + setSelected({ + memberId, + profileImage: profileImageFileName, + }); }} /> ), @@ -658,19 +690,17 @@ export function SharedPostPage({ postId }: { postId: number }) { -

- {sharedPost.data.publisherAccount.nickname} -

+

{post.data.publisherAccount.nickname}

- {sharedPost.data.publisherAccount.birthYear != null - ? getAge(+sharedPost.data.publisherAccount.birthYear) + {post.data.publisherAccount.birthYear != null + ? getAge(+post.data.publisherAccount.birthYear) : new Date().getFullYear()}

-

{sharedPost.data.address.roadAddress}

+

{post.data.address.roadAddress}

diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index d38dc2f019..f5f547ab4e 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -21,7 +21,6 @@ import { useRecommendationMate } from '@/features/recommendation'; import { useDormitorySharedPosts, usePaging, - useSharedPostProps, useSharedPosts, type GetDormitorySharedPostsDTO, type GetSharedPostsDTO, @@ -127,9 +126,6 @@ export function SharedPostsPage() { >(null); const { setAuthUserData } = useAuthActions(); - const { setSharedPostProps, reset: resetSharedPostProps } = - useSharedPostProps(); - const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); const { data: userData } = useUserData(auth?.accessToken != null); @@ -204,9 +200,9 @@ export function SharedPostsPage() { {(selected === 'hasRoom' || selected === 'dormitory') && ( { - resetSharedPostProps(); - setSharedPostProps(prev => ({ ...prev, type: selected })); - router.push('/shared/writing'); + router.push( + `/shared/writing/${selected === 'hasRoom' ? 'room' : 'dormitory'}`, + ); }} > 작성하기 @@ -223,7 +219,6 @@ export function SharedPostsPage() { post={post} onClick={() => { router.push(`/shared/${post.id}`); - setSharedPostProps(prev => ({ ...prev, type: selected })); }} /> )) @@ -232,8 +227,9 @@ export function SharedPostsPage() { key={post.id} post={post} onClick={() => { - router.push(`/shared/${post.id}`); - setSharedPostProps(prev => ({ ...prev, type: selected })); + router.push( + `/shared/${selected === 'hasRoom' ? 'room' : 'dormitory'}/${post.id}`, + ); }} /> ))} diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index 6e86647605..81645fb392 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -25,7 +25,6 @@ import { getImageURL, putImage } from '@/features/image'; import { useCreateDormitorySharedPost, useCreateSharedPost, - usePostMateCardInputSection, useSharedPostProps, useUpdateDormitorySharedPost, useUpdateSharedPost, @@ -415,7 +414,13 @@ interface ButtonActiveProps { $isSelected: boolean; } -export function WritingPostPage() { +export function WritingPostPage({ + postId, + type, +}: { + postId?: number; + type: 'hasRoom' | 'dormitory'; +}) { const router = useRouter(); const imageInputRef = useRef(null); @@ -425,38 +430,25 @@ export function WritingPostPage() { useState(false); const { - type, - postId, title, content, images, mateLimit, houseSize, address, + mateCard, selectedOptions, selectedExtraOptions, expectedMonthlyFee, + derivedMateCardFeatures, setSharedPostProps, handleOptionClick, handleExtraOptionClick, + handleMateCardOptionalFeatureChange, + handleMateCardEssentialFeatureChange, isOptionSelected, isExtraOptionSelected, - } = useSharedPostProps(); - - const { - gender, - birthYear, - mbti, - major, - budget, - derivedFeatures, - setBirthYear, - setMbti, - setMajor, - setBudget, - handleEssentialFeatureChange, - handleOptionalFeatureChange, - } = usePostMateCardInputSection(); + } = useSharedPostProps({ postId, type }); const { mutate: createSharedPost } = useCreateSharedPost(); const { mutate: updateSharedPost } = useUpdateSharedPost(); @@ -533,24 +525,31 @@ export function WritingPostPage() { }; const handleCreatePost = (event: React.MouseEvent) => { - // if (!isPostCreatable || !isMateCardCreatable) return; + const extractFileName = (url: string): string => { + const regex = + /\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.\w+)/; + const match = url.match(regex); + return match != null ? match[1] : ''; + }; const dealType = selectedOptions.budget; const { roomType } = selectedOptions; const { floorType } = selectedOptions; if ( - dealType == null || - roomType == null || - floorType == null || - address == null || - selectedOptions.roomCount == null || - !(selectedOptions.roomCount in CountTypeValue) || - selectedOptions.restRoomCount == null || - !(selectedOptions.restRoomCount in CountTypeValue) + type === 'hasRoom' && + (dealType == null || + roomType == null || + floorType == null || + selectedOptions.roomCount == null || + !(selectedOptions.roomCount in CountTypeValue) || + selectedOptions.restRoomCount == null || + !(selectedOptions.restRoomCount in CountTypeValue)) ) return; + if (address == null || images.length < 2) return; + const numberOfRoomOption = selectedOptions.roomCount as | '1개' | '2개' @@ -595,10 +594,10 @@ export function WritingPostPage() { const putResults = await Promise.allSettled( urls.map(async ({ url, fileName, file, uploaded }) => { - if (uploaded) return { fileName: url }; + if (uploaded) return { uploaded, fileName: url }; if (file != null) await putImage(url, file); - return { fileName }; + return { uploaded, fileName }; }), ); @@ -607,8 +606,11 @@ export function WritingPostPage() { >((prev, result) => { if (result.status === 'rejected' || result.value.fileName == null) return prev; + const { uploaded, fileName } = result.value; return prev.concat({ - fileName: result.value.fileName, + fileName: uploaded + ? `images/${extractFileName(fileName)}` + : fileName, isThumbNail: prev.length === 0, order: prev.length + 1, }); @@ -643,12 +645,12 @@ export function WritingPostPage() { }, }, locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, + oldAddress: address.jibunAddress, + roadAddress: address.roadAddress, }, roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, + location: address.roadAddress, + features: derivedMateCardFeatures, }, participationData: { recruitmentCapacity: mateLimit, @@ -706,12 +708,12 @@ export function WritingPostPage() { }, }, locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, + oldAddress: address.jibunAddress, + roadAddress: address.roadAddress, }, roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, + location: address.roadAddress, + features: derivedMateCardFeatures, }, participationData: { recruitmentCapacity: mateLimit, @@ -743,36 +745,17 @@ export function WritingPostPage() { } } else if (type === 'dormitory') { if (postId == null) { - createDormitorySharedPost({ - imageFilesData: uploadedImages, - postData: { title, content }, - locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, - }, - roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, - }, - participationData: { - recruitmentCapacity: mateLimit, - participationMemberIds: - auth?.user != null ? [auth.user.memberId] : [], - }, - }); - } else if (postId != null) { - updateDormitorySharedPost({ - postId, - postData: { + createDormitorySharedPost( + { imageFilesData: uploadedImages, postData: { title, content }, locationData: { - oldAddress: address?.jibunAddress, - roadAddress: address?.roadAddress, + oldAddress: address.jibunAddress, + roadAddress: address.roadAddress, }, roomMateCardData: { - location: address?.roadAddress, - features: derivedFeatures, + location: address.roadAddress, + features: derivedMateCardFeatures, }, participationData: { recruitmentCapacity: mateLimit, @@ -780,7 +763,68 @@ export function WritingPostPage() { auth?.user != null ? [auth.user.memberId] : [], }, }, - }); + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); + } else if (postId != null) { + updateDormitorySharedPost( + { + postId, + postData: { + imageFilesData: uploadedImages, + postData: { title, content }, + locationData: { + oldAddress: address.jibunAddress, + roadAddress: address.roadAddress, + }, + roomMateCardData: { + location: address.roadAddress, + features: derivedMateCardFeatures, + }, + participationData: { + recruitmentCapacity: mateLimit, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, + }, + }, + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 수정되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 수정에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); } } } catch (error) { @@ -942,22 +986,22 @@ export function WritingPostPage() { {showMateCardForm && ( {}} - onMateAgeChange={setBirthYear} - onMbtiChange={setMbti} - onMajorChange={setMajor} - onBudgetChange={setBudget} + onMateAgeChange={() => {}} + onMbtiChange={() => {}} + onMajorChange={() => {}} + onBudgetChange={() => {}} /> )} diff --git a/src/app/shared/dormitory/[postId]/page.tsx b/src/app/shared/dormitory/[postId]/page.tsx new file mode 100644 index 0000000000..f3c65f83df --- /dev/null +++ b/src/app/shared/dormitory/[postId]/page.tsx @@ -0,0 +1,9 @@ +import { SharedPostPage } from '@/app/pages'; + +export default function Page({ + params: { postId }, +}: { + params: { postId: string }; +}) { + return ; +} diff --git a/src/app/shared/[postId]/page.tsx b/src/app/shared/room/[postId]/page.tsx similarity index 69% rename from src/app/shared/[postId]/page.tsx rename to src/app/shared/room/[postId]/page.tsx index 5c1f9dc90b..0997478567 100644 --- a/src/app/shared/[postId]/page.tsx +++ b/src/app/shared/room/[postId]/page.tsx @@ -5,5 +5,5 @@ export default function Page({ }: { params: { postId: string }; }) { - return ; + return ; } diff --git a/src/app/shared/writing/dormitory/[postId]/page.tsx b/src/app/shared/writing/dormitory/[postId]/page.tsx new file mode 100644 index 0000000000..0227db26b3 --- /dev/null +++ b/src/app/shared/writing/dormitory/[postId]/page.tsx @@ -0,0 +1,9 @@ +import { WritingPostPage } from '@/app/pages'; + +export default function Page({ + params: { postId }, +}: { + params: { postId: string }; +}) { + return ; +} diff --git a/src/app/shared/writing/dormitory/page.tsx b/src/app/shared/writing/dormitory/page.tsx new file mode 100644 index 0000000000..63c8058a56 --- /dev/null +++ b/src/app/shared/writing/dormitory/page.tsx @@ -0,0 +1,5 @@ +import { WritingPostPage } from '@/app/pages'; + +export default function Page() { + return ; +} diff --git a/src/app/shared/writing/room/[postId]/page.tsx b/src/app/shared/writing/room/[postId]/page.tsx new file mode 100644 index 0000000000..e40d73887d --- /dev/null +++ b/src/app/shared/writing/room/[postId]/page.tsx @@ -0,0 +1,9 @@ +import { WritingPostPage } from '@/app/pages'; + +export default function Page({ + params: { postId }, +}: { + params: { postId: string }; +}) { + return ; +} diff --git a/src/app/shared/writing/page.tsx b/src/app/shared/writing/room/page.tsx similarity index 64% rename from src/app/shared/writing/page.tsx rename to src/app/shared/writing/room/page.tsx index 84ba7e9f1e..2f8cf87c76 100644 --- a/src/app/shared/writing/page.tsx +++ b/src/app/shared/writing/room/page.tsx @@ -1,5 +1,5 @@ import { WritingPostPage } from '@/app/pages'; export default function Page() { - return ; + return ; } diff --git a/src/components/RangeSlider.tsx b/src/components/RangeSlider.tsx index a7cff45397..ae5c7982c1 100644 --- a/src/components/RangeSlider.tsx +++ b/src/components/RangeSlider.tsx @@ -107,11 +107,9 @@ export function RangeSlider({ min, max, step, onChange }: Props) { }, [containerRef, state, max, min]); useEffect(() => { - setState({ - low: (min + max - 1) * 0.25, - high: (min + max - 1) * 0.75, - }); - }, []); + // width 값을 초기에 계산하기 위해 추가된 effect. + setState({ low: min, high: max }); + }, [setState, min, max]); return ( diff --git a/src/components/chat/ChattingRoom.tsx b/src/components/chat/ChattingRoom.tsx index acf3d036f4..56e28276c2 100644 --- a/src/components/chat/ChattingRoom.tsx +++ b/src/components/chat/ChattingRoom.tsx @@ -247,7 +247,7 @@ export function ChattingRoom({ stompClient.publish({ destination, body: JSON.stringify({ - roomId: roomId, + roomId, sender: user, message: inputMessage, nickname: userName, diff --git a/src/components/shared-post-page/ImageGrid.tsx b/src/components/shared-post-page/ImageGrid.tsx index aaa100e4c7..116632a19a 100644 --- a/src/components/shared-post-page/ImageGrid.tsx +++ b/src/components/shared-post-page/ImageGrid.tsx @@ -103,7 +103,7 @@ export function ImageGrid({ images: imagesParam, className }: Props) { image !== 'none' ? ( {image} diff --git a/src/components/shared-posts/SharedPostsMenu.tsx b/src/components/shared-posts/SharedPostsMenu.tsx index 34ad5cec80..60471bb2b1 100644 --- a/src/components/shared-posts/SharedPostsMenu.tsx +++ b/src/components/shared-posts/SharedPostsMenu.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { type SharedPostsType } from '@/entities/shared-posts-filter'; +import { useAuthValue, useUserData } from '@/features/auth'; const styles = { container: styled.div` @@ -55,7 +56,8 @@ export function SharedPostsMenu({ handleSelect, className, }: Props & React.ComponentProps<'div'>) { - // const auth = useAuthValue(); + const auth = useAuthValue(); + const { data: user } = useUserData(auth?.accessToken != null); return ( @@ -75,8 +77,16 @@ export function SharedPostsMenu({ > 방 없는 메이트 - {/* {auth?.user?.univCertified === true ?? ( - )} */} + {user?.univCertified ?? ( + { + handleSelect('dormitory'); + }} + className={selected === 'dormitory' ? 'selected' : ''} + > + 기숙사 메이트 + + )} { handleSelect('dormitory'); diff --git a/src/components/shared-posts/filter/RoomTypeFilter.tsx b/src/components/shared-posts/filter/RoomTypeFilter.tsx index 2efa88c5ae..227cf24ab7 100644 --- a/src/components/shared-posts/filter/RoomTypeFilter.tsx +++ b/src/components/shared-posts/filter/RoomTypeFilter.tsx @@ -224,15 +224,18 @@ export function RoomTypeFilter() {

거실 유무

{ - setFilter(prev => ({ - ...prev, - roomInfo: { - ...prev.roomInfo, - hasLivingRoom: !prev.roomInfo.hasLivingRoom, - }, - })); + setFilter(prev => { + const value = prev.roomInfo.hasLivingRoom ?? false; + return { + ...prev, + roomInfo: { + ...prev.roomInfo, + hasLivingRoom: !value, + }, + }; + }); }} />
diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index 523864c9be..be3f35dc2b 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -47,15 +47,12 @@ export interface SharedPost { title: string; content: string; roomMateFeatures: { - location: string; - features: { - smoking: string; - roomSharingOption: string; - mateAge: string; - options: string; // 프런트에서 파싱 필요. - }; + smoking?: string; + roomSharingOption?: string; + mateAge?: string; + options: string; // 프런트에서 파싱 필요. }; - participants: Array<{ memberId: string; profileImage: string }>; + participants: Array<{ memberId: string; profileImageFileName: string }>; roomImages: Array<{ fileName: string; isThumbnail: boolean; @@ -145,13 +142,10 @@ export interface DormitorySharedPost { title: string; content: string; roomMateFeatures: { - location: string; - features: { - smoking: string; - roomSharingOption: string; - mateAge: string; - options: string; - }; + smoking?: string; + roomSharingOption?: string; + mateAge?: string; + options: string; }; participants: Array<{ memberId: string; diff --git a/src/entities/shared-posts-filter/shared-posts-filter.atom.ts b/src/entities/shared-posts-filter/shared-posts-filter.atom.ts index 415767c3eb..ac0849eed5 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.atom.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.atom.ts @@ -5,9 +5,7 @@ import { type SharedPostsFilter } from './shared-posts-filter.type'; export const sharedPostsFilterState = atom({ key: 'sharedPostsFilterState', default: { - roomInfo: { - hasLivingRoom: false, - }, + roomInfo: {}, dealInfo: {}, extraInfo: {}, }, diff --git a/src/entities/shared-posts-filter/shared-posts-filter.type.ts b/src/entities/shared-posts-filter/shared-posts-filter.type.ts index 1ec83e8775..09d1de79c9 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.type.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.type.ts @@ -57,7 +57,7 @@ export interface SharedPostsFilter { cardType?: CardType; roomInfo: { roomType?: Partial>; - hasLivingRoom: boolean; + hasLivingRoom?: boolean; roomCount?: CountType; restRoomCount?: CountType; size?: { low: number; high: number }; diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index 006b750724..8e6d5dd26d 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -6,7 +6,7 @@ import { type GetSharedPostDTO, type GetSharedPostsDTO, } from './shared.dto'; -import { type SharedPostProps, type GetSharedPostsProps } from './shared.type'; +import { type GetSharedPostsProps, type SharedPostProps } from './shared.type'; import { type SuccessBaseDTO } from '@/shared/types'; @@ -19,7 +19,7 @@ export const getSharedPosts = async ({ const baseURL = '/maru-api/shared/posts/studio'; let query = ''; - if (filter != null) { + if (filter != null && Object.keys(filter).length > 0) { query += `filter=${JSON.stringify(filter)}`; } @@ -70,6 +70,10 @@ export const getDormitorySharedPosts = async ({ const baseURL = '/maru-api/shared/posts/dormitory'; let query = ''; + if (filter != null && Object.keys(filter).length > 0) { + query += `filter=${JSON.stringify(filter)}`; + } + if (search != null) { query += `&search=${search}`; } diff --git a/src/features/shared/shared.atom.ts b/src/features/shared/shared.atom.ts index 3956a6579c..60f4d93a72 100644 --- a/src/features/shared/shared.atom.ts +++ b/src/features/shared/shared.atom.ts @@ -9,8 +9,6 @@ import { import { type NaverAddress } from '@/features/geocoding'; export const sharedPostPropState = atom<{ - type: 'hasRoom' | 'dormitory'; - postId?: number; title: string; content: string; images: ImageFile[]; @@ -20,11 +18,23 @@ export const sharedPostPropState = atom<{ houseSize: number; selectedExtraOptions: SelectedExtraOptions; selectedOptions: SelectedOptions; + mateCard: { + gender?: string; + birthYear?: number; + location?: string; + mbti?: string; + major?: string; + budget?: string; + features: { + smoking?: string; + roomSharingOption?: string; + mateAge?: number; + options: Set; + }; + }; }>({ key: 'sharedPostPropState', default: { - type: 'hasRoom', - postId: undefined, title: '', content: '', images: [], @@ -34,5 +44,6 @@ export const sharedPostPropState = atom<{ houseSize: 0, selectedExtraOptions: {}, selectedOptions: {}, + mateCard: { features: { options: new Set() } }, }, }); diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 18bea894a6..7fbba296f8 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -17,7 +17,10 @@ import { updateSharedPost, } from './shared.api'; import { sharedPostPropState } from './shared.atom'; -import { type GetSharedPostDTO } from './shared.dto'; +import { + type GetDormitorySharedPostDTO, + type GetSharedPostDTO, +} from './shared.dto'; import { type GetSharedPostsProps, type SelectedExtraOptions, @@ -26,7 +29,7 @@ import { } from './shared.type'; import { fromAddrToCoord } from '../geocoding'; -import { useAuthValue } from '@/features/auth'; +import { useAuthValue, useUserData } from '@/features/auth'; import { useDebounce } from '@/shared/debounce'; import { type FailureDTO, type SuccessBaseDTO } from '@/shared/types'; @@ -97,57 +100,128 @@ export const usePaging = ({ ); }; -export const useSharedPostProps = () => { +export const useSharedPostProps = ({ + postId, + type, +}: { + postId?: number; + type?: 'hasRoom' | 'dormitory'; +}) => { const [state, setState] = useRecoilState(sharedPostPropState); const reset = useResetRecoilState(sharedPostPropState); - const setStateWithPost = ({ data }: GetSharedPostDTO) => { - fromAddrToCoord({ query: data.address.roadAddress }) - .then(res => { - const address = res.data.addresses.shift(); - if (address != null) setState(prev => ({ ...prev, address })); - }) - .catch(err => { - console.error(err); - }); - - let roomCount = '1개'; - if (data.roomInfo.numberOfRoom === 2) roomCount = '2개'; - else if (data.roomInfo.numberOfRoom === 3) roomCount = '3개 이상'; - - let restRoomCount = '1개'; - if (data.roomInfo.numberOfBathRoom === 2) restRoomCount = '2개'; - else if (data.roomInfo.numberOfBathRoom === 3) restRoomCount = '3개'; - - setState({ - type: state.type, - postId: data.id, - title: data.title, - content: data.content, - images: data.roomImages.map(({ fileName }) => ({ - url: fileName, - uploaded: true, - })), - mateLimit: data.roomInfo.recruitmentCapacity, - expectedMonthlyFee: data.roomInfo.expectedPayment, - houseSize: data.roomInfo.size, - selectedOptions: { - roomType: data.roomInfo.roomType, - roomCount, - budget: data.roomInfo.rentalType, - floorType: data.roomInfo.floorType, - livingRoom: data.roomInfo.hasLivingRoom ? '유' : '무', - restRoomCount, - }, - selectedExtraOptions: { - canPark: data.roomInfo.extraOption.canPark, - hasAirConditioner: data.roomInfo.extraOption.hasAirConditioner, - hasRefrigerator: data.roomInfo.extraOption.hasRefrigerator, - hasWasher: data.roomInfo.extraOption.hasWasher, - hasTerrace: data.roomInfo.extraOption.hasTerrace, - }, - }); - }; + useEffect(() => { + reset(); + + if (postId == null) return; + + const setStateWithPost = ({ + data, + }: GetSharedPostDTO | GetDormitorySharedPostDTO) => { + fromAddrToCoord({ query: data.address.roadAddress }) + .then(res => { + const address = res.data.addresses.shift(); + if (address != null) setState(prev => ({ ...prev, address })); + }) + .catch(err => { + console.error(err); + }); + + let roomCount = '1개'; + let restRoomCount = '1개'; + if ('roomInfo' in data) { + if (data.roomInfo.numberOfRoom === 2) roomCount = '2개'; + else if (data.roomInfo.numberOfRoom === 3) roomCount = '3개 이상'; + + if (data.roomInfo.numberOfBathRoom === 2) restRoomCount = '2개'; + else if (data.roomInfo.numberOfBathRoom === 3) restRoomCount = '3개'; + } + + if ('roomInfo' in data) { + setState({ + ...state, + title: data.title, + content: data.content, + images: data.roomImages.map(({ fileName }) => ({ + url: fileName, + uploaded: true, + })), + mateLimit: data.roomInfo.recruitmentCapacity, + expectedMonthlyFee: data.roomInfo.expectedPayment, + houseSize: data.roomInfo.size, + selectedOptions: { + roomType: data.roomInfo.roomType, + roomCount, + budget: data.roomInfo.rentalType, + floorType: data.roomInfo.floorType, + livingRoom: data.roomInfo.hasLivingRoom ? '유' : '무', + restRoomCount, + }, + selectedExtraOptions: { + canPark: data.roomInfo.extraOption.canPark, + hasAirConditioner: data.roomInfo.extraOption.hasAirConditioner, + hasRefrigerator: data.roomInfo.extraOption.hasRefrigerator, + hasWasher: data.roomInfo.extraOption.hasWasher, + hasTerrace: data.roomInfo.extraOption.hasTerrace, + }, + mateCard: { + ...state.mateCard, + location: data.address.roadAddress, + features: { + smoking: data.roomMateFeatures.smoking, + roomSharingOption: data.roomMateFeatures.roomSharingOption, + mateAge: + data.roomMateFeatures.mateAge != null + ? +data.roomMateFeatures.mateAge + : undefined, + options: new Set( + JSON.parse(data.roomMateFeatures.options) as string[], + ), + }, + }, + }); + } else { + setState({ + ...state, + title: data.title, + content: data.content, + images: data.roomImages.map(({ fileName }) => ({ + url: fileName, + uploaded: true, + })), + mateLimit: data.recruitmentCapacity, + expectedMonthlyFee: 0, + houseSize: 0, + selectedOptions: {}, + selectedExtraOptions: {}, + mateCard: { + ...state.mateCard, + location: data.address.roadAddress, + features: { + smoking: data.roomMateFeatures.smoking, + roomSharingOption: data.roomMateFeatures.roomSharingOption, + mateAge: + data.roomMateFeatures.mateAge != null + ? +data.roomMateFeatures.mateAge + : undefined, + options: new Set( + JSON.parse(data.roomMateFeatures.options) as string[], + ), + }, + }, + }); + } + }; + + (async () => { + const post = + type === 'hasRoom' + ? await getSharedPost(postId) + : await getDormitorySharedPost(postId); + + setStateWithPost(post.data); + })(); + }, []); const handleOptionClick = ( optionName: keyof SelectedOptions, @@ -181,94 +255,86 @@ export const useSharedPostProps = () => { const isExtraOptionSelected = (item: keyof SelectedExtraOptions) => state.selectedExtraOptions[item] === true; - return { - ...state, - setSharedPostProps: setState, - setStateWithPost, - reset, - handleOptionClick, - handleExtraOptionClick, - isOptionSelected, - isExtraOptionSelected, - }; -}; - -export const usePostMateCardInputSection = () => { - const [gender, setGender] = useState(undefined); - const [birthYear, setBirthYear] = useState(undefined); - const [location, setLocation] = useState(undefined); - const [mbti, setMbti] = useState(undefined); - const [major, setMajor] = useState(undefined); - const [budget, setBudget] = useState(undefined); - - const [features, setFeatures] = useState<{ - smoking?: string; - roomSharingOption?: string; - mateAge?: number; - options: Set; - }>({ options: new Set() }); - - const handleEssentialFeatureChange = ( + const handleMateCardEssentialFeatureChange = ( key: 'smoking' | 'roomSharingOption' | 'mateAge', value: string | number | undefined, ) => { - setFeatures(prev => { - if (prev[key] === value) { - const newFeatures = { ...prev }; + setState(prev => { + if (prev.mateCard.features[key] === value) { + const newFeatures = { ...prev.mateCard.features }; newFeatures[key] = undefined; - return newFeatures; + return { + ...prev, + mateCard: { ...prev.mateCard, features: newFeatures }, + }; } - return { ...prev, [key]: value }; + return { + ...prev, + mateCard: { + ...prev.mateCard, + features: { ...prev.mateCard.features, [key]: value }, + }, + }; }); }; - const handleOptionalFeatureChange = (option: string) => { - setFeatures(prev => { - const { options } = prev; + const handleMateCardOptionalFeatureChange = (option: string) => { + setState(prev => { + const { options } = prev.mateCard.features; const newOptions = new Set(options); if (options.has(option)) newOptions.delete(option); else newOptions.add(option); - return { ...prev, options: newOptions }; + return { + ...prev, + mateCard: { + ...prev.mateCard, + features: { ...prev.mateCard.features, options: newOptions }, + }, + }; }); }; - const derivedFeatures = useMemo(() => { + const derivedMateCardFeatures = useMemo(() => { const options: string[] = []; + const { features } = state.mateCard; features.options.forEach(option => options.push(option)); return { smoking: features?.smoking ?? '상관없어요', roomSharingOption: features?.roomSharingOption ?? '상관없어요', - mateAge: birthYear, + mateAge: state.mateCard.birthYear, options: JSON.stringify(options), }; - }, [features, birthYear]); + }, [state.mateCard]); const auth = useAuthValue(); + const { data: user } = useUserData(auth?.accessToken != null); + useEffect(() => { - if (auth?.user != null) { - setGender(auth.user.gender); + if (user?.gender != null) { + setState(prev => ({ + ...prev, + mateCard: { + ...prev.mateCard, + gender: user.gender, + }, + })); } - }, [auth?.user]); + }, [user, setState]); return { - gender, - setGender, - birthYear, - setBirthYear, - location, - setLocation, - mbti, - setMbti, - major, - setMajor, - budget, - setBudget, - derivedFeatures, - handleEssentialFeatureChange, - handleOptionalFeatureChange, + ...state, + derivedMateCardFeatures, + setSharedPostProps: setState, + reset, + handleOptionClick, + handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, + handleMateCardOptionalFeatureChange, + handleMateCardEssentialFeatureChange, }; }; @@ -291,7 +357,6 @@ export const useSharedPosts = ({ await getSharedPosts({ filter: debounceFilter, search, page }).then( response => response.data, ), - staleTime: 60000, enabled, }); }; @@ -339,16 +404,20 @@ export const useDormitorySharedPosts = ({ search, page, enabled, -}: GetSharedPostsProps & { enabled: boolean }) => - useQuery({ - queryKey: ['/api/shared/posts/dormitory', { filter, search, page }], +}: GetSharedPostsProps & { enabled: boolean }) => { + const debounceFilter = useDebounce(filter, 1000); + + return useQuery({ + queryKey: ['/api/shared/posts/dormitory', { debounceFilter, search, page }], queryFn: async () => - await getDormitorySharedPosts({ filter, search, page }).then( - response => response.data, - ), - staleTime: 60000, + await getDormitorySharedPosts({ + filter: debounceFilter, + search, + page, + }).then(response => response.data), enabled, }); +}; export const useDormitorySharedPost = ({ postId, From a3d9bdb932d32c3ded9ae7b2c7d876dc40c24383 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Tue, 14 May 2024 23:18:54 +0900 Subject: [PATCH 107/130] feat: writing-post-page (#79) --- src/app/pages/mobile/index.ts | 1 + .../pages/mobile/mobile-writing-post-page.tsx | 966 ++++++++++++++++++ src/app/shared/writing/page.tsx | 7 +- src/components/UserInputSection.tsx | 10 + .../writing-post-page/LocationSearchBox.tsx | 13 + .../writing-post-page/MateSearchBox.tsx | 13 + 6 files changed, 1009 insertions(+), 1 deletion(-) create mode 100644 src/app/pages/mobile/mobile-writing-post-page.tsx diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts index dc190847db..112ed0b61b 100644 --- a/src/app/pages/mobile/index.ts +++ b/src/app/pages/mobile/index.ts @@ -4,3 +4,4 @@ export * from './mobile-profile-page'; export * from './mobile-user-input-page'; export * from './mobile-setting-page'; export * from './mobile-shared-posts-page'; +export * from './mobile-writing-post-page'; diff --git a/src/app/pages/mobile/mobile-writing-post-page.tsx b/src/app/pages/mobile/mobile-writing-post-page.tsx new file mode 100644 index 0000000000..cf0208d938 --- /dev/null +++ b/src/app/pages/mobile/mobile-writing-post-page.tsx @@ -0,0 +1,966 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import React, { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { UserInputSection } from '@/components'; +import { + LocationSearchBox, + MateSearchBox, +} from '@/components/writing-post-page'; +import { + CountTypeValue, + type DealType, + DealTypeValue, + RoomTypeValue, + type RoomType, + FloorTypeValue, + type FloorType, + AdditionalInfoTypeValue, + LivingRoomTypeValue, +} from '@/entities/shared-posts-filter'; +import { useAuthValue } from '@/features/auth'; +import { getImageURL, putImage } from '@/features/image'; +import { + useCreateSharedPost, + useCreateSharedPostProps, + usePostMateCardInputSection, + type ImageFile, +} from '@/features/shared'; +import { useToast } from '@/features/toast'; + +const styles = { + pageContainer: styled.div` + display: flex; + align-items: center; + width: 100vw; + min-width: 390px; + flex-direction: column; + gap: 1rem; + padding: 2rem 2rem; + `, + postContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 2rem; + align-self: stretch; + + border-radius: 16px; + background: #fff; + `, + essentialInfoContainer: styled.div` + display: flex; + flex: 1 0 0; + width: 100%; + flex-direction: column; + gap: 1rem; + `, + essentialRow: styled.div` + display: flex; + width: 100%; + flex-wrap: wrap; + flex-direction: column; + gap: 0.5rem; + + .column { + display: flex; + flex-direction: column; + + gap: 1rem; + flex: 1 0 0; + } + `, + mateCardContainer: styled.div` + display: flex; + width: 100%; + flex-direction: column; + gap: 1rem; + + button { + all: unset; + cursor: pointer; + + display: flex; + width: fit-content; + padding: 0.5rem 1rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + + border-radius: 0.5rem; + background: #ededed; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + button[class~='edit'] { + display: flex; + width: fit-content; + padding: 0.5rem 1rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + + border-radius: 0.5rem; + background: #e15637; + + color: #eee; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + `, + row: styled.div` + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + `, + option: styled.h2` + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: normal; + align-self: stretch; + `, + captionRow: styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 1rem; + align-self: stretch; + + .caption { + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + dealInfoContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + `, + roomInfoContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + `, + caption: styled.span` + color: rgba(53, 55, 58, 0.5); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + optionRow: styled.div` + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 0.5rem 1rem; + align-self: stretch; + `, + optionCategory: styled.h1` + align-self: stretch; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + `, + createButton: styled.button` + all: unset; + + cursor: pointer; + + display: flex; + width: 7.125rem; + height: fit-content; + padding: 0.5rem 1.5rem; + justify-content: center; + align-items: center; + + border-radius: 8px; + background: var(--Black, #35373a); + + color: #fff; + font-family: Pretendard; + font-size: 1.125rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + + @media (max-width: 768px) { + font-size: 0.75rem; + width: 4rem; + padding: 0.25rem 1rem; + } + `, + titleInput: styled.input` + all: unset; + + display: flex; + padding: 0.5rem; + flex-direction: column; + justify-content: center; + align-items: flex-start; + align-self: stretch; + border-radius: 8px; + background: #ededed; + font-size: 0.875rem; + `, + contentInput: styled.textarea` + all: unset; + + display: flex; + width: 90%; + height: 100%; + padding: 1rem; + flex-direction: column; + justify-content: center; + align-items: flex-start; + align-self: stretch; + border-radius: 8px; + background: #ededed; + font-size: 0.875rem; + `, + optionButtonContainer: styled.div` + display: flex; + align-items: flex-start; + gap: 0.5rem; + + span { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + customRadioButton: styled.div` + display: inline-block; + width: 1rem; + height: 1rem; + cursor: pointer; + background-size: cover; + ${props => + props.$isSelected + ? { + backgroundImage: `url('/button-icon/Radio button checked.svg')`, + } + : { + backgroundImage: `url('/button-icon/Radio button unchecked.svg')`, + }}; + `, + customCheckBox: styled.div` + display: inline-block; + width: 1rem; + height: 1rem; + cursor: pointer; + background-size: cover; + ${props => + props.$isSelected + ? { + backgroundImage: `url('/button-icon/Check box.svg')`, + } + : { + backgroundImage: `url('/button-icon/Check box outline blank.svg')`, + }}; + `, + images: styled.div` + display: flex; + width: 100%; + flex-wrap: wrap; + align-items: center; + align-self: stretch; + gap: 1rem; + + overflow-x: auto; + `, + image: styled.img` + width: 9.5rem; + height: 7rem; + background: #ededed; + + object-fit: cover; + object-position: center; + + cursor: pointer; + `, + imageAddButton: styled.button` + all: unset; + + border: 0.5px solid #80808080; + cursor: pointer; + + width: 9.5rem; + height: 7rem; + background: #ededed; + background-image: url(https://s3-alpha-sig.figma.com/img/7307/09fa/b5d93c9ac77c2570ffbee89fe8a76c98?Expires=1714348800&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=Xr~6xrrHnQFb6NwdPlxTuJ2wd7kTnRZ-9kpTlGxYQL-bU7ZVcN8IMQ4k6yEAj~x3y2roX-poLo4XP4x6-adpxlciddzg0ZUuWg0B3VrMgMwbl~sTasgqAe~0SL9E4kkEx7OilanZoC5fJlVBglfb8kE1nZBaG5wEp3FCbLZhzZTnl~29Loisbo1pwteh~2ABpLSVttEztULov1lzws4qcrHY5QpGb8KM4PxBTBTfQDMa8an5QmG~uUlt-bYgVEFMuA2vsKHc-aY8HoiF7v03UDHSGNOVrX1Ajt7ARWqJtOiM~epvCYTkVJPmkNe6WcCgRm37xGKbH2LEzn9aEZJyFA__); + background-position: center; + background-repeat: no-repeat; + `, + inputContainer: styled.div` + display: flex; + width: fit-content; + padding: 0.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + border: 1px solid var(--Black, #35373a); + `, + input: styled.input<{ $width: number }>` + all: unset; + width: ${({ $width }) => `${$width}rem`}; + flex: 1 0 0; + color: var(--Gray-5, #828282); + text-align: right; + font-size: 0.75rem; + `, + inputPlaceholder: styled.span` + color: var(--Gray-5, #828282); + text-align: right; + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + mates: styled.div` + display: flex; + align-items: center; + align-self: stretch; + gap: 1rem; + + overflow-x: auto; + `, + mate: styled.img` + width: 5.0625rem; + height: 5.125rem; + border-radius: 50%; + border: 1px solid #dcddea; + background: #fff; + `, + mateAddButton: styled.input` + all: unset; + cursor: pointer; + + width: 3.0625rem; + height: 3.125rem; + + border-radius: 50%; + border: 1px solid #dcddea; + background: #fff; + background-image: url('/icon-plus.png'); + background-position: center; + background-repeat: no-repeat; + `, + address: styled.span` + color: #e15637; + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + addressFindButtonContainer: styled.button` + all: unset; + cursor: pointer; + + display: flex; + padding: 0.5rem; + justify-content: center; + align-items: center; + gap: 0.5rem; + + border-radius: 8px; + background: #ededed; + + span { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + addressFindButtonIcon: styled.img` + width: 1rem; + height: 1rem; + `, +}; + +interface ButtonActiveProps { + $isSelected: boolean; +} + +export function MobileWritingPostPage() { + const router = useRouter(); + + const imageInputRef = useRef(null); + const [showMateSearchBox, setShowMateSearchBox] = useState(false); + const [showMateCardForm, setShowMateCardForm] = useState(false); + const [showLocationSearchBox, setShowLocationSearchBox] = + useState(false); + + const { + title, + content, + images, + mateLimit, + houseSize, + address, + selectedOptions, + selectedExtraOptions, + expectedMonthlyFee, + setTitle, + setContent, + setImages, + setMateLimit, + setHouseSize, + setAddress, + setExpectedMonthlyFee, + handleOptionClick, + handleExtraOptionClick, + isOptionSelected, + isExtraOptionSelected, + } = useCreateSharedPostProps(); + + const { + gender, + birthYear, + mbti, + major, + derivedFeatures, + setBirthYear, + setMbti, + setMajor, + setBudget, + handleEssentialFeatureChange, + handleOptionalFeatureChange, + } = usePostMateCardInputSection(); + + const { mutate } = useCreateSharedPost(); + const { createToast } = useToast(); + + const auth = useAuthValue(); + + const handleTitleInputChanged = ( + event: React.ChangeEvent, + ) => { + setTitle(event.target.value); + }; + + const handleContentInputChanged = ( + event: React.ChangeEvent, + ) => { + setContent(event.target.value); + }; + + const handleImageInputClicked = () => { + imageInputRef.current?.click(); + }; + + const handleFileChanged = (event: React.ChangeEvent) => { + const { files } = event.target; + if (files != null) { + const imagesArray = Array.from(files).map(file => ({ + file, + url: URL.createObjectURL(file), + extension: `.${file.type.split('/')[1]}`, + })); + setImages(prevImages => [...prevImages, ...imagesArray]); + } + }; + + const handleRemoveImage = (removeImage: ImageFile) => { + setImages(prev => prev.filter(image => image.url !== removeImage.url)); + }; + + const convertToNumber = (value: string) => { + const updatedValue = value.replace(/^0+/, ''); + + if (updatedValue.length === 0) return 0; + + const numberUpdateValue = Number(updatedValue); + if (!Number.isNaN(numberUpdateValue)) return numberUpdateValue; + return null; + }; + + const handleNumberInput = ( + value: string, + setter: (value: number) => void, + ) => { + const converted = convertToNumber(value); + if (converted == null) return; + setter(converted); + }; + + const handleCreatePost = (event: React.MouseEvent) => { + // if (!isPostCreatable || !isMateCardCreatable) return; + + const dealType = selectedOptions.budget; + const { roomType } = selectedOptions; + const { floorType } = selectedOptions; + + if ( + dealType == null || + roomType == null || + floorType == null || + address == null || + selectedOptions.roomCount == null || + !(selectedOptions.roomCount in CountTypeValue) || + selectedOptions.restRoomCount == null || + !(selectedOptions.restRoomCount in CountTypeValue) + ) + return; + + const numberOfRoomOption = selectedOptions.roomCount as + | '1개' + | '2개' + | '3개 이상'; + const numberOfRoom = CountTypeValue[numberOfRoomOption]; + + const numberOfBathRoomOption = selectedOptions.restRoomCount as + | '1개' + | '2개' + | '3개 이상'; + const numberOfBathRoom = CountTypeValue[numberOfBathRoomOption]; + + const dealTypeValue = DealTypeValue[dealType as DealType]; + const roomTypeValue = RoomTypeValue[roomType as RoomType]; + const floorTypeValue = FloorTypeValue[floorType as FloorType]; + + (async () => { + try { + const getResults = await Promise.allSettled( + images.map(async ({ extension, file }) => { + const result = await getImageURL(extension); + return { + ...result.data.data, + file, + }; + }), + ); + + const urls = getResults.reduce< + Array<{ file: File; fileName: string; url: string }> + >((prev, result) => { + if (result.status === 'rejected') return prev; + return prev.concat(result.value); + }, []); + + const putResults = await Promise.allSettled( + urls.map(async url => { + await putImage(url.url, url.file); + return { fileName: url.fileName }; + }), + ); + + const uploadedImages = putResults.reduce< + Array<{ fileName: string; isThumbNail: boolean; order: number }> + >((prev, result) => { + if (result.status === 'rejected') return prev; + return prev.concat({ + fileName: result.value.fileName, + isThumbNail: prev.length === 0, + order: prev.length + 1, + }); + }, []); + + mutate( + { + imageFilesData: uploadedImages, + postData: { title, content }, + transactionData: { + rentalType: dealTypeValue, + expectedPayment: expectedMonthlyFee, + }, + roomDetailData: { + roomType: roomTypeValue, + floorType: floorTypeValue, + size: houseSize, + numberOfRoom, + numberOfBathRoom, + hasLivingRoom: selectedOptions.livingRoom === '유', + recruitmentCapacity: mateLimit, + extraOption: { + canPark: selectedExtraOptions.canPark ?? false, + hasAirConditioner: + selectedExtraOptions.hasAirConditioner ?? false, + hasRefrigerator: selectedExtraOptions.hasRefrigerator ?? false, + hasWasher: selectedExtraOptions.hasWasher ?? false, + hasTerrace: selectedExtraOptions.hasTerrace ?? false, + }, + }, + locationData: { + oldAddress: address?.jibunAddress, + roadAddress: address?.roadAddress, + }, + roomMateCardData: { + location: address?.roadAddress, + features: derivedFeatures, + }, + participationMemberIds: + auth?.user != null ? [auth.user.memberId] : [], + }, + { + onSuccess: () => { + createToast({ + message: '게시글이 정상적으로 업로드되었습니다.', + option: { + duration: 3000, + }, + }); + router.back(); + }, + onError: () => { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + }, + }, + ); + } catch (error) { + createToast({ + message: '게시글 업로드에 실패했습니다.', + option: { + duration: 3000, + }, + }); + } + })(); + }; + + useEffect( + () => () => { + images.forEach(image => { + URL.revokeObjectURL(image.url); + }); + }, + [images], + ); + + return ( + + + + + 기본 정보 + + 작성하기 + + + 제목 + + 위치 정보 + + { + setShowLocationSearchBox(true); + }} + > + + 위치 찾기 + + + 상세 주소: + + + {address?.roadAddress ?? '주소를 입력해주세요.'} + + + {showLocationSearchBox && ( + { + setAddress(selectedAddress); + setShowLocationSearchBox(false); + }} + setHidden={() => { + setShowLocationSearchBox(false); + }} + /> + )} + +
+ 상세 정보 + +
+
+ + 사진 + 최소 2장 이상 업로드 + + + {images.map(image => ( + { + handleRemoveImage(image); + }} + /> + ))} + + + + +
+
+ +
+ 모집 할 인원 + + { + handleNumberInput(event.target.value, value => { + setMateLimit(value); + }); + }} + $width={3} + /> + + +
+
+ 메이트 + + { + setShowMateSearchBox(true); + }} + /> + {showMateSearchBox && ( + { + setShowMateSearchBox(false); + }} + /> + )} + +
+
+ + 메이트 카드 + + {showMateCardForm && ( +
+ {}} + onMateAgeChange={setBirthYear} + onMbtiChange={setMbti} + onMajorChange={setMajor} + onBudgetChange={setBudget} + /> +
+ )} +
+
+ + 거래 정보 + 거래 방식 + + {Object.keys(DealTypeValue).map(option => ( + + { + handleOptionClick('budget', option); + }} + /> + {option} + + ))} + + 희망 메이트 월 분담금 + + { + handleNumberInput(event.target.value, value => { + setExpectedMonthlyFee(value); + }); + }} + $width={3} + /> + 만원 + + + + 방 정보 + + + {Object.keys(FloorTypeValue).map(option => ( + + { + handleOptionClick('floorType', option); + }} + /> + {option} + + ))} + + 추가 옵션 + + {Object.keys(AdditionalInfoTypeValue).map(option => ( + + { + handleExtraOptionClick(option); + }} + /> + {option} + + ))} + + 방 종류 + + {Object.keys(RoomTypeValue).map(option => ( + + { + handleOptionClick('roomType', option); + }} + /> + {option} + + ))} + + 거실 + + {Object.keys(LivingRoomTypeValue).map(option => ( + + { + handleOptionClick('livingRoom', option); + }} + /> + {option} + + ))} + + 방 개수 + + {Object.keys(CountTypeValue).map(option => ( + + { + handleOptionClick('roomCount', option); + }} + /> + {option} + + ))} + + 화장실 개수 + + {Object.keys(CountTypeValue).map(option => ( + + { + handleOptionClick('restRoomCount', option); + }} + /> + {option} + + ))} + + 전체 면적 + + { + handleNumberInput(event.target.value, value => { + setHouseSize(value); + }); + }} + $width={2} + /> + + + +
+
+ ); +} diff --git a/src/app/shared/writing/page.tsx b/src/app/shared/writing/page.tsx index 84ba7e9f1e..b164225a73 100644 --- a/src/app/shared/writing/page.tsx +++ b/src/app/shared/writing/page.tsx @@ -1,5 +1,10 @@ +'use client'; + import { WritingPostPage } from '@/app/pages'; +import { MobileWritingPostPage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page() { - return ; + const isMobile = useIsMobile(); + return <>{isMobile ? : }; } diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index e64d0e496e..c9f41a2e70 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -20,11 +20,21 @@ const styles = { border-radius: 0 0 30px 30px; width: 100%; } + + @media (max-width: 400px) { + border-radius: 0 0 30px 30px; + width: 100%; + height: 80rem; + } `, horizontalLine: styled.div` width: 43.75rem; height: 0.0625rem; background: var(--Gray-9, #d3d0d7); + + @media (max-width: 768px) { + width: 100%; + } `, }; diff --git a/src/components/writing-post-page/LocationSearchBox.tsx b/src/components/writing-post-page/LocationSearchBox.tsx index c95663d2e0..4b2c0d9eba 100644 --- a/src/components/writing-post-page/LocationSearchBox.tsx +++ b/src/components/writing-post-page/LocationSearchBox.tsx @@ -28,10 +28,18 @@ const styles = { display: flex; flex-direction: column; gap: 1rem; + + @media (max-width: 768px) { + padding: 1.2rem; + } `, title: styled.h1` font-size: 1.25rem; margin-bottom: 1rem; + + @media (max-width: 768px) { + font-size: 0.875rem; + } `, locationInput: styled.input<{ $empty: boolean }>` width: 100%; @@ -48,6 +56,11 @@ const styles = { &:focus { outline: none; } + + @media (max-width: 768px) { + padding: 0.5rem; + font-size: 0.75rem; + } `, addressSearchResult: styled.ul` display: flex; diff --git a/src/components/writing-post-page/MateSearchBox.tsx b/src/components/writing-post-page/MateSearchBox.tsx index 313202392a..f06fd5a753 100644 --- a/src/components/writing-post-page/MateSearchBox.tsx +++ b/src/components/writing-post-page/MateSearchBox.tsx @@ -20,15 +20,28 @@ const styles = { display: flex; flex-direction: column; gap: 1rem; + + @media (max-width: 768px) { + padding: 1rem; + width: 60dvw; + } `, row: styled.div` display: flex; flex-direction: row; gap: 0.5rem; align-items: end; + + @media (max-width: 768px) { + flex-wrap: wrap; + } `, title: styled.h1` font-size: 1.25rem; + + @media (max-width: 768px) { + font-size: 1rem; + } `, caption: styled.span` font-size: 0.85rem; From ecac3c8180f5e227ea7b62ad2ee52868ba1dcbcf Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 15 May 2024 00:42:29 +0900 Subject: [PATCH 108/130] feat: shared-post-page (#79) --- src/app/pages/mobile/index.ts | 1 + .../pages/mobile/mobile-shared-post-page.tsx | 566 ++++++++++++++++++ src/app/pages/writing-post-page.tsx | 3 +- src/app/shared/[postId]/page.tsx | 15 +- src/components/shared-post-page/ImageGrid.tsx | 4 + 5 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 src/app/pages/mobile/mobile-shared-post-page.tsx diff --git a/src/app/pages/mobile/index.ts b/src/app/pages/mobile/index.ts index 112ed0b61b..0da2e2dd7b 100644 --- a/src/app/pages/mobile/index.ts +++ b/src/app/pages/mobile/index.ts @@ -5,3 +5,4 @@ export * from './mobile-user-input-page'; export * from './mobile-setting-page'; export * from './mobile-shared-posts-page'; export * from './mobile-writing-post-page'; +export * from './mobile-shared-post-page'; diff --git a/src/app/pages/mobile/mobile-shared-post-page.tsx b/src/app/pages/mobile/mobile-shared-post-page.tsx new file mode 100644 index 0000000000..cacba9773d --- /dev/null +++ b/src/app/pages/mobile/mobile-shared-post-page.tsx @@ -0,0 +1,566 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { Bookmark, CircularProfileImage } from '@/components'; +import { ImageGrid } from '@/components/shared-post-page'; +import { useAuthValue, useUserData } from '@/features/auth'; +import { useCreateChatRoom } from '@/features/chat'; +import { + useFollowUser, + useFollowingListData, + useUnfollowUser, +} from '@/features/profile'; +import { useScrapSharedPost, useSharedPost } from '@/features/shared'; +import { getAge } from '@/shared'; + +const styles = { + container: styled.div` + display: flex; + align-items: center; + width: 100vw; + min-width: 390px; + flex-direction: column; + gap: 1rem; + padding: 1rem 0; + `, + contentContainer: styled.div` + display: flex; + width: 100%; + justify-content: center; + align-items: flex-start; + gap: 1rem; + `, + postContainer: styled.div` + display: flex; + padding: 0 1rem; + width: 100%; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + + border-radius: 16px; + background: #fff; + `, + mateContainer: styled.div` + display: flex; + width: 100%; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + `, + ImageGrid: styled(ImageGrid)` + width: 100%; + `, + postInfoContainer: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + + width: 100%; + + div { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + + h1 { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + + overflow: hidden; + color: #000; + text-overflow: ellipsis; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + } + `, + postInfoContent: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 500; + line-height: normal; + + & > span { + width: 100%; + } + + div { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + } + `, + postContentContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.8rem; + align-self: stretch; + + h2 { + align-self: stretch; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + + p { + align-self: stretch; + + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + divider: styled.div` + width: 100%; + height: 0.0625rem; + background: #d3d0d7; + `, + dealInfoContainer: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + align-self: stretch; + + h2 { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + + div { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + roomInfoContainer: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + align-self: stretch; + + h2 { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + + div { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + `, + locationInfoContainer: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.8rem; + align-self: stretch; + + h2 { + align-self: stretch; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + } + + p { + align-self: stretch; + color: var(--Black, #35373a); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + #map { + height: 13.375rem; + align-self: stretch; + } + `, + mates: styled.div` + display: flex; + flex-direction: column; + padding: 1rem; + align-items: flex-start; + + border-radius: 16px; + background: #fff; + `, + mate: styled.img<{ $selected?: boolean; $zIndex?: number }>` + cursor: pointer; + + display: flex; + width: 3.75rem; + height: 3.75rem; + justify-content: center; + align-items: center; + flex-shrink: 0; + + border-radius: 50%; + border: 1px solid + ${({ $selected }) => + $selected != null && $selected ? '#e15637' : '#DCDDEA'}; + background: #c4c4c4; + + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + + z-index: ${({ $zIndex }) => $zIndex}; + + &:not(:first-child) { + margin-top: -1rem; + } + `, + selectedMateContainer: styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + align-self: stretch; + + border-radius: 16px; + background: #fff; + `, + profile: styled.div` + display: flex; + gap: 1.25rem; + align-items: center; + align-self: stretch; + `, + profileInfo: styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.5rem; + align-self: stretch; + + .name { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + } + + div { + color: #000; + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 500; + line-height: normal; + } + `, + buttons: styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + align-self: stretch; + + div { + display: flex; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + } + `, + chattingButton: styled.button` + all: unset; + cursor: pointer; + + display: flex; + width: 2.125rem; + padding: 0.25rem 0.75rem; + justify-content: center; + align-items: center; + gap: 0.25rem; + + border-radius: 8px; + background: var(--Black, #35373a); + + color: #fff; + font-family: Pretendard; + font-size: 0.875rem; + font-style: normal; + font-weight: 600; + line-height: 1.5rem; + `, +}; + +export function MobileSharedPostPage({ postId }: { postId: number }) { + const auth = useAuthValue(); + const [, setMap] = useState(null); + + const [selected, setSelected] = useState< + | { + memberId: string; + profileImage: string; + } + | undefined + >(undefined); + + const { isLoading, data: sharedPost } = useSharedPost({ + postId, + enabled: auth?.accessToken !== undefined, + }); + + const { data: userData } = useUserData(auth?.accessToken !== undefined); + const [userId, setUserId] = useState(''); + + useEffect(() => { + if (userData !== undefined) { + setUserId(userData.memberId); + } + }, [userData]); + + const { mutate: scrapPost } = useScrapSharedPost(); + + const followList = useFollowingListData(); + const [isFollowed, setIsFollowed] = useState( + followList.data?.data.followingList[ + sharedPost?.data.publisherAccount.memberId ?? '' + ] != null, + ); + + const { mutate: follow } = useFollowUser( + sharedPost?.data.publisherAccount.memberId ?? '', + ); + const { mutate: unfollow } = useUnfollowUser( + sharedPost?.data.publisherAccount.memberId ?? '', + ); + + useEffect(() => { + const center = new naver.maps.LatLng(37.6090857, 126.9966865); + setMap( + new naver.maps.Map('map', { + center, + disableKineticPan: false, + scrollWheel: false, + }), + ); + }, []); + + const [roomName, setRoomName] = useState(''); + + useEffect(() => { + if (sharedPost !== undefined) { + setRoomName(sharedPost.data.publisherAccount.nickname); + } + }, [sharedPost]); + + const members = [userId]; + const { mutate: chattingMutate } = useCreateChatRoom(roomName, members); + + if (isLoading || sharedPost == null) return <>; + + return ( + + + + + + + + +

+ {sharedPost.data.publisherAccount.nickname} +

+
+

+ {sharedPost.data.publisherAccount.birthYear != null + ? getAge(+sharedPost.data.publisherAccount.birthYear) + : new Date().getFullYear()} +

+

{sharedPost.data.address.roadAddress}

+
+
+ +
+ { + if (isFollowed) unfollow(); + else follow(); + setIsFollowed(prev => !prev); + }} + hasBorder + color="#888" + /> +
+ { + chattingMutate(); + }} + > + 채팅 + +
+ + {sharedPost.data.participants.map( + ({ memberId, profileImage }, index) => ( + { + setSelected({ memberId, profileImage }); + }} + /> + ), + )} + +
+
+
+ fileName)} + /> + +
+

{sharedPost.data.title}

+ { + scrapPost(postId); + }} + color="black" + /> +
+ + 모집 {sharedPost.data.roomInfo.recruitmentCapacity}명 + + {sharedPost.data.roomInfo.roomType} · 방{' '} + {sharedPost.data.roomInfo.numberOfRoom} · 화장실{' '} + {sharedPost.data.roomInfo.numberOfBathRoom} + +
+ + 희망 월 분담금 {sharedPost.data.roomInfo.expectedPayment}만원 + + + 저장 {sharedPost.data.scrapCount} · 조회{' '} + {sharedPost.data.viewCount} + +
+
+
+ +

상세 정보

+

{sharedPost.data.content}

+
+ + +

거래 정보

+
+ 거래 방식 + {sharedPost.data.roomInfo.rentalType} +
+
+ 희망 월 분담금 + {sharedPost.data.roomInfo.expectedPayment} +
+
+ +

방 정보

+
+ 방 종류 + {sharedPost.data.roomInfo.roomType} +
+
+ 거실 보유 + + {sharedPost.data.roomInfo.hasLivingRoom ? '유' : '무'} + +
+
+ 방 개수 + {sharedPost.data.roomInfo.numberOfRoom}개 +
+
+ 화장실 개수 + {sharedPost.data.roomInfo.numberOfBathRoom}개 +
+
+ 평수 + {sharedPost.data.roomInfo.size}평 +
+
+ +

위치 정보

+

{sharedPost.data.address.roadAddress}

+
+ + + + + ); +} diff --git a/src/app/pages/writing-post-page.tsx b/src/app/pages/writing-post-page.tsx index b6b34586fb..5a5cdf0b7f 100644 --- a/src/app/pages/writing-post-page.tsx +++ b/src/app/pages/writing-post-page.tsx @@ -444,7 +444,6 @@ export function WritingPostPage() { birthYear, mbti, major, - budget, derivedFeatures, setBirthYear, setMbti, @@ -796,7 +795,7 @@ export function WritingPostPage() { location={address?.roadAddress ?? '주소를 입력해주세요.'} mbti={mbti} major={major} - budget={budget} + budget={undefined} features={undefined} isMySelf type="mateCard" diff --git a/src/app/shared/[postId]/page.tsx b/src/app/shared/[postId]/page.tsx index 5c1f9dc90b..db28c34765 100644 --- a/src/app/shared/[postId]/page.tsx +++ b/src/app/shared/[postId]/page.tsx @@ -1,9 +1,22 @@ +'use client'; + import { SharedPostPage } from '@/app/pages'; +import { MobileSharedPostPage } from '@/app/pages/mobile'; +import { useIsMobile } from '@/shared/mobile'; export default function Page({ params: { postId }, }: { params: { postId: string }; }) { - return ; + const isMobile = useIsMobile(); + return ( + <> + {isMobile ? ( + + ) : ( + + )} + + ); } diff --git a/src/components/shared-post-page/ImageGrid.tsx b/src/components/shared-post-page/ImageGrid.tsx index aaa100e4c7..0fcf37a8b0 100644 --- a/src/components/shared-post-page/ImageGrid.tsx +++ b/src/components/shared-post-page/ImageGrid.tsx @@ -30,6 +30,10 @@ const styles = { grid-auto-rows: 20.625rem; gap: 0.81rem; + @media (max-width: 768px) { + grid-auto-rows: 10.25rem; + } + img { width: 100%; height: 100%; From 8e137dd79d24c958c3b30cd7d177714b3a8a5e69 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 00:45:50 +0900 Subject: [PATCH 109/130] fix: Fix the issue of floorTypes incorrect value (#75) --- src/entities/shared-posts-filter/shared-posts-filter.hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts index 0467587afa..05065abf6a 100644 --- a/src/entities/shared-posts-filter/shared-posts-filter.hook.ts +++ b/src/entities/shared-posts-filter/shared-posts-filter.hook.ts @@ -100,7 +100,7 @@ export const useSharedPostsFilter = () => { end: filter.roomInfo.size.high, } : undefined, - floorTypes: floorTypes.length === 0 ? undefined : rentalTypes, + floorTypes: floorTypes.length === 0 ? undefined : floorTypes, canPark: filter.extraInfo.주차가능, hasAirConditioner: filter.extraInfo.에어컨, hasRefrigerator: filter.extraInfo.냉장고, From 0a4418e0acc906b66d3ee38dc85f1e0b1bd4888c Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 00:48:20 +0900 Subject: [PATCH 110/130] fix: Fix the issue of recruitment mate incorrect value (#75) --- src/components/shared-posts/PostCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index cf1da85dac..7cd8022cd5 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -131,7 +131,7 @@ export function PostCard({ }) { const recruitmentCapacity = 'roomInfo' in post - ? post.roomInfo.expectedPayment + ? post.roomInfo.recruitmentCapacity : post.recruitmentCapacity; return ( From 80d70b3422e38f7f25498e3807c59a9b01bfa114 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 15 May 2024 01:37:04 +0900 Subject: [PATCH 111/130] feat: chatting-component (#79) --- src/app/pages/mobile/mobile-main-page.tsx | 2 +- src/components/FloatingChatting.tsx | 25 +- src/components/chat/ChatMenu.tsx | 4 + src/components/chat/MobileChattingBox.tsx | 237 +++++++++++++ src/components/chat/MobileChattingRoom.tsx | 367 +++++++++++++++++++++ 5 files changed, 631 insertions(+), 4 deletions(-) create mode 100644 src/components/chat/MobileChattingBox.tsx create mode 100644 src/components/chat/MobileChattingRoom.tsx diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx index be200704a0..e3c99888d3 100644 --- a/src/app/pages/mobile/mobile-main-page.tsx +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -49,7 +49,7 @@ const styles = { } `, mateRecommendationContainer: styled.div` - width: 100vw; + width: 100%; display: flex; flex-direction: column; gap: 0.5rem; diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 55d9a70697..399482672a 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -2,14 +2,16 @@ import { Client } from '@stomp/stompjs'; import axios from 'axios'; -import React, { useEffect, useState } from 'react'; +import React, { type ReactElement, useEffect, useState } from 'react'; import styled from 'styled-components'; import { ChattingList } from './chat/ChattingList'; import { ChattingRoom } from './chat/ChattingRoom'; +import { MobileChattingBox } from './chat/MobileChattingBox'; import { useAuthValue, useUserData } from '@/features/auth'; import { type GetChatRoomDTO } from '@/features/chat'; +import { useIsMobile } from '@/shared/mobile'; const styles = { chattingButton: styled.div` @@ -23,7 +25,7 @@ const styles = { border-radius: 100px; background: #e15637; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); - z-index: 100; + z-index: 20000; transition: transform 0.3s ease; cursor: pointer; @@ -34,6 +36,11 @@ const styles = { buttonIcon: styled.img` width: 2rem; height: 2rem; + + @media (max-width: 768px) { + width: 1.5rem; + height: 1.5rem; + } `, container: styled.div` position: fixed; @@ -305,12 +312,24 @@ export function FloatingChatting() { setIsChatOpen(prevState => !prevState); }; + const isMobile = useIsMobile(); + const [chatBox, setChatBox] = useState(); + useEffect(() => { + if (isChatOpen) { + if (!isMobile) { + setChatBox(() as ReactElement); + } else { + setChatBox(() as ReactElement); + } + } else setChatBox(undefined); + }, [isChatOpen, isMobile]); + return ( <> - {isChatOpen && } + {chatBox} ); } diff --git a/src/components/chat/ChatMenu.tsx b/src/components/chat/ChatMenu.tsx index 9492cd6577..17083b6574 100644 --- a/src/components/chat/ChatMenu.tsx +++ b/src/components/chat/ChatMenu.tsx @@ -20,6 +20,10 @@ const styles = { background: #fff; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); z-index: 20; + + @media (max-width: 768px) { + border-radius: 0; + } `, header: styled.div` width: 100%; diff --git a/src/components/chat/MobileChattingBox.tsx b/src/components/chat/MobileChattingBox.tsx new file mode 100644 index 0000000000..d6b054f918 --- /dev/null +++ b/src/components/chat/MobileChattingBox.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { Client } from '@stomp/stompjs'; +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { ChattingList } from './ChattingList'; +import { MobileChattingRoom } from './MobileChattingRoom'; + +import { useAuthValue, useUserData } from '@/features/auth'; +import { type GetChatRoomDTO } from '@/features/chat'; + +const styles = { + container: styled.div` + display: flex; + width: 100vw; + height: 100vh; + min-width: 390px; + padding-bottom: 2rem; + flex-direction: column; + align-items: center; + position: absolute; + top: 4.5rem; + z-index: 100; + `, + chattingSection: styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow-y: scroll; + background-color: white; + + &::-webkit-scrollbar { + width: 0.5rem; + } + &::-webkit-scrollbar-thumb { + background-color: #ced3da; + border-radius: 4px; + } + `, + searchButton: styled.img` + width: 1.2rem; + height: 1.2rem; + cursor: pointer; + `, + searchInput: styled.input` + flex: 1; + font-size: 1.25rem; + padding: 0.8rem; + height: 2rem; + width: 8rem; + background-color: transparent; + border: #bdbdbd solid 1px; + border-radius: 1.2rem; + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + + &:focus { + outline: none; + } + `, +}; + +interface ChatRoom { + roomId: number; + roomName: string; + unreadCount: number; + lastMessage: string; + lastMessageTime: string; +} + +interface Message { + createAt: string; + message: string; + messageId: string; + nickname: string; + roomId: number; + sender: string; +} + +export function MobileChattingBox() { + const [isChatRoomOpen, setIsChatRoomOpen] = useState(false); + const [chatRooms, setChatRooms] = useState([]); + const [message, setMessage] = useState(); + const [selectedRoomId, setSelectedRoomId] = useState(0); + const [selectedRoomName, setSelectedRoomName] = useState(''); + const [selectedRoomLastTime, setSelectedRoomLastTime] = useState(''); + + const auth = useAuthValue(); + const { data } = useUserData(auth?.accessToken !== undefined); + const [userId, setUserId] = useState(''); + const [userName, setUserName] = useState(''); + + const [stompClient, setStompClient] = useState(null); + + useEffect(() => { + const initializeChat = async () => { + try { + const stomp = new Client({ + brokerURL: `ws://ec2-54-180-133-123.ap-northeast-2.compute.amazonaws.com:8080/ws`, + connectHeaders: { + Authorization: `Bearer ${auth?.accessToken}`, + }, + debug: (str: string) => { + console.log(str); + }, + reconnectDelay: 5000, + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + }); + setStompClient(stomp); + stomp.activate(); + + stomp.onConnect = () => { + console.log('WebSocket 연결이 열렸습니다.'); + stomp.subscribe(`/roomList/${userId}`, frame => { + try { + const newMessage: Message = JSON.parse(frame.body); + if (newMessage != null) setMessage(newMessage); + } catch (error) { + console.error('오류가 발생했습니다:', error); + } + }); + }; + } catch (error) { + console.error('채팅 룸 생성 중 오류가 발생했습니다:', error); + } + }; + + if (auth?.accessToken != null) { + void initializeChat(); + } + + return () => { + if (stompClient != null && stompClient.connected) { + void stompClient.deactivate(); + } + }; + }, [auth?.accessToken, userId]); + + useEffect(() => { + if (data !== undefined) { + setUserId(data.memberId); + setUserName(data.name); + } + }, [data]); + + const handleChatRoomClick = () => { + setIsChatRoomOpen(true); + }; + + useEffect(() => { + setTimeout(() => { + (async () => { + if (!isChatRoomOpen) { + try { + const res = await axios.get('/maru-api/chatRoom'); + const chatRoomListData: ChatRoom[] = res.data.data; + setChatRooms(chatRoomListData); + return true; + } catch (error) { + console.error(error); + return false; + } + } + return true; + })(); + }, 50); + }, [auth, isChatRoomOpen]); + + useEffect(() => { + if (!isChatRoomOpen) { + setChatRooms(prevChatRooms => + prevChatRooms.map(room => { + if (room.roomId === message?.roomId) { + return { + ...room, + unreadCount: room.unreadCount + 1, + lastMessage: message.message, + }; + } + return room; + }), + ); + } + }, [message, isChatRoomOpen]); + + // const roomName = 'test2'; + // const members = ['naver_htT4VdDRPKqGqKpnncpa71HCA4CVg5LdRC1cWZhCnF8']; + // const { mutate: chattingCreate } = useCreateChatRoom(roomName, members); + + return ( + <> + + + {/* */} + {chatRooms.map((room, index) => ( + { + handleChatRoomClick(); + setSelectedRoomId(room.roomId); + setSelectedRoomName(room.roomName); + setSelectedRoomLastTime(room.lastMessageTime); + }} + /> + ))} + + + {isChatRoomOpen && ( + + )} + + ); +} diff --git a/src/components/chat/MobileChattingRoom.tsx b/src/components/chat/MobileChattingRoom.tsx new file mode 100644 index 0000000000..7ecfa91486 --- /dev/null +++ b/src/components/chat/MobileChattingRoom.tsx @@ -0,0 +1,367 @@ +'use client'; + +import { Client } from '@stomp/stompjs'; +import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { ChatMenu } from './ChatMenu'; +import { ReceiverMessage } from './ReceiverMessage'; +import { SenderMessage } from './SenderMessage'; + +import { useAuthValue } from '@/features/auth'; +import { useEnterChatRoom, useExitChatRoom } from '@/features/chat'; + +const styles = { + container: styled.div` + display: flex; + width: 100vw; + height: calc(100% - 2rem); + min-width: 390px; + padding-bottom: 2rem; + flex-direction: column; + align-items: center; + position: absolute; + top: 4.5rem; + background-color: white; + z-index: 200; + `, + header: styled.div` + width: 100%; + height: 3.625rem; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.8rem; + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + `, + roomInfo: styled.div` + display: flex; + flex-direction: column; + align-items: center; + `, + roomName: styled.p` + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + `, + latestTime: styled.p` + color: var(--Text-gray, #666668); + font-family: 'Noto Sans KR'; + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + `, + menu: styled.div` + width: 1rem; + height: 1rem; + flex-shrink: 0; + background: url('kebab-horizontal.svg') no-repeat; + cursor: pointer; + `, + messageContainer: styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; + width: 100%; + height: calc(100% - 7.5rem); + box-shadow: 0px -1px 0px 0px #e5e5ea inset; + position: relative; + + &::-webkit-scrollbar { + width: 0.5rem; + } + &::-webkit-scrollbar-thumb { + background-color: #ced3da; + border-radius: 4px; + } + `, + senderFrame: styled.div` + display: flex; + justify-content: flex-end; + padding-right: 0.8rem; + margin: 0.8rem 0; + `, + receiverFrame: styled.div` + display: flex; + justify-content: flex-start; + padding-left: 0.8rem; + margin: 0.8rem 0; + `, + messageInput: styled.div` + width: 100%; + height: 3rem; + display: flex; + `, + messageInputField: styled.input` + flex: 1; + font-size: 1.25rem; + padding: 0.8rem; + border: none; + color: var(--Text-grayDark, #2c2c2e); + font-family: 'Noto Sans KR'; + font-size: 0.875rem; + font-style: normal; + font-weight: 400; + line-height: 1.125rem; + + &:focus { + outline: none; + } + `, + backButton: styled.img` + width: 1rem; + height: 1rem; + cursor: pointer; + `, +}; + +interface Content { + roomId: number; + message: string; + sender: string; + nickname: string; +} + +function calTimeDiff(time: string, type: string) { + const lastTime = new Date(time); + if (type === 'server') lastTime.setHours(lastTime.getHours() + 9); + const currentTime = new Date(); + const timeDiff = Math.floor( + (currentTime.getTime() - lastTime.getTime()) / (1000 * 60), + ); + + if (timeDiff < 60) return `${timeDiff}분 전`; + + if (timeDiff < 60 * 24) return `${Math.floor(timeDiff / 60)}시간 전`; + + return `${Math.floor(timeDiff / (60 * 24))}일 전`; +} + +export function MobileChattingRoom({ + userId, + userName, + roomId, + roomName, + lastTime, + onRoomClick, +}: { + userId: string | undefined; + userName: string; + roomId: number; + roomName: string; + lastTime: string; + onRoomClick: React.Dispatch>; +}) { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [stompClient, setStompClient] = useState(null); + const [isMenuClick, setIsMenuClick] = useState(false); + const [isBackClick, setIsBackClick] = useState(false); + + const auth = useAuthValue(); + const user = userId; + const [roomData, setRoomData] = useState< + | [ + { + messageId: string; + sender: string; + message: string; + createdAt: string; + nickname: string; + }, + ] + | undefined + >(); + + const chattingRoom = useEnterChatRoom(roomId, 0, 10); + useEffect(() => { + if (chattingRoom != null) { + setRoomData(chattingRoom.data?.data); + } + }, [chattingRoom]); + + const [time, setTime] = useState(lastTime); + const [type, setType] = useState('server'); + + useEffect(() => { + const initializeChat = async () => { + try { + const stomp = new Client({ + brokerURL: `ws://ec2-54-180-133-123.ap-northeast-2.compute.amazonaws.com:8080/ws`, + connectHeaders: { + Authorization: `Bearer ${auth?.accessToken}`, + }, + debug: (str: string) => { + console.log(str); + }, + reconnectDelay: 5000, + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + }); + setStompClient(stomp); + stomp.activate(); + + stomp.onConnect = () => { + console.log('WebSocket 연결이 열렸습니다.'); + stomp.subscribe(`/room/${roomId}`, frame => { + try { + setTime(new Date().toISOString()); + setType('client'); + const parsedMessage = JSON.parse(frame.body); + setMessages(prevMessages => [...prevMessages, parsedMessage]); + } catch (error) { + console.error('오류가 발생했습니다:', error); + } + }); + }; + } catch (error) { + console.error('채팅 룸 생성 중 오류가 발생했습니다:', error); + } + }; + + if (auth?.accessToken != null) { + void initializeChat(); + } + + return () => { + if (stompClient != null && stompClient.connected) { + void stompClient.deactivate(); + } + }; + }, [auth?.accessToken]); + + const sendMessage = () => { + if (stompClient !== null && stompClient.connected) { + const destination = `/send/${roomId}`; + + stompClient.publish({ + destination, + body: JSON.stringify({ + roomId: roomId, + sender: user, + message: inputMessage, + nickname: userName, + }), + }); + } + + setInputMessage(''); + }; + + const handleMenuClick = () => { + setIsMenuClick(prev => !prev); + }; + + const { mutate: exit } = useExitChatRoom(roomId); + + const handleBackClick = () => { + exit(); + setIsBackClick(prev => !prev); + onRoomClick(isBackClick); + }; + + function handleKeyUp(event: React.KeyboardEvent) { + if (event.keyCode === 13) { + sendMessage(); + } + } + + const messageContainerRef = useRef(null); + + useEffect(() => { + if (messageContainerRef.current != null) { + messageContainerRef.current.scrollTop = + messageContainerRef.current.scrollHeight; + } + }, [roomData]); + + useEffect(() => { + if (messageContainerRef.current != null) { + messageContainerRef.current.scrollTop = + messageContainerRef.current.scrollHeight; + } + }, [messages]); + + return ( + + + + + {roomName} + {calTimeDiff(time, type)} + + + {isMenuClick && ( + + )} + + + {roomData + ?.slice() + .reverse() + ?.map((message, index) => ( +
+ {message.sender === userId ? ( + + + + ) : ( + + + + )} +
+ ))} + {messages.map((message, index) => ( +
+ {message.sender === userId ? ( + + + + ) : ( + + + + )} +
+ ))} +
+ + { + setInputMessage(e.target.value); + }} + onKeyUp={handleKeyUp} + /> + +
+ ); +} From 1a9f51c44a6760661810dc230736d5cb7a9f882d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 02:01:09 +0900 Subject: [PATCH 112/130] feat: Change pointer appearance when hover, redirect profile page when user profile image clicked. (#75) --- src/app/pages/shared-post-page.tsx | 32 ++++++++++++++++++------ src/components/shared-posts/PostCard.tsx | 25 +++++++++++++++--- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index 15eff7f737..fb47d65486 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -449,14 +449,6 @@ export function SharedPostPage({ const { mutate: deleteSharedPost } = useDeleteSharedPost(); - const [selected, setSelected] = useState< - | { - memberId: string; - profileImage: string; - } - | undefined - >(undefined); - const { isLoading: isSharedPostLoading, data: sharedPost } = useSharedPost({ postId, enabled: type === 'hasRoom' && auth?.accessToken != null, @@ -468,6 +460,21 @@ export function SharedPostPage({ enabled: type === 'dormitory' && auth?.accessToken != null, }); + const [selected, setSelected] = useState< + | { + memberId: string; + profileImage: string; + } + | undefined + >( + sharedPost != null + ? { + memberId: sharedPost.data.publisherAccount.memberId, + profileImage: sharedPost.data.publisherAccount.profileImageFileName, + } + : undefined, + ); + const { data: userData } = useUserData(auth?.accessToken != null); const [userId, setUserId] = useState(''); @@ -493,6 +500,15 @@ export function SharedPostPage({ sharedPost?.data.publisherAccount.memberId ?? '', ); + useEffect(() => { + if (selected != null || sharedPost == null) return; + + setSelected({ + memberId: sharedPost.data.publisherAccount.memberId, + profileImage: sharedPost.data.publisherAccount.profileImageFileName, + }); + }, [selected, sharedPost]); + useEffect(() => { if (sharedPost?.data.address.roadAddress != null) { fromAddrToCoord({ query: sharedPost?.data.address.roadAddress }).then( diff --git a/src/components/shared-posts/PostCard.tsx b/src/components/shared-posts/PostCard.tsx index 7cd8022cd5..be8c02eeb9 100644 --- a/src/components/shared-posts/PostCard.tsx +++ b/src/components/shared-posts/PostCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useRouter } from 'next/navigation'; import styled from 'styled-components'; import { HorizontalDivider } from '@/components'; @@ -24,6 +25,8 @@ const styles = { border-radius: 16px; object-fit: cover; + + cursor: pointer; `, content: styled.div` flex-grow: 1; @@ -44,6 +47,8 @@ const styles = { font-style: normal; font-weight: 700; line-height: normal; + + cursor: pointer; } h2 { @@ -68,6 +73,8 @@ const styles = { writer: styled.div` position: relative; + cursor: pointer; + display: flex; flex-shrink: 0; flex-direction: column; @@ -129,6 +136,8 @@ export function PostCard({ post: SharedPostListItem | DormitorySharedPostListItem; onClick: () => void; }) { + const router = useRouter(); + const recruitmentCapacity = 'roomInfo' in post ? post.roomInfo.recruitmentCapacity @@ -136,9 +145,13 @@ export function PostCard({ return (
- - - + + +

{post.title}

{post.address.roadAddress}

@@ -156,7 +169,11 @@ export function PostCard({ )}
- + { + router.push(`/profile/${post.publisherAccount.memberId}`); + }} + >

50%

From 45d3d1ff18daff826192d2855b26fe354f6349ac Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 02:13:56 +0900 Subject: [PATCH 113/130] feat: Add InitializationProvider --- src/app/layout.tsx | 7 +++-- .../lib/providers/InitializationProvider.tsx | 30 +++++++++++++++++++ src/app/lib/providers/index.ts | 1 + src/app/pages/main-page.tsx | 17 +---------- src/app/pages/shared-posts-page.tsx | 13 +------- 5 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 src/app/lib/providers/InitializationProvider.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cfcc5e8a94..fc4ca1d38d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import './globals.scss'; import { AuthProvider, + InitialzationProvider, RecoilRootProvider, StyledComponentsRegistry, TanstackQueryProvider, @@ -39,8 +40,10 @@ export default function RootLayout({ -
{children}
- + +
{children}
+ +
diff --git a/src/app/lib/providers/InitializationProvider.tsx b/src/app/lib/providers/InitializationProvider.tsx new file mode 100644 index 0000000000..0acae5e823 --- /dev/null +++ b/src/app/lib/providers/InitializationProvider.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; + +import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; + +export function InitialzationProvider({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + + const auth = useAuthValue(); + const { setAuthUserData } = useAuthActions(); + + const { data: userData } = useUserData(auth?.accessToken != null); + + useEffect(() => { + if (userData != null) { + setAuthUserData(userData); + if (userData.initialized) { + router.replace('/profile'); + } + } + }, [userData, router, setAuthUserData]); + + return <>{children}; +} diff --git a/src/app/lib/providers/index.ts b/src/app/lib/providers/index.ts index ab1cfd8961..6622941879 100644 --- a/src/app/lib/providers/index.ts +++ b/src/app/lib/providers/index.ts @@ -1,4 +1,5 @@ export * from './AuthProvider'; +export * from './InitializationProvider'; export * from './RecoilRootProvider'; export * from './StyledComponentsRegistry'; export * from './TanstackQueryProvider'; diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index 6fa40ae057..19f4553cfc 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -1,13 +1,12 @@ 'use client'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { CircularButton } from '@/components'; import { UserCard } from '@/components/main-page'; -import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; +import { useAuthValue } from '@/features/auth'; import { getGeolocation } from '@/features/geocoding'; import { useRecommendationMate } from '@/features/recommendation'; @@ -87,12 +86,7 @@ const styles = { }; export function MainPage() { - const router = useRouter(); - const auth = useAuthValue(); - const { setAuthUserData } = useAuthActions(); - - const { data: userData } = useUserData(auth?.accessToken !== undefined); const { data: recommendationMates } = useRecommendationMate({ memberId: auth?.user?.memberId ?? 'undefined', @@ -138,15 +132,6 @@ export function MainPage() { }); }, []); - useEffect(() => { - if (userData !== undefined) { - setAuthUserData(userData); - if (userData.initialized) { - // router.replace('/profile'); - } - } - }, [userData, router, setAuthUserData]); - return ( diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index f5f547ab4e..42b4a7ef86 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -16,7 +16,7 @@ import { useSharedPostsFilter, type SharedPostsType, } from '@/entities/shared-posts-filter'; -import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; +import { useAuthValue } from '@/features/auth'; import { useRecommendationMate } from '@/features/recommendation'; import { useDormitorySharedPosts, @@ -124,10 +124,8 @@ export function SharedPostsPage() { const [prevSharedPosts, setPrevSharedPosts] = useState< GetSharedPostsDTO | GetDormitorySharedPostsDTO | null >(null); - const { setAuthUserData } = useAuthActions(); const { filter, derivedFilter, reset: resetFilter } = useSharedPostsFilter(); - const { data: userData } = useUserData(auth?.accessToken != null); const { page, @@ -183,15 +181,6 @@ export function SharedPostsPage() { } }, [selected, dormitorySharedPosts, sharedPosts]); - useEffect(() => { - if (userData != null) { - setAuthUserData(userData); - if (userData.initialized) { - // router.replace('/profile'); - } - } - }, [userData, router, setAuthUserData]); - return ( From 7bd662cf03fd4d727763c737554f90458c1eaeeb Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 14:48:42 +0900 Subject: [PATCH 114/130] feat: Add axios-interceptor --- src/app/layout.tsx | 1 + src/app/lib/axiso-interceptor.ts | 54 ++++++++++++++++++++++++++ src/app/lib/providers/AuthProvider.tsx | 4 +- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/app/lib/axiso-interceptor.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fc4ca1d38d..2af585cc9d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import React from 'react'; import './globals.scss'; +import './lib/axiso-interceptor'; import { AuthProvider, diff --git a/src/app/lib/axiso-interceptor.ts b/src/app/lib/axiso-interceptor.ts new file mode 100644 index 0000000000..4a95f5b3ef --- /dev/null +++ b/src/app/lib/axiso-interceptor.ts @@ -0,0 +1,54 @@ +'use client'; + +import axios, { type AxiosRequestConfig, isAxiosError } from 'axios'; +import { useRouter } from 'next/navigation'; + +import { postTokenRefresh, useAuthActions } from '@/features/auth'; +import { load } from '@/shared/storage'; + +interface AxiosRequestConfigWithRetryCount extends AxiosRequestConfig { + retryCount?: number; +} + +axios.interceptors.response.use( + response => response, + async error => { + if (!isAxiosError(error)) return await Promise.reject(error); + + const router = useRouter(); + const { login, logout } = useAuthActions(); + + const refreshToken = load({ type: 'local', key: 'refreshToken' }); + const config = error.config as AxiosRequestConfigWithRetryCount; + if ( + error.response?.status === 401 && + refreshToken != null && + (config?.retryCount ?? 0) < 3 + ) { + config.retryCount = (config?.retryCount ?? 0) + 1; + try { + const response = await postTokenRefresh(refreshToken); + + const token = response.data.accessToken; + const newConfig = { + ...config, + headers: { + ...config.headers, + Authorization: `Bearer ${token}`, + }, + }; + + login(response.data); + return await axios(newConfig); + } catch (refreshError) { + logout(); + router.replace('/'); + return await Promise.reject(refreshError); + } + } + + logout(); + router.replace('/'); + return await Promise.reject(error); + }, +); diff --git a/src/app/lib/providers/AuthProvider.tsx b/src/app/lib/providers/AuthProvider.tsx index f7cb64a8bb..fb7ac43486 100644 --- a/src/app/lib/providers/AuthProvider.tsx +++ b/src/app/lib/providers/AuthProvider.tsx @@ -2,7 +2,7 @@ import { isAxiosError } from 'axios'; import { usePathname, useRouter } from 'next/navigation'; -import { useLayoutEffect, useState, useCallback } from 'react'; +import { useCallback, useLayoutEffect, useState } from 'react'; import { getUserData, @@ -82,7 +82,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useLayoutEffect(() => { checkAndRefreshToken(); - }, [checkAndRefreshToken]); + }); if (pathName !== '/' && pathName !== '/login' && (isLoading || auth == null)) return <>; From d7c7d3a0547360cb722c264aa977a8336c0bb808 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Wed, 15 May 2024 15:56:36 +0900 Subject: [PATCH 115/130] feat: mobile filter layout, apply naver map api (#79) --- src/app/pages/setting-page.tsx | 8 +- src/app/pages/user-input-page.tsx | 9 +- src/components/UserInputSection.tsx | 1 + src/components/card/OptionSection.tsx | 6 +- src/components/card/VitalSection.tsx | 145 ++++++++++++++---- .../shared-posts/SharedPostFilterItem.tsx | 4 + .../shared-posts/filter/DealTypeFilter.tsx | 14 ++ .../shared-posts/filter/ExtraInfoFilter.tsx | 15 ++ .../shared-posts/filter/MateCardFilter.tsx | 10 ++ .../shared-posts/filter/RoomTypeFilter.tsx | 15 ++ 10 files changed, 193 insertions(+), 34 deletions(-) diff --git a/src/app/pages/setting-page.tsx b/src/app/pages/setting-page.tsx index 9b42848ac5..a13fd0624b 100644 --- a/src/app/pages/setting-page.tsx +++ b/src/app/pages/setting-page.tsx @@ -70,10 +70,11 @@ const styles = { width: 18.375rem; flex-direction: column; align-items: flex-start; - gap: 1rem; + gap: 1.2rem; `, miniCardList: styled.li` display: flex; + height: 2rem; align-items: center; gap: 2rem; align-self: stretch; @@ -108,7 +109,9 @@ const styles = { `, miniCardText: styled.p` flex: 1 0 0; - height: 1.5rem; + display: flex; + height: 3rem; + align-items: center; font-family: 'Noto Sans KR'; font-size: 1rem; font-style: normal; @@ -214,7 +217,6 @@ export function SettingPage({ cardId }: { cardId: number }) { if (option.includes('E') || option.includes('I')) setInitialMbti(option); if (option.includes(',')) { - console.log(option); setInitialBudget(option); } if (majorArray.includes(option)) setInitialMajor(option); diff --git a/src/app/pages/user-input-page.tsx b/src/app/pages/user-input-page.tsx index cff2fafdaa..006c750348 100644 --- a/src/app/pages/user-input-page.tsx +++ b/src/app/pages/user-input-page.tsx @@ -93,11 +93,12 @@ const styles = { width: 18.375rem; flex-direction: column; align-items: flex-start; - gap: 1rem; + gap: 1.2rem; `, miniCardList: styled.li` display: flex; - align-items: flex-start; + height: 2rem; + align-items: center; gap: 2rem; align-self: stretch; `, @@ -143,7 +144,9 @@ const styles = { `, miniCardText: styled.p` flex: 1 0 0; - height: 1.5rem; + display: flex; + height: 3rem; + align-items: center; font-family: 'Noto Sans KR'; font-size: 1rem; font-style: normal; diff --git a/src/components/UserInputSection.tsx b/src/components/UserInputSection.tsx index c9f41a2e70..a339684ac2 100644 --- a/src/components/UserInputSection.tsx +++ b/src/components/UserInputSection.tsx @@ -8,6 +8,7 @@ import { VitalSection } from './card/VitalSection'; const styles = { checkContainer: styled.div` display: flex; + position: relative; width: 50rem; padding: 2rem; flex-direction: column; diff --git a/src/components/card/OptionSection.tsx b/src/components/card/OptionSection.tsx index cb6b01164b..96c83394a0 100644 --- a/src/components/card/OptionSection.tsx +++ b/src/components/card/OptionSection.tsx @@ -8,6 +8,8 @@ import { MajorSelector } from './MajorSelector'; import { MbtiToggle } from './MbtiToggle'; import { Slider } from './Slider'; +import { useIsMobile } from '@/shared/mobile'; + const styles = { container: styled.div` width: 46rem; @@ -363,6 +365,8 @@ export function OptionSection({ setIsMbtiSelected(!isMbtiSelected); }; + const isMobile = useIsMobile(); + return ( 선택 @@ -582,7 +586,7 @@ export function OptionSection({ style={{ color: '#888', fontFamily: 'Noto Sans KR', - fontSize: '0.475rem', + fontSize: isMobile ? '0.475rem' : '1rem', fontStyle: 'normal', fontWeight: '500', lineHeight: 'normal', diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 05f99958ca..752b6f5328 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -3,6 +3,9 @@ import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { fromAddrToCoord, type NaverAddress } from '@/features/geocoding'; +import { useIsMobile } from '@/shared/mobile'; + const styles = { vitalContainer: styled.div` display: flex; @@ -31,6 +34,7 @@ const styles = { `, vitalListContainer: styled.ul` display: flex; + width: 100%; flex-direction: column; align-items: flex-start; gap: 1rem; @@ -38,6 +42,7 @@ const styles = { `, vitalList: styled.li` display: flex; + width: 100%; align-items: center; gap: 2rem; align-self: stretch; @@ -209,6 +214,43 @@ const styles = { } } `, + + searchAddrContainer: styled.div` + min-width: 25rem; + min-height: 10rem; + display: flex; + position: absolute; + left: 15dvw; + top: 30dvh; + flex-direction: column; + z-index: 200; + background-color: white; + padding: 2rem; + gap: 1rem; + border-radius: 20px; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); + + @media (max-width: 768px) { + top: 10.5rem; + left: 5.5rem; + padding: 1rem; + gap: 0.5rem; + min-width: 15rem; + } + `, + searchResults: styled.p` + color: var(--Main-2, #767d86); + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + width: 100%; + + @media (max-width: 768px) { + font-size: 0.625rem; + } + `, }; interface CheckItemProps { @@ -321,7 +363,11 @@ export function VitalSection({ [onFeatureChange], ); + const [searchText, setSearchText] = useState(''); + const [addresses, setAddresses] = useState([]); const [initialLocation, setInitialLocation] = useState(''); + const [locationBoxClick, setLocationBoxClick] = useState(false); + useEffect(() => { if (location !== undefined && type === 'myCard') { setInitialLocation(location); @@ -333,10 +379,6 @@ export function VitalSection({ setLocation(initialLocation); }, [initialLocation]); - const handleLocationChange = (event: React.ChangeEvent) => { - setLocation(event.target.value); - }; - useEffect(() => { onLocationChange(locationInput); }, [onLocationChange, locationInput]); @@ -372,6 +414,8 @@ export function VitalSection({ ageValueString = `±${ageValue}년생`; } + const isMobile = useIsMobile(); + return ( 필수 @@ -404,29 +448,76 @@ export function VitalSection({ - - 희망 지역 - - {type === 'myCard' ? ( - - - - ) : ( - - {location} - +
{ + event.preventDefault(); + fromAddrToCoord({ query: searchText }) + .then(response => { + setAddresses(response.data.addresses); + }) + .catch((error: Error) => { + console.log(error); + }); + }} + > + + 희망 지역 + + {type === 'myCard' ? ( +
+ + { + setSearchText(event.target.value); + }} + onClick={() => { + setLocationBoxClick(prev => !prev); + }} + /> + + {isMobile && locationInput != null ? ( + + {locationInput} + + ) : null} +
+ ) : ( + + {location} + + )} +
+ {locationBoxClick && ( + + {addresses.map(address => ( + { + setLocationBoxClick(false); + setLocation(address.roadAddress); + }} + > + {address.roadAddress} + + ))} + )}
diff --git a/src/components/shared-posts/SharedPostFilterItem.tsx b/src/components/shared-posts/SharedPostFilterItem.tsx index f7be157569..075d0a8477 100644 --- a/src/components/shared-posts/SharedPostFilterItem.tsx +++ b/src/components/shared-posts/SharedPostFilterItem.tsx @@ -58,6 +58,10 @@ const styles = { z-index: 10; padding: 2.31rem; + + @media (max-width: 768px) { + padding: 1.8rem; + } `, }; diff --git a/src/components/shared-posts/filter/DealTypeFilter.tsx b/src/components/shared-posts/filter/DealTypeFilter.tsx index ca68dc91ba..e7b6ec7625 100644 --- a/src/components/shared-posts/filter/DealTypeFilter.tsx +++ b/src/components/shared-posts/filter/DealTypeFilter.tsx @@ -20,6 +20,11 @@ const styles = { background: #fff; width: 20rem; + + @media (max-width: 768px) { + width: 10rem; + gap: 1rem; + } `, item: styled.div` width: 100%; @@ -35,6 +40,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 1rem; + } } div { @@ -62,6 +71,11 @@ const styles = { font-weight: 400; line-height: normal; + @media (max-width: 768px) { + font-size: 0.75rem; + padding: 0.5rem 1rem; + } + transition: 150ms border ease-in-out, 150ms background-color ease-in-out, diff --git a/src/components/shared-posts/filter/ExtraInfoFilter.tsx b/src/components/shared-posts/filter/ExtraInfoFilter.tsx index d2263e4433..35088b18a8 100644 --- a/src/components/shared-posts/filter/ExtraInfoFilter.tsx +++ b/src/components/shared-posts/filter/ExtraInfoFilter.tsx @@ -15,6 +15,12 @@ const styles = { align-items: flex-start; gap: 1rem; + @media (max-width: 768px) { + width: 12rem; + flex-wrap: wrap; + gap: 0.5rem; + } + h1 { color: #000; font-family: 'Noto Sans KR'; @@ -22,6 +28,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 1rem; + } } div { @@ -52,6 +62,11 @@ const styles = { font-weight: 400; line-height: normal; + @media (max-width: 768px) { + font-size: 0.75rem; + padding: 0.5rem 1rem; + } + transition: 150ms border ease-in-out, 150ms background-color ease-in-out, diff --git a/src/components/shared-posts/filter/MateCardFilter.tsx b/src/components/shared-posts/filter/MateCardFilter.tsx index 3a6db66bf4..ce3795a667 100644 --- a/src/components/shared-posts/filter/MateCardFilter.tsx +++ b/src/components/shared-posts/filter/MateCardFilter.tsx @@ -19,6 +19,11 @@ const styles = { font-weight: 400; line-height: normal; + @media (max-width: 768px) { + font-size: 0.875rem; + gap: 0.5rem; + } + button { all: unset; @@ -27,6 +32,11 @@ const styles = { transition: 200ms background-color ease-in-out; border-radius: 8px; min-width: 15rem; + + @media (max-width: 768px) { + min-width: 10rem; + padding: 0.5rem; + } } button:hover { diff --git a/src/components/shared-posts/filter/RoomTypeFilter.tsx b/src/components/shared-posts/filter/RoomTypeFilter.tsx index 2efa88c5ae..def0d5d586 100644 --- a/src/components/shared-posts/filter/RoomTypeFilter.tsx +++ b/src/components/shared-posts/filter/RoomTypeFilter.tsx @@ -20,6 +20,11 @@ const styles = { width: 30rem; + @media (max-width: 768px) { + gap: 1rem; + width: 18rem; + } + h1 { color: #000; font-family: 'Noto Sans KR'; @@ -27,6 +32,10 @@ const styles = { font-style: normal; font-weight: 500; line-height: normal; + + @media (max-width: 768px) { + font-size: 1rem; + } } button { @@ -42,6 +51,11 @@ const styles = { border-radius: 8px; border: 1px solid #000; + @media (max-width: 768px) { + font-size: 0.75rem; + padding: 0.5rem 1rem; + } + transition: 150ms border ease-in-out, 150ms background-color ease-in-out, @@ -66,6 +80,7 @@ const styles = { display: flex; align-items: flex-start; gap: 0.5rem; + flex-wrap: wrap; } `, livingRoom: styled.div` From 3260e10c28cbdf6a8e2b7cbbdd9f2e1037d02260 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 16:12:31 +0900 Subject: [PATCH 116/130] feat: Show roommate features in shared-post page (#75) --- src/app/pages/shared-post-page.tsx | 84 ++++++++++++++++++- src/components/CircularProfileImage.tsx | 6 -- .../shared-post-page/CardToggleButton.tsx | 33 +++++++- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index fb47d65486..42fc1606a3 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { Bookmark, CircularProfileImage } from '@/components'; -import { ImageGrid } from '@/components/shared-post-page'; +import { CardToggleButton, ImageGrid } from '@/components/shared-post-page'; import { useAuthValue, useUserData } from '@/features/auth'; import { useCreateChatRoom } from '@/features/chat'; import { fromAddrToCoord } from '@/features/geocoding'; @@ -430,6 +430,45 @@ const styles = { font-weight: 600; line-height: 1.5rem; `, + mateCardContainer: styled.div` + display: flex; + padding: 1rem; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + + border-radius: 1rem; + background: #fff; + + color: #000; + font-family: 'Noto Sans KR'; + font-size: 1rem; + font-style: normal; + line-height: normal; + + .content { + display: flex; + width: 100%; + flex-direction: column; + padding-inline: calc(24px + 0.75rem); + gap: 1rem; + + font-size: 0.9rem; + + div { + display: flex; + flex-direction: column; + gap: 0.5rem; + + h3 { + font-size: 1rem; + font-weight: bold; + } + } + } + `, }; export function SharedPostPage({ @@ -551,6 +590,13 @@ export function SharedPostPage({ [type, sharedPost, dormitorySharedPost], ); + const [showMateCard, setShowMateCard] = useState(false); + + let mateAge: string; + if (post?.data.roomMateFeatures.mateAge == null) mateAge = '상관없어요'; + else if (post?.data.roomMateFeatures.mateAge === '0') mateAge = '동갑'; + else mateAge = `±${post.data.roomMateFeatures.mateAge}`; + if (isLoading || post == null) return <>; return ( @@ -713,10 +759,9 @@ export function SharedPostPage({

{post.data.publisherAccount.birthYear != null - ? getAge(+post.data.publisherAccount.birthYear) + ? `${getAge(+post.data.publisherAccount.birthYear)}세` : new Date().getFullYear()}

-

{post.data.address.roadAddress}

@@ -743,6 +788,39 @@ export function SharedPostPage({
+ + { + setShowMateCard(prev => !prev); + }} + /> + {showMateCard && ( +
+
+

흡연 여부

+

{post.data.roomMateFeatures.smoking}

+
+
+

메이트와 방 공유 여부

+

{post.data.roomMateFeatures.roomSharingOption}

+
+
+

나이

+

{mateAge}

+
+
+

선택 옵션

+

+ {( + JSON.parse(post.data.roomMateFeatures.options) as string[] + ).join(', ')} +

+
+
+ )} +
diff --git a/src/components/CircularProfileImage.tsx b/src/components/CircularProfileImage.tsx index 8d5f687201..e22b497b30 100644 --- a/src/components/CircularProfileImage.tsx +++ b/src/components/CircularProfileImage.tsx @@ -74,12 +74,6 @@ export function CircularProfileImage({ return ( - - {percentage}% ); } diff --git a/src/components/shared-post-page/CardToggleButton.tsx b/src/components/shared-post-page/CardToggleButton.tsx index c468a58ef6..d9608602f4 100644 --- a/src/components/shared-post-page/CardToggleButton.tsx +++ b/src/components/shared-post-page/CardToggleButton.tsx @@ -16,6 +16,18 @@ const styles = { line-height: normal; cursor: pointer; + + align-items: center; + + div { + display: flex; + align-items: center; + } + + user-select: none; + -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; `, }; @@ -33,10 +45,25 @@ function ToggleIcon() { ); } -export function CardToggleButton({ label }: { label: string }) { +export function CardToggleButton({ + label, + isOpen, + onClick, +}: { + label: string; + isOpen: boolean; + onClick: () => void; +}) { return ( - - + +
+ +
{label}
); From d7635eece6cb5af8e98eaf37732d18d8675ac930 Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Wed, 15 May 2024 23:59:19 +0900 Subject: [PATCH 117/130] fix: Remove axios-interceptor.ts --- src/app/layout.tsx | 1 - src/app/lib/axiso-interceptor.ts | 54 ----------------------------- src/app/pages/main-page.tsx | 2 +- src/app/pages/shared-posts-page.tsx | 2 +- 4 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 src/app/lib/axiso-interceptor.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2af585cc9d..fc4ca1d38d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,6 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import React from 'react'; import './globals.scss'; -import './lib/axiso-interceptor'; import { AuthProvider, diff --git a/src/app/lib/axiso-interceptor.ts b/src/app/lib/axiso-interceptor.ts deleted file mode 100644 index 4a95f5b3ef..0000000000 --- a/src/app/lib/axiso-interceptor.ts +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import axios, { type AxiosRequestConfig, isAxiosError } from 'axios'; -import { useRouter } from 'next/navigation'; - -import { postTokenRefresh, useAuthActions } from '@/features/auth'; -import { load } from '@/shared/storage'; - -interface AxiosRequestConfigWithRetryCount extends AxiosRequestConfig { - retryCount?: number; -} - -axios.interceptors.response.use( - response => response, - async error => { - if (!isAxiosError(error)) return await Promise.reject(error); - - const router = useRouter(); - const { login, logout } = useAuthActions(); - - const refreshToken = load({ type: 'local', key: 'refreshToken' }); - const config = error.config as AxiosRequestConfigWithRetryCount; - if ( - error.response?.status === 401 && - refreshToken != null && - (config?.retryCount ?? 0) < 3 - ) { - config.retryCount = (config?.retryCount ?? 0) + 1; - try { - const response = await postTokenRefresh(refreshToken); - - const token = response.data.accessToken; - const newConfig = { - ...config, - headers: { - ...config.headers, - Authorization: `Bearer ${token}`, - }, - }; - - login(response.data); - return await axios(newConfig); - } catch (refreshError) { - logout(); - router.replace('/'); - return await Promise.reject(refreshError); - } - } - - logout(); - router.replace('/'); - return await Promise.reject(error); - }, -); diff --git a/src/app/pages/main-page.tsx b/src/app/pages/main-page.tsx index 19f4553cfc..6a49c86dd0 100644 --- a/src/app/pages/main-page.tsx +++ b/src/app/pages/main-page.tsx @@ -91,7 +91,7 @@ export function MainPage() { const { data: recommendationMates } = useRecommendationMate({ memberId: auth?.user?.memberId ?? 'undefined', cardType: 'mate', - enabled: auth?.accessToken != null, + enabled: auth?.accessToken != null && false, }); const [map, setMap] = useState(null); diff --git a/src/app/pages/shared-posts-page.tsx b/src/app/pages/shared-posts-page.tsx index 42b4a7ef86..698868fef5 100644 --- a/src/app/pages/shared-posts-page.tsx +++ b/src/app/pages/shared-posts-page.tsx @@ -161,7 +161,7 @@ export function SharedPostsPage() { const { data: recommendationMates } = useRecommendationMate({ memberId: auth?.user?.memberId ?? 'undefined', cardType: filter.cardType ?? 'mate', - enabled: auth?.accessToken != null && selected === 'homeless', + enabled: auth?.accessToken != null && selected === 'homeless' && false, }); useEffect(() => { From 774ac9963a0675755514cb2dcae8c50b62c0315f Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Thu, 16 May 2024 14:05:45 +0900 Subject: [PATCH 118/130] feat: Apply API changes (#75) --- src/app/pages/shared-post-page.tsx | 127 +++++++++---------- src/entities/shared-post/shared-post.type.ts | 11 +- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/app/pages/shared-post-page.tsx b/src/app/pages/shared-post-page.tsx index 42fc1606a3..5c798016be 100644 --- a/src/app/pages/shared-post-page.tsx +++ b/src/app/pages/shared-post-page.tsx @@ -9,11 +9,7 @@ import { CardToggleButton, ImageGrid } from '@/components/shared-post-page'; import { useAuthValue, useUserData } from '@/features/auth'; import { useCreateChatRoom } from '@/features/chat'; import { fromAddrToCoord } from '@/features/geocoding'; -import { - useFollowUser, - useFollowingListData, - useUnfollowUser, -} from '@/features/profile'; +import { useFollowUser, useUnfollowUser } from '@/features/profile'; import { useDeleteSharedPost, useDormitorySharedPost, @@ -365,6 +361,7 @@ const styles = { padding: 0.5rem 1.5rem; justify-content: center; align-items: center; + align-self: stretch; gap: 0.25rem; flex: 1 0 0; @@ -499,21 +496,6 @@ export function SharedPostPage({ enabled: type === 'dormitory' && auth?.accessToken != null, }); - const [selected, setSelected] = useState< - | { - memberId: string; - profileImage: string; - } - | undefined - >( - sharedPost != null - ? { - memberId: sharedPost.data.publisherAccount.memberId, - profileImage: sharedPost.data.publisherAccount.profileImageFileName, - } - : undefined, - ); - const { data: userData } = useUserData(auth?.accessToken != null); const [userId, setUserId] = useState(''); @@ -523,31 +505,6 @@ export function SharedPostPage({ } }, [userData]); - const { mutate: scrapPost } = useScrapSharedPost(); - - const followList = useFollowingListData(); - const [isFollowed, setIsFollowed] = useState( - followList.data?.data.followingList[ - sharedPost?.data.publisherAccount.memberId ?? '' - ] != null, - ); - - const { mutate: follow } = useFollowUser( - sharedPost?.data.publisherAccount.memberId ?? '', - ); - const { mutate: unfollow } = useUnfollowUser( - sharedPost?.data.publisherAccount.memberId ?? '', - ); - - useEffect(() => { - if (selected != null || sharedPost == null) return; - - setSelected({ - memberId: sharedPost.data.publisherAccount.memberId, - profileImage: sharedPost.data.publisherAccount.profileImageFileName, - }); - }, [selected, sharedPost]); - useEffect(() => { if (sharedPost?.data.address.roadAddress != null) { fromAddrToCoord({ query: sharedPost?.data.address.roadAddress }).then( @@ -597,6 +554,41 @@ export function SharedPostPage({ else if (post?.data.roomMateFeatures.mateAge === '0') mateAge = '동갑'; else mateAge = `±${post.data.roomMateFeatures.mateAge}`; + const [selected, setSelected] = useState< + | { + memberId: string; + nickname: string; + profileImageFileName: string; + birthYear: string; + isScrapped: boolean; + } + | undefined + >(post != null ? post.data.participants[0] : undefined); + + useEffect(() => { + if (post == null || selected != null) return; + + setSelected(post.data.participants[0]); + }, [selected, post]); + + const { mutate: scrapPost } = useScrapSharedPost(); + + const { mutate: follow } = useFollowUser(selected?.memberId ?? ''); + const { mutate: unfollow } = useUnfollowUser(selected?.memberId ?? ''); + + const [followList, setFollowList] = useState>({}); + + useEffect(() => { + if (post == null) return; + + const { participants } = post.data; + const newFollowList: Record = {}; + participants.forEach(({ memberId, isScrapped }) => { + newFollowList[memberId] = isScrapped; + }); + setFollowList(newFollowList); + }, [post]); + if (isLoading || post == null) return <>; return ( @@ -730,36 +722,31 @@ export function SharedPostPage({ - {post.data.participants.map( - ({ memberId, profileImageFileName }, index) => ( - { - setSelected({ - memberId, - profileImage: profileImageFileName, - }); - }} - /> - ), - )} + {post.data.participants.map((participant, index) => ( + { + setSelected(participant); + }} + /> + ))} -

{post.data.publisherAccount.nickname}

+

{selected?.nickname}

- {post.data.publisherAccount.birthYear != null - ? `${getAge(+post.data.publisherAccount.birthYear)}세` + {selected?.birthYear != null + ? `${getAge(+selected.birthYear)}세` : new Date().getFullYear()}

@@ -776,11 +763,15 @@ export function SharedPostPage({
프로필 보기 { - if (isFollowed) unfollow(); + if ( + selected == null || + followList[selected.memberId] == null + ) + return; + if (followList[selected.memberId]) unfollow(); else follow(); - setIsFollowed(prev => !prev); }} hasBorder color="#888" diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index be3f35dc2b..36242aacd8 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -52,7 +52,13 @@ export interface SharedPost { mateAge?: string; options: string; // 프런트에서 파싱 필요. }; - participants: Array<{ memberId: string; profileImageFileName: string }>; + participants: Array<{ + memberId: string; + nickname: string; + profileImageFileName: string; + birthYear: string; + isScrapped: boolean; + }>; roomImages: Array<{ fileName: string; isThumbnail: boolean; @@ -149,7 +155,10 @@ export interface DormitorySharedPost { }; participants: Array<{ memberId: string; + nickname: string; profileImageFileName: string; + birthYear: string; + isScrapped: boolean; }>; roomImages: Array<{ fileName: string; From 6a1e1864ecbba25e442d9a7e9bf4b685bf7551f2 Mon Sep 17 00:00:00 2001 From: he2e2 Date: Thu, 16 May 2024 16:13:06 +0900 Subject: [PATCH 119/130] fix: mobile userCard, chattingBox (#79) --- src/app/layout.tsx | 6 +- src/app/pages/mobile/mobile-main-page.tsx | 56 ++++--------------- .../pages/mobile/mobile-shared-posts-page.tsx | 1 + src/components/FloatingChatting.tsx | 4 +- src/components/NavigationBar.tsx | 1 + src/components/card/VitalSection.tsx | 4 +- src/components/chat/MobileChattingBox.tsx | 36 +++++++++++- src/components/chat/MobileChattingRoom.tsx | 11 ++-- src/components/main-page/UserCard.tsx | 18 +++++- 9 files changed, 76 insertions(+), 61 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cfcc5e8a94..b48509d443 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -39,8 +39,10 @@ export default function RootLayout({ -
{children}
- +
+ {children} + +
diff --git a/src/app/pages/mobile/mobile-main-page.tsx b/src/app/pages/mobile/mobile-main-page.tsx index e3c99888d3..c368688b1d 100644 --- a/src/app/pages/mobile/mobile-main-page.tsx +++ b/src/app/pages/mobile/mobile-main-page.tsx @@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; -import { CircularButton } from '@/components'; import { UserCard } from '@/components/main-page'; import { useAuthActions, useAuthValue, useUserData } from '@/features/auth'; import { getGeolocation } from '@/features/geocoding'; @@ -16,7 +15,6 @@ const styles = { display: flex; width: 100vw; min-width: 390px; - height: 47.8125rem; padding-bottom: 2rem; flex-direction: column; align-items: center; @@ -83,18 +81,13 @@ const styles = { `, mateRecommendation: styled.div` display: flex; - width: 69%; - padding: 0.5rem 0rem; + width: 100%; + padding: 0.5rem 1rem; align-items: center; - gap: 0.25rem; + gap: 1rem 0.5rem; flex-shrink: 0; overflow-x: auto; - - -ms-overflow-style: none; - scrollbar-width: none; - scrollbar ::-webkit-scrollbar { - display: none; - } + flex-wrap: wrap; `, }; @@ -116,18 +109,6 @@ export function MobileMainPage() { const scrollRef = useRef(null); - const handleScrollRight = () => { - if (scrollRef.current !== null) { - scrollRef.current.scrollBy({ left: 300, behavior: 'smooth' }); - } - }; - - const handleScrollLeft = () => { - if (scrollRef.current !== null) { - scrollRef.current.scrollBy({ left: -300, behavior: 'smooth' }); - } - }; - useEffect(() => { getGeolocation({ onSuccess: position => { @@ -173,28 +154,13 @@ export function MobileMainPage() {

{auth?.user?.name}님의 추천 메이트

- - - - {recommendationMates?.map(({ name, similarity, userId }) => ( - - - - ))} - - - + + {recommendationMates?.map(({ name, similarity, userId }) => ( + + + + ))} + ); diff --git a/src/app/pages/mobile/mobile-shared-posts-page.tsx b/src/app/pages/mobile/mobile-shared-posts-page.tsx index 2d931740ae..c64c5a5bf2 100644 --- a/src/app/pages/mobile/mobile-shared-posts-page.tsx +++ b/src/app/pages/mobile/mobile-shared-posts-page.tsx @@ -127,6 +127,7 @@ const styles = { @media (max-width: 768px) { gap: 1rem 1rem; padding-left: 0; + justify-content: flex-start; } `, }; diff --git a/src/components/FloatingChatting.tsx b/src/components/FloatingChatting.tsx index 399482672a..62d0489683 100644 --- a/src/components/FloatingChatting.tsx +++ b/src/components/FloatingChatting.tsx @@ -25,7 +25,7 @@ const styles = { border-radius: 100px; background: #e15637; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); - z-index: 20000; + z-index: 3147483800; transition: transform 0.3s ease; cursor: pointer; @@ -55,7 +55,7 @@ const styles = { border-radius: 20px; background: #fff; box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.2); - z-index: 100; + z-index: 1000; -webkit-transition: 0.4s; transition: 0.4s; overflow: hidden; diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 8f05aaabd4..c6cadc3285 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -38,6 +38,7 @@ const styles = { min-width: 390px; width: 100vw; padding: 1rem 2rem; + z-index: 10000; } `, mobileMenuIcon: styled.div` diff --git a/src/components/card/VitalSection.tsx b/src/components/card/VitalSection.tsx index 752b6f5328..32201a63cc 100644 --- a/src/components/card/VitalSection.tsx +++ b/src/components/card/VitalSection.tsx @@ -220,8 +220,8 @@ const styles = { min-height: 10rem; display: flex; position: absolute; - left: 15dvw; - top: 30dvh; + left: 9rem; + top: 12rem; flex-direction: column; z-index: 200; background-color: white; diff --git a/src/components/chat/MobileChattingBox.tsx b/src/components/chat/MobileChattingBox.tsx index d6b054f918..240c6dc6aa 100644 --- a/src/components/chat/MobileChattingBox.tsx +++ b/src/components/chat/MobileChattingBox.tsx @@ -21,8 +21,26 @@ const styles = { flex-direction: column; align-items: center; position: absolute; - top: 4.5rem; - z-index: 100; + top: 0; + z-index: 20000; + `, + chattingHeader: styled.div` + display: inline-flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; + gap: 1rem; + width: 100%; + height: 3.25rem; + flex-shrink: 0; + background: var(--background, #f7f6f9); + `, + title: styled.span` + font-family: 'Baloo 2'; + font-size: 1.575rem; + font-style: normal; + font-weight: 700; + line-height: normal; `, chattingSection: styled.div` width: 100%; @@ -198,6 +216,20 @@ export function MobileChattingBox() { return ( <> + +
+ + maru{' '} + + + chat + +
+
+ + +
+
{/*
); diff --git a/src/entities/shared-post/shared-post.type.ts b/src/entities/shared-post/shared-post.type.ts index 36242aacd8..76157551c8 100644 --- a/src/entities/shared-post/shared-post.type.ts +++ b/src/entities/shared-post/shared-post.type.ts @@ -2,6 +2,7 @@ export interface SharedPostListItem { id: number; title: string; content: string; + score: number; thumbnail: { fileName: string; isThumbnail: boolean; @@ -113,6 +114,7 @@ export interface DormitorySharedPostListItem { id: number; title: string; content: string; + score: number; thumbnail: { fileName: string; isThumbNail: boolean; diff --git a/src/features/profile/profile.api.ts b/src/features/profile/profile.api.ts index 9ab4c61afd..2c55463523 100644 --- a/src/features/profile/profile.api.ts +++ b/src/features/profile/profile.api.ts @@ -1,13 +1,16 @@ import axios from 'axios'; import { - type PostUserProfileDTO, - type GetUserCardDTO, - type PutUserCardDTO, type GetFollowingListDTO, + type GetRecommendMatesDTO, + type GetUserCardDTO, type PostSearchDTO, + type PostUserProfileDTO, + type PutUserCardDTO, } from './profile.dto'; +import { type CardType } from '@/entities/shared-posts-filter'; + export const postUserProfile = async (memberId: string) => { const res = await axios.post(`/maru-api/profile`, { memberId, @@ -72,8 +75,8 @@ export const postSearchUser = async (email: string) => { export const postEmail = async (email: string, univName: string) => { const res = await axios.post(`/maru-api/profile/certificate`, { - email: email, - univName: univName, + email, + univName, }); return res.data; @@ -85,10 +88,17 @@ export const postCertificate = async ( code: number, ) => { const res = await axios.post(`/maru-api/profile/certificate`, { - email: email, - univName: univName, - code: code, + email, + univName, + code, }); return res.data; }; + +export const getRecommendMates = async (cardOption: CardType) => { + const res = await axios.get( + `/maru-api/profile/recommend?cardOption=${cardOption}`, + ); + return res.data; +}; diff --git a/src/features/profile/profile.dto.ts b/src/features/profile/profile.dto.ts index 6d3dca47a4..49d004ba41 100644 --- a/src/features/profile/profile.dto.ts +++ b/src/features/profile/profile.dto.ts @@ -76,3 +76,19 @@ export interface PostSearchDTO extends SuccessBaseDTO { profileImageUrl: string; }; } + +export interface GetRecommendMatesDTO extends SuccessBaseDTO { + data: Array<{ + memberId: string; + nickname: string; + profileImageUrl: string; + location: string; + options: { + smoking: string; + roomSharingOption: string; + mateAge: number; + options: string; // string[] 으로 파싱 필요 + }; + score: number; + }>; +} diff --git a/src/features/profile/profile.hook.ts b/src/features/profile/profile.hook.ts index 5447816352..21cecd0e6e 100644 --- a/src/features/profile/profile.hook.ts +++ b/src/features/profile/profile.hook.ts @@ -10,8 +10,11 @@ import { postUserProfile, postEmail, postCertificate, + getRecommendMates, } from './profile.api'; +import { type CardType } from '@/entities/shared-posts-filter'; + export const useUserProfile = (memberId: string) => useMutation({ mutationFn: async () => await postUserProfile(memberId), @@ -79,3 +82,16 @@ export const useCertification = ( mutationFn: async () => await postCertificate(email, univName, code), onSuccess: data => data.data, }); + +export const useRecommendMates = ({ + enabled, + cardOption, +}: { + enabled: boolean; + cardOption: CardType; +}) => + useQuery({ + queryKey: ['/api/profile/recommend', cardOption], + queryFn: async () => await getRecommendMates(cardOption), + enabled, + }); diff --git a/src/features/recommendation/index.ts b/src/features/recommendation/index.ts deleted file mode 100644 index 180f80df51..0000000000 --- a/src/features/recommendation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './recommendation.api'; -export * from './recommendation.hook'; diff --git a/src/features/recommendation/recommendation.api.ts b/src/features/recommendation/recommendation.api.ts deleted file mode 100644 index b5b6a9c7b8..0000000000 --- a/src/features/recommendation/recommendation.api.ts +++ /dev/null @@ -1,13 +0,0 @@ -import axios from 'axios'; - -import { type GetRecommendationMateDTO } from './recommendation.dto'; - -import { type CardType } from '@/entities/shared-posts-filter'; - -export const getRecommendationMate = async ( - memberId: string, - cardType: CardType, -) => - await axios.get( - `http://localhost:8000/recommendation/${memberId}/${cardType}`, - ); diff --git a/src/features/recommendation/recommendation.dto.ts b/src/features/recommendation/recommendation.dto.ts deleted file mode 100644 index 076698d4a4..0000000000 --- a/src/features/recommendation/recommendation.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type CardType } from '@/entities/shared-posts-filter'; - -export interface GetRecommendationMateDTO { - user: { userId: string; gender: string }; - recommendation: Array<{ - userId: string; - name: string; - similarity: number; - cardType: CardType; - }>; -} diff --git a/src/features/recommendation/recommendation.hook.ts b/src/features/recommendation/recommendation.hook.ts deleted file mode 100644 index a5a890ae4b..0000000000 --- a/src/features/recommendation/recommendation.hook.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { getRecommendationMate } from './recommendation.api'; - -import { type CardType } from '@/entities/shared-posts-filter'; - -export const useRecommendationMate = ({ - memberId, - cardType, - enabled, -}: { - memberId: string; - cardType: CardType; - enabled: boolean; -}) => - useQuery({ - queryKey: ['/api/match/recommendation', { memberId, cardType }], - queryFn: async () => - await getRecommendationMate(memberId, cardType).then( - response => response.data.recommendation, - ), - enabled, - }); diff --git a/src/features/shared/shared.api.ts b/src/features/shared/shared.api.ts index 8e6d5dd26d..e415046c8c 100644 --- a/src/features/shared/shared.api.ts +++ b/src/features/shared/shared.api.ts @@ -12,6 +12,7 @@ import { type SuccessBaseDTO } from '@/shared/types'; export const getSharedPosts = async ({ filter, + cardOption, search, page, }: GetSharedPostsProps) => { @@ -19,7 +20,11 @@ export const getSharedPosts = async ({ const baseURL = '/maru-api/shared/posts/studio'; let query = ''; - if (filter != null && Object.keys(filter).length > 0) { + if ( + filter != null && + Object.keys(filter).length > 0 && + JSON.stringify(filter).length > 2 + ) { query += `filter=${JSON.stringify(filter)}`; } @@ -29,7 +34,7 @@ export const getSharedPosts = async ({ query += `&page=${page}`; - return `${baseURL}?${encodeURI(query)}`; + return `${baseURL}?${encodeURIComponent(query)}&cardOption=${cardOption}`; }; return await axios.get(getURI()); @@ -63,6 +68,7 @@ export const scrapPost = async (postId: number) => export const getDormitorySharedPosts = async ({ filter, + cardOption, search, page, }: GetSharedPostsProps) => { @@ -70,7 +76,11 @@ export const getDormitorySharedPosts = async ({ const baseURL = '/maru-api/shared/posts/dormitory'; let query = ''; - if (filter != null && Object.keys(filter).length > 0) { + if ( + filter != null && + Object.keys(filter).length > 0 && + JSON.stringify(filter).length > 2 + ) { query += `filter=${JSON.stringify(filter)}`; } @@ -80,7 +90,7 @@ export const getDormitorySharedPosts = async ({ query += `&page=${page}`; - return `${baseURL}?${encodeURI(query)}`; + return `${baseURL}?${encodeURIComponent(query)}&cardOption=${cardOption}`; }; return await axios.get(getURI()); diff --git a/src/features/shared/shared.hook.ts b/src/features/shared/shared.hook.ts index 7fbba296f8..b95ee5410b 100644 --- a/src/features/shared/shared.hook.ts +++ b/src/features/shared/shared.hook.ts @@ -346,17 +346,24 @@ export const useCreateSharedPost = () => export const useSharedPosts = ({ filter, search, + cardOption, page, enabled, }: GetSharedPostsProps & { enabled: boolean }) => { const debounceFilter = useDebounce(filter, 1000); return useQuery({ - queryKey: ['/api/shared/posts/studio', { debounceFilter, search, page }], + queryKey: [ + '/api/shared/posts/studio', + { cardOption, debounceFilter, search, page }, + ], queryFn: async () => - await getSharedPosts({ filter: debounceFilter, search, page }).then( - response => response.data, - ), + await getSharedPosts({ + cardOption, + filter: debounceFilter, + search, + page, + }).then(response => response.data), enabled, }); }; @@ -402,15 +409,20 @@ export const useCreateDormitorySharedPost = () => export const useDormitorySharedPosts = ({ filter, search, + cardOption, page, enabled, }: GetSharedPostsProps & { enabled: boolean }) => { const debounceFilter = useDebounce(filter, 1000); return useQuery({ - queryKey: ['/api/shared/posts/dormitory', { debounceFilter, search, page }], + queryKey: [ + '/api/shared/posts/dormitory', + { cardOption, debounceFilter, search, page }, + ], queryFn: async () => await getDormitorySharedPosts({ + cardOption, filter: debounceFilter, search, page, diff --git a/src/features/shared/shared.type.ts b/src/features/shared/shared.type.ts index 69851082a1..03abeeaf3d 100644 --- a/src/features/shared/shared.type.ts +++ b/src/features/shared/shared.type.ts @@ -17,6 +17,7 @@ export interface GetSharedPostsFilter { export interface GetSharedPostsProps { filter?: GetSharedPostsFilter; search?: string; + cardOption: 'my' | 'mate'; page: number; } From d2313c5a125a2c7c964e91def549b1b67ba42b6d Mon Sep 17 00:00:00 2001 From: cjeongmin Date: Sun, 19 May 2024 20:00:19 +0900 Subject: [PATCH 129/130] fix: Remove menu item --- src/components/shared-posts/SharedPostsMenu.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/shared-posts/SharedPostsMenu.tsx b/src/components/shared-posts/SharedPostsMenu.tsx index 912eec330a..13f662e707 100644 --- a/src/components/shared-posts/SharedPostsMenu.tsx +++ b/src/components/shared-posts/SharedPostsMenu.tsx @@ -100,14 +100,6 @@ export function SharedPostsMenu({ 기숙사 메이트 )} - { - handleSelect('dormitory'); - }} - className={selected === 'dormitory' ? 'selected' : ''} - > - 기숙사 메이트 - ); } From 6f03001fee18f71bdfadfeb719581e2a7bbc90fd Mon Sep 17 00:00:00 2001 From: Choi Jeongmin Date: Mon, 20 May 2024 21:19:43 +0900 Subject: [PATCH 130/130] release: 0.6.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b30e54873..7ab38f99fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "maru", - "version": "0.3.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index ebbfead9bc..209ec47936 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maru", - "version": "0.5.0", + "version": "0.6.0", "private": true, "scripts": { "dev": "next dev",