Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import BlockBtn from "@/components/button/BlockBtn";
type ApplicationBottomActionBarProps = {
label: string;
onClick: () => void;
disabled?: boolean;
};

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

import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { usePostSubmitApplication } from "@/apis/applications";
import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores";
import { useUniversitySearch } from "@/apis/universities";
Expand Down Expand Up @@ -41,7 +41,8 @@ const ApplyPageContent = () => {
// TODO: 서버의 모학교 미인증 에러 코드가 확정되면 GPA 조회 실패 시 학교 인증으로 보내는 fallback을 추가한다.
const { data: gpaScoreData } = useGetMyGpaScore({ enabled: shouldFetchGpaScore });
const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore();
const { mutate: postSubmitApplication } = usePostSubmitApplication({
const isSubmitLockedRef = useRef(false);
const { mutate: postSubmitApplication, isPending: isSubmitApplicationPending } = usePostSubmitApplication({
onSuccess: () => {
setStep(99);
},
Expand Down Expand Up @@ -70,6 +71,10 @@ const ApplyPageContent = () => {
};

const handleSubmit = async () => {
if (isSubmitLockedRef.current || isSubmitApplicationPending) {
return;
}

if (curGpaScore === null) {
showIconToast("logo", "GPA를 선택해주세요.");
return;
Expand All @@ -89,13 +94,21 @@ const ApplyPageContent = () => {
return;
}

postSubmitApplication({
gpaScoreId: curGpaScore,
languageTestScoreId: curLanguageTestScore,
universityChoiceRequest: {
choices: selectedUniversityIds,
isSubmitLockedRef.current = true;
postSubmitApplication(
{
gpaScoreId: curGpaScore,
languageTestScoreId: curLanguageTestScore,
universityChoiceRequest: {
choices: selectedUniversityIds,
},
},
{
onSettled: () => {
isSubmitLockedRef.current = false;
},
},
});
);
};

const gpaScoreList = gpaScoreData?.gpaScoreStatusResponseList ?? [];
Expand Down Expand Up @@ -154,6 +167,7 @@ const ApplyPageContent = () => {
.filter(Boolean) as ListUniversity[]
}
onNext={handleSubmit}
isSubmitting={isSubmitApplicationPending}
/>
)}
{step === 99 && <DoneStep />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBa
type ConfirmStepProps = {
universityList: ListUniversity[];
onNext: () => void;
isSubmitting?: boolean;
};

type ConfirmUniversityCardProps = {
Expand Down Expand Up @@ -38,7 +39,7 @@ const ConfirmUniversityCard = ({ universityList }: ConfirmUniversityCardProps) =
);
};

const ConfirmStep = ({ universityList, onNext }: ConfirmStepProps) => {
const ConfirmStep = ({ universityList, onNext, isSubmitting = false }: ConfirmStepProps) => {
return (
<div className="px-5 pb-40 pt-[76px]">
<div className="flex items-center justify-center">
Expand All @@ -55,7 +56,7 @@ const ConfirmStep = ({ universityList, onNext }: ConfirmStepProps) => {
</div>

<ConfirmUniversityCard universityList={universityList} />
<ApplicationBottomActionBar label="제출하기" onClick={onNext} />
<ApplicationBottomActionBar label="제출하기" onClick={onNext} disabled={isSubmitting} />
</div>
);
};
Expand Down
45 changes: 28 additions & 17 deletions apps/web/src/app/university/score/submit/gpa/GpaSubmitForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import clsx from "clsx";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { Suspense, useEffect, useRef, useState } from "react";
import { Controller, type SubmitHandler, useForm } from "react-hook-form";
import { usePostGpaScore } from "@/apis/Scores";
import { getSchoolEmailVerificationPath } from "@/app/my/school-email/_lib/returnTo";
Expand All @@ -22,7 +22,8 @@ const GpaSubmitForm = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const [showResult, setShowResult] = useState(false);
const [submittedData, setSubmittedData] = useState<GpaFormData | null>(null);
const { mutateAsync: postGpaScore } = usePostGpaScore();
const isSubmitLockedRef = useRef(false);
const { mutateAsync: postGpaScore, isPending: isPostGpaScorePending } = usePostGpaScore();

// 2. react-hook-form 설정
const {
Expand All @@ -31,12 +32,13 @@ const GpaSubmitForm = () => {
control,
watch,
reset,
formState: { errors, isValid },
formState: { errors, isSubmitting, isValid },
} = useForm<GpaFormData>({
resolver: zodResolver(gpaSchema),
mode: "onChange",
});
const selectedFile = watch("file");
const isSubmitDisabled = !isValid || isSubmitting || isPostGpaScorePending;

useEffect(() => {
if (!isAuthInitialized || !isAuthenticated || homeUniversityId !== null) {
Expand All @@ -52,18 +54,27 @@ const GpaSubmitForm = () => {

// 3. 폼 제출 핸들러
const onSubmit: SubmitHandler<GpaFormData> = async (data) => {
await postGpaScore({
gpaScoreRequest: {
gpa: Number(data.gpa),
gpaCriteria: Number(data.gpaCriteria),
issueDate: "2025-01-01", // TODO: 실제 날짜 데이터로 변경
},
file: data.file[0],
});
// 성공 시, 결과 화면에 보여줄 데이터를 저장하고 상태 변경
setSubmittedData(data);
setShowResult(true);
reset();
if (isSubmitLockedRef.current) return;

isSubmitLockedRef.current = true;
try {
await postGpaScore({
gpaScoreRequest: {
gpa: Number(data.gpa),
gpaCriteria: Number(data.gpaCriteria),
issueDate: "2025-01-01", // TODO: 실제 날짜 데이터로 변경
},
file: data.file[0],
});
// 성공 시, 결과 화면에 보여줄 데이터를 저장하고 상태 변경
setSubmittedData(data);
setShowResult(true);
reset();
} catch (_error) {
// 실패 토스트는 React Query 전역 onError에서 단일 처리
} finally {
isSubmitLockedRef.current = false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

if (showResult && submittedData) {
Expand Down Expand Up @@ -175,9 +186,9 @@ const GpaSubmitForm = () => {
<button
className={clsx(
"mb-10 w-full rounded-lg py-4 text-white typo-sb-9",
isValid ? "bg-primary" : "cursor-not-allowed bg-k-100",
isSubmitDisabled ? "cursor-not-allowed bg-k-100" : "bg-primary",
)}
disabled={!isValid}
disabled={isSubmitDisabled}
>
다음
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import clsx from "clsx";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Suspense, useState } from "react";
import { Suspense, useRef, useState } from "react";
import { Controller, type SubmitHandler, useForm } from "react-hook-form";
import { usePostLanguageTestScore } from "@/apis/Scores";
import SubmitLinkTab from "@/components/score/SubmitLinkTab";
Expand All @@ -17,7 +17,8 @@ import { type LanguageTestFormData, languageTestSchema } from "./_lib/schema";
const LanguageTestSubmitForm = () => {
const router = useRouter();
const [showResult, setShowResult] = useState(false);
const { mutateAsync: postLanguageTestScore } = usePostLanguageTestScore();
const isSubmitLockedRef = useRef(false);
const { mutateAsync: postLanguageTestScore, isPending: isPostLanguageTestScorePending } = usePostLanguageTestScore();
const [submittedData, setSubmittedData] = useState<LanguageTestFormData | null>(null);

// 3. react-hook-form 설정
Expand All @@ -27,16 +28,20 @@ const LanguageTestSubmitForm = () => {
control,
watch,
reset,
formState: { errors, isValid },
formState: { errors, isSubmitting, isValid },
} = useForm<LanguageTestFormData>({
resolver: zodResolver(languageTestSchema),
mode: "onChange", // 입력값이 변경될 때마다 유효성 검사
});

const selectedFile = watch("file");
const isSubmitDisabled = !isValid || isSubmitting || isPostLanguageTestScorePending;

// 4. 폼 제출 핸들러
const onSubmit: SubmitHandler<LanguageTestFormData> = async (data) => {
if (isSubmitLockedRef.current) return;

isSubmitLockedRef.current = true;
try {
await postLanguageTestScore({
languageTestScoreRequest: {
Expand All @@ -53,6 +58,8 @@ const LanguageTestSubmitForm = () => {
setSubmittedData(data);
} catch (_error) {
// 실패 토스트는 React Query 전역 onError에서 단일 처리
} finally {
isSubmitLockedRef.current = false;
}
};

Expand Down Expand Up @@ -162,9 +169,9 @@ const LanguageTestSubmitForm = () => {
<button
className={clsx(
"mb-10 w-full rounded-lg py-4 text-white typo-sb-9",
isValid ? "bg-primary" : "cursor-not-allowed bg-k-100",
isSubmitDisabled ? "cursor-not-allowed bg-k-100" : "bg-primary",
)}
disabled={!isValid}
disabled={isSubmitDisabled}
>
다음
</button>
Expand Down
Loading