-
Notifications
You must be signed in to change notification settings - Fork 3
[feature] 지원서 작성/편집 기능 구현 및 질문 컴포넌트 구조화 #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6475ce9
5e3e5e7
c8310bf
1c4e26b
03b96d5
4fbce29
7c87edc
c5d7b7c
e23ab73
4179669
54d8826
31b8d6e
7186329
a6fae14
b64e5e1
30fb4a2
ecd73a3
6b14dc9
4906413
9dd308b
c31a915
be67290
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| const APPLICATION_FORM = { | ||
| SHORT_TEXT: { | ||
| placeholder: '답변입력란(최대 20자)', | ||
| }, | ||
| LONG_TEXT: { | ||
| placeholder: '답변입력란(최대 500자)', | ||
| }, | ||
| CHOICE: { | ||
| placeholder: '항목(최대 20자)', | ||
| }, | ||
| } as const; | ||
| export default APPLICATION_FORM; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // 지원하기 : 지원서 입력 컴포넌트 | ||
| // 특정 지원서를 사용자들이 입력할 수 있는 지원서 작성 부분 | ||
| // Todo 추후 특정 지원서를 받아서 answer 모드로 렌더링 | ||
| import { useState } from 'react'; | ||
| import ShortText from '@/pages/AdminPage/application/fields/ShortText'; | ||
| import Choice from '@/pages/AdminPage/application/fields/Choice'; | ||
|
|
||
| interface QuestionData { | ||
| title: string; | ||
| description: string; | ||
| items?: { value: string }[]; | ||
| } | ||
|
|
||
| interface AnswerData { | ||
| [id: number]: string; | ||
| } | ||
|
|
||
| const ApplicationForm = () => { | ||
| const [questions] = useState<Record<number, QuestionData>>({ | ||
| 1: { | ||
| title: '이름을 입력해주세요', | ||
| description: '본명을 입력해 주세요', | ||
| }, | ||
| 2: { | ||
| title: '자기소개를 해주세요', | ||
| description: '300자 이내로 입력해주세요', | ||
| }, | ||
| 3: { | ||
| title: '지원 분야를 선택해주세요', | ||
| description: '중복 선택은 불가능합니다', | ||
| items: [ | ||
| { value: '프론트엔드' }, | ||
| { value: '백엔드' }, | ||
| { value: '디자인' }, | ||
| ], | ||
| }, | ||
| 4: { | ||
| title: '희망하는 활동 시간을 선택해주세요', | ||
| description: '가장 편한 시간을 골라주세요', | ||
| items: [], | ||
| }, | ||
| 5: { | ||
| title: '이전에 프로젝트 경험이 있나요?', | ||
| description: '간단하게 작성해주세요', | ||
| items: [{ value: 'React' }], | ||
| }, | ||
| 6: { | ||
| title: '사용 가능한 기술 스택을 선택해주세요', | ||
| description: '복수 선택 가능', | ||
| items: [ | ||
| { value: 'React' }, | ||
| { value: 'Node.js' }, | ||
| { value: 'Python' }, | ||
| { value: 'Figma' }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| const [answers, setAnswers] = useState<AnswerData>({}); | ||
|
|
||
| const handleAnswerChange = (id: number) => (value: string) => { | ||
| setAnswers((prev) => ({ | ||
| ...prev, | ||
| [id]: value, | ||
| })); | ||
| }; | ||
|
|
||
| return <></>; | ||
oesnuj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| export default ApplicationForm; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // 지원서 제작하기 : 지원서 제작 컴포넌트 | ||
| // 지원서 수정과 제작을 맡을 컴포넌트 | ||
| // Todo: 질문 삭제 및 질문 추가 기능 구현 | ||
| import { useState } from 'react'; | ||
| import QuestionBuilder from '@/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder'; | ||
|
Comment on lines
+1
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 테스트 커버리지가 부족합니다. 정적 분석에서 지적한 대로 대부분의 라인이 테스트로 커버되지 않았습니다. 컴포넌트의 핵심 기능에 대한 단위 테스트 추가를 고려하세요. 단위 테스트 작성을 도와드릴까요? 다음과 같은 테스트 케이스들을 생성할 수 있습니다:
🧰 Tools🪛 GitHub Check: codecov/patch[warning] 4-5: frontend/src/pages/AdminPage/application/CreateForm.tsx#L4-L5 🤖 Prompt for AI Agents |
||
|
|
||
| type QuestionType = | ||
| | 'SHORT_TEXT' | ||
| | 'LONG_TEXT' | ||
| | 'CHOICE' | ||
| | 'MULTI_CHOICE' | ||
| | 'EMAIL' | ||
| | 'PHONE_NUMBER' | ||
| | 'NAME'; | ||
oesnuj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| type Question = { | ||
| title: string; | ||
| description: string; | ||
| type: QuestionType; | ||
| options: { | ||
| required: boolean; | ||
| }; | ||
| items?: { value: string }[]; | ||
| }; | ||
|
|
||
| const CreateForm = () => { | ||
| const [questions, setQuestions] = useState<Record<number, Question>>({ | ||
| 1: { | ||
| title: '이름을 입력해주세요', | ||
| description: '본명을 입력해 주세요', | ||
| options: { | ||
| required: true, | ||
| }, | ||
| type: 'SHORT_TEXT', | ||
| }, | ||
| 2: { | ||
| title: '지원 분야를 선택해주세요', | ||
| description: '중복 선택 가능합니다', | ||
| options: { | ||
| required: false, | ||
| }, | ||
| items: [ | ||
| { value: '프론트엔드' }, | ||
| { value: '백엔드' }, | ||
| { value: '디자인' }, | ||
| ], | ||
| type: 'MULTI_CHOICE', | ||
| }, | ||
| }); | ||
|
|
||
| const handleTitleChange = (id: number) => (value: string) => { | ||
| setQuestions((prev) => ({ | ||
| ...prev, | ||
| [id]: { | ||
| ...prev[id], | ||
| title: value, | ||
| }, | ||
| })); | ||
| }; | ||
|
|
||
| const handleDescriptionChange = (id: number) => (value: string) => { | ||
| setQuestions((prev) => ({ | ||
| ...prev, | ||
| [id]: { | ||
| ...prev[id], | ||
| description: value, | ||
| }, | ||
| })); | ||
| }; | ||
|
|
||
| const handleItemsChange = (id: number) => (newItems: { value: string }[]) => { | ||
| setQuestions((prev) => ({ | ||
| ...prev, | ||
| [id]: { | ||
| ...prev[id], | ||
| items: newItems, | ||
| }, | ||
| })); | ||
| }; | ||
|
|
||
| const handleTypeChange = (id: number) => (newType: QuestionType) => { | ||
| setQuestions((prev) => { | ||
| const prevQuestion = prev[id]; | ||
| const isChoiceType = newType === 'CHOICE' || newType === 'MULTI_CHOICE'; | ||
|
|
||
| const updatedQuestion: Question = { | ||
| ...prevQuestion, | ||
| type: newType, | ||
| }; | ||
|
|
||
| if (isChoiceType) { | ||
| updatedQuestion.items = | ||
| !prevQuestion.items || prevQuestion.items.length < 2 | ||
| ? [{ value: '' }, { value: '' }] | ||
| : prevQuestion.items; | ||
| } else { | ||
| delete updatedQuestion.items; | ||
| } | ||
oesnuj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return { | ||
| ...prev, | ||
| [id]: updatedQuestion, | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| const handleRequiredChange = (id: number) => (value: boolean) => { | ||
| setQuestions((prev) => ({ | ||
| ...prev, | ||
| [id]: { | ||
| ...prev[id], | ||
| options: { | ||
| ...prev[id].options, | ||
| required: value, | ||
| }, | ||
| }, | ||
| })); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| {Object.entries(questions).map(([id, question]) => ( | ||
| <QuestionBuilder | ||
| key={id} | ||
| id={Number(id)} | ||
| title={question.title} | ||
| description={question.description} | ||
| required={question.options?.required ?? false} | ||
| items={question.items} | ||
| onTitleChange={handleTitleChange(Number(id))} | ||
| onDescriptionChange={handleDescriptionChange(Number(id))} | ||
| onItemsChange={handleItemsChange(Number(id))} | ||
| onTypeChange={handleTypeChange(Number(id))} | ||
| onRequiredChange={handleRequiredChange(Number(id))} | ||
| type={question.type} | ||
| /> | ||
| ))} | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default CreateForm; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import styled from 'styled-components'; | ||
|
Check warning on line 1 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
|
|
||
| export const QuestionMenu = styled.div` | ||
|
Check warning on line 3 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| display: flex; | ||
| width: 140px; | ||
| flex-direction: column; | ||
| gap: 4px; | ||
| `; | ||
|
|
||
| export const RequiredToggleButton = styled.div` | ||
|
Check warning on line 10 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| display: flex; | ||
| border: none; | ||
| padding: 12px 16px; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| border-radius: 0.375rem; | ||
| background: #f5f5f5; | ||
| cursor: pointer; | ||
| margin: 0; | ||
| color: #787878; | ||
| font-size: 0.875rem; | ||
| font-weight: 600; | ||
| `; | ||
|
|
||
| export const RequiredToggleCircle = styled.span<{ active?: boolean }>` | ||
|
Check warning on line 25 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| position: relative; | ||
| width: 16px; | ||
| height: 16px; | ||
| border-radius: 50%; | ||
| border: 1px solid ${(props) => (props.active ? '#ff5000' : '#ccc')}; | ||
| background-color: ${(props) => (props.active ? '#ff5000' : 'white')}; | ||
|
|
||
| ${({ active }) => | ||
| active && | ||
| ` | ||
| background-color: #fff; | ||
| &::after { | ||
| content: ''; | ||
| width: 10px; | ||
| height: 10px; | ||
| background-color: #ff5000; | ||
| border-radius: 50%; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 50%; | ||
| transform: translate(-50%, -50%); | ||
| } | ||
| `} | ||
| `; | ||
|
|
||
| export const DropDownWrapper = styled.div` | ||
|
Check warning on line 51 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| position: relative; | ||
| width: 100%; | ||
| `; | ||
|
|
||
| export const Dropdown = styled.select` | ||
|
Check warning on line 56 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| display: flex; | ||
| width: 100%; | ||
| border: none; | ||
| padding: 12px 16px; | ||
| border-radius: 0.375rem; | ||
| background: #f5f5f5; | ||
| color: #787878; | ||
| font-size: 0.875rem; | ||
| font-weight: 600; | ||
| cursor: pointer; | ||
| appearance: none; | ||
| `; | ||
|
|
||
| export const DropdownIcon = styled.img` | ||
|
Check warning on line 70 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| position: absolute; | ||
| top: 50%; | ||
| right: 19px; | ||
| transform: translateY(-50%); | ||
| pointer-events: none; | ||
| `; | ||
|
|
||
| export const SelectionToggleWrapper = styled.div` | ||
|
Check warning on line 78 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| display: flex; | ||
| background-color: #f7f7f7; | ||
| border-radius: 0.375rem; | ||
| padding: 2px; | ||
| `; | ||
|
|
||
| export const SelectionToggleButton = styled.button<{ active: boolean }>` | ||
|
Check warning on line 85 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| border: none; | ||
| background-color: ${(props) => (props.active ? '#ddd' : 'transparent')}; | ||
| color: #787878; | ||
| font-size: 0.875rem; | ||
| border-radius: 0.375rem; | ||
| padding: 10px; | ||
| font-weight: 600; | ||
| cursor: pointer; | ||
| letter-spacing: -0.42px; | ||
| white-space: nowrap; | ||
| `; | ||
|
|
||
| export const QuestionWrapper = styled.div` | ||
|
Check warning on line 98 in frontend/src/pages/AdminPage/application/components/QuestionBuilder/QuestionBuilder.styles.ts
|
||
| display: flex; | ||
| gap: 36px; | ||
| `; | ||
|
Comment on lines
+1
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 색상 상수화 및 디자인 토큰 도입을 고려해주세요. 여러 색상값( 색상 상수 파일을 만들어 다음과 같이 사용하는 것을 제안합니다: // theme/colors.ts
export const COLORS = {
PRIMARY: '#ff5000',
GRAY_LIGHT: '#f5f5f5',
GRAY_MEDIUM: '#787878',
GRAY_BORDER: '#ccc',
GRAY_DARK: '#ddd',
WHITE: '#ffffff',
} as const;+import { COLORS } from '@/theme/colors';
export const RequiredToggleButton = styled.div`
// ...
- background: #f5f5f5;
+ background: ${COLORS.GRAY_LIGHT};
- color: #787878;
+ color: ${COLORS.GRAY_MEDIUM};
// ...
`;🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.