11'use client'
22
3- import { useEffect , useState , type ChangeEvent , type FormEvent } from 'react'
3+ import {
4+ useCallback ,
5+ useEffect ,
6+ useId ,
7+ useMemo ,
8+ useRef ,
9+ useState ,
10+ type ChangeEvent ,
11+ type FormEvent
12+ } from 'react'
413
514import {
615 GdgButton ,
@@ -14,7 +23,7 @@ import {
1423import { GdgInput } from '@/components/ui/input/GdgInput'
1524import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi'
1625import { cn } from '@/utils/cn'
17- import { formatPhoneNumberInput } from '@/utils/phoneNumber'
26+ import { formatPhoneNumberInput , toPhoneDigits } from '@/utils/phoneNumber'
1827
1928type RecruitStep = 0 | 1 | 2 | 3
2029
@@ -92,35 +101,22 @@ function StepBar({
92101 onStepClick : ( step : RecruitStep ) => void
93102} ) {
94103 return (
95- < div className = "relative" >
96- < div className = "pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2" >
97- { [ 0 , 1 , 2 ] . map ( ( lineIndex ) => (
98- < div
99- key = { `line-${ lineIndex } ` }
100- className = { cn ( 'absolute h-px' , lineIndex < maxReachedStep ? 'bg-red' : 'bg-gray-600' ) }
101- style = { { left : `${ 12.5 + lineIndex * 25 } %` , width : '25%' } }
102- />
103- ) ) }
104- </ div >
105-
106- < div className = "relative z-10 grid grid-cols-4 items-center" >
107- { STEPS . map ( ( step , index ) => {
108- const stepIndex = index as RecruitStep
109- const isCurrent = stepIndex === currentStep
110- const isActivated = stepIndex <= maxReachedStep
111- const isClickable = stepIndex <= maxReachedStep
112-
113- return (
104+ < div className = "flex w-full items-center" >
105+ { STEPS . map ( ( step , index ) => {
106+ const stepIndex = index as RecruitStep
107+ const isCurrent = stepIndex === currentStep
108+ const isActivated = stepIndex <= maxReachedStep
109+ const isClickable = stepIndex <= maxReachedStep
110+ const isLast = index === STEPS . length - 1
111+
112+ return (
113+ < div key = { step } className = { cn ( 'flex items-center' , isLast ? '' : 'flex-1' ) } >
114114 < button
115- key = { step }
116115 type = "button"
117116 onClick = { ( ) => onStepClick ( stepIndex ) }
118117 disabled = { ! isClickable }
119118 className = { cn (
120119 'rounded-full border bg-black px-3 py-1.5 whitespace-nowrap typo-b3 mobile:typo-c1 transition-colors' ,
121- index === 0 && 'justify-self-start' ,
122- ( index === 1 || index === 2 ) && 'justify-self-center' ,
123- index === 3 && 'justify-self-end' ,
124120 isCurrent && 'bg-red border-red text-white' ,
125121 ! isCurrent && isActivated && 'border-red text-white' ,
126122 ! isCurrent && ! isActivated && 'border-gray-600 text-gray-600' ,
@@ -129,9 +125,17 @@ function StepBar({
129125 >
130126 { step }
131127 </ button >
132- )
133- } ) }
134- </ div >
128+ { ! isLast && (
129+ < div
130+ className = { cn (
131+ 'h-px flex-1' ,
132+ index < maxReachedStep ? 'bg-red' : 'bg-gray-600'
133+ ) }
134+ />
135+ ) }
136+ </ div >
137+ )
138+ } ) }
135139 </ div >
136140 )
137141}
@@ -165,22 +169,17 @@ function TextareaField({
165169 status = { error ? 'error' : undefined }
166170 statusMessage = { error ? '※ 필수 입력 사항입니다.' : undefined }
167171 >
168- < div className = "space-y-2" >
169- < GdgTextarea
170- name = { name }
171- value = { value }
172- onChange = { onChange }
173- maxLength = { maxLength }
174- rows = { rows }
175- state = { error ? 'error' : 'default' }
176- placeholder = "내용을 입력하세요."
177- fullWidth
178- className = "[&_textarea]:mobile:text-sm [&_textarea]:mobile:leading-5"
179- />
180- < p className = "typo-b2 text-right text-gray-700 mobile:typo-c1" >
181- ({ value . length } /{ maxLength } )
182- </ p >
183- </ div >
172+ < GdgTextarea
173+ name = { name }
174+ value = { value }
175+ onChange = { onChange }
176+ maxLength = { maxLength }
177+ rows = { rows }
178+ state = { error ? 'error' : 'default' }
179+ placeholder = "내용을 입력하세요."
180+ fullWidth
181+ className = "[&_textarea]:mobile:text-sm [&_textarea]:mobile:leading-5"
182+ />
184183 </ GdgFieldContainer >
185184 )
186185}
@@ -209,6 +208,30 @@ export default function RecruitCore() {
209208 files : [ ]
210209 } )
211210
211+ const isCurrentStepValid = useMemo ( ( ) => {
212+ if ( currentStep === 0 ) {
213+ return Boolean (
214+ formData . name . trim ( ) &&
215+ formData . studentId . trim ( ) &&
216+ formData . email . trim ( ) &&
217+ formData . major . trim ( ) &&
218+ formData . phone . trim ( )
219+ )
220+ }
221+ if ( currentStep === 1 ) {
222+ return Boolean (
223+ formData . team . trim ( ) &&
224+ formData . motivation . trim ( ) &&
225+ formData . role . trim ( ) &&
226+ formData . strength . trim ( ) &&
227+ formData . determination . trim ( )
228+ )
229+ }
230+ if ( currentStep === 2 ) return scheduleChecked
231+ if ( currentStep === 3 ) return agreementChecked
232+ return false
233+ } , [ currentStep , formData , scheduleChecked , agreementChecked ] )
234+
212235 useEffect ( ( ) => {
213236 let active = true
214237
@@ -220,15 +243,14 @@ export default function RecruitCore() {
220243 const payload = unwrapPrefill ( response . data )
221244 if ( ! payload ) return
222245
223- setFormData ( ( prev ) => ( {
224- ...prev ,
225- name : prev . name || payload . name || '' ,
226- studentId : prev . studentId || payload . studentId || '' ,
227- email : prev . email || payload . email || '' ,
228- major : prev . major || payload . major || '' ,
229- phone : prev . phone || payload . phone || ''
230- } ) )
231- } catch ( error ) {
246+ setFormData ( ( prev ) => ( {
247+ ...prev ,
248+ name : prev . name || payload . name || '' ,
249+ studentId : prev . studentId || payload . studentId || '' ,
250+ email : prev . email || payload . email || '' ,
251+ major : prev . major || payload . major || '' ,
252+ phone : formatPhoneNumberInput ( prev . phone || payload . phone || '' )
253+ } ) ) } catch ( error ) {
232254 if ( ! active ) return
233255 console . error ( '[코어 리크루팅] 기본 정보 불러오기에 실패했습니다.' , error )
234256 setPrefillError ( '기본 정보를 불러오지 못했습니다. 직접 입력해주세요.' )
@@ -352,8 +374,10 @@ export default function RecruitCore() {
352374 const payload = {
353375 snapshot : {
354376 name : formData . name ,
355- phone : formData . phone ,
356- major : formData . major
377+ studentId : formData . studentId ,
378+ phone : toPhoneDigits ( formData . phone ) ,
379+ major : formData . major ,
380+ email : formData . email
357381 } ,
358382 team : formData . team ,
359383 motivation : formData . motivation ,
@@ -767,7 +791,14 @@ export default function RecruitCore() {
767791 </ GdgButton >
768792 ) : null }
769793
770- < GdgButton type = "submit" device = "pc" size = "small" variant = "active" className = "w-30.5" >
794+ < GdgButton
795+ type = "submit"
796+ device = "pc"
797+ size = "small"
798+ variant = { isCurrentStepValid ? 'active' : 'disabled' }
799+ className = "w-30.5"
800+ disabled = { ! isCurrentStepValid }
801+ >
771802 { currentStep === 3 ? '제출하기' : '다음' }
772803 </ GdgButton >
773804 </ div >
@@ -793,8 +824,9 @@ export default function RecruitCore() {
793824 type = "submit"
794825 device = "mobile"
795826 size = "small"
796- variant = " active"
827+ variant = { isCurrentStepValid ? ' active' : 'disabled' }
797828 className = "w-27.25"
829+ disabled = { ! isCurrentStepValid }
798830 >
799831 { currentStep === 3 ? '제출하기' : '다음' }
800832 </ GdgButton >
0 commit comments