diff --git a/src/apis/Auth/postAppleAuth.ts b/src/apis/Auth/postAppleAuth.ts index 2ef03e0a..900d27d5 100644 --- a/src/apis/Auth/postAppleAuth.ts +++ b/src/apis/Auth/postAppleAuth.ts @@ -1,46 +1,46 @@ -import { useRouter, useSearchParams } from "next/navigation"; - -import { AxiosError } from "axios"; - -import { validateSafeRedirect } from "@/utils/authUtils"; - -import { AppleAuthRequest, AppleAuthResponse, authApi } from "./api"; +import { useMutation } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { useRouter, useSearchParams } from "next/navigation"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; +import { validateSafeRedirect } from "@/utils/authUtils"; +import { type AppleAuthRequest, type AppleAuthResponse, authApi } from "./api"; /** * @description 애플 로그인을 위한 useMutation 커스텀 훅 */ const usePostAppleAuth = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - return useMutation({ - mutationFn: (data) => authApi.postAppleAuth(data), - onSuccess: (data) => { - if (data.isRegistered) { - // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 - // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 - useAuthStore.getState().setAccessToken(data.accessToken); - - // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 - const redirectParam = searchParams.get("redirect"); - const safeRedirect = validateSafeRedirect(redirectParam); - - toast.success("로그인에 성공했습니다."); - router.replace(safeRedirect); - } else { - // 새로운 회원일 시 - 회원가입 페이지로 이동 - router.push(`/sign-up?token=${data.signUpToken}`); - } - }, - onError: () => { - toast.error("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); - router.push("/login"); - }, - }); + const router = useRouter(); + const searchParams = useSearchParams(); + + return useMutation({ + mutationFn: (data) => authApi.postAppleAuth(data), + onSuccess: (data) => { + if (data.isRegistered) { + // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 + // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 + useAuthStore.getState().setAccessToken(data.accessToken); + + // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 + const redirectParam = searchParams.get("redirect"); + const safeRedirect = validateSafeRedirect(redirectParam); + + toast.success("로그인에 성공했습니다."); + + setTimeout(() => { + router.push(safeRedirect); + }, 100); + } else { + // 새로운 회원일 시 - 회원가입 페이지로 이동 + router.push(`/sign-up?token=${data.signUpToken}`); + } + }, + onError: () => { + toast.error("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); + router.push("/login"); + }, + }); }; export default usePostAppleAuth; diff --git a/src/apis/Auth/postEmailLogin.ts b/src/apis/Auth/postEmailLogin.ts index af5d54ce..cab8915f 100644 --- a/src/apis/Auth/postEmailLogin.ts +++ b/src/apis/Auth/postEmailLogin.ts @@ -1,40 +1,46 @@ -import { useRouter, useSearchParams } from "next/navigation"; - -import { AxiosError } from "axios"; - -import { validateSafeRedirect } from "@/utils/authUtils"; - -import { EmailLoginRequest, EmailLoginResponse, authApi } from "./api"; +import { useMutation } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { useRouter, useSearchParams } from "next/navigation"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; +import { validateSafeRedirect } from "@/utils/authUtils"; +import { + authApi, + type EmailLoginRequest, + type EmailLoginResponse, +} from "./api"; /** * @description 이메일 로그인을 위한 useMutation 커스텀 훅 */ const usePostEmailAuth = () => { - const { setAccessToken } = useAuthStore(); - const searchParams = useSearchParams(); - const router = useRouter(); - - return useMutation({ - mutationFn: (data) => authApi.postEmailLogin(data), - onSuccess: (data) => { - const { accessToken } = data; - - // Zustand persist가 자동으로 localStorage에 저장 - // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 - setAccessToken(accessToken); - - // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 - const redirectParam = searchParams.get("redirect"); - const safeRedirect = validateSafeRedirect(redirectParam); - - toast.success("로그인에 성공했습니다."); - router.replace(safeRedirect); - }, - }); + const { setAccessToken } = useAuthStore(); + const searchParams = useSearchParams(); + const router = useRouter(); + + return useMutation({ + mutationFn: (data) => authApi.postEmailLogin(data), + onSuccess: (data) => { + const { accessToken } = data; + + // Zustand persist가 자동으로 localStorage에 저장 + // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 + setAccessToken(accessToken); + + // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 + const redirectParam = searchParams.get("redirect"); + const safeRedirect = validateSafeRedirect(redirectParam); + + toast.success("로그인에 성공했습니다."); + + // Zustand persist middleware가 localStorage에 저장할 시간을 보장 + // 토큰 저장 후 리다이렉트하여 타이밍 이슈 방지 + setTimeout(() => { + router.push(safeRedirect); + }, 100); + }, + }); }; export default usePostEmailAuth; diff --git a/src/apis/Auth/postKakaoAuth.ts b/src/apis/Auth/postKakaoAuth.ts index 9179848b..40894959 100644 --- a/src/apis/Auth/postKakaoAuth.ts +++ b/src/apis/Auth/postKakaoAuth.ts @@ -1,47 +1,47 @@ -import { useRouter, useSearchParams } from "next/navigation"; - -import { AxiosError } from "axios"; - -import { validateSafeRedirect } from "@/utils/authUtils"; - -import { KakaoAuthRequest, KakaoAuthResponse, authApi } from "./api"; +import { useMutation } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { useRouter, useSearchParams } from "next/navigation"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; +import { validateSafeRedirect } from "@/utils/authUtils"; +import { authApi, type KakaoAuthRequest, type KakaoAuthResponse } from "./api"; /** * @description 카카오 로그인을 위한 useMutation 커스텀 훅 */ const usePostKakaoAuth = () => { - const { setAccessToken } = useAuthStore(); - const router = useRouter(); - const searchParams = useSearchParams(); - - return useMutation({ - mutationFn: (data) => authApi.postKakaoAuth(data), - onSuccess: (data) => { - if (data.isRegistered) { - // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 - // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 - setAccessToken(data.accessToken); - - // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 - const redirectParam = searchParams.get("redirect"); - const safeRedirect = validateSafeRedirect(redirectParam); - - toast.success("로그인에 성공했습니다."); - router.replace(safeRedirect); - } else { - // 새로운 회원일 시 - 회원가입 페이지로 이동 - router.push(`/sign-up?token=${data.signUpToken}`); - } - }, - onError: () => { - toast.error("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); - router.push("/login"); - }, - }); + const { setAccessToken } = useAuthStore(); + const router = useRouter(); + const searchParams = useSearchParams(); + + return useMutation({ + mutationFn: (data) => authApi.postKakaoAuth(data), + onSuccess: (data) => { + if (data.isRegistered) { + // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 + // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 + setAccessToken(data.accessToken); + + // 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지 + const redirectParam = searchParams.get("redirect"); + const safeRedirect = validateSafeRedirect(redirectParam); + + toast.success("로그인에 성공했습니다."); + + setTimeout(() => { + router.push(safeRedirect); + }, 100); + } else { + // 새로운 회원일 시 - 회원가입 페이지로 이동 + router.push(`/sign-up?token=${data.signUpToken}`); + } + }, + onError: () => { + toast.error("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); + router.push("/login"); + }, + }); }; export default usePostKakaoAuth; diff --git a/src/apis/universities/server/getRecommendedUniversity.ts b/src/apis/universities/server/getRecommendedUniversity.ts index 64813b4c..591cd140 100644 --- a/src/apis/universities/server/getRecommendedUniversity.ts +++ b/src/apis/universities/server/getRecommendedUniversity.ts @@ -1,14 +1,20 @@ +import type { ListUniversity } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; -import { ListUniversity } from "@/types/university"; - -type GetRecommendedUniversityResponse = { recommendedUniversities: ListUniversity[] }; +type GetRecommendedUniversityResponse = { + recommendedUniversities: ListUniversity[]; +}; const getRecommendedUniversity = async () => { - const endpoint = "/univ-apply-infos/recommend"; + const endpoint = "/univ-apply-infos/recommend"; + + const res = await serverFetch(endpoint); + + if (!res.ok) { + console.error(`Failed to fetch recommended universities:`, res.error); + } - const res = await serverFetch(endpoint); - return res; + return res; }; export default getRecommendedUniversity; diff --git a/src/apis/universities/server/getSearchUniversitiesByFilter.ts b/src/apis/universities/server/getSearchUniversitiesByFilter.ts index 22d3a56d..b582de74 100644 --- a/src/apis/universities/server/getSearchUniversitiesByFilter.ts +++ b/src/apis/universities/server/getSearchUniversitiesByFilter.ts @@ -1,52 +1,66 @@ import { URLSearchParams } from "url"; - +import type { + CountryCode, + LanguageTestType, + ListUniversity, +} from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; -import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; - interface UniversitySearchResponse { - univApplyInfoPreviews: ListUniversity[]; + univApplyInfoPreviews: ListUniversity[]; } /** * 필터 검색에 사용될 파라미터 타입 */ export interface UniversitySearchFilterParams { - languageTestType?: LanguageTestType; - testScore?: number; - countryCode?: CountryCode[]; + languageTestType?: LanguageTestType; + testScore?: number; + countryCode?: CountryCode[]; } export const getSearchUniversitiesByFilter = async ( - filters: UniversitySearchFilterParams, + filters: UniversitySearchFilterParams, ): Promise => { - const params = new URLSearchParams(); - - if (filters.languageTestType) { - params.append("languageTestType", filters.languageTestType); - } - if (filters.testScore !== undefined) { - params.append("testScore", String(filters.testScore)); - } - // countryCode는 여러 개일 수 있으므로 각각 append 해줍니다. - if (filters.countryCode) { - filters.countryCode.forEach((code) => params.append("countryCode", code)); - } - - // 필터 값이 하나도 없으면 빈 배열을 반환합니다. - if (params.size === 0) { - return []; - } - - const endpoint = `/univ-apply-infos/search/filter?${params.toString()}`; - const response = await serverFetch(endpoint); - - return response.ok ? response.data.univApplyInfoPreviews : []; + const params = new URLSearchParams(); + + if (filters.languageTestType) { + params.append("languageTestType", filters.languageTestType); + } + if (filters.testScore !== undefined) { + params.append("testScore", String(filters.testScore)); + } + // countryCode는 여러 개일 수 있으므로 각각 append 해줍니다. + if (filters.countryCode) { + filters.countryCode.forEach((code) => params.append("countryCode", code)); + } + + // 필터 값이 하나도 없으면 빈 배열을 반환합니다. + if (params.size === 0) { + return []; + } + + const endpoint = `/univ-apply-infos/search/filter?${params.toString()}`; + const response = await serverFetch(endpoint); + + if (!response.ok) { + console.error(`Failed to search universities by filter:`, response.error); + return []; + } + + return response.data.univApplyInfoPreviews; }; -export const getSearchUniversitiesAllRegions = async (): Promise => { - const endpoint = `/univ-apply-infos/search/filter`; - const response = await serverFetch(endpoint); +export const getSearchUniversitiesAllRegions = async (): Promise< + ListUniversity[] +> => { + const endpoint = `/univ-apply-infos/search/filter`; + const response = await serverFetch(endpoint); + + if (!response.ok) { + console.error(`Failed to fetch all regions universities:`, response.error); + return []; + } - return response.ok ? response.data.univApplyInfoPreviews : []; + return response.data.univApplyInfoPreviews; }; diff --git a/src/apis/universities/server/getSearchUniversitiesByText.ts b/src/apis/universities/server/getSearchUniversitiesByText.ts index c6da3522..6403bf40 100644 --- a/src/apis/universities/server/getSearchUniversitiesByText.ts +++ b/src/apis/universities/server/getSearchUniversitiesByText.ts @@ -1,45 +1,60 @@ +import { + type AllRegionsUniversityList, + type ListUniversity, + RegionEnumExtend, +} from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; -import { AllRegionsUniversityList, ListUniversity, RegionEnumExtend } from "@/types/university"; - // --- 타입 정의 --- interface UniversitySearchResponse { - univApplyInfoPreviews: ListUniversity[]; + univApplyInfoPreviews: ListUniversity[]; } -export const getUniversitiesByText = async (value: string): Promise => { - if (value === null || value === undefined) { - return []; - } - const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`; - const response = await serverFetch(endpoint); - return response.ok ? response.data.univApplyInfoPreviews : []; +export const getUniversitiesByText = async ( + value: string, +): Promise => { + if (value === null || value === undefined) { + return []; + } + const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`; + const response = await serverFetch(endpoint); + + if (!response.ok) { + console.error( + `Failed to search universities by text (value: "${value}"):`, + response.error, + ); + return []; + } + + return response.data.univApplyInfoPreviews; }; export const getAllUniversities = async (): Promise => { - return getUniversitiesByText(""); + return getUniversitiesByText(""); }; -export const getCategorizedUniversities = async (): Promise => { - // 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다. - const allUniversities = await getAllUniversities(); - - const categorizedList: AllRegionsUniversityList = { - [RegionEnumExtend.ALL]: allUniversities, - [RegionEnumExtend.AMERICAS]: [], - [RegionEnumExtend.EUROPE]: [], - [RegionEnumExtend.ASIA]: [], - [RegionEnumExtend.CHINA]: [], - }; - if (!allUniversities) return categorizedList; - - for (const university of allUniversities) { - const region = university.region as RegionEnumExtend; // API 응답의 region 타입을 enum으로 간주 - - if (region && categorizedList.hasOwnProperty(region)) { - categorizedList[region].push(university); - } - } - - return categorizedList; -}; +export const getCategorizedUniversities = + async (): Promise => { + // 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다. + const allUniversities = await getAllUniversities(); + + const categorizedList: AllRegionsUniversityList = { + [RegionEnumExtend.ALL]: allUniversities, + [RegionEnumExtend.AMERICAS]: [], + [RegionEnumExtend.EUROPE]: [], + [RegionEnumExtend.ASIA]: [], + [RegionEnumExtend.CHINA]: [], + }; + if (!allUniversities) return categorizedList; + + for (const university of allUniversities) { + const region = university.region as RegionEnumExtend; // API 응답의 region 타입을 enum으로 간주 + + if (region && Object.hasOwn(categorizedList, region)) { + categorizedList[region].push(university); + } + } + + return categorizedList; + }; diff --git a/src/apis/universities/server/getUniversityDetail.ts b/src/apis/universities/server/getUniversityDetail.ts index 548082d7..3d91ea7b 100644 --- a/src/apis/universities/server/getUniversityDetail.ts +++ b/src/apis/universities/server/getUniversityDetail.ts @@ -1,9 +1,20 @@ +import type { University } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; -import { University } from "@/types/university"; +export const getUniversityDetail = async ( + universityInfoForApplyId: number, +): Promise => { + const result = await serverFetch( + `/univ-apply-infos/${universityInfoForApplyId}`, + ); -export const getUniversityDetail = async (universityInfoForApplyId: number): Promise => { - const result = await serverFetch(`/univ-apply-infos/${universityInfoForApplyId}`); + if (!result.ok) { + console.error( + `Failed to fetch university detail (ID: ${universityInfoForApplyId}):`, + result.error, + ); + return undefined; + } - return result.data; + return result.data; }; diff --git a/src/app/mentor/_ui/MentorClient/index.tsx b/src/app/mentor/_ui/MentorClient/index.tsx index 32f389aa..823f8bbc 100644 --- a/src/app/mentor/_ui/MentorClient/index.tsx +++ b/src/app/mentor/_ui/MentorClient/index.tsx @@ -1,72 +1,71 @@ "use client"; import { useState } from "react"; - -import { tokenParse } from "@/utils/jwtUtils"; - import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; - +import useAuthStore from "@/lib/zustand/useAuthStore"; +import { UserRole } from "@/types/mentor"; +import { tokenParse } from "@/utils/jwtUtils"; import MenteePageTabs from "./_ui/MenteePageTabs"; import MentorFindSection from "./_ui/MentorFindSection"; import MentorPage from "./_ui/MentorPage"; -import { UserRole } from "@/types/mentor"; - -import useAuthStore from "@/lib/zustand/useAuthStore"; - const MentorClient = () => { - const { isLoading, accessToken } = useAuthStore(); - const parsedToken = tokenParse(accessToken); - const userRole = parsedToken?.role; - const isMentor = userRole === UserRole.MENTOR || userRole === UserRole.ADMIN; - const isAdmin = userRole === UserRole.ADMIN; + const { isInitialized, accessToken } = useAuthStore(); + const parsedToken = tokenParse(accessToken); + const userRole = parsedToken?.role; + const isMentor = userRole === UserRole.MENTOR || userRole === UserRole.ADMIN; + const isAdmin = userRole === UserRole.ADMIN; - // 어드민 전용: 뷰 전환 상태 (true: 멘토 뷰, false: 멘티 뷰) - const [showMentorView, setShowMentorView] = useState(true); + // 어드민 전용: 뷰 전환 상태 (true: 멘토 뷰, false: 멘티 뷰) + const [showMentorView, setShowMentorView] = useState(true); - if (isLoading || !accessToken) return ; // 로딩 중일 때 스피너 표시 + if (!isInitialized || !accessToken) return ; - // 어드민이 아닌 경우 기존 로직대로 - const shouldShowMentorView = isAdmin ? showMentorView : isMentor; + // 어드민이 아닌 경우 기존 로직대로 + const shouldShowMentorView = isAdmin ? showMentorView : isMentor; - return ( - <> - {/* 어드민 전용 뷰 전환 버튼 */} - {isAdmin && ( -
- - -
- )} + return ( + <> + {/* 어드민 전용 뷰 전환 버튼 */} + {isAdmin && ( +
+ + +
+ )} - {shouldShowMentorView ? ( - // 멘토페이지 - - ) : ( - // 멘티페이지 - <> - {/* 탭 및 나의 멘토 , 멘티요청 리스트 채팅카드 */} - - {/* 멘토찾기 섹션 */} - - - )} - - ); + {shouldShowMentorView ? ( + // 멘토페이지 + + ) : ( + // 멘티페이지 + <> + {/* 탭 및 나의 멘토 , 멘티요청 리스트 채팅카드 */} + + {/* 멘토찾기 섹션 */} + + + )} + + ); }; export default MentorClient; diff --git a/src/app/my/modify/_ui/ModifyContent/index.tsx b/src/app/my/modify/_ui/ModifyContent/index.tsx index f9c02dd0..197ca9d7 100644 --- a/src/app/my/modify/_ui/ModifyContent/index.tsx +++ b/src/app/my/modify/_ui/ModifyContent/index.tsx @@ -1,75 +1,95 @@ "use client"; -import { FormProvider } from "react-hook-form"; - import clsx from "clsx"; +import { FormProvider } from "react-hook-form"; import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; - +import { UserRole } from "@/types/mentor"; import useModifyUserHookform from "./_hooks/useModifyUserHookform"; import ImageInputFiled from "./_ui/ImageInputFiled"; import InputField from "./_ui/InputFiled"; import ReadOnlyField from "./_ui/ReadOnlyField"; -import { UserRole } from "@/types/mentor"; - const ModifyContent = () => { - const { methods, myInfo, onSubmit } = useModifyUserHookform(); + const { methods, myInfo, onSubmit } = useModifyUserHookform(); - const defaultUniversity: string = - myInfo?.role === UserRole.MENTOR && myInfo.attendedUniversity ? myInfo.attendedUniversity : "인하대학교"; + const defaultUniversity: string = + (myInfo?.role === UserRole.MENTOR || myInfo?.role === UserRole.ADMIN) && + myInfo.attendedUniversity + ? myInfo.attendedUniversity + : "인하대학교"; - const { - handleSubmit, - formState: { isValid, isDirty }, - } = methods; + const { + handleSubmit, + formState: { isValid, isDirty }, + } = methods; - if (!myInfo) { - return ; - } - return ( - -
-
- {/* Profile Image Section */} - + if (!myInfo) { + return ; + } + return ( + +
+ + {/* Profile Image Section */} + - {/* Form Fields */} -
- {/* 닉네임 - 수정 가능 */} - + {/* Form Fields */} +
+ {/* 닉네임 - 수정 가능 */} + - {/* 출신학교 - 읽기 전용 */} - + {/* 출신학교 - 읽기 전용 */} + - {/* 수학 학교 - 읽기 전용 */} - + {/* 수학 학교 - 읽기 전용 */} + - {/* 사용자 유형 - 읽기 전용 */} - -
+ {/* 사용자 유형 - 읽기 전용 */} + +
- {/* Submit Button */} -
- -
- -
-
- ); + {/* Submit Button */} +
+ +
+ +
+
+ ); }; export default ModifyContent; diff --git a/src/components/login/signup/SignupSurvey.tsx b/src/components/login/signup/SignupSurvey.tsx index 8f98196b..fb59fab5 100644 --- a/src/components/login/signup/SignupSurvey.tsx +++ b/src/components/login/signup/SignupSurvey.tsx @@ -2,171 +2,181 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; - +import { usePostSignUp } from "@/apis/Auth"; +import { useUploadProfileImagePublic } from "@/apis/image-upload"; import { Progress } from "@/components/ui/Progress"; +import useAuthStore from "@/lib/zustand/useAuthStore"; +import { toast } from "@/lib/zustand/useToastStore"; +import type { PreparationStatus, SignUpRequest } from "@/types/auth"; +import type { RegionKo } from "@/types/university"; import SignupPolicyScreen from "./SignupPolicyScreen"; import SignupPrepareScreen from "./SignupPrepareScreen"; import SignupProfileScreen from "./SignupProfileScreen"; import SignupRegionScreen from "./SignupRegionScreen"; -import { PreparationStatus, SignUpRequest } from "@/types/auth"; -import { RegionKo } from "@/types/university"; - -import { usePostSignUp } from "@/apis/Auth"; -import { useUploadProfileImagePublic } from "@/apis/image-upload"; -import useAuthStore from "@/lib/zustand/useAuthStore"; -import { toast } from "@/lib/zustand/useToastStore"; - type SignupSurveyProps = { - baseNickname: string; - baseEmail: string; - baseProfileImageUrl: string; + baseNickname: string; + baseEmail: string; + baseProfileImageUrl: string; }; -const SignupSurvey = ({ baseNickname, baseEmail, baseProfileImageUrl }: SignupSurveyProps) => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const signUpToken = searchParams?.get("token"); - if (!signUpToken) { - router.push("/login"); - } - const { setAccessToken } = useAuthStore(); - const [curStage, setCurStage] = useState(1); - const [curProgress, setCurProgress] = useState(0); - - const [curPreparation, setCurPreparation] = useState(null); - - const [region, setRegion] = useState(null); - const [countries, setCountries] = useState([]); - - const [nickname, setNickname] = useState(baseNickname); - const [profileImageFile, setProfileImageFile] = useState(null); - - const signUpMutation = usePostSignUp(); - const uploadImageMutation = useUploadProfileImagePublic(); - - useEffect(() => { - setCurProgress(((curStage - 1) / 3) * 100); - }, [curStage]); - - const createRegisterRequest = async (): Promise => { - const submitRegion: RegionKo[] = region === "아직 잘 모르겠어요" ? [] : [region as RegionKo]; - - if (!curPreparation) { - throw new Error("준비 단계를 선택해주세요"); - } - - let imageUrl: string | null = baseProfileImageUrl; - - if (profileImageFile) { - try { - const result = await uploadImageMutation.mutateAsync(profileImageFile); - imageUrl = result.fileUrl; - } catch (err: unknown) { - const error = err as { message?: string }; - console.error("Error", error.message); - // toast.error는 hook의 onError에서 이미 처리되므로 중복 호출 제거 - } - } - - return { - signUpToken: signUpToken as string, - interestedRegions: submitRegion, - interestedCountries: countries, - preparationStatus: curPreparation, - nickname, - profileImageUrl: imageUrl, - }; - }; - - const submitRegisterRequest = async () => { - try { - const registerRequest = await createRegisterRequest(); - signUpMutation.mutate(registerRequest, { - onSuccess: (data) => { - setAccessToken(data.accessToken); - toast.success("회원가입이 완료되었습니다."); - router.push("/"); - }, - onError: (error: unknown) => { - const axiosError = error as { - response?: { data?: { message?: string } }; - message?: string; - }; - if (axiosError.response) { - console.error("Axios response error", axiosError.response); - toast.error(axiosError.response.data?.message || "회원가입에 실패했습니다."); - } else { - console.error("Error", axiosError.message); - toast.error(axiosError.message || "회원가입에 실패했습니다."); - } - }, - }); - } catch (err: unknown) { - const error = err as { message?: string }; - console.error("Error", error.message); - toast.error(error.message || "회원가입에 실패했습니다."); - } - }; - - const renderCurrentSurvey = () => { - switch (curStage) { - case 1: - return ( - { - setCurStage(2); - }} - /> - ); - case 2: - return ( - { - setCurStage(3); - }} - /> - ); - case 3: - return ( - { - setCurStage(4); - }} - /> - ); - case 4: - return ( - - ); - default: - return
회원 가입이 완료되었습니다
; - } - }; - - return ( -
-
- -
- {renderCurrentSurvey()} -
- ); +const SignupSurvey = ({ + baseNickname, + baseEmail, + baseProfileImageUrl, +}: SignupSurveyProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const signUpToken = searchParams?.get("token"); + if (!signUpToken) { + router.push("/login"); + } + const { setAccessToken } = useAuthStore(); + const [curStage, setCurStage] = useState(1); + const [curProgress, setCurProgress] = useState(0); + + const [curPreparation, setCurPreparation] = + useState(null); + + const [region, setRegion] = useState( + null, + ); + const [countries, setCountries] = useState([]); + + const [nickname, setNickname] = useState(baseNickname); + const [profileImageFile, setProfileImageFile] = useState(null); + + const signUpMutation = usePostSignUp(); + const uploadImageMutation = useUploadProfileImagePublic(); + + useEffect(() => { + setCurProgress(((curStage - 1) / 3) * 100); + }, [curStage]); + + const createRegisterRequest = async (): Promise => { + const submitRegion: RegionKo[] = + region === "아직 잘 모르겠어요" ? [] : [region as RegionKo]; + + if (!curPreparation) { + throw new Error("준비 단계를 선택해주세요"); + } + + let imageUrl: string | null = baseProfileImageUrl; + + if (profileImageFile) { + try { + const result = await uploadImageMutation.mutateAsync(profileImageFile); + imageUrl = result.fileUrl; + } catch (err: unknown) { + const error = err as { message?: string }; + console.error("Error", error.message); + // toast.error는 hook의 onError에서 이미 처리되므로 중복 호출 제거 + } + } + + return { + signUpToken: signUpToken as string, + interestedRegions: submitRegion, + interestedCountries: countries, + preparationStatus: curPreparation, + nickname, + profileImageUrl: imageUrl, + }; + }; + + const submitRegisterRequest = async () => { + try { + const registerRequest = await createRegisterRequest(); + signUpMutation.mutate(registerRequest, { + onSuccess: (data) => { + setAccessToken(data.accessToken); + toast.success("회원가입이 완료되었습니다."); + + setTimeout(() => { + router.push("/"); + }, 100); + }, + onError: (error: unknown) => { + const axiosError = error as { + response?: { data?: { message?: string } }; + message?: string; + }; + if (axiosError.response) { + console.error("Axios response error", axiosError.response); + toast.error( + axiosError.response.data?.message || "회원가입에 실패했습니다.", + ); + } else { + console.error("Error", axiosError.message); + toast.error(axiosError.message || "회원가입에 실패했습니다."); + } + }, + }); + } catch (err: unknown) { + const error = err as { message?: string }; + console.error("Error", error.message); + toast.error(error.message || "회원가입에 실패했습니다."); + } + }; + + const renderCurrentSurvey = () => { + switch (curStage) { + case 1: + return ( + { + setCurStage(2); + }} + /> + ); + case 2: + return ( + { + setCurStage(3); + }} + /> + ); + case 3: + return ( + { + setCurStage(4); + }} + /> + ); + case 4: + return ( + + ); + default: + return
회원 가입이 완료되었습니다
; + } + }; + + return ( +
+
+ +
+ {renderCurrentSurvey()} +
+ ); }; export default SignupSurvey; diff --git a/src/components/mentor/MentorApplyCountContent/index.tsx b/src/components/mentor/MentorApplyCountContent/index.tsx index f8a668d3..24aeb0b5 100644 --- a/src/components/mentor/MentorApplyCountContent/index.tsx +++ b/src/components/mentor/MentorApplyCountContent/index.tsx @@ -2,63 +2,64 @@ import Link from "next/link"; import { useState } from "react"; - -import { tokenParse } from "@/utils/jwtUtils"; - -import { UserRole } from "@/types/mentor"; - import { useGetMentoringUncheckedCount } from "@/apis/mentor"; import useAuthStore from "@/lib/zustand/useAuthStore"; +import { UserRole } from "@/types/mentor"; +import { tokenParse } from "@/utils/jwtUtils"; const MentorApplyCountContent = () => { - // 로그인 된경우에만 신규 신청 카운트 모달 표시 - const { accessToken, isLoading } = useAuthStore(); - const isMentor = - tokenParse(accessToken)?.role === UserRole.MENTOR || tokenParse(accessToken)?.role === UserRole.ADMIN; + // 로그인 된경우에만 신규 신청 카운트 모달 표시 + const { accessToken, isInitialized } = useAuthStore(); + const isMentor = + tokenParse(accessToken)?.role === UserRole.MENTOR || + tokenParse(accessToken)?.role === UserRole.ADMIN; - const { data: count, isSuccess } = useGetMentoringUncheckedCount(!!accessToken && isMentor && !isLoading); + const { data: count, isSuccess } = useGetMentoringUncheckedCount( + isInitialized && !!accessToken && isMentor, + ); - const [isModalOpen, setIsModalOpen] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(true); - // 신규 신청 없으면 표시 - if (!isMentor || isLoading || !isSuccess || !isModalOpen || count === 0) return null; + // 신규 신청 없으면 표시 + if (!isInitialized || !isMentor || !isSuccess || !isModalOpen || count === 0) + return null; - return ( -
- {/* close button */} - - setIsModalOpen(false)}> -
- {/* left: message */} -
-

알림

-

새로운 요청이 들어왔어요!

-

어서 요청을 수락해주세요.

-
+ return ( +
+ {/* close button */} + + setIsModalOpen(false)}> +
+ {/* left: message */} +
+

알림

+

새로운 요청이 들어왔어요!

+

어서 요청을 수락해주세요.

+
- {/* divider */} -
+ {/* divider */} +
- {/* right: count */} -
- 신규 신청 -
{count}명
-
-
- -
- ); + {/* right: count */} +
+ 신규 신청 +
{count}명
+
+
+ +
+ ); }; export default MentorApplyCountContent;