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
4 changes: 1 addition & 3 deletions apps/university-web/src/apis/applications/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ export interface UseSubmitApplicationRequest {
gpaScoreId: number;
languageTestScoreId: number;
universityChoiceRequest: {
firstChoiceUniversityId: number | null;
secondChoiceUniversityId: number | null;
thirdChoiceUniversityId: number | null;
choices: number[];
};
}

Expand Down
10 changes: 5 additions & 5 deletions apps/university-web/src/components/ui/Tab/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type TabProps = {
choices: string[];
choice: string;
setChoice: React.Dispatch<React.SetStateAction<string>>;
type TabProps<T extends string> = {
choices: readonly T[];
choice: T;
setChoice: (choice: T) => void;
color?: {
activeBtn?: string;
deactiveBtn?: string;
Expand All @@ -10,7 +10,7 @@ type TabProps = {
};
};

const Tab = ({ choices, choice, setChoice, color }: TabProps) => {
const Tab = <const T extends string>({ choices, choice, setChoice, color }: TabProps<T>) => {
const defaultColor = {
activeBtnFont: "text-black",
deactiveBtnFont: "text-gray-200",
Expand Down
12 changes: 3 additions & 9 deletions apps/university-web/src/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,11 @@ export interface ApplicationScoreRequest {
}

export interface ApplicationUniversityRequest {
firstChoiceUniversityId: number;
secondChoiceUniversityId: number;
thirdChoiceUniversityId: number;
choices: number[];
}

export interface ApplicationListResponse {
firstChoice: ScoreSheet[];
secondChoice: ScoreSheet[];
thirdChoice: ScoreSheet[];
choices: ScoreSheet[][];
}

export interface ApplicationStatusResponse {
Expand All @@ -65,9 +61,7 @@ export interface SubmitApplicationRequest {
gpaScoreId: number;
languageTestScoreId: number;
universityChoiceRequest: {
firstChoiceUniversityId: number | null;
secondChoiceUniversityId: number | null;
thirdChoiceUniversityId: number | null;
choices: number[];
};
}

Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/apis/applications/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ export interface UseSubmitApplicationRequest {
gpaScoreId: number;
languageTestScoreId: number;
universityChoiceRequest: {
firstChoiceUniversityId: number | null;
secondChoiceUniversityId: number | null;
thirdChoiceUniversityId: number | null;
choices: number[];
};
}

Expand Down
80 changes: 33 additions & 47 deletions apps/web/src/app/university/application/ScorePageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,37 @@ import { useGetApplicationsList } from "@/apis/applications";
import ConfirmCancelModal from "@/components/modal/ConfirmCancelModal";
import ButtonTab from "@/components/ui/ButtonTab";
import Tab from "@/components/ui/Tab";
import { REGIONS_KO } from "@/constants/university";
import { DEFAULT_MAX_CHOICE_COUNT, getHomeUniversityById, REGIONS_KO } from "@/constants/university";
import useAuthStore from "@/lib/zustand/useAuthStore";
import type { ScoreSheet as ScoreSheetType } from "@/types/application";
import type { RegionKo } from "@/types/university";
import ApplicationSectionTitle from "./_components/ApplicationSectionTitle";
import ScoreSearchBar from "./ScoreSearchBar";
import ScoreSearchField from "./ScoreSearchField";
import ScoreSheet from "./ScoreSheet";

const PREFERENCE_CHOICE: ("1순위" | "2순위" | "3순위")[] = ["1순위", "2순위", "3순위"];

interface ScoreData {
firstChoice: ScoreSheetType[];
secondChoice: ScoreSheetType[];
thirdChoice: ScoreSheetType[];
}

const ScorePageContent = () => {
const router = useRouter();
const searchRef = useRef<HTMLInputElement>(null!);
const homeUniversityId = useAuthStore((state) => state.homeUniversityId);
const maxChoiceCount = getHomeUniversityById(homeUniversityId)?.maxChoiceCount ?? DEFAULT_MAX_CHOICE_COUNT;

const [searchActive, setSearchActive] = useState(false);
const [preference, setPreference] = useState<"1순위" | "2순위" | "3순위">("1순위");
const [preferenceIndex, setPreferenceIndex] = useState(0);
const [regionFilter, setRegionFilter] = useState<RegionKo | "">("");
const [searchValue, setSearchValue] = useState("");
const [showNeedApply, _setShowNeedApply] = useState(false);

const initialData: ScoreData = {
firstChoice: [],
secondChoice: [],
thirdChoice: [],
};

const { data: scoreResponseData = initialData, isError, isLoading } = useGetApplicationsList();
const emptyChoices = useMemo(
() => Array.from({ length: maxChoiceCount }, () => [] as ScoreSheetType[]),
[maxChoiceCount],
);
const { data: scoreResponseData, isError, isLoading } = useGetApplicationsList();
const scoreChoices = scoreResponseData?.choices ?? emptyChoices;
const preferenceChoices = useMemo(
() => Array.from({ length: Math.max(scoreChoices.length, maxChoiceCount) }, (_, index) => `${index + 1}순위`),
[maxChoiceCount, scoreChoices.length],
);

const filteredAndSortedData = useMemo(() => {
// ✨ 1. 대학 이름(koreanName)을 기준으로 중복을 제거하는 헬퍼 함수
Expand All @@ -49,17 +47,10 @@ const ScorePageContent = () => {
return Array.from(universityMap.values());
};

// ✨ 2. API 응답 데이터를 받자마자 중복부터 제거합니다.
const firstChoice = uniqueByKoreanName(scoreResponseData?.firstChoice || []);
const secondChoice = uniqueByKoreanName(scoreResponseData?.secondChoice || []);
const thirdChoice = uniqueByKoreanName(scoreResponseData?.thirdChoice || []);

// 3. 중복이 제거된 데이터를 정렬합니다.
const sortedData = {
firstChoice: [...firstChoice].sort((a, b) => b.applicants.length - a.applicants.length),
secondChoice: [...secondChoice].sort((a, b) => b.applicants.length - a.applicants.length),
thirdChoice: [...thirdChoice].sort((a, b) => b.applicants.length - a.applicants.length),
};
// ✨ 2. API 응답 데이터를 받자마자 중복부터 제거하고 정렬합니다.
const sortedData = scoreChoices.map((choice) =>
uniqueByKoreanName(choice).sort((a, b) => b.applicants.length - a.applicants.length),
);

// 4. 기존 필터링 로직을 적용합니다.
const applyFilters = (data: ScoreSheetType[]) => {
Expand All @@ -73,12 +64,8 @@ const ScorePageContent = () => {
return result;
};

return {
firstChoice: applyFilters(sortedData.firstChoice),
secondChoice: applyFilters(sortedData.secondChoice),
thirdChoice: applyFilters(sortedData.thirdChoice),
};
}, [scoreResponseData, regionFilter, searchValue]);
return sortedData.map(applyFilters);
}, [scoreChoices, regionFilter, searchValue]);

// (이하 코드는 동일)
const handleSearch = (event: FormEvent) => {
Expand All @@ -102,19 +89,18 @@ const ScorePageContent = () => {
setSearchActive(true);
};

const getScoreSheet = () => {
switch (preference) {
case "1순위":
return filteredAndSortedData.firstChoice;
case "2순위":
return filteredAndSortedData.secondChoice;
case "3순위":
return filteredAndSortedData.thirdChoice;
default:
return [];
}
const handlePreferenceChange = (nextPreference: string) => {
const nextIndex = preferenceChoices.indexOf(nextPreference);
setPreferenceIndex(nextIndex >= 0 ? nextIndex : 0);
};
const scoreSheets = getScoreSheet();

const selectedPreference = preferenceChoices[preferenceIndex] ?? preferenceChoices[0] ?? "1순위";
const scoreSheets = filteredAndSortedData[preferenceIndex] ?? [];

useEffect(() => {
if (preferenceIndex < preferenceChoices.length) return;
setPreferenceIndex(0);
}, [preferenceChoices.length, preferenceIndex]);

useEffect(() => {
if (isLoading) return;
Expand All @@ -141,7 +127,7 @@ const ScorePageContent = () => {
) : (
<>
<div className="mt-4 rounded-lg bg-white px-2 shadow-sdwB">
<Tab choices={PREFERENCE_CHOICE} choice={preference} setChoice={setPreference} />
<Tab choices={preferenceChoices} choice={selectedPreference} setChoice={handlePreferenceChange} />
</div>
<ButtonTab
choices={REGIONS_KO}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ type ApplicationBottomActionBarProps = {

const ApplicationBottomActionBar = ({ label, onClick }: ApplicationBottomActionBarProps) => {
return (
<div className="fixed bottom-14 w-full max-w-app bg-white">
<div className="mb-[37px] px-5">
<BlockBtn onClick={onClick}>{label}</BlockBtn>
</div>
<div className="fixed bottom-[78px] left-1/2 w-full max-w-app -translate-x-1/2 px-5">
<BlockBtn onClick={onClick}>{label}</BlockBtn>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";
import { useMemo, useState } from "react";
import { usePostSubmitApplication } from "@/apis/applications";
import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores";
import { useUniversitySearch } from "@/apis/universities";
import TopDetailNavigation from "@/components/layout/TopDetailNavigation";
import ProgressBar from "@/components/ui/ProgressBar";
import { DEFAULT_MAX_CHOICE_COUNT, getHomeUniversityById } from "@/constants/university";
import { showIconToast } from "@/lib/toast/showIconToast";
import useAuthStore from "@/lib/zustand/useAuthStore";
import type { ListUniversity } from "@/types/university";
import ConfirmStep from "./ConfirmStep";
import DoneStep from "./DoneStep";
Expand All @@ -16,11 +18,22 @@ import GpaStep from "./GpaStep";
import LanguageStep from "./LanguageStep";
import UniversityStep from "./UniversityStep";

const APPLY_PROGRESS_TOTAL_STEPS = 5;

const ApplyPageContent = () => {
const router = useRouter();
const homeUniversityId = useAuthStore((state) => state.homeUniversityId);
const [step, setStep] = useState<number>(1);
const maxChoiceCount = getHomeUniversityById(homeUniversityId)?.maxChoiceCount ?? DEFAULT_MAX_CHOICE_COUNT;
const universitySearchOptions = useMemo(
() => ({
useDefaultTermId: true,
homeUniversityId: homeUniversityId ?? undefined,
}),
[homeUniversityId],
);

const { data: universityList = [] } = useUniversitySearch("", undefined, { useDefaultTermId: true });
const { data: universityList = [] } = useUniversitySearch("", undefined, universitySearchOptions);
const { data: gpaScoreList = [] } = useGetMyGpaScore();
const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore();
const { mutate: postSubmitApplication } = usePostSubmitApplication({
Expand Down Expand Up @@ -54,7 +67,11 @@ const ApplyPageContent = () => {
return;
}

if (curUniversityList.length === 0 || curUniversityList[0] === 0) {
const selectedUniversityIds = curUniversityList
.filter((universityId) => Number.isInteger(universityId) && universityId > 0)
.slice(0, maxChoiceCount);
Comment thread
yoonc01 marked this conversation as resolved.

if (selectedUniversityIds.length === 0) {
showIconToast("logo", "대학교를 선택해주세요.");
return;
}
Expand All @@ -63,19 +80,22 @@ const ApplyPageContent = () => {
gpaScoreId: curGpaScore,
languageTestScoreId: curLanguageTestScore,
universityChoiceRequest: {
firstChoiceUniversityId: curUniversityList[0] || null,
secondChoiceUniversityId: curUniversityList[1] || null,
thirdChoiceUniversityId: curUniversityList[2] || null,
choices: selectedUniversityIds,
},
});
};

const isDataExist = gpaScoreList.length === 0 || languageTestScoreList.length === 0;
const hasSelectedUniversity = curUniversityList.some((universityId) => universityId > 0);
const progressStep = step === 3 && hasSelectedUniversity ? APPLY_PROGRESS_TOTAL_STEPS : step + 1;

return (
<>
<TopDetailNavigation title="지원하기" handleBack={goPrevStep} />
<div className="mt-1 px-5">
{(step === 1 || step === 2 || step === 3) && <ProgressBar currentStep={step} totalSteps={3} />}
{(step === 1 || step === 2 || step === 3) && (
<ProgressBar currentStep={progressStep} totalSteps={APPLY_PROGRESS_TOTAL_STEPS} />
)}
</div>
{isDataExist ? (
<EmptyGPA />
Expand All @@ -102,6 +122,7 @@ const ApplyPageContent = () => {
universityList={universityList}
curUniversityList={curUniversityList}
setCurUniversityList={setCurUniversityList}
maxChoiceCount={maxChoiceCount}
onNext={goNextStep}
/>
)}
Expand Down
63 changes: 40 additions & 23 deletions apps/web/src/app/university/application/apply/ConfirmStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,58 @@ import clsx from "clsx";
import { IconCheck } from "@/public/svgs/mentor";
import type { ListUniversity } from "@/types/university";
import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar";
import ApplicationSectionTitle from "../_components/ApplicationSectionTitle";

type ConfirmStepProps = {
universityList: ListUniversity[];
onNext: () => void;
};

type ConfirmUniversityCardProps = {
universityList: ListUniversity[];
};

const ConfirmUniversityCard = ({ universityList }: ConfirmUniversityCardProps) => {
if (universityList.length === 0) return null;

return (
<div className="mt-10 rounded-lg border border-secondary bg-white px-5 py-6 shadow-[0_0_5px_rgba(0,0,0,0.25)]">
{universityList.map((university, index) => (
<div
key={university.id}
className={clsx(
"flex items-center justify-between gap-4 py-[14px]",
index === 0 && "pt-0",
index === universityList.length - 1 && "pb-0",
index < universityList.length - 1 && "border-b border-k-200",
)}
>
<span className="shrink-0 text-k-900 typo-sb-8">{index + 1}지망</span>
<span className="min-w-0 text-right text-primary typo-bold-5">
[{university.country}] {university.koreanName}
</span>
</div>
))}
</div>
);
};

const ConfirmStep = ({ universityList, onNext }: ConfirmStepProps) => {
return (
<div className="my-5 px-5 pb-40">
<div className="px-5 pb-40 pt-[76px]">
<div className="flex items-center justify-center">
<IconCheck />
</div>
<ApplicationSectionTitle
className="mt-4 text-center"
title="지원 내용을 확인해주세요"
description="제출 후에는 성적을 변경할 수 없습니다. 선택한 학교를 한 번 더 확인해주세요."
/>

<div className="mt-5 rounded-lg bg-white p-4 shadow-sdwB">
<div className="space-y-2">
{universityList.map((university, index) => (
<div
key={university.id}
className={clsx(
"flex items-center justify-between px-4 py-4 typo-regular-2",
index < universityList.length - 1 && "border-b border-k-50",
)}
>
<span className="text-k-500 typo-medium-2">{index + 1}지망</span>
<span className="text-primary typo-sb-9">{university.koreanName}</span>
</div>
))}
</div>

<div className="mt-1 text-center">
<h2 className="text-k-800 typo-bold-5">
<span className="text-secondary">지원</span> 확인하기
</h2>
<p className="mt-1 whitespace-pre-line text-k-600 typo-medium-3">
{"지원은 총 3번만 수정 가능하며,\n제출 완료 후 성적을 변경 하실 수 없습니다."}
</p>
</div>

<ConfirmUniversityCard universityList={universityList} />
<ApplicationBottomActionBar label="제출하기" onClick={onNext} />
</div>
);
Expand Down
Loading
Loading