From 459cd2c0cedb2ebf6cae8435eae89665a8115bb3 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sun, 21 Jun 2026 16:10:08 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EC=9D=B8=EA=B8=B0=20=ED=8C=8C?= =?UTF-8?q?=EA=B2=AC=ED=95=99=EA=B5=90=20=EC=83=81=EC=84=B8=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/apis/universities/api.ts | 1 + apps/web/src/apis/universities/api.ts | 1 + .../_ui/PopularUniversityCard.tsx | 9 +++-- apps/web/src/app/(home)/page.tsx | 34 +++++++++++++++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/apps/university-web/src/apis/universities/api.ts b/apps/university-web/src/apis/universities/api.ts index 6b0055a3..88b1de5d 100644 --- a/apps/university-web/src/apis/universities/api.ts +++ b/apps/university-web/src/apis/universities/api.ts @@ -5,6 +5,7 @@ export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; koreanName: string; + homeUniversityName?: HomeUniversityName; region: string; country: string; logoImageUrl: string; diff --git a/apps/web/src/apis/universities/api.ts b/apps/web/src/apis/universities/api.ts index 6b0055a3..88b1de5d 100644 --- a/apps/web/src/apis/universities/api.ts +++ b/apps/web/src/apis/universities/api.ts @@ -5,6 +5,7 @@ export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; koreanName: string; + homeUniversityName?: HomeUniversityName; region: string; country: string; logoImageUrl: string; diff --git a/apps/web/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx b/apps/web/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx index 1c725e09..ab055b4c 100644 --- a/apps/web/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx +++ b/apps/web/src/app/(home)/_ui/PopularUniversitySection/_ui/PopularUniversityCard.tsx @@ -20,9 +20,12 @@ const PopularUniversityCard = ({ quality = 60, // 기본값을 60으로 낮춤 }: PopularUniversityCardProps) => { const homeUniversitySlug = getHomeUniversitySlugByName(university.homeUniversityName); - const universityDetailHref = homeUniversitySlug - ? `/university/${homeUniversitySlug}/${university.id}` - : "/university"; + + if (!homeUniversitySlug) { + return null; + } + + const universityDetailHref = `/university/${homeUniversitySlug}/${university.id}`; return ( diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 0b4c5bb0..5a400505 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { getHomeNewsList } from "@/apis/news/server/getNewsList"; import { getCategorizedUniversities, getRecommendedUniversity } from "@/apis/universities/server"; +import { getHomeUniversitySlugByName } from "@/constants/university"; import { type ListUniversity, RegionEnumExtend } from "@/types/university"; import { createUrl } from "@/utils/seo"; import FindLastYearScoreBar from "./_ui/FindLastYearScoreBar"; @@ -61,13 +62,42 @@ const resolveRecommendedUniversitiesHomeUniversityName = ( const homeUniversityNameById = new Map( allUniversities.map((university) => [university.id, university.homeUniversityName]), ); + const homeUniversityNamesByTermAndName = new Map>(); + + allUniversities.forEach((university) => { + const key = `${university.term}:${university.koreanName}`; + const names = homeUniversityNamesByTermAndName.get(key) ?? new Set(); + + names.add(university.homeUniversityName); + homeUniversityNamesByTermAndName.set(key, names); + }); return recommendedUniversities.map((university) => ({ ...university, - homeUniversityName: university.homeUniversityName ?? homeUniversityNameById.get(university.id), + homeUniversityName: + university.homeUniversityName ?? + homeUniversityNameById.get(university.id) ?? + getUniqueHomeUniversityName(homeUniversityNamesByTermAndName, university), })); }; +const getUniqueHomeUniversityName = ( + homeUniversityNamesByTermAndName: Map>, + university: ListUniversity, +) => { + const names = homeUniversityNamesByTermAndName.get(`${university.term}:${university.koreanName}`); + + if (!names || names.size !== 1) { + return undefined; + } + + return names.values().next().value; +}; + +const hasUniversityDetailRoute = (university: ListUniversity) => { + return getHomeUniversitySlugByName(university.homeUniversityName) !== undefined; +}; + const HomePage = async () => { const newsList = await getHomeNewsList(); const { data } = await getRecommendedUniversity(); @@ -78,7 +108,7 @@ const HomePage = async () => { const resolvedRecommendedUniversities = resolveRecommendedUniversitiesHomeUniversityName( recommendedUniversities, allUniversities, - ); + ).filter(hasUniversityDetailRoute); return ( <> From d4ba60ad2c2d101282ff1197c3a4b98e8f5845a1 Mon Sep 17 00:00:00 2001 From: manNomi Date: Mon, 22 Jun 2026 13:48:37 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=8C=80=ED=95=99=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=A1=B0=EA=B1=B4=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/apis/universities/api.ts | 26 +++++++++-- .../src/apis/universities/getSearchFilter.ts | 8 ++++ .../server/getSearchUniversitiesByFilter.ts | 30 ++++++++++++- .../server/getSearchUniversitiesByText.ts | 43 ++++++++++++++++--- .../university/[homeUniversity]/[id]/page.tsx | 14 +++--- .../app/university/[homeUniversity]/page.tsx | 4 +- .../src/constants/university.ts | 7 +++ apps/web/src/apis/universities/api.ts | 26 +++++++++-- .../src/apis/universities/getSearchFilter.ts | 8 ++++ .../server/getSearchUniversitiesByFilter.ts | 30 ++++++++++++- .../server/getSearchUniversitiesByText.ts | 43 ++++++++++++++++--- apps/web/src/constants/university.ts | 7 +++ 12 files changed, 214 insertions(+), 32 deletions(-) diff --git a/apps/university-web/src/apis/universities/api.ts b/apps/university-web/src/apis/universities/api.ts index 88b1de5d..561edc66 100644 --- a/apps/university-web/src/apis/universities/api.ts +++ b/apps/university-web/src/apis/universities/api.ts @@ -1,6 +1,13 @@ +import { DEFAULT_UNIVERSITY_TERM_ID } from "@/constants/university"; import type { HomeUniversityName } from "@/types/university"; import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; +const getClientUniversityTermId = () => { + const termId = Number(process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; @@ -175,16 +182,29 @@ export const universitiesApi = { return res.data; }, - getSearchText: async (params?: { value?: string }): Promise => { + getSearchText: async (params?: { + value?: string; + termId?: number; + homeUniversityId?: number; + }): Promise => { const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { - params: { value: params?.value ?? "" }, + params: { + value: params?.value ?? "", + termId: params?.termId ?? getClientUniversityTermId(), + homeUniversityId: params?.homeUniversityId, + }, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const requestParams = { + ...params?.params, + termId: params?.params?.termId ?? getClientUniversityTermId(), + }; + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { - params: params?.params, + params: requestParams, }); return res.data; }, diff --git a/apps/university-web/src/apis/universities/getSearchFilter.ts b/apps/university-web/src/apis/universities/getSearchFilter.ts index 6094fb57..55c33fc7 100644 --- a/apps/university-web/src/apis/universities/getSearchFilter.ts +++ b/apps/university-web/src/apis/universities/getSearchFilter.ts @@ -10,6 +10,8 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; } // API 응답에 homeUniversityName이 포함된 타입 @@ -38,6 +40,12 @@ const useGetUniversitySearchByFilter = ( if (filters.countryCode && filters.countryCode.length > 0) { params.countryCode = filters.countryCode; } + if (filters.termId !== undefined) { + params.termId = filters.termId; + } + if (filters.homeUniversityId !== undefined) { + params.homeUniversityId = filters.homeUniversityId; + } return params; }; diff --git a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts index a6b5dee7..190279ce 100644 --- a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts +++ b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts @@ -1,4 +1,5 @@ import { URLSearchParams } from "node:url"; +import { DEFAULT_UNIVERSITY_TERM_ID } from "@/constants/university"; import type { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; import { assertUniversitySsgResponse } from "./assertUniversitySsgResponse"; @@ -14,8 +15,16 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; } +const getUniversityTermId = () => { + const termId = Number(process.env.UNIVERSITY_TERM_ID ?? process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + export const getSearchUniversitiesByFilter = async ( filters: UniversitySearchFilterParams, ): Promise => { @@ -31,6 +40,12 @@ export const getSearchUniversitiesByFilter = async ( if (filters.countryCode) { filters.countryCode.forEach((code) => params.append("countryCode", code)); } + if (filters.termId !== undefined) { + params.append("termId", String(filters.termId)); + } + if (filters.homeUniversityId !== undefined) { + params.append("homeUniversityId", String(filters.homeUniversityId)); + } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. if (params.size === 0) { @@ -43,8 +58,19 @@ export const getSearchUniversitiesByFilter = async ( .univApplyInfoPreviews; }; -export const getSearchUniversitiesAllRegions = async (): Promise => { - const endpoint = `/univ-apply-infos/search/text?value=`; +export const getSearchUniversitiesAllRegions = async ( + params: Pick = {}, +): Promise => { + const searchParams = new URLSearchParams({ + value: "", + termId: String(params.termId ?? getUniversityTermId()), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(params.homeUniversityId)); + } + + const endpoint = `/univ-apply-infos/search/text?${searchParams.toString()}`; const response = await serverFetch(endpoint); return assertUniversitySsgResponse(response, "search all universities").univApplyInfoPreviews; }; diff --git a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts index 45dfa54a..c2f4bc44 100644 --- a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts +++ b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts @@ -1,3 +1,5 @@ +import { URLSearchParams } from "node:url"; +import { DEFAULT_UNIVERSITY_TERM_ID } from "@/constants/university"; import { type AllRegionsUniversityList, type ListUniversity, RegionEnumExtend } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; import { assertUniversitySsgResponse } from "./assertUniversitySsgResponse"; @@ -7,22 +9,51 @@ interface UniversitySearchResponse { univApplyInfoPreviews: ListUniversity[]; } -export const getUniversitiesByText = async (value: string): Promise => { +export interface UniversitySearchTextParams { + termId?: number; + homeUniversityId?: number; +} + +const getUniversityTermId = () => { + const termId = Number(process.env.UNIVERSITY_TERM_ID ?? process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + +const createSearchTextEndpoint = (value: string, params: UniversitySearchTextParams = {}) => { + const searchParams = new URLSearchParams({ + value, + termId: String(params.termId ?? getUniversityTermId()), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(params.homeUniversityId)); + } + + return `/univ-apply-infos/search/text?${searchParams.toString()}`; +}; + +export const getUniversitiesByText = async ( + value: string, + params?: UniversitySearchTextParams, +): Promise => { if (value === null || value === undefined) { return []; } - const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`; + const endpoint = createSearchTextEndpoint(value, params); const response = await serverFetch(endpoint); return assertUniversitySsgResponse(response, `search universities by text "${value}"`).univApplyInfoPreviews; }; -export const getAllUniversities = async (): Promise => { - return getUniversitiesByText(""); +export const getAllUniversities = async (params?: UniversitySearchTextParams): Promise => { + return getUniversitiesByText("", params); }; -export const getCategorizedUniversities = async (): Promise => { +export const getCategorizedUniversities = async ( + params?: UniversitySearchTextParams, +): Promise => { // 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다. - const allUniversities = await getAllUniversities(); + const allUniversities = await getAllUniversities(params); const categorizedList: AllRegionsUniversityList = { [RegionEnumExtend.ALL]: allUniversities, diff --git a/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx b/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx index 7668f157..2a4f59d9 100644 --- a/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx +++ b/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation"; import { getAllUniversities, getUniversityDetailWithStatus } from "@/apis/universities/server"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import { getHomeUniversityBySlug, HOME_UNIVERSITY_SLUGS, isMatchedHomeUniversityName } from "@/constants/university"; +import { getHomeUniversityBySlug, HOME_UNIVERSITY_SLUGS } from "@/constants/university"; import type { HomeUniversitySlug } from "@/types/university"; import { normalizeImageUrlToUploadCdn } from "@/utils/cdnUrl"; import { createUrl, NO_INDEX_ROBOTS } from "@/utils/seo"; @@ -17,21 +17,17 @@ export const dynamicParams = false; // 모든 homeUniversity + id 조합에 대해 정적 경로 생성 export async function generateStaticParams() { - const universities = await getAllUniversities(); - const params: { homeUniversity: string; id: string }[] = []; - // 각 대학에 대해 모든 homeUniversity 슬러그와 조합 for (const slug of HOME_UNIVERSITY_SLUGS) { const homeUniversityInfo = getHomeUniversityBySlug(slug); if (!homeUniversityInfo) continue; - // 해당 홈대학에 속하는 대학들만 필터링 - const filteredUniversities = universities.filter((uni) => - isMatchedHomeUniversityName(uni.homeUniversityName, homeUniversityInfo.name), - ); + const universities = await getAllUniversities({ + homeUniversityId: homeUniversityInfo.homeUniversityId, + }); - for (const university of filteredUniversities) { + for (const university of universities) { params.push({ homeUniversity: slug, id: String(university.id), diff --git a/apps/university-web/src/app/university/[homeUniversity]/page.tsx b/apps/university-web/src/app/university/[homeUniversity]/page.tsx index 324f7ed3..926c4331 100644 --- a/apps/university-web/src/app/university/[homeUniversity]/page.tsx +++ b/apps/university-web/src/app/university/[homeUniversity]/page.tsx @@ -53,7 +53,9 @@ const UniversityListPage = async ({ params }: PageProps) => { notFound(); } - const allUniversities = await getSearchUniversitiesAllRegions(); + const allUniversities = await getSearchUniversitiesAllRegions({ + homeUniversityId: universityInfo.homeUniversityId, + }); // homeUniversityName으로 프론트에서 필터링 const filteredUniversities = allUniversities.filter((university) => diff --git a/apps/university-web/src/constants/university.ts b/apps/university-web/src/constants/university.ts index 5a89afe3..f6c1746a 100644 --- a/apps/university-web/src/constants/university.ts +++ b/apps/university-web/src/constants/university.ts @@ -33,6 +33,7 @@ export const HOME_UNIVERSITY_TO_SLUG_MAP: Record { + const termId = Number(process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; @@ -175,16 +182,29 @@ export const universitiesApi = { return res.data; }, - getSearchText: async (params?: { value?: string }): Promise => { + getSearchText: async (params?: { + value?: string; + termId?: number; + homeUniversityId?: number; + }): Promise => { const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { - params: { value: params?.value ?? "" }, + params: { + value: params?.value ?? "", + termId: params?.termId ?? getClientUniversityTermId(), + homeUniversityId: params?.homeUniversityId, + }, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const requestParams = { + ...params?.params, + termId: params?.params?.termId ?? getClientUniversityTermId(), + }; + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { - params: params?.params, + params: requestParams, }); return res.data; }, diff --git a/apps/web/src/apis/universities/getSearchFilter.ts b/apps/web/src/apis/universities/getSearchFilter.ts index 6094fb57..55c33fc7 100644 --- a/apps/web/src/apis/universities/getSearchFilter.ts +++ b/apps/web/src/apis/universities/getSearchFilter.ts @@ -10,6 +10,8 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; } // API 응답에 homeUniversityName이 포함된 타입 @@ -38,6 +40,12 @@ const useGetUniversitySearchByFilter = ( if (filters.countryCode && filters.countryCode.length > 0) { params.countryCode = filters.countryCode; } + if (filters.termId !== undefined) { + params.termId = filters.termId; + } + if (filters.homeUniversityId !== undefined) { + params.homeUniversityId = filters.homeUniversityId; + } return params; }; diff --git a/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts b/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts index 12396f36..829d6c7f 100644 --- a/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts +++ b/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts @@ -1,4 +1,5 @@ import { URLSearchParams } from "node:url"; +import { DEFAULT_UNIVERSITY_TERM_ID } from "@/constants/university"; import type { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; @@ -13,8 +14,16 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; } +const getUniversityTermId = () => { + const termId = Number(process.env.UNIVERSITY_TERM_ID ?? process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + export const getSearchUniversitiesByFilter = async ( filters: UniversitySearchFilterParams, ): Promise => { @@ -30,6 +39,12 @@ export const getSearchUniversitiesByFilter = async ( if (filters.countryCode) { filters.countryCode.forEach((code) => params.append("countryCode", code)); } + if (filters.termId !== undefined) { + params.append("termId", String(filters.termId)); + } + if (filters.homeUniversityId !== undefined) { + params.append("homeUniversityId", String(filters.homeUniversityId)); + } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. if (params.size === 0) { @@ -46,8 +61,19 @@ export const getSearchUniversitiesByFilter = async ( return response.data.univApplyInfoPreviews; }; -export const getSearchUniversitiesAllRegions = async (): Promise => { - const endpoint = `/univ-apply-infos/search/text?value=`; +export const getSearchUniversitiesAllRegions = async ( + params: Pick = {}, +): Promise => { + const searchParams = new URLSearchParams({ + value: "", + termId: String(params.termId ?? getUniversityTermId()), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(params.homeUniversityId)); + } + + const endpoint = `/univ-apply-infos/search/text?${searchParams.toString()}`; const response = await serverFetch(endpoint); if (!response.ok) { diff --git a/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts b/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts index d2267eaf..0474773f 100644 --- a/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts +++ b/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts @@ -1,3 +1,5 @@ +import { URLSearchParams } from "node:url"; +import { DEFAULT_UNIVERSITY_TERM_ID } from "@/constants/university"; import { type AllRegionsUniversityList, type ListUniversity, RegionEnumExtend } from "@/types/university"; import serverFetch from "@/utils/serverFetchUtil"; @@ -6,11 +8,38 @@ interface UniversitySearchResponse { univApplyInfoPreviews: ListUniversity[]; } -export const getUniversitiesByText = async (value: string): Promise => { +export interface UniversitySearchTextParams { + termId?: number; + homeUniversityId?: number; +} + +const getUniversityTermId = () => { + const termId = Number(process.env.UNIVERSITY_TERM_ID ?? process.env.NEXT_PUBLIC_UNIVERSITY_TERM_ID); + + return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; +}; + +const createSearchTextEndpoint = (value: string, params: UniversitySearchTextParams = {}) => { + const searchParams = new URLSearchParams({ + value, + termId: String(params.termId ?? getUniversityTermId()), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(params.homeUniversityId)); + } + + return `/univ-apply-infos/search/text?${searchParams.toString()}`; +}; + +export const getUniversitiesByText = async ( + value: string, + params?: UniversitySearchTextParams, +): Promise => { if (value === null || value === undefined) { return []; } - const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`; + const endpoint = createSearchTextEndpoint(value, params); const response = await serverFetch(endpoint); if (!response.ok) { @@ -20,13 +49,15 @@ export const getUniversitiesByText = async (value: string): Promise => { - return getUniversitiesByText(""); +export const getAllUniversities = async (params?: UniversitySearchTextParams): Promise => { + return getUniversitiesByText("", params); }; -export const getCategorizedUniversities = async (): Promise => { +export const getCategorizedUniversities = async ( + params?: UniversitySearchTextParams, +): Promise => { // 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다. - const allUniversities = await getAllUniversities(); + const allUniversities = await getAllUniversities(params); const categorizedList: AllRegionsUniversityList = { [RegionEnumExtend.ALL]: allUniversities, diff --git a/apps/web/src/constants/university.ts b/apps/web/src/constants/university.ts index 5a89afe3..f6c1746a 100644 --- a/apps/web/src/constants/university.ts +++ b/apps/web/src/constants/university.ts @@ -33,6 +33,7 @@ export const HOME_UNIVERSITY_TO_SLUG_MAP: Record Date: Mon, 22 Jun 2026 14:17:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/apis/universities/api.ts | 33 +++++++++++++---- .../src/apis/universities/getSearchFilter.ts | 4 +++ .../src/apis/universities/getSearchText.ts | 16 +++++++-- .../server/getSearchUniversitiesByFilter.ts | 25 ++++++++++--- .../server/getSearchUniversitiesByText.ts | 21 +++++++++-- .../university/[homeUniversity]/[id]/page.tsx | 36 ++++++++++--------- .../app/university/[homeUniversity]/page.tsx | 11 ++---- apps/web/src/apis/universities/api.ts | 33 +++++++++++++---- .../src/apis/universities/getSearchFilter.ts | 4 +++ .../src/apis/universities/getSearchText.ts | 16 +++++++-- .../server/getSearchUniversitiesByFilter.ts | 25 ++++++++++--- .../server/getSearchUniversitiesByText.ts | 21 +++++++++-- .../application/apply/ApplyPageContent.tsx | 2 +- 13 files changed, 189 insertions(+), 58 deletions(-) diff --git a/apps/university-web/src/apis/universities/api.ts b/apps/university-web/src/apis/universities/api.ts index 561edc66..a418af23 100644 --- a/apps/university-web/src/apis/universities/api.ts +++ b/apps/university-web/src/apis/universities/api.ts @@ -8,6 +8,20 @@ const getClientUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const getScopedTermId = (termId: unknown, useDefaultTermId?: boolean) => { + if (termId === undefined) { + return useDefaultTermId ? getClientUniversityTermId() : undefined; + } + + return normalizePositiveInt(termId) ?? getClientUniversityTermId(); +}; + export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; @@ -185,22 +199,27 @@ export const universitiesApi = { getSearchText: async (params?: { value?: string; termId?: number; + useDefaultTermId?: boolean; homeUniversityId?: number; }): Promise => { + const requestParams = { + value: params?.value ?? "", + termId: getScopedTermId(params?.termId, params?.useDefaultTermId), + homeUniversityId: normalizePositiveInt(params?.homeUniversityId), + }; + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { - params: { - value: params?.value ?? "", - termId: params?.termId ?? getClientUniversityTermId(), - homeUniversityId: params?.homeUniversityId, - }, + params: requestParams, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const { termId, homeUniversityId, useDefaultTermId, ...restParams } = params?.params ?? {}; const requestParams = { - ...params?.params, - termId: params?.params?.termId ?? getClientUniversityTermId(), + ...restParams, + termId: getScopedTermId(termId, useDefaultTermId === true), + homeUniversityId: normalizePositiveInt(homeUniversityId), }; const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { diff --git a/apps/university-web/src/apis/universities/getSearchFilter.ts b/apps/university-web/src/apis/universities/getSearchFilter.ts index 55c33fc7..fe27a947 100644 --- a/apps/university-web/src/apis/universities/getSearchFilter.ts +++ b/apps/university-web/src/apis/universities/getSearchFilter.ts @@ -12,6 +12,7 @@ export interface UniversitySearchFilterParams { countryCode?: CountryCode[]; termId?: number; homeUniversityId?: number; + useDefaultTermId?: boolean; } // API 응답에 homeUniversityName이 포함된 타입 @@ -46,6 +47,9 @@ const useGetUniversitySearchByFilter = ( if (filters.homeUniversityId !== undefined) { params.homeUniversityId = filters.homeUniversityId; } + if (filters.useDefaultTermId) { + params.useDefaultTermId = true; + } return params; }; diff --git a/apps/university-web/src/apis/universities/getSearchText.ts b/apps/university-web/src/apis/universities/getSearchText.ts index ed9cefbb..272f2b19 100644 --- a/apps/university-web/src/apis/universities/getSearchText.ts +++ b/apps/university-web/src/apis/universities/getSearchText.ts @@ -12,13 +12,23 @@ interface ListUniversityWithHome extends ListUniversity { homeUniversityName?: HomeUniversityName; } +export interface UniversitySearchOptions { + termId?: number; + homeUniversityId?: number; + useDefaultTermId?: boolean; +} + /** * @description 대학 검색을 위한 useQuery 커스텀 훅 * 모든 대학 데이터를 한 번만 가져와 캐싱하고, 검색어에 따라 클라이언트에서 필터링합니다. * @param searchValue - 검색어 * @param homeUniversityName - 홈 대학교 이름 (선택적 필터) */ -const useUniversitySearch = (searchValue: string, homeUniversityName?: HomeUniversityName) => { +const useUniversitySearch = ( + searchValue: string, + homeUniversityName?: HomeUniversityName, + options: UniversitySearchOptions = {}, +) => { // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. const { data: allUniversities, @@ -26,8 +36,8 @@ const useUniversitySearch = (searchValue: string, homeUniversityName?: HomeUnive isError, error, } = useQuery({ - queryKey: [QueryKeys.universities.searchText], - queryFn: () => universitiesApi.getSearchText({ value: "" }), + queryKey: [QueryKeys.universities.searchText, options], + queryFn: () => universitiesApi.getSearchText({ value: "", ...options }), staleTime: Infinity, gcTime: Infinity, select: (data) => data.univApplyInfoPreviews as unknown as ListUniversityWithHome[], diff --git a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts index 190279ce..19c1e707 100644 --- a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts +++ b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByFilter.ts @@ -25,6 +25,22 @@ const getUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const assertPositiveInt = (name: string, value: unknown) => { + const positiveInt = normalizePositiveInt(value); + + if (positiveInt === undefined) { + throw new Error(`${name} must be a positive integer.`); + } + + return positiveInt; +}; + export const getSearchUniversitiesByFilter = async ( filters: UniversitySearchFilterParams, ): Promise => { @@ -41,10 +57,10 @@ export const getSearchUniversitiesByFilter = async ( filters.countryCode.forEach((code) => params.append("countryCode", code)); } if (filters.termId !== undefined) { - params.append("termId", String(filters.termId)); + params.append("termId", String(assertPositiveInt("termId", filters.termId))); } if (filters.homeUniversityId !== undefined) { - params.append("homeUniversityId", String(filters.homeUniversityId)); + params.append("homeUniversityId", String(assertPositiveInt("homeUniversityId", filters.homeUniversityId))); } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. @@ -61,13 +77,14 @@ export const getSearchUniversitiesByFilter = async ( export const getSearchUniversitiesAllRegions = async ( params: Pick = {}, ): Promise => { + const termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); const searchParams = new URLSearchParams({ value: "", - termId: String(params.termId ?? getUniversityTermId()), + termId: String(termId), }); if (params.homeUniversityId !== undefined) { - searchParams.set("homeUniversityId", String(params.homeUniversityId)); + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", params.homeUniversityId))); } const endpoint = `/univ-apply-infos/search/text?${searchParams.toString()}`; diff --git a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts index c2f4bc44..89fb4049 100644 --- a/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts +++ b/apps/university-web/src/apis/universities/server/getSearchUniversitiesByText.ts @@ -20,14 +20,31 @@ const getUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const assertPositiveInt = (name: string, value: unknown) => { + const positiveInt = normalizePositiveInt(value); + + if (positiveInt === undefined) { + throw new Error(`${name} must be a positive integer.`); + } + + return positiveInt; +}; + const createSearchTextEndpoint = (value: string, params: UniversitySearchTextParams = {}) => { + const termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); const searchParams = new URLSearchParams({ value, - termId: String(params.termId ?? getUniversityTermId()), + termId: String(termId), }); if (params.homeUniversityId !== undefined) { - searchParams.set("homeUniversityId", String(params.homeUniversityId)); + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", params.homeUniversityId))); } return `/univ-apply-infos/search/text?${searchParams.toString()}`; diff --git a/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx b/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx index 2a4f59d9..d3fefb34 100644 --- a/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx +++ b/apps/university-web/src/app/university/[homeUniversity]/[id]/page.tsx @@ -17,25 +17,27 @@ export const dynamicParams = false; // 모든 homeUniversity + id 조합에 대해 정적 경로 생성 export async function generateStaticParams() { - const params: { homeUniversity: string; id: string }[] = []; - - for (const slug of HOME_UNIVERSITY_SLUGS) { - const homeUniversityInfo = getHomeUniversityBySlug(slug); - if (!homeUniversityInfo) continue; - - const universities = await getAllUniversities({ - homeUniversityId: homeUniversityInfo.homeUniversityId, - }); - - for (const university of universities) { - params.push({ - homeUniversity: slug, - id: String(university.id), + const scopedUniversitiesBySlug = await Promise.all( + HOME_UNIVERSITY_SLUGS.map(async (slug) => { + const homeUniversityInfo = getHomeUniversityBySlug(slug); + if (!homeUniversityInfo) { + return { slug, universities: [] }; + } + + const universities = await getAllUniversities({ + homeUniversityId: homeUniversityInfo.homeUniversityId, }); - } - } - return params; + return { slug, universities }; + }), + ); + + return scopedUniversitiesBySlug.flatMap(({ slug, universities }) => + universities.map((university) => ({ + homeUniversity: slug, + id: String(university.id), + })), + ); } type PageProps = { diff --git a/apps/university-web/src/app/university/[homeUniversity]/page.tsx b/apps/university-web/src/app/university/[homeUniversity]/page.tsx index 926c4331..e5cc67f7 100644 --- a/apps/university-web/src/app/university/[homeUniversity]/page.tsx +++ b/apps/university-web/src/app/university/[homeUniversity]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation"; import { getSearchUniversitiesAllRegions } from "@/apis/universities/server"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import { getHomeUniversityBySlug, HOME_UNIVERSITY_SLUGS, isMatchedHomeUniversityName } from "@/constants/university"; +import { getHomeUniversityBySlug, HOME_UNIVERSITY_SLUGS } from "@/constants/university"; import type { HomeUniversitySlug } from "@/types/university"; import UniversityListContent from "./_ui/UniversityListContent"; @@ -53,19 +53,14 @@ const UniversityListPage = async ({ params }: PageProps) => { notFound(); } - const allUniversities = await getSearchUniversitiesAllRegions({ + const universities = await getSearchUniversitiesAllRegions({ homeUniversityId: universityInfo.homeUniversityId, }); - // homeUniversityName으로 프론트에서 필터링 - const filteredUniversities = allUniversities.filter((university) => - isMatchedHomeUniversityName(university.homeUniversityName, universityInfo.name), - ); - return ( <> - + ); }; diff --git a/apps/web/src/apis/universities/api.ts b/apps/web/src/apis/universities/api.ts index 561edc66..a418af23 100644 --- a/apps/web/src/apis/universities/api.ts +++ b/apps/web/src/apis/universities/api.ts @@ -8,6 +8,20 @@ const getClientUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const getScopedTermId = (termId: unknown, useDefaultTermId?: boolean) => { + if (termId === undefined) { + return useDefaultTermId ? getClientUniversityTermId() : undefined; + } + + return normalizePositiveInt(termId) ?? getClientUniversityTermId(); +}; + export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { id: number; term: string; @@ -185,22 +199,27 @@ export const universitiesApi = { getSearchText: async (params?: { value?: string; termId?: number; + useDefaultTermId?: boolean; homeUniversityId?: number; }): Promise => { + const requestParams = { + value: params?.value ?? "", + termId: getScopedTermId(params?.termId, params?.useDefaultTermId), + homeUniversityId: normalizePositiveInt(params?.homeUniversityId), + }; + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { - params: { - value: params?.value ?? "", - termId: params?.termId ?? getClientUniversityTermId(), - homeUniversityId: params?.homeUniversityId, - }, + params: requestParams, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const { termId, homeUniversityId, useDefaultTermId, ...restParams } = params?.params ?? {}; const requestParams = { - ...params?.params, - termId: params?.params?.termId ?? getClientUniversityTermId(), + ...restParams, + termId: getScopedTermId(termId, useDefaultTermId === true), + homeUniversityId: normalizePositiveInt(homeUniversityId), }; const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { diff --git a/apps/web/src/apis/universities/getSearchFilter.ts b/apps/web/src/apis/universities/getSearchFilter.ts index 55c33fc7..fe27a947 100644 --- a/apps/web/src/apis/universities/getSearchFilter.ts +++ b/apps/web/src/apis/universities/getSearchFilter.ts @@ -12,6 +12,7 @@ export interface UniversitySearchFilterParams { countryCode?: CountryCode[]; termId?: number; homeUniversityId?: number; + useDefaultTermId?: boolean; } // API 응답에 homeUniversityName이 포함된 타입 @@ -46,6 +47,9 @@ const useGetUniversitySearchByFilter = ( if (filters.homeUniversityId !== undefined) { params.homeUniversityId = filters.homeUniversityId; } + if (filters.useDefaultTermId) { + params.useDefaultTermId = true; + } return params; }; diff --git a/apps/web/src/apis/universities/getSearchText.ts b/apps/web/src/apis/universities/getSearchText.ts index ed9cefbb..272f2b19 100644 --- a/apps/web/src/apis/universities/getSearchText.ts +++ b/apps/web/src/apis/universities/getSearchText.ts @@ -12,13 +12,23 @@ interface ListUniversityWithHome extends ListUniversity { homeUniversityName?: HomeUniversityName; } +export interface UniversitySearchOptions { + termId?: number; + homeUniversityId?: number; + useDefaultTermId?: boolean; +} + /** * @description 대학 검색을 위한 useQuery 커스텀 훅 * 모든 대학 데이터를 한 번만 가져와 캐싱하고, 검색어에 따라 클라이언트에서 필터링합니다. * @param searchValue - 검색어 * @param homeUniversityName - 홈 대학교 이름 (선택적 필터) */ -const useUniversitySearch = (searchValue: string, homeUniversityName?: HomeUniversityName) => { +const useUniversitySearch = ( + searchValue: string, + homeUniversityName?: HomeUniversityName, + options: UniversitySearchOptions = {}, +) => { // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. const { data: allUniversities, @@ -26,8 +36,8 @@ const useUniversitySearch = (searchValue: string, homeUniversityName?: HomeUnive isError, error, } = useQuery({ - queryKey: [QueryKeys.universities.searchText], - queryFn: () => universitiesApi.getSearchText({ value: "" }), + queryKey: [QueryKeys.universities.searchText, options], + queryFn: () => universitiesApi.getSearchText({ value: "", ...options }), staleTime: Infinity, gcTime: Infinity, select: (data) => data.univApplyInfoPreviews as unknown as ListUniversityWithHome[], diff --git a/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts b/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts index 829d6c7f..c98051b6 100644 --- a/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts +++ b/apps/web/src/apis/universities/server/getSearchUniversitiesByFilter.ts @@ -24,6 +24,22 @@ const getUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const assertPositiveInt = (name: string, value: unknown) => { + const positiveInt = normalizePositiveInt(value); + + if (positiveInt === undefined) { + throw new Error(`${name} must be a positive integer.`); + } + + return positiveInt; +}; + export const getSearchUniversitiesByFilter = async ( filters: UniversitySearchFilterParams, ): Promise => { @@ -40,10 +56,10 @@ export const getSearchUniversitiesByFilter = async ( filters.countryCode.forEach((code) => params.append("countryCode", code)); } if (filters.termId !== undefined) { - params.append("termId", String(filters.termId)); + params.append("termId", String(assertPositiveInt("termId", filters.termId))); } if (filters.homeUniversityId !== undefined) { - params.append("homeUniversityId", String(filters.homeUniversityId)); + params.append("homeUniversityId", String(assertPositiveInt("homeUniversityId", filters.homeUniversityId))); } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. @@ -64,13 +80,14 @@ export const getSearchUniversitiesByFilter = async ( export const getSearchUniversitiesAllRegions = async ( params: Pick = {}, ): Promise => { + const termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); const searchParams = new URLSearchParams({ value: "", - termId: String(params.termId ?? getUniversityTermId()), + termId: String(termId), }); if (params.homeUniversityId !== undefined) { - searchParams.set("homeUniversityId", String(params.homeUniversityId)); + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", params.homeUniversityId))); } const endpoint = `/univ-apply-infos/search/text?${searchParams.toString()}`; diff --git a/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts b/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts index 0474773f..6cb17f5a 100644 --- a/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts +++ b/apps/web/src/apis/universities/server/getSearchUniversitiesByText.ts @@ -19,14 +19,31 @@ const getUniversityTermId = () => { return Number.isInteger(termId) && termId > 0 ? termId : DEFAULT_UNIVERSITY_TERM_ID; }; +const normalizePositiveInt = (value: unknown) => { + const numberValue = typeof value === "string" && value.trim() !== "" ? Number(value) : value; + + return typeof numberValue === "number" && Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined; +}; + +const assertPositiveInt = (name: string, value: unknown) => { + const positiveInt = normalizePositiveInt(value); + + if (positiveInt === undefined) { + throw new Error(`${name} must be a positive integer.`); + } + + return positiveInt; +}; + const createSearchTextEndpoint = (value: string, params: UniversitySearchTextParams = {}) => { + const termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); const searchParams = new URLSearchParams({ value, - termId: String(params.termId ?? getUniversityTermId()), + termId: String(termId), }); if (params.homeUniversityId !== undefined) { - searchParams.set("homeUniversityId", String(params.homeUniversityId)); + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", params.homeUniversityId))); } return `/univ-apply-infos/search/text?${searchParams.toString()}`; diff --git a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx index ca69f263..823f95c3 100644 --- a/apps/web/src/app/university/application/apply/ApplyPageContent.tsx +++ b/apps/web/src/app/university/application/apply/ApplyPageContent.tsx @@ -20,7 +20,7 @@ const ApplyPageContent = () => { const router = useRouter(); const [step, setStep] = useState(1); - const { data: universityList = [] } = useUniversitySearch(""); + const { data: universityList = [] } = useUniversitySearch("", undefined, { useDefaultTermId: true }); const { data: gpaScoreList = [] } = useGetMyGpaScore(); const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore(); const { mutate: postSubmitApplication } = usePostSubmitApplication({