diff --git a/apps/university-web/src/apis/universities/api.ts b/apps/university-web/src/apis/universities/api.ts index 6b0055a3..a418af23 100644 --- a/apps/university-web/src/apis/universities/api.ts +++ b/apps/university-web/src/apis/universities/api.ts @@ -1,10 +1,32 @@ +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; +}; + +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; koreanName: string; + homeUniversityName?: HomeUniversityName; region: string; country: string; logoImageUrl: string; @@ -174,16 +196,34 @@ export const universitiesApi = { return res.data; }, - getSearchText: async (params?: { value?: string }): Promise => { + 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 ?? "" }, + params: requestParams, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const { termId, homeUniversityId, useDefaultTermId, ...restParams } = params?.params ?? {}; + const requestParams = { + ...restParams, + termId: getScopedTermId(termId, useDefaultTermId === true), + homeUniversityId: normalizePositiveInt(homeUniversityId), + }; + 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..fe27a947 100644 --- a/apps/university-web/src/apis/universities/getSearchFilter.ts +++ b/apps/university-web/src/apis/universities/getSearchFilter.ts @@ -10,6 +10,9 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; + useDefaultTermId?: boolean; } // API 응답에 homeUniversityName이 포함된 타입 @@ -38,6 +41,15 @@ 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; + } + 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 a6b5dee7..19c1e707 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,32 @@ 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; +}; + +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 => { @@ -31,6 +56,12 @@ export const getSearchUniversitiesByFilter = async ( if (filters.countryCode) { filters.countryCode.forEach((code) => params.append("countryCode", code)); } + if (filters.termId !== undefined) { + params.append("termId", String(assertPositiveInt("termId", filters.termId))); + } + if (filters.homeUniversityId !== undefined) { + params.append("homeUniversityId", String(assertPositiveInt("homeUniversityId", filters.homeUniversityId))); + } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. if (params.size === 0) { @@ -43,8 +74,20 @@ 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 termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); + const searchParams = new URLSearchParams({ + value: "", + termId: String(termId), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", 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..89fb4049 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,68 @@ 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 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(termId), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", 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..d3fefb34 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,29 +17,27 @@ 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), - ); - - for (const university of filteredUniversities) { - 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 324f7ed3..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,17 +53,14 @@ const UniversityListPage = async ({ params }: PageProps) => { notFound(); } - const allUniversities = await getSearchUniversitiesAllRegions(); - - // homeUniversityName으로 프론트에서 필터링 - const filteredUniversities = allUniversities.filter((university) => - isMatchedHomeUniversityName(university.homeUniversityName, universityInfo.name), - ); + const universities = await getSearchUniversitiesAllRegions({ + homeUniversityId: universityInfo.homeUniversityId, + }); return ( <> - + ); }; 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; +}; + +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; koreanName: string; + homeUniversityName?: HomeUniversityName; region: string; country: string; logoImageUrl: string; @@ -174,16 +196,34 @@ export const universitiesApi = { return res.data; }, - getSearchText: async (params?: { value?: string }): Promise => { + 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 ?? "" }, + params: requestParams, }); return res.data; }, getSearchFilter: async (params?: { params?: Record }): Promise => { + const { termId, homeUniversityId, useDefaultTermId, ...restParams } = params?.params ?? {}; + const requestParams = { + ...restParams, + termId: getScopedTermId(termId, useDefaultTermId === true), + homeUniversityId: normalizePositiveInt(homeUniversityId), + }; + 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..fe27a947 100644 --- a/apps/web/src/apis/universities/getSearchFilter.ts +++ b/apps/web/src/apis/universities/getSearchFilter.ts @@ -10,6 +10,9 @@ export interface UniversitySearchFilterParams { languageTestType?: LanguageTestType; testScore?: number; countryCode?: CountryCode[]; + termId?: number; + homeUniversityId?: number; + useDefaultTermId?: boolean; } // API 응답에 homeUniversityName이 포함된 타입 @@ -38,6 +41,15 @@ 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; + } + 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 12396f36..c98051b6 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,32 @@ 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; +}; + +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 => { @@ -30,6 +55,12 @@ export const getSearchUniversitiesByFilter = async ( if (filters.countryCode) { filters.countryCode.forEach((code) => params.append("countryCode", code)); } + if (filters.termId !== undefined) { + params.append("termId", String(assertPositiveInt("termId", filters.termId))); + } + if (filters.homeUniversityId !== undefined) { + params.append("homeUniversityId", String(assertPositiveInt("homeUniversityId", filters.homeUniversityId))); + } // 필터 값이 하나도 없으면 빈 배열을 반환합니다. if (params.size === 0) { @@ -46,8 +77,20 @@ 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 termId = params.termId === undefined ? getUniversityTermId() : assertPositiveInt("termId", params.termId); + const searchParams = new URLSearchParams({ + value: "", + termId: String(termId), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", 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..6cb17f5a 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,55 @@ 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 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(termId), + }); + + if (params.homeUniversityId !== undefined) { + searchParams.set("homeUniversityId", String(assertPositiveInt("homeUniversityId", 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 +66,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/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 ( <> 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({ 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