From 0725f56a9a57dae54f523e83cd0f37b65fd4045d Mon Sep 17 00:00:00 2001 From: whqtker Date: Thu, 3 Sep 2026 00:37:12 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=ED=95=84=ED=84=B0=EB=B3=84=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A7=80=EC=9B=90=EC=9E=90=20=EC=88=98=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/applications/api.ts | 11 ++-- .../src/apis/applications/getApplicants.ts | 53 ++----------------- .../src/apis/applications/getCompetitors.ts | 9 ++-- .../apis/applications/homeUniversityScope.ts | 40 -------------- .../_pages/ApprovedApplicationStatusPage.tsx | 26 ++++++--- 5 files changed, 36 insertions(+), 103 deletions(-) delete mode 100644 apps/web/src/apis/applications/homeUniversityScope.ts diff --git a/apps/web/src/apis/applications/api.ts b/apps/web/src/apis/applications/api.ts index 61f3d952..df742f31 100644 --- a/apps/web/src/apis/applications/api.ts +++ b/apps/web/src/apis/applications/api.ts @@ -1,10 +1,9 @@ import type { AxiosResponse } from "axios"; import type { ApplicationListResponse, ApplicationPreviewResponse } from "@/types/application"; +import type { RegionEnum } from "@/types/university"; import { axiosInstance } from "@/utils/axiosInstance"; -// ====== Query Keys ====== export const ApplicationsQueryKeys = { - competitorsApplicationList: "competitorsApplicationList", applicationPreview: "applicationPreview", } as const; @@ -25,13 +24,17 @@ export interface UseSubmitApplicationRequest { }; } +export type ApplicantsSearchParams = { + region?: RegionEnum; + keyword?: string; +}; // ====== API Functions ====== export const applicationsApi = { /** * 전체 지원자 현황 조회 */ - getApplicationsList: async (): Promise> => { - return axiosInstance.get("/applications"); + getApplicationsList: async (params?: ApplicantsSearchParams): Promise> => { + return axiosInstance.get("/applications", { params }); }, /** diff --git a/apps/web/src/apis/applications/getApplicants.ts b/apps/web/src/apis/applications/getApplicants.ts index be825ee7..2f484b10 100644 --- a/apps/web/src/apis/applications/getApplicants.ts +++ b/apps/web/src/apis/applications/getApplicants.ts @@ -1,69 +1,26 @@ import { type UseQueryOptions, type UseQueryResult, useQuery } from "@tanstack/react-query"; import type { AxiosError, AxiosResponse } from "axios"; -import { useMemo } from "react"; -import useAuthStore from "@/lib/zustand/useAuthStore"; import type { ApplicationListResponse } from "@/types/application"; import { QueryKeys } from "../queryKeys"; -import { universitiesApi } from "../universities/api"; -import { ApplicationsQueryKeys, applicationsApi } from "./api"; -import { filterApplicationsByHomeUniversityScope } from "./homeUniversityScope"; +import { type ApplicantsSearchParams, applicationsApi } from "./api"; type UseGetApplicationsListOptions = Omit< UseQueryOptions, AxiosError<{ message: string }>, ApplicationListResponse>, "queryKey" | "queryFn" >; -/** - * 지원 기간에는 지원한 대학만, 기간 종료 후에는 소속 대학의 전체 현황을 공개한다. - * 전체 현황을 다시 공개할 때 반환값을 "applications"로 변경한다. - */ -type ApplicationStatusEndpoint = "applications" | "competitors"; - -const getApplicationStatusEndpoint = (): ApplicationStatusEndpoint => "competitors"; - -const applicationStatusEndpoint = getApplicationStatusEndpoint(); -const shouldFilterByHomeUniversity = applicationStatusEndpoint === "applications"; -const applicationStatusQueryFn = - applicationStatusEndpoint === "competitors" ? applicationsApi.getCompetitors : applicationsApi.getApplicationsList; - -/** - * @description 내가 지원한 대학의 지원자 현황 조회 훅 - */ const useGetApplicationsList = ( + params?: ApplicantsSearchParams, props?: UseGetApplicationsListOptions, ): UseQueryResult> => { - const homeUniversityId = useAuthStore((state) => state.homeUniversityId); - - const applicationsQuery = useQuery({ - queryKey: [ApplicationsQueryKeys.competitorsApplicationList], - queryFn: applicationStatusQueryFn, + return useQuery({ + queryKey: [QueryKeys.applications.applicants, params], + queryFn: () => applicationsApi.getApplicationsList(params), staleTime: 1000 * 60 * 5, // 5분간 캐시 select: (response) => response.data, ...props, }); - - // GET /applications를 사용할 때만 소속 대학의 파견학교 목록으로 범위를 제한한다. - const { data: scopedUniversities } = useQuery({ - queryKey: [QueryKeys.universities.searchText, { homeUniversityId }], - queryFn: () => universitiesApi.getSearchText({ value: "", homeUniversityId: homeUniversityId ?? undefined }), - enabled: shouldFilterByHomeUniversity && homeUniversityId !== null, - staleTime: 1000 * 60 * 5, - select: (response) => response.univApplyInfoPreviews, - }); - - const scopedData = useMemo(() => { - if (!applicationsQuery.data || !shouldFilterByHomeUniversity || homeUniversityId === null) { - return applicationsQuery.data; - } - - return filterApplicationsByHomeUniversityScope(applicationsQuery.data, scopedUniversities); - }, [applicationsQuery.data, homeUniversityId, scopedUniversities]); - - return { ...applicationsQuery, data: scopedData } as UseQueryResult< - ApplicationListResponse, - AxiosError<{ message: string }> - >; }; export default useGetApplicationsList; diff --git a/apps/web/src/apis/applications/getCompetitors.ts b/apps/web/src/apis/applications/getCompetitors.ts index 48df6135..dc415193 100644 --- a/apps/web/src/apis/applications/getCompetitors.ts +++ b/apps/web/src/apis/applications/getCompetitors.ts @@ -1,6 +1,5 @@ -import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; +import { type UseQueryOptions, type UseQueryResult, useQuery } from "@tanstack/react-query"; import type { AxiosError, AxiosResponse } from "axios"; - import type { ApplicationListResponse } from "@/types/application"; import { QueryKeys } from "../queryKeys"; import { applicationsApi } from "./api"; @@ -10,8 +9,10 @@ type UseGetCompetitorsOptions = Omit< "queryKey" | "queryFn" >; -const useGetCompetitors = (props?: UseGetCompetitorsOptions) => { - return useQuery({ +const useGetCompetitors = ( + props?: UseGetCompetitorsOptions, +): UseQueryResult> => { + return useQuery, AxiosError<{ message: string }>, ApplicationListResponse>({ queryKey: [QueryKeys.applications.competitors], queryFn: applicationsApi.getCompetitors, select: (response) => response.data, diff --git a/apps/web/src/apis/applications/homeUniversityScope.ts b/apps/web/src/apis/applications/homeUniversityScope.ts deleted file mode 100644 index 56362be3..00000000 --- a/apps/web/src/apis/applications/homeUniversityScope.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ApplicationListResponse, ScoreSheet } from "@/types/application"; - -/** - * 지원자 현황 응답을 소속 대학 기준으로 걸러내기 위한 키. - * - * GET /applications 응답(ScoreSheet)에는 홈 대학 식별자도, 지원 정보 id도 없다. - * 그래서 파견학교 목록과 겹치는 필드(파견학교명 + 국가 + 모집인원)를 합쳐 식별한다. - * - * 파견학교명만으로는 부족하다. 서로 다른 홈 대학이 같은 이름의 파견학교를 가질 수 있고 - * (예: 경희대·중앙대 모두 보유한 국립정치대학교), 그 경우 이름만으로는 구분되지 않는다. - */ -type ScopeKeySource = { - koreanName: string; - country: string; - studentCapacity: number | null; -}; - -export const createHomeUniversityScopeKey = ({ koreanName, country, studentCapacity }: ScopeKeySource): string => - `${koreanName}|${country}|${studentCapacity ?? ""}`; - -/** - * 소속 대학의 파견학교 목록에 포함된 항목만 남긴다. - * 기준 목록이 비어 있으면 필터링하지 않고 원본을 그대로 돌려준다. - */ -export const filterApplicationsByHomeUniversityScope = ( - applications: ApplicationListResponse, - scopedUniversities: ScopeKeySource[] | undefined, -): ApplicationListResponse => { - if (!scopedUniversities || scopedUniversities.length === 0) { - return applications; - } - - const allowedKeys = new Set(scopedUniversities.map(createHomeUniversityScopeKey)); - - return { - choices: applications.choices.map((scoreSheets: ScoreSheet[]) => - scoreSheets.filter((scoreSheet) => allowedKeys.has(createHomeUniversityScopeKey(scoreSheet))), - ), - }; -}; diff --git a/apps/web/src/app/university/application/_pages/ApprovedApplicationStatusPage.tsx b/apps/web/src/app/university/application/_pages/ApprovedApplicationStatusPage.tsx index ff2f466f..46f91ab6 100644 --- a/apps/web/src/app/university/application/_pages/ApprovedApplicationStatusPage.tsx +++ b/apps/web/src/app/university/application/_pages/ApprovedApplicationStatusPage.tsx @@ -5,14 +5,14 @@ import clsx from "clsx"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { type ReactNode, useEffect, useMemo, useState } from "react"; -import { useGetApplicationsList } from "@/apis/applications"; +import { useGetApplicationsList, useGetCompetitors } from "@/apis/applications"; import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; import { DEFAULT_MAX_CHOICE_COUNT, getHomeUniversityById, REGIONS_KO } from "@/constants/university"; import { SKIP_GLOBAL_ERROR_TOAST_META } from "@/lib/react-query/errorToastMeta"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { IconExpandMoreFilled } from "@/public/svgs/community"; import type { Applicant, ScoreSheet as ScoreSheetType } from "@/types/application"; -import type { RegionKo } from "@/types/university"; +import { type RegionKo, regionMapping } from "@/types/university"; import { getApplicationDetailHref, MobileScoreSheet, ScoreSheetLogo } from "../ScoreSheet"; type ApplicationAccessErrorCode = "APPLICATION_NOT_FOUND" | "APPLICATION_NOT_APPROVED"; @@ -46,7 +46,7 @@ type ScorePageViewProps = { displayedScoreSheets: ScoreSheetType[]; totalUniversityCount: number; applicantUniversityCount: number; - participantCount: number; + participantCount: number | null; scope: ApplicantScope; regionFilter: RegionKo | ""; sortMode: ScoreSort; @@ -68,15 +68,20 @@ const ApprovedApplicationStatusPage = () => { () => Array.from({ length: maxChoiceCount }, () => [] as ScoreSheetType[]), [maxChoiceCount], ); + const applicantSearchParams = useMemo( + () => ({ region: regionFilter ? (regionMapping[regionFilter] ?? undefined) : undefined }), + [regionFilter], + ); const { data: scoreResponseData, isError, isLoading, error, refetch, - } = useGetApplicationsList({ + } = useGetCompetitors({ meta: SKIP_GLOBAL_ERROR_TOAST_META, }); + const { data: applicantResponseData } = useGetApplicationsList(applicantSearchParams); const scoreChoices = scoreResponseData?.choices ?? emptyChoices; const isApplicationMissingOrUnapproved = isApplicationAccessError(error); @@ -87,7 +92,10 @@ const ApprovedApplicationStatusPage = () => { ); const totalUniversityCount = allScoreSheets.length; const applicantUniversityCount = allScoreSheets.filter((scoreSheet) => scoreSheet.applicants.length > 0).length; - const participantCount = useMemo(() => getParticipantCount(allScoreSheets), [allScoreSheets]); + const participantCount = useMemo( + () => (applicantResponseData ? getParticipantCount(applicantResponseData.choices.flat()) : null), + [applicantResponseData], + ); const displayedScoreSheets = useMemo(() => { let result = @@ -243,7 +251,7 @@ const AppliedUniversityRow = ({ preference, scoreSheet }: AppliedUniversity) => ); }; -const ParticipantBanner = ({ participantCount }: { participantCount: number }) => ( +const ParticipantBanner = ({ participantCount }: { participantCount: number | null }) => (
@@ -251,7 +259,11 @@ const ParticipantBanner = ({ participantCount }: { participantCount: number }) =

솔리드 커넥션과 함께하고 있어요

-

총 {participantCount}명이 성적 공유 참여중!

+

+ {participantCount === null + ? "지원자 수를 불러오는 중이에요." + : `총 ${participantCount}명이 성적 공유 참여중!`} +