Skip to content

Commit 2924f6a

Browse files
authored
hotfix: 임시 저장 지원서를 직군 전체가 공유하는 문제 (#342)
* fix: 임시 회원의 최초 프로필 정보 등록 여부 확인 API에 잘못 적용되어있는 Params를 제거합니다. * fix: 임시 저장 API 호출 시 잘못 작성된 params를 제거합니다. * fix: 서버 draft는 유지하고 클라이언트의 draft 값만 조정하도록 변경합니다. - useDeleteDraftMutation는 사용자 프로필까지 삭제를 하기 때문 * feat: 다른 지원서에 접근했을 때 예외 케이스를 추가합니다. * feat: 현재 로그인한 회원 프로필 조회 API를 추가합니다. * feat: 파트 변경 시 수행되는 로직을 추가합니다. 1. 현재 프로필 백업 2. 기존 draft + 프로필 삭제 3. 백업된 프로필로 현재 프로필 복원 4. 지원서 작성하기 페이지로 이동 * feat: 상수 데이터를 수정합니다. * feat: profile 등록 시 임시 데이터를 추가합니다. * fix(JDS): input 태그를 사용하는 컴포넌트에 브라우저 autofill 스타일을 추가합니다.
1 parent 86c3148 commit 2924f6a

File tree

13 files changed

+189
-73
lines changed

13 files changed

+189
-73
lines changed

apps/web/src/apis/apply/api.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import axios from "axios";
33

44
import {
55
applicationStatusResponseSchema,
6-
memberProfileResponseSchema,
6+
memberMeResponseSchema,
77
memberProfileInitialStatusResponseSchema,
88
questionResponseSchema,
99
answersResponseSchema,
1010
type ApplicationStatusResponseSchema,
11-
type MemberProfileResponseSchema,
11+
type MemberMeResponseSchema,
1212
type MemberProfileInitialStatusResponseSchema,
1313
type QuestionResponseSchema,
1414
type AnswersResponseSchema,
@@ -39,18 +39,16 @@ export const applyApi = {
3939
memberProfileInitialStatusResponseSchema,
4040
),
4141

42-
getStatus: (email: string) => {
43-
const params = new URLSearchParams({ email });
44-
return httpClient.get<ApplicationStatusResponseSchema>(
45-
`${API_ENDPOINT.applyStatus}?${params.toString()}`,
42+
getStatus: () =>
43+
httpClient.get<ApplicationStatusResponseSchema>(
44+
API_ENDPOINT.applyStatus,
4645
applicationStatusResponseSchema,
47-
);
48-
},
46+
),
4947

50-
getProfile: () =>
51-
httpClient.get<MemberProfileResponseSchema>(
52-
API_ENDPOINT.memberProfile,
53-
memberProfileResponseSchema,
48+
getMe: () =>
49+
httpClient.get<MemberMeResponseSchema>(
50+
API_ENDPOINT.memberMe,
51+
memberMeResponseSchema,
5452
),
5553

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

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

69-
saveDraft: (jobFamily: JobFamily, answers: AnswersPayload) => {
70-
const params = new URLSearchParams({ jobFamily });
71-
return httpClient.post<null>(`${API_ENDPOINT.draft}?${params.toString()}`, answers);
72-
},
67+
saveDraft: (answers: AnswersPayload) =>
68+
httpClient.post<null>(API_ENDPOINT.draft, answers),
7369

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

apps/web/src/apis/apply/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ export {
1010
export {
1111
// 스키마
1212
applicationStatusResponseSchema,
13-
memberProfileResponseSchema,
13+
memberMeResponseSchema,
1414
memberProfileInitialStatusResponseSchema,
1515
questionResponseSchema,
1616
answersResponseSchema,
1717
// 스키마 타입
1818
type ApplicationStatusResponseSchema,
19-
type MemberProfileResponseSchema,
19+
type MemberMeResponseSchema,
2020
type MemberProfileInitialStatusResponseSchema,
2121
type QuestionResponseSchema,
2222
type AnswersResponseSchema,

apps/web/src/apis/apply/queryKeys.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const applyQueryKeys = {
77
all: ["apply"] as const,
88
status: {
99
all: () => [...applyQueryKeys.all, "status"] as const,
10-
byEmail: (email: string) => [...applyQueryKeys.status.all(), "email", email] as const,
10+
current: () => [...applyQueryKeys.status.all(), "current"] as const,
1111
},
1212
questions: {
1313
all: () => [...applyQueryKeys.all, "questions"] as const,
@@ -49,15 +49,15 @@ export const applyMutationKeys = {
4949

5050
export const applyQueries = {
5151
status: {
52-
byEmail: (email: string) => ({
53-
queryKey: applyQueryKeys.status.byEmail(email),
54-
queryFn: () => applyApi.getStatus(email),
52+
current: () => ({
53+
queryKey: applyQueryKeys.status.current(),
54+
queryFn: () => applyApi.getStatus(),
5555
}),
5656
},
5757
profile: {
5858
me: () => ({
5959
queryKey: applyQueryKeys.profile.me(),
60-
queryFn: applyApi.getProfile,
60+
queryFn: applyApi.getMe,
6161
}),
6262
initialStatus: () => ({
6363
queryKey: applyQueryKeys.profile.initialStatus(),

apps/web/src/apis/apply/schemas.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ export const interestedDomainSchema = z.enum([
6565
"HR",
6666
]);
6767

68-
export const memberProfileResponseSchema = z.object({
68+
export const memberMeResponseSchema = z.object({
69+
id: z.number(),
6970
name: z.string(),
70-
phoneNumber: z.string(),
7171
careerDetails: careerDetailsSchema,
7272
region: regionSchema,
7373
experiencePeriod: experiencePeriodSchema,
7474
interestedDomains: z.array(interestedDomainSchema),
7575
});
7676

77-
export type MemberProfileResponseSchema = z.infer<typeof memberProfileResponseSchema>;
77+
export type MemberMeResponseSchema = z.infer<typeof memberMeResponseSchema>;
7878

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

apps/web/src/constants/apiEndpoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const API_ENDPOINT = {
55
verifyEmailCode: "/auth/code",
66
checkEmailExists: "/auth/login/exist",
77
applyMember: "/members/apply",
8-
memberProfile: "/members/profile",
8+
memberMe: "/members/me",
99
memberProfileInitialStatus: "/members/profile/initial/status",
1010
pinLogin: "/auth/login/pin",
1111
registerMember: "/members/apply",

apps/web/src/constants/applyMessages.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,16 @@ export const APPLY_DIALOG = {
6767
),
6868
primaryAction: "확인",
6969
},
70+
jobFamilyMismatch: (savedJobFamilyKorean: string, currentJobFamilyKorean: string) => ({
71+
header: `${savedJobFamilyKorean} 지원서가 임시저장되어 있어요`,
72+
body: (
73+
<>
74+
{currentJobFamilyKorean}로 새로 지원하시면
75+
<br />
76+
기존 {savedJobFamilyKorean} 지원서는 사라져요.
77+
</>
78+
),
79+
primaryAction: `지원서 이어서 작성하기`,
80+
secondaryAction: `새로 지원하기`,
81+
}),
7082
};

apps/web/src/features/apply/steps/IdentityVerificationStep.tsx

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { useEffect, useState } from "react";
33
import { Controller } from "react-hook-form";
44
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
55

6+
import { applyApi, type JobFamily } from "@/apis/apply";
67
import { APPLY_DIALOG, APPLY_MESSAGE } from "@/constants/applyMessages.tsx";
7-
import { APPLY_TITLE } from "@/constants/applyPageData";
8+
import { findJobFamilyOption, APPLY_TITLE } from "@/constants/applyPageData";
89
import { PATH } from "@/constants/path";
910
import { ApplyStepLayout } from "@/features/shared/components";
10-
import { useCheckApplyStatusMutation, usePinLoginMutation } from "@/hooks/apply";
11+
import {
12+
useCheckApplyStatusMutation,
13+
useDeleteDraftMutation,
14+
useMemberProfileMutation,
15+
usePinLoginMutation,
16+
} from "@/hooks/apply";
1117
import { useApplyEmailForm } from "@/hooks/useApplyEmailForm";
1218
import { useApplyPinForm } from "@/hooks/useApplyPinForm";
1319
import type { ContinueWritingFunnelSteps } from "@/types/funnel";
@@ -33,14 +39,21 @@ interface IdentityVerificationStepProps {
3339
) => void;
3440
}
3541

36-
export function IdentityVerificationStep({
37-
context,
38-
dispatch,
39-
}: IdentityVerificationStepProps) {
42+
interface JobFamilyMismatchDialog {
43+
isOpen: boolean;
44+
savedJobFamily: JobFamily | null;
45+
}
46+
47+
export function IdentityVerificationStep({ context, dispatch }: IdentityVerificationStepProps) {
4048
const location = useLocation();
4149
const navigate = useNavigate();
4250
const [searchParams, setSearchParams] = useSearchParams();
4351
const [isSubmittedDialogOpen, setIsSubmittedDialogOpen] = useState(false);
52+
const [mismatchDialog, setMismatchDialog] = useState<JobFamilyMismatchDialog>({
53+
isOpen: false,
54+
savedJobFamily: null,
55+
});
56+
const [verifiedEmail, setVerifiedEmail] = useState<string>("");
4457

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

7487
// 3. URL 파라미터 정리
75-
setSearchParams(prev => {
76-
prev.delete("pinReset");
77-
prev.delete("email");
78-
return prev;
79-
}, { replace: true });
88+
setSearchParams(
89+
prev => {
90+
prev.delete("pinReset");
91+
prev.delete("email");
92+
return prev;
93+
},
94+
{ replace: true },
95+
);
8096
}, [isPinResetSuccess, prefillEmail, setEmailValue, setSearchParams]);
8197

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

85-
const { mutate: checkApplyStatusMutate, isPending: isCheckingStatus } = useCheckApplyStatusMutation();
101+
const [isChangingJobFamily, setIsChangingJobFamily] = useState(false);
102+
103+
const { mutate: checkApplyStatusMutate, isPending: isCheckingStatus } =
104+
useCheckApplyStatusMutation();
105+
const { mutateAsync: deleteDraftAsync } = useDeleteDraftMutation();
106+
const { mutateAsync: updateProfileAsync } = useMemberProfileMutation();
86107

87108
const handleCheckApplyStatus = (userEmail: string) => {
88-
checkApplyStatusMutate(userEmail, {
109+
checkApplyStatusMutate(undefined, {
89110
onSuccess: data => {
90111
if (data.result === "PROFILE_NOT_REGISTERED") {
91112
dispatch("goToProfile", userEmail);
@@ -97,9 +118,30 @@ export function IdentityVerificationStep({
97118
return;
98119
}
99120

100-
// CONTINUE (TEMP_SAVED 또는 JOINED)
101-
toastController.positive(APPLY_MESSAGE.success.continueWriting);
102-
dispatch("goToApply", userEmail);
121+
// CONTINUE (TEMP_SAVED 또는 JOINED) → draft 확인
122+
void applyApi
123+
.getDraft()
124+
.then(draft => {
125+
// 파트 불일치 체크: draft에 저장된 jobFamily와 현재 접근한 jobFamily가 다른 경우
126+
if (draft.jobFamily != null && draft.jobFamily !== context.jobFamily) {
127+
setVerifiedEmail(userEmail);
128+
setMismatchDialog({
129+
isOpen: true,
130+
savedJobFamily: draft.jobFamily,
131+
});
132+
return;
133+
}
134+
135+
// 같은 파트 또는 draft 없음 → 이어서 작성
136+
toastController.positive(APPLY_MESSAGE.success.continueWriting);
137+
dispatch("goToApply", userEmail);
138+
})
139+
.catch((error: unknown) => {
140+
// draft 조회 실패 시에도 이어서 작성 가능 (빈 폼으로 시작)
141+
handleError(error, "임시저장 데이터 조회 실패");
142+
toastController.positive(APPLY_MESSAGE.success.continueWriting);
143+
dispatch("goToApply", userEmail);
144+
});
103145
},
104146
onError: error => {
105147
handleError(error, "지원 상태 확인 실패");
@@ -132,6 +174,56 @@ export function IdentityVerificationStep({
132174
void navigate(`${PATH.resetPin}?returnTo=${encodeURIComponent(returnTo)}`);
133175
};
134176

177+
// 파트 불일치 다이얼로그: "기존 파트 지원서 이어서 작성하기" 선택
178+
const handleContinueSavedDraft = () => {
179+
if (mismatchDialog.savedJobFamily) {
180+
void navigate(`${PATH.applyContinue}/${mismatchDialog.savedJobFamily}`);
181+
}
182+
setMismatchDialog({ isOpen: false, savedJobFamily: null });
183+
};
184+
185+
// 파트 불일치 다이얼로그: "새로운 파트로 지원하기" 선택
186+
const handleStartNewApplication = async () => {
187+
setIsChangingJobFamily(true);
188+
setMismatchDialog({ isOpen: false, savedJobFamily: null });
189+
190+
try {
191+
// 1. 현재 프로필 백업
192+
const profile = await applyApi.getMe();
193+
194+
// 2. 기존 draft + 프로필 삭제
195+
await deleteDraftAsync();
196+
197+
// 3. 프로필 복원 (새로운 jobFamily로)
198+
await updateProfileAsync({
199+
name: profile.name,
200+
phoneNumber: "01012345678", // TODO: getMe 응답에 phoneNumber가 없어서 임시 처리
201+
careerDetails: profile.careerDetails,
202+
region: profile.region,
203+
experiencePeriod: profile.experiencePeriod,
204+
interestedDomains: profile.interestedDomains,
205+
jobFamily: context.jobFamily,
206+
});
207+
208+
// 4. 지원서 작성으로 이동
209+
toastController.positive(APPLY_MESSAGE.success.continueWriting);
210+
dispatch("goToApply", verifiedEmail);
211+
} catch (error) {
212+
handleError(error, "파트 변경 실패");
213+
toastController.destructive("파트 변경에 실패했습니다. 다시 시도해주세요.");
214+
} finally {
215+
setIsChangingJobFamily(false);
216+
}
217+
};
218+
219+
// 다이얼로그 메시지 생성
220+
const mismatchDialogContent = mismatchDialog.savedJobFamily
221+
? APPLY_DIALOG.jobFamilyMismatch(
222+
findJobFamilyOption(mismatchDialog.savedJobFamily).korean,
223+
findJobFamilyOption(context.jobFamily).korean,
224+
)
225+
: null;
226+
135227
return (
136228
<ApplyStepLayout
137229
variant='auth'
@@ -213,6 +305,29 @@ export function IdentityVerificationStep({
213305
onClick: () => setIsSubmittedDialogOpen(false),
214306
}}
215307
/>
308+
309+
{mismatchDialogContent && (
310+
<Dialog
311+
open={mismatchDialog.isOpen || isChangingJobFamily}
312+
onOpenChange={open =>
313+
!open &&
314+
!isChangingJobFamily &&
315+
setMismatchDialog({ isOpen: false, savedJobFamily: null })
316+
}
317+
header={mismatchDialogContent.header}
318+
body={mismatchDialogContent.body}
319+
primaryAction={{
320+
children: mismatchDialogContent.primaryAction,
321+
onClick: handleContinueSavedDraft,
322+
disabled: isChangingJobFamily,
323+
}}
324+
secondaryAction={{
325+
children: isChangingJobFamily ? "변경 중..." : mismatchDialogContent.secondaryAction,
326+
onClick: () => void handleStartNewApplication(),
327+
disabled: isChangingJobFamily,
328+
}}
329+
/>
330+
)}
216331
</ApplyStepLayout>
217332
);
218333
}

apps/web/src/features/apply/steps/registration/RegistrationStep.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,8 @@ export function RegistrationStep({ context, onNext, onBack }: RegistrationStepPr
7979

8080
//지원서 임시 저장
8181
const handleSaveDraft = useCallback(() => {
82-
saveDraftMutate({
83-
jobFamily,
84-
answers: { answers, portfolios: formattedPortfolios },
85-
});
86-
}, [saveDraftMutate, answers, formattedPortfolios, jobFamily]);
82+
saveDraftMutate({ answers, portfolios: formattedPortfolios });
83+
}, [saveDraftMutate, answers, formattedPortfolios]);
8784

8885
//지원서 제출
8986
const handleSubmit = useCallback(() => {

0 commit comments

Comments
 (0)