Skip to content

Commit 7fbf62a

Browse files
authored
Merge pull request #272 from CSE-Shaco/develop
fix: 지원서 제출 로직 개선 및 디자인 시스템 고도화
2 parents b3d3e38 + 7e81458 commit 7fbf62a

10 files changed

Lines changed: 807 additions & 341 deletions

File tree

src/app/admin/recruit-manager/page.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default function Page() {
7272
dir: apiSortDirection,
7373
question: query || undefined,
7474
};
75-
const res = await apiClient.get('/recruit/members', { params });
75+
const res = await apiClient.get('/recruit/member', { params });
7676
const list = Array.isArray(res?.data?.data) ? res.data.data : [];
7777
const total = res?.data?.meta?.totalElements ?? list.length;
7878
const computedTotalPages = Math.max(1, Math.ceil(total / rowsPerPage));
@@ -97,7 +97,7 @@ export default function Page() {
9797
const pageSize = 200;
9898
const baseParams = { size: pageSize, sort: 'createdAt', dir: 'DESC' };
9999

100-
const firstRes = await apiClient.get('/recruit/members', { params: { ...baseParams, page: 0 } });
100+
const firstRes = await apiClient.get('/recruit/member', { params: { ...baseParams, page: 0 } });
101101
const firstList = Array.isArray(firstRes?.data?.data) ? firstRes.data.data : [];
102102
const total = firstRes?.data?.meta?.totalElements ?? firstList.length;
103103
const totalPagesForStats = Math.max(1, Math.ceil(total / pageSize));
@@ -106,7 +106,7 @@ export default function Page() {
106106
if (totalPagesForStats > 1) {
107107
const promises = [];
108108
for (let p = 1; p < totalPagesForStats; p++) {
109-
promises.push(apiClient.get('/recruit/members', { params: { ...baseParams, page: p } }));
109+
promises.push(apiClient.get('/recruit/member', { params: { ...baseParams, page: p } }));
110110
}
111111
const results = await Promise.allSettled(promises);
112112
results.forEach((res) => {
@@ -148,7 +148,7 @@ export default function Page() {
148148
);
149149

150150
try {
151-
await apiClient.patch(`/recruit/members/${userId}/payment`, { isPayed: nextValue });
151+
await apiClient.patch(`/recruit/member/${userId}/payment`, { isPayed: nextValue });
152152
} catch (err) {
153153
// rollback
154154
setCurrentUsers(prevUsers);
@@ -184,7 +184,7 @@ export default function Page() {
184184
if (!memberId) {
185185
throw new Error('멤버 ID를 확인할 수 없습니다.');
186186
}
187-
const res = await apiClient.get(`/recruit/members/${memberId}`);
187+
const res = await apiClient.get(`/recruit/member/${memberId}`);
188188
const detail = res?.data?.data ?? null;
189189
if (!detail) {
190190
throw new Error('상세 정보를 불러오지 못했습니다.');

src/app/recruit/core/page.tsx

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
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

514
import {
615
GdgButton,
@@ -14,7 +23,7 @@ import {
1423
import { GdgInput } from '@/components/ui/input/GdgInput'
1524
import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi'
1625
import { cn } from '@/utils/cn'
17-
import { formatPhoneNumberInput } from '@/utils/phoneNumber'
26+
import { formatPhoneNumberInput, toPhoneDigits } from '@/utils/phoneNumber'
1827

1928
type 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

Comments
 (0)