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
28 changes: 12 additions & 16 deletions apps/web/src/apis/apply/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import axios from "axios";

import {
applicationStatusResponseSchema,
memberProfileResponseSchema,
memberMeResponseSchema,
memberProfileInitialStatusResponseSchema,
questionResponseSchema,
answersResponseSchema,
type ApplicationStatusResponseSchema,
type MemberProfileResponseSchema,
type MemberMeResponseSchema,
type MemberProfileInitialStatusResponseSchema,
type QuestionResponseSchema,
type AnswersResponseSchema,
Expand Down Expand Up @@ -39,18 +39,16 @@ export const applyApi = {
memberProfileInitialStatusResponseSchema,
),

getStatus: (email: string) => {
const params = new URLSearchParams({ email });
return httpClient.get<ApplicationStatusResponseSchema>(
`${API_ENDPOINT.applyStatus}?${params.toString()}`,
getStatus: () =>
httpClient.get<ApplicationStatusResponseSchema>(
API_ENDPOINT.applyStatus,
applicationStatusResponseSchema,
);
},
),

getProfile: () =>
httpClient.get<MemberProfileResponseSchema>(
API_ENDPOINT.memberProfile,
memberProfileResponseSchema,
getMe: () =>
httpClient.get<MemberMeResponseSchema>(
API_ENDPOINT.memberMe,
memberMeResponseSchema,
),

updateProfile: (data: MemberProfilePayload) =>
Expand All @@ -66,10 +64,8 @@ export const applyApi = {

getDraft: () => httpClient.get<AnswersResponseSchema>(API_ENDPOINT.draft, answersResponseSchema),

saveDraft: (jobFamily: JobFamily, answers: AnswersPayload) => {
const params = new URLSearchParams({ jobFamily });
return httpClient.post<null>(`${API_ENDPOINT.draft}?${params.toString()}`, answers);
},
saveDraft: (answers: AnswersPayload) =>
httpClient.post<null>(API_ENDPOINT.draft, answers),

deleteDraft: () => httpClient.delete<null>(API_ENDPOINT.draft),

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/apis/apply/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export {
export {
// 스키마
applicationStatusResponseSchema,
memberProfileResponseSchema,
memberMeResponseSchema,
memberProfileInitialStatusResponseSchema,
questionResponseSchema,
answersResponseSchema,
// 스키마 타입
type ApplicationStatusResponseSchema,
type MemberProfileResponseSchema,
type MemberMeResponseSchema,
type MemberProfileInitialStatusResponseSchema,
type QuestionResponseSchema,
type AnswersResponseSchema,
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/apis/apply/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const applyQueryKeys = {
all: ["apply"] as const,
status: {
all: () => [...applyQueryKeys.all, "status"] as const,
byEmail: (email: string) => [...applyQueryKeys.status.all(), "email", email] as const,
current: () => [...applyQueryKeys.status.all(), "current"] as const,
},
questions: {
all: () => [...applyQueryKeys.all, "questions"] as const,
Expand Down Expand Up @@ -49,15 +49,15 @@ export const applyMutationKeys = {

export const applyQueries = {
status: {
byEmail: (email: string) => ({
queryKey: applyQueryKeys.status.byEmail(email),
queryFn: () => applyApi.getStatus(email),
current: () => ({
queryKey: applyQueryKeys.status.current(),
queryFn: () => applyApi.getStatus(),
}),
},
profile: {
me: () => ({
queryKey: applyQueryKeys.profile.me(),
queryFn: applyApi.getProfile,
queryFn: applyApi.getMe,
}),
initialStatus: () => ({
queryKey: applyQueryKeys.profile.initialStatus(),
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/apis/apply/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,16 @@ export const interestedDomainSchema = z.enum([
"HR",
]);

export const memberProfileResponseSchema = z.object({
export const memberMeResponseSchema = z.object({
id: z.number(),
name: z.string(),
phoneNumber: z.string(),
careerDetails: careerDetailsSchema,
region: regionSchema,
experiencePeriod: experiencePeriodSchema,
interestedDomains: z.array(interestedDomainSchema),
});

export type MemberProfileResponseSchema = z.infer<typeof memberProfileResponseSchema>;
export type MemberMeResponseSchema = z.infer<typeof memberMeResponseSchema>;

export const questionInputTypeSchema = z.enum(["TEXT", "URL", "FILE", "SELECT"]);

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/constants/apiEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const API_ENDPOINT = {
verifyEmailCode: "/auth/code",
checkEmailExists: "/auth/login/exist",
applyMember: "/members/apply",
memberProfile: "/members/profile",
memberMe: "/members/me",
memberProfileInitialStatus: "/members/profile/initial/status",
pinLogin: "/auth/login/pin",
registerMember: "/members/apply",
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/constants/applyMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,16 @@ export const APPLY_DIALOG = {
),
primaryAction: "확인",
},
jobFamilyMismatch: (savedJobFamilyKorean: string, currentJobFamilyKorean: string) => ({
header: `${savedJobFamilyKorean} 지원서가 임시저장되어 있어요`,
body: (
<>
{currentJobFamilyKorean}로 새로 지원하시면
<br />
기존 {savedJobFamilyKorean} 지원서는 사라져요.
</>
),
primaryAction: `지원서 이어서 작성하기`,
secondaryAction: `새로 지원하기`,
}),
};
147 changes: 131 additions & 16 deletions apps/web/src/features/apply/steps/IdentityVerificationStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { useEffect, useState } from "react";
import { Controller } from "react-hook-form";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";

import { applyApi, type JobFamily } from "@/apis/apply";
import { APPLY_DIALOG, APPLY_MESSAGE } from "@/constants/applyMessages.tsx";
import { APPLY_TITLE } from "@/constants/applyPageData";
import { findJobFamilyOption, APPLY_TITLE } from "@/constants/applyPageData";
import { PATH } from "@/constants/path";
import { ApplyStepLayout } from "@/features/shared/components";
import { useCheckApplyStatusMutation, usePinLoginMutation } from "@/hooks/apply";
import {
useCheckApplyStatusMutation,
useDeleteDraftMutation,
useMemberProfileMutation,
usePinLoginMutation,
} from "@/hooks/apply";
import { useApplyEmailForm } from "@/hooks/useApplyEmailForm";
import { useApplyPinForm } from "@/hooks/useApplyPinForm";
import type { ContinueWritingFunnelSteps } from "@/types/funnel";
Expand All @@ -33,14 +39,21 @@ interface IdentityVerificationStepProps {
) => void;
}

export function IdentityVerificationStep({
context,
dispatch,
}: IdentityVerificationStepProps) {
interface JobFamilyMismatchDialog {
isOpen: boolean;
savedJobFamily: JobFamily | null;
}

export function IdentityVerificationStep({ context, dispatch }: IdentityVerificationStepProps) {
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [isSubmittedDialogOpen, setIsSubmittedDialogOpen] = useState(false);
const [mismatchDialog, setMismatchDialog] = useState<JobFamilyMismatchDialog>({
isOpen: false,
savedJobFamily: null,
});
const [verifiedEmail, setVerifiedEmail] = useState<string>("");

//PIN 재설정 후 돌아왔을 때 파라미터
const isPinResetSuccess = searchParams.get("pinReset") === "success";
Expand Down Expand Up @@ -72,20 +85,28 @@ export function IdentityVerificationStep({
toastController.basic("PIN 재설정 완료", "새로운 PIN을 입력해 본인 확인을 다시 진행해주세요.");

// 3. URL 파라미터 정리
setSearchParams(prev => {
prev.delete("pinReset");
prev.delete("email");
return prev;
}, { replace: true });
setSearchParams(
prev => {
prev.delete("pinReset");
prev.delete("email");
return prev;
},
{ replace: true },
);
}, [isPinResetSuccess, prefillEmail, setEmailValue, setSearchParams]);

const email = watchEmail("email");
const isFormValid = emailFormState.isValid && pinFormState.isValid;

const { mutate: checkApplyStatusMutate, isPending: isCheckingStatus } = useCheckApplyStatusMutation();
const [isChangingJobFamily, setIsChangingJobFamily] = useState(false);

const { mutate: checkApplyStatusMutate, isPending: isCheckingStatus } =
useCheckApplyStatusMutation();
const { mutateAsync: deleteDraftAsync } = useDeleteDraftMutation();
const { mutateAsync: updateProfileAsync } = useMemberProfileMutation();

const handleCheckApplyStatus = (userEmail: string) => {
checkApplyStatusMutate(userEmail, {
checkApplyStatusMutate(undefined, {
onSuccess: data => {
if (data.result === "PROFILE_NOT_REGISTERED") {
dispatch("goToProfile", userEmail);
Expand All @@ -97,9 +118,30 @@ export function IdentityVerificationStep({
return;
}

// CONTINUE (TEMP_SAVED 또는 JOINED)
toastController.positive(APPLY_MESSAGE.success.continueWriting);
dispatch("goToApply", userEmail);
// CONTINUE (TEMP_SAVED 또는 JOINED) → draft 확인
void applyApi
.getDraft()
.then(draft => {
// 파트 불일치 체크: draft에 저장된 jobFamily와 현재 접근한 jobFamily가 다른 경우
if (draft.jobFamily != null && draft.jobFamily !== context.jobFamily) {
setVerifiedEmail(userEmail);
setMismatchDialog({
isOpen: true,
savedJobFamily: draft.jobFamily,
});
return;
}

// 같은 파트 또는 draft 없음 → 이어서 작성
toastController.positive(APPLY_MESSAGE.success.continueWriting);
dispatch("goToApply", userEmail);
})
.catch((error: unknown) => {
// draft 조회 실패 시에도 이어서 작성 가능 (빈 폼으로 시작)
handleError(error, "임시저장 데이터 조회 실패");
toastController.positive(APPLY_MESSAGE.success.continueWriting);
dispatch("goToApply", userEmail);
});
},
onError: error => {
handleError(error, "지원 상태 확인 실패");
Expand Down Expand Up @@ -132,6 +174,56 @@ export function IdentityVerificationStep({
void navigate(`${PATH.resetPin}?returnTo=${encodeURIComponent(returnTo)}`);
};

// 파트 불일치 다이얼로그: "기존 파트 지원서 이어서 작성하기" 선택
const handleContinueSavedDraft = () => {
if (mismatchDialog.savedJobFamily) {
void navigate(`${PATH.applyContinue}/${mismatchDialog.savedJobFamily}`);
}
setMismatchDialog({ isOpen: false, savedJobFamily: null });
};

// 파트 불일치 다이얼로그: "새로운 파트로 지원하기" 선택
const handleStartNewApplication = async () => {
setIsChangingJobFamily(true);
setMismatchDialog({ isOpen: false, savedJobFamily: null });

try {
// 1. 현재 프로필 백업
const profile = await applyApi.getMe();

// 2. 기존 draft + 프로필 삭제
await deleteDraftAsync();

// 3. 프로필 복원 (새로운 jobFamily로)
await updateProfileAsync({
name: profile.name,
phoneNumber: "01012345678", // TODO: getMe 응답에 phoneNumber가 없어서 임시 처리
careerDetails: profile.careerDetails,
region: profile.region,
experiencePeriod: profile.experiencePeriod,
interestedDomains: profile.interestedDomains,
jobFamily: context.jobFamily,
});

// 4. 지원서 작성으로 이동
toastController.positive(APPLY_MESSAGE.success.continueWriting);
dispatch("goToApply", verifiedEmail);
} catch (error) {
handleError(error, "파트 변경 실패");
toastController.destructive("파트 변경에 실패했습니다. 다시 시도해주세요.");
} finally {
setIsChangingJobFamily(false);
}
};

// 다이얼로그 메시지 생성
const mismatchDialogContent = mismatchDialog.savedJobFamily
? APPLY_DIALOG.jobFamilyMismatch(
findJobFamilyOption(mismatchDialog.savedJobFamily).korean,
findJobFamilyOption(context.jobFamily).korean,
)
: null;

return (
<ApplyStepLayout
variant='auth'
Expand Down Expand Up @@ -213,6 +305,29 @@ export function IdentityVerificationStep({
onClick: () => setIsSubmittedDialogOpen(false),
}}
/>

{mismatchDialogContent && (
<Dialog
open={mismatchDialog.isOpen || isChangingJobFamily}
onOpenChange={open =>
!open &&
!isChangingJobFamily &&
setMismatchDialog({ isOpen: false, savedJobFamily: null })
}
header={mismatchDialogContent.header}
body={mismatchDialogContent.body}
primaryAction={{
children: mismatchDialogContent.primaryAction,
onClick: handleContinueSavedDraft,
disabled: isChangingJobFamily,
}}
secondaryAction={{
children: isChangingJobFamily ? "변경 중..." : mismatchDialogContent.secondaryAction,
onClick: () => void handleStartNewApplication(),
disabled: isChangingJobFamily,
}}
/>
)}
</ApplyStepLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,8 @@ export function RegistrationStep({ context, onNext, onBack }: RegistrationStepPr

//지원서 임시 저장
const handleSaveDraft = useCallback(() => {
saveDraftMutate({
jobFamily,
answers: { answers, portfolios: formattedPortfolios },
});
}, [saveDraftMutate, answers, formattedPortfolios, jobFamily]);
saveDraftMutate({ answers, portfolios: formattedPortfolios });
}, [saveDraftMutate, answers, formattedPortfolios]);

//지원서 제출
const handleSubmit = useCallback(() => {
Expand Down
Loading