Skip to content

Commit 6314bc0

Browse files
committed
hotfix: 지원서 수정 시 기존 내용 유지
1 parent af2a513 commit 6314bc0

2 files changed

Lines changed: 193 additions & 91 deletions

File tree

src/pages/applications/ApplicationEditPage.tsx

Lines changed: 142 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,85 @@ const normalizeLabel = (value?: string) => (value ?? "").trim().toLowerCase();
2626
const getOptionKey = (option: RecruitmentItemOption) =>
2727
String(option.id ?? `order-${option.order}`);
2828

29+
const extractSelectedOptionKeys = (
30+
item: RecruitmentDetailItem,
31+
answer?: ApplicationAnswerDetail
32+
) => {
33+
if (!answer || !item.options?.length) return [];
34+
35+
const optionMap = new Map<number, RecruitmentItemOption>();
36+
const optionTitleMap = new Map<string, RecruitmentItemOption>();
37+
38+
item.options.forEach((option) => {
39+
if (typeof option.id === "number") {
40+
optionMap.set(option.id, option);
41+
}
42+
if (option.title) {
43+
optionTitleMap.set(option.title.trim(), option);
44+
}
45+
});
46+
47+
const byIds =
48+
answer.selectedOptionIds
49+
?.map((id) => optionMap.get(id))
50+
.filter((option): option is RecruitmentItemOption => Boolean(option))
51+
.map((option) => getOptionKey(option)) ?? [];
52+
if (byIds.length > 0) return byIds;
53+
54+
const byTitles =
55+
answer.selectedOptionTitles
56+
?.map((title) => optionTitleMap.get(title.trim()))
57+
.filter((option): option is RecruitmentItemOption => Boolean(option))
58+
.map((option) => getOptionKey(option)) ?? [];
59+
if (byTitles.length > 0) return byTitles;
60+
61+
const hasSelectionFlags =
62+
answer.selectOptions?.some(
63+
(option) =>
64+
option.selected !== undefined ||
65+
option.isSelected !== undefined ||
66+
option.checked !== undefined
67+
) ?? false;
68+
69+
const bySelectOptions =
70+
answer.selectOptions
71+
?.filter((option) =>
72+
hasSelectionFlags
73+
? option.selected || option.isSelected || option.checked
74+
: true
75+
)
76+
.map((option) => {
77+
if (option.optionId && optionMap.has(option.optionId)) {
78+
return getOptionKey(optionMap.get(option.optionId)!);
79+
}
80+
if (option.title) {
81+
const matched = optionTitleMap.get(option.title.trim());
82+
if (matched) return getOptionKey(matched);
83+
}
84+
return null;
85+
})
86+
.filter((key): key is string => Boolean(key)) ?? [];
87+
88+
return bySelectOptions;
89+
};
90+
91+
const normalizeSelectValue = (value: string | string[] | undefined) => {
92+
if (Array.isArray(value)) return value;
93+
if (typeof value === "string") {
94+
const trimmed = value.trim();
95+
if (trimmed.startsWith("[")) {
96+
try {
97+
const parsed = JSON.parse(trimmed);
98+
if (Array.isArray(parsed)) return parsed as string[];
99+
} catch {
100+
// ignore parse errors
101+
}
102+
}
103+
return trimmed.length > 0 ? [trimmed] : [];
104+
}
105+
return [];
106+
};
107+
29108
const ApplicationEditPage = () => {
30109
const { applicationId } = useParams<{ applicationId: string }>();
31110
const navigate = useNavigate();
@@ -44,16 +123,12 @@ const ApplicationEditPage = () => {
44123
} = useApplicantInfoStore();
45124
const applicantIdentity = useMemo(
46125
() => ({
47-
name:
48-
(storeApplicantName ||
49-
locationState?.applicantName ||
50-
""
51-
).trim(),
52-
email:
53-
(storeApplicantEmail ||
54-
locationState?.applicantEmail ||
55-
""
56-
).trim(),
126+
name: (storeApplicantName || locationState?.applicantName || "").trim(),
127+
email: (
128+
storeApplicantEmail ||
129+
locationState?.applicantEmail ||
130+
""
131+
).trim(),
57132
}),
58133
[
59134
storeApplicantName,
@@ -67,7 +142,7 @@ const ApplicationEditPage = () => {
67142
const hasApplicantIdentity =
68143
applicantIdentity.name.length > 0 && applicantIdentity.email.length > 0;
69144
const [answers, setAnswers] = useState<AnswerState>({});
70-
const [fixedAnswers, setFixedAnswers] = useState<Record<number, string>>({});
145+
const [fixedAnswers, setFixedAnswers] = useState<AnswerState>({});
71146
const [defaultFixedAnswers, setDefaultFixedAnswers] = useState<
72147
Record<string, string>
73148
>(() =>
@@ -141,11 +216,19 @@ const ApplicationEditPage = () => {
141216

142217
const fixedFieldEntries = useMemo(() => {
143218
const usedIds = new Set<number>();
144-
return FIXED_FIELDS.map((field, index) => {
219+
const matchesFieldTitle = (title: string, normalizedLabel: string) => {
220+
const normalizedTitle = normalizeLabel(title);
221+
return (
222+
normalizedTitle === normalizedLabel ||
223+
normalizedTitle.includes(normalizedLabel)
224+
);
225+
};
226+
227+
return FIXED_FIELDS.map((field) => {
145228
const normalizedLabel = normalizeLabel(field.label);
146229
const byLabel = sortedItems.find(
147230
(item) =>
148-
normalizeLabel(item.title) === normalizedLabel &&
231+
matchesFieldTitle(item.title, normalizedLabel) &&
149232
!usedIds.has(item.id)
150233
);
151234

@@ -154,19 +237,6 @@ const ApplicationEditPage = () => {
154237
return { field, item: byLabel };
155238
}
156239

157-
const byOrder = sortedItems.find(
158-
(item) =>
159-
item.order === index + 1 &&
160-
item.type === "TEXT" &&
161-
item.required &&
162-
!usedIds.has(item.id)
163-
);
164-
165-
if (byOrder) {
166-
usedIds.add(byOrder.id);
167-
return { field, item: byOrder };
168-
}
169-
170240
return { field, item: null };
171241
});
172242
}, [sortedItems]);
@@ -179,6 +249,16 @@ const ApplicationEditPage = () => {
179249
[fixedFieldEntries]
180250
);
181251

252+
const fixedItemFieldMap = useMemo(() => {
253+
const map = new Map<number, string>();
254+
fixedFieldEntries.forEach(({ field, item }) => {
255+
if (item) {
256+
map.set(item.id, field.id);
257+
}
258+
});
259+
return map;
260+
}, [fixedFieldEntries]);
261+
182262
const fixedItemIdSet = useMemo(
183263
() => new Set(fixedItems.map((item) => item.id)),
184264
[fixedItems]
@@ -209,6 +289,8 @@ const ApplicationEditPage = () => {
209289
return map;
210290
}, [detail?.answers]);
211291

292+
const [isInitialized, setIsInitialized] = useState(false);
293+
212294
useEffect(() => {
213295
if (!detail) return;
214296
setDefaultFixedAnswers((previous) => ({
@@ -220,9 +302,9 @@ const ApplicationEditPage = () => {
220302
}, [detail]);
221303

222304
useEffect(() => {
223-
if (!detail || sortedItems.length === 0) return;
305+
if (!detail || sortedItems.length === 0 || isInitialized) return;
224306

225-
const initialFixed: Record<number, string> = {};
307+
const initialFixed: AnswerState = {};
226308
fixedItems.forEach((item) => {
227309
const answer =
228310
answerMapById.get(item.id) ?? answerMapByOrder.get(item.order);
@@ -237,27 +319,25 @@ const ApplicationEditPage = () => {
237319
}
238320

239321
if (item.type === "SELECT") {
240-
const selectedIds = answer.selectedOptionIds ?? [];
241-
const optionMap = new Map<number, RecruitmentItemOption>();
242-
item.options?.forEach((option) => {
243-
if (typeof option.id === "number") {
244-
optionMap.set(option.id, option);
245-
}
246-
});
247-
248-
const selectedKeys = selectedIds
249-
.map((id) => optionMap.get(id))
250-
.filter((option): option is RecruitmentItemOption => Boolean(option))
251-
.map((option) => getOptionKey(option));
252-
322+
const selectedKeys = extractSelectedOptionKeys(item, answer);
253323
initialFixed[item.id] = item.multiple
254-
? JSON.stringify(selectedKeys)
324+
? selectedKeys
255325
: selectedKeys[0] ?? "";
256326
return;
257327
}
258328

329+
const fieldId = fixedItemFieldMap.get(item.id);
330+
const fallbackValue =
331+
fieldId === "applicant-name"
332+
? detail?.name ?? ""
333+
: fieldId === "applicant-email"
334+
? detail?.email ?? ""
335+
: fieldId === "applicant-phone"
336+
? detail?.tel ?? ""
337+
: detail?.name ?? "";
338+
259339
const textValue =
260-
answer.text ?? answer.answer ?? answer.value ?? detail?.name ?? "";
340+
answer.text ?? answer.answer ?? answer.value ?? fallbackValue;
261341
initialFixed[item.id] = textValue;
262342
});
263343

@@ -277,11 +357,7 @@ const ApplicationEditPage = () => {
277357
}
278358

279359
if (item.type === "SELECT") {
280-
const selectedIds = answer.selectedOptionIds ?? [];
281-
const selectedKeys = selectedIds
282-
.map((id) => item.options?.find((option) => option.id === id))
283-
.filter((option): option is RecruitmentItemOption => Boolean(option))
284-
.map((option) => getOptionKey(option));
360+
const selectedKeys = extractSelectedOptionKeys(item, answer);
285361
initialAnswers[item.id] = item.multiple
286362
? selectedKeys
287363
: selectedKeys[0] ?? "";
@@ -301,10 +377,13 @@ const ApplicationEditPage = () => {
301377

302378
setFixedAnswers(initialFixed);
303379
setAnswers(initialAnswers);
380+
setIsInitialized(true);
304381
}, [
305382
detail,
383+
isInitialized,
306384
sortedItems.length,
307385
fixedItems,
386+
fixedItemFieldMap,
308387
dynamicItems,
309388
answerMapById,
310389
answerMapByOrder,
@@ -357,7 +436,7 @@ const ApplicationEditPage = () => {
357436
});
358437
};
359438

360-
const handleFixedAnswerChange = (itemId: number, value: string) => {
439+
const handleFixedAnswerChange = (itemId: number, value: AnswerValue) => {
361440
setFixedAnswers((prev) => ({
362441
...prev,
363442
[itemId]: value,
@@ -379,7 +458,7 @@ const ApplicationEditPage = () => {
379458

380459
const value = answers[item.id];
381460
if (item.type === "SELECT") {
382-
const normalized = Array.isArray(value) ? value : value ? [value] : [];
461+
const normalized = normalizeSelectValue(value);
383462
if (normalized.length === 0) {
384463
count += 1;
385464
}
@@ -400,11 +479,11 @@ const ApplicationEditPage = () => {
400479

401480
fixedItems.forEach((item) => {
402481
if (!item.required) return;
403-
const rawValue = fixedAnswers[item.id] ?? "";
482+
const rawValue = fixedAnswers[item.id];
404483
const value =
405-
item.type === "SELECT" && rawValue.startsWith("[")
406-
? JSON.parse(rawValue)
407-
: rawValue;
484+
item.type === "SELECT"
485+
? normalizeSelectValue(rawValue)
486+
: rawValue ?? "";
408487

409488
if (Array.isArray(value)) {
410489
if (value.length === 0) {
@@ -507,25 +586,10 @@ const ApplicationEditPage = () => {
507586
const fixedAnswersPayload = fixedItems
508587
.filter((item) => item.type !== "ANNOUNCEMENT")
509588
.map((item) => {
510-
let value: string | string[] = fixedAnswers[item.id] ?? "";
511-
512-
if (typeof value === "string" && value.startsWith("[")) {
513-
try {
514-
const parsed = JSON.parse(value);
515-
if (Array.isArray(parsed)) {
516-
value = parsed;
517-
}
518-
} catch {
519-
// ignore
520-
}
521-
}
589+
const value: string | string[] = fixedAnswers[item.id] ?? "";
522590

523591
if (item.type === "SELECT") {
524-
const normalized = Array.isArray(value)
525-
? value
526-
: value
527-
? [value]
528-
: [];
592+
const normalized = normalizeSelectValue(value);
529593
const optionIds = buildSelectedOptionIds(item, normalized);
530594
return {
531595
itemId: item.id,
@@ -552,11 +616,7 @@ const ApplicationEditPage = () => {
552616
const value = answers[item.id];
553617

554618
if (item.type === "SELECT") {
555-
const normalized = Array.isArray(value)
556-
? value
557-
: value
558-
? [value]
559-
: [];
619+
const normalized = normalizeSelectValue(value);
560620
const optionIds = buildSelectedOptionIds(item, normalized);
561621
return {
562622
itemId: item.id,
@@ -627,8 +687,8 @@ const ApplicationEditPage = () => {
627687
지원자 정보를 확인할 수 없습니다.
628688
</p>
629689
<p className="text-15-medium text-black-60">
630-
지원서 조회를 위해 이름과 이메일이 필요합니다. 지원서 조회 페이지에서
631-
다시 로그인해 주세요.
690+
지원서 조회를 위해 이름과 이메일이 필요합니다. 지원서 조회
691+
페이지에서 다시 로그인해 주세요.
632692
</p>
633693
<button
634694
type="button"
@@ -684,6 +744,9 @@ const ApplicationEditPage = () => {
684744
<h2 className="text-title-18-semibold text-black-90">
685745
지원자 기본 정보
686746
</h2>
747+
<p className="text-13-regular text-black-50">
748+
이름, 전화번호, 이메일은 수정할 수 없습니다.
749+
</p>
687750
<div className="flex flex-col gap-4">
688751
{fixedFieldEntries.map(({ field, item }, index) => {
689752
const hasItem = Boolean(item);
@@ -719,6 +782,7 @@ const ApplicationEditPage = () => {
719782
}}
720783
onDateChange={() => undefined}
721784
onSelectChange={() => undefined}
785+
isReadOnly
722786
/>
723787
);
724788
})}

0 commit comments

Comments
 (0)