diff --git a/apps/university-web/src/apis/applications/api.ts b/apps/university-web/src/apis/applications/api.ts index 6e233dd5a..111d02e64 100644 --- a/apps/university-web/src/apis/applications/api.ts +++ b/apps/university-web/src/apis/applications/api.ts @@ -16,9 +16,7 @@ export interface UseSubmitApplicationRequest { gpaScoreId: number; languageTestScoreId: number; universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; + choices: number[]; }; } diff --git a/apps/university-web/src/components/ui/Tab/index.tsx b/apps/university-web/src/components/ui/Tab/index.tsx index 27baa9db8..07eb2342d 100644 --- a/apps/university-web/src/components/ui/Tab/index.tsx +++ b/apps/university-web/src/components/ui/Tab/index.tsx @@ -1,7 +1,7 @@ -type TabProps = { - choices: string[]; - choice: string; - setChoice: React.Dispatch>; +type TabProps = { + choices: readonly T[]; + choice: T; + setChoice: (choice: T) => void; color?: { activeBtn?: string; deactiveBtn?: string; @@ -10,7 +10,7 @@ type TabProps = { }; }; -const Tab = ({ choices, choice, setChoice, color }: TabProps) => { +const Tab = ({ choices, choice, setChoice, color }: TabProps) => { const defaultColor = { activeBtnFont: "text-black", deactiveBtnFont: "text-gray-200", diff --git a/apps/university-web/src/types/application.ts b/apps/university-web/src/types/application.ts index 6dcdc3db3..5503e75a4 100644 --- a/apps/university-web/src/types/application.ts +++ b/apps/university-web/src/types/application.ts @@ -45,15 +45,11 @@ export interface ApplicationScoreRequest { } export interface ApplicationUniversityRequest { - firstChoiceUniversityId: number; - secondChoiceUniversityId: number; - thirdChoiceUniversityId: number; + choices: number[]; } export interface ApplicationListResponse { - firstChoice: ScoreSheet[]; - secondChoice: ScoreSheet[]; - thirdChoice: ScoreSheet[]; + choices: ScoreSheet[][]; } export interface ApplicationStatusResponse { @@ -65,9 +61,7 @@ export interface SubmitApplicationRequest { gpaScoreId: number; languageTestScoreId: number; universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; + choices: number[]; }; } diff --git a/apps/web/src/apis/applications/api.ts b/apps/web/src/apis/applications/api.ts index 6e233dd5a..111d02e64 100644 --- a/apps/web/src/apis/applications/api.ts +++ b/apps/web/src/apis/applications/api.ts @@ -16,9 +16,7 @@ export interface UseSubmitApplicationRequest { gpaScoreId: number; languageTestScoreId: number; universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; + choices: number[]; }; } diff --git a/apps/web/src/app/university/application/ScorePageContent.tsx b/apps/web/src/app/university/application/ScorePageContent.tsx index 6e030ea7d..b44bd1715 100644 --- a/apps/web/src/app/university/application/ScorePageContent.tsx +++ b/apps/web/src/app/university/application/ScorePageContent.tsx @@ -6,7 +6,8 @@ import { useGetApplicationsList } from "@/apis/applications"; import ConfirmCancelModal from "@/components/modal/ConfirmCancelModal"; import ButtonTab from "@/components/ui/ButtonTab"; import Tab from "@/components/ui/Tab"; -import { REGIONS_KO } from "@/constants/university"; +import { DEFAULT_MAX_CHOICE_COUNT, getHomeUniversityById, REGIONS_KO } from "@/constants/university"; +import useAuthStore from "@/lib/zustand/useAuthStore"; import type { ScoreSheet as ScoreSheetType } from "@/types/application"; import type { RegionKo } from "@/types/university"; import ApplicationSectionTitle from "./_components/ApplicationSectionTitle"; @@ -14,31 +15,28 @@ import ScoreSearchBar from "./ScoreSearchBar"; import ScoreSearchField from "./ScoreSearchField"; import ScoreSheet from "./ScoreSheet"; -const PREFERENCE_CHOICE: ("1순위" | "2순위" | "3순위")[] = ["1순위", "2순위", "3순위"]; - -interface ScoreData { - firstChoice: ScoreSheetType[]; - secondChoice: ScoreSheetType[]; - thirdChoice: ScoreSheetType[]; -} - const ScorePageContent = () => { const router = useRouter(); const searchRef = useRef(null!); + const homeUniversityId = useAuthStore((state) => state.homeUniversityId); + const maxChoiceCount = getHomeUniversityById(homeUniversityId)?.maxChoiceCount ?? DEFAULT_MAX_CHOICE_COUNT; const [searchActive, setSearchActive] = useState(false); - const [preference, setPreference] = useState<"1순위" | "2순위" | "3순위">("1순위"); + const [preferenceIndex, setPreferenceIndex] = useState(0); const [regionFilter, setRegionFilter] = useState(""); const [searchValue, setSearchValue] = useState(""); const [showNeedApply, _setShowNeedApply] = useState(false); - const initialData: ScoreData = { - firstChoice: [], - secondChoice: [], - thirdChoice: [], - }; - - const { data: scoreResponseData = initialData, isError, isLoading } = useGetApplicationsList(); + const emptyChoices = useMemo( + () => Array.from({ length: maxChoiceCount }, () => [] as ScoreSheetType[]), + [maxChoiceCount], + ); + const { data: scoreResponseData, isError, isLoading } = useGetApplicationsList(); + const scoreChoices = scoreResponseData?.choices ?? emptyChoices; + const preferenceChoices = useMemo( + () => Array.from({ length: Math.max(scoreChoices.length, maxChoiceCount) }, (_, index) => `${index + 1}순위`), + [maxChoiceCount, scoreChoices.length], + ); const filteredAndSortedData = useMemo(() => { // ✨ 1. 대학 이름(koreanName)을 기준으로 중복을 제거하는 헬퍼 함수 @@ -49,17 +47,10 @@ const ScorePageContent = () => { return Array.from(universityMap.values()); }; - // ✨ 2. API 응답 데이터를 받자마자 중복부터 제거합니다. - const firstChoice = uniqueByKoreanName(scoreResponseData?.firstChoice || []); - const secondChoice = uniqueByKoreanName(scoreResponseData?.secondChoice || []); - const thirdChoice = uniqueByKoreanName(scoreResponseData?.thirdChoice || []); - - // 3. 중복이 제거된 데이터를 정렬합니다. - const sortedData = { - firstChoice: [...firstChoice].sort((a, b) => b.applicants.length - a.applicants.length), - secondChoice: [...secondChoice].sort((a, b) => b.applicants.length - a.applicants.length), - thirdChoice: [...thirdChoice].sort((a, b) => b.applicants.length - a.applicants.length), - }; + // ✨ 2. API 응답 데이터를 받자마자 중복부터 제거하고 정렬합니다. + const sortedData = scoreChoices.map((choice) => + uniqueByKoreanName(choice).sort((a, b) => b.applicants.length - a.applicants.length), + ); // 4. 기존 필터링 로직을 적용합니다. const applyFilters = (data: ScoreSheetType[]) => { @@ -73,12 +64,8 @@ const ScorePageContent = () => { return result; }; - return { - firstChoice: applyFilters(sortedData.firstChoice), - secondChoice: applyFilters(sortedData.secondChoice), - thirdChoice: applyFilters(sortedData.thirdChoice), - }; - }, [scoreResponseData, regionFilter, searchValue]); + return sortedData.map(applyFilters); + }, [scoreChoices, regionFilter, searchValue]); // (이하 코드는 동일) const handleSearch = (event: FormEvent) => { @@ -102,19 +89,18 @@ const ScorePageContent = () => { setSearchActive(true); }; - const getScoreSheet = () => { - switch (preference) { - case "1순위": - return filteredAndSortedData.firstChoice; - case "2순위": - return filteredAndSortedData.secondChoice; - case "3순위": - return filteredAndSortedData.thirdChoice; - default: - return []; - } + const handlePreferenceChange = (nextPreference: string) => { + const nextIndex = preferenceChoices.indexOf(nextPreference); + setPreferenceIndex(nextIndex >= 0 ? nextIndex : 0); }; - const scoreSheets = getScoreSheet(); + + const selectedPreference = preferenceChoices[preferenceIndex] ?? preferenceChoices[0] ?? "1순위"; + const scoreSheets = filteredAndSortedData[preferenceIndex] ?? []; + + useEffect(() => { + if (preferenceIndex < preferenceChoices.length) return; + setPreferenceIndex(0); + }, [preferenceChoices.length, preferenceIndex]); useEffect(() => { if (isLoading) return; @@ -141,7 +127,7 @@ const ScorePageContent = () => { ) : ( <>
- +
{ return ( -
-
- {label} -
+
+ {label}
); }; diff --git a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx index 823f95c3c..80f29ff19 100644 --- a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx +++ b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx @@ -1,13 +1,15 @@ "use client"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { usePostSubmitApplication } from "@/apis/applications"; import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores"; import { useUniversitySearch } from "@/apis/universities"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; import ProgressBar from "@/components/ui/ProgressBar"; +import { DEFAULT_MAX_CHOICE_COUNT, getHomeUniversityById } from "@/constants/university"; import { showIconToast } from "@/lib/toast/showIconToast"; +import useAuthStore from "@/lib/zustand/useAuthStore"; import type { ListUniversity } from "@/types/university"; import ConfirmStep from "./ConfirmStep"; import DoneStep from "./DoneStep"; @@ -16,11 +18,22 @@ import GpaStep from "./GpaStep"; import LanguageStep from "./LanguageStep"; import UniversityStep from "./UniversityStep"; +const APPLY_PROGRESS_TOTAL_STEPS = 5; + const ApplyPageContent = () => { const router = useRouter(); + const homeUniversityId = useAuthStore((state) => state.homeUniversityId); const [step, setStep] = useState(1); + const maxChoiceCount = getHomeUniversityById(homeUniversityId)?.maxChoiceCount ?? DEFAULT_MAX_CHOICE_COUNT; + const universitySearchOptions = useMemo( + () => ({ + useDefaultTermId: true, + homeUniversityId: homeUniversityId ?? undefined, + }), + [homeUniversityId], + ); - const { data: universityList = [] } = useUniversitySearch("", undefined, { useDefaultTermId: true }); + const { data: universityList = [] } = useUniversitySearch("", undefined, universitySearchOptions); const { data: gpaScoreList = [] } = useGetMyGpaScore(); const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore(); const { mutate: postSubmitApplication } = usePostSubmitApplication({ @@ -54,7 +67,11 @@ const ApplyPageContent = () => { return; } - if (curUniversityList.length === 0 || curUniversityList[0] === 0) { + const selectedUniversityIds = curUniversityList + .filter((universityId) => Number.isInteger(universityId) && universityId > 0) + .slice(0, maxChoiceCount); + + if (selectedUniversityIds.length === 0) { showIconToast("logo", "대학교를 선택해주세요."); return; } @@ -63,19 +80,22 @@ const ApplyPageContent = () => { gpaScoreId: curGpaScore, languageTestScoreId: curLanguageTestScore, universityChoiceRequest: { - firstChoiceUniversityId: curUniversityList[0] || null, - secondChoiceUniversityId: curUniversityList[1] || null, - thirdChoiceUniversityId: curUniversityList[2] || null, + choices: selectedUniversityIds, }, }); }; const isDataExist = gpaScoreList.length === 0 || languageTestScoreList.length === 0; + const hasSelectedUniversity = curUniversityList.some((universityId) => universityId > 0); + const progressStep = step === 3 && hasSelectedUniversity ? APPLY_PROGRESS_TOTAL_STEPS : step + 1; + return ( <>
- {(step === 1 || step === 2 || step === 3) && } + {(step === 1 || step === 2 || step === 3) && ( + + )}
{isDataExist ? ( @@ -102,6 +122,7 @@ const ApplyPageContent = () => { universityList={universityList} curUniversityList={curUniversityList} setCurUniversityList={setCurUniversityList} + maxChoiceCount={maxChoiceCount} onNext={goNextStep} /> )} diff --git a/apps/web/src/app/university/application/apply/ConfirmStep.tsx b/apps/web/src/app/university/application/apply/ConfirmStep.tsx index 26cdc5d3e..cd8fa7a65 100644 --- a/apps/web/src/app/university/application/apply/ConfirmStep.tsx +++ b/apps/web/src/app/university/application/apply/ConfirmStep.tsx @@ -3,41 +3,58 @@ import clsx from "clsx"; import { IconCheck } from "@/public/svgs/mentor"; import type { ListUniversity } from "@/types/university"; import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar"; -import ApplicationSectionTitle from "../_components/ApplicationSectionTitle"; type ConfirmStepProps = { universityList: ListUniversity[]; onNext: () => void; }; +type ConfirmUniversityCardProps = { + universityList: ListUniversity[]; +}; + +const ConfirmUniversityCard = ({ universityList }: ConfirmUniversityCardProps) => { + if (universityList.length === 0) return null; + + return ( +
+ {universityList.map((university, index) => ( +
+ {index + 1}지망 + + [{university.country}] {university.koreanName} + +
+ ))} +
+ ); +}; + const ConfirmStep = ({ universityList, onNext }: ConfirmStepProps) => { return ( -
+
- - -
-
- {universityList.map((university, index) => ( -
- {index + 1}지망 - {university.koreanName} -
- ))} -
+ +
+

+ 지원 확인하기 +

+

+ {"지원은 총 3번만 수정 가능하며,\n제출 완료 후 성적을 변경 하실 수 없습니다."} +

+ +
); diff --git a/apps/web/src/app/university/application/apply/UniversityStep.tsx b/apps/web/src/app/university/application/apply/UniversityStep.tsx index bbbfd7c25..d234563e8 100644 --- a/apps/web/src/app/university/application/apply/UniversityStep.tsx +++ b/apps/web/src/app/university/application/apply/UniversityStep.tsx @@ -1,32 +1,90 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import TextModal from "@/components/modal/TextModal"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/Select"; import type { ListUniversity } from "@/types/university"; import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar"; -import ApplicationSectionTitle from "../_components/ApplicationSectionTitle"; type UniversityStepProps = { universityList: ListUniversity[]; curUniversityList: number[]; setCurUniversityList: (idList: number[]) => void; + maxChoiceCount: number; onNext: () => void; }; -const UniversityStep = ({ universityList, curUniversityList, setCurUniversityList, onNext }: UniversityStepProps) => { +type UniversityChoiceSelectProps = { + index: number; + universityList: ListUniversity[]; + selectedUniversityId?: number; + isSelectable: boolean; + onSelect: (index: number, universityId: number) => void; + isDisabled: (universityId: number, currentIndex: number) => boolean; +}; + +const UniversityChoiceSelect = ({ + index, + universityList, + selectedUniversityId, + isSelectable, + onSelect, + isDisabled, +}: UniversityChoiceSelectProps) => { + return ( +
+ + +
+ ); +}; + +const UniversityStep = ({ + universityList, + curUniversityList, + setCurUniversityList, + maxChoiceCount, + onNext, +}: UniversityStepProps) => { const [isModalOpen, setIsModalOpen] = useState(false); + const choiceIndexes = useMemo(() => Array.from({ length: maxChoiceCount }, (_, index) => index), [maxChoiceCount]); const handleSelect = (index: number, value: number) => { - const newList = [...curUniversityList]; + const newList = curUniversityList.slice(0, maxChoiceCount); newList[index] = value; + if (value === 0) { + newList.length = index + 1; + } setCurUniversityList(newList); }; const isDisabled = (universityId: number, currentIndex: number) => - curUniversityList.some((pickedId, i) => i !== currentIndex && pickedId === universityId); + curUniversityList.some((pickedId, i) => i !== currentIndex && pickedId > 0 && pickedId === universityId); + + const isSelectable = (index: number) => index === 0 || Number(curUniversityList[index - 1]) > 0; const handleNext = () => { if (curUniversityList.length === 0 || curUniversityList[0] === 0) { @@ -38,81 +96,20 @@ const UniversityStep = ({ universityList, curUniversityList, setCurUniversityLis return ( <> -
- -
-

본 과정 완료 후, 지원자 현황을 확인할 수 있습니다.

-
-
- - -
-
- - -
-
- - -
-
+
+

본 과정 완료 후, 지원자 현황을 확인할 수 있습니다.

+
+ {choiceIndexes.map((index) => ( + + ))}
diff --git a/apps/web/src/app/university/score/ScoreScreen.tsx b/apps/web/src/app/university/score/ScoreScreen.tsx index 0c835884b..ddd7f847c 100644 --- a/apps/web/src/app/university/score/ScoreScreen.tsx +++ b/apps/web/src/app/university/score/ScoreScreen.tsx @@ -10,9 +10,12 @@ import { IconSolidConnectionSmallLogo } from "@/public/svgs/my"; import { formatLanguageTestScore, languageTestMapping, ScoreSubmitStatus } from "@/types/score"; import ScoreCard from "./ScoreCard"; +const SCORE_TAB_CHOICES = ["공인어학", "학점"] as const; +type ScoreTab = (typeof SCORE_TAB_CHOICES)[number]; + const ScoreScreen = () => { const router = useRouter(); - const [curTab, setCurTab] = useState<"공인어학" | "학점">("공인어학"); + const [curTab, setCurTab] = useState("공인어학"); const { data: gpaScoreList = [] } = useGetMyGpaScore(); const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore(); const isEmptyCurrentTab = curTab === "공인어학" ? languageTestScoreList.length === 0 : gpaScoreList.length === 0; @@ -30,7 +33,7 @@ const ScoreScreen = () => { return (
- + choices={SCORE_TAB_CHOICES} choice={curTab} setChoice={setCurTab} /> {isEmptyCurrentTab ? (
diff --git a/apps/web/src/components/ui/Tab/index.tsx b/apps/web/src/components/ui/Tab/index.tsx index 27baa9db8..07eb2342d 100644 --- a/apps/web/src/components/ui/Tab/index.tsx +++ b/apps/web/src/components/ui/Tab/index.tsx @@ -1,7 +1,7 @@ -type TabProps = { - choices: string[]; - choice: string; - setChoice: React.Dispatch>; +type TabProps = { + choices: readonly T[]; + choice: T; + setChoice: (choice: T) => void; color?: { activeBtn?: string; deactiveBtn?: string; @@ -10,7 +10,7 @@ type TabProps = { }; }; -const Tab = ({ choices, choice, setChoice, color }: TabProps) => { +const Tab = ({ choices, choice, setChoice, color }: TabProps) => { const defaultColor = { activeBtnFont: "text-black", deactiveBtnFont: "text-gray-200", diff --git a/apps/web/src/constants/university.ts b/apps/web/src/constants/university.ts index 7ef79805e..13f95ea49 100644 --- a/apps/web/src/constants/university.ts +++ b/apps/web/src/constants/university.ts @@ -35,17 +35,21 @@ export interface HomeUniversityInfo { name: HomeUniversity; slug: HomeUniversitySlug; shortName: string; + maxChoiceCount: number; logoUrl: string; description: string; color: string; } +export const DEFAULT_MAX_CHOICE_COUNT = 3; + export const HOME_UNIVERSITY_LIST: HomeUniversityInfo[] = [ { homeUniversityId: 1, name: HomeUniversity.INHA, slug: "inha", shortName: "인하대", + maxChoiceCount: 3, logoUrl: "/images/univs/inha.png", description: "인하대학교 교환학생 프로그램", color: "#004C98", @@ -55,6 +59,7 @@ export const HOME_UNIVERSITY_LIST: HomeUniversityInfo[] = [ name: HomeUniversity.KYUNGHEE, slug: "kyunghee", shortName: "경희대", + maxChoiceCount: 5, logoUrl: "/images/univs/kyunghee.png", description: "경희대학교 교환학생 프로그램", color: "#8C1515", @@ -71,6 +76,10 @@ export const getHomeUniversityBySlug = (slug: string): HomeUniversityInfo | unde return HOME_UNIVERSITY_LIST.find((uni) => uni.slug === slug); }; +export const getHomeUniversityById = (homeUniversityId: number | null | undefined): HomeUniversityInfo | undefined => { + return HOME_UNIVERSITY_LIST.find((uni) => uni.homeUniversityId === homeUniversityId); +}; + export const normalizeHomeUniversityName = (value: string | null | undefined): HomeUniversity | undefined => { if (!value) { return undefined; diff --git a/apps/web/src/lib/zustand/useAuthStore.ts b/apps/web/src/lib/zustand/useAuthStore.ts index bce263d47..b07a63375 100644 --- a/apps/web/src/lib/zustand/useAuthStore.ts +++ b/apps/web/src/lib/zustand/useAuthStore.ts @@ -1,27 +1,41 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { UserRole } from "@/types/mentor"; -import { isTokenExpired } from "@/utils/jwtUtils"; +import { isTokenExpired, tokenParse } from "@/utils/jwtUtils"; -const parseUserRoleFromToken = (token: string | null): UserRole | null => { - if (!token || isTokenExpired(token)) return null; +const parseUserRole = (role: string | undefined): UserRole | null => { + if (role === UserRole.MENTOR || role === UserRole.MENTEE || role === UserRole.ADMIN) { + return role; + } - try { - const payload = JSON.parse(atob(token.split(".")[1])) as { role?: string }; + return null; +}; - if (payload.role === UserRole.MENTOR || payload.role === UserRole.MENTEE || payload.role === UserRole.ADMIN) { - return payload.role; - } +const parseHomeUniversityId = (homeUniversity: string | undefined): number | null => { + const homeUniversityId = Number(homeUniversity); - return null; - } catch { - return null; - } + return Number.isInteger(homeUniversityId) && homeUniversityId > 0 ? homeUniversityId : null; }; type RefreshStatus = "idle" | "refreshing" | "success" | "failed"; type ClientRole = UserRole.MENTOR | UserRole.MENTEE; +const parseAuthToken = (token: string | null) => { + if (!token || isTokenExpired(token)) { + return { + serverRole: null, + homeUniversityId: null, + }; + } + + const payload = tokenParse(token); + + return { + serverRole: parseUserRole(payload?.role), + homeUniversityId: parseHomeUniversityId(payload?.home_university), + }; +}; + const resolveClientRole = (serverRole: UserRole | null, currentClientRole: ClientRole | null): ClientRole | null => { if (serverRole === UserRole.ADMIN) { return currentClientRole ?? UserRole.MENTOR; @@ -38,6 +52,7 @@ interface AuthState { accessToken: string | null; serverRole: UserRole | null; clientRole: ClientRole | null; + homeUniversityId: number | null; isAuthenticated: boolean; isLoading: boolean; isInitialized: boolean; @@ -57,6 +72,7 @@ const useAuthStore = create()( accessToken: null, serverRole: null, clientRole: null, + homeUniversityId: null, isAuthenticated: false, isLoading: false, isInitialized: false, @@ -65,12 +81,13 @@ const useAuthStore = create()( setAccessToken: (token) => { set((state) => { - const serverRole = parseUserRoleFromToken(token); + const { serverRole, homeUniversityId } = parseAuthToken(token); return { accessToken: token, serverRole, clientRole: resolveClientRole(serverRole, state.clientRole), + homeUniversityId, isAuthenticated: true, isLoading: false, isInitialized: true, @@ -85,6 +102,7 @@ const useAuthStore = create()( accessToken: null, serverRole: null, clientRole: null, + homeUniversityId: null, isAuthenticated: false, isLoading: false, isInitialized: true, @@ -133,15 +151,17 @@ const useAuthStore = create()( state.accessToken = null; state.serverRole = null; state.clientRole = null; + state.homeUniversityId = null; state.isAuthenticated = false; // 저장된 로그인 흔적이 있으면 ReissueProvider가 refresh를 마칠 때까지 인증 분기를 보류합니다. state.isInitialized = !hadStoredAuth; state.isLoading = hadStoredAuth; state.refreshStatus = hadStoredAuth ? "refreshing" : "idle"; } else { - const serverRole = parseUserRoleFromToken(state.accessToken); + const { serverRole, homeUniversityId } = parseAuthToken(state.accessToken); state.serverRole = serverRole; state.clientRole = resolveClientRole(serverRole, state.clientRole); + state.homeUniversityId = homeUniversityId; state.isAuthenticated = true; state.isInitialized = true; state.isLoading = false; diff --git a/apps/web/src/types/application.ts b/apps/web/src/types/application.ts index 6dcdc3db3..5503e75a4 100644 --- a/apps/web/src/types/application.ts +++ b/apps/web/src/types/application.ts @@ -45,15 +45,11 @@ export interface ApplicationScoreRequest { } export interface ApplicationUniversityRequest { - firstChoiceUniversityId: number; - secondChoiceUniversityId: number; - thirdChoiceUniversityId: number; + choices: number[]; } export interface ApplicationListResponse { - firstChoice: ScoreSheet[]; - secondChoice: ScoreSheet[]; - thirdChoice: ScoreSheet[]; + choices: ScoreSheet[][]; } export interface ApplicationStatusResponse { @@ -65,9 +61,7 @@ export interface SubmitApplicationRequest { gpaScoreId: number; languageTestScoreId: number; universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; + choices: number[]; }; } diff --git a/apps/web/src/utils/jwtUtils.ts b/apps/web/src/utils/jwtUtils.ts index 5d39c2a19..9c0a35eec 100644 --- a/apps/web/src/utils/jwtUtils.ts +++ b/apps/web/src/utils/jwtUtils.ts @@ -1,6 +1,7 @@ interface JwtPayload { sub: number | string; - role: string; + role?: string; + home_university?: string; iat: number; exp: number; } diff --git a/packages/api-schema/src/apiDefinitionRegistry.ts b/packages/api-schema/src/apiDefinitionRegistry.ts index b2b5d2ff0..fb5a15266 100644 --- a/packages/api-schema/src/apiDefinitionRegistry.ts +++ b/packages/api-schema/src/apiDefinitionRegistry.ts @@ -376,9 +376,7 @@ export const brunoApiDefinitionRegistry = [ "gpaScoreId": 1, "languageTestScoreId": 1, "universityChoiceRequest": { - "firstChoiceUniversityId": 1, - "secondChoiceUniversityId": 2, - "thirdChoiceUniversityId": 3 + "choices": [1, 2, 3] } }, hasBody: true,