Skip to content

Commit dd7c2d8

Browse files
authored
Merge pull request #409 from kakao-tech-campus-3rd-step3/feat/interview-status-option#408
[FEAT] 지원폼 생성 페이지에 면접 전형 옵션 기능 추가(#408)
2 parents 0f10263 + 481fa33 commit dd7c2d8

File tree

12 files changed

+243
-108
lines changed

12 files changed

+243
-108
lines changed

src/app/mocks/repositories/applyForm.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const applyForms: Record<string, ApplicationFormData> = {
55
title: '카태켐 12기 지원서',
66
description: '카카오테크 캠퍼스 12기 모집을 위한 지원서입니다.',
77
recruitDate: '2025-03-01 ~ 2025-03-31',
8+
interviewRequired: false,
89
formQuestions: [
910
{
1011
questionNum: 1,
@@ -29,20 +30,6 @@ const applyForms: Record<string, ApplicationFormData> = {
2930
optionList: [{ value: 'JAVA' }, { value: 'C' }, { value: 'C++' }],
3031
displayOrder: 3,
3132
},
32-
{
33-
questionNum: 4,
34-
question: '면접 시간을 설정해주세요.',
35-
fieldType: 'TIME_SLOT',
36-
isRequired: true,
37-
displayOrder: 4,
38-
timeSlotOptions: {
39-
date: '2025-09-24 ~ 2025-09-25',
40-
availableTime: {
41-
start: '10:00',
42-
end: '21:00',
43-
},
44-
},
45-
},
4633
],
4734
},
4835
};

src/pages/Routes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { MainPage } from '@/pages/user/Main/Page.tsx';
1010
import { NoticeDetailPage } from '@/pages/user/Notice/DetailPage';
1111
import { NoticeListPage } from '@/pages/user/Notice/Page';
1212
import { ApplicationDetailPage } from './admin/ApplicationDetail/Page';
13-
import { ApplicationFormBuilder } from './admin/ApplicationFormBuilder/Page';
13+
import { ApplicationFormBuilderPage } from './admin/ApplicationFormBuilder/Page';
1414
import { KakaoCallback } from './admin/Login/KakaoCallback';
1515
import { LoginPage } from './admin/Login/Page';
1616
import { AdminSignupPage } from './admin/Signup/Page';
@@ -69,7 +69,7 @@ export const router = createBrowserRouter([
6969
},
7070
{
7171
path: ADMIN.APPLICATION_FORM_BUILDER,
72-
element: <ApplicationFormBuilder />,
72+
element: <ApplicationFormBuilderPage />,
7373
},
7474
],
7575
},

src/pages/admin/ApplicationFormBuilder/Page.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import { ApplicationFieldsFormTableSection } from './components/FieldsFormTableS
1212
import { ApplicationFormBuilderHeaderSection } from './components/HeaderSection';
1313
import type { ApplicationFormData } from './types/fieldType';
1414

15-
export const ApplicationFormBuilder = () => {
15+
export const ApplicationFormBuilderPage = () => {
1616
const { clubId } = useParams();
1717
const [isEditMode, setIsEditMode] = useState(false);
18-
1918
const { data, isLoading, error } = useAdaptedApplicationForm(Number(clubId));
2019
const { adaptedPatchForm } = useAdaptedPatchApplicationForm(Number(clubId));
2120

@@ -24,6 +23,7 @@ export const ApplicationFormBuilder = () => {
2423
title: '',
2524
description: '',
2625
recruitDate: '',
26+
interviewRequired: false,
2727
formQuestions: [],
2828
},
2929
});
@@ -50,6 +50,38 @@ export const ApplicationFormBuilder = () => {
5050
});
5151
});
5252

53+
const isInterviewMode = formHandler.watch('interviewRequired');
54+
const handleInterviewChange = (checked: boolean) => {
55+
formHandler.setValue('interviewRequired', checked);
56+
57+
const currentQuestions = formHandler.getValues('formQuestions');
58+
59+
if (checked) {
60+
const filteredQuestions = currentQuestions.filter((q) => q.fieldType !== 'TIME_SLOT');
61+
formHandler.setValue('formQuestions', [
62+
{
63+
questionNum: 1,
64+
fieldType: 'TIME_SLOT',
65+
displayOrder: 1,
66+
question: '면접 가능한 시간을 선택해주세요',
67+
isRequired: true,
68+
optionList: [],
69+
timeSlotOptions: {
70+
date: '',
71+
availableTime: { start: '09:00:00', end: '18:00:00' },
72+
},
73+
},
74+
...filteredQuestions.map((q, i) => ({ ...q, questionNum: i + 2, displayOrder: i + 2 })),
75+
]);
76+
} else {
77+
const filtered = currentQuestions.filter((q) => q.fieldType !== 'TIME_SLOT');
78+
formHandler.setValue(
79+
'formQuestions',
80+
filtered.map((q, i) => ({ ...q, questionNum: i + 1, displayOrder: i + 1 })),
81+
);
82+
}
83+
};
84+
5385
if (isLoading) return <LoadingSpinner />;
5486
if (error) return <div>에러발생 : {error.message}</div>;
5587

@@ -61,8 +93,11 @@ export const ApplicationFormBuilder = () => {
6193
onEdit={handleEdit}
6294
onSave={handleSave}
6395
onCancel={handleCancel}
96+
isInterviewMode={isInterviewMode}
97+
onInterviewChange={handleInterviewChange}
6498
/>
6599
<ApplicationInfoSection formHandler={formHandler} isEditMode={isEditMode} />
100+
66101
<ApplicationFieldsFormTableSection formHandler={formHandler} isEditMode={isEditMode} />
67102
</ContentContainer>
68103
</Layout>

src/pages/admin/ApplicationFormBuilder/components/ApplicationInfoSection/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const ApplicationInfoSection = ({ formHandler, isEditMode }: Props) => {
4242
return (
4343
<>
4444
<Global styles={datePickerStyles} />
45-
<Layout>
45+
<Layout isEditMode={isEditMode}>
4646
<Wrapper>
4747
<UnderlineInputField
4848
placeholder='ex. 동아리명 10기 신입부원 모집'
@@ -93,16 +93,17 @@ export const ApplicationInfoSection = ({ formHandler, isEditMode }: Props) => {
9393
);
9494
};
9595

96-
const Layout = styled.div(({ theme }) => ({
96+
const Layout = styled.div<{ isEditMode: boolean }>(({ theme, isEditMode }) => ({
9797
width: '100%',
9898
border: `1px solid ${theme.colors.gray200}`,
9999
borderRadius: theme.radius.sm,
100100
padding: '1.5rem',
101-
backgroundColor: 'white',
101+
backgroundColor: isEditMode ? theme.colors.bg : theme.colors.gray00,
102102
display: 'flex',
103103
flexDirection: 'column',
104104
gap: '2.5rem',
105105
boxSizing: 'border-box',
106+
cursor: 'auto',
106107

107108
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
108109
padding: '1rem',

src/pages/admin/ApplicationFormBuilder/components/FieldsFormTableSection/FormFieldItem/Builders/TimeslotFieldBuilder.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { datePickerStyles } from '@/pages/admin/ApplicationFormBuilder/styles/da
88
import * as S from '@/pages/admin/ApplicationFormBuilder/styles/timeslot.styled';
99
import { generateTimes } from '@/pages/admin/ApplicationFormBuilder/utils/generateTimes';
1010
import { Dropdown } from '@/shared/components/Dropdown';
11+
import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineInputField';
1112
import { Text } from '@/shared/components/Text';
1213
import type { CustomInputProps } from '@/pages/admin/ApplicationFormBuilder/types/clubInfo';
1314
import type { ApplicationFormData } from '@/pages/admin/ApplicationFormBuilder/types/fieldType';
@@ -27,7 +28,12 @@ const CustomInput = ({ value, onClick }: CustomInputProps) => (
2728
);
2829

2930
export const TimeslotFieldBuilder = ({ formHandler, questionIndex, isEditMode }: Props) => {
30-
const { register, setValue, watch } = formHandler;
31+
const {
32+
register,
33+
setValue,
34+
watch,
35+
formState: { errors },
36+
} = formHandler;
3137

3238
const timeSlotDate = watch(`formQuestions.${questionIndex}.timeSlotOptions.date`);
3339

@@ -54,7 +60,19 @@ export const TimeslotFieldBuilder = ({ formHandler, questionIndex, isEditMode }:
5460
return (
5561
<>
5662
<Global styles={datePickerStyles} />
57-
<S.Layout>
63+
<S.HeaderWrapper>
64+
<OutlineInputField
65+
placeholder='질문 내용을 입력하세요.'
66+
{...register(`formQuestions.${questionIndex}.question`, {
67+
required: '질문 내용을 입력해주세요.',
68+
minLength: { value: 1, message: '질문 내용은 최소 한 글자 이상 입력해야 합니다.' },
69+
})}
70+
invalid={!!errors.formQuestions?.[questionIndex]?.question}
71+
message={errors.formQuestions?.[questionIndex]?.question?.message}
72+
disabled={!isEditMode}
73+
/>
74+
</S.HeaderWrapper>
75+
<S.Container>
5876
<S.DatePickerWrapper>
5977
<DatePicker
6078
locale={ko}
@@ -71,9 +89,14 @@ export const TimeslotFieldBuilder = ({ formHandler, questionIndex, isEditMode }:
7189
<input
7290
type='hidden'
7391
{...register(`formQuestions.${questionIndex}.timeSlotOptions.date`, {
74-
required: '모집 기간을 선택해주세요',
92+
required: '면접 기간을 선택해주세요',
7593
})}
7694
/>
95+
{errors.formQuestions?.[questionIndex]?.timeSlotOptions?.date && (
96+
<S.ErrorMessage>
97+
{errors.formQuestions[questionIndex].timeSlotOptions.date.message}
98+
</S.ErrorMessage>
99+
)}
77100
</S.DatePickerWrapper>
78101

79102
<S.TimeSelectContainer>
@@ -96,7 +119,7 @@ export const TimeslotFieldBuilder = ({ formHandler, questionIndex, isEditMode }:
96119
/>
97120
</S.TimeSelectWrapper>
98121
</S.TimeSelectContainer>
99-
</S.Layout>
122+
</S.Container>
100123
</>
101124
);
102125
};

src/pages/admin/ApplicationFormBuilder/components/FieldsFormTableSection/FormFieldItem/index.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { OutlineInputField } from '@/shared/components/Form/InputField/OutlineIn
1111
import { CheckboxOptionsBuilder } from './Builders/CheckboxOptionsBuilder';
1212
import { RadioOptionsBuilder } from './Builders/RadioOptionsBuilder';
1313
import { TextOptionsBuilder } from './Builders/TextOptionsBuilder';
14-
import { TimeslotFieldBuilder } from './Builders/TimeslotFieldBuilder';
1514
import type {
1615
QuestionType,
1716
ApplicationFormData,
@@ -23,7 +22,7 @@ type Props = {
2322
onRemove?: () => void;
2423
isEditMode: boolean;
2524
};
26-
const fieldTypes: QuestionType[] = ['텍스트', '라디오', '체크박스', '타임슬롯'];
25+
const fieldTypes: QuestionType[] = ['텍스트', '라디오', '체크박스'];
2726

2827
export const FormFieldItem = ({ formHandler, index, onRemove, isEditMode }: Props) => {
2928
const {
@@ -40,7 +39,18 @@ export const FormFieldItem = ({ formHandler, index, onRemove, isEditMode }: Prop
4039

4140
const questionType = watch(`formQuestions.${index}.fieldType`);
4241

43-
const currentDisplayType = reverseTypeMapping[questionType] || '텍스트';
42+
const getDisplayType = (qType: typeof questionType): QuestionType => {
43+
switch (qType) {
44+
case 'TEXT':
45+
case 'RADIO':
46+
case 'CHECKBOX':
47+
return reverseTypeMapping[qType];
48+
case 'TIME_SLOT':
49+
default:
50+
return '텍스트';
51+
}
52+
};
53+
const currentDisplayType = getDisplayType(questionType);
4454

4555
const renderOptionsBuilder = () => {
4656
switch (currentDisplayType) {
@@ -62,14 +72,7 @@ export const FormFieldItem = ({ formHandler, index, onRemove, isEditMode }: Prop
6272
isEditMode={isEditMode}
6373
/>
6474
);
65-
case '타임슬롯':
66-
return (
67-
<TimeslotFieldBuilder
68-
formHandler={formHandler}
69-
questionIndex={index}
70-
isEditMode={isEditMode}
71-
/>
72-
);
75+
7376
default:
7477
return null;
7578
}
@@ -81,11 +84,6 @@ export const FormFieldItem = ({ formHandler, index, onRemove, isEditMode }: Prop
8184

8285
if (newType === 'RADIO' || newType === 'CHECKBOX') {
8386
setValue(`formQuestions.${index}.optionList`, [{ value: '' }]);
84-
} else if (newType === 'TIME_SLOT') {
85-
setValue(`formQuestions.${index}.timeSlotOptions`, {
86-
date: '',
87-
availableTime: { start: '07:00:00', end: '07:00:00' },
88-
});
8987
}
9088
};
9189

src/pages/admin/ApplicationFormBuilder/components/FieldsFormTableSection/index.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
33
import { useFieldArray } from 'react-hook-form';
44
import { AddFieldButton } from './AddFieldButton';
55
import { FormFieldItem } from './FormFieldItem';
6+
import { TimeslotFieldBuilder } from './FormFieldItem/Builders/TimeslotFieldBuilder';
67
import type { ApplicationFormData } from '@/pages/admin/ApplicationFormBuilder/types/fieldType';
78

89
type Props = {
@@ -35,12 +36,20 @@ export const ApplicationFieldsFormTableSection = ({ formHandler, isEditMode }: P
3536
{fields.map((data, index) => (
3637
<div key={data.id}>
3738
{index !== 0 && <Divider />}
38-
<FormFieldItem
39-
index={index}
40-
formHandler={formHandler}
41-
onRemove={() => remove(index)}
42-
isEditMode={isEditMode}
43-
/>
39+
{data.fieldType === 'TIME_SLOT' ? (
40+
<TimeslotFieldBuilder
41+
formHandler={formHandler}
42+
questionIndex={index}
43+
isEditMode={isEditMode}
44+
/>
45+
) : (
46+
<FormFieldItem
47+
index={index}
48+
formHandler={formHandler}
49+
onRemove={() => remove(index)}
50+
isEditMode={isEditMode}
51+
/>
52+
)}
4453
</div>
4554
))}
4655
{isEditMode && <AddFieldButton onClick={handleAddFormField} />}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import styled from '@emotion/styled';
2+
3+
export const Container = styled.div(({ theme }) => ({
4+
width: '100%',
5+
display: 'flex',
6+
flexDirection: 'column',
7+
gap: '2rem',
8+
padding: '2.5rem 0 0 0',
9+
boxSizing: 'border-box',
10+
11+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
12+
gap: '0.5rem',
13+
},
14+
}));
15+
16+
export const HeaderWrapper = styled.div({
17+
display: 'flex',
18+
justifyContent: 'space-between',
19+
alignItems: 'center',
20+
});
21+
22+
export const ButtonWrapper = styled.div({
23+
display: 'flex',
24+
gap: '0.5rem',
25+
});
26+
27+
export const Title = styled.h1(({ theme }) => ({
28+
fontSize: '2.5rem',
29+
fontWeight: theme.font.weight.medium,
30+
31+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
32+
fontSize: '2rem',
33+
},
34+
}));
35+
36+
export const CheckboxWrapper = styled.div(({ theme }) => ({
37+
display: 'flex',
38+
gap: '0.5rem',
39+
justifyContent: 'flex-end',
40+
alignItems: 'center',
41+
42+
[`@media (max-width: ${theme.breakpoints.mobile})`]: {
43+
padding: '1.5rem 0 0.5rem 0',
44+
justifyContent: 'flex-start',
45+
},
46+
}));
47+
48+
export const CustomCheckbox = styled.input(({ theme }) => ({
49+
width: '1.15rem',
50+
height: '1.15rem',
51+
cursor: 'pointer',
52+
appearance: 'none',
53+
border: `2px solid ${theme.colors.primary}`,
54+
borderRadius: '4px',
55+
position: 'relative',
56+
transition: 'all 0.2s ease',
57+
58+
'&:checked': {
59+
backgroundColor: theme.colors.primary,
60+
borderColor: theme.colors.primary,
61+
},
62+
63+
'&:checked::after': {
64+
content: '"✓"',
65+
position: 'absolute',
66+
top: '50%',
67+
left: '50%',
68+
transform: 'translate(-50%, -50%)',
69+
color: 'white',
70+
fontSize: '1.2rem',
71+
fontWeight: 'bold',
72+
},
73+
74+
'&:hover': {
75+
borderColor: theme.colors.primary800,
76+
boxShadow: `0 0 0 3px ${theme.colors.primary}20`,
77+
},
78+
79+
'&:focus': {
80+
outline: 'none',
81+
boxShadow: `0 0 0 3px ${theme.colors.primary}40`,
82+
},
83+
}));

0 commit comments

Comments
 (0)