@@ -26,6 +26,85 @@ const normalizeLabel = (value?: string) => (value ?? "").trim().toLowerCase();
2626const 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+
29108const 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