Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions apps/university-web/src/apis/universities/api.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -174,16 +196,34 @@ export const universitiesApi = {
return res.data;
},

getSearchText: async (params?: { value?: string }): Promise<SearchTextResponse> => {
getSearchText: async (params?: {
value?: string;
termId?: number;
useDefaultTermId?: boolean;
homeUniversityId?: number;
}): Promise<SearchTextResponse> => {
const requestParams = {
value: params?.value ?? "",
termId: getScopedTermId(params?.termId, params?.useDefaultTermId),
homeUniversityId: normalizePositiveInt(params?.homeUniversityId),
};

const res = await publicAxiosInstance.get<SearchTextResponse>(`/univ-apply-infos/search/text`, {
params: { value: params?.value ?? "" },
params: requestParams,
});
return res.data;
},

getSearchFilter: async (params?: { params?: Record<string, unknown> }): Promise<SearchFilterResponse> => {
const { termId, homeUniversityId, useDefaultTermId, ...restParams } = params?.params ?? {};
const requestParams = {
...restParams,
termId: getScopedTermId(termId, useDefaultTermId === true),
homeUniversityId: normalizePositiveInt(homeUniversityId),
};

const res = await publicAxiosInstance.get<SearchFilterResponse>(`/univ-apply-infos/search/filter`, {
params: params?.params,
params: requestParams,
});
return res.data;
},
Expand Down
12 changes: 12 additions & 0 deletions apps/university-web/src/apis/universities/getSearchFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface UniversitySearchFilterParams {
languageTestType?: LanguageTestType;
testScore?: number;
countryCode?: CountryCode[];
termId?: number;
homeUniversityId?: number;
useDefaultTermId?: boolean;
}

// API 응답에 homeUniversityName이 포함된 타입
Expand Down Expand Up @@ -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;
};

Expand Down
16 changes: 13 additions & 3 deletions apps/university-web/src/apis/universities/getSearchText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,32 @@ 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,
isLoading,
isError,
error,
} = useQuery<SearchTextResponse, AxiosError, ListUniversityWithHome[]>({
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[],
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<ListUniversity[]> => {
Expand All @@ -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)));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 필터 값이 하나도 없으면 빈 배열을 반환합니다.
if (params.size === 0) {
Expand All @@ -43,8 +74,20 @@ export const getSearchUniversitiesByFilter = async (
.univApplyInfoPreviews;
};

export const getSearchUniversitiesAllRegions = async (): Promise<ListUniversity[]> => {
const endpoint = `/univ-apply-infos/search/text?value=`;
export const getSearchUniversitiesAllRegions = async (
params: Pick<UniversitySearchFilterParams, "termId" | "homeUniversityId"> = {},
): Promise<ListUniversity[]> => {
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<UniversitySearchResponse>(endpoint);
return assertUniversitySsgResponse(response, "search all universities").univApplyInfoPreviews;
};
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,22 +9,68 @@ interface UniversitySearchResponse {
univApplyInfoPreviews: ListUniversity[];
}

export const getUniversitiesByText = async (value: string): Promise<ListUniversity[]> => {
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)));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return `/univ-apply-infos/search/text?${searchParams.toString()}`;
};

export const getUniversitiesByText = async (
value: string,
params?: UniversitySearchTextParams,
): Promise<ListUniversity[]> => {
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<UniversitySearchResponse>(endpoint);
return assertUniversitySsgResponse(response, `search universities by text "${value}"`).univApplyInfoPreviews;
};

export const getAllUniversities = async (): Promise<ListUniversity[]> => {
return getUniversitiesByText("");
export const getAllUniversities = async (params?: UniversitySearchTextParams): Promise<ListUniversity[]> => {
return getUniversitiesByText("", params);
};

export const getCategorizedUniversities = async (): Promise<AllRegionsUniversityList> => {
export const getCategorizedUniversities = async (
params?: UniversitySearchTextParams,
): Promise<AllRegionsUniversityList> => {
// 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다.
const allUniversities = await getAllUniversities();
const allUniversities = await getAllUniversities(params);

const categorizedList: AllRegionsUniversityList = {
[RegionEnumExtend.ALL]: allUniversities,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<>
<TopDetailNavigation title={`${universityInfo.shortName} 파견학교`} backHref="/university" />
<UniversityListContent universities={filteredUniversities} homeUniversitySlug={homeUniversitySlug} />
<UniversityListContent universities={universities} homeUniversitySlug={homeUniversitySlug} />
</>
);
};
Expand Down
Loading
Loading