From 62d374bca0e7f8d442c92dead6a3d5ca28454dc7 Mon Sep 17 00:00:00 2001 From: Kim0426 <706shin1728@naver.com> Date: Thu, 15 Aug 2024 22:09:14 +0900 Subject: [PATCH 01/61] =?UTF-8?q?chore:=20react-hook-form=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7213322c..2727bd8c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-calendar": "^4.7.0", "react-cookie": "6.1.1", "react-dom": "18.2.0", + "react-hook-form": "^7.52.2", "styled-components": "6.1.0", "swiper": "11.0.5" }, From a4052590f2928bef56e2cafae49f49b3b54bd611 Mon Sep 17 00:00:00 2001 From: Kim0426 <706shin1728@naver.com> Date: Sat, 17 Aug 2024 22:42:21 +0900 Subject: [PATCH 02/61] =?UTF-8?q?refactor:=20=ED=9B=85=ED=8F=BC=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A9=98=ED=86=A0=EB=A7=81=20=EC=86=8C=EA=B0=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- src/app/add-profile/page.tsx | 130 +++++++----------- src/app/add-profile/schema.ts | 35 +++++ src/app/login/oauth2/code/kakao/page.tsx | 1 + .../SingleForm/ProfileForm/ProfileForm.tsx | 109 ++++++++------- .../form/{profileForm.ts => profileForm.tsx} | 8 +- 6 files changed, 156 insertions(+), 131 deletions(-) create mode 100644 src/app/add-profile/schema.ts rename src/types/form/{profileForm.ts => profileForm.tsx} (62%) diff --git a/package.json b/package.json index 2727bd8c..ef203520 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@hookform/resolvers": "^3.9.0", "@mui/material": "^5.14.18", "@mui/styled-engine-sc": "^6.0.0-alpha.6", "@tanstack/react-query": "4.36.1", @@ -33,7 +34,8 @@ "react-dom": "18.2.0", "react-hook-form": "^7.52.2", "styled-components": "6.1.0", - "swiper": "11.0.5" + "swiper": "11.0.5", + "yup": "^1.4.0" }, "devDependencies": { "@swc-jotai/react-refresh": "^0.1.0", diff --git a/src/app/add-profile/page.tsx b/src/app/add-profile/page.tsx index fdc2ec67..36b61840 100644 --- a/src/app/add-profile/page.tsx +++ b/src/app/add-profile/page.tsx @@ -1,12 +1,16 @@ 'use client'; + +import { useEffect } from 'react'; import ProgressBar from '@/components/Bar/ProgressBar'; -import ClickedBtn from '@/components/Button/ClickedBtn'; -import NextBtn from '@/components/Button/NextBtn'; + +import { addProfileSchema } from '@/app/add-profile/schema'; import BackHeader from '@/components/Header/BackHeader'; -import DimmedModal from '@/components/Modal/DimmedModal'; + +import { yupResolver } from '@hookform/resolvers/yup'; import FullModal from '@/components/Modal/FullModal'; import ProfileForm from '@/components/SingleForm/ProfileForm'; import SingleValidator from '@/components/Validator/SingleValidator'; +import { useForm } from 'react-hook-form'; import { PROFILE_DIRECTION, PROFILE_PLACEHOLDER, @@ -21,7 +25,6 @@ import { } from '@/stores/senior'; import { useAtom } from 'jotai'; import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import styled from 'styled-components'; @@ -29,68 +32,36 @@ function AddProfilePage() { const [singleIntro, setSingleIntro] = useAtom(sSingleIntroduce); const [multiIntro, setMultiIntro] = useAtom(sMultiIntroduce); const [recommended, setRecommended] = useAtom(sRecommendedFor); - const [flag, setFlag] = useState(false); - const [alertMsg, setAlertMsg] = useState(''); + const { + register, + trigger, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(addProfileSchema), + defaultValues: { + singleIntro, + multiIntro, + recommended, + }, + mode: 'onBlur', + }); + + useEffect(() => { + trigger(); + }, []); const router = useRouter(); - const [buttonAct, setButtonAct] = useState(false); const { modal, modalHandler, portalElement } = useModal( 'senior-best-case-portal', ); + const hasErrors = + errors.multiIntro || errors.recommended || errors.singleIntro; - const updateBtnSet = () => { - setButtonAct( - singleIntro.length >= 10 && - multiIntro.length >= 50 && - recommended.length >= 50, - ); - }; - - useEffect(() => { - updateBtnSet(); - - if ( - singleIntro.length >= 10 && - multiIntro.length >= 50 && - recommended.length >= 50 - ) { - setFlag(false); - } - }, [singleIntro, multiIntro, recommended]); - - const handleClick = () => { - if (!singleIntro) { - showAlert('한줄소개를 입력해주세요'); - return; - } - - if (!multiIntro) { - showAlert('자기소개를 입력해주세요'); - return; - } - - if (!recommended) { - showAlert('추천대상을 입력해주세요'); + const handleClick = async () => { + if (hasErrors) { return; } - - if (multiIntro.length < 50) { - showAlert('자기소개를 50자 이상 작성해주세요'); - return; - } - if (recommended.length < 50) { - showAlert('추천대상을 50자 이상 작성해주세요'); - return; - } - - setFlag(false); router.push('/add-time'); - return; - }; - - const showAlert = (msg: string) => { - setAlertMsg(msg); - setFlag(true); - return; }; return ( @@ -102,35 +73,37 @@ function AddProfilePage() {
{PROFILE_SUB_DIRECTION.addProfile}
setSingleIntro(e)} /> -
- {flag && ( - - )} -
+ + {errors.singleIntro && ( + + )} setMultiIntro(e)} /> +
- {flag && ( + {errors.multiIntro && ( setRecommended(e)} />
- {flag && ( + {errors.recommended && ( 이전 - {buttonAct ? ( - 다음 + {!hasErrors ? ( + + 다음 + ) : ( - 다음 + 다음 )}
{modal && portalElement diff --git a/src/app/add-profile/schema.ts b/src/app/add-profile/schema.ts new file mode 100644 index 00000000..4468d40c --- /dev/null +++ b/src/app/add-profile/schema.ts @@ -0,0 +1,35 @@ +import { object, string, InferType, ValidationError } from 'yup'; + +const VALIDATE_MSG = { + no_single_intro: '한줄소개를 입력해주세요', + no_multi_intro: '자기소개를 입력해주세요', + no_recommended: '추천대상을 입력해주세요', + min_single_intro_length: '최소 10자 이상 입력해 주세요.', + min_multi_intro_length: '자기소개를 50자 이상 작성해주세요', + min_recommended_length: '추천대상을 50자 이상 작성해주세요', +}; + +export const addProfileSchema = object({ + singleIntro: string() + .required(VALIDATE_MSG.no_single_intro) + .min(10, VALIDATE_MSG.min_single_intro_length), + multiIntro: string() + .required(VALIDATE_MSG.no_multi_intro) + .min(50, VALIDATE_MSG.min_multi_intro_length), + recommended: string() + .required(VALIDATE_MSG.no_recommended) + .min(50, VALIDATE_MSG.min_recommended_length), +}); + +type AddProfile = InferType; + +export const validateAddProfileError = async (data: AddProfile) => { + try { + await addProfileSchema.validate(data); + } catch (e) { + if (e instanceof ValidationError) { + console.log(e); + throw e; + } + } +}; diff --git a/src/app/login/oauth2/code/kakao/page.tsx b/src/app/login/oauth2/code/kakao/page.tsx index e63f6b3d..f4980e96 100644 --- a/src/app/login/oauth2/code/kakao/page.tsx +++ b/src/app/login/oauth2/code/kakao/page.tsx @@ -1,4 +1,5 @@ 'use client'; + import React from 'react'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; diff --git a/src/components/SingleForm/ProfileForm/ProfileForm.tsx b/src/components/SingleForm/ProfileForm/ProfileForm.tsx index 3335c7f1..4a38115f 100644 --- a/src/components/SingleForm/ProfileForm/ProfileForm.tsx +++ b/src/components/SingleForm/ProfileForm/ProfileForm.tsx @@ -1,60 +1,69 @@ +import React, { forwardRef, useEffect, useState } from 'react'; import { ProfileFormProps } from '@/types/form/profileForm'; import { ProfileFormContainer, ProfileTitleContainer, } from './ProfileForm.styled'; -import { useEffect, useState } from 'react'; -function ProfileForm(props: ProfileFormProps) { - const [charCount, setCharCount] = useState(0); - useEffect(() => { - if (props.loadStr) { - const targetForm = document.querySelector( - `.profile-form-${props.formType}`, - ) as HTMLInputElement | HTMLTextAreaElement; - targetForm.value = props.loadStr; - setCharCount(props.loadStr.length); - return; - } - }, [props.loadStr]); +const ProfileForm = forwardRef( + (props, ref) => { + const [charCount, setCharCount] = useState(0); - const handleChange = ( - e: React.ChangeEvent, - ) => { - if (props.maxLength && e.currentTarget.value.length > props.maxLength) { - e.currentTarget.value = e.currentTarget.value.slice(0, props.maxLength); - } - props.changeHandler(e.currentTarget.value); - setCharCount(e.currentTarget.value.length); - }; + useEffect(() => { + if (props.loadStr) { + const targetForm = document.querySelector( + `.profile-form-${props.formType}`, + ) as HTMLInputElement | HTMLTextAreaElement; + if (targetForm) { + targetForm.value = props.loadStr; + } + setCharCount(props.loadStr.length); + } + }, [props.loadStr]); - return ( - - -
{props.title}
-
- {charCount} / {props.maxLength || 0} 자 -
-
- {props.lineType == 'single' && ( - - )} -
- ); -} + const handleChange = ( + e: React.ChangeEvent, + ) => { + if (props.maxLength && e.currentTarget.value.length > props.maxLength) { + e.currentTarget.value = e.currentTarget.value.slice(0, props.maxLength); + } + + if (props.changeHandler) { + props.changeHandler(e.currentTarget.value); + } + + setCharCount(e.currentTarget.value.length); + }; + + return ( + + +
{props.title}
+
+ {charCount} / {props.maxLength || 0} 자 +
+
+ {props.lineType === 'single' && ( + + )} +
+ ); + }, +); export default ProfileForm; diff --git a/src/types/form/profileForm.ts b/src/types/form/profileForm.tsx similarity index 62% rename from src/types/form/profileForm.ts rename to src/types/form/profileForm.tsx index e7107132..edf20cfd 100644 --- a/src/types/form/profileForm.ts +++ b/src/types/form/profileForm.tsx @@ -1,4 +1,6 @@ import { PrimitiveAtom, SetStateAction } from 'jotai'; +import { ComponentPropsWithRef } from 'react'; +import { RegisterOptions, UseFormRegisterReturn } from 'react-hook-form'; type SetAtom = (...args: Args) => Result; @@ -6,13 +8,13 @@ type SetAtom = (...args: Args) => Result; export type lineType = 'single' | 'multi'; export type profileFormType = 'singleIntro' | 'multiIntro' | 'recommendedFor'; -export interface ProfileFormProps { +export interface ProfileFormProps extends ComponentPropsWithRef<'input'> { flag: boolean; lineType: lineType; title: string; maxLength?: number; // lineType이 'multi'인 경우에만 들어옴 - placeholder: string; loadStr: string; formType: profileFormType; - changeHandler: SetAtom<[SetStateAction], void>; + changeHandler?: SetAtom<[SetStateAction], void>; + register?:UseFormRegisterReturn } From 6ff737d0e0dd2306c8e15b4964b04bf4abdb4246 Mon Sep 17 00:00:00 2001 From: Kim0426 <706shin1728@naver.com> Date: Sun, 18 Aug 2024 14:48:40 +0900 Subject: [PATCH 03/61] =?UTF-8?q?refactor:=20componentProps=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/add-profile/page.tsx | 43 ++++++------------- .../SingleForm/ProfileForm/ProfileForm.tsx | 12 ++++-- src/types/form/profileForm.tsx | 11 +++-- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/src/app/add-profile/page.tsx b/src/app/add-profile/page.tsx index 36b61840..2310345c 100644 --- a/src/app/add-profile/page.tsx +++ b/src/app/add-profile/page.tsx @@ -44,7 +44,7 @@ function AddProfilePage() { multiIntro, recommended, }, - mode: 'onBlur', + mode: 'onChange', }); useEffect(() => { @@ -55,7 +55,9 @@ function AddProfilePage() { 'senior-best-case-portal', ); const hasErrors = - errors.multiIntro || errors.recommended || errors.singleIntro; + errors.multiIntro?.message || + errors.recommended?.message || + errors.singleIntro?.message; const handleClick = async () => { if (hasErrors) { @@ -138,13 +140,12 @@ function AddProfilePage() { > 이전 - {!hasErrors ? ( - - 다음 - - ) : ( - 다음 - )} + 0} + > + 다음 +
{modal && portalElement ? createPortal( @@ -209,27 +210,7 @@ const ShowProfBtn = styled.button` border: none; cursor: pointer; `; -const NextAddBtn = styled.button` - display: flex; - width: 57%; - padding: 1rem 0rem; - justify-content: center; - align-items: center; - gap: 0.625rem; - margin-left: 0.4rem; - border-radius: 0.75rem; - background: #dee2e6; - border: none; - color: #fff; - text-align: center; - font-family: Pretendard; - font-size: 1.125rem; - font-style: normal; - font-weight: 700; - line-height: normal; - cursor: pointer; -`; -const NextAddBtnSet = styled.button` +const NextAddBtnSet = styled.button<{ hasError: boolean | undefined }>` display: flex; width: 57%; padding: 1rem 0rem; @@ -238,7 +219,7 @@ const NextAddBtnSet = styled.button` gap: 0.625rem; margin-left: 0.4rem; border: none; - background: #2fc4b2; + background: ${({ hasError }) => (hasError ? '#dee2e6;' : '#2fc4b2')}; border-radius: 0.75rem; color: #fff; text-align: center; diff --git a/src/components/SingleForm/ProfileForm/ProfileForm.tsx b/src/components/SingleForm/ProfileForm/ProfileForm.tsx index 4a38115f..15ebffd8 100644 --- a/src/components/SingleForm/ProfileForm/ProfileForm.tsx +++ b/src/components/SingleForm/ProfileForm/ProfileForm.tsx @@ -6,7 +6,7 @@ import { } from './ProfileForm.styled'; const ProfileForm = forwardRef( - (props, ref) => { + (props, _ref) => { const [charCount, setCharCount] = useState(0); useEffect(() => { @@ -49,7 +49,10 @@ const ProfileForm = forwardRef( className={`profile-form-${props.formType}`} placeholder={props.placeholder} {...props?.register} - onChange={handleChange} + onChange={(e) => { + props.register?.onChange(e); + handleChange(e); + }} /> )} {props.lineType === 'multi' && ( @@ -58,7 +61,10 @@ const ProfileForm = forwardRef( className={`profile-form-${props.formType}`} placeholder={props.placeholder} {...props?.register} - onChange={handleChange} + onChange={(e) => { + props.register?.onChange(e); + handleChange(e); + }} > )} diff --git a/src/types/form/profileForm.tsx b/src/types/form/profileForm.tsx index edf20cfd..7ddd533d 100644 --- a/src/types/form/profileForm.tsx +++ b/src/types/form/profileForm.tsx @@ -1,6 +1,7 @@ import { PrimitiveAtom, SetStateAction } from 'jotai'; -import { ComponentPropsWithRef } from 'react'; -import { RegisterOptions, UseFormRegisterReturn } from 'react-hook-form'; +import { ChangeEvent } from 'react'; + +import { UseFormRegisterReturn } from 'react-hook-form'; type SetAtom = (...args: Args) => Result; @@ -8,7 +9,7 @@ type SetAtom = (...args: Args) => Result; export type lineType = 'single' | 'multi'; export type profileFormType = 'singleIntro' | 'multiIntro' | 'recommendedFor'; -export interface ProfileFormProps extends ComponentPropsWithRef<'input'> { +export interface ProfileFormProps { flag: boolean; lineType: lineType; title: string; @@ -16,5 +17,7 @@ export interface ProfileFormProps extends ComponentPropsWithRef<'input'> { loadStr: string; formType: profileFormType; changeHandler?: SetAtom<[SetStateAction], void>; - register?:UseFormRegisterReturn + register?: UseFormRegisterReturn; + placeholder?: string; + onChange?: (e: ChangeEvent) => void; } From f9ad3386ea90f858a91b33aeff9db6f571e7689f Mon Sep 17 00:00:00 2001 From: Kim0426 <706shin1728@naver.com> Date: Sun, 18 Aug 2024 14:56:31 +0900 Subject: [PATCH 04/61] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/add-profile/page.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/add-profile/page.tsx b/src/app/add-profile/page.tsx index 2310345c..0a33d7b3 100644 --- a/src/app/add-profile/page.tsx +++ b/src/app/add-profile/page.tsx @@ -10,7 +10,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import FullModal from '@/components/Modal/FullModal'; import ProfileForm from '@/components/SingleForm/ProfileForm'; import SingleValidator from '@/components/Validator/SingleValidator'; -import { useForm } from 'react-hook-form'; +import { FieldError, useForm } from 'react-hook-form'; import { PROFILE_DIRECTION, PROFILE_PLACEHOLDER, @@ -55,9 +55,7 @@ function AddProfilePage() { 'senior-best-case-portal', ); const hasErrors = - errors.multiIntro?.message || - errors.recommended?.message || - errors.singleIntro?.message; + errors.multiIntro || errors.recommended || errors.singleIntro; const handleClick = async () => { if (hasErrors) { @@ -140,10 +138,7 @@ function AddProfilePage() { > 이전 - 0} - > + 다음 @@ -210,7 +205,7 @@ const ShowProfBtn = styled.button` border: none; cursor: pointer; `; -const NextAddBtnSet = styled.button<{ hasError: boolean | undefined }>` +const NextAddBtnSet = styled.button<{ hasError: FieldError | undefined }>` display: flex; width: 57%; padding: 1rem 0rem; From 09d0aac723c2810c455b7803f6f2ea9d81dc4f0b Mon Sep 17 00:00:00 2001 From: Kim0426 <706shin1728@naver.com> Date: Wed, 21 Aug 2024 19:04:54 +0900 Subject: [PATCH 05/61] =?UTF-8?q?fix:=20trigger=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/add-profile/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/add-profile/page.tsx b/src/app/add-profile/page.tsx index 0a33d7b3..e8a4a44d 100644 --- a/src/app/add-profile/page.tsx +++ b/src/app/add-profile/page.tsx @@ -1,16 +1,16 @@ 'use client'; -import { useEffect } from 'react'; import ProgressBar from '@/components/Bar/ProgressBar'; import { addProfileSchema } from '@/app/add-profile/schema'; import BackHeader from '@/components/Header/BackHeader'; +import { FieldError, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import FullModal from '@/components/Modal/FullModal'; import ProfileForm from '@/components/SingleForm/ProfileForm'; import SingleValidator from '@/components/Validator/SingleValidator'; -import { FieldError, useForm } from 'react-hook-form'; + import { PROFILE_DIRECTION, PROFILE_PLACEHOLDER, @@ -47,9 +47,6 @@ function AddProfilePage() { mode: 'onChange', }); - useEffect(() => { - trigger(); - }, []); const router = useRouter(); const { modal, modalHandler, portalElement } = useModal( 'senior-best-case-portal', From 03e441112b522f2c8c5494fd19300c4a40b32db7 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Tue, 27 Aug 2024 20:32:56 +0900 Subject: [PATCH 06/61] =?UTF-8?q?RAC-423=20feat:=20=ED=8A=9C=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=96=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: reactour 패키지 설치 * feat: reactour와 step 새로 추가 * feaet: useTutorial 훅 구현 * fix: 빌드 에러 수정 및 스타일 일부 수정 * fix: 불필요한 콘솔로그 제거 * refactor: 튜토리얼 끝났는지 여부 추가 * refactor: 왼쪽 오른쪽 버튼 이미지로 변경 --- package.json | 2 + public/left_white.png | Bin 0 -> 260 bytes public/right_white.png | Bin 0 -> 269 bytes public/tutorial_bottom.png | Bin 0 -> 347 bytes src/api/api.ts | 11 +- src/app/login/oauth2/code/kakao/page.tsx | 4 +- src/app/page.tsx | 27 +-- .../Bar/FieldTapBar/FieldTapBar.tsx | 8 +- src/components/Bar/MenuBar/MenuBar.tsx | 2 + src/components/Bar/UnivTapBar/UnivTapBar.tsx | 1 + src/components/Provider/providers.tsx | 155 +++++++++++++++++- .../SeniorProfile/SeniorProfile.tsx | 5 +- src/hooks/useAuth.ts | 17 +- src/hooks/useTutorial.ts | 30 ++++ src/stores/signup.ts | 2 + src/types/modal/menubar.ts | 1 + src/types/profile/seniorProfile.ts | 1 + 17 files changed, 230 insertions(+), 36 deletions(-) create mode 100644 public/left_white.png create mode 100644 public/right_white.png create mode 100644 public/tutorial_bottom.png create mode 100644 src/hooks/useTutorial.ts diff --git a/package.json b/package.json index ef203520..5af49732 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@hookform/resolvers": "^3.9.0", "@mui/material": "^5.14.18", "@mui/styled-engine-sc": "^6.0.0-alpha.6", + "@reactour/mask": "^1.1.0", + "@reactour/tour": "^3.7.0", "@tanstack/react-query": "4.36.1", "@tanstack/react-query-devtools": "4.36.1", "@typescript-eslint/eslint-plugin": "6.0.0", diff --git a/public/left_white.png b/public/left_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c9ed39949126788f51fc5ff251975be9fa61b7 GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}`#fD7Lo!(3 z&Pe2HP~d6Zoq6|;a!Je6ghr2p0+D@o|5qgJ{JwX=@ol|~j2sROOacuD`Z+pI=qaVo z?Oqq`x&6^n$#f}3{-dk=COIAO{;2gJB97r`eX8ISMSF*a^JxsXrnoEEvp#;{tyRmL z`ZwY~fA#CFDL1)KOg1t-|K;j!Il=9x6^;E`63Swlg!w!a-3ydhHa`}cQ{cd5`RS3% zynPdbsstu3*1r;!qhxY0tJ=^%E7x^H8v~011JF4KIARP=ZZ&WgfBOsQ69!LLKbLh* G2~7ZcePKfY literal 0 HcmV?d00001 diff --git a/public/right_white.png b/public/right_white.png new file mode 100644 index 0000000000000000000000000000000000000000..bdc2ba02b0f85b5601627a543127f143df6f2645 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}$30yfLo!(3 z&T!;vHQ;gC`||rUxr53*&x9Sj$|gD(eAtw^^oY+rg=sq6jEo!(3`_zI4#`ZD>Lz_F zzMFG@ov8S_=r37^Uf%!me|zn$30wMJ{hg`iwO@UX#!*|%Heq3lrS2AiUDZK6>S_gE z{c&?{c)O-_{ajnRr!;3~`Lw6|I<}`>GZMV$)X_ib^fO00Z-ZIzopr0D`h@TL1t6 literal 0 HcmV?d00001 diff --git a/public/tutorial_bottom.png b/public/tutorial_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..00e37266b061a3a8279f1137da74fd1c90efe3c7 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^5PixHaqaNOl)9T62in6#W}U1#VTX20xRd#4NtAatNzGH_w6ZB`!2+hC~S5=AoNu8 z(ZGGn^rK%m|6jK0l61i0J4)L-tQ@%K;G-7VZ{s+Sy3^tC?{C?>4fg^PFh*=Odd$w^;vi#KFg5zd=5dG^8Z81tj5l|>JmU!*+t%z55@wp4Ou zm1xA*!$#^lKbdlvAMJWMSd=j3%2%J=iG{u1%5MO9vW-oM<#pK_P^&suWho@&07^0ucDn=&6A;!pg^$iF;} VlkIZn8DMBJc)I$ztaD0e0ss}-i6j63 literal 0 HcmV?d00001 diff --git a/src/api/api.ts b/src/api/api.ts index 4a5a04b4..5fc31713 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -8,15 +8,16 @@ const instance = axios.create({ }); instance.interceptors.request.use( - (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + async ( + config: InternalAxiosRequestConfig, + ): Promise => { const { getAccessToken, removeTokens } = useAuth(); - const accessTkn = getAccessToken(); - const router = useRouter(); + const accessTkn = await getAccessToken(); - if (!accessTkn) { + if (!accessTkn && typeof window !== 'undefined') { // refresh token까지 만료된 경우 removeTokens(); - router.replace('/'); + window.location.href = '/'; } else { config.headers.Authorization = `Bearer ${accessTkn}`; } diff --git a/src/app/login/oauth2/code/kakao/page.tsx b/src/app/login/oauth2/code/kakao/page.tsx index f4980e96..ac3c9714 100644 --- a/src/app/login/oauth2/code/kakao/page.tsx +++ b/src/app/login/oauth2/code/kakao/page.tsx @@ -6,12 +6,13 @@ import { useRouter } from 'next/navigation'; import axios from 'axios'; import useAuth from '@/hooks/useAuth'; import { useAtom, useSetAtom } from 'jotai'; -import { socialIdAtom } from '@/stores/signup'; +import { socialIdAtom, isTutorialFinished } from '@/stores/signup'; import Spinner from '@/components/Spinner'; import styled from 'styled-components'; function KakaoPage() { const setSocialId = useSetAtom(socialIdAtom); + const setTutorialFinished = useSetAtom(isTutorialFinished); const router = useRouter(); const { setAccessToken, setRefreshToken, setUserType } = useAuth(); useEffect(() => { @@ -47,6 +48,7 @@ function KakaoPage() { expires: response.data.refreshExpiration, }); setUserType(response.data.role); + setTutorialFinished(response.data.isTutorial); router.replace('/'); return; diff --git a/src/app/page.tsx b/src/app/page.tsx index a2122165..b714f0e1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ 'use client'; import MenuBar from '../components/Bar/MenuBar'; -import Login from '../components/kakao/login'; import { useEffect } from 'react'; import usePrevPath from '../hooks/usePrevPath'; import styled from 'styled-components'; @@ -14,15 +13,21 @@ import DimmedModal from '../components/Modal/DimmedModal'; import SearchModal from '../components/Modal/SearchModal'; import { sfactiveTabAtom, suactiveTabAtom } from '../stores/tap'; import axios from 'axios'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { isTutorialFinished } from '@/stores/signup'; +import { useTour } from '@reactour/tour'; import LogoLayer from '@/components/LogoLayer/LogoLayer'; import { listDataAtom, pageNumAtom } from '@/stores/home'; import Footer from '@/components/Footer'; +import useTutorial from '@/hooks/useTutorial'; + export default function Home() { const { setCurrentPath } = usePrevPath(); const [data, setData] = useAtom(listDataAtom); const [page, setPage] = useAtom(pageNumAtom); + const { isTutorialFinish } = useTutorial(); + const field = useAtomValue(sfactiveTabAtom); const postgradu = useAtomValue(suactiveTabAtom); @@ -137,23 +142,7 @@ const HomeLayer = styled.div` height: inherit; padding-bottom: 3.5rem; `; -const Logo = styled.div` - display: flex; - .none-name { - font-size: 1.3rem; - } - .bold-name { - font-size: 1.3rem; - font-weight: 700; - } -`; -const HomeTopLayer = styled.div` - height: 4rem; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 1rem; -`; + const HomeBannerLayer = styled.div` height: 6.7rem; padding: 0 1rem; diff --git a/src/components/Bar/FieldTapBar/FieldTapBar.tsx b/src/components/Bar/FieldTapBar/FieldTapBar.tsx index 878c2c0d..fe1b4664 100644 --- a/src/components/Bar/FieldTapBar/FieldTapBar.tsx +++ b/src/components/Bar/FieldTapBar/FieldTapBar.tsx @@ -1,11 +1,12 @@ 'use client'; -import React from 'react'; +import React, { ComponentPropsWithoutRef } from 'react'; import { TapStyle } from './FieldTapBar.styled'; import { sftapType } from '@/types/tap/tap'; import { sfactiveTabAtom } from '@/stores/tap'; import { useAtom, useSetAtom } from 'jotai'; import { SFTAB } from '@/constants/tab/ctap'; import { pageNumAtom } from '@/stores/home'; + function FieldTapBar() { const [fpActiveTab, setFpActiveTab] = useAtom(sfactiveTabAtom); const setPageNum = useSetAtom(pageNumAtom); @@ -19,9 +20,12 @@ function FieldTapBar() {
handleTabClick(SFTAB.ALL)} + onClick={() => { + handleTabClick(SFTAB.ALL); + }} > 전체분야 diff --git a/src/components/Bar/MenuBar/MenuBar.tsx b/src/components/Bar/MenuBar/MenuBar.tsx index d5f78549..dad1c22d 100644 --- a/src/components/Bar/MenuBar/MenuBar.tsx +++ b/src/components/Bar/MenuBar/MenuBar.tsx @@ -90,6 +90,7 @@ function MenuBar(props: MenubarProps) { {token ? ( { setActiveMenu('mentoring'); router.push(mentoringPath); @@ -124,6 +125,7 @@ function MenuBar(props: MenubarProps) { ) : ( { setActiveMenu('mentoring'); handleClick(); diff --git a/src/components/Bar/UnivTapBar/UnivTapBar.tsx b/src/components/Bar/UnivTapBar/UnivTapBar.tsx index 27dda2a6..a9f0a85b 100644 --- a/src/components/Bar/UnivTapBar/UnivTapBar.tsx +++ b/src/components/Bar/UnivTapBar/UnivTapBar.tsx @@ -21,6 +21,7 @@ function UnivTapBar() { handleTabClick(SMTAB.ALL)} + className="tutorial_school" > 전체선택 diff --git a/src/components/Provider/providers.tsx b/src/components/Provider/providers.tsx index cd3fd88a..8cc8a4c4 100644 --- a/src/components/Provider/providers.tsx +++ b/src/components/Provider/providers.tsx @@ -1,7 +1,158 @@ 'use client'; +import Image from 'next/image'; +import ArrowRight from '../../../public/right_white.png'; +import ArrowLeft from '../../../public/left_white.png'; +import { TourProvider } from '@reactour/tour'; +import { Provider as JotaiProvider } from 'jotai'; +import styled from 'styled-components'; -import { Provider } from 'jotai'; +interface StepTextProps { + size?: string; + margin?: string; + bold?: boolean; +} + +const StepText = styled.p` + font-size: ${({ size }) => size || '14px'}; + margin: ${({ margin }) => margin || '0'}; + font-weight: ${({ bold }) => (bold ? '600' : 'normal')}; + line-height: 20px; + color: #ffffff; + padding: 0; +`; +const tourSteps = [ + { + selector: '.tutorial_major', + content: ( +
+ 대학원 김선배에 있는 + 모든 선배들을 확인할 수 있어요. + + 선배 전공분야 확인 + +
+ ), + position: 'top' as const, + }, + { + selector: '.tutorial_school', + content: ( +
+ + 선배 학교 확인 + + 대학원 김선배에 있는 + 모든 선배들을 확인할 수 있어요. +
+ ), + position: 'right' as const, + }, + { + selector: '.tutorial_card', + content: ( +
+ + 선배 프로필 + + + 내가 원하는 선배가 어떤 것을 연구하는지, + + 보다 자세한 정보를 확인할 수 있어요. +
+ ), + position: 'bottom' as const, + }, + { + selector: '.tutorial_mentoring', + content: ( +
+ 내가 진행하거나 + 진행한 멘토링을 확인할 수 있어요. + + 내 멘토링 + +
+ ), + position: 'top' as const, + }, +]; export default function Providers({ children }: { children: React.ReactNode }) { - return {children}; + return ( + + ({ + ...base, + color: '#ffffff', + }), + popover: (base) => ({ + ...base, + color: '#2fc4b2', + background: 'none', + boxShadow: 'none', + '--reactour-accent': '#2FC4B2', + }), + maskWrapper: (base) => ({ + ...base, + background: 'none', + }), + controls: (base) => ({ + ...base, + background: 'none', + }), + badge: (base) => ({ + ...base, + opacity: 0, + }), + dot: (base) => ({ + ...base, + }), + button: (base) => ({ + ...base, + color: '#ffffff', + svg: '#ffffff', + stroke: '#ffffff', + }), + maskRect: (base) => ({ + ...base, + background: 'none', + }), + }} + disableKeyboardNavigation + steps={tourSteps} + nextButton={(props) => ( + 튜토리얼_다음버튼 props.setCurrentStep(props.currentStep + 1)} + style={{ + cursor: 'pointer', + }} + {...props} + /> + )} + prevButton={(props) => ( + 튜토리얼_이전버튼 props.setCurrentStep(props.currentStep - 1)} + style={{ + cursor: 'pointer', + }} + {...props} + /> + )} + > + {children} + + + ); } diff --git a/src/components/SeniorProfile/SeniorProfile.tsx b/src/components/SeniorProfile/SeniorProfile.tsx index a4b1614a..1585da92 100644 --- a/src/components/SeniorProfile/SeniorProfile.tsx +++ b/src/components/SeniorProfile/SeniorProfile.tsx @@ -20,7 +20,7 @@ function SeniorProfile({ data }: SeniorProfileProps) { const router = useRouter(); return ( - + { @@ -39,7 +39,8 @@ function SeniorProfile({ data }: SeniorProfileProps) { )} - {data.postgradu ? `[${data.postgradu.replace('학교', '')}]` : ''}  + {data.postgradu ? `[${data.postgradu.replace('학교', '')}]` : ''} +  
{data.professor ? `${data.professor} 교수님` : ''}
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 5203ad8a..54295010 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -5,9 +5,11 @@ import { SetTokenProps } from '@/types/user/user'; import axios from 'axios'; import { useAtom } from 'jotai'; import { useCookies } from 'react-cookie'; +import { Cookies } from 'react-cookie'; function useAuth() { - const [cookies, setCookie, removeCookie] = useCookies(['refresh_token']); + const cookies = new Cookies(); + const refresh_token = cookies.get('refreshToken'); // const [accessTkn, setAccessTkn] = useAtom(accessTokenAtom); // const [accessExp, setAccessExp] = useAtom(accessExpireAtom); // const [type, setType] = useAtom(userTypeAtom); @@ -29,7 +31,10 @@ function useAuth() { /** cookie에 refresh token 값 및 만료 시간 저장 */ function setRefreshToken(props: SetTokenProps) { const expires = calculateExpires(props.expires); - setCookie('refresh_token', props.token, { path: '/', expires }); + cookies.set('refresh_token', props.token, { + path: '/', + expires, + }); } /** ADMIN | USER | SENIOR 값에 맞춰 user type 세팅 */ @@ -84,8 +89,8 @@ function useAuth() { /** refresh token 반환 */ function getRefreshToken() { - if (cookies.refresh_token) { - return cookies.refresh_token; + if (refresh_token) { + return refresh_token; } return ''; } @@ -182,7 +187,9 @@ function useAuth() { localStorage.removeItem('userType'); } - removeCookie('refresh_token', { path: '/' }); + cookies.remove('refresh_token', { + path: '/', + }); } } diff --git a/src/hooks/useTutorial.ts b/src/hooks/useTutorial.ts new file mode 100644 index 00000000..2faf1e70 --- /dev/null +++ b/src/hooks/useTutorial.ts @@ -0,0 +1,30 @@ +import { useSetAtom, useAtom } from 'jotai'; +import useAuth from '@/hooks/useAuth'; +import { useTour } from '@reactour/tour'; +import { isTutorialFinished } from '@/stores/signup'; +import instance from '@/api/api'; +import { useEffect } from 'react'; +function useTutorial() { + const [isTutorialFinish, setTutorialFinished] = useAtom(isTutorialFinished); + const { setIsOpen: setTutorialStepOpen } = useTour(); + const { getUserType } = useAuth(); + + const setTutorialFinish = async () => { + const userType = getUserType(); + + if (!userType || userType !== 'junior' || isTutorialFinish) { + return; + } + setTutorialStepOpen(true); + setTutorialFinished(true); + await instance.patch( + `${process.env.NEXT_PUBLIC_SERVER_URL}/user/me/tutorial`, + ); + }; + useEffect(() => { + setTutorialFinish(); + }, []); + return { isTutorialFinish }; +} + +export default useTutorial; diff --git a/src/stores/signup.ts b/src/stores/signup.ts index 32104ef7..48549afb 100644 --- a/src/stores/signup.ts +++ b/src/stores/signup.ts @@ -17,5 +17,7 @@ export const remainPhoneNum = atom(''); export const phoneNumValidation = atom(false); export const socialIdAtom = atom(''); +//Query로 마이그레이션 필요 +export const isTutorialFinished = atom(false); export const certifiRegAtom = atom('WAITING'); export const profileRegAtom = atom(false); diff --git a/src/types/modal/menubar.ts b/src/types/modal/menubar.ts index 30d88694..092c1d61 100644 --- a/src/types/modal/menubar.ts +++ b/src/types/modal/menubar.ts @@ -1,6 +1,7 @@ export interface MenubarProps { modalHandler?: () => void; onClick?: () => void; + className?: string; } export interface NotLoginProps { diff --git a/src/types/profile/seniorProfile.ts b/src/types/profile/seniorProfile.ts index 1a177d11..710ce738 100644 --- a/src/types/profile/seniorProfile.ts +++ b/src/types/profile/seniorProfile.ts @@ -1,5 +1,6 @@ export interface SeniorProfileProps { data: SeniorProfileData; + className?: string; } export interface SeniorProfileData { From 3626edccb5ebf0d77ad6660e050c3e2ad9f7ddc8 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sat, 31 Aug 2024 18:06:08 +0900 Subject: [PATCH 07/61] =?UTF-8?q?RAC-416=20feat:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=9D=90=EB=A6=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(API=20=EC=97=B0=EA=B2=B0=EC=99=84=EB=A3=8C)=20(#287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 탈퇴페이지 문자열 상수화 * feat: 탈퇴하기 라우터 연결 * feat: 탈퇴페이지 퍼널로 흐름 구현 * feaet: useFunnel 훅 구현 * feat: signout context 구현 * feat: 회원 정보 선택 퍼널 구현 * chore: react-hook-form 의존성 설치 * feat: 탈퇴 안내 마크업 * feat: 회원 탈퇴 이유 선택 기능 추가 * refactor: context 기본 값 변경 * feat: 드롭다운 구현 * feat: API 연결 제외 * feat: 회원탈퇴 연결 * feat: 탈퇴 안내 법적 공지 마크업 추가 * refactor: 탈퇴 후 재가입 안내 메시지 추가 * feat: 회원 탈퇴 후 토큰 제거 로직 추가 --- public/arrow-up.png | Bin 0 -> 222 bytes public/signout.png | Bin 0 -> 9751 bytes public/signout_junior.png | Bin 0 -> 22392 bytes public/signout_senior.png | Bin 0 -> 26826 bytes public/signoutarrow.png | Bin 0 -> 306 bytes src/app/signout/(components)/Header/index.tsx | 32 ++++ .../(components)/signout-finish/index.tsx | 56 +++++++ .../(components)/signout-info/index.tsx | 48 ++++++ .../(components)/signout-reason/index.tsx | 154 ++++++++++++++++++ .../signout-type-select/index.tsx | 87 ++++++++++ src/app/signout/constant.ts | 24 +++ src/app/signout/page.tsx | 54 ++++++ src/app/signout/signoutContext.tsx | 74 +++++++++ .../JuniorManage/JuniorManage.tsx | 4 + .../SeniorManage/SeniorManage.tsx | 5 + src/hooks/useFunnel/Funnel.tsx | 31 ++++ src/hooks/useFunnel/index.tsx | 97 +++++++++++ 17 files changed, 666 insertions(+) create mode 100644 public/arrow-up.png create mode 100644 public/signout.png create mode 100644 public/signout_junior.png create mode 100644 public/signout_senior.png create mode 100644 public/signoutarrow.png create mode 100644 src/app/signout/(components)/Header/index.tsx create mode 100644 src/app/signout/(components)/signout-finish/index.tsx create mode 100644 src/app/signout/(components)/signout-info/index.tsx create mode 100644 src/app/signout/(components)/signout-reason/index.tsx create mode 100644 src/app/signout/(components)/signout-type-select/index.tsx create mode 100644 src/app/signout/constant.ts create mode 100644 src/app/signout/page.tsx create mode 100644 src/app/signout/signoutContext.tsx create mode 100644 src/hooks/useFunnel/Funnel.tsx create mode 100644 src/hooks/useFunnel/index.tsx diff --git a/public/arrow-up.png b/public/arrow-up.png new file mode 100644 index 0000000000000000000000000000000000000000..7b463b4aa4b0ad54b0e7fbb573a8fb25ad10f20e GIT binary patch literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^Qb5el!3HFM1>S!Kq!^2X+?^QKos)S9xI4t9DP^%Ps`;kkhW@4wrJx2;^ursq)TOqeCs?0UL{Fp2mS;;MW0o- z(sH)y+fGdFF8tTD^o0Hm=^)W+$26s$b=fse5dsGsS|(clUfq2{{E$=LnWh~ZWtK>2 zJ8m-MPdb*Rpr-S7-XgQ5B_%nxel1>QR>*0n@6`X5A?Aec&8vSVGQRoRH#z47*J5KS V|MV-HIe>0t@O1TaS?83{1OUCSQf~kN literal 0 HcmV?d00001 diff --git a/public/signout.png b/public/signout.png new file mode 100644 index 0000000000000000000000000000000000000000..37cf060d32cf13bb93f494c07ab7181f70cc759c GIT binary patch literal 9751 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 literal 0 HcmV?d00001 diff --git a/public/signout_senior.png b/public/signout_senior.png new file mode 100644 index 0000000000000000000000000000000000000000..c918bdd4ca21472240344e8fd97ca119fbca3f57 GIT binary patch literal 26826 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 literal 0 HcmV?d00001 diff --git a/public/signoutarrow.png b/public/signoutarrow.png new file mode 100644 index 0000000000000000000000000000000000000000..3e5f3c4869f9381139c36ac402395443eb056f1b GIT binary patch literal 306 zcmV-20nPr2P)Px#>`6pHR5%gUl)(*yFbqXI0wb^hiAU)M?FQ`zWs`P;D5iHLFzt~M7=aO}9Aqhg z65zn0NXU`M%g5h#PNyRuMeob5^HozkG~vK;6R1ALjRAS#yP^oC0og1OYMTeupfsr_ z>}0cxHJTWZEC9j|N|OX3U89*Oj6fO$#3>)*+`_=jDF%V?9t4heiluiul~u*rPnr+l z8U^#<4_RfdyCE)^CfiOy+eQn?YGcdTY0D5kqrd3D#x-Z}D{dMESmrmZj)GIHZSJdS z!XBDP8%YkTvacDqUWPw*G&(9`itlLjQt<& void }) { + return ( + + 뒤로가기 화살표 +
회원 탈퇴
+
+ ); +} + +const HeaderContainer = styled.header` + width: inherit; + margin: 20px auto; + height: 56px; + display: flex; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding-left: 0.75rem; + + > img { + margin-right: 0.5rem; + cursor: pointer; + } + + > div { + font-size: 16px; + font-weight: 700; + } +`; diff --git a/src/app/signout/(components)/signout-finish/index.tsx b/src/app/signout/(components)/signout-finish/index.tsx new file mode 100644 index 00000000..583f749a --- /dev/null +++ b/src/app/signout/(components)/signout-finish/index.tsx @@ -0,0 +1,56 @@ +'use client'; + +import styled from 'styled-components'; +import Image from 'next/image'; +import { SignOutInfoContainer } from '@/app/signout/(components)/signout-type-select'; +import SignOutImage from '/public/signout.png'; +import { useSignOutInfo } from '@/app/signout/signoutContext'; +import NextBtn from '@/components/Button/NextBtn'; +import instance from '@/api/api'; +import { useRouter } from 'next/navigation'; +export function SignOutFinish() { + const router = useRouter(); + const { signOutInfo } = useSignOutInfo(); + const _handleSignOutFinish = async () => { + //회원탈퇴 FLow + //mutate로 바꿔야 함 + //회원탈퇴 API -> 토큰 제거 -> 버튼에 GA이벤트..? + if (signOutInfo) { + await instance + .post('/auth/signout/KAKAO', { + reason: signOutInfo.signOutReason, + etc: signOutInfo.etc, + }) + .then((res) => { + _deleteAuthContent(); + router.push('/'); + }); + } + }; + + const _deleteAuthContent = () => { + localStorage.removeItem('accessToken'); + localStorage.removeItem('accessExpire'); + localStorage.removeItem('userType'); + }; + return ( + + + logo + +
+ +
+
+ ); +} + +const FinishImageWrapper = styled.div` + display: flex; + margin: 150px auto; + justify-content: center; +`; diff --git a/src/app/signout/(components)/signout-info/index.tsx b/src/app/signout/(components)/signout-info/index.tsx new file mode 100644 index 00000000..ca10788d --- /dev/null +++ b/src/app/signout/(components)/signout-info/index.tsx @@ -0,0 +1,48 @@ +import { SignOutInfoContainer } from '@/app/signout/(components)/signout-type-select'; +import styled from 'styled-components'; +import NextBtn from '@/components/Button/NextBtn'; +export function SignOutInfo({ onClick }: { onClick: () => void }) { + return ( + +

회원 탈퇴 안내

+ +
+ 1. 회원정보는 탈퇴 후 삭제 또는 서비스와 격리하여 보존 조치합니다. +
+ +
+ 2. 불량 이용자의 재가입 방지, 명예훼손 등 권리 침해 분쟁 및 수사 협조 + 등을 위한 한시적인 개인정보 보존 조치는 개인정보 취급 방침을 따릅니다. +
+ +
+ 3. 탈퇴 후에는 같은 휴대전화 번호로 일정 기간 동안 재가입이 + 제한됩니다. 회원 탈퇴를 신중히 진행해주세요. +
+ +
4. 탈퇴 후 15일 이내 재가입이 가능합니다.
+
+
+ +
+
+ ); +} + +const SignOutInfoContent = styled.div` + display: flex; + flex-direction: column; + width: 100%; + margin-top: 50px; + min-height: 330px; + background-color: #f0f0f0; + + > div { + display: flex; + height: 80%; + font-size: 14px; + align-items: center; + padding: 0 20px; + gap: 10px; + } +`; diff --git a/src/app/signout/(components)/signout-reason/index.tsx b/src/app/signout/(components)/signout-reason/index.tsx new file mode 100644 index 00000000..12e30013 --- /dev/null +++ b/src/app/signout/(components)/signout-reason/index.tsx @@ -0,0 +1,154 @@ +import { SignOutInfoContainer } from '@/app/signout/(components)/signout-type-select'; +import { useSignOutInfo } from '@/app/signout/signoutContext'; +import NextBtn from '@/components/Button/NextBtn'; +import Image from 'next/image'; +import styled from 'styled-components'; +import ArrowDownGray from '../../../../../public/arrow-down.png'; +import MatchingForm from '@/components/SingleForm/MatchingForm'; +import TextForm from '@/components/SingleForm/TextForm'; +import { useState } from 'react'; + +import { + SIGNOUT_REASON_JUNIOR, + SIGNOUT_REASON_SENIOR, +} from '@/app/signout/constant'; + +type SignOutReasonType = + | keyof typeof SIGNOUT_REASON_JUNIOR + | keyof typeof SIGNOUT_REASON_SENIOR; + +export function SignOutReason({ onClick }: { onClick: () => void }) { + const { signOutInfo, setSignOutInfo, getSignOutReasonMessage } = + useSignOutInfo(); + const [isDropdownOpen, setDropdownOpen] = useState(false); + const [etcLength, setETCLength] = useState(0); + + const signOutReasons = signOutInfo?.isJunior + ? Object.entries(SIGNOUT_REASON_JUNIOR) + : Object.entries(SIGNOUT_REASON_SENIOR); + + const handleReasonClick = (reason: SignOutReasonType) => { + if (signOutInfo) { + setSignOutInfo?.({ + isJunior: signOutInfo.isJunior, + signOutReason: reason, + }); + setDropdownOpen(false); + } + }; + + return ( + +
+ +

+ 탈퇴 이유 + * +

+ setDropdownOpen((prev) => !prev)}> + + {getSignOutReasonMessage() || '이유를 선택하세요'} + + {isDropdownOpen && ( + arrowDown + )} + + {isDropdownOpen && ( + + {signOutReasons.map(([key, value]) => ( + handleReasonClick(key as SignOutReasonType)} + > + {value} + + ))} + + )} + + {signOutInfo?.signOutReason === 'ETC' && ( + { + setETCLength(v.length); + setSignOutInfo?.({ + isJunior: signOutInfo.isJunior, + signOutReason: 'ETC', + etc: v + '', + }); + }} + /> + )} +
+
+
+ +
+
+ ); +} + +const ReasonContainer = styled.div` + font-weight: bold; + display: flex; + flex-direction: column; + color: #1f1f1f; +`; + +const DropDownContainer = styled.div` + display: flex; + font-weight: normal; + justify-content: space-between; + align-items: center; + cursor: pointer; + color: #3e3e3e; + border: 1px solid #ccc; + border-radius: 6px; + width: 325px; + margin: 25px auto; +`; + +const SelectedReason = styled.div` + color: #3c3c3c; + padding-left: 18px; + height: 42px; + display: flex; + align-items: center; +`; + +const DropdownList = styled.div` + background-color: white; + border: 1px solid #dfdfdf; + color: #3c3c3c; + font-weight: normal; + border-radius: 15px; + margin-top: 5px; +`; + +const RequiredMark = styled.span` + color: #ff7272; + font-weight: bold; + margin-left: 3px; +`; + +const ReasonItem = styled.div` + cursor: pointer; + padding: 10px; + border-bottom: 1px solid #dfdfdf; + &:hover { + font-weight: bold; + } +`; diff --git a/src/app/signout/(components)/signout-type-select/index.tsx b/src/app/signout/(components)/signout-type-select/index.tsx new file mode 100644 index 00000000..8d1046e3 --- /dev/null +++ b/src/app/signout/(components)/signout-type-select/index.tsx @@ -0,0 +1,87 @@ +import Image from 'next/image'; +import styled from 'styled-components'; +import SignOutJuniorImage from '/public/signout_junior.png'; +import SignOutSeniorImage from '/public/signout_senior.png'; +import NextBtn from '@/components/Button/NextBtn'; +import { useSignOutInfo } from '@/app/signout/signoutContext'; + +export function SignOutTypeSelect({ onClick }: { onClick: () => void }) { + const { setSignOutInfo, signOutInfo } = useSignOutInfo(); + + const isJunior = signOutInfo?.isJunior; + + return ( + +

회원 유형 선택

+
+ + setSignOutInfo?.({ + signOutReason: 'DIS_SATISFACTION', + isJunior: true, + }) + } + /> + { + setSignOutInfo?.({ + signOutReason: 'DIS_SATISFACTION', + isJunior: false, + }); + }} + /> +
+
+ +
+
+ ); +} + +export const SignOutInfoContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100vh; + + > h1 { + font-size: 20px; + margin-left: 24px; + font-weight: bold; + color: #212529; + } + + > .image_container { + display: flex; + margin-top: 5rem; + margin-left: auto; + margin-right: auto; + gap: 5px; + align-items: flex-end; + width: 90%; + cursor: pointer; + } + + > .nextBtn_container { + margin-top: auto; + } +`; + +const StyledImage = styled(Image)<{ isSelected: boolean }>` + cursor: pointer; + border: ${(props) => props.isSelected && '2px solid #2fc4b2'}; +`; diff --git a/src/app/signout/constant.ts b/src/app/signout/constant.ts new file mode 100644 index 00000000..dd7be54e --- /dev/null +++ b/src/app/signout/constant.ts @@ -0,0 +1,24 @@ +export const SIGNOUT_REASON_SENIOR = { + PRIVACY: '개인정보 문제', + REVENUE: '수익창출의 어려움', + FEW_JUNIOR: '다양하지 않은 후배 풀', + DIS_SATISFACTION: '멘토링 진행의 불만족', + ETC: '기타', +}; + +export const SIGNOUT_REASON_JUNIOR = { + PRIVACY: '개인정보 문제', + EXPENSE: '비용 문제', + FEW_SENIOR: '다양하지 않은 선배 풀', + DIS_SATISFACTION: '멘토링 진행의 불만족', + ETC: '기타', +}; + +export const SIGNOUT_JUNIOR_SELECT = { + JUNIOR: '대학원 후배 탈퇴', + SENIOR: '대학원 선배 탈퇴', +}; + +export const SIGNOUT_MESSAGE = { + signouDone: `회원 탈퇴 완료.\n다시 뵐 수 있었으면 좋겠습니다.`, +}; diff --git a/src/app/signout/page.tsx b/src/app/signout/page.tsx new file mode 100644 index 00000000..fcb78768 --- /dev/null +++ b/src/app/signout/page.tsx @@ -0,0 +1,54 @@ +'use client'; +import useFunnel from '@/hooks/useFunnel'; +import instance from '@/api/api'; +import { SignOutInfoProvider } from '@/app/signout/signoutContext'; + +import { SignOutFinish } from '@/app/signout/(components)/signout-finish'; +import { SignOutReason } from '@/app/signout/(components)/signout-reason'; +import { SignOutInfo } from '@/app/signout/(components)/signout-info'; +import { SignOutTypeSelect } from '@/app/signout/(components)/signout-type-select'; + +import { SignOutHeader } from '@/app/signout/(components)/Header'; + +const signOutSteps = [ + 'signout_info', + 'signout_type_select', + 'signout_reason', + 'signout_finish', +] as const; +export default function SignOut() { + const [SignoutFunnel, setSignoutStep, prevStep, _activeStep] = useFunnel( + signOutSteps, + { + initialStep: 'signout_info', + stepChangeType: 'replace', + } as const, + ); + + return ( +
+ + prevStep()} /> + + + + setSignoutStep('signout_type_select')} + /> + + + setSignoutStep('signout_reason')} + /> + + + setSignoutStep('signout_finish')} /> + + + + + + +
+ ); +} diff --git a/src/app/signout/signoutContext.tsx b/src/app/signout/signoutContext.tsx new file mode 100644 index 00000000..de7105ed --- /dev/null +++ b/src/app/signout/signoutContext.tsx @@ -0,0 +1,74 @@ +import { + Dispatch, + ReactNode, + SetStateAction, + createContext, + useMemo, + useState, + useContext, +} from 'react'; +import { SIGNOUT_REASON_JUNIOR, SIGNOUT_REASON_SENIOR } from './constant'; + +interface SignOutInfo { + isJunior: boolean; + signOutReason: + | keyof typeof SIGNOUT_REASON_JUNIOR + | keyof typeof SIGNOUT_REASON_SENIOR; + etc?: string; +} + +interface SignOutInfoContextType { + signOutInfo: SignOutInfo | null; + setSignOutInfo: Dispatch> | null; + getSignOutReasonMessage: () => string; +} + +const SignOutInfoContext = createContext({ + signOutInfo: { + isJunior: false, + signOutReason: 'ETC', + }, + getSignOutReasonMessage: () => '', + setSignOutInfo: null, +}); + +function SignOutInfoProvider({ children }: { children: ReactNode }) { + const [signOutInfo, setSignOutInfo] = useState(null); + + const getSignOutReasonMessage = () => { + if (signOutInfo?.isJunior) { + return SIGNOUT_REASON_JUNIOR[ + signOutInfo.signOutReason as keyof typeof SIGNOUT_REASON_JUNIOR + ]; + } else if (signOutInfo) { + return SIGNOUT_REASON_SENIOR[ + signOutInfo.signOutReason as keyof typeof SIGNOUT_REASON_SENIOR + ]; + } + return ''; + }; + + const value = useMemo(() => { + return { + signOutInfo, + setSignOutInfo, + getSignOutReasonMessage, + }; + }, [signOutInfo?.isJunior, signOutInfo?.signOutReason, signOutInfo?.etc]); + + return ( + + {children} + + ); +} + +function useSignOutInfo() { + const context = useContext(SignOutInfoContext); + if (!context) { + throw new Error('useSignOutInfo must be used within a SignOutInfoProvider'); + } + return context; +} + +export { SignOutInfoProvider, useSignOutInfo }; diff --git a/src/components/Profile/ProfileManage/JuniorManage/JuniorManage.tsx b/src/components/Profile/ProfileManage/JuniorManage/JuniorManage.tsx index af682852..dcc5d0b4 100644 --- a/src/components/Profile/ProfileManage/JuniorManage/JuniorManage.tsx +++ b/src/components/Profile/ProfileManage/JuniorManage/JuniorManage.tsx @@ -110,6 +110,10 @@ function JuniorManage(props: NotSeniorProps) { content="대학원 선배 회원으로 변경" onClick={handleClick} /> + router.push('/signout')} + /> ); } diff --git a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx index c9a0ee6a..7c5afb2e 100644 --- a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx +++ b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx @@ -169,6 +169,7 @@ function SeniorManage(props: SeniorManageProps) { + + router.push('/signout')} + content="탈퇴하기" + />
diff --git a/src/hooks/useFunnel/Funnel.tsx b/src/hooks/useFunnel/Funnel.tsx new file mode 100644 index 00000000..7675f1e2 --- /dev/null +++ b/src/hooks/useFunnel/Funnel.tsx @@ -0,0 +1,31 @@ +import { Children, ReactElement, ReactNode, isValidElement } from 'react'; +export interface FunnelProps { + steps?: Steps; + step?: Steps[number]; + children: ReactElement | Array; +} + +interface StepProps { + children: ReactNode; + name: string; + onEnter?: () => void; +} +type StepArray = Readonly; +function Funnel({ + step, + steps, + children, +}: FunnelProps): ReactElement { + const targetStep = Children.toArray(children) + .filter(isValidElement>) + .filter((i) => steps?.includes(i.props.name ?? '')); + + const target = targetStep.find((child) => child.props.name === step); + + return <>{target}; +} + +Funnel.Step = ({ children }: { children: ReactNode }) => { + return <>{children}; +}; +export default Funnel; diff --git a/src/hooks/useFunnel/index.tsx b/src/hooks/useFunnel/index.tsx new file mode 100644 index 00000000..13a00718 --- /dev/null +++ b/src/hooks/useFunnel/index.tsx @@ -0,0 +1,97 @@ +import { ReactElement, ReactNode, useEffect, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Funnel from './Funnel'; + +type StepArray = Readonly; + +export interface FunnelProps { + steps: Steps; + step: Steps[number]; + children: ReactElement | Array; +} + +interface StepProps { + name: Steps[number]; + children: ReactNode; +} + +interface RouteFunnel { + (props: FunnelProps): ReactElement; +} + +interface RouterFunnelStep { + (props: StepProps): ReactElement; +} + +interface FunnelOptions { + initialStep?: Steps[number]; + stepQueryKey?: string; + stepChangeType?: 'push' | 'replace'; +} + +function useFunnel( + steps: Steps, + options: FunnelOptions = { + initialStep: steps[0], + stepChangeType: 'push', + }, +): [ + RouteFunnel & { Step: RouterFunnelStep }, + (step: Steps[number]) => void, + () => void, + number, +] { + const router = useRouter(); + const searchParams = useSearchParams(); + + const getCurrentStep = () => { + return ( + searchParams.get(options.stepQueryKey as string) ?? options.initialStep + ); + }; + + const [currentStep, setCurrentStep] = useState(() => getCurrentStep()); + const activeStepIndex = steps.findIndex((s) => s === currentStep); + + const updateStep = (step: Steps[number]) => { + setCurrentStep(step); + const searchParam = new URLSearchParams(searchParams); + searchParam.set(options.stepQueryKey ?? 'step', step); + if (options.stepChangeType === 'push') router.push(`?${searchParam}`); + else { + router.replace(`?${searchParam}`); + } + }; + + const prevStep = () => { + if (currentStep && activeStepIndex > 0) { + updateStep(steps[activeStepIndex - 1]); + } else { + router.back(); + } + }; + + const FunnelComponent: RouteFunnel = (props) => { + return ( + + {props.children} + + ); + }; + + const Step: RouterFunnelStep = ({ name, children }) => { + if (name === currentStep) { + return <>{children}; + } + return <>; + }; + + return [ + Object.assign(FunnelComponent, { Step }), + updateStep, + prevStep, + activeStepIndex, + ]; +} + +export default useFunnel; From ab3e4d7417e522a911c041a471cbc9ee13c20b39 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Wed, 4 Sep 2024 10:47:10 +0900 Subject: [PATCH 08/61] =?UTF-8?q?RAC-428=20feat:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=ED=9B=84=20=EC=9E=AC=ED=99=9C=EC=84=B1=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20(#290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: overlay-kit로 메인 페이지 검색, 로그인 모달 변경 * feat: kakao 로그인 응답 타입정의 * feat: 탈퇴 회원인 경우 모달 띄우는 로직 추가 * feat: 탈퇴 후 overlay.open으로 모달 열기 * feat: 동의,비동의 시 로직 처리하게 분기 * feat: rejoin 성공 api 연결 * feat: 탈퇴 흐름 구현 훅으로 분리 * feat: 재활성화 모달 최종 구현 * refactor: 비활성화 취소 안되는 문제 수정 * refactor: 불필요 코드 제거 * refactor: overflow hidden 적용 --- package.json | 1 + src/api/auth/login/kakaoAuthFetch.tsx | 27 ++++++ src/api/auth/rejoin/rejoinPatchFetch.ts | 17 ++++ src/api/model.ts | 89 ++++++++++++++++++ src/app/layout.tsx | 56 +++++------ src/app/login/oauth2/code/kakao/page.tsx | 68 +------------- src/app/page.tsx | 47 +++++----- .../signout-type-select/index.tsx | 2 +- .../AccountReactivation.tsx | 60 ++++++++++++ .../Content/AccountReactivation/index.tsx | 3 + src/components/Modal/FullModal/FullModal.tsx | 7 ++ .../Modal/SearchModal/SearchModal.tsx | 1 + .../HomeSearchForm/HomeSearchForm.tsx | 2 +- src/hooks/useKakaoLogin.tsx | 93 +++++++++++++++++++ src/hooks/useModal.ts | 6 +- src/lib/overlay.tsx | 11 +++ src/types/modal/full.ts | 3 +- 17 files changed, 376 insertions(+), 117 deletions(-) create mode 100644 src/api/auth/login/kakaoAuthFetch.tsx create mode 100644 src/api/auth/rejoin/rejoinPatchFetch.ts create mode 100644 src/api/model.ts create mode 100644 src/components/Content/AccountReactivation/AccountReactivation.tsx create mode 100644 src/components/Content/AccountReactivation/index.tsx create mode 100644 src/hooks/useKakaoLogin.tsx create mode 100644 src/lib/overlay.tsx diff --git a/package.json b/package.json index 5af49732..271a4f40 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "jotai": "^2.5.1", "jquery": "^3.7.1", "next": "13.5.4", + "overlay-kit": "^1.4.1", "prettier": "3.0.3", "react": "18.2.0", "react-calendar": "^4.7.0", diff --git a/src/api/auth/login/kakaoAuthFetch.tsx b/src/api/auth/login/kakaoAuthFetch.tsx new file mode 100644 index 00000000..db759c88 --- /dev/null +++ b/src/api/auth/login/kakaoAuthFetch.tsx @@ -0,0 +1,27 @@ +import instance from '@/api/api'; +import { ResponseModel } from '@/api/model'; +import axios from 'axios'; + +export interface KakaoAuthFetchResponse extends ResponseModel { + data: { + accessToken: string; + accessExpiration: number; + refreshToken: string; + role: 'USER' | 'ADMIN' | 'SENIOR'; + isTutorial: boolean; + refreshExpiration: number; + socialId: string; + isDelete: boolean; + }; +} + +export const kakaoAuthFetch = async ({ code }: { code: string }) => { + return await axios.post( + window.location.hostname.includes('localhost') + ? `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/dev/login/KAKAO` + : `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/login/KAKAO`, + { + code: code, + }, + ); +}; diff --git a/src/api/auth/rejoin/rejoinPatchFetch.ts b/src/api/auth/rejoin/rejoinPatchFetch.ts new file mode 100644 index 00000000..66db7f7f --- /dev/null +++ b/src/api/auth/rejoin/rejoinPatchFetch.ts @@ -0,0 +1,17 @@ +import instance from '@/api/api'; +import { KakaoAuthFetchResponse } from '@/api/auth/login/kakaoAuthFetch'; + +interface RejoinFetchRequest { + socialId: string; + rejoin: boolean; +} + +export const rejoinPatchFetch = async ({ + socialId, + rejoin, +}: RejoinFetchRequest) => { + return await instance.patch('/auth/rejoin/KAKAO', { + socialId, + rejoin, + }); +}; diff --git a/src/api/model.ts b/src/api/model.ts new file mode 100644 index 00000000..95f0e966 --- /dev/null +++ b/src/api/model.ts @@ -0,0 +1,89 @@ +export interface ResponseModel { + code: SuccessStatusType | ErrorStatusType; + message: string; +} + +/** + * 성공 코드 + * @see https://www.notion.so/5375748c8c4147ed85f0980d7fc06bcb + */ +export type SuccessStatusType = + | 'UR200' + | 'UR201' + | 'UR202' + | 'UR203' + | 'SNR200' + | 'SNR201' + | 'SNR202' + | 'SNR203' + | 'MT200' + | 'MT201' + | 'MT202' + | 'MT203' + | 'PM200' + | 'PM201' + | 'PM202' + | 'PM203' + | 'RV200' + | 'RV201' + | 'RV202' + | 'RV203' + | 'AU200' + | 'AU201' + | 'AU202' + | 'AU203' + | 'AU204' + | 'AU205' + | 'ACT200' + | 'ACT201' + | 'ACT202' + | 'ACT203' + | 'IMG200' + | 'IMG201' + | 'IMG202' + | 'IMG203' + | 'SLR200' + | 'SLR201' + | 'SLR202' + | 'SLR203' + | 'ADM200' + | 'ADM201'; + +/** + * 에러 코드 + * @see https://www.notion.so/240430-c0e2fd72f06b45028e8e463d6faa32f9 + */ +export type ErrorStatusType = + | 'EX1000' + | 'EX900' + | 'EX901' + | 'EX902' + | 'EX903' + | 'EX904' + | 'EX800' + | 'EX801' + | 'EX802' + | 'EX700' + | 'EX701' + | 'EX702' + | 'EX703' + | 'EX704' + | 'EX705' + | 'EX706' + | 'EX600' + | 'EX601' + | 'EX500' + | 'EX501' + | 'EX400' + | 'EX401' + | 'EX402' + | 'EX403' + | 'EX404' + | 'EX405' + | 'EX406' + | 'EX300' + | 'EX301' + | 'EX302' + | 'EX200' + | 'EX201' + | 'EX202'; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 29989d0a..0fafae5e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,7 +5,7 @@ import StyledComponentsRegistry from '@/lib/registry'; import GTMAnalytics from '@/components/GA/GTM'; import GoogleAnalytics from '@/components/GA/GA'; import { SERVICE_METADATA } from '@/constants/meta/metaData'; - +import OverlayKitProvider from '@/lib/overlay'; export const metadata: Metadata = { title: SERVICE_METADATA.title, description: SERVICE_METADATA.description, @@ -27,32 +27,34 @@ export default function RootLayout({ {process.env.NEXT_PUBLIC_GA4_ID ? : <>} - {children} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + {children} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/login/oauth2/code/kakao/page.tsx b/src/app/login/oauth2/code/kakao/page.tsx index ac3c9714..ca8b1719 100644 --- a/src/app/login/oauth2/code/kakao/page.tsx +++ b/src/app/login/oauth2/code/kakao/page.tsx @@ -1,74 +1,12 @@ 'use client'; import React from 'react'; -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import axios from 'axios'; -import useAuth from '@/hooks/useAuth'; -import { useAtom, useSetAtom } from 'jotai'; -import { socialIdAtom, isTutorialFinished } from '@/stores/signup'; -import Spinner from '@/components/Spinner'; import styled from 'styled-components'; +import Spinner from '@/components/Spinner'; +import useKakaoLogin from '@/hooks/useKakaoLogin'; function KakaoPage() { - const setSocialId = useSetAtom(socialIdAtom); - const setTutorialFinished = useSetAtom(isTutorialFinished); - const router = useRouter(); - const { setAccessToken, setRefreshToken, setUserType } = useAuth(); - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const code = urlParams.get('code'); - axios - .post( - window.location.hostname.includes('localhost') - ? `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/dev/login/KAKAO` - : `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/login/KAKAO`, - { - code: code, - }, - ) - .then((res) => { - const response = res.data; - if (response.code == 'AU205') { - setSocialId(response.data.socialId); - if (typeof window !== undefined) { - window.localStorage.setItem('socialId', response.data.socialId); - } - router.push('/signup/select'); - return; - } - - if (response.code == 'AU204') { - setAccessToken({ - token: response.data.accessToken, - expires: response.data.accessExpiration, - }); - setRefreshToken({ - token: response.data.refreshToken, - expires: response.data.refreshExpiration, - }); - setUserType(response.data.role); - setTutorialFinished(response.data.isTutorial); - - router.replace('/'); - return; - } - - router.replace('/'); - }) - .catch((err) => { - console.error(err); - router.replace('/'); - }); - }, []); - - useEffect(() => { - const loginTimeout = setTimeout(() => { - router.replace('/'); - }, 15000); - - return () => clearTimeout(loginTimeout); - }, []); + useKakaoLogin(); return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index b714f0e1..20f9362e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,20 +7,19 @@ import SeniorProfile from '../components/SeniorProfile/SeniorProfile'; import FieldTapBar from '../components/Bar/FieldTapBar/FieldTapBar'; import UnivTapBar from '../components/Bar/UnivTapBar/UnivTapBar'; import SwiperComponent from '../components/Swiper/Swiper'; -import { createPortal } from 'react-dom'; import useModal from '../hooks/useModal'; import DimmedModal from '../components/Modal/DimmedModal'; import SearchModal from '../components/Modal/SearchModal'; import { sfactiveTabAtom, suactiveTabAtom } from '../stores/tap'; import axios from 'axios'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { isTutorialFinished } from '@/stores/signup'; -import { useTour } from '@reactour/tour'; + import LogoLayer from '@/components/LogoLayer/LogoLayer'; import { listDataAtom, pageNumAtom } from '@/stores/home'; import Footer from '@/components/Footer'; import useTutorial from '@/hooks/useTutorial'; +import { overlay } from 'overlay-kit'; export default function Home() { const { setCurrentPath } = usePrevPath(); @@ -84,15 +83,9 @@ export default function Home() { }; }, [page]); - const { modal, modalHandler, portalElement } = useModal( - 'login-request-portal', - ); + const { modal, modalHandler } = useModal(''); - const { - modal: searchModal, - modalHandler: searchModalHandler, - portalElement: searchPortalElement, - } = useModal('search-portal'); + const { modal: searchModal, modalHandler: searchModalHandler } = useModal(''); return ( @@ -121,17 +114,29 @@ export default function Home() { - {modal && portalElement - ? createPortal( - , - portalElement, - ) + + {modal + ? overlay.open(({ unmount }) => { + return ( + { + unmount(); + }} + /> + ); + }) : ''} - {searchModal && searchPortalElement - ? createPortal( - , - searchPortalElement, - ) + {searchModal + ? overlay.open(({ unmount }) => { + return ( + { + unmount(); + }} + /> + ); + }) : ''} ); diff --git a/src/app/signout/(components)/signout-type-select/index.tsx b/src/app/signout/(components)/signout-type-select/index.tsx index 8d1046e3..90c4c3a0 100644 --- a/src/app/signout/(components)/signout-type-select/index.tsx +++ b/src/app/signout/(components)/signout-type-select/index.tsx @@ -56,7 +56,7 @@ export const SignOutInfoContainer = styled.div` display: flex; flex-direction: column; justify-content: space-between; - height: 100vh; + height: 80vh; > h1 { font-size: 20px; diff --git a/src/components/Content/AccountReactivation/AccountReactivation.tsx b/src/components/Content/AccountReactivation/AccountReactivation.tsx new file mode 100644 index 00000000..37a520b2 --- /dev/null +++ b/src/components/Content/AccountReactivation/AccountReactivation.tsx @@ -0,0 +1,60 @@ +import styled from 'styled-components'; +import NextBtn from '@/components/Button/NextBtn'; + +import { BtnStyleNon } from '@/components/Button/NextBtn/NextBtn.styled'; +interface AccountReactivationProps { + onActive?: () => void; + onNonActive?: () => void; +} + +const Container = styled.div` + width: 100%; + height: 100vh; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + flex-direction: column; + text-align: center; + + h2 { + font-size: 24px; + margin-bottom: 15px; + } + + div { + font-size: 16px; + width: 100%; + margin-bottom: 20px; + } + + @media (max-width: 600px) { + h2 { + font-size: 20px; + } + + div { + font-size: 14px; + } + } +`; + +function AccountReactivation({ + onActive, + onNonActive, +}: AccountReactivationProps) { + return ( + +

계정을 재활성화 하시겠어요?

+
+ “재활성화 합니다.” 버튼을 클릭하시면 계정이 재활성화 되며 계정 + 비활성화가 중단됩니다. +
+ + 아니오. +
+ ); +} + +export default AccountReactivation; diff --git a/src/components/Content/AccountReactivation/index.tsx b/src/components/Content/AccountReactivation/index.tsx new file mode 100644 index 00000000..e736c1a7 --- /dev/null +++ b/src/components/Content/AccountReactivation/index.tsx @@ -0,0 +1,3 @@ +import AccountReactivation from './AccountReactivation'; + +export default AccountReactivation; diff --git a/src/components/Modal/FullModal/FullModal.tsx b/src/components/Modal/FullModal/FullModal.tsx index 8fc7b1ab..e04aeb15 100644 --- a/src/components/Modal/FullModal/FullModal.tsx +++ b/src/components/Modal/FullModal/FullModal.tsx @@ -11,6 +11,7 @@ import SmentoringSpec from '@/components/Mentoring/MentoringSpec/SmentoringSpec/ import SelectCalendar from '@/components/Content/SelectCalendar'; import { firAbleTimeAtom } from '@/stores/mentoring'; import MentoringSpec from '@/components/Mentoring/MentoringSpec/JmentoringSpec'; +import AccountReactivation from '@/components/Content/AccountReactivation'; function FullModal(props: FullModalProps) { return ( <> @@ -18,6 +19,12 @@ function FullModal(props: FullModalProps) { {props.modalType == 'best-case' && ( )} + {props.modalType === 'account-reactive' && ( + + )} {props.modalType == 'login-request' && ( )} diff --git a/src/components/Modal/SearchModal/SearchModal.tsx b/src/components/Modal/SearchModal/SearchModal.tsx index caa9337e..21a32723 100644 --- a/src/components/Modal/SearchModal/SearchModal.tsx +++ b/src/components/Modal/SearchModal/SearchModal.tsx @@ -8,6 +8,7 @@ export default function SearchModal(props: SearchModalProps) { const ModalClick = () => { props.modalHandler(); }; + console.log(props.modalHandler); return ( e.stopPropagation()}> diff --git a/src/components/SingleForm/HomeSearchForm/HomeSearchForm.tsx b/src/components/SingleForm/HomeSearchForm/HomeSearchForm.tsx index 1df749ab..1c307f87 100644 --- a/src/components/SingleForm/HomeSearchForm/HomeSearchForm.tsx +++ b/src/components/SingleForm/HomeSearchForm/HomeSearchForm.tsx @@ -17,8 +17,8 @@ function HomeSearchForm(props: SearchModalProps) { const keyPressDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { - router.push(`/search-results?searchTerm=${searchTerm}`); props.modalHandler(); + router.push(`/search-results?searchTerm=${searchTerm}`); } }; return ( diff --git a/src/hooks/useKakaoLogin.tsx b/src/hooks/useKakaoLogin.tsx new file mode 100644 index 00000000..5bfbf8a4 --- /dev/null +++ b/src/hooks/useKakaoLogin.tsx @@ -0,0 +1,93 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import useAuth from '@/hooks/useAuth'; +import AccountReactivation from '@/components/Content/AccountReactivation'; +import { useSetAtom } from 'jotai'; +import { socialIdAtom, isTutorialFinished } from '@/stores/signup'; +import { overlay } from 'overlay-kit'; +import { + KakaoAuthFetchResponse, + kakaoAuthFetch, +} from '@/api/auth/login/kakaoAuthFetch'; +import { rejoinPatchFetch } from '@/api/auth/rejoin/rejoinPatchFetch'; +import FullModal from '@/components/Modal/FullModal'; + +const useKakaoLogin = () => { + const setSocialId = useSetAtom(socialIdAtom); + const setTutorialFinished = useSetAtom(isTutorialFinished); + const router = useRouter(); + const { setAccessToken, setRefreshToken, setUserType } = useAuth(); + + const setUserContext = (userRes: KakaoAuthFetchResponse) => { + const { + accessExpiration, + accessToken, + refreshToken, + role, + isTutorial, + refreshExpiration, + socialId, + } = userRes.data; + + setAccessToken({ token: accessToken, expires: accessExpiration }); + setRefreshToken({ token: refreshToken, expires: refreshExpiration }); + setUserType(role); + setTutorialFinished(isTutorial); + setSocialId(socialId); + }; + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + + const fetchKakaoData = async () => { + try { + const { data: kakaoAuthFetchRes } = await kakaoAuthFetch({ + code: code ?? '', + }); + + const { socialId, isDelete } = kakaoAuthFetchRes.data; + + if (kakaoAuthFetchRes.code === 'AU204') { + setUserContext(kakaoAuthFetchRes); + router.replace('/'); + return; + } + + if (isDelete) { + const agreeActivateAccount = await overlay.openAsync( + ({ close, unmount }) => ( + { + const res = await rejoinPatchFetch({ + socialId, + rejoin: true, + }); + setUserContext(res.data); + }} + cancelModalHandler={async () => { + await rejoinPatchFetch({ socialId, rejoin: false }).then( + () => { + close(false); + unmount(); + }, + ); + }} + /> + ), + ); + if (!agreeActivateAccount) { + router.push('/signup/select'); + } + } + } catch (error) { + console.error(error); + } + }; + + fetchKakaoData(); + }, []); +}; + +export default useKakaoLogin; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts index 7be5a1d9..888dabde 100644 --- a/src/hooks/useModal.ts +++ b/src/hooks/useModal.ts @@ -6,7 +6,11 @@ function useModal(portalId: string) { const [portalElement, setPortalElement] = useState(null); useEffect(() => { - setPortalElement(document.getElementById(portalId)); + const portalIdEl = document.getElementById(portalId); + if (!portalIdEl) { + return; + } + setPortalElement(portalIdEl); }, [modal]); const modalHandler = () => { diff --git a/src/lib/overlay.tsx b/src/lib/overlay.tsx new file mode 100644 index 00000000..58ca8e45 --- /dev/null +++ b/src/lib/overlay.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { OverlayProvider } from 'overlay-kit'; +import { ReactNode } from 'react'; +export default function OverlayKitProvider({ + children, +}: { + children: ReactNode; +}) { + return {children}; +} diff --git a/src/types/modal/full.ts b/src/types/modal/full.ts index 39d89089..72046e01 100644 --- a/src/types/modal/full.ts +++ b/src/types/modal/full.ts @@ -11,7 +11,8 @@ export type FullModalType = | 'senior-mentoring-time' | 'senior-mentoring-spec' | 'select-date-calendar' - | 'junior-mentoring-spec'; + | 'junior-mentoring-spec' + | 'account-reactive'; export interface FullModalProps { modalType: FullModalType; From 38e0984e84285755768a5587e66f16b3765801cd Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Thu, 12 Sep 2024 22:25:57 +0900 Subject: [PATCH 09/61] =?UTF-8?q?refactor:=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20UI=20=EC=88=98=EC=A0=95=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SeniorProfile/SeniorProfile.styled.ts | 35 +++++++++++----- .../SeniorProfile/SeniorProfile.tsx | 41 ++++++++++--------- src/components/SeniorProfile/constant.ts | 1 + 3 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 src/components/SeniorProfile/constant.ts diff --git a/src/components/SeniorProfile/SeniorProfile.styled.ts b/src/components/SeniorProfile/SeniorProfile.styled.ts index 8e753502..2f390f3e 100644 --- a/src/components/SeniorProfile/SeniorProfile.styled.ts +++ b/src/components/SeniorProfile/SeniorProfile.styled.ts @@ -25,31 +25,43 @@ export const SeniorProfileImg = styled.img` `; export const SeniorProfileInfo = styled.div` - height: 4rem; - margin-top: 1.5rem; + height: 74px; + margin-top: 1rem; + display: flex; + flex-direction: column; + justify-content: space-between; `; export const SPmajor = styled.div` color: #555555; display: flex; + flex-direction: column; font-weight: 600; + font-size: 16px; font-size: 1rem; - #professor-str { + + .professor-str { + margin-top: 3px; + line-height: 20px; + font-size: 14px; font-weight: 600; } + span { + line-height: 20px; + font-size: 14px; + font-weight: 400; + } `; export const SPnickname = styled.div` - font-weight: 600; - color: #333537; + font-weight: 700; + line-height: 16.8px; + color: #21b1a0; padding: 0.1rem 0; - font-size: 1.12rem; + font-size: 12px; display: flex; #nickname-str { - color: #64686c; - font-weight: 400; - font-size: 0.75rem; - margin-top: 0.3rem; + font-weight: 500; } `; @@ -69,7 +81,8 @@ export const Keyword = styled.div` height: 1.75rem; padding: 0.31rem 0.63rem; border-radius: 0.25rem; - background-color: rgba(47, 196, 178, 0.1); + + background: rgba(124, 143, 141, 0.1); margin-left: 0.25rem; font-size: 0.75rem; white-space: nowrap; diff --git a/src/components/SeniorProfile/SeniorProfile.tsx b/src/components/SeniorProfile/SeniorProfile.tsx index 1585da92..16ab593c 100644 --- a/src/components/SeniorProfile/SeniorProfile.tsx +++ b/src/components/SeniorProfile/SeniorProfile.tsx @@ -7,18 +7,18 @@ import { SeniorProfileInfo, SPmajor, SPnickname, - SPField, Skeyword, Keyword, SPWrapper, } from './SeniorProfile.styled'; import { SeniorProfileProps } from '@/types/profile/seniorProfile'; import { useRouter } from 'next/navigation'; +import { SeniorProfileKeyWordMaxLength } from '@/components/SeniorProfile/constant'; import auth from '../../../public/auth_mark.png'; -import arrow from '../../../public/arrow-right-bold.png'; function SeniorProfile({ data }: SeniorProfileProps) { const router = useRouter(); + console.log(data); return ( @@ -30,8 +30,8 @@ function SeniorProfile({ data }: SeniorProfileProps) { - {data.nickName ? data.nickName : ''}  -
선배님 
+ {data.nickName ? data.nickName : ''} +
 선배님 
{data.certification ? ( auth ) : ( @@ -39,28 +39,31 @@ function SeniorProfile({ data }: SeniorProfileProps) { )}
- {data.postgradu ? `[${data.postgradu.replace('학교', '')}]` : ''} -   -
- {data.professor ? `${data.professor} 교수님` : ''} +
+ {data.postgradu + ? `[${data.postgradu.replace('학교', '')}]` + : ''}{' '} + {data.lab} +
+ +
+ {data.professor}  교수님
- {data.lab ? data.lab : ''} - arrow {data.keyword && - data.keyword.map((keyword, index) => ( - {keyword} - ))} + data.keyword + .map((keyword, index) => {keyword}) + .splice(0, SeniorProfileKeyWordMaxLength)} + + {data.keyword.length > SeniorProfileKeyWordMaxLength && ( + + +{data.keyword.length - SeniorProfileKeyWordMaxLength} + + )} ); diff --git a/src/components/SeniorProfile/constant.ts b/src/components/SeniorProfile/constant.ts new file mode 100644 index 00000000..bced6bc2 --- /dev/null +++ b/src/components/SeniorProfile/constant.ts @@ -0,0 +1 @@ +export const SeniorProfileKeyWordMaxLength = 2; From 5417254c6d9f3a95f719f255b06ccd9cb9330daf Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Fri, 13 Sep 2024 00:11:11 +0900 Subject: [PATCH 10/61] =?UTF-8?q?refactor:=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EB=B0=94=20=EB=86=92=EC=9D=B4,=20=ED=99=9C=EC=84=B1=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20(#293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/home-act.png | Bin 1123 -> 519 bytes public/mentoring-act.svg | 6 ++++++ public/myA.svg | 11 +++++++++++ src/components/Bar/MenuBar/MenuBar.styled.ts | 2 +- src/components/Bar/MenuBar/MenuBar.tsx | 4 ++-- 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 public/mentoring-act.svg create mode 100644 public/myA.svg diff --git a/public/home-act.png b/public/home-act.png index 902f1d8a2ed8ca524c48d50f2abc6f97d1a5ad2e..e57f8e8041538089ca603678b7099d9871b47aeb 100644 GIT binary patch literal 519 zcmV+i0{H!jP)Px$!AV3xR9Fe^nd@zXFcgIup(8LtN9YLMp#2%O7%bUT`4KMOAl)F{BqL;mjNlQ{ z+Cacql!qTs)sZ4FB+_@!J$8;s6#d6nN})tKToHwvc&8LDX%5+@w7`273$O-KI}*6= z_AFijLiTtME#M(+e;79-L= zsaXnd1vVw{(&N*HqL`(S&G(F96WWPy8lrl^G%y*%zR!3hWEw?{VH3&*y{hUOMW$8= zuzf|FaZt8dDyAHRX9u$IpPFFR9<1}H#gx`}KjYB`Tu#)40_y_IIrj!j%9Df@DQW3; z+zTuLF8$i9fg;W=Nk-0LhZ3ELax#C;h;z{eG~uy002ov JPDHLkV1lJ-?5O|% literal 1123 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9EO-XP4l)OOlRpde#$ zkh>GZx^prwfgF}}M_)$E)e-c?47?^)}x;TbZFut98HcL2Azb69CYIPqkn1tzNbCAGkM~ES&j}vF9#700XC)N5tUH303 zsBt;_**NQ;9k1MetFqQpmyNerZWs77Rc+Y`akrWoEtLz(?_F4h~h3En1efL)cZ~Zy*(}vKfS95ru_6f|*Tph&V ze(rO5{{QrvIIqXSA(53!j3!;-JRxPNw#99d6yr9f<1O1ff)o#wJ(+Pq#PbnTjB6jK z?nD)}1uIS%D$05)F?IFH2v3?AvV&nIP(akPmGgp-rCL|t5+|07ZW&=$w@Jl}QOd_% z0w+H4Y0#gTe5h2#Qt(0alNkk)p4V7^0PTr!on*?`4Ahk4K1r0ZSq#W<3(8|`21@0) z2Bk3`21fB1ap<-uf0_azMHs_K+CF!!f^wyF9j(_77KbYIS9 znzek^#Ph8zA1?m?RNdSVyJOSuC3pF3<%B-`dLhkd9x_|rxxbb5PXj}^%$7sHrJa~w z?T=!)b6|Isp0H5E$?LL#;X)fc{UyM$*T|5l;6Q-6=auB~f}=N|-RWKMXMOmu zwYSc5TrGCUteF&NQ!}aJgZ`np!rJ_1P3JH4L^0YFOr6>rBmJ<+sPL)K6|NK3KWrlo z-TPa5smqHoCOE%1ysENlTYl#b6)V;$d6RR07*{%7Z?j^wsoeGAr`z0yOD(%axmi>i)P2G~ba_bckWlAg;b3MwH+(4wjEGt3`{sK?9Dm9Le!B_3&#taLeR=0=ZS#fC_;$Tm zGPN)7&6M75smK2q?1aspi#NqB%1EEd6L;jzy~wSX_xx76^SS4CR8hKX-n-|I)~v9t z^U{l~{oIvv`L3R+WKFwQbn~iD=1V|s*vc@P+j{}?YkU diff --git a/public/mentoring-act.svg b/public/mentoring-act.svg new file mode 100644 index 00000000..c15abbf1 --- /dev/null +++ b/public/mentoring-act.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/myA.svg b/public/myA.svg new file mode 100644 index 00000000..6adc6b43 --- /dev/null +++ b/public/myA.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/Bar/MenuBar/MenuBar.styled.ts b/src/components/Bar/MenuBar/MenuBar.styled.ts index 12609185..9f898815 100644 --- a/src/components/Bar/MenuBar/MenuBar.styled.ts +++ b/src/components/Bar/MenuBar/MenuBar.styled.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const MenuContainer = styled.div` border-radius: 0.75rem 0.75rem 0rem 0rem; width: inherit; - height: 3.75rem; + height: 4.875rem; position: absolute; position: fixed; bottom: 0; diff --git a/src/components/Bar/MenuBar/MenuBar.tsx b/src/components/Bar/MenuBar/MenuBar.tsx index dad1c22d..dbf1becb 100644 --- a/src/components/Bar/MenuBar/MenuBar.tsx +++ b/src/components/Bar/MenuBar/MenuBar.tsx @@ -7,10 +7,10 @@ import useAuth from '@/hooks/useAuth'; import Image from 'next/image'; import home from '../../../../public/home.png'; import mentor from '@/../../public/mentor.png'; -import mentorA from '@/../../public/mentoring-act.png'; +import mentorA from '@/../../public/mentoring-act.svg'; import homeA from '@/../../public/home-act.png'; import my from '@/../../public/my.png'; -import myA from '@/../../public/mypage-act.png'; +import myA from '@/../../public/myA.svg'; import { useEffect, useState } from 'react'; import { menuBarAtom } from '@/stores/home'; function MenuBar(props: MenubarProps) { From 36c1329c8686cb6e16fcae90cc8d315f33ba0c54 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sat, 14 Sep 2024 19:18:18 +0900 Subject: [PATCH 11/61] =?UTF-8?q?RAC-430=20Refactor:=20FullModal=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#2?= =?UTF-8?q?91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: useFullmodal 훅과 불필요한 portal 제거 FullModal안에 모달이 있는 경우 모달이 뜨지 않아서 overlaykit로 변경 * refactor: modalHandler 선택적으로 받게 수정 * refactor: 불필요한 modalHandler 제거 * refactor: 안쓰는 변수 제거 * refactor: riseupmodal의 zindex 조정 * refactor: bankmodal element 선언 제거 * refactor: 불필요한 modalHandler 제거 * refactor: junior 멘토링 부분 searchModal구현부 useModal제거 * refactor: search-modal부분 제거 --- src/app/add-profile/page.tsx | 20 +-- src/app/junior/mentoring/page.tsx | 22 +--- src/app/layout.tsx | 10 -- src/app/mypage/page.tsx | 80 +++--------- src/app/page.tsx | 50 +++----- src/app/search-results/page.tsx | 16 --- src/app/senior/edit-profile/page.tsx | 24 ++-- src/app/senior/mentoring/page.tsx | 21 +-- .../common-info/senior-info/major/page.tsx | 1 + .../Bar/TapBar/JuniorTab/JTabBar.tsx | 25 ++-- .../Bar/TapBar/SeniorTab/STabBar.tsx | 53 +++----- .../MBestCaseContent/MBestCaseContent.tsx | 7 - .../Content/ProfileModify/ProfileModify.tsx | 36 +++--- .../Content/SInfoModify/SInfoModify.tsx | 28 ++-- src/components/Modal/FullModal/FullModal.tsx | 120 +++++++++--------- .../Modal/RiseUpModal/RiseUpModal.styled.ts | 1 - .../Modal/SearchModal/SearchModal.tsx | 2 +- .../SeniorManage/SeniorManage.tsx | 70 +++------- src/components/Scheduler/Scheduler.tsx | 31 ++--- src/components/SelectTime/SelectTime.tsx | 27 ++-- .../SingleForm/BankForm/BankForm.tsx | 6 +- src/hooks/useFullModal.tsx | 57 +++++++++ src/types/modal/full.ts | 30 +++-- 23 files changed, 307 insertions(+), 430 deletions(-) create mode 100644 src/hooks/useFullModal.tsx diff --git a/src/app/add-profile/page.tsx b/src/app/add-profile/page.tsx index e8a4a44d..b3dbc411 100644 --- a/src/app/add-profile/page.tsx +++ b/src/app/add-profile/page.tsx @@ -7,7 +7,6 @@ import BackHeader from '@/components/Header/BackHeader'; import { FieldError, useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import FullModal from '@/components/Modal/FullModal'; import ProfileForm from '@/components/SingleForm/ProfileForm'; import SingleValidator from '@/components/Validator/SingleValidator'; @@ -17,7 +16,6 @@ import { PROFILE_SUB_DIRECTION, PROFILE_TITLE, } from '@/constants/form/cProfileForm'; -import useModal from '@/hooks/useModal'; import { sMultiIntroduce, sRecommendedFor, @@ -25,16 +23,18 @@ import { } from '@/stores/senior'; import { useAtom } from 'jotai'; import { useRouter } from 'next/navigation'; -import { createPortal } from 'react-dom'; import styled from 'styled-components'; +import useFullModal from '@/hooks/useFullModal'; function AddProfilePage() { + const { openModal } = useFullModal({ + modalType: 'best-case', + }); const [singleIntro, setSingleIntro] = useAtom(sSingleIntroduce); const [multiIntro, setMultiIntro] = useAtom(sMultiIntroduce); const [recommended, setRecommended] = useAtom(sRecommendedFor); const { register, - trigger, handleSubmit, formState: { errors }, } = useForm({ @@ -48,9 +48,7 @@ function AddProfilePage() { }); const router = useRouter(); - const { modal, modalHandler, portalElement } = useModal( - 'senior-best-case-portal', - ); + const hasErrors = errors.multiIntro || errors.recommended || errors.singleIntro; @@ -126,7 +124,7 @@ function AddProfilePage() { /> )}
- 프로필 예시 보기 + 프로필 예시 보기
{ @@ -139,12 +137,6 @@ function AddProfilePage() { 다음
- {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); } diff --git a/src/app/junior/mentoring/page.tsx b/src/app/junior/mentoring/page.tsx index 7b0a2270..df71992d 100644 --- a/src/app/junior/mentoring/page.tsx +++ b/src/app/junior/mentoring/page.tsx @@ -1,20 +1,14 @@ 'use client'; import TapBar from '@/components/Bar/TapBar/JuniorTab/JTabBar'; import React, { useEffect } from 'react'; -import { createPortal } from 'react-dom'; -import useModal from '@/hooks/useModal'; import styled from 'styled-components'; import MenuBar from '@/components/Bar/MenuBar'; import LogoLayer from '@/components/LogoLayer/LogoLayer'; import SearchModal from '@/components/Modal/SearchModal'; +import { overlay } from 'overlay-kit'; import useAuth from '@/hooks/useAuth'; function JuniorMentoringPage() { - const { - modal: searchModal, - modalHandler: searchModalHandler, - portalElement: searchPortalElement, - } = useModal('search-portal'); const { getAccessToken } = useAuth(); useEffect(() => { @@ -31,19 +25,17 @@ function JuniorMentoringPage() { }); }, []); + const openSearchModal = () => { + overlay.open(({ unmount }) => ); + }; + return (
- + - + - {searchModal && searchPortalElement - ? createPortal( - , - searchPortalElement, - ) - : ''}
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0fafae5e..d10c4b86 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,24 +30,14 @@ export default function RootLayout({ {children}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 0509efdc..bd0cc326 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -9,7 +9,6 @@ import { useAtom } from 'jotai'; import NotLmypage from '../../components/NotLogin/NotLmypage/NotLmypage'; import useModal from '../../hooks/useModal'; import { createPortal } from 'react-dom'; -import FullModal from '../../components/Modal/FullModal'; import DimmedModal from '../../components/Modal/DimmedModal'; import { userType } from '../../types/user/user'; import SalaryBox from '../../components/Box/SalaryBox'; @@ -23,6 +22,8 @@ import findExCode from '@/utils/findExCode'; import Footer from '@/components/Footer'; import { certifiRegAtom, profileRegAtom } from '@/stores/signup'; import { useRouter } from 'next/navigation'; +import useFullModal from '@/hooks/useFullModal'; +import { overlay } from 'overlay-kit'; function MyPage() { const [nickName, setnickName] = useState(null); @@ -32,9 +33,7 @@ function MyPage() { const [certifiReg, setCertifiReg] = useAtom(certifiRegAtom); const [profileReg, setProfileReg] = useAtom(profileRegAtom); const [senior, setSenior] = useAtom(mySeniorId); - const { modal, modalHandler, portalElement } = useModal( - 'login-request-full-portal', - ); + const { modal: BModal, modalHandler: BModalHandler, @@ -45,31 +44,28 @@ function MyPage() { modalHandler: seiorChangemodalHandler, portalElement: seniorChangePortalElement, } = useModal('senior-request-portal'); - const { - modal: searchModal, - modalHandler: searchModalHandler, - portalElement: searchPortalElement, - } = useModal('search-portal'); + const openSearchModal = () => { + overlay.open(({ unmount }) => ); + }; const { modal: suggestModal, modalHandler: suggestModalHandler, portalElement: suggesPortalElement, } = useModal('suggest-mypage-portal'); - const { - modal: infoModal, - modalHandler: infoHandler, - portalElement: infoPortalElement, - } = useModal('senior-info-modify-portal'); + const { modal: authModal, modalHandler: authHandler, portalElement: authPortalElement, } = useModal('senior-auth-portal'); - const { - modal: loginRequestModal, - modalHandler: loginRequestHandler, - portalElement: loginRequestElement, - } = useModal('login-request-portal'); + + const { openModal: openSeniorInfoModifyModal } = useFullModal({ + modalType: 'senior-info-modify', + }); + + const { openModal: openLoginRequestModal } = useFullModal({ + modalType: 'login-request', + }); const { getAccessToken, getUserType, removeTokens } = useAuth(); const [accessTkn, setAccessTkn] = useState(''); @@ -148,7 +144,7 @@ function MyPage() {
- + {accessTkn ? (
) : ( - + )}
- - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : ''} + {seniorChangemodal && seniorChangePortalElement ? createPortal( , - searchPortalElement, - ) - : ''} + {suggestModal && suggesPortalElement ? createPortal( , suggesPortalElement, ) : ''} - {infoModal && infoPortalElement - ? createPortal( - , - infoPortalElement, - ) - : null} - {BModal && BPotalElement - ? createPortal( - , - BPotalElement, - ) - : null} + {authModal && authPortalElement ? createPortal( , - loginRequestElement, - ) - : null}
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 20f9362e..6646dcf0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,6 @@ import SeniorProfile from '../components/SeniorProfile/SeniorProfile'; import FieldTapBar from '../components/Bar/FieldTapBar/FieldTapBar'; import UnivTapBar from '../components/Bar/UnivTapBar/UnivTapBar'; import SwiperComponent from '../components/Swiper/Swiper'; -import useModal from '../hooks/useModal'; import DimmedModal from '../components/Modal/DimmedModal'; import SearchModal from '../components/Modal/SearchModal'; import { sfactiveTabAtom, suactiveTabAtom } from '../stores/tap'; @@ -83,13 +82,15 @@ export default function Home() { }; }, [page]); - const { modal, modalHandler } = useModal(''); - - const { modal: searchModal, modalHandler: searchModalHandler } = useModal(''); - return ( - + { + overlay.open(({ unmount }) => { + return unmount()} />; + }); + }} + /> @@ -112,32 +113,19 @@ export default function Home() {
- + { + overlay.open(({ unmount }) => { + return ( + unmount()} + /> + ); + }); + }} + /> - - {modal - ? overlay.open(({ unmount }) => { - return ( - { - unmount(); - }} - /> - ); - }) - : ''} - {searchModal - ? overlay.open(({ unmount }) => { - return ( - { - unmount(); - }} - /> - ); - }) - : ''} ); } diff --git a/src/app/search-results/page.tsx b/src/app/search-results/page.tsx index 69c8930e..179e35c9 100644 --- a/src/app/search-results/page.tsx +++ b/src/app/search-results/page.tsx @@ -8,9 +8,7 @@ import axios from 'axios'; import Image from 'next/image'; import arrow from '../../../public/arrow.png'; import SearchDropDown from '@/components/DropDown/SearchDropDown'; -import useModal from '@/hooks/useModal'; import SearchModal from '@/components/Modal/SearchModal'; -import { createPortal } from 'react-dom'; import Spinner from '@/components/Spinner'; import { SeniorProfileData } from '@/types/profile/seniorProfile'; @@ -23,11 +21,6 @@ function SearchResultPage() { const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const [length, setLength] = useState(0); - const { - modal: searchModal, - modalHandler: searchModalHandler, - portalElement: searchPortalElement, - } = useModal('search-portal'); useEffect(() => { setIsLoading(true); @@ -110,9 +103,6 @@ function SearchResultPage() { }} /> - - {searchTerm} - 총 {length}건 @@ -136,12 +126,6 @@ function SearchResultPage() {
해당하는 선배가 없어요
)} - {searchModal && searchPortalElement - ? createPortal( - , - searchPortalElement, - ) - : ''} ); } diff --git a/src/app/senior/edit-profile/page.tsx b/src/app/senior/edit-profile/page.tsx index b35b6fa6..a4ef5bec 100644 --- a/src/app/senior/edit-profile/page.tsx +++ b/src/app/senior/edit-profile/page.tsx @@ -34,17 +34,18 @@ import { useAtom, useSetAtom } from 'jotai'; import { useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; +import useFullModal from '@/hooks/useFullModal'; import styled from 'styled-components'; function EditProfilePage() { const { getAccessToken, removeTokens } = useAuth(); const [modalType, setModalType] = useState('postgradu'); const { modal, modalHandler, portalElement } = useModal('senior-info-portal'); - const { - modal: timeModal, - modalHandler: timeModalHandler, - portalElement: timePortalElement, - } = useModal('senior-mentoring-time-portal'); + + const { openModal: openSeniorMentoringTimeModal } = useFullModal({ + modalType: 'senior-mentoring-time', + }); + const [flag, setFlag] = useState(false); const [labFlag, setLabFlag] = useState(false); const [fieldFlag, setFieldFlag] = useState(false); @@ -452,7 +453,7 @@ function EditProfilePage() { >
추가하기 @@ -462,7 +463,7 @@ function EditProfilePage() { ) : (
입력된 정기 일정이 없습니다.
-
+
+ 추가하기
@@ -487,15 +488,6 @@ function EditProfilePage() { portalElement, ) : null} - {timeModal && timePortalElement - ? createPortal( - , - timePortalElement, - ) - : null}
); } diff --git a/src/app/senior/mentoring/page.tsx b/src/app/senior/mentoring/page.tsx index 4295a04e..48abb6a7 100644 --- a/src/app/senior/mentoring/page.tsx +++ b/src/app/senior/mentoring/page.tsx @@ -3,19 +3,16 @@ import STapBar from '@/components/Bar/TapBar/SeniorTab/STabBar'; import React, { useEffect } from 'react'; import styled from 'styled-components'; import useAuth from '@/hooks/useAuth'; -import { createPortal } from 'react-dom'; -import useModal from '@/hooks/useModal'; import LogoLayer from '@/components/LogoLayer/LogoLayer'; import MenuBar from '@/components/Bar/MenuBar'; import SearchModal from '@/components/Modal/SearchModal'; +import { overlay } from 'overlay-kit'; function SeniorMentoringPage() { - const { - modal: searchModal, - modalHandler: searchModalHandler, - portalElement: searchPortalElement, - } = useModal('search-portal'); const { getAccessToken } = useAuth(); + const openSearchModal = () => { + overlay.open(({ unmount }) => ); + }; useEffect(() => { getAccessToken().then((tkn) => { @@ -33,17 +30,11 @@ function SeniorMentoringPage() { return (
- + - + - {searchModal && searchPortalElement - ? createPortal( - , - searchPortalElement, - ) - : ''}
); } diff --git a/src/app/signup/select/common-info/senior-info/major/page.tsx b/src/app/signup/select/common-info/senior-info/major/page.tsx index a01a38a8..95df0df6 100644 --- a/src/app/signup/select/common-info/senior-info/major/page.tsx +++ b/src/app/signup/select/common-info/senior-info/major/page.tsx @@ -7,6 +7,7 @@ import useModal from '@/hooks/useModal'; import { sMajorAtom, sPostGraduAtom } from '@/stores/senior'; import { ModalType } from '@/types/modal/riseUp'; import { useAtomValue } from 'jotai'; +import { overlay } from 'overlay-kit'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; diff --git a/src/components/Bar/TapBar/JuniorTab/JTabBar.tsx b/src/components/Bar/TapBar/JuniorTab/JTabBar.tsx index 7064de4a..f0899579 100644 --- a/src/components/Bar/TapBar/JuniorTab/JTabBar.tsx +++ b/src/components/Bar/TapBar/JuniorTab/JTabBar.tsx @@ -22,9 +22,9 @@ import useModal from '@/hooks/useModal'; import { ModalMentoringType } from '@/types/modal/mentoringDetail'; import { createPortal } from 'react-dom'; import DimmedModal from '@/components/Modal/DimmedModal'; -import FullModal from '@/components/Modal/FullModal'; import { useRouter } from 'next/navigation'; import findExCode from '@/utils/findExCode'; +import useFullModal from '@/hooks/useFullModal'; import { JMCancelAtom } from '@/stores/condition'; import { REVIEW_FORM_URL } from '@/constants/form/reviewForm'; import { StyledSModalBtn } from '@/components/Button/ModalBtn/ModalBtn.styled'; @@ -42,6 +42,9 @@ function convertDateType(date: string) { return new Date(year, month, day, hour, minute); } function TabBar() { + const { openModal: openJuniorMentoringSpecModal } = useFullModal({ + modalType: 'junior-mentoring-spec', + }); const router = useRouter(); const [modalType, setModalType] = useState('junior'); const [activeTab, setActiveTab] = useAtom(activeTabAtom); @@ -50,9 +53,7 @@ function TabBar() { setActiveTab(tabIndex); }; const { getAccessToken, removeTokens } = useAuth(); - const { modal, modalHandler, portalElement } = useModal( - 'junior-mentoring-detail', - ); + const { modal: cancelModal, modalHandler: cancelModalHandler, @@ -146,7 +147,7 @@ function TabBar() { { setModalType('junior'); setSelectedMentoringId(el.mentoringId); @@ -171,7 +172,7 @@ function TabBar() { { setModalType('junior'); setSelectedMentoringId(el.mentoringId); @@ -229,17 +230,7 @@ function TabBar() { {renderTabContent()} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} + {cancelModal && cancelPortalElement ? createPortal( { setActiveTab(tabIndex); }; + + const { openModal: openAcceptMentoringModal } = useFullModal({ + modalType: 'accept-mentoring', + }); const { getAccessToken, removeTokens } = useAuth(); - const { modal, modalHandler, portalElement } = useModal( - 'senior-mentoring-detail', - ); + const { modal: cancelModal, modalHandler: cancelModalHandler, portalElement: cancelPortalElement, } = useModal('senior-mentoring-cancel'); - const { - modal: acceptModal, - modalHandler: acceptModalHandler, - portalElement: acceptPortalElement, - } = useModal('senior-mentoring-accept'); + const { modal: successModal, modalHandler: successModalHandler, @@ -63,6 +57,14 @@ function STabBar() { null, ); const [prevMentoringInfoLength, setPrevMentoringInfoLength] = useState(0); + + const { openModal: openSeniorMentoringSpecModal } = useFullModal({ + modalType: 'senior-mentoring-spec', + mentoringId: selectedMentoringId ?? 0, + cancelModalHandler: cancelModalHandler, + acceptModalHandler: openAcceptMentoringModal, + }); + const SMCancel = useAtomValue(SMCancelAtom); useEffect(() => { if (SMCancel === true) { @@ -117,7 +119,7 @@ function STabBar() { ? '신청서 보고 수락하기' : '신청서 보기' } - modalHandler={modalHandler} + modalHandler={openSeniorMentoringSpecModal} onClick={() => { setModalType('senior'); setSelectedMentoringId(el.mentoringId); @@ -174,18 +176,7 @@ function STabBar() { {renderTabContent()} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} + {cancelModal && cancelPortalElement ? createPortal( , - acceptPortalElement, - ) - : null} + {successModal && cancelPortalElement ? createPortal( void }) { diff --git a/src/components/Content/ProfileModify/ProfileModify.tsx b/src/components/Content/ProfileModify/ProfileModify.tsx index a96f3ec5..d971beaf 100644 --- a/src/components/Content/ProfileModify/ProfileModify.tsx +++ b/src/components/Content/ProfileModify/ProfileModify.tsx @@ -15,8 +15,6 @@ import SingleValidator from '@/components/Validator/SingleValidator'; import ClickedBtn from '@/components/Button/ClickedBtn'; import useAuth from '@/hooks/useAuth'; import axios from 'axios'; -import useModal from '@/hooks/useModal'; -import { createPortal } from 'react-dom'; import RiseUpModal from '@/components/Modal/RiseUpModal'; import { ModalType } from '@/types/modal/riseUp'; import { useAtom } from 'jotai'; @@ -29,6 +27,7 @@ import { import Scheduler from '@/components/Scheduler'; import { useRouter } from 'next/navigation'; import findExCode from '@/utils/findExCode'; +import { overlay } from 'overlay-kit'; function ProfileModify({ modalHandler }: { modalHandler: () => void }) { const router = useRouter(); @@ -45,11 +44,6 @@ function ProfileModify({ modalHandler }: { modalHandler: () => void }) { const [times, setTimes] = useAtom(sAbleTime); const [submitFlag, setSubmitFlag] = useState(false); const { getAccessToken, removeTokens } = useAuth(); - const { - modal, - modalHandler: infoHandler, - portalElement, - } = useModal('senior-info-portal'); useEffect(() => { getAccessToken().then((accessTkn) => { @@ -91,12 +85,30 @@ function ProfileModify({ modalHandler }: { modalHandler: () => void }) { const clickKeyword = () => { setModalType('keyword'); - infoHandler(); + overlay.open(({ unmount }) => { + return ( + { + unmount(); + }} + /> + ); + }); }; const clickField = () => { setModalType('field'); - infoHandler(); + overlay.open(({ unmount }) => { + return ( + { + unmount(); + }} + /> + ); + }); }; const dedupeInTotalField = (fields: Array) => { @@ -249,12 +261,6 @@ function ProfileModify({ modalHandler }: { modalHandler: () => void }) { - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); } diff --git a/src/components/Content/SInfoModify/SInfoModify.tsx b/src/components/Content/SInfoModify/SInfoModify.tsx index 9dd1b841..3cec7d57 100644 --- a/src/components/Content/SInfoModify/SInfoModify.tsx +++ b/src/components/Content/SInfoModify/SInfoModify.tsx @@ -7,7 +7,6 @@ import { ValidatorBox, } from './SInfoModify.styled'; import x_icon from '../../../../public/x.png'; -import user_icon from '../../../../public/user.png'; import camera_icon from '../../../../public/camera.png'; import Image from 'next/image'; import RoundedImage from '@/components/Image/RoundedImage'; @@ -15,13 +14,8 @@ import NicknameForm from '@/components/SingleForm/NicknameForm'; import PhoneNumForm from '@/components/SingleForm/PhoneNumForm'; import { useEffect, useRef, useState } from 'react'; import SingleValidator from '@/components/Validator/SingleValidator'; -import ClickedBtn from '@/components/Button/ClickedBtn'; import useAuth from '@/hooks/useAuth'; import axios from 'axios'; -import { - StaticImageData, - StaticImport, -} from 'next/dist/shared/lib/get-img-props'; import { useAtom, useAtomValue } from 'jotai'; import { changeNickname, @@ -34,11 +28,9 @@ import { import NextBtn from '@/components/Button/NextBtn'; import ModalBtn from '@/components/Button/ModalBtn'; import { bankNameAtom } from '@/stores/bankName'; +import { overlay } from 'overlay-kit'; import { ModalType } from '@/types/modal/riseUp'; -import useModal from '@/hooks/useModal'; import RiseUpModal from '@/components/Modal/RiseUpModal'; -import { createPortal } from 'react-dom'; -import { useRouter } from 'next/navigation'; import findExCode from '@/utils/findExCode'; function SInfoModify({ @@ -48,13 +40,8 @@ function SInfoModify({ bModalHandler: () => void; modalHandler: () => void; }) { - const router = useRouter(); const [flag, setFlag] = useState(false); - const { - modal: BModal, - modalHandler: BModalHandler, - portalElement: BPotalElement, - } = useModal('senior-info-portal'); + const [modalType, setModalType] = useState('bank'); const [submitFlag, setSubmitFlag] = useState(false); const [accHolder, setAccHolder] = useState(''); @@ -270,6 +257,17 @@ function SInfoModify({ modalHandler={bModalHandler} onClick={() => { setModalType('bank'); + overlay.open(({ unmount, close }) => { + return ( + { + close(); + unmount(); + }} + modalType="bank" + /> + ); + }); }} />
diff --git a/src/components/Modal/FullModal/FullModal.tsx b/src/components/Modal/FullModal/FullModal.tsx index e04aeb15..77e32c7d 100644 --- a/src/components/Modal/FullModal/FullModal.tsx +++ b/src/components/Modal/FullModal/FullModal.tsx @@ -12,69 +12,67 @@ import SelectCalendar from '@/components/Content/SelectCalendar'; import { firAbleTimeAtom } from '@/stores/mentoring'; import MentoringSpec from '@/components/Mentoring/MentoringSpec/JmentoringSpec'; import AccountReactivation from '@/components/Content/AccountReactivation'; + function FullModal(props: FullModalProps) { return ( - <> - - {props.modalType == 'best-case' && ( - - )} - {props.modalType === 'account-reactive' && ( - - )} - {props.modalType == 'login-request' && ( - - )} - {props.modalType == 'senior-my-profile' && ( - - )} - {props.modalType == 'junior-mentoring-spec' && ( - {} - } - mentoringId={props.mentoringId ? props.mentoringId : 0} - /> - )} - {props.modalType == 'profile-modify' && ( - - )} - {props.modalType == 'accept-mentoring' && ( - - )} - {props.modalType == 'senior-info-modify' && ( - {}} - modalHandler={props.modalHandler} - /> - )} - {props.modalType == 'senior-mentoring-time' && ( - - )} - {props.modalType == 'senior-mentoring-spec' && ( - {} - } - modalHandler={props.modalHandler} - acceptModalHandler={ - props.acceptModalHandler ? props.acceptModalHandler : () => {} - } - mentoringId={props.mentoringId ? props.mentoringId : 0} - /> - )} - {props.modalType == 'select-date-calendar' && ( - - )} - - + + {(() => { + switch (props.modalType) { + case 'best-case': + return ; + case 'account-reactive': + return ( + + ); + case 'login-request': + return ; + case 'senior-my-profile': + return ; + case 'junior-mentoring-spec': + return ( + {})} + mentoringId={props.mentoringId || 0} + /> + ); + case 'profile-modify': + return ; + case 'accept-mentoring': + return ; + case 'senior-info-modify': + return ( + {})} + modalHandler={props.modalHandler} + /> + ); + case 'senior-mentoring-time': + return ; + case 'senior-mentoring-spec': + return ( + {})} + modalHandler={props.modalHandler} + acceptModalHandler={props.acceptModalHandler || (() => {})} + mentoringId={props.mentoringId || 0} + /> + ); + case 'select-date-calendar': + return ( + + ); + default: + return null; + } + })()} + ); } diff --git a/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts b/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts index 89478cf9..cf99d914 100644 --- a/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts +++ b/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts @@ -7,7 +7,6 @@ export const ModalBackground = styled.div` transform: translateY(-50%); width: 360px; height: 100vh; - z-index: 2; @keyframes modalAppear { from { diff --git a/src/components/Modal/SearchModal/SearchModal.tsx b/src/components/Modal/SearchModal/SearchModal.tsx index 21a32723..ab115990 100644 --- a/src/components/Modal/SearchModal/SearchModal.tsx +++ b/src/components/Modal/SearchModal/SearchModal.tsx @@ -8,7 +8,7 @@ export default function SearchModal(props: SearchModalProps) { const ModalClick = () => { props.modalHandler(); }; - console.log(props.modalHandler); + return ( e.stopPropagation()}> diff --git a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx index 7c5afb2e..a61f281c 100644 --- a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx +++ b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx @@ -1,26 +1,21 @@ import { - SeniorManageAuthBox, SeniorManageContainer, SeniorManageContentContainer, - SeniorManageAuthValue, } from './SeniorManage.styled'; import ContentComponent from '../../Box/ContentBox'; import TitleComponent from '../../Box/TitleBox'; import { SeniorManageProps } from '@/types/profile/seniorManage'; -import { certiRegType } from '@/types/profile/profile'; + import useModal from '@/hooks/useModal'; import { createPortal } from 'react-dom'; -import FullModal from '@/components/Modal/FullModal'; -import DimmedModal from '@/components/Modal/DimmedModal'; import Router, { useRouter } from 'next/navigation'; -import { mySeniorId } from '@/stores/senior'; import useAuth from '@/hooks/useAuth'; import axios from 'axios'; -import { userType } from '@/types/user/user'; import { socialIdAtom, userTypeAtom } from '@/stores/signup'; import { useAtom, useSetAtom } from 'jotai'; -import { useEffect } from 'react'; import findExCode from '@/utils/findExCode'; +import useFullModal from '@/hooks/useFullModal'; +import DimmedModal from '@/components/Modal/DimmedModal'; function SeniorManage(props: SeniorManageProps) { const router = useRouter(); const { @@ -30,26 +25,25 @@ function SeniorManage(props: SeniorManageProps) { setRefreshToken, removeTokens, } = useAuth(); - const { modal, modalHandler, portalElement } = useModal( - 'senior-my-profile-portal', - ); + const [socialId, setSocialId] = useAtom(socialIdAtom); const setuserTypeAtom = useSetAtom(userTypeAtom); - const { - modal: modifyModal, - modalHandler: modifyHandler, - portalElement: modifyPortal, - } = useModal('profile-modify-portal'); + + const { openModal: _openSeniorMyProfileModal } = useFullModal({ + modalType: 'senior-my-profile', + }); + + const { openModal: openSeniorInfoModifyModal } = useFullModal({ + modalType: 'senior-info-modify', + bModalHandler: props.modalHandler, + }); + const { modal: setJModal, modalHandler: juniorHandler, portalElement: juniorPortal, } = useModal('junior-request-portal'); - const { - modal: infoModal, - modalHandler: infoHandler, - portalElement: infoPortal, - } = useModal('senior-info-modify-portal'); + const { modal: registerModal, modalHandler: registerHandler, @@ -167,7 +161,10 @@ function SeniorManage(props: SeniorManageProps) { - + - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} - {modifyModal && modifyPortal - ? createPortal( - , - modifyPortal, - ) - : null} - {infoModal && infoPortal - ? createPortal( - , - infoPortal, - ) - : null} + {registerModal && registerPortal ? createPortal( { setTimeData(timeData.filter((_, idx) => idx !== removeIdx)); }; @@ -37,7 +35,9 @@ function Scheduler() {
{PROFILE_SUB_DIRECTION.addTimeEmpty}
- +추가하기 + + +추가하기 + ) : ( @@ -64,18 +64,11 @@ function Scheduler() { ))} - +추가하기 + + +추가하기 +
)} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : ''}
); diff --git a/src/components/SelectTime/SelectTime.tsx b/src/components/SelectTime/SelectTime.tsx index 8fd41a45..cdccf0e2 100644 --- a/src/components/SelectTime/SelectTime.tsx +++ b/src/components/SelectTime/SelectTime.tsx @@ -8,19 +8,19 @@ import { } from './SelectTime.styled'; import Image from 'next/image'; import down_arrow from '../../../public/arrow-down.png'; -import useModal from '@/hooks/useModal'; -import { createPortal } from 'react-dom'; -import FullModal from '../Modal/FullModal'; import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; import { MENTORING_SCHEDULE } from '@/constants/form/cMentoringApply'; +import useFullModal from '@/hooks/useFullModal'; function SelectTime(props: SelectTimeProps) { const targetAtomValue = useAtomValue(props.targetAtom); const [thisFlag, setThisFlag] = useState(false); - const { modal, modalHandler, portalElement } = useModal( - 'select-date-calendar', - ); + + const { openModal: openSelectDateCalendarModal } = useFullModal({ + modalType: 'select-date-calendar', + targetAtom: props.targetAtom, + }); const [inputValue, setInputValue] = useState( `${props.numStr}${MENTORING_SCHEDULE.selectPlaceholder}`, ); @@ -60,21 +60,14 @@ function SelectTime(props: SelectTimeProps) { return ( - + {inputValue} 아래 화살표 - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} {thisFlag && ( 일정이 선택되지 않았습니다. diff --git a/src/components/SingleForm/BankForm/BankForm.tsx b/src/components/SingleForm/BankForm/BankForm.tsx index d515b7a3..ba93235b 100644 --- a/src/components/SingleForm/BankForm/BankForm.tsx +++ b/src/components/SingleForm/BankForm/BankForm.tsx @@ -43,8 +43,8 @@ function BankForm({ clickHandler }: { clickHandler: () => void }) { const [bank, setBank] = useAtom(bankNameAtom); const handleClick = (selectedBank: string) => { - setBank(selectedBank); clickHandler(); + setBank(selectedBank); }; return ( @@ -52,7 +52,9 @@ function BankForm({ clickHandler }: { clickHandler: () => void }) {

은행선택

{ + clickHandler(); + }} src={x_btn} alt="x_btn" width={21} diff --git a/src/hooks/useFullModal.tsx b/src/hooks/useFullModal.tsx new file mode 100644 index 00000000..b4d3ebf8 --- /dev/null +++ b/src/hooks/useFullModal.tsx @@ -0,0 +1,57 @@ +import FullModal from '@/components/Modal/FullModal'; +import { FullModalProps } from '@/types/modal/full'; +import { overlay } from 'overlay-kit'; +import { useState } from 'react'; + +interface UseFullModalProps extends FullModalProps { + overlayId?: string; +} +const useFullModal = ({ ...props }: Partial) => { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => { + setIsOpen(true); + overlay.open( + ({ unmount }) => { + return ( + { + if (props.modalHandler) { + props.modalHandler(); + } + closeModal(unmount); + }} + cancelModalHandler={() => { + if (props.cancelModalHandler) { + props.cancelModalHandler(); + } + closeModal(unmount); + }} + /> + ); + }, + { + overlayId: props.overlayId ?? '', + }, + ); + }; + + const closeModal = (unmount: () => void) => { + setIsOpen(false); + unmount(); + }; + + const toggleModal = () => { + if (isOpen) { + closeModal(() => {}); + } else { + openModal(); + } + }; + + return { openModal, closeModal, toggleModal, isOpen }; +}; + +export default useFullModal; diff --git a/src/types/modal/full.ts b/src/types/modal/full.ts index 72046e01..dac23660 100644 --- a/src/types/modal/full.ts +++ b/src/types/modal/full.ts @@ -1,21 +1,23 @@ import { PrimitiveAtom } from 'jotai'; /** FullModal로 띄울 컨텐츠 새로 구현할 때마다 타입으로 추가 */ -export type FullModalType = - | 'best-case' - | 'login-request' - | 'senior-my-profile' - | 'profile-modify' - | 'accept-mentoring' - | 'senior-info-modify' - | 'senior-mentoring-time' - | 'senior-mentoring-spec' - | 'select-date-calendar' - | 'junior-mentoring-spec' - | 'account-reactive'; -export interface FullModalProps { - modalType: FullModalType; +export interface FullModalType { + modalType: + | 'best-case' + | 'login-request' + | 'senior-my-profile' + | 'profile-modify' + | 'accept-mentoring' + | 'senior-info-modify' + | 'senior-mentoring-time' + | 'senior-mentoring-spec' + | 'select-date-calendar' + | 'junior-mentoring-spec' + | 'account-reactive'; +} + +export interface FullModalProps extends FullModalType { modalHandler: () => void; bModalHandler?: () => void; selectedMentoringId?: number; From 9dd2984d50f14ee761a81f936d9323d6901d9526 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sat, 14 Sep 2024 20:08:01 +0900 Subject: [PATCH 12/61] =?UTF-8?q?RAC-430=20Refactor:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20portal=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20?= =?UTF-8?q?RiseUpModal=20overlay-kit=EB=A1=9C=20=EA=B4=80=EB=A6=AC=20(#294?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: useFullmodal 훅과 불필요한 portal 제거 FullModal안에 모달이 있는 경우 모달이 뜨지 않아서 overlaykit로 변경 * refactor: modalHandler 선택적으로 받게 수정 * refactor: 불필요한 modalHandler 제거 * refactor: 안쓰는 변수 제거 * refactor: riseupmodal의 zindex 조정 * refactor: bankmodal element 선언 제거 * refactor: 불필요한 modalHandler 제거 * refactor: junior 멘토링 부분 searchModal구현부 useModal제거 * refactor: search-modal부분 제거 * refactor: senior-info-portal부분 제거 * refactor: DimmenedModal 제외 불필요한 portal 제거 --- src/app/layout.tsx | 5 +-- .../[seniorId]/question/page.tsx | 21 +++++-------- src/app/mypage/page.tsx | 14 ++++++--- src/app/senior/account/page.tsx | 17 +++++----- src/app/senior/edit-profile/page.tsx | 24 +++++++------- .../common-info/senior-info/field/page.tsx | 31 +++++-------------- .../common-info/senior-info/major/page.tsx | 27 +++++++++------- .../SmentoringCancel.styled.ts | 2 +- .../SmentoringCancel/SmentoringCancel.tsx | 3 -- .../ShortRiseUpModal.styled.ts | 1 - 10 files changed, 64 insertions(+), 81 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d10c4b86..3983a7c1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -29,13 +29,10 @@ export default function RootLayout({ {children} -
-
-
@@ -43,7 +40,6 @@ export default function RootLayout({
-
@@ -51,3 +47,4 @@ export default function RootLayout({ ); } + diff --git a/src/app/mentoring-apply/[seniorId]/question/page.tsx b/src/app/mentoring-apply/[seniorId]/question/page.tsx index 22a9ed38..5ff12632 100644 --- a/src/app/mentoring-apply/[seniorId]/question/page.tsx +++ b/src/app/mentoring-apply/[seniorId]/question/page.tsx @@ -8,18 +8,16 @@ import { MENTORING_NOTICE, MENTORING_QUESTION, } from '@/constants/form/cMentoringApply'; -import useModal from '@/hooks/useModal'; import { questionAtom, subjectAtom } from '@/stores/mentoring'; import { useAtomValue } from 'jotai'; +import { overlay } from 'overlay-kit'; import { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; import styled from 'styled-components'; function MentoringApplyQuestionPage() { const [active, setActive] = useState(false); const subject = useAtomValue(subjectAtom); const question = useAtomValue(questionAtom); - const { modal, portalElement, modalHandler } = useModal('pay-amount-portal'); if (typeof window !== 'undefined') { window.localStorage.setItem('topic', subject); @@ -32,7 +30,13 @@ function MentoringApplyQuestionPage() { }, [subject, question]); const clickHandler = () => { - if (subject.length > 9 && question.length > 9) modalHandler(); + if (subject.length > 9 && question.length > 9) { + overlay.open(({ unmount }) => { + return ( + + ); + }); + } }; return ( @@ -77,15 +81,6 @@ function MentoringApplyQuestionPage() { 다음으로 - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index bd0cc326..487410f3 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -44,9 +44,7 @@ function MyPage() { modalHandler: seiorChangemodalHandler, portalElement: seniorChangePortalElement, } = useModal('senior-request-portal'); - const openSearchModal = () => { - overlay.open(({ unmount }) => ); - }; + const { modal: suggestModal, modalHandler: suggestModalHandler, @@ -144,7 +142,13 @@ function MyPage() {
- + { + overlay.open(({ unmount }) => { + return ; + }); + }} + /> {accessTkn ? (
('bank'); const [flag, setFlag] = useState(false); const [accountNumber, setAccountNumber] = useState(''); @@ -81,6 +82,12 @@ function AccountPage() { } }; + const openRiseUpModal = () => { + overlay.open(({ unmount }) => { + return ; + }); + }; + return ( @@ -112,7 +119,7 @@ function AccountPage() { $isGet={!bank} type="bankInfo" btnText={bank ? bank : '은행을 선택해주세요.'} - modalHandler={modalHandler} + modalHandler={openRiseUpModal} onClick={() => { setModalType('bank'); }} @@ -141,12 +148,6 @@ function AccountPage() { ) : ( 완료 )} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); } diff --git a/src/app/senior/edit-profile/page.tsx b/src/app/senior/edit-profile/page.tsx index a4ef5bec..5d475156 100644 --- a/src/app/senior/edit-profile/page.tsx +++ b/src/app/senior/edit-profile/page.tsx @@ -36,11 +36,11 @@ import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import useFullModal from '@/hooks/useFullModal'; import styled from 'styled-components'; +import { overlay } from 'overlay-kit'; function EditProfilePage() { const { getAccessToken, removeTokens } = useAuth(); const [modalType, setModalType] = useState('postgradu'); - const { modal, modalHandler, portalElement } = useModal('senior-info-portal'); const { openModal: openSeniorMentoringTimeModal } = useFullModal({ modalType: 'senior-mentoring-time', @@ -264,6 +264,12 @@ function EditProfilePage() { setFlag(true); }; + const openRiseUpModal = () => { + overlay.open(({ unmount }) => { + return ; + }); + }; + return (
@@ -297,7 +303,7 @@ function EditProfilePage() { className="modify-btn" onClick={() => { setModalType('field'); - modalHandler(); + openRiseUpModal(); }} > 수정 @@ -306,7 +312,7 @@ function EditProfilePage() { { setModalType('field'); }} @@ -324,7 +330,7 @@ function EditProfilePage() { className="modify-btn" onClick={() => { setModalType('keyword'); - modalHandler(); + openRiseUpModal(); }} > 수정 @@ -333,7 +339,7 @@ function EditProfilePage() { { setModalType('keyword'); }} @@ -482,12 +488,6 @@ function EditProfilePage() { )}
- {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null}
); } @@ -667,4 +667,4 @@ const EPMentoring = styled.div` letter-spacing: -0.03125rem; margin-bottom: 0.56rem; } -`; +`; \ No newline at end of file diff --git a/src/app/signup/select/common-info/senior-info/field/page.tsx b/src/app/signup/select/common-info/senior-info/field/page.tsx index ca9fc913..86ae0087 100644 --- a/src/app/signup/select/common-info/senior-info/field/page.tsx +++ b/src/app/signup/select/common-info/senior-info/field/page.tsx @@ -13,7 +13,6 @@ import { } from '@/stores/senior'; import { changeNickname, phoneNum } from '@/stores/signup'; import { ModalType } from '@/types/modal/riseUp'; -import axios from 'axios'; import { useAtomValue } from 'jotai'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -22,16 +21,15 @@ import { createPortal } from 'react-dom'; import styled from 'styled-components'; import BackHeader from '@/components/Header/BackHeader'; import ProgressBar from '@/components/Bar/ProgressBar'; -import findExCode from '@/utils/findExCode'; import { detectReload, preventClose } from '@/utils/reloadFun'; import { SENIOR_FIELD } from '@/constants/signup/senior'; +import { overlay } from 'overlay-kit'; function SeniorInfoPage() { - const [modalType, setModalType] = useState('postgradu'); const [emptyPart, setEmptyPart] = useState(''); const [flag, setFlag] = useState(false); const [ableSubmit, setAbleSubmit] = useState(false); - const { modal, modalHandler, portalElement } = useModal('senior-info-portal'); + const router = useRouter(); const { getAccessToken, @@ -42,15 +40,6 @@ function SeniorInfoPage() { } = useAuth(); const [socialId, setSocialId] = useState(null); - const phoneNumber = useAtomValue(phoneNum); - const nickName = useAtomValue(changeNickname); - const marketingReceive = useAtomValue(option); - - const certification = useAtomValue(photoUrlAtom); - const sPostGradu = useAtomValue(sPostGraduAtom); - const sMajor = useAtomValue(sMajorAtom); - const sLab = useAtomValue(sLabAtom); - const sProfessor = useAtomValue(sProfessorAtom); const sField = useAtomValue(sFieldAtom); const sKeyword = useAtomValue(sKeywordAtom); @@ -84,13 +73,15 @@ function SeniorInfoPage() { }, [sField, sKeyword]); const fieldHandler = () => { - setModalType('field'); - modalHandler(); + overlay.open(({ unmount }) => { + return ; + }); }; const keywordHandler = () => { - setModalType('keyword'); - modalHandler(); + overlay.open(({ unmount }) => { + return ; + }); }; const formatField = (fields: string) => { @@ -272,12 +263,6 @@ function SeniorInfoPage() { 다음으로 - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); diff --git a/src/app/signup/select/common-info/senior-info/major/page.tsx b/src/app/signup/select/common-info/senior-info/major/page.tsx index 95df0df6..bee79cf3 100644 --- a/src/app/signup/select/common-info/senior-info/major/page.tsx +++ b/src/app/signup/select/common-info/senior-info/major/page.tsx @@ -3,14 +3,13 @@ import ModalBtn from '@/components/Button/ModalBtn'; import NextBtn from '@/components/Button/NextBtn'; import RiseUpModal from '@/components/Modal/RiseUpModal'; import SingleValidator from '@/components/Validator/SingleValidator'; -import useModal from '@/hooks/useModal'; import { sMajorAtom, sPostGraduAtom } from '@/stores/senior'; import { ModalType } from '@/types/modal/riseUp'; import { useAtomValue } from 'jotai'; import { overlay } from 'overlay-kit'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; + import styled from 'styled-components'; import BackHeader from '@/components/Header/BackHeader'; import ProgressBar from '@/components/Bar/ProgressBar'; @@ -21,7 +20,7 @@ function SeniorInfoPage() { const [modalType, setModalType] = useState('postgradu'); const [emptyPart, setEmptyPart] = useState(''); const [flag, setFlag] = useState(false); - const { modal, modalHandler, portalElement } = useModal('senior-info-portal'); + const router = useRouter(); const sPostGradu = useAtomValue(sPostGraduAtom); const sMajor = useAtomValue(sMajorAtom); @@ -81,7 +80,13 @@ function SeniorInfoPage() { btnText={ sPostGradu ? sPostGradu : SENIOR_MAJOR.graduateSchoolPlaceholder } - modalHandler={modalHandler} + modalHandler={() => { + overlay.open(({ unmount }) => { + return ( + + ); + }); + }} onClick={() => { setModalType('postgradu'); }} @@ -95,7 +100,13 @@ function SeniorInfoPage() { $isGet={!sMajor} type="seniorInfo" btnText={sMajor ? sMajor : SENIOR_MAJOR.majorPlaceholder} - modalHandler={modalHandler} + modalHandler={() => { + overlay.open(({ unmount }) => { + return ( + + ); + }); + }} onClick={() => { setModalType('major'); }} @@ -115,12 +126,6 @@ function SeniorInfoPage() { ) : ( )} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); diff --git a/src/components/Mentoring/SmentoringCancel/SmentoringCancel.styled.ts b/src/components/Mentoring/SmentoringCancel/SmentoringCancel.styled.ts index f6e1766f..c13309e3 100644 --- a/src/components/Mentoring/SmentoringCancel/SmentoringCancel.styled.ts +++ b/src/components/Mentoring/SmentoringCancel/SmentoringCancel.styled.ts @@ -63,7 +63,7 @@ export const SMCBgContainer = styled.div` top: 0; left: 50%; transform: translateX(-50%); - z-index: 1; + z-index: 0; background-color: rgba(39, 39, 39, 0.48); `; export const SMCbtnCancelT = styled.button` diff --git a/src/components/Mentoring/SmentoringCancel/SmentoringCancel.tsx b/src/components/Mentoring/SmentoringCancel/SmentoringCancel.tsx index 260d77f6..1b17fbe6 100644 --- a/src/components/Mentoring/SmentoringCancel/SmentoringCancel.tsx +++ b/src/components/Mentoring/SmentoringCancel/SmentoringCancel.tsx @@ -24,9 +24,6 @@ import { SMCbtnCancelT, SMCbtnCancelF, } from './SmentoringCancel.styled'; -import useModal from '@/hooks/useModal'; -import { createPortal } from 'react-dom'; -import DimmedModal from '@/components/Modal/DimmedModal'; function SmentoringCancel(props: ModalMentoringProps) { const { getAccessToken } = useAuth(); diff --git a/src/components/Modal/ShortRiseUpModal/ShortRiseUpModal.styled.ts b/src/components/Modal/ShortRiseUpModal/ShortRiseUpModal.styled.ts index ac60fdd5..423e8d94 100644 --- a/src/components/Modal/ShortRiseUpModal/ShortRiseUpModal.styled.ts +++ b/src/components/Modal/ShortRiseUpModal/ShortRiseUpModal.styled.ts @@ -7,7 +7,6 @@ export const ModalBackground = styled.div` transform: translateY(-50%); width: 360px; height: 100vh; - z-index: 2; @keyframes modalAppear { from { From 50450e2b533548fee3d2ff0f1ef435691dd8bb86 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sat, 21 Sep 2024 17:49:29 +0900 Subject: [PATCH 13/61] =?UTF-8?q?RAC-433:=20Feat:=20=EC=84=A0=EB=B0=B0=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=83=88=EB=A1=9C=EC=9A=B4=20UI=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: userinfo api 훅 레이어 분리 * refactor: 프로필 수정 api 레이어 추가 * refactor: 선배->후배 권한으로 바꿀 시 튜토리얼 여부 상태 변경 * refactor: 토큰이 있고 주니어 일때는 튜토리얼 안띄우게 변경 * refactor: 선배 회원 프로필 수정 멘트 변경 * refactor: 불필요한 변수 제거 * refactor: senior/me api 레이어 분리 및 스키마 생성 * refactor: 스키마 및 api 타입 업데이트 * refacor: keywordform 요구사항 설정 * refactor: SelectForm 유효성 검사 로직 변경 * refactor: 스키마 max 속성 제거 * refactor: 새로운 디자인 대로 스타일 수정 * refactor: 상수 문구 변경 * refactor: SingleValidator 이미지 제거 * refactor: 새로운 디자인대로 최종 구현 완료 * refactor: 상태와 UI 분리, 모든 필드의 유효성 검사 판별 함수 추가 --- src/api/api.ts | 26 +- src/api/user/_images/postUserProfileImage.ts | 24 + src/api/user/info/changeUserInfoFetch.ts | 24 + src/api/user/info/useInfoFetch.ts | 15 + src/api/user/profile/getSeniorProfile.ts | 25 + src/api/user/profile/updateSeniorProfile.ts | 45 ++ src/app/mypage/edit/page.tsx | 122 +-- src/app/mypage/page.tsx | 2 +- .../edit-profile/edit-profile-schema.ts | 41 + src/app/senior/edit-profile/page.tsx | 721 +++++++----------- .../Button/ModalBtn/ModalBtn.styled.ts | 12 +- .../Modal/RiseUpModal/RiseUpModal.tsx | 3 + .../SeniorManage/SeniorManage.tsx | 8 +- .../SingleForm/KeywordForm/KeywordForm.tsx | 61 +- .../ProfileForm/ProfileForm.styled.ts | 43 +- .../SingleForm/ProfileForm/ProfileForm.tsx | 67 +- .../SelectForm/SelectForm.styled.ts | 7 +- .../SingleForm/SelectForm/SelectForm.tsx | 68 +- .../SingleForm/TextForm/TextForm.styled.ts | 12 +- .../SingleForm/TextForm/TextForm.tsx | 13 +- .../SingleValidator/SingleValidator.styled.ts | 4 +- .../SingleValidator/SingleValidator.tsx | 7 +- src/constants/form/cProfileForm.ts | 13 +- src/constants/form/cProfileModifyForm.ts | 2 +- src/hooks/useSEdit.ts | 142 ++++ src/hooks/useTutorial.ts | 7 +- src/types/button/selectedBtn.ts | 2 +- src/types/form/profileForm.tsx | 4 + src/types/form/textForm.ts | 6 +- 29 files changed, 813 insertions(+), 713 deletions(-) create mode 100644 src/api/user/_images/postUserProfileImage.ts create mode 100644 src/api/user/info/changeUserInfoFetch.ts create mode 100644 src/api/user/info/useInfoFetch.ts create mode 100644 src/api/user/profile/getSeniorProfile.ts create mode 100644 src/api/user/profile/updateSeniorProfile.ts create mode 100644 src/app/senior/edit-profile/edit-profile-schema.ts create mode 100644 src/hooks/useSEdit.ts diff --git a/src/api/api.ts b/src/api/api.ts index 5fc31713..131e5d34 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,7 +1,6 @@ import useAuth from '@/hooks/useAuth'; -import axios from 'axios'; -import type { InternalAxiosRequestConfig } from 'axios'; -import { useRouter } from 'next/navigation'; +import findExCode from '@/utils/findExCode'; +import axios, { InternalAxiosRequestConfig } from 'axios'; const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_SERVER_URL, @@ -15,9 +14,9 @@ instance.interceptors.request.use( const accessTkn = await getAccessToken(); if (!accessTkn && typeof window !== 'undefined') { - // refresh token까지 만료된 경우 removeTokens(); window.location.href = '/'; + return Promise.reject(new Error('Access token is missing')); // 에러 반환 } else { config.headers.Authorization = `Bearer ${accessTkn}`; } @@ -25,7 +24,24 @@ instance.interceptors.request.use( return config; }, (error) => { - console.error(error); + console.error('Request error:', error); + return Promise.reject(error); + }, +); + +instance.interceptors.response.use( + (res) => { + const { removeTokens } = useAuth(); + if (findExCode(res.data.code)) { + removeTokens(); + if (typeof window !== 'undefined') { + window.location.reload(); + } + } + return res; + }, + (error) => { + console.error('Response error:', error); return Promise.reject(error); }, ); diff --git a/src/api/user/_images/postUserProfileImage.ts b/src/api/user/_images/postUserProfileImage.ts new file mode 100644 index 00000000..3b1ccf6a --- /dev/null +++ b/src/api/user/_images/postUserProfileImage.ts @@ -0,0 +1,24 @@ +import instance from '@/api/api'; +import { ResponseModel } from '@/api/model'; + +interface PostUserProfileImageResponse extends ResponseModel { + data: { + profileUrl: string; + }; +} + +interface PostUserProfileImageRequest { + profileFile: File; +} + +export const postUserProfileImage = async ({ + profileFile, +}: PostUserProfileImageRequest) => { + const formData = new FormData(); + formData.append('profileFile', profileFile); + + return await instance.post( + '/image/upload/profile', + formData, + ); +}; diff --git a/src/api/user/info/changeUserInfoFetch.ts b/src/api/user/info/changeUserInfoFetch.ts new file mode 100644 index 00000000..2af2547d --- /dev/null +++ b/src/api/user/info/changeUserInfoFetch.ts @@ -0,0 +1,24 @@ +import instance from '@/api/api'; +import { ResponseModel } from '@/api/model'; + +interface ChangeUserInfoFetchResponse extends ResponseModel { + data: {}; +} + +interface ChangeUserInfoFetchRequest { + profile: string; + nickName: string; + phoneNumber?: string; +} + +export const changeUserInfo = async ({ + profile, + nickName, + phoneNumber, +}: ChangeUserInfoFetchRequest) => { + return await instance.patch('/user/me/info', { + profile, + nickName, + phoneNumber, + }); +}; diff --git a/src/api/user/info/useInfoFetch.ts b/src/api/user/info/useInfoFetch.ts new file mode 100644 index 00000000..855adcb8 --- /dev/null +++ b/src/api/user/info/useInfoFetch.ts @@ -0,0 +1,15 @@ +import instance from '@/api/api'; + +import { ResponseModel } from '@/api/model'; + +interface UserInfoFetchResponse extends ResponseModel { + data: { + nickName: string; + profile: string; + phoneNumber: string; + }; +} + +export const userInfoFetch = async () => { + return await instance.get('/user/me/info'); +}; diff --git a/src/api/user/profile/getSeniorProfile.ts b/src/api/user/profile/getSeniorProfile.ts new file mode 100644 index 00000000..4b2a1c51 --- /dev/null +++ b/src/api/user/profile/getSeniorProfile.ts @@ -0,0 +1,25 @@ +import instance from '@/api/api'; +import { ResponseModel } from '@/api/model'; + +interface SeniorProfileFetchResponse extends ResponseModel { + data: { + lab: string; + keyword: string[]; + info: string; + target: string; + chatLink: string; + field: string[]; + oneLiner: string; + times: Time[]; + }; +} + +interface Time { + day: string; + startTime: string; + endTime: string; +} + +export const seniorProfileFetch = async () => { + return await instance.get('/senior/me/profile'); +}; diff --git a/src/api/user/profile/updateSeniorProfile.ts b/src/api/user/profile/updateSeniorProfile.ts new file mode 100644 index 00000000..4ca61b2d --- /dev/null +++ b/src/api/user/profile/updateSeniorProfile.ts @@ -0,0 +1,45 @@ +import instance from '@/api/api'; +import { ResponseModel } from '@/api/model'; +import { TimeObj } from '@/types/scheduler/scheduler'; + +interface UpdateSeniorProfileRequest { + lab: string; + keyword: string; + info: string; + target: string; + chatLink: string; + field: string; + oneLiner: string; + times: TimeObj[]; +} + +interface UpdateSeniorProfileResponse extends ResponseModel { + data: { + seniorId: number; + }; +} + +export const updateSeniorProfile = async ({ + lab, + keyword, + info, + target, + chatLink, + field, + oneLiner, + times, +}: UpdateSeniorProfileRequest) => { + return await instance.patch( + '/senior/me/profile', + { + lab, + keyword, + info, + target, + chatLink, + field, + oneLiner, + times, + }, + ); +}; diff --git a/src/app/mypage/edit/page.tsx b/src/app/mypage/edit/page.tsx index f87dfeb5..4ac4016c 100644 --- a/src/app/mypage/edit/page.tsx +++ b/src/app/mypage/edit/page.tsx @@ -3,7 +3,8 @@ import styled from 'styled-components'; import NicknameForm from '@/components/SingleForm/NicknameForm'; import PhoneNumForm from '@/components/SingleForm/PhoneNumForm'; import React, { useState, useEffect } from 'react'; -import axios from 'axios'; +import { postUserProfileImage } from '@/api/user/_images/postUserProfileImage'; +import { changeUserInfo } from '@/api/user/info/changeUserInfoFetch'; import { useAtom, useAtomValue } from 'jotai'; import { changeNickname, @@ -19,6 +20,7 @@ import Photo from '@/components/Photo'; import useAuth from '@/hooks/useAuth'; import { useRouter } from 'next/navigation'; import BackHeader from '@/components/Header/BackHeader'; +import { userInfoFetch } from '@/api/user/info/useInfoFetch'; import findExCode from '@/utils/findExCode'; function page() { const [photoUrl, setPhotoUrl] = useState(null); @@ -26,109 +28,49 @@ function page() { const [myNickName, setNickName] = useAtom(nickname); const changeNick = useAtomValue(changeNickname); const [phoneNumber, setPhoneNumber] = useAtom(remainPhoneNum); - const [profile, setprofile] = useState(null); + const [profile, setprofile] = useState(''); const selectpPhotoUrl = photoUrl ? URL.createObjectURL(photoUrl) : ''; - const { getAccessToken, removeTokens } = useAuth(); + const { removeTokens } = useAuth(); const router = useRouter(); - const [nickAvail, setNickAvail] = useState(false); + const availability = useAtomValue(notDuplicate); const availablePhone = useAtomValue(phoneNumValidation); const newAvailability = useAtomValue(newNotDuplicate); const sameUser = useAtomValue(sameUserAtom); const fullNum = useAtomValue(phoneNum); useEffect(() => { - getAccessToken().then((token) => { - if (token) { - const headers = { - Authorization: `Bearer ${token}`, - }; - axios - .get(`${process.env.NEXT_PUBLIC_SERVER_URL}/user/me/info`, { - headers, - }) - .then((res) => { - if (findExCode(res.data.code)) { - removeTokens(); - location.reload(); - return; - } - - setNickName(res.data.data.nickName); - setPhoneNumber(res.data.data.phoneNumber); - setprofile(res.data.data.profile); - }) - .catch(function (error) { - console.error(error); - }); + userInfoFetch().then((res) => { + if (findExCode(res.data.code)) { + removeTokens(); + location.reload(); } + const { nickName, profile } = res.data.data; + setNickName(nickName); + setprofile(profile); }); - }); + }, []); const handleClick = async () => { - getAccessToken().then(async (token) => { - if (photoUrl) { - const formData = new FormData(); - formData.append('profileFile', photoUrl); - - if (token) { - await axios - .post( - `${process.env.NEXT_PUBLIC_SERVER_URL}/image/upload/profile`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - Authorization: `Bearer ${token}`, - }, - }, - ) - .then((response) => { - const res = response.data; - if (findExCode(res.code)) { - removeTokens(); - location.reload(); - return; - } - if (res.code == 'IMG202') { - editProfileUrl = res.data.profileUrl; - } - }) - .catch((err) => { - console.error(err); - }); - } + if (photoUrl) { + const { data } = await postUserProfileImage({ + profileFile: photoUrl, + }); + if (data.code === 'IMG202') { + editProfileUrl = data.data.profileUrl; } - if (editProfileUrl || changeNick || fullNum) { - axios - .patch( - `${process.env.NEXT_PUBLIC_SERVER_URL}/user/me/info`, - { - profile: editProfileUrl ? editProfileUrl : profile, - nickName: changeNick ? changeNick : myNickName, - phoneNumber: fullNum ? fullNum : phoneNumber, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ) - .then((response) => { - const res = response.data; - if (findExCode(res.code)) { - removeTokens(); - location.reload(); - return; - } - if (res.code == 'UR201') { - router.push('/mypage'); - } - }) - .catch((err) => { - console.error(err); - }); + } + + if (editProfileUrl || changeNick || fullNum) { + const { data } = await changeUserInfo({ + profile: editProfileUrl ? editProfileUrl : profile, + nickName: changeNick ? changeNick : myNickName, + phoneNumber: fullNum ? fullNum : phoneNumber, + }); + + if (data.code === 'UR201') { + router.push('/mypage'); } - }); + } }; return ( diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index 487410f3..737a113f 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -227,4 +227,4 @@ function MyPage() { ); } -export default MyPage; \ No newline at end of file +export default MyPage; diff --git a/src/app/senior/edit-profile/edit-profile-schema.ts b/src/app/senior/edit-profile/edit-profile-schema.ts new file mode 100644 index 00000000..e72d370b --- /dev/null +++ b/src/app/senior/edit-profile/edit-profile-schema.ts @@ -0,0 +1,41 @@ +import * as yup from 'yup'; + +const timeDataSchema = yup.object().shape({ + day: yup.string().required(), + startTime: yup.string().required(), + endTime: yup.string().required(), +}); + +const scheduleSchema = yup.object().shape({ + times: yup + .array() + .of(timeDataSchema) + .required() + .min(3, '최소 3개 이상 일정을 추가해주세요'), +}); + +export const editProfileSchema = yup.object({ + lab: yup + .string() + .min(1, '연구실 이름을 입력해주세요') + .required('연구실 이름을 입력해주세요'), + field: yup.string().required('최소 1개 이상 선택해주세요').min(1), + keyword: yup.string().required('최소 1개 이상 입력해주세요').min(1), + singleIntro: yup + .string() + .required('최소 10자 이상 작성해주세요.') + .min(10, '최소 10자 이상 작성해주세요.') + .max(100, '100자 이내로 입력해주세요'), + multiIntro: yup + .string() + .required('최소 50자 이상 작성해주세요.') + .min(50, '최소 50자 이상 작성해주세요.') + .max(1000, '1000자 이내로 입력해주세요.'), + recommended: yup + .string() + .required('최소 50자 이상 작성해주세요.') + .min(50, '최소 50자 이상 작성해주세요.') + .max(1000, '1000자 이내로 입력해주세요.'), + chatLink: yup.string().required().min(1), + timeData: scheduleSchema, +}); diff --git a/src/app/senior/edit-profile/page.tsx b/src/app/senior/edit-profile/page.tsx index 5d475156..79a15591 100644 --- a/src/app/senior/edit-profile/page.tsx +++ b/src/app/senior/edit-profile/page.tsx @@ -2,73 +2,65 @@ import ClickedBtn from '@/components/Button/ClickedBtn'; import ModalBtn from '@/components/Button/ModalBtn'; import BackHeader from '@/components/Header/BackHeader'; -import FullModal from '@/components/Modal/FullModal'; import RiseUpModal from '@/components/Modal/RiseUpModal'; import ProfileForm from '@/components/SingleForm/ProfileForm'; -import TextForm from '@/components/SingleForm/TextForm'; -import SingleValidator from '@/components/Validator/SingleValidator'; + +import { TextFormEl } from '@/components/SingleForm/TextForm/TextForm.styled'; import { PROFILE_PLACEHOLDER, PROFILE_TITLE, } from '@/constants/form/cProfileForm'; -import useAuth from '@/hooks/useAuth'; -import useModal from '@/hooks/useModal'; -import { - sAbleTime, - sChatLink, - sFieldAtom, - sKeywordAtom, - sLabAtom, - sMultiIntroduce, - sRecommendedFor, - sSingleIntroduce, - selectedFieldAtom, - selectedKeywordAtom, - totalFieldAtom, - totalKeywordAtom, -} from '@/stores/senior'; -import { ModalType } from '@/types/modal/riseUp'; -import findExCode from '@/utils/findExCode'; -import axios from 'axios'; -import { useAtom, useSetAtom } from 'jotai'; -import { useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; + import useFullModal from '@/hooks/useFullModal'; import styled from 'styled-components'; import { overlay } from 'overlay-kit'; +import { editProfileSchema } from '@/app/senior/edit-profile/edit-profile-schema'; +import { useForm, FormProvider } from 'react-hook-form'; -function EditProfilePage() { - const { getAccessToken, removeTokens } = useAuth(); - const [modalType, setModalType] = useState('postgradu'); +import { yupResolver } from '@hookform/resolvers/yup'; +import useSEdit from '@/hooks/useSEdit'; +import { useEffect, useState } from 'react'; +function EditProfilePage() { const { openModal: openSeniorMentoringTimeModal } = useFullModal({ modalType: 'senior-mentoring-time', }); - const [flag, setFlag] = useState(false); - const [labFlag, setLabFlag] = useState(false); - const [fieldFlag, setFieldFlag] = useState(false); - const [keywordFlag, setKeywordFlag] = useState(false); - const [singleFlag, setSingleFlag] = useState(false); - const [multiFlag, setMultiFlag] = useState(false); - const [recommendFlag, setRecommendFlag] = useState(false); - const [chatLinkFlag, setChatLinkFlag] = useState(false); - const [timeFlag, setTimeFlag] = useState(false); - const [singleIntro, setSingleIntro] = useAtom(sSingleIntroduce); - const [multiIntro, setMultiIntro] = useAtom(sMultiIntroduce); - const [recommended, setRecommended] = useAtom(sRecommendedFor); - const [chatLink, setChatLink] = useAtom(sChatLink); - const [sField, setSfield] = useAtom(sFieldAtom); - const [totalField, setTotalField] = useAtom(totalFieldAtom); - const setSelectedField = useSetAtom(selectedFieldAtom); - const setTotalKeyword = useSetAtom(totalKeywordAtom); - const setSelectedKeyword = useSetAtom(selectedKeywordAtom); - const [sLab, setSlab] = useAtom(sLabAtom); - const [sKeyword, setSkeyword] = useAtom(sKeywordAtom); - const [timeData, setTimeData] = useAtom(sAbleTime); - const router = useRouter(); + const editProfileMethod = useForm({ + resolver: yupResolver(editProfileSchema), + mode: 'all', + }); + + const { + register, + trigger, + setValue, + handleSubmit, + watch, + formState: { errors }, + } = editProfileMethod; + + const { + singleIntro, + setSingleIntro, + multiIntro, + setMultiIntro, + recommended, + setRecommended, + chatLink, + setChatLink, + sField, + allFieldValid, + sLab, + setSlab, + sKeyword, + timeData, + setTimeData, + checkAllFieldIsValid, + handleClickConfirmBtn, + } = useSEdit(); + const [_allFieldState, _setAllFieldState] = useState(checkAllFieldIsValid()); const clickHandler = (removeIdx: number) => { setTimeData(timeData.filter((_, idx) => idx !== removeIdx)); }; @@ -83,429 +75,250 @@ function EditProfilePage() { return resultArray.join(', '); }; - function validateLab() { - if (sLab.length <= 0) setLabFlag(true); - else setLabFlag(false); - } - - function validateField() { - if (sField.length <= 0) setFieldFlag(true); - else setFieldFlag(false); - } - - function validateKeyword() { - if (sKeyword.length <= 0) setKeywordFlag(true); - else setKeywordFlag(false); - } - - function validateSingleIntro() { - if (singleIntro.length < 10) setSingleFlag(true); - else setSingleFlag(false); - } - - function validateMultiIntro() { - if (multiIntro.length < 50) setMultiFlag(true); - else setMultiFlag(false); - } - - function validateRecommended() { - if (recommended.length < 50) setRecommendFlag(true); - else setRecommendFlag(false); - } - - function validateChatLink() { - if (chatLink.length <= 0) setChatLinkFlag(true); - else setChatLinkFlag(false); - } - - useEffect(() => { - validateLab(); - }, [sLab]); - useEffect(() => { - validateField(); - }, [sField]); - useEffect(() => { - validateKeyword(); - }, [sKeyword]); - useEffect(() => { - validateSingleIntro(); - }, [singleIntro]); - useEffect(() => { - validateMultiIntro(); - }, [multiIntro]); - useEffect(() => { - validateRecommended(); - }, [recommended]); - useEffect(() => { - validateChatLink(); - }, [chatLink]); - useEffect(() => { - validateTime(); - }, [timeData]); - - function validateTime() { - if (timeData.length < 3) setTimeFlag(true); - else setTimeFlag(false); - } - - useEffect(() => { - const fetchData = async () => { - getAccessToken().then(async (token) => { - if (!token) { - // 알림톡으로 들어와서 토큰 없을 시, 로그인으로 이동 - const REST_API_KEY = process.env.NEXT_PUBLIC_REST_API_KEY; - const REDIRECT_URI = - window.location.origin + '/login/oauth2/code/kakao'; - const link = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`; - window.location.href = link; - return; - } - - if (token) { - try { - const headers = { - Authorization: `Bearer ${token}`, - }; - - axios - .get(`${process.env.NEXT_PUBLIC_SERVER_URL}/senior/me/profile`, { - headers, - }) - .then((response) => { - const res = response.data; - - if (findExCode(res.code)) { - removeTokens(); - location.reload(); - return; - } - - const tempFields = [...totalField]; - res.data.field.forEach((el: string) => { - if (!tempFields.includes(el)) tempFields.push(el); - }); - - setTimeData(res.data.times ? res.data.times : []); - setTotalField(tempFields); - setSelectedField(res.data.field); - setTotalKeyword(res.data.keyword); - setSelectedKeyword(res.data.keyword); - setSfield(res.data.field.join(',')); - setSkeyword(res.data.keyword.join(',')); - setChatLink(res.data.chatLink ? res.data.chatLink : ''); - setMultiIntro(res.data.info ? res.data.info : ''); - setSingleIntro(res.data.oneLiner ? res.data.oneLiner : ''); - setRecommended(res.data.target ? res.data.target : ''); - setSlab(res.data.lab); - }) - .catch((err) => { - console.error(err); - }); - } catch (error) { - console.error(error); - } - } - }); - }; - - fetchData(); - }, []); - - const handleClick = () => { - const areConditionsMet = - singleIntro.length >= 10 && - multiIntro.length >= 50 && - recommended.length >= 50; - - getAccessToken().then((token) => { - if ( - token && - areConditionsMet && - chatLink && - timeData.length >= 3 && - sField && - sKeyword && - sLab - ) { - setFlag(false); - axios - .patch( - `${process.env.NEXT_PUBLIC_SERVER_URL}/senior/me/profile`, - { - lab: sLab, - keyword: sKeyword, - info: multiIntro, - target: recommended, - chatLink: chatLink, - field: sField, - oneLiner: singleIntro, - times: timeData, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ) - .then((res) => { - if (findExCode(res.data.code)) { - removeTokens(); - location.reload(); - return; - } - router.back(); - }) - .catch(function (error) { - console.error(error); - }); + const openRiseUpModal = (modalType: 'field' | 'keyword') => { + overlay.open(({ unmount }) => { + if (sField === '') { + trigger('field'); + } + if (modalType === 'keyword' && sKeyword === '') { + trigger('keyword'); } - }); - - setFlag(true); - }; - const openRiseUpModal = () => { - overlay.open(({ unmount }) => { - return ; + return ( + + { + unmount(); + if (modalType === 'keyword') { + setValue('keyword', ''); + } + if (modalType === 'field') { + setValue('field', ''); + } + }} + modalType={modalType} + /> + + ); }); }; return (
- - - 프로필 정보 -
- - -
- 연구실명 
*
-
- {labFlag && ( -
 연구실명을 입력해주세요
- )} -
- -
- - -
- 연구분야 
*
- {fieldFlag && ( -
 최소 1개 이상 선택해주세요
+
{ + e.preventDefault(); + handleSubmit(() => handleClickConfirmBtn()); + }} + > + + +
+ + +
연구실 이름
+ {errors.lab?.message && ( +
  {errors.lab.message}
)} -
- - - { - setModalType('field'); + /> + + + +
+ 연구 분야 + {!sField && ( +
 {errors?.field?.message}
+ )} +
+
+ openRiseUpModal('field')} + /> +
+ + +
+ 연구 주제 + {!sKeyword && ( +
 {errors?.keyword?.message}
+ )} +
+
+ openRiseUpModal('keyword')} + /> +
+
+ + setSingleIntro(e)} + errorMessage={errors?.singleIntro?.message} + /> + + setMultiIntro(e)} + errorMessage={errors?.multiIntro?.message} + /> + + { + setRecommended(e); + }} + errorMessage={errors?.recommended?.message} + /> + + +
+
연락 방법
+
+ { + setChatLink(e.currentTarget.value); }} /> -
- - -
- 연구주제 
*
- {keywordFlag && ( -
 최소 1개 이상 입력해주세요
- )} -
- -
- { - setModalType('keyword'); + + +
- -
- 멘토링 정보 - -
- {singleFlag && ( - - )} -
- -
- {multiFlag && ( - - )} -
- -
- {recommendFlag && ( - - )} -
- -
-
카카오톡 오픈 채팅방 링크
-
- 매칭된 후배와 대화할 오픈채팅 방이에요. -
- 비대면 회의 링크나 급한 공지를 전달해요. + > +
가능한 멘토링 일정
+ {errors.timeData?.message && ( +
{errors.timeData.message}
+ )}
-
- { - setChatLink(e.currentTarget.value); - }} - /> -
- -
-
가능 정기일정
- {timeFlag && ( -
최소 3개 이상 일정을 추가해주세요
- )} -
- - {timeData && timeData.length > 0 ? ( - <> - {timeData && - timeData.map((el, idx) => ( - - {el.day}요일 {el.startTime} ~ {el.endTime} -
clickHandler(idx)} - style={{ cursor: 'pointer' }} - > - 삭제 -
-
- ))} -
+ + {timeData && timeData.length > 0 ? ( + <> + {timeData && + timeData.map((el, idx) => ( + + {el.day}요일 {el.startTime} ~ {el.endTime} +
clickHandler(idx)} + style={{ cursor: 'pointer' }} + > + 삭제 +
+
+ ))} +
- 추가하기 + 추가
-
- + + ) : ( + +
+ 가능한 일정을 3개 이상 알려주세요. +
+
+ + 추가 +
+
+ )} +
+
+
+ {allFieldValid ? ( + ) : ( - -
입력된 정기 일정이 없습니다.
-
- + 추가하기 -
-
+ {}} + /> )} - - -
- {chatLink && timeData.length >= 3 ? ( - - ) : ( - - )} -
- +
+ +
); } export default EditProfilePage; -const EditPContainer = styled.div``; +const EditPContainer = styled.div` + display: flex; + justify-content: space-between; + flex-direction: column; +`; const SetDataBox = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; #setData-btn { - display: inline-flex; + display: flex; padding: 0.3125rem 0.625rem; align-items: center; + min-width: 50px; + height: 40px; gap: 0.25rem; + justify-content: center; border-radius: 0.25rem; background: #495565; color: #fff; font-family: Pretendard; - font-size: 0.75rem; + font-size: 13px; font-style: normal; font-weight: 700; line-height: 1.125rem; /* 150% */ @@ -535,19 +348,22 @@ const IntroCardTimeBox = styled.div` `; const SetDataForm = styled.div` margin-left: 1rem; + margin-top: 0.5rem; padding: 0 0.75rem; width: 93%; - height: 3.1875rem; + height: 44px; flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; border-radius: 0.5rem; - background: #f8f9fa; + background: white; + border: 1px solid #dcdfe4; #setDataF-msg { - color: #adb5bd; + color: #a6abb0; font-family: 'Noto Sans JP'; - font-size: 1rem; + font-size: 0.875rem; + font-size: 13px; font-style: normal; font-weight: 400; line-height: normal; @@ -567,7 +383,6 @@ const SetData = styled.div` #setData-title { color: #212529; font-family: Pretendard; - font-size: 1rem; font-style: normal; font-weight: 700; line-height: 140%; /* 1.4rem */ @@ -579,11 +394,13 @@ const MBtnFont = styled.div` display: flex; justify-content: space-between; color: #212529; - font-family: Noto Sans JP; - font-size: 0.875rem; + font-family: Pretendard; + font-size: 1rem; font-style: normal; - font-weight: 400; + font-weight: 700; + margin-bottom: 0.31rem; line-height: normal; + #font-color { color: #00a0e1; font-family: Noto Sans JP; @@ -624,23 +441,25 @@ const EPTitle = styled.div` letter-spacing: -0.03125rem; `; const BtnBox = styled.div` - margin-top: 1rem; + margin-top: 1.8rem; + margin-bottom: 1.8rem; `; const EPMentoring = styled.div` margin-left: 1rem; #add-chat-link-form { width: 95%; - height: 3.1875rem; + height: 44px; flex-shrink: 0; border-radius: 0.5rem; - border: 1px solid #c2cede; + border: 1px solid #dcdfe4; background: #fff; + font-size: 13px; padding: 0.87rem 0.96rem; &::placeholder { color: #adb5bd; font-family: 'Noto Sans JP'; - font-size: 1rem; + font-size: 13px; font-style: normal; font-weight: 400; line-height: normal; @@ -667,4 +486,4 @@ const EPMentoring = styled.div` letter-spacing: -0.03125rem; margin-bottom: 0.56rem; } -`; \ No newline at end of file +`; diff --git a/src/components/Button/ModalBtn/ModalBtn.styled.ts b/src/components/Button/ModalBtn/ModalBtn.styled.ts index 472b1821..70009e5f 100644 --- a/src/components/Button/ModalBtn/ModalBtn.styled.ts +++ b/src/components/Button/ModalBtn/ModalBtn.styled.ts @@ -18,7 +18,7 @@ export const StyledSModalBtn = styled.button` align-items: center; justify-content: center; width: 92%; - height: 2.375rem; + height: 44px; cursor: pointer; border-radius: 0.5rem; background: #2fc4b2; @@ -54,22 +54,22 @@ export const StyledMSBtn = styled.button` export const SInfoBtn = styled.button` color: ${(props) => (props.$isGet ? '#ADB5BD' : '#212529')}; margin-top: 0.5rem; - height: 3.1875rem; + height: 44px; width: 97%; flex-shrink: 0; border-radius: 0.5rem; - border: 1px solid #c2cede; - background: #f8f9fa; + border: 0.8px solid #dcdfe4; padding: 0.8rem; + background-color: white; display: flex; justify-content: space-between; align-items: center; text-align: center; font-family: Pretendard; - font-size: 1rem; + font-size: 13px; font-style: normal; font-weight: 400; line-height: normal; - color: #495565; + color: #a6abb0; cursor: pointer; `; diff --git a/src/components/Modal/RiseUpModal/RiseUpModal.tsx b/src/components/Modal/RiseUpModal/RiseUpModal.tsx index 9f6d8e74..0f92e8cb 100644 --- a/src/components/Modal/RiseUpModal/RiseUpModal.tsx +++ b/src/components/Modal/RiseUpModal/RiseUpModal.tsx @@ -4,6 +4,9 @@ import SearchForm from '@/components/SingleForm/SearchForm'; import SelectForm from '@/components/SingleForm/SelectForm'; import KeywordForm from '@/components/SingleForm/KeywordForm/KeywordForm'; import BankForm from '@/components/SingleForm/BankForm'; +import { useForm, FormProvider } from 'react-hook-form'; +import { editProfileSchema } from '@/app/senior/edit-profile/edit-profile-schema'; +import { yupResolver } from '@hookform/resolvers/yup'; function RiseUpModal(props: RiseUpModalProps) { return ( diff --git a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx index a61f281c..f7d22392 100644 --- a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx +++ b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx @@ -11,13 +11,18 @@ import { createPortal } from 'react-dom'; import Router, { useRouter } from 'next/navigation'; import useAuth from '@/hooks/useAuth'; import axios from 'axios'; -import { socialIdAtom, userTypeAtom } from '@/stores/signup'; +import { + isTutorialFinished, + socialIdAtom, + userTypeAtom, +} from '@/stores/signup'; import { useAtom, useSetAtom } from 'jotai'; import findExCode from '@/utils/findExCode'; import useFullModal from '@/hooks/useFullModal'; import DimmedModal from '@/components/Modal/DimmedModal'; function SeniorManage(props: SeniorManageProps) { const router = useRouter(); + const setTutorialStatus = useSetAtom(isTutorialFinished); const { getAccessToken, setUserType, @@ -141,6 +146,7 @@ function SeniorManage(props: SeniorManageProps) { expires: res.data.refreshExpiration, }); setUserType(res.data.role); + setTutorialStatus(res.data.isTutorial); router.replace('/'); return; diff --git a/src/components/SingleForm/KeywordForm/KeywordForm.tsx b/src/components/SingleForm/KeywordForm/KeywordForm.tsx index 532f416d..979d22a0 100644 --- a/src/components/SingleForm/KeywordForm/KeywordForm.tsx +++ b/src/components/SingleForm/KeywordForm/KeywordForm.tsx @@ -1,8 +1,5 @@ -import SingleValidator from '@/components/Validator/SingleValidator'; -import TextForm from '../TextForm'; -import ClickedBtn from '@/components/Button/ClickedBtn'; import { useEffect, useState } from 'react'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtom, useSetAtom } from 'jotai'; import { sKeywordAtom, selectedKeywordAtom, @@ -16,49 +13,34 @@ import { } from './Keyword.styled'; import { SELECT_KEYWORD_TEXT } from '@/constants/keyword/keyword'; import SelectedBtn from '@/components/Button/SelectedBtn'; +import { useFormContext } from 'react-hook-form'; function KeywordForm({ clickHandler }: { clickHandler: () => void }) { - const [flag, setFlag] = useState(false); + const { + register, + watch, + setError, + setValue, + formState: { errors }, + } = useFormContext(); const [totalBtns, setTotalBtns] = useAtom(totalKeywordAtom); const [selected, setSelected] = useAtom(selectedKeywordAtom); const setSKeyword = useSetAtom(sKeywordAtom); - const [userInputKeyword, setUserInputKeyword] = useState(''); const [inputCount, setInputCount] = useState(0); - const handleConfirm = () => { - if (selected.length == 0) setFlag(true); - else { - setFlag(false); - setSKeyword(selected.join(',')); - clickHandler(); - } + setSKeyword(() => selected.join(',')); + clickHandler(); }; const addKeyword = () => { - if (userInputKeyword && inputCount < 6) { - setTotalBtns([...totalBtns, userInputKeyword]); - setSelected([...selected, userInputKeyword]); - setUserInputKeyword(''); + if (watch('keyword') && inputCount < 6) { + setTotalBtns([...totalBtns, watch('keyword')]); + setSelected([...selected, watch('keyword')]); setInputCount(inputCount + 1); + setValue('keyword', ''); } }; - const handleInputChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - - if (newValue.length <= 20) { - setUserInputKeyword(newValue); - } - }; - - useEffect(() => { - if (selected.length > 0) { - setFlag(false); - } else { - setFlag(true); - } - }, [selected]); - return ( @@ -68,7 +50,7 @@ function KeywordForm({ clickHandler }: { clickHandler: () => void }) {
{SELECT_KEYWORD_TEXT.keywordText}
*
- {flag && ( + {errors?.keyword && (
{SELECT_KEYWORD_TEXT.keywordAlert}
)}
@@ -88,21 +70,14 @@ function KeywordForm({ clickHandler }: { clickHandler: () => void }) { {selected.length < 6 && ( - + )} - {flag ? ( + {selected.length === 0 ? ( ) : (
- {flag &&
{SELECT_FIELD_TEXT.fieldAlert}
} + {errors?.field?.message && ( +
{SELECT_FIELD_TEXT.fieldAlert}
+ )}
{SELECT_FIELD_TEXT.fieldDirection} @@ -72,28 +60,24 @@ function SelectForm(props: SelectFormProps) { {totalBtns && totalBtns.map((el, idx) => ( { + setSelected(newSelected); + setSField(newSelected.join(',')); + }} key={idx} /> ))} - setUserInputField(e.currentTarget.value)} - maxLength={10} - /> + - {flag ? ( + {selected.length === 0 ? ( ) : (
)} - {modal && portalElement - ? createPortal( - , - portalElement, - ) - : null} ); } diff --git a/src/components/Content/ChangeJunior/ChangeJunior.tsx b/src/components/Content/ChangeJunior/ChangeJunior.tsx index a2b78ead..8beccc63 100644 --- a/src/components/Content/ChangeJunior/ChangeJunior.tsx +++ b/src/components/Content/ChangeJunior/ChangeJunior.tsx @@ -9,6 +9,7 @@ function ChangeJunior({ modalHandler }: { modalHandler: () => void }) { const handleClick = () => { router.push('/mypage'); + modalHandler(); }; return ( diff --git a/src/components/Content/PayAmount/PayAmount.tsx b/src/components/Content/PayAmount/PayAmount.tsx index ef8e0629..449eeab6 100644 --- a/src/components/Content/PayAmount/PayAmount.tsx +++ b/src/components/Content/PayAmount/PayAmount.tsx @@ -20,6 +20,7 @@ function PayAmount({ modalHandler }: { modalHandler: () => void }) { const nextClick = () => { router.push(`/mentoring-apply/${seniorId}/schedule`); + modalHandler(); }; return ( diff --git a/src/components/Content/SNotRegistered/SNotRegistered.tsx b/src/components/Content/SNotRegistered/SNotRegistered.tsx index e59ea64c..19291d38 100644 --- a/src/components/Content/SNotRegistered/SNotRegistered.tsx +++ b/src/components/Content/SNotRegistered/SNotRegistered.tsx @@ -20,6 +20,7 @@ function SNotRegistered({ modalHandler }: { modalHandler: () => void }) { { router.push('/add-profile'); + modalHandler(); }} > {PROFILE_NOT_REGISTERED.btnText} diff --git a/src/components/NotJunior/NotJunior.tsx b/src/components/NotJunior/NotJunior.tsx index 6bde1f8a..7a73af5b 100644 --- a/src/components/NotJunior/NotJunior.tsx +++ b/src/components/NotJunior/NotJunior.tsx @@ -24,6 +24,7 @@ function NotJunior(props: NotJuniorProps) { const seniorJoin = () => { router.push(`/signup/select/common-info/matching-info`); + props.modalHandler(); }; return ( diff --git a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx index f7d22392..3cd537d7 100644 --- a/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx +++ b/src/components/Profile/ProfileManage/SeniorManage/SeniorManage.tsx @@ -10,6 +10,7 @@ import useModal from '@/hooks/useModal'; import { createPortal } from 'react-dom'; import Router, { useRouter } from 'next/navigation'; import useAuth from '@/hooks/useAuth'; +import useDimmedModal from '@/hooks/useDimmedModal'; import axios from 'axios'; import { isTutorialFinished, @@ -43,17 +44,13 @@ function SeniorManage(props: SeniorManageProps) { bModalHandler: props.modalHandler, }); - const { - modal: setJModal, - modalHandler: juniorHandler, - portalElement: juniorPortal, - } = useModal('junior-request-portal'); + const { openModal: openNotRegisteredModal } = useDimmedModal({ + modalType: 'notRegistered', + }); - const { - modal: registerModal, - modalHandler: registerHandler, - portalElement: registerPortal, - } = useModal('senior-profile-not-registered'); + const { openModal: openNotJuniorModal } = useDimmedModal({ + modalType: 'notJunior', + }); const MyprofHandler = () => { if (checkRegister()) { @@ -75,7 +72,7 @@ function SeniorManage(props: SeniorManageProps) { const checkRegister = () => { if (props.profileReg) return true; if (!props.profileReg) { - registerHandler(); + openNotRegisteredModal(); return false; } }; @@ -105,7 +102,7 @@ function SeniorManage(props: SeniorManageProps) { if (response.data.data.possible == false) { setSocialId(response.data.data.socialId); - juniorHandler(); + openNotJuniorModal(); } } }); @@ -198,22 +195,6 @@ function SeniorManage(props: SeniorManageProps) { onClick={changeJunior} /> - - {registerModal && registerPortal - ? createPortal( - , - registerPortal, - ) - : null} - {setJModal && juniorPortal - ? createPortal( - , - juniorPortal, - ) - : null} ); } diff --git a/src/hooks/useDimmedModal.tsx b/src/hooks/useDimmedModal.tsx new file mode 100644 index 00000000..10e8b703 --- /dev/null +++ b/src/hooks/useDimmedModal.tsx @@ -0,0 +1,52 @@ +import DimmedModal from '@/components/Modal/DimmedModal'; + +import { DimmedModalProps } from '@/types/modal/dimmed'; +import { overlay } from 'overlay-kit'; +import { useState } from 'react'; + +interface UseDimmedModalProps extends DimmedModalProps { + overlayId?: string; +} +const useDimmedModal = ({ ...props }: Partial) => { + const [isOpen, setIsOpen] = useState(false); + + const openModal = () => { + setIsOpen(true); + overlay.open( + ({ unmount }) => { + return ( + { + if (props.modalHandler) { + props.modalHandler(); + } + closeModal(unmount); + }} + /> + ); + }, + { + overlayId: props.overlayId ?? '', + }, + ); + }; + + const closeModal = (unmount: () => void) => { + setIsOpen(false); + unmount(); + }; + + const toggleModal = () => { + if (isOpen) { + closeModal(() => {}); + } else { + openModal(); + } + }; + + return { openModal, closeModal, toggleModal, isOpen }; +}; + +export default useDimmedModal; diff --git a/src/hooks/useKakaoLogin.tsx b/src/hooks/useKakaoLogin.tsx index 21c4c804..219c591f 100644 --- a/src/hooks/useKakaoLogin.tsx +++ b/src/hooks/useKakaoLogin.tsx @@ -84,7 +84,7 @@ const useKakaoLogin = () => { ), ); if (!agreeActivateAccount) { - router.push('/signup/select'); + router.push('/'); } } } catch (error) { From c67ae009ab2257dd492c75db1c289f25f7e1a9d5 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sat, 28 Sep 2024 14:30:34 +0900 Subject: [PATCH 16/61] =?UTF-8?q?RAC-438=20Refactor:=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=ED=8F=AC=ED=95=A8=ED=95=98=EB=8A=94=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=EC=8A=A4=EC=99=80,=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=ED=8F=AC=ED=95=A8=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20(#298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: findExCode yup으로 유효성 검사하도록 수정 * refactor: 토큰을 포함하는 인스턴스와 토큰을 포함하지 않는인스턴스 분리 --- src/api/api.ts | 21 ++- src/api/auth/login/kakaoAuthFetch.tsx | 6 +- src/api/model.ts | 85 ++++++++++- src/api/senior/[id]/getDetailSeniorInfo.ts | 37 +++++ src/api/user/_images/postUserProfileImage.ts | 4 +- src/api/user/info/changeUserInfoFetch.ts | 15 +- src/api/user/info/useInfoFetch.ts | 4 +- src/api/user/profile/getSeniorProfile.ts | 6 +- src/api/user/profile/updateSeniorProfile.ts | 4 +- src/app/senior/info/[seniorId]/page.tsx | 134 +++++++++--------- .../(components)/signout-finish/index.tsx | 4 +- src/app/signout/page.tsx | 1 - src/hooks/useTutorial.ts | 4 +- src/utils/findExCode.ts | 7 +- src/utils/findSuccessCode.ts | 5 + 15 files changed, 228 insertions(+), 109 deletions(-) create mode 100644 src/api/senior/[id]/getDetailSeniorInfo.ts create mode 100644 src/utils/findSuccessCode.ts diff --git a/src/api/api.ts b/src/api/api.ts index 131e5d34..565b319f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -2,24 +2,21 @@ import useAuth from '@/hooks/useAuth'; import findExCode from '@/utils/findExCode'; import axios, { InternalAxiosRequestConfig } from 'axios'; -const instance = axios.create({ +const withAuthInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_SERVER_URL, }); -instance.interceptors.request.use( +const withOutAuthInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_SERVER_URL, +}); +withAuthInstance.interceptors.request.use( async ( config: InternalAxiosRequestConfig, ): Promise => { - const { getAccessToken, removeTokens } = useAuth(); + const { getAccessToken } = useAuth(); const accessTkn = await getAccessToken(); - if (!accessTkn && typeof window !== 'undefined') { - removeTokens(); - window.location.href = '/'; - return Promise.reject(new Error('Access token is missing')); // 에러 반환 - } else { - config.headers.Authorization = `Bearer ${accessTkn}`; - } + config.headers.Authorization = `Bearer ${accessTkn}`; return config; }, @@ -29,7 +26,7 @@ instance.interceptors.request.use( }, ); -instance.interceptors.response.use( +withAuthInstance.interceptors.response.use( (res) => { const { removeTokens } = useAuth(); if (findExCode(res.data.code)) { @@ -46,4 +43,4 @@ instance.interceptors.response.use( }, ); -export default instance; +export { withAuthInstance, withOutAuthInstance }; diff --git a/src/api/auth/login/kakaoAuthFetch.tsx b/src/api/auth/login/kakaoAuthFetch.tsx index db759c88..e1cca933 100644 --- a/src/api/auth/login/kakaoAuthFetch.tsx +++ b/src/api/auth/login/kakaoAuthFetch.tsx @@ -1,6 +1,6 @@ -import instance from '@/api/api'; +import { withOutAuthInstance } from '@/api/api'; + import { ResponseModel } from '@/api/model'; -import axios from 'axios'; export interface KakaoAuthFetchResponse extends ResponseModel { data: { @@ -16,7 +16,7 @@ export interface KakaoAuthFetchResponse extends ResponseModel { } export const kakaoAuthFetch = async ({ code }: { code: string }) => { - return await axios.post( + return await withOutAuthInstance.post( window.location.hostname.includes('localhost') ? `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/dev/login/KAKAO` : `${process.env.NEXT_PUBLIC_SERVER_URL}/auth/login/KAKAO`, diff --git a/src/api/model.ts b/src/api/model.ts index 95f0e966..e2006161 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,3 +1,5 @@ +import { string } from 'yup'; + export interface ResponseModel { code: SuccessStatusType | ErrorStatusType; message: string; @@ -48,7 +50,6 @@ export type SuccessStatusType = | 'SLR203' | 'ADM200' | 'ADM201'; - /** * 에러 코드 * @see https://www.notion.so/240430-c0e2fd72f06b45028e8e463d6faa32f9 @@ -87,3 +88,85 @@ export type ErrorStatusType = | 'EX200' | 'EX201' | 'EX202'; + +const allErrorStatusTypes = [ + 'EX1000', + 'EX900', + 'EX901', + 'EX902', + 'EX903', + 'EX904', + 'EX800', + 'EX801', + 'EX802', + 'EX700', + 'EX701', + 'EX702', + 'EX703', + 'EX704', + 'EX705', + 'EX706', + 'EX600', + 'EX601', + 'EX500', + 'EX501', + 'EX400', + 'EX401', + 'EX402', + 'EX403', + 'EX404', + 'EX405', + 'EX406', + 'EX300', + 'EX301', + 'EX302', + 'EX200', + 'EX201', + 'EX202', +] as const; + +const allSuccessStatusTypes = [ + 'UR200', + 'UR201', + 'UR202', + 'UR203', + 'SNR200', + 'SNR201', + 'SNR202', + 'SNR203', + 'MT200', + 'MT201', + 'MT202', + 'MT203', + 'PM200', + 'PM201', + 'PM202', + 'PM203', + 'RV200', + 'RV201', + 'RV202', + 'RV203', + 'AU200', + 'AU201', + 'AU202', + 'AU203', + 'AU204', + 'AU205', + 'ACT200', + 'ACT201', + 'ACT202', + 'ACT203', + 'IMG200', + 'IMG201', + 'IMG202', + 'IMG203', + 'SLR200', + 'SLR201', + 'SLR202', + 'SLR203', + 'ADM200', + 'ADM201', +] as const; + +export const errorStatusSchema = string().oneOf(allErrorStatusTypes); +export const successStatusSchema = string().oneOf(allSuccessStatusTypes); diff --git a/src/api/senior/[id]/getDetailSeniorInfo.ts b/src/api/senior/[id]/getDetailSeniorInfo.ts new file mode 100644 index 00000000..698cf30e --- /dev/null +++ b/src/api/senior/[id]/getDetailSeniorInfo.ts @@ -0,0 +1,37 @@ +import { ResponseModel } from '@/api/model'; +import { withOutAuthInstance } from '@/api/api'; + +interface SeniorInfoRequest { + seniorId: string; +} +interface SeniorInfoResponse extends ResponseModel { + data: { + isMine: boolean; + certification: boolean; + nickName: string; + term: number; + profile: string; + postgradu: string; + major: string; + lab: string; + professor: string; + keyword: string[]; + info: string; + oneLiner: string; + times: TimeObj[]; + }; +} + +export interface TimeObj { + day: string; + startTime: string; + endTime: string; +} + +export const getDetailSeniorInfoFetch = async ({ + seniorId, +}: SeniorInfoRequest) => { + return await withOutAuthInstance.get( + `/senior/${seniorId}`, + ); +}; diff --git a/src/api/user/_images/postUserProfileImage.ts b/src/api/user/_images/postUserProfileImage.ts index 3b1ccf6a..9a37f1aa 100644 --- a/src/api/user/_images/postUserProfileImage.ts +++ b/src/api/user/_images/postUserProfileImage.ts @@ -1,4 +1,4 @@ -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { ResponseModel } from '@/api/model'; interface PostUserProfileImageResponse extends ResponseModel { @@ -17,7 +17,7 @@ export const postUserProfileImage = async ({ const formData = new FormData(); formData.append('profileFile', profileFile); - return await instance.post( + return await withAuthInstance.post( '/image/upload/profile', formData, ); diff --git a/src/api/user/info/changeUserInfoFetch.ts b/src/api/user/info/changeUserInfoFetch.ts index 2af2547d..81546e05 100644 --- a/src/api/user/info/changeUserInfoFetch.ts +++ b/src/api/user/info/changeUserInfoFetch.ts @@ -1,4 +1,4 @@ -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { ResponseModel } from '@/api/model'; interface ChangeUserInfoFetchResponse extends ResponseModel { @@ -16,9 +16,12 @@ export const changeUserInfo = async ({ nickName, phoneNumber, }: ChangeUserInfoFetchRequest) => { - return await instance.patch('/user/me/info', { - profile, - nickName, - phoneNumber, - }); + return await withAuthInstance.patch( + '/user/me/info', + { + profile, + nickName, + phoneNumber, + }, + ); }; diff --git a/src/api/user/info/useInfoFetch.ts b/src/api/user/info/useInfoFetch.ts index 855adcb8..8c46b96b 100644 --- a/src/api/user/info/useInfoFetch.ts +++ b/src/api/user/info/useInfoFetch.ts @@ -1,4 +1,4 @@ -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { ResponseModel } from '@/api/model'; @@ -11,5 +11,5 @@ interface UserInfoFetchResponse extends ResponseModel { } export const userInfoFetch = async () => { - return await instance.get('/user/me/info'); + return await withAuthInstance.get('/user/me/info'); }; diff --git a/src/api/user/profile/getSeniorProfile.ts b/src/api/user/profile/getSeniorProfile.ts index 4b2a1c51..6f2e7d31 100644 --- a/src/api/user/profile/getSeniorProfile.ts +++ b/src/api/user/profile/getSeniorProfile.ts @@ -1,4 +1,4 @@ -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { ResponseModel } from '@/api/model'; interface SeniorProfileFetchResponse extends ResponseModel { @@ -21,5 +21,7 @@ interface Time { } export const seniorProfileFetch = async () => { - return await instance.get('/senior/me/profile'); + return await withAuthInstance.get( + '/senior/me/profile', + ); }; diff --git a/src/api/user/profile/updateSeniorProfile.ts b/src/api/user/profile/updateSeniorProfile.ts index 4ca61b2d..146652e0 100644 --- a/src/api/user/profile/updateSeniorProfile.ts +++ b/src/api/user/profile/updateSeniorProfile.ts @@ -1,4 +1,4 @@ -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { ResponseModel } from '@/api/model'; import { TimeObj } from '@/types/scheduler/scheduler'; @@ -29,7 +29,7 @@ export const updateSeniorProfile = async ({ oneLiner, times, }: UpdateSeniorProfileRequest) => { - return await instance.patch( + return await withAuthInstance.patch( '/senior/me/profile', { lab, diff --git a/src/app/senior/info/[seniorId]/page.tsx b/src/app/senior/info/[seniorId]/page.tsx index 52ab13ee..c7871d41 100644 --- a/src/app/senior/info/[seniorId]/page.tsx +++ b/src/app/senior/info/[seniorId]/page.tsx @@ -11,6 +11,7 @@ import { subjectAtom, thiAbleTimeAtom, } from '@/stores/mentoring'; +import { getDetailSeniorInfoFetch } from '@/api/senior/[id]/getDetailSeniorInfo'; import { enterSeniorId, mySeniorId } from '@/stores/senior'; import findExCode from '@/utils/findExCode'; import axios from 'axios'; @@ -19,19 +20,20 @@ import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import styled from 'styled-components'; +import findSuccessCode from '@/utils/findSuccessCode'; import useDimmedModal from '@/hooks/useDimmedModal'; -function SeniorInfoPage() { +import type { TimeObj } from '@/types/scheduler/scheduler'; +function SeniorInfoPage({ params }: { params: { seniorId: string } }) { const router = useRouter(); const currentPath = usePathname(); const pathArr = currentPath.split('/'); - const mySeiorId = useAtomValue(mySeniorId).toString(); const koreanCharWidth = 1.2; // 한글 글자 너비로 가정 - const { getAccessToken, getUserType, removeTokens } = useAuth(); + const { getUserType } = useAuth(); const [findSeniorId, setFindSeniorId] = useAtom(enterSeniorId); const [info, setInfo] = useState(''); - const [keyword, setKeyword] = useState([]); + const [keyword, setKeyword] = useState([]); const [lab, setLab] = useState(''); const [major, setMajor] = useState(''); const [nickName, setNickName] = useState(''); @@ -41,8 +43,8 @@ function SeniorInfoPage() { const [profile, setProfile] = useState(''); const [target, setTarget] = useState(''); const [term, setTerm] = useState(30); - const [times, setTimes] = useState([]); - const [mine, setMine] = useState('false'); + const [times, setTimes] = useState([]); + const [mine, setMine] = useState(false); const [overWidth, setOverWidth] = useState(false); const setTempSubject = useSetAtom(subjectAtom); const setTempQuestion = useSetAtom(questionAtom); @@ -55,6 +57,10 @@ function SeniorInfoPage() { modalType: 'changeJunior', }); + const { openModal: openMentoringNotLoginModal } = useDimmedModal({ + modalType: 'mentoringLogin', + }); + useEffect(() => { setTempSubject(''); setTempQuestion(''); @@ -63,10 +69,6 @@ function SeniorInfoPage() { setThiAbleTime(''); }, []); - const { openModal: openMentoringNotLoginModal } = useDimmedModal({ - modalType: 'mentoringLogin', - }); - useEffect(() => { const totalWidth = 14 * koreanCharWidth * (major.length + postgradu.length + 3); @@ -74,69 +76,61 @@ function SeniorInfoPage() { }, [major, postgradu]); useEffect(() => { - const seniorId = pathArr[pathArr.length - 1]; - setFindSeniorId(seniorId); - - getAccessToken().then((accessTkn) => { - axios - .get( - `${process.env.NEXT_PUBLIC_SERVER_URL}/senior/${seniorId}`, - accessTkn - ? { headers: { Authorization: `Bearer ${accessTkn}` } } - : {}, - ) - .then((response) => { - const res = response.data; - - if (findExCode(res.code)) { - removeTokens(); - location.reload(); - return; - } - - if (res.code == 'SNR200') { - setMine(res.data.isMine); - setInfo(res.data.info); - setKeyword(res.data.keyword); - setLab(res.data.lab); - setMajor(res.data.major); - setNickName(res.data.nickName); - setOneLiner(res.data.oneLiner); - setPostgradu(res.data.postgradu); - setProfessor(res.data.professor); - setProfile(res.data.profile); - setTarget(res.data.target); - setTerm(res.data.term); - setTimes(res.data.times); - setCertification(res.data.certification); - } - }) - .catch((err) => { - console.error(err); - }); - }); + const fetchSeniorInfo = async () => { + setFindSeniorId(params.seniorId); + const { data: seniorInfoFetchRes } = await getDetailSeniorInfoFetch({ + seniorId: params.seniorId, + }); + if (findSuccessCode(seniorInfoFetchRes.code)) { + const { + isMine, + info, + keyword, + lab, + major, + nickName, + oneLiner, + postgradu, + professor, + profile, + term, + times, + certification, + } = seniorInfoFetchRes.data; + + setMine(isMine); + setInfo(info); + setKeyword(keyword); + setLab(lab); + setMajor(major); + setNickName(nickName); + setOneLiner(oneLiner); + setPostgradu(postgradu); + setProfessor(professor); + setProfile(profile); + setTarget(target); + setTerm(term); + setTimes(times); + setCertification(certification); + } + + if (findExCode(seniorInfoFetchRes.code)) { + //FIXME - 에러처리 + } + }; + fetchSeniorInfo(); }, []); const applyHandler = () => { - getAccessToken().then((accessTkn) => { - if (accessTkn) { - const userType = getUserType(); - - if (userType == 'junior') { - const seniorId = pathArr[pathArr.length - 1]; - router.push(`/mentoring-apply/${seniorId}/question`); - return; - } - - if (userType == 'senior') { - // 후배 회원 전환 요청 모달 출현 - openChangeJuniorModal(); - } - } else { - // 로그인 요청 모달 출현 - openMentoringNotLoginModal(); - } - }); + const userType = getUserType(); + + if (userType === 'junior') { + router.push(`/mentoring-apply/${params.seniorId}/question`); + } else if (userType === 'senior') { + openChangeJuniorModal(); + } else { + openMentoringNotLoginModal(); + } }; const editHandler = () => { diff --git a/src/app/signout/(components)/signout-finish/index.tsx b/src/app/signout/(components)/signout-finish/index.tsx index 583f749a..53a13e9f 100644 --- a/src/app/signout/(components)/signout-finish/index.tsx +++ b/src/app/signout/(components)/signout-finish/index.tsx @@ -6,7 +6,7 @@ import { SignOutInfoContainer } from '@/app/signout/(components)/signout-type-se import SignOutImage from '/public/signout.png'; import { useSignOutInfo } from '@/app/signout/signoutContext'; import NextBtn from '@/components/Button/NextBtn'; -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { useRouter } from 'next/navigation'; export function SignOutFinish() { const router = useRouter(); @@ -16,7 +16,7 @@ export function SignOutFinish() { //mutate로 바꿔야 함 //회원탈퇴 API -> 토큰 제거 -> 버튼에 GA이벤트..? if (signOutInfo) { - await instance + await withAuthInstance .post('/auth/signout/KAKAO', { reason: signOutInfo.signOutReason, etc: signOutInfo.etc, diff --git a/src/app/signout/page.tsx b/src/app/signout/page.tsx index fcb78768..71d7c361 100644 --- a/src/app/signout/page.tsx +++ b/src/app/signout/page.tsx @@ -1,6 +1,5 @@ 'use client'; import useFunnel from '@/hooks/useFunnel'; -import instance from '@/api/api'; import { SignOutInfoProvider } from '@/app/signout/signoutContext'; import { SignOutFinish } from '@/app/signout/(components)/signout-finish'; diff --git a/src/hooks/useTutorial.ts b/src/hooks/useTutorial.ts index 606931d3..512dfaec 100644 --- a/src/hooks/useTutorial.ts +++ b/src/hooks/useTutorial.ts @@ -2,7 +2,7 @@ import { useSetAtom, useAtom } from 'jotai'; import useAuth from '@/hooks/useAuth'; import { useTour } from '@reactour/tour'; import { isTutorialFinished } from '@/stores/signup'; -import instance from '@/api/api'; +import { withAuthInstance } from '@/api/api'; import { useEffect } from 'react'; function useTutorial() { @@ -20,7 +20,7 @@ function useTutorial() { setTutorialStepOpen(true); setTutorialFinished(true); - await instance.patch( + await withAuthInstance.patch( `${process.env.NEXT_PUBLIC_SERVER_URL}/user/me/tutorial`, ); }; diff --git a/src/utils/findExCode.ts b/src/utils/findExCode.ts index 40290904..096f1e6c 100644 --- a/src/utils/findExCode.ts +++ b/src/utils/findExCode.ts @@ -1,6 +1,5 @@ -export default function findExCode(code: string) { - const targetCode = ['EX200', 'EX201', 'EX300', 'EX903']; +import { errorStatusSchema } from '@/api/model'; - if (targetCode.includes(code)) return true; - else return false; +export default function findExCode(code: string) { + return errorStatusSchema.isValidSync(code); } diff --git a/src/utils/findSuccessCode.ts b/src/utils/findSuccessCode.ts new file mode 100644 index 00000000..f70f1458 --- /dev/null +++ b/src/utils/findSuccessCode.ts @@ -0,0 +1,5 @@ +import { successStatusSchema } from '@/api/model'; + +export default function findSuccessCode(code: string) { + return successStatusSchema.isValidSync(code); +} From 7b3b6ee071e953e05064763d70118b9c6218bb64 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Sun, 29 Sep 2024 18:43:12 +0900 Subject: [PATCH 17/61] =?UTF-8?q?RAC=20438=20Refactor:=20=EC=84=A0?= =?UTF-8?q?=EB=B0=B0=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?UI=20=EC=88=98=EC=A0=95=20(#299)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: findExCode yup으로 유효성 검사하도록 수정 * refactor: 토큰을 포함하는 인스턴스와 토큰을 포함하지 않는인스턴스 분리 * refactor: 선배 프로필 닉네임, 멘토링 시간 부분 수정 * style: profile card 스타일 수정 * refactor: 프로필 목록 카드 UI 수정 * refactor: 연구분야 폼 스타일 수정 * refactor: width 수정 * style: 프로필 선배 상세 UI 수정 --- src/app/senior/info/[seniorId]/page.tsx | 18 +++---- .../Card/IntroCard/IntroCard.styled.ts | 36 +++++-------- src/components/Card/IntroCard/IntroCard.tsx | 7 +-- .../Card/KeywordCard/KeywordCard.styled.ts | 22 ++++---- .../Card/KeywordCard/KeywordCard.tsx | 2 +- .../Card/ProfileCard/ProfileCard.styled.ts | 54 +++++++++++++------ .../Card/ProfileCard/ProfileCard.tsx | 20 ++++--- .../AuthLabeledText/AuthLabeled.styled.ts | 15 +++--- src/types/card/introCard.ts | 1 - src/types/card/profileCard.ts | 2 + 10 files changed, 94 insertions(+), 83 deletions(-) diff --git a/src/app/senior/info/[seniorId]/page.tsx b/src/app/senior/info/[seniorId]/page.tsx index c7871d41..699ae513 100644 --- a/src/app/senior/info/[seniorId]/page.tsx +++ b/src/app/senior/info/[seniorId]/page.tsx @@ -24,6 +24,7 @@ import findSuccessCode from '@/utils/findSuccessCode'; import useDimmedModal from '@/hooks/useDimmedModal'; import type { TimeObj } from '@/types/scheduler/scheduler'; + function SeniorInfoPage({ params }: { params: { seniorId: string } }) { const router = useRouter(); const currentPath = usePathname(); @@ -144,6 +145,8 @@ function SeniorInfoPage({ params }: { params: { seniorId: string } }) {
- +
@@ -188,13 +186,13 @@ const SeniorInfoPageContainer = styled.div` const SeniorInfoContentWrapper = styled.div` width: inherit; height: auto; - background-color: #f1f3f5; position: relative; padding-bottom: 4.5rem; + background-color: #f8f9fb; `; const SeniorInfoContent = styled.div<{ $overWidth: boolean }>` - width: 95%; + width: 100%; height: auto; position: relative; display: flex; @@ -204,14 +202,12 @@ const SeniorInfoContent = styled.div<{ $overWidth: boolean }>` #profile-card-wrapper { width: 100%; height: ${(props) => (props.$overWidth ? '8.25rem' : '7.25rem')}; - margin-top: 1.5rem; - margin-bottom: 0.5rem; } #keyword-card-wrapper { width: 100%; + margin-top: 28px; height: auto; - margin-bottom: 0.625rem; } #intro-card-wrapper { diff --git a/src/components/Card/IntroCard/IntroCard.styled.ts b/src/components/Card/IntroCard/IntroCard.styled.ts index a7676ae7..5193e26d 100644 --- a/src/components/Card/IntroCard/IntroCard.styled.ts +++ b/src/components/Card/IntroCard/IntroCard.styled.ts @@ -2,55 +2,45 @@ import styled from 'styled-components'; export const IntroCardContainer = styled.div` width: 100%; + margin: 28px auto; height: max-content; min-height: 7.25rem; + margin-top: 15px; + background-color: #f8f9fb; border-radius: 16px; - background-color: #fff; - box-shadow: 0px 0px 8px 0px rgba(73, 85, 101, 0.2); position: relative; - padding: 1.5rem 0 3rem 1.25rem; -`; - -export const IntroCardOneLiner = styled.div` - width: 88%; - height: max-content; - font-weight: 700; - margin-bottom: 1.25rem; + padding: 1rem 1rem; `; export const IntroCardTextBox = styled.div<{ $isFull: boolean }>` - width: 90%; + width: 100%; height: max-content; min-height: 6.5rem; - border-radius: 4px; - border-width: 2px; - border-style: solid; - border-color: ${(props) => - props.$isFull ? 'transparent' : 'rgba(47, 196, 178, 0.3)'}; - background-color: #f8f9fa; + min-width: 330px; + border-radius: 16px; + background-color: #fff; letter-spacing: -0.5px; font-size: 15px; - padding: 0.75rem; margin-bottom: 2.5rem; white-space: pre-wrap; word-wrap: break-word; line-height: 130%; + padding: 0.75rem; `; export const IntroCardTextDesc = styled.div` width: max-content; - max-width: 88%; height: 1rem; font-size: 14px; font-weight: 700; - margin-bottom: 0.375rem; + margin-bottom: 10px; `; export const IntroCardTimeBox = styled.div` - width: 90%; + width: 100%; height: 2.5rem; - border-radius: 4px; - background-color: #f8f9fa; + border-radius: 6px; + background-color: #fff; font-size: 15px; padding: 0.75rem; margin-bottom: 0.5rem; diff --git a/src/components/Card/IntroCard/IntroCard.tsx b/src/components/Card/IntroCard/IntroCard.tsx index fc0902e5..ab466239 100644 --- a/src/components/Card/IntroCard/IntroCard.tsx +++ b/src/components/Card/IntroCard/IntroCard.tsx @@ -1,7 +1,6 @@ import { IntroCardProps } from '@/types/card/introCard'; import { IntroCardContainer, - IntroCardOneLiner, IntroCardTextBox, IntroCardTextDesc, IntroCardTimeBox, @@ -23,13 +22,11 @@ function IntroCard(props: IntroCardProps) { return ( - - {props.oneLiner ? `\"${props.oneLiner}\"` : EMPTY_SENIOR_INFO.oneLiner} - + 자기소개 {props.info || EMPTY_SENIOR_INFO.info} - 이런 분들에게 추천드려요. + 이런 후배에게 추천해요 {props.target || EMPTY_SENIOR_INFO.info} diff --git a/src/components/Card/KeywordCard/KeywordCard.styled.ts b/src/components/Card/KeywordCard/KeywordCard.styled.ts index e44e2799..0a48a611 100644 --- a/src/components/Card/KeywordCard/KeywordCard.styled.ts +++ b/src/components/Card/KeywordCard/KeywordCard.styled.ts @@ -1,32 +1,32 @@ import styled from 'styled-components'; export const KeywordCardContainer = styled.div` - width: 100%; + width: 95%; + margin: 0 auto; height: auto; border-radius: 16px; - background-color: #fff; - box-shadow: 0px 0px 8px 0px rgba(73, 85, 101, 0.2); + background: #f8f8f8; position: relative; - padding: 1.2rem 1rem; #keyword-card-lab-name { - width: max-content; - height: 1.25rem; + width: 100%; font-size: 14px; font-weight: 700; - margin-bottom: 0.55rem; + margin-bottom: 10px; } `; export const KeywordCardArrayBox = styled.div` - width: 90%; + width: 100%; + min-width: 330px; + border-radius: 15px; height: auto; + padding: 1.2rem; + background: #fff; min-height: 4.125rem; display: flex; flex-wrap: wrap; gap: 0.625rem; - top: 3rem; - left: 1rem; `; export const KeywordCardEl = styled.div` @@ -38,5 +38,5 @@ export const KeywordCardEl = styled.div` font-size: 12px; white-space: nowrap; border-radius: 4px; - background-color: rgba(47, 196, 178, 0.1); + background-color: rgba(124, 143, 141, 0.1); `; diff --git a/src/components/Card/KeywordCard/KeywordCard.tsx b/src/components/Card/KeywordCard/KeywordCard.tsx index 3146afc9..9206a54a 100644 --- a/src/components/Card/KeywordCard/KeywordCard.tsx +++ b/src/components/Card/KeywordCard/KeywordCard.tsx @@ -8,7 +8,7 @@ import { function KeywordCard(props: KeywordCardProps) { return ( -
{props.lab || '연구실 이름'}
+
연구분야
{props.keyword && props.keyword.length > 0 && diff --git a/src/components/Card/ProfileCard/ProfileCard.styled.ts b/src/components/Card/ProfileCard/ProfileCard.styled.ts index f4411688..61d51769 100644 --- a/src/components/Card/ProfileCard/ProfileCard.styled.ts +++ b/src/components/Card/ProfileCard/ProfileCard.styled.ts @@ -2,31 +2,48 @@ import styled from 'styled-components'; export const ProfileCardContainer = styled.div<{ $overWidth: boolean }>` width: 100%; - height: ${(props) => (props.$overWidth ? '8.25rem' : '7.25rem')}; - border-radius: 16px; + height: ${(props) => (props.$overWidth ? '10rem' : '8rem')}; background-color: #fff; - box-shadow: 0px 0px 8px 0px rgba(73, 85, 101, 0.2); position: relative; #profile-img-wrapper { position: absolute; - top: 50%; + top: 30%; left: 1rem; transform: translateY(-50%); } + + #profile-card-one-linear { + font-size: 13px; + line-height: 15px; + color: #6d747e; + position: absolute; + bottom: 10%; + left: 50%; + transform: translateX(-50%); + width: 13rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + } `; export const ProfileCardInfo = styled.div<{ $overWidth: boolean }>` - width: 13rem; - height: 4.7rem; + max-width: 13rem; position: absolute; - top: 50%; - right: 1rem; + top: 30%; + right: 2rem; transform: translateY(-50%); #profile-card-professor { font-size: 14px; - margin-top: ${(props) => (props.$overWidth ? '0.875rem' : '')}; + margin-top: 3px; + #professor-name { + font-weight: 600; + } } `; @@ -35,31 +52,38 @@ export const ProfileCardInfoTop = styled.div` height: 1.57rem; display: flex; justify-content: space-between; + align-items: center; margin-bottom: 0.5rem; #profile-card-mentoring-time { width: max-content; height: 1.125rem; + align-items: center; font-size: 12px; - font-weight: 700; display: flex; line-height: 1.125rem; } #mentoring-time-desc { - color: #868e96; + color: #6d747e; + font-weight: 500; } #mentoring-time-term { - color: #2fc4b2; + color: #212529; + font-weight: 600; } `; export const ProfileCardInfoMid = styled.div` width: 13rem; height: 1.25rem; - font-size: 14px; + font-size: 15px; display: flex; - justify-content: space-between; - color: #868e96; + gap: 5px; + font-weight: 700; + color: #212529; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; diff --git a/src/components/Card/ProfileCard/ProfileCard.tsx b/src/components/Card/ProfileCard/ProfileCard.tsx index c4012495..4252ac20 100644 --- a/src/components/Card/ProfileCard/ProfileCard.tsx +++ b/src/components/Card/ProfileCard/ProfileCard.tsx @@ -16,7 +16,7 @@ function ProfileCard(props: ProfileCardProps) { useEffect(() => { const totalWidth = - 14 * koreanCharWidth * (props.major.length + props.postgradu.length + 3); + 14 * koreanCharWidth * (props.lab.length + props.postgradu.length + 3); if (totalWidth >= 208) setOverWidth(true); }, []); @@ -31,7 +31,7 @@ function ProfileCard(props: ProfileCardProps) {
@@ -40,16 +40,20 @@ function ProfileCard(props: ProfileCardProps) {
-
- {props.postgradu ? `${props.postgradu} 대학원` : '익명 대학원'} -   -
-
{props.major ? props.major : '익명 학과'}
+
{`[${props.postgradu.replaceAll('학교', '')}]`}
+ +
{props.lab}
- {props.professor ? `${props.professor} 교수님` : '익명 교수님'} + + {props.professor ? `${props.professor} ` : '익명 '} + + 교수님
+
+ "{props.oneLinear ?? '한 줄 소개'}" +
); } diff --git a/src/components/Text/AuthLabeledText/AuthLabeled.styled.ts b/src/components/Text/AuthLabeledText/AuthLabeled.styled.ts index 6577ebe1..511539a1 100644 --- a/src/components/Text/AuthLabeledText/AuthLabeled.styled.ts +++ b/src/components/Text/AuthLabeledText/AuthLabeled.styled.ts @@ -5,22 +5,21 @@ export const AuthLabeledContainer = styled.div` height: 1.5rem; display: flex; position: relative; + display: flex; + align-items: center; + gap: 3px; #auth-labeled-str { width: max-content; - height: 1.5rem; - line-height: 1.5rem; - margin-right: 1.5rem; - font-size: 18px; + height: 20px; + line-height: 19px; + font-size: 14px; font-weight: 700; + color: #21b1a0; } #auth-mark-icon { width: 1rem; height: 1rem; - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); } `; diff --git a/src/types/card/introCard.ts b/src/types/card/introCard.ts index 408087a3..f94a372e 100644 --- a/src/types/card/introCard.ts +++ b/src/types/card/introCard.ts @@ -5,7 +5,6 @@ export type TimeType = { }; export interface IntroCardProps { - oneLiner: string; info: string; target: string; times: Array; diff --git a/src/types/card/profileCard.ts b/src/types/card/profileCard.ts index eb26b1dc..0a1b9acb 100644 --- a/src/types/card/profileCard.ts +++ b/src/types/card/profileCard.ts @@ -6,4 +6,6 @@ export interface ProfileCardProps { postgradu: string; major: string; professor: string; + lab: string; + oneLinear: string; } From 57492b1f356cf1654086a859d9cb54619e100c08 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Mon, 30 Sep 2024 21:30:35 +0900 Subject: [PATCH 18/61] =?UTF-8?q?RAC=20439=20Refactor:=20=EC=84=A0?= =?UTF-8?q?=EB=B0=B0=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?RiseUpModal=EC=9D=98=20=EB=8B=AB=EA=B8=B0=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: selectform모달 ui 수정 * refactor: 선배 프로필 연구 주제, 분야 모달 UI 수정 * style: 선배 프로필 수정 모달마다 닫기 버튼 추가 및 스타일 수정 * refactor: 상수 문구 변경 --- .../Button/SelectedBtn/SelectedBtn.styled.ts | 20 ++++---- .../Modal/RiseUpModal/RiseUpModal.styled.ts | 8 +-- .../SingleForm/KeywordForm/Keyword.styled.ts | 37 ++++++++++---- .../SingleForm/KeywordForm/KeywordForm.tsx | 32 ++++++++---- .../SelectForm/SelectForm.styled.ts | 51 ++++++++++--------- .../SingleForm/SelectForm/SelectForm.tsx | 32 +++++++----- src/constants/field/field.ts | 5 +- src/constants/keyword/keyword.ts | 5 +- 8 files changed, 115 insertions(+), 75 deletions(-) diff --git a/src/components/Button/SelectedBtn/SelectedBtn.styled.ts b/src/components/Button/SelectedBtn/SelectedBtn.styled.ts index c9cf2386..9c62deb8 100644 --- a/src/components/Button/SelectedBtn/SelectedBtn.styled.ts +++ b/src/components/Button/SelectedBtn/SelectedBtn.styled.ts @@ -1,24 +1,26 @@ import styled from 'styled-components'; export const StyledSelectedBtn = styled.button<{ $selected: boolean }>` - width: max-content; - height: 2.125rem; + min-width: 60px; + width: auto; + height: 2.25rem; margin: 0.25rem 0.5rem 0.25rem 0; - padding: 0.3rem 0.75rem; + padding: 0.3rem 0.5rem; font-size: 16px; font-family: Pretendard; - border-radius: 8px; + border-radius: 4px; cursor: pointer; - background-color: ${(props) => (props.$selected ? '#2FC4B2' : '#F8F9FA')}; - color: ${(props) => (props.$selected ? '#fff' : '#ADB5BD')}; - border: ${(props) => (props.$selected ? 'none' : '1px solid #DEE2E6')}; + background-color: ${(props) => + props.$selected ? '#2FC4B2' : 'rgba(124, 143, 141, 0.1)'}; + color: ${(props) => (props.$selected ? '#fff' : '#4C4D4E')}; + border: ${(props) => (props.$selected ? '1px solid #21B1A0' : '1px #D7D7D7')}; font-weight: ${(props) => (props.$selected ? '700' : '400')}; #selected-x-btn { width: 1rem; height: 1rem; - margin-left: 0.375rem; - margin-bottom: 0.2rem; + margin-left: 0.15rem; + margin-bottom: 0.15rem; cursor: pointer; vertical-align: middle; } diff --git a/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts b/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts index cf99d914..94f208e2 100644 --- a/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts +++ b/src/components/Modal/RiseUpModal/RiseUpModal.styled.ts @@ -5,7 +5,9 @@ export const ModalBackground = styled.div` top: 50%; background: rgba(0, 0, 0, 0.7); transform: translateY(-50%); - width: 360px; + width: 375px; + margin: 0 auto; + padding: 0; height: 100vh; @keyframes modalAppear { @@ -20,13 +22,13 @@ export const ModalBackground = styled.div` } .rise-up-modal { - width: inherit; + width: 100%; + margin: 0 auto; height: 38rem; position: absolute; top: 7.9rem; animation: modalAppear 0.5s ease-out; border-radius: 1.9rem 1.9rem 0 0; - z-index: 1; background-color: #fff; } `; diff --git a/src/components/SingleForm/KeywordForm/Keyword.styled.ts b/src/components/SingleForm/KeywordForm/Keyword.styled.ts index ebd31249..4332f99e 100644 --- a/src/components/SingleForm/KeywordForm/Keyword.styled.ts +++ b/src/components/SingleForm/KeywordForm/Keyword.styled.ts @@ -10,6 +10,8 @@ export const KeywordFormContainer = styled.div` h3 { margin-bottom: 1rem; + font-size: 16px; + color: #020202; } #select-keyword-subtitle { @@ -18,7 +20,7 @@ export const KeywordFormContainer = styled.div` font-size: 14px; display: flex; justify-content: space-between; - margin-bottom: 0.375rem; + margin-bottom: 7px; #select-keyword-subtitle-text { width: 3.7rem; @@ -39,13 +41,12 @@ export const KeywordFormContainer = styled.div` #select-keyword-direction { width: 18.5rem; - height: 2.5rem; - color: #868e96; - font-size: 14px; + color: #464c51; + font-size: 13px; flex-wrap: pre; line-height: 140%; letter-spacing: -0.5px; - margin-bottom: 0.125rem; + margin-bottom: 7px; } #keyword-submit-btn { @@ -55,23 +56,36 @@ export const KeywordFormContainer = styled.div` border-radius: 12px; background-color: #2fc4b2; color: #fff; - font-size: 18px; + font-size: 16px; font-weight: 700; position: absolute; bottom: 0; cursor: pointer; } - #keyword-submit-btn-non { + .keyword-close-btn { + width: 100%; + height: 3.313rem; + border: none; + border-radius: 12px; + background-color: #fff; + color: #6d747e; + font-size: 16px; + font-weight: 700; + position: absolute; + bottom: 0; + cursor: pointer; + } + .keyword-submit-btn-non { width: 100%; height: 3.313rem; border: none; border-radius: 12px; background-color: #adb5bd; color: #fff; - font-size: 18px; + font-size: 16px; font-weight: 700; position: absolute; - bottom: 0; + bottom: 3.5rem; cursor: pointer; } `; @@ -90,9 +104,10 @@ export const KeywordFormWrapper = styled.div` export const KeywordInputFormBox = styled.div` width: 100%; - height: 3.19rem; + font-size: 13px; + height: 44px; border-radius: 8px; - border: 1px solid #c2cede; + border: 1px solid #dcdfe4; background: #fff; margin-top: 0.625rem; padding: 1rem 0.5rem; diff --git a/src/components/SingleForm/KeywordForm/KeywordForm.tsx b/src/components/SingleForm/KeywordForm/KeywordForm.tsx index 979d22a0..ddbdb274 100644 --- a/src/components/SingleForm/KeywordForm/KeywordForm.tsx +++ b/src/components/SingleForm/KeywordForm/KeywordForm.tsx @@ -14,6 +14,7 @@ import { import { SELECT_KEYWORD_TEXT } from '@/constants/keyword/keyword'; import SelectedBtn from '@/components/Button/SelectedBtn'; import { useFormContext } from 'react-hook-form'; +import NextBtn from '@/components/Button/NextBtn'; function KeywordForm({ clickHandler }: { clickHandler: () => void }) { const { @@ -45,18 +46,15 @@ function KeywordForm({ clickHandler }: { clickHandler: () => void }) {

{SELECT_KEYWORD_TEXT.keywordTitle}

+
+ {SELECT_KEYWORD_TEXT.keywordDirection} +
-
-
{SELECT_KEYWORD_TEXT.keywordText}
-
*
-
{errors?.keyword && ( -
{SELECT_KEYWORD_TEXT.keywordAlert}
+
*{SELECT_KEYWORD_TEXT.keywordAlert}
)}
-
- {SELECT_KEYWORD_TEXT.keywordDirection} -
+ {totalBtns && totalBtns.map((el, idx) => ( @@ -70,7 +68,12 @@ function KeywordForm({ clickHandler }: { clickHandler: () => void }) { {selected.length < 6 && ( - + @@ -78,12 +81,19 @@ function KeywordForm({ clickHandler }: { clickHandler: () => void }) { )}
{selected.length === 0 ? ( - + ) : ( )} +
); } diff --git a/src/components/SingleForm/SelectForm/SelectForm.styled.ts b/src/components/SingleForm/SelectForm/SelectForm.styled.ts index 7cb2f20d..47c381c0 100644 --- a/src/components/SingleForm/SelectForm/SelectForm.styled.ts +++ b/src/components/SingleForm/SelectForm/SelectForm.styled.ts @@ -1,52 +1,57 @@ import styled from 'styled-components'; export const SelectFormContainer = styled.div` - width: 20rem; + width: 90%; + margin: 0 auto; height: 70%; + overflow: hidden; position: absolute; top: 2rem; + color: #464c51; left: 50%; transform: translateX(-50%); + font-size: 13px; h3 { margin-bottom: 1rem; + font-size: 15px; } #select-field-subtitle { - width: 14.1rem; - height: 1.1rem; font-size: 14px; display: flex; justify-content: space-between; margin-bottom: 0.375rem; - #select-field-subtitle-text { - width: 3.7rem; - height: 1.1rem; - display: flex; - justify-content: space-between; - - #field-star { - color: #00a0e1; - font-weight: 700; - } - } - #field-alert { color: #ff5757; + font-size: 11.93px; } } #select-field-direction { - width: 12.5rem; - height: 2.5rem; - color: #868e96; - font-size: 14px; - flex-wrap: pre; + width: 100%; + display: inline-flex; + flex-wrap: wrap; + height: auto; + font-size: 13px; line-height: 140%; letter-spacing: -0.5px; margin-bottom: 0.125rem; } + .field-close-btn { + width: 100%; + height: 3.313rem; + border: none; + border-radius: 12px; + background-color: #fff; + color: #6d747e; + font-size: 18px; + font-weight: 700; + position: absolute; + bottom: 0; + cursor: pointer; + } #field-submit-btn { width: 100%; @@ -58,7 +63,7 @@ export const SelectFormContainer = styled.div` font-size: 18px; font-weight: 700; position: absolute; - bottom: 0; + bottom: 3.5rem; cursor: pointer; } #field-submit-btn-non { @@ -71,14 +76,13 @@ export const SelectFormContainer = styled.div` font-size: 18px; font-weight: 700; position: absolute; - bottom: 0; + bottom: 3.5rem; cursor: pointer; } `; export const SelectFormBtnContainer = styled.div` width: 100%; - height: max-content; `; export const SelectFormWrapper = styled.div` @@ -97,6 +101,7 @@ export const FieldInputFormBox = styled.div` margin-top: 0.625rem; padding: 1rem 0.5rem; display: flex; + align-items: center; font-size: 13px; justify-content: space-between; diff --git a/src/components/SingleForm/SelectForm/SelectForm.tsx b/src/components/SingleForm/SelectForm/SelectForm.tsx index ccea8e86..2586c621 100644 --- a/src/components/SingleForm/SelectForm/SelectForm.tsx +++ b/src/components/SingleForm/SelectForm/SelectForm.tsx @@ -16,7 +16,6 @@ function SelectForm(props: SelectFormProps) { const { register, watch, - setError, setValue, formState: { errors }, } = useFormContext(); @@ -44,18 +43,16 @@ function SelectForm(props: SelectFormProps) {

{SELECT_FIELD_TEXT.fieldTitle}

-
-
-
{SELECT_FIELD_TEXT.fieldText}
-
*
-
- {errors?.field?.message && ( -
{SELECT_FIELD_TEXT.fieldAlert}
- )} -
+
- {SELECT_FIELD_TEXT.fieldDirection} +
{SELECT_FIELD_TEXT.fieldDirection}
+
+ {errors?.field?.message && ( +
*{SELECT_FIELD_TEXT.fieldAlert}
+ )} +
+ {totalBtns && totalBtns.map((el, idx) => ( @@ -71,19 +68,26 @@ function SelectForm(props: SelectFormProps) { ))} - +
{selected.length === 0 ? ( - + ) : ( )} +
); } diff --git a/src/constants/field/field.ts b/src/constants/field/field.ts index 14f4fbd4..0120a66c 100644 --- a/src/constants/field/field.ts +++ b/src/constants/field/field.ts @@ -2,7 +2,8 @@ export const SELECT_FIELD_TEXT = { fieldTitle: '연구 분야에 대해 알려주세요.', fieldText: '연구분야', fieldAlert: '최소 1개 이상을 선택해주세요', - fieldDirection: `연구실이 연구중인 분야를 선택하거나\n직접 입력해주세요.`, + fieldDirection: `연구 분야는 가장 적합하다고 생각하시는 분야를 선택하거나 \n 입력해 주시면 됩니다.`, fieldInputDirection: '연구분야를 직접 입력해주세요.', - fieldInputBtnText: '입력완료', + fieldInputBtnText: '추가', + placeholder: '연구 분야를 직접 입력해주세요. ', }; diff --git a/src/constants/keyword/keyword.ts b/src/constants/keyword/keyword.ts index 08d36d21..41304a4a 100644 --- a/src/constants/keyword/keyword.ts +++ b/src/constants/keyword/keyword.ts @@ -2,7 +2,8 @@ export const SELECT_KEYWORD_TEXT = { keywordTitle: '연구 주제에 대해 알려주세요.', keywordText: '연구주제', keywordAlert: '최소 1개 이상을 선택해주세요', - keywordDirection: `연구실의 연구 주제를 잘 설명하는 키워드를 알려주세요.\n키워드 입력 후 ‘입력완료' 버튼을 누르면 추가됩니다.`, + keywordDirection: `연구 주제를 잘 설명하는 키워드를 알려 주세요`, keywordInputDirection: '연구주제를 직접 입력해주세요.', - keywordInputBtnText: '입력완료', + keywordInputBtnText: '추가', + placeholder: '연구 주제를 직접 입력해 주세요.', }; From 66d35d45c63c78102af175c2c5fbf7d660f3af2d Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Tue, 1 Oct 2024 15:23:14 +0900 Subject: [PATCH 19/61] =?UTF-8?q?RAC=20437=20Refactor:=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=84=A0=EB=B0=B0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20Query=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ;탈퇴 문구 수정 * refactor: 선배 리스트 조회 API query로 변경 --- src/api/senior/getSeinorList.ts | 47 ++++++ src/app/add-profile/schema.ts | 1 - src/app/page.tsx | 74 +++------ .../(components)/signout-info/index.tsx | 6 +- src/components/Provider/providers.tsx | 155 +++++++++--------- .../SeniorProfile/SeniorProfile.tsx | 1 - src/hooks/query/useGetSeniorListQuery.ts | 16 ++ 7 files changed, 171 insertions(+), 129 deletions(-) create mode 100644 src/api/senior/getSeinorList.ts create mode 100644 src/hooks/query/useGetSeniorListQuery.ts diff --git a/src/api/senior/getSeinorList.ts b/src/api/senior/getSeinorList.ts new file mode 100644 index 00000000..0e125302 --- /dev/null +++ b/src/api/senior/getSeinorList.ts @@ -0,0 +1,47 @@ +import { withOutAuthInstance } from '@/api/api'; +import { ResponseModel } from '@/api/model'; + +interface GetSeniorListRequest { + field: string; + postgradu: string; + page: number; +} + +interface SeniorItem { + seniorId: number; + certification: boolean; + profile: string; + nickName: string; + postgradu: string; + major: string; + lab: string; + professor: string; + keyword: string[]; +} + +interface GetSeniorListResponse extends ResponseModel { + data: { + seniorSearchResponses: SeniorItem[]; + totalElements: number; + }; +} +export const getSeniorList = async ({ + field, + page, + postgradu, +}: GetSeniorListRequest) => { + try { + return await withOutAuthInstance.get( + '/senior/field', + { + params: { + field, + page, + postgradu, + }, + }, + ); + } catch (e) { + throw e; + } +}; diff --git a/src/app/add-profile/schema.ts b/src/app/add-profile/schema.ts index 4468d40c..2214e940 100644 --- a/src/app/add-profile/schema.ts +++ b/src/app/add-profile/schema.ts @@ -28,7 +28,6 @@ export const validateAddProfileError = async (data: AddProfile) => { await addProfileSchema.validate(data); } catch (e) { if (e instanceof ValidationError) { - console.log(e); throw e; } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 6646dcf0..511ec3d5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,11 +10,10 @@ import SwiperComponent from '../components/Swiper/Swiper'; import DimmedModal from '../components/Modal/DimmedModal'; import SearchModal from '../components/Modal/SearchModal'; import { sfactiveTabAtom, suactiveTabAtom } from '../stores/tap'; -import axios from 'axios'; -import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; +import { useGetSeniorListQuery } from '@/hooks/query/useGetSeniorListQuery'; import LogoLayer from '@/components/LogoLayer/LogoLayer'; -import { listDataAtom, pageNumAtom } from '@/stores/home'; import Footer from '@/components/Footer'; import useTutorial from '@/hooks/useTutorial'; @@ -22,65 +21,42 @@ import { overlay } from 'overlay-kit'; export default function Home() { const { setCurrentPath } = usePrevPath(); - const [data, setData] = useAtom(listDataAtom); - const [page, setPage] = useAtom(pageNumAtom); const { isTutorialFinish } = useTutorial(); const field = useAtomValue(sfactiveTabAtom); const postgradu = useAtomValue(suactiveTabAtom); - useEffect(() => { - setPage(1); - if (field && postgradu) { - axios - .get( - `${process.env.NEXT_PUBLIC_SERVER_URL}/senior/field?field=${field}&postgradu=${postgradu}`, - ) - .then((res) => { - setData(res.data.data.seniorSearchResponses); - }) - .catch((err) => { - console.error(err); - }); - } - }, [field, postgradu]); + const { + data: seniorListData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useGetSeniorListQuery(field, postgradu); useEffect(() => { setCurrentPath(); - const infiniteBottom = () => { - let isScrollAtBottom = - window.innerHeight + window.scrollY >= document.body.offsetHeight - 5; - if (isScrollAtBottom) { - axios - .get( - `${ - process.env.NEXT_PUBLIC_SERVER_URL - }/senior/field?field=${field}&postgradu=${postgradu}&page=${ - page + 1 - }`, - ) - .then((response) => { - const res = response.data; - if (res.code == 'SNR200') { - setData((data) => [...data, ...res.data.seniorSearchResponses]); - setPage((page) => - res.data.totalElements / 10 <= page ? page : page + 1, - ); - } - }) - .catch((err) => { - console.error(err); - }); + const handleScroll = () => { + if ( + window.innerHeight + window.scrollY >= document.body.offsetHeight - 5 && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); } }; - window.addEventListener('scroll', infiniteBottom); + window.addEventListener('scroll', handleScroll); return () => { - window.removeEventListener('scroll', infiniteBottom); + window.removeEventListener('scroll', handleScroll); }; - }, [page]); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + const seniorList = + seniorListData?.pages.flatMap( + (page) => page.data.data.seniorSearchResponses, + ) || []; return ( @@ -101,8 +77,8 @@ export default function Home() { - {data && data.length > 0 ? ( - data.map((el, idx) => ( + {seniorList.length > 0 ? ( + seniorList.map((el, idx) => (
diff --git a/src/app/signout/(components)/signout-info/index.tsx b/src/app/signout/(components)/signout-info/index.tsx index ca10788d..742908df 100644 --- a/src/app/signout/(components)/signout-info/index.tsx +++ b/src/app/signout/(components)/signout-info/index.tsx @@ -16,11 +16,11 @@ export function SignOutInfo({ onClick }: { onClick: () => void }) {
- 3. 탈퇴 후에는 같은 휴대전화 번호로 일정 기간 동안 재가입이 - 제한됩니다. 회원 탈퇴를 신중히 진행해주세요. + 3. 탈퇴 후에는 같은 계정으로 30일 동안 재가입이 제한됩니다. 회원 + 탈퇴를 신중히 진행해주세요.
-
4. 탈퇴 후 15일 이내 재가입이 가능합니다.
+
4. 탈퇴 후 15일 이내 로그인 시 재활성화가 가능합니다.
diff --git a/src/components/Provider/providers.tsx b/src/components/Provider/providers.tsx index 8cc8a4c4..01c92af8 100644 --- a/src/components/Provider/providers.tsx +++ b/src/components/Provider/providers.tsx @@ -5,6 +5,7 @@ import ArrowLeft from '../../../public/left_white.png'; import { TourProvider } from '@reactour/tour'; import { Provider as JotaiProvider } from 'jotai'; import styled from 'styled-components'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; interface StepTextProps { size?: string; @@ -77,82 +78,86 @@ const tourSteps = [ }, ]; +const queryClient = new QueryClient(); + export default function Providers({ children }: { children: React.ReactNode }) { return ( - - ({ - ...base, - color: '#ffffff', - }), - popover: (base) => ({ - ...base, - color: '#2fc4b2', - background: 'none', - boxShadow: 'none', - '--reactour-accent': '#2FC4B2', - }), - maskWrapper: (base) => ({ - ...base, - background: 'none', - }), - controls: (base) => ({ - ...base, - background: 'none', - }), - badge: (base) => ({ - ...base, - opacity: 0, - }), - dot: (base) => ({ - ...base, - }), - button: (base) => ({ - ...base, - color: '#ffffff', - svg: '#ffffff', - stroke: '#ffffff', - }), - maskRect: (base) => ({ - ...base, - background: 'none', - }), - }} - disableKeyboardNavigation - steps={tourSteps} - nextButton={(props) => ( - 튜토리얼_다음버튼 props.setCurrentStep(props.currentStep + 1)} - style={{ - cursor: 'pointer', - }} - {...props} - /> - )} - prevButton={(props) => ( - 튜토리얼_이전버튼 props.setCurrentStep(props.currentStep - 1)} - style={{ - cursor: 'pointer', - }} - {...props} - /> - )} - > - {children} - - + + + ({ + ...base, + color: '#ffffff', + }), + popover: (base) => ({ + ...base, + color: '#2fc4b2', + background: 'none', + boxShadow: 'none', + '--reactour-accent': '#2FC4B2', + }), + maskWrapper: (base) => ({ + ...base, + background: 'none', + }), + controls: (base) => ({ + ...base, + background: 'none', + }), + badge: (base) => ({ + ...base, + opacity: 0, + }), + dot: (base) => ({ + ...base, + }), + button: (base) => ({ + ...base, + color: '#ffffff', + svg: '#ffffff', + stroke: '#ffffff', + }), + maskRect: (base) => ({ + ...base, + background: 'none', + }), + }} + disableKeyboardNavigation + steps={tourSteps} + nextButton={(props) => ( + 튜토리얼_다음버튼 props.setCurrentStep(props.currentStep + 1)} + style={{ + cursor: 'pointer', + }} + {...props} + /> + )} + prevButton={(props) => ( + 튜토리얼_이전버튼 props.setCurrentStep(props.currentStep - 1)} + style={{ + cursor: 'pointer', + }} + {...props} + /> + )} + > + {children} + + + ); } diff --git a/src/components/SeniorProfile/SeniorProfile.tsx b/src/components/SeniorProfile/SeniorProfile.tsx index 16ab593c..7e337046 100644 --- a/src/components/SeniorProfile/SeniorProfile.tsx +++ b/src/components/SeniorProfile/SeniorProfile.tsx @@ -18,7 +18,6 @@ import auth from '../../../public/auth_mark.png'; function SeniorProfile({ data }: SeniorProfileProps) { const router = useRouter(); - console.log(data); return ( diff --git a/src/hooks/query/useGetSeniorListQuery.ts b/src/hooks/query/useGetSeniorListQuery.ts new file mode 100644 index 00000000..aca38730 --- /dev/null +++ b/src/hooks/query/useGetSeniorListQuery.ts @@ -0,0 +1,16 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getSeniorList } from '@/api/senior/getSeinorList'; + +export const useGetSeniorListQuery = (field: string, postgradu: string) => { + return useInfiniteQuery({ + suspense: true, + queryKey: ['seniorList', field, postgradu], + queryFn: ({ pageParam = 1 }) => + getSeniorList({ field, postgradu, page: pageParam }), + getNextPageParam: (lastPage, allPages) => { + const totalPages = lastPage.data.data.totalElements; + const nextPage = allPages.length + 1; + return nextPage <= totalPages ? nextPage : undefined; + }, + }); +}; From 85535c245b5d1171d354e2e464dc4bad911dbbb8 Mon Sep 17 00:00:00 2001 From: kimhyojung <706shin1728@naver.com> Date: Wed, 2 Oct 2024 17:45:55 +0900 Subject: [PATCH 20/61] =?UTF-8?q?RAC=20437=20Feat:=20=EC=84=A0=EB=B0=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20SSR=20=EC=A0=81=EC=9A=A9=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ;탈퇴 문구 수정 * refactor: 선배 리스트 조회 API query로 변경 * feat: 선배 리스트 조회 useInfinityQuery prefetch 적용 --- src/api/senior/getSeinorList.ts | 11 +- src/app/page.tsx | 153 +++-------------------- src/components/SeniorList/index.tsx | 143 +++++++++++++++++++++ src/hooks/query/useGetSeniorListQuery.ts | 4 +- src/hooks/useAuth.ts | 6 +- 5 files changed, 168 insertions(+), 149 deletions(-) create mode 100644 src/components/SeniorList/index.tsx diff --git a/src/api/senior/getSeinorList.ts b/src/api/senior/getSeinorList.ts index 0e125302..6915b13c 100644 --- a/src/api/senior/getSeinorList.ts +++ b/src/api/senior/getSeinorList.ts @@ -31,17 +31,16 @@ export const getSeniorList = async ({ postgradu, }: GetSeniorListRequest) => { try { - return await withOutAuthInstance.get( - '/senior/field', - { + return ( + await withOutAuthInstance.get('/senior/field', { params: { field, page, postgradu, }, - }, - ); + }) + ).data; } catch (e) { throw e; } -}; +}; \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 511ec3d5..b8851101 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,140 +1,19 @@ -'use client'; -import MenuBar from '../components/Bar/MenuBar'; -import { useEffect } from 'react'; -import usePrevPath from '../hooks/usePrevPath'; -import styled from 'styled-components'; -import SeniorProfile from '../components/SeniorProfile/SeniorProfile'; -import FieldTapBar from '../components/Bar/FieldTapBar/FieldTapBar'; -import UnivTapBar from '../components/Bar/UnivTapBar/UnivTapBar'; -import SwiperComponent from '../components/Swiper/Swiper'; -import DimmedModal from '../components/Modal/DimmedModal'; -import SearchModal from '../components/Modal/SearchModal'; -import { sfactiveTabAtom, suactiveTabAtom } from '../stores/tap'; -import { useAtomValue } from 'jotai'; - -import { useGetSeniorListQuery } from '@/hooks/query/useGetSeniorListQuery'; -import LogoLayer from '@/components/LogoLayer/LogoLayer'; -import Footer from '@/components/Footer'; - -import useTutorial from '@/hooks/useTutorial'; -import { overlay } from 'overlay-kit'; - -export default function Home() { - const { setCurrentPath } = usePrevPath(); - const { isTutorialFinish } = useTutorial(); - - const field = useAtomValue(sfactiveTabAtom); - const postgradu = useAtomValue(suactiveTabAtom); - - const { - data: seniorListData, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useGetSeniorListQuery(field, postgradu); - - useEffect(() => { - setCurrentPath(); - - const handleScroll = () => { - if ( - window.innerHeight + window.scrollY >= document.body.offsetHeight - 5 && - hasNextPage && - !isFetchingNextPage - ) { - fetchNextPage(); - } - }; - - window.addEventListener('scroll', handleScroll); - - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); - - const seniorList = - seniorListData?.pages.flatMap( - (page) => page.data.data.seniorSearchResponses, - ) || []; +import { Hydrate, QueryClient, dehydrate } from '@tanstack/react-query'; +import { getSeniorList } from '@/api/senior/getSeinorList'; +import { SeniorList } from '@/components/SeniorList'; + +export default async function Home() { + const queryClient = new QueryClient(); + await queryClient.prefetchInfiniteQuery({ + queryKey: ['seniorList'], + queryFn: () => getSeniorList({ field: 'all', postgradu: 'all', page: 1 }), + getNextPageParam: (lastPage) => + lastPage.data.seniorSearchResponses.length + 1, + }); return ( - - { - overlay.open(({ unmount }) => { - return unmount()} />; - }); - }} - /> - - - - - - - - - - - {seniorList.length > 0 ? ( - seniorList.map((el, idx) => ( -
- -
- )) - ) : ( -
해당하는 선배가 없어요
- )} -
-
- - { - overlay.open(({ unmount }) => { - return ( - unmount()} - /> - ); - }); - }} - /> - - + + + ); -} - -const HomeLayer = styled.div` - width: inherit; - height: inherit; - padding-bottom: 3.5rem; -`; - -const HomeBannerLayer = styled.div` - height: 6.7rem; - padding: 0 1rem; -`; -const HomeFieldLayer = styled.div` - margin: 0 0.5rem; - overflow-x: auto; - white-space: nowrap; -`; -const HomeUnivLayer = styled.div` - border-top: 1px solid #c2cede; - overflow-x: auto; - white-space: nowrap; - padding: 1rem 0.9rem; -`; -const HomeProfileLayer = styled.div` - min-height: 22rem; - height: inherit; - padding: 1rem; -`; -const MenuBarWrapper = styled.div` - position: fixed; - bottom: 0; - width: inherit; - z-index: 1; -`; +} \ No newline at end of file diff --git a/src/components/SeniorList/index.tsx b/src/components/SeniorList/index.tsx new file mode 100644 index 00000000..9bafff2f --- /dev/null +++ b/src/components/SeniorList/index.tsx @@ -0,0 +1,143 @@ +'use client'; + +import MenuBar from '@/components/Bar/MenuBar'; +import { Suspense, useEffect } from 'react'; +import usePrevPath from '@/hooks/usePrevPath'; +import styled from 'styled-components'; +import SeniorProfile from '@/components/SeniorProfile/SeniorProfile'; +import FieldTapBar from '@/components/Bar/FieldTapBar/FieldTapBar'; + +import UnivTapBar from '@/components/Bar/UnivTapBar/UnivTapBar'; +import SwiperComponent from '@/components/Swiper/Swiper'; +import DimmedModal from '@/components/Modal/DimmedModal'; +import SearchModal from '@/components/Modal/SearchModal'; +import { sfactiveTabAtom, suactiveTabAtom } from '@/stores/tap'; +import { useAtomValue } from 'jotai'; + +import { useGetSeniorListQuery } from '@/hooks/query/useGetSeniorListQuery'; +import LogoLayer from '@/components/LogoLayer/LogoLayer'; +import Footer from '@/components/Footer'; + +import useTutorial from '@/hooks/useTutorial'; +import { overlay } from 'overlay-kit'; + +export function SeniorList() { + const { setCurrentPath } = usePrevPath(); + const { isTutorialFinish } = useTutorial(); + + const field = useAtomValue(sfactiveTabAtom); + const postgradu = useAtomValue(suactiveTabAtom); + + const { + data: seniorListData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useGetSeniorListQuery(field, postgradu); + + useEffect(() => { + setCurrentPath(); + + const handleScroll = () => { + if ( + window.innerHeight + window.scrollY >= document.body.offsetHeight - 5 && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); + } + }; + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + const seniorList = + seniorListData?.pages.flatMap((page) => page.data.seniorSearchResponses) || + []; + + return ( + 로딩 중...
}> + + { + overlay.open(({ unmount }) => { + return unmount()} />; + }); + }} + /> + + + + + + + + + + + {seniorList.length > 0 ? ( + seniorList.map((el, idx) => ( +
+ +
+ )) + ) : ( +
해당하는 선배가 없어요
+ )} +
+