Skip to content
172 changes: 172 additions & 0 deletions src/app/application/ScorePageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";

import { getApplicationListApi, getCompetitorsApplicationListApi } from "@/services/application";

import CloudSpinnerPage from "@/components/loading/CloudSpinnerPage";
import ConfirmCancelModal from "@/components/modal/ConfirmCancelModal";
import ButtonTab from "@/components/ui/ButtonTab";
import Tab from "@/components/ui/Tab";

import ScoreSearchBar from "./ScoreSearchBar";
import ScoreSearchField from "./ScoreSearchField";
import ScoreSheets from "./ScoreSheets";

import { REGIONS_KO } from "@/constants/university";
import { ApplicationListResponse } from "@/types/application";
import { RegionKo } from "@/types/university";

const PREFERENCE_CHOICE: string[] = ["1순위", "2순위", "3순위"];

const ScorePageContent = () => {
const router = useRouter();
const [loading, setLoading] = useState<boolean>(true);
// 검색
const [searchActive, setSearchActive] = useState<boolean>(false); // 검색 창 활성화 여부
const searchRef = useRef<HTMLInputElement>(null);
// 점수 데이터
const [scoreData, setScoreData] = useState<ApplicationListResponse>({
firstChoice: [],
secondChoice: [],
thirdChoice: [],
});
const [filteredScoreData, setFilteredScoreData] = useState<ApplicationListResponse>({
firstChoice: [],
secondChoice: [],
thirdChoice: [],
});
const [preference, setPreference] = useState<"1순위" | "2순위" | "3순위">("1순위");
const [filter, setFilter] = useState<RegionKo | "">("");

const [showNeedApply, setShowNeedApply] = useState<boolean>(false);

useEffect(() => {
const fetchData = async () => {
try {
if (true) {
const scoreResponse = await getCompetitorsApplicationListApi();

const scoreResponseData = scoreResponse.data;
scoreResponseData.firstChoice.sort((a, b) => b.applicants.length - a.applicants.length);
scoreResponseData.secondChoice.sort((a, b) => b.applicants.length - a.applicants.length);
scoreResponseData.thirdChoice.sort((a, b) => b.applicants.length - a.applicants.length);
setScoreData(scoreResponseData);
setFilteredScoreData(scoreResponseData);
}
} catch (err) {
if (err.response) {
if (err.response.status === 404) {
setShowNeedApply(true);
} else if (err.response.status === 401 || err.response.status === 403) {
alert("로그인이 필요합니다");
document.location.href = "/login";
} else {
alert(err.response.data?.message);
}
} else {
console.error("Error", err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
}, []);

const handleSearch = (event: React.FormEvent) => {
event.preventDefault();
const keyWord = searchRef.current?.value || "";
setFilter("");
setFilteredScoreData(
keyWord
? {
firstChoice: scoreData.firstChoice.filter((sheet) => sheet.koreanName.includes(keyWord)),
secondChoice: scoreData.secondChoice.filter((sheet) => sheet.koreanName.includes(keyWord)),
thirdChoice: scoreData.thirdChoice.filter((sheet) => sheet.koreanName.includes(keyWord)),
}
: scoreData,
);
setSearchActive(false);
};

const handleSearchField = (keyWord: string) => {
if (searchRef.current) {
searchRef.current.value = keyWord;
}
};

const handleSearchClick = () => {
setSearchActive(true);
};

const hotKeyWords = ["RMIT", "오스트라바", "칼스루에", "그라츠", "추오", "프라하", "보라스", "빈", "메모리얼"];

useEffect(() => {
if (filter) {
setFilteredScoreData({
firstChoice: scoreData.firstChoice.filter((sheet) => sheet.region === filter),
secondChoice: scoreData.secondChoice.filter((sheet) => sheet.region === filter),
thirdChoice: scoreData.thirdChoice.filter((sheet) => sheet.region === filter),
});
} else {
setFilteredScoreData(scoreData);
}
}, [filter, scoreData]);

if (loading) {
return <CloudSpinnerPage />;
}

const getScoreSheet = () => {
if (preference === "1순위") {
return filteredScoreData.firstChoice;
}
if (preference === "2순위") {
return filteredScoreData.secondChoice;
}
if (preference === "3순위") {
return filteredScoreData.thirdChoice;
}
return [];
};

if (searchActive) {
return (
<>
<ScoreSearchBar textRef={searchRef} searchHandler={handleSearch} onClick={() => {}} />
<ScoreSearchField
keyWords={hotKeyWords}
setKeyWord={(e) => {
handleSearchField(e);
}}
/>
</>
);
}

return (
<>
<ScoreSearchBar onClick={handleSearchClick} textRef={searchRef} searchHandler={handleSearch} />
<Tab choices={PREFERENCE_CHOICE} choice={preference} setChoice={setPreference} />
<ButtonTab choices={REGIONS_KO} choice={filter} setChoice={setFilter} style={{ padding: "10px 0 10px 18px" }} />
<ScoreSheets scoreSheets={getScoreSheet()} />
<ConfirmCancelModal
isOpen={showNeedApply}
handleCancel={() => {
router.push("/");
}}
handleConfirm={() => {
router.push("/application/apply");
}}
title=""
content={"점수 공유현황을 확인하려면 지원절차를\n진행해주세요."}
cancelText="확인"
approveText="학교 지원하기"
/>
</>
);
};

export default ScorePageContent;
178 changes: 178 additions & 0 deletions src/app/application/apply/ApplyPageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

import { postApplicationApi } from "@/services/application";
import { getMyGpaScoreApi, getMyLanguageTestScoreApi } from "@/services/score";
import { getUniversityListPublicApi } from "@/services/university";

import TopDetailNavigation from "@/components/layout/TopDetailNavigation";
import ProgressBar from "@/components/ui/ProgressBar";

import ConfirmStep from "./ConfirmStep";
import DoneStep from "./DoneStep";
import GpaStep from "./GpaStep";
import LanguageStep from "./LanguageStep";
import UniversityStep from "./UniversityStep";

import { GpaScore, LanguageTestScore } from "@/types/score";
import { ListUniversity } from "@/types/university";

const ApplyPageContent = () => {
const router = useRouter();
const [step, setStep] = useState<number>(1);

const [languageTestScoreList, setLanguageTestScoreList] = useState<LanguageTestScore[]>([]);
const [gpaScoreList, setGpaScoreList] = useState<GpaScore[]>([]);
const [universityList, setUniversityList] = useState<ListUniversity[]>([]);

const [curLanguageTestScore, setCurLanguageTestScore] = useState<number | null>(null);
const [curGpaScore, setCurGpaScore] = useState<number | null>(null);
const [curUniversityList, setCurUniversityList] = useState<number[]>([]);

const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

useEffect(() => {
const fetchAll = async () => {
try {
const [gpaRes, languageRes, universityRes] = await Promise.all([
getMyGpaScoreApi(),
getMyLanguageTestScoreApi(),
getUniversityListPublicApi(),
]);
setGpaScoreList(
gpaRes.data.gpaScoreStatusResponseList.filter((score: GpaScore) => score.verifyStatus === "APPROVED"),
);
setLanguageTestScoreList(
languageRes.data.languageTestScoreStatusResponseList.filter(
(score: LanguageTestScore) => score.verifyStatus === "APPROVED",
),
);

// 대학명을 지역/나라, 대학명 가나다 순으로 정렬합니다
const sortedUniversityList = [...universityRes.data].sort((a, b) => {
// 1) region 비교
const regionCompare = a.region.localeCompare(b.region);
if (regionCompare !== 0) return regionCompare;

// 2) country 비교
const countryCompare = a.country.localeCompare(b.country);
if (countryCompare !== 0) return countryCompare;

// 3) 같은 region, country라면 대학명을 비교(가나다 순)
return a.koreanName.localeCompare(b.koreanName);
});
setUniversityList(sortedUniversityList);
} catch (err) {
if (err.response) {
console.error("Axios response error", err.response);
if (err.response.status === 401 || err.response.status === 403) {
alert("로그인이 필요합니다");
document.location.href = "/login";
} else {
alert(err.response.data?.message);
}
} else {
console.error("Error", err.message);
alert(err.message);
}
}
};
fetchAll();
}, []);

// 다음 스텝으로 넘어가기
const goNextStep = () => setStep((prev) => prev + 1);
// 이전 스텝으로 돌아가기
const goPrevStep = () => {
if (step === 1) {
router.back();
}
setStep((prev) => prev - 1);
};

const handleSubmit = async () => {
if (!curGpaScore) {
alert("GPA를 선택해주세요.");
return;
}

if (!curLanguageTestScore) {
alert("어학성적을 선택해주세요.");
return;
}

if (curUniversityList.length === 0 || curUniversityList[0] === 0) {
alert("대학교를 선택해주세요.");
return;
}

if (isSubmitting) return;
setIsSubmitting(true); // TODO: 현재 임시 submit 처리, 이후에 통합 처리 추가
try {
await postApplicationApi({
gpaScoreId: curGpaScore,
languageTestScoreId: curLanguageTestScore,
universityChoiceRequest: {
firstChoiceUniversityId: curUniversityList[0] || null,
secondChoiceUniversityId: curUniversityList[1] || null,
thirdChoiceUniversityId: curUniversityList[2] || null,
},
});
setStep(99);
} catch (err) {
alert(err.response.data.message);
} finally {
setIsSubmitting(false);
}
};

return (
<>
<TopDetailNavigation title="지원하기" handleBack={goPrevStep} />
<div className="px-5">
{(step === 1 || step === 2 || step === 3) && <ProgressBar currentStep={step} totalSteps={3} />}
</div>
{step === 1 && (
<LanguageStep
languageTestScoreList={languageTestScoreList}
curLanguageTestScore={curLanguageTestScore}
setCurLanguageTestScore={setCurLanguageTestScore}
onNext={goNextStep}
/>
)}
{step === 2 && (
<GpaStep
gpaScoreList={gpaScoreList}
curGpaScore={curGpaScore}
setCurGpaScore={setCurGpaScore}
onNext={goNextStep}
/>
)}
{step === 3 && (
<UniversityStep
universityList={universityList}
curUniversityList={curUniversityList}
setCurUniversityList={setCurUniversityList}
onNext={goNextStep}
/>
)}
{step === 4 && (
<ConfirmStep
languageTestScore={languageTestScoreList.find((score) => score.id === curLanguageTestScore)}
gpaScore={gpaScoreList.find((score) => score.id === curGpaScore)}
universityList={
curUniversityList
.map((id) => universityList.find((university) => university.id === id))
.filter(Boolean) as ListUniversity[]
}
onNext={handleSubmit}
/>
)}
{step === 99 && <DoneStep />}
</>
);
};

export default ApplyPageContent;
Loading