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
40 changes: 39 additions & 1 deletion apps/web/src/apis/applications/getApplicants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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";

type UseGetApplicationsListOptions = Omit<
UseQueryOptions<AxiosResponse<ApplicationListResponse>, AxiosError<{ message: string }>, ApplicationListResponse>,
Expand All @@ -11,17 +16,50 @@ type UseGetApplicationsListOptions = Omit<

/**
* @description 지원 목록 조회 훅
*
* `GET /applications` 는 전체 지원자 현황을 내려주고 소속 대학을 구분해주지 않는다.
* 그래서 access token 에서 파싱된 소속 대학(useAuthStore.homeUniversityId)으로
* 파견학교 목록을 따로 받아, 그 범위에 속한 항목만 남기도록 클라이언트에서 걸러낸다.
*/
const useGetApplicationsList = (
props?: UseGetApplicationsListOptions,
): UseQueryResult<ApplicationListResponse, AxiosError<{ message: string }>> => {
return useQuery({
const homeUniversityId = useAuthStore((state) => state.homeUniversityId);

const applicationsQuery = useQuery({
queryKey: [ApplicationsQueryKeys.competitorsApplicationList],
queryFn: applicationsApi.getApplicationsList,
staleTime: 1000 * 60 * 5, // 5분간 캐시
select: (response) => response.data,
...props,
});

// 필터 기준이 되는 소속 대학의 파견학교 목록
const { data: scopedUniversities } = useQuery({
queryKey: [QueryKeys.universities.searchText, { homeUniversityId }],
queryFn: () => universitiesApi.getSearchText({ value: "", homeUniversityId: homeUniversityId ?? undefined }),
enabled: homeUniversityId !== null,
staleTime: 1000 * 60 * 5,
select: (response) => response.univApplyInfoPreviews,
});

const scopedData = useMemo(() => {
if (!applicationsQuery.data) {
return applicationsQuery.data;
}

// 소속 대학을 모르면(비인증·학교 미인증) 기존과 동일하게 전체를 보여준다.
if (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;
40 changes: 40 additions & 0 deletions apps/web/src/apis/applications/homeUniversityScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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;
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail closed when the university scope is unavailable

When an authenticated user's scope request is still loading, fails, or legitimately returns zero universities, this branch returns the complete /applications response. Because useGetApplicationsList exposes only the applications query's loading/error state, both ApprovedApplicationStatusPage and the university-detail page can then render applicants from other home universities—briefly during ordinary request races and indefinitely after a scope-query failure. Keep the result loading/erroring until the scope resolves, and treat a successful empty scope as an empty filtered result rather than returning all applications.

Useful? React with 👍 / 👎.

}

const allowedKeys = new Set(scopedUniversities.map(createHomeUniversityScopeKey));

return {
choices: applications.choices.map((scoreSheets: ScoreSheet[]) =>
scoreSheets.filter((scoreSheet) => allowedKeys.has(createHomeUniversityScopeKey(scoreSheet))),
),
};
};
Loading