From dbefd758713aa5bcd9d6619fa5faf2e2cccd4e4d Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 01:41:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20jwt=EC=97=90=EC=84=9C=20=ED=99=88?= =?UTF-8?q?=20=EB=8C=80=ED=95=99=20ID=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/lib/zustand/useAuthStore.ts | 48 +++++++++++++++++------- apps/web/src/utils/jwtUtils.ts | 3 +- 2 files changed, 36 insertions(+), 15 deletions(-) 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/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; } From 29878f8006c2b16563004608c3b08c7d28883d00 Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 01:47:04 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=99=88=20=EB=8C=80=ED=95=99?= =?UTF-8?q?=EB=B3=84=20=EC=A7=80=EC=9B=90=20=EC=A7=80=EB=A7=9D=20=EC=88=98?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/applications/api.ts | 4 +- .../application/ScorePageContent.tsx | 68 +++++------ .../application/apply/ApplyPageContent.tsx | 26 ++++- .../application/apply/UniversityStep.tsx | 107 ++++++------------ apps/web/src/constants/university.ts | 9 ++ apps/web/src/types/application.ts | 12 +- 6 files changed, 98 insertions(+), 128 deletions(-) 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..12360a9d5 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,35 @@ 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[]; + choices: 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 [preference, setPreference] = useState("1순위"); const [regionFilter, setRegionFilter] = useState(""); const [searchValue, setSearchValue] = useState(""); const [showNeedApply, _setShowNeedApply] = useState(false); const initialData: ScoreData = { - firstChoice: [], - secondChoice: [], - thirdChoice: [], + choices: Array.from({ length: maxChoiceCount }, () => []), }; const { data: scoreResponseData = initialData, isError, isLoading } = useGetApplicationsList(); + const preferenceChoices = useMemo( + () => + Array.from( + { length: Math.max(scoreResponseData.choices.length, maxChoiceCount) }, + (_, index) => `${index + 1}순위`, + ), + [maxChoiceCount, scoreResponseData.choices.length], + ); const filteredAndSortedData = useMemo(() => { // ✨ 1. 대학 이름(koreanName)을 기준으로 중복을 제거하는 헬퍼 함수 @@ -49,17 +54,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 = scoreResponseData.choices.map((choice) => + uniqueByKoreanName(choice).sort((a, b) => b.applicants.length - a.applicants.length), + ); // 4. 기존 필터링 로직을 적용합니다. const applyFilters = (data: ScoreSheetType[]) => { @@ -73,11 +71,7 @@ const ScorePageContent = () => { return result; }; - return { - firstChoice: applyFilters(sortedData.firstChoice), - secondChoice: applyFilters(sortedData.secondChoice), - thirdChoice: applyFilters(sortedData.thirdChoice), - }; + return sortedData.map(applyFilters); }, [scoreResponseData, regionFilter, searchValue]); // (이하 코드는 동일) @@ -102,19 +96,8 @@ 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 scoreSheets = getScoreSheet(); + const selectedChoiceIndex = Math.max(Number(preference.replace("순위", "")) - 1, 0); + const scoreSheets = filteredAndSortedData[selectedChoiceIndex] ?? []; useEffect(() => { if (isLoading) return; @@ -123,6 +106,11 @@ const ScorePageContent = () => { } }, [isError, isLoading, router]); + useEffect(() => { + if (preferenceChoices.includes(preference)) return; + setPreference(preferenceChoices[0] ?? "1순위"); + }, [preference, preferenceChoices]); + const hotKeyWords = ["RMIT", "오스트라바", "칼스루에", "그라츠", "추오", "프라하", "보라스", "빈", "메모리얼"]; return ( @@ -141,7 +129,7 @@ const ScorePageContent = () => { ) : ( <>
- +
{ 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 +65,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,9 +78,7 @@ const ApplyPageContent = () => { gpaScoreId: curGpaScore, languageTestScoreId: curLanguageTestScore, universityChoiceRequest: { - firstChoiceUniversityId: curUniversityList[0] || null, - secondChoiceUniversityId: curUniversityList[1] || null, - thirdChoiceUniversityId: curUniversityList[2] || null, + choices: selectedUniversityIds, }, }); }; @@ -102,6 +115,7 @@ const ApplyPageContent = () => { universityList={universityList} curUniversityList={curUniversityList} setCurUniversityList={setCurUniversityList} + maxChoiceCount={maxChoiceCount} onNext={goNextStep} /> )} diff --git a/apps/web/src/app/university/application/apply/UniversityStep.tsx b/apps/web/src/app/university/application/apply/UniversityStep.tsx index bbbfd7c25..f9829b77c 100644 --- a/apps/web/src/app/university/application/apply/UniversityStep.tsx +++ b/apps/web/src/app/university/application/apply/UniversityStep.tsx @@ -13,20 +13,28 @@ type UniversityStepProps = { universityList: ListUniversity[]; curUniversityList: number[]; setCurUniversityList: (idList: number[]) => void; + maxChoiceCount: number; onNext: () => void; }; -const UniversityStep = ({ universityList, curUniversityList, setCurUniversityList, onNext }: UniversityStepProps) => { +const UniversityStep = ({ + universityList, + curUniversityList, + setCurUniversityList, + maxChoiceCount, + onNext, +}: UniversityStepProps) => { const [isModalOpen, setIsModalOpen] = useState(false); + const choiceIndexes = Array.from({ length: maxChoiceCount }, (_, index) => index); const handleSelect = (index: number, value: number) => { - const newList = [...curUniversityList]; + const newList = curUniversityList.slice(0, maxChoiceCount); newList[index] = value; 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 handleNext = () => { if (curUniversityList.length === 0 || curUniversityList[0] === 0) { @@ -41,77 +49,36 @@ const UniversityStep = ({ universityList, curUniversityList, setCurUniversityLis

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

-
- - -
-
- - -
-
- - -
+ {choiceIndexes.map((index) => ( +
+ + +
+ ))}
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/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[]; }; } From 8411b206a5e8d8e0e2b3a0620f3eaafb9b134a3c Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 01:50:27 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EC=A7=80=EC=9B=90=20=EC=88=9C?= =?UTF-8?q?=EC=9C=84=20=ED=83=AD=20=EC=83=81=ED=83=9C=20=EB=8B=A8=EC=88=9C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ScorePageContent.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/university/application/ScorePageContent.tsx b/apps/web/src/app/university/application/ScorePageContent.tsx index 12360a9d5..092f7cf4f 100644 --- a/apps/web/src/app/university/application/ScorePageContent.tsx +++ b/apps/web/src/app/university/application/ScorePageContent.tsx @@ -26,7 +26,7 @@ const ScorePageContent = () => { const maxChoiceCount = getHomeUniversityById(homeUniversityId)?.maxChoiceCount ?? DEFAULT_MAX_CHOICE_COUNT; const [searchActive, setSearchActive] = useState(false); - const [preference, setPreference] = useState("1순위"); + const [preferenceIndex, setPreferenceIndex] = useState(0); const [regionFilter, setRegionFilter] = useState(""); const [searchValue, setSearchValue] = useState(""); const [showNeedApply, _setShowNeedApply] = useState(false); @@ -96,8 +96,13 @@ const ScorePageContent = () => { setSearchActive(true); }; - const selectedChoiceIndex = Math.max(Number(preference.replace("순위", "")) - 1, 0); - const scoreSheets = filteredAndSortedData[selectedChoiceIndex] ?? []; + const handlePreferenceChange = (nextPreference: string) => { + const nextIndex = preferenceChoices.indexOf(nextPreference); + setPreferenceIndex(nextIndex >= 0 ? nextIndex : 0); + }; + + const selectedPreference = preferenceChoices[preferenceIndex] ?? preferenceChoices[0] ?? "1순위"; + const scoreSheets = filteredAndSortedData[preferenceIndex] ?? []; useEffect(() => { if (isLoading) return; @@ -106,11 +111,6 @@ const ScorePageContent = () => { } }, [isError, isLoading, router]); - useEffect(() => { - if (preferenceChoices.includes(preference)) return; - setPreference(preferenceChoices[0] ?? "1순위"); - }, [preference, preferenceChoices]); - const hotKeyWords = ["RMIT", "오스트라바", "칼스루에", "그라츠", "추오", "프라하", "보라스", "빈", "메모리얼"]; return ( @@ -129,7 +129,7 @@ const ScorePageContent = () => { ) : ( <>
- +
Date: Wed, 24 Jun 2026 02:23:43 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=EC=A7=80=EC=9B=90=ED=95=98=EA=B8=B0?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApplicationBottomActionBar.tsx | 6 +- .../application/apply/ApplyPageContent.tsx | 9 +- .../application/apply/ConfirmStep.tsx | 63 ++++++++----- .../application/apply/UniversityStep.tsx | 88 +++++++++++-------- 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/apps/web/src/app/university/application/_components/ApplicationBottomActionBar.tsx b/apps/web/src/app/university/application/_components/ApplicationBottomActionBar.tsx index c1ca01114..7b97e940e 100644 --- a/apps/web/src/app/university/application/_components/ApplicationBottomActionBar.tsx +++ b/apps/web/src/app/university/application/_components/ApplicationBottomActionBar.tsx @@ -7,10 +7,8 @@ type ApplicationBottomActionBarProps = { const ApplicationBottomActionBar = ({ label, onClick }: ApplicationBottomActionBarProps) => { 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 041c345b5..80f29ff19 100644 --- a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx +++ b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx @@ -18,6 +18,8 @@ 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); @@ -84,11 +86,16 @@ const ApplyPageContent = () => { }; 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 ? ( 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 f9829b77c..4df5ea697 100644 --- a/apps/web/src/app/university/application/apply/UniversityStep.tsx +++ b/apps/web/src/app/university/application/apply/UniversityStep.tsx @@ -7,7 +7,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import type { ListUniversity } from "@/types/university"; import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar"; -import ApplicationSectionTitle from "../_components/ApplicationSectionTitle"; type UniversityStepProps = { universityList: ListUniversity[]; @@ -17,6 +16,46 @@ type UniversityStepProps = { onNext: () => void; }; +type UniversityChoiceSelectProps = { + index: number; + universityList: ListUniversity[]; + selectedUniversityId?: number; + onSelect: (index: number, universityId: number) => void; + isDisabled: (universityId: number, currentIndex: number) => boolean; +}; + +const UniversityChoiceSelect = ({ + index, + universityList, + selectedUniversityId, + onSelect, + isDisabled, +}: UniversityChoiceSelectProps) => { + return ( +
+ + +
+ ); +}; + const UniversityStep = ({ universityList, curUniversityList, @@ -46,40 +85,19 @@ const UniversityStep = ({ return ( <> -
- -
-

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

-
- {choiceIndexes.map((index) => ( -
- - -
- ))} -
+
+

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

+
+ {choiceIndexes.map((index) => ( + + ))}
From bcc96ac63240f55b94225b0d98daf55cfa4e42a0 Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 02:26:02 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EC=A7=80=EC=9B=90=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=84=A0=ED=83=9D=20=EC=83=81=ED=83=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ScorePageContent.tsx | 32 +++++++++---------- .../application/apply/UniversityStep.tsx | 4 +-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/apps/web/src/app/university/application/ScorePageContent.tsx b/apps/web/src/app/university/application/ScorePageContent.tsx index 092f7cf4f..b44bd1715 100644 --- a/apps/web/src/app/university/application/ScorePageContent.tsx +++ b/apps/web/src/app/university/application/ScorePageContent.tsx @@ -15,10 +15,6 @@ import ScoreSearchBar from "./ScoreSearchBar"; import ScoreSearchField from "./ScoreSearchField"; import ScoreSheet from "./ScoreSheet"; -interface ScoreData { - choices: ScoreSheetType[][]; -} - const ScorePageContent = () => { const router = useRouter(); const searchRef = useRef(null!); @@ -31,18 +27,15 @@ const ScorePageContent = () => { const [searchValue, setSearchValue] = useState(""); const [showNeedApply, _setShowNeedApply] = useState(false); - const initialData: ScoreData = { - choices: Array.from({ length: maxChoiceCount }, () => []), - }; - - 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(scoreResponseData.choices.length, maxChoiceCount) }, - (_, index) => `${index + 1}순위`, - ), - [maxChoiceCount, scoreResponseData.choices.length], + () => Array.from({ length: Math.max(scoreChoices.length, maxChoiceCount) }, (_, index) => `${index + 1}순위`), + [maxChoiceCount, scoreChoices.length], ); const filteredAndSortedData = useMemo(() => { @@ -55,7 +48,7 @@ const ScorePageContent = () => { }; // ✨ 2. API 응답 데이터를 받자마자 중복부터 제거하고 정렬합니다. - const sortedData = scoreResponseData.choices.map((choice) => + const sortedData = scoreChoices.map((choice) => uniqueByKoreanName(choice).sort((a, b) => b.applicants.length - a.applicants.length), ); @@ -72,7 +65,7 @@ const ScorePageContent = () => { }; return sortedData.map(applyFilters); - }, [scoreResponseData, regionFilter, searchValue]); + }, [scoreChoices, regionFilter, searchValue]); // (이하 코드는 동일) const handleSearch = (event: FormEvent) => { @@ -104,6 +97,11 @@ const ScorePageContent = () => { 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; if (isError) { diff --git a/apps/web/src/app/university/application/apply/UniversityStep.tsx b/apps/web/src/app/university/application/apply/UniversityStep.tsx index 4df5ea697..6625289aa 100644 --- a/apps/web/src/app/university/application/apply/UniversityStep.tsx +++ b/apps/web/src/app/university/application/apply/UniversityStep.tsx @@ -1,6 +1,6 @@ "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"; @@ -64,7 +64,7 @@ const UniversityStep = ({ onNext, }: UniversityStepProps) => { const [isModalOpen, setIsModalOpen] = useState(false); - const choiceIndexes = Array.from({ length: maxChoiceCount }, (_, index) => index); + const choiceIndexes = useMemo(() => Array.from({ length: maxChoiceCount }, (_, index) => index), [maxChoiceCount]); const handleSelect = (index: number, value: number) => { const newList = curUniversityList.slice(0, maxChoiceCount); From f6ad8e1df7bd31570b3cd58f0371d909b93395bd Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 02:38:25 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EC=A7=80=EC=9B=90=20=EC=A7=80?= =?UTF-8?q?=EB=A7=9D=20=EC=88=9C=EC=84=9C=20=EC=84=A0=ED=83=9D=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/apply/UniversityStep.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/university/application/apply/UniversityStep.tsx b/apps/web/src/app/university/application/apply/UniversityStep.tsx index 6625289aa..d234563e8 100644 --- a/apps/web/src/app/university/application/apply/UniversityStep.tsx +++ b/apps/web/src/app/university/application/apply/UniversityStep.tsx @@ -20,6 +20,7 @@ type UniversityChoiceSelectProps = { index: number; universityList: ListUniversity[]; selectedUniversityId?: number; + isSelectable: boolean; onSelect: (index: number, universityId: number) => void; isDisabled: (universityId: number, currentIndex: number) => boolean; }; @@ -28,13 +29,18 @@ const UniversityChoiceSelect = ({ index, universityList, selectedUniversityId, + isSelectable, onSelect, isDisabled, }: UniversityChoiceSelectProps) => { return (
- onSelect(index, Number(value))} + > @@ -69,12 +75,17 @@ const UniversityStep = ({ const handleSelect = (index: number, value: number) => { 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 > 0 && pickedId === universityId); + const isSelectable = (index: number) => index === 0 || Number(curUniversityList[index - 1]) > 0; + const handleNext = () => { if (curUniversityList.length === 0 || curUniversityList[0] === 0) { setIsModalOpen(true); @@ -94,6 +105,7 @@ const UniversityStep = ({ index={index} universityList={universityList} selectedUniversityId={curUniversityList[index]} + isSelectable={isSelectable(index)} onSelect={handleSelect} isDisabled={isDisabled} /> From 8ca72057614a6e0b7a2babe7d4f0214078ed6970 Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Wed, 24 Jun 2026 02:55:17 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=EC=A7=80=EC=9B=90=20API=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=EC=A0=95=ED=95=A9=EC=84=B1=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/university-web/src/apis/applications/api.ts | 4 +--- apps/university-web/src/components/ui/Tab/index.tsx | 10 +++++----- apps/university-web/src/types/application.ts | 12 +++--------- apps/web/src/app/university/score/ScoreScreen.tsx | 7 +++++-- apps/web/src/components/ui/Tab/index.tsx | 10 +++++----- packages/api-schema/src/apiDefinitionRegistry.ts | 4 +--- 6 files changed, 20 insertions(+), 27 deletions(-) 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/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/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,