@@ -44,6 +44,11 @@ type RecruitFormState = {
4444
4545type DuplicateCheckStatus = 'idle' | 'checking' | 'available' | 'duplicate' | 'unverified' | 'error'
4646
47+ type PresignedUploadResponse = {
48+ key ?: string
49+ uploadUrl ?: string
50+ }
51+
4752interface DuplicateCheckState {
4853 status : DuplicateCheckStatus
4954 message ?: string
@@ -350,21 +355,17 @@ export default function Recruit() {
350355 }
351356 const payload = Object . fromEntries ( buildRecruitMap ( ) )
352357 if ( formData . enrolledClassification === '군휴학' && formData . proofFile ) {
353- const fd = new FormData ( )
354- fd . append (
355- 'request' ,
356- new Blob ( [ JSON . stringify ( payload ) ] , { type : 'application/json' } )
357- )
358- fd . append ( 'file' , formData . proofFile )
359- await axios . post ( `${ process . env . NEXT_PUBLIC_BASE_API_URL } /recruit/member/apply` , fd , {
360- headers : { 'Content-Type' : 'multipart/form-data' }
361- } )
362- } else {
363- await axios . post (
364- `${ process . env . NEXT_PUBLIC_BASE_API_URL } /recruit/member/apply` ,
365- payload
366- )
358+ const proofFileKey = await uploadProofFile ( formData . proofFile )
359+ const step6 = payload [ 6 ] as Record < string , unknown >
360+ payload [ 6 ] = {
361+ ...step6 ,
362+ proofFileUrl : proofFileKey
363+ }
367364 }
365+ await axios . post (
366+ `${ process . env . NEXT_PUBLIC_BASE_API_URL } /recruit/member/apply` ,
367+ payload
368+ )
368369 router . push ( '/recruit/member/completed?from=recruit' )
369370 } catch ( error : any ) {
370371 setGlobalError ( error . response ?. data ?. message || '지원서 제출 중 오류가 발생했습니다.' )
@@ -373,6 +374,50 @@ export default function Recruit() {
373374 }
374375 }
375376
377+ const uploadProofFile = async ( file : File ) => {
378+ const presignedResponse = await axios . post (
379+ `${ process . env . NEXT_PUBLIC_BASE_API_URL } /recruit/member/apply/proof-file/presigned-upload` ,
380+ {
381+ fileName : file . name ,
382+ contentType : file . type || 'application/octet-stream' ,
383+ fileSize : file . size
384+ }
385+ )
386+
387+ const payload = unwrapPresignedUploadResponse ( presignedResponse . data )
388+ if ( ! payload ?. key || ! payload . uploadUrl ) {
389+ throw new Error ( '증빙 파일 업로드 URL 발급 응답이 올바르지 않습니다.' )
390+ }
391+
392+ const uploadResponse = await fetch ( payload . uploadUrl , {
393+ method : 'PUT' ,
394+ headers : {
395+ 'Content-Type' : file . type || 'application/octet-stream'
396+ } ,
397+ body : file
398+ } )
399+
400+ if ( ! uploadResponse . ok ) {
401+ throw new Error ( '증빙 파일 S3 업로드에 실패했습니다.' )
402+ }
403+
404+ return payload . key
405+ }
406+
407+ const unwrapPresignedUploadResponse = ( raw : unknown ) : PresignedUploadResponse | null => {
408+ if ( ! raw || typeof raw !== 'object' ) return null
409+
410+ const record = raw as Record < string , unknown >
411+ if ( 'data' in record && record . data && typeof record . data === 'object' ) {
412+ return unwrapPresignedUploadResponse ( record . data )
413+ }
414+
415+ return {
416+ key : typeof record . key === 'string' ? record . key : undefined ,
417+ uploadUrl : typeof record . uploadUrl === 'string' ? record . uploadUrl : undefined
418+ }
419+ }
420+
376421 const studentStatus : GdgFieldStatus | undefined =
377422 formData . studentId . trim ( ) === studentCheckState . verifiedValue
378423 ? 'success'
0 commit comments