From 3be69e6c8f27bc97330ce3c9eb8a042e4842b39f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 05:32:10 +0000 Subject: [PATCH 1/7] Create draft PR for #384 From c82239ccfb658a99586ce42d3fadcfeaf624bb44 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park <121204715+lurgi@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:59:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?test:=20DashboardCreate=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DashboardCreate.stories.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 frontend/src/pages/DashboardCreate/DashboardCreate.stories.tsx diff --git a/frontend/src/pages/DashboardCreate/DashboardCreate.stories.tsx b/frontend/src/pages/DashboardCreate/DashboardCreate.stories.tsx new file mode 100644 index 000000000..d21655068 --- /dev/null +++ b/frontend/src/pages/DashboardCreate/DashboardCreate.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { reactRouterParameters } from 'storybook-addon-remix-react-router'; +import DashboardCreate from './index'; + +const meta: Meta = { + title: 'Pages/DashboardCreate', + component: DashboardCreate, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'DashboardCreate 컴포넌트는 공고를 작성하고 지원서를 구성한 후 게시할 수 있는 양식을 제공합니다.', + }, + }, + reactRouter: reactRouterParameters({ + location: { + pathParams: { dashboardId: '1' }, + }, + routing: { path: '/dashboard/:dashboardId/create' }, + }), + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; From d612e81ea8e6859897f0f9a7b92f47e54c3933a3 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park <121204715+lurgi@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:59:25 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B3=A0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20Recruitment=20=EB=8B=A8=EA=B3=84=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/DashboardCreate/Recruitment/index.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx b/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx index dd94c9f0d..cfc260794 100644 --- a/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx +++ b/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import Button from '@components/common/Button'; import ChevronButton from '@components/common/ChevronButton'; import DateInput from '@components/common/DateInput'; @@ -23,7 +23,12 @@ export default function Recruitment({ recruitmentInfoState, setRecruitmentInfoSt const today = new Date().toISOString().split('T')[0]; const startDateText = startDate ? formatDate(startDate) : ''; const endDateText = endDate ? formatDate(endDate) : ''; - const isNextButtonValid = !!(endDate && contentText && startDate && title); + + useEffect(() => { + setContentText(quillRef.current?.unprivilegedEditor?.getText()); + }, [quillRef]); + + const isNextButtonValid = !!(endDate && contentText?.trim() && startDate && title.trim()); const handleStartDate = (e: React.ChangeEvent) => { setRecruitmentInfoState((prev) => ({ @@ -51,9 +56,6 @@ export default function Recruitment({ recruitmentInfoState, setRecruitmentInfoSt ...prev, postingContent: string, })); - }; - - const handlePostingContentBlur = () => { setContentText(quillRef.current?.unprivilegedEditor?.getText()); }; @@ -99,7 +101,6 @@ export default function Recruitment({ recruitmentInfoState, setRecruitmentInfoSt quillRef={quillRef} value={recruitmentInfoState.postingContent} onChange={handlePostingContentChange} - onBlur={handlePostingContentBlur} /> From f2541eacd2721efcced8da9d2f2e5f1478cbb5c5 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park <121204715+lurgi@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:33:44 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Apply/QuestionBuilder/index.tsx | 28 ++++++------ .../CheckBoxField/CheckBoxField.stories.tsx | 12 +++--- .../recruitment/CheckBoxField/index.tsx | 43 ++++++++++--------- .../RadioInputField.stories.tsx | 12 +++--- .../recruitment/RadioInputField/index.tsx | 43 ++++++++++--------- .../hooks/useDashboardCreateForm/index.tsx | 10 +++-- frontend/src/types/dashboard.ts | 2 +- 7 files changed, 80 insertions(+), 70 deletions(-) diff --git a/frontend/src/components/dashboard/DashboardCreate/Apply/QuestionBuilder/index.tsx b/frontend/src/components/dashboard/DashboardCreate/Apply/QuestionBuilder/index.tsx index 8f2ea3c81..3c74e77dc 100644 --- a/frontend/src/components/dashboard/DashboardCreate/Apply/QuestionBuilder/index.tsx +++ b/frontend/src/components/dashboard/DashboardCreate/Apply/QuestionBuilder/index.tsx @@ -1,15 +1,17 @@ import { useState } from 'react'; -import { Question, QuestionChoice, QuestionControlActionType, QuestionOptionValue } from '@customTypes/dashboard'; +import { Question, QuestionControlActionType, QuestionOptionValue } from '@customTypes/dashboard'; import InputField from '@components/common/InputField'; import Dropdown from '@components/common/Dropdown'; import ToggleSwitch from '@components/common/ToggleSwitch'; import { QUESTION_TYPE_NAME } from '@constants/constants'; + +import CheckBoxField from '@components/recruitment/CheckBoxField'; +import RadioInputField from '@components/recruitment/RadioInputField'; import QuestionController from '../QuestionController'; import S from './style'; -import QuestionChoicesBuilder from '../QuestionChoicesBuilder'; interface QuestionBuilderProps { index: number; @@ -23,10 +25,6 @@ interface QuestionBuilderProps { deleteQuestion: (index: number) => void; } -function getSortedChoices(choices: QuestionChoice[]): QuestionOptionValue[] { - return choices?.sort((a, b) => a.orderIndex - b.orderIndex).map((item) => ({ value: item.choice })); -} - export default function QuestionBuilder({ index, question, @@ -40,7 +38,6 @@ export default function QuestionBuilder({ }: QuestionBuilderProps) { const [title, setTitle] = useState(question?.question || ''); const [currentQuestionType, setCurrentQuestionType] = useState(question?.type || 'SHORT_ANSWER'); - const [choices, setChoices] = useState(getSortedChoices(question?.choices) || []); const [isRequired, setIsRequired] = useState(question?.required || true); const handleChangeTitle = (event: React.ChangeEvent) => { @@ -50,14 +47,12 @@ export default function QuestionBuilder({ const handleChangeQuestionType = (type: Question['type']) => { if (type === currentQuestionType) return; - if (type === 'SHORT_ANSWER' || type === 'LONG_ANSWER') setChoices([]); setCurrentQuestionType(type); setQuestionType(index)(type); }; const handleUpdateQuestionChoices = (newChoices: QuestionOptionValue[]) => { - setChoices(newChoices); setQuestionOptions(index)(newChoices); }; @@ -99,10 +94,17 @@ export default function QuestionBuilder({ /> - {(currentQuestionType === 'SINGLE_CHOICE' || currentQuestionType === 'MULTIPLE_CHOICE') && ( - + )} + + {currentQuestionType === 'MULTIPLE_CHOICE' && ( + )} diff --git a/frontend/src/components/recruitment/CheckBoxField/CheckBoxField.stories.tsx b/frontend/src/components/recruitment/CheckBoxField/CheckBoxField.stories.tsx index cdeb8ac3e..28380726a 100644 --- a/frontend/src/components/recruitment/CheckBoxField/CheckBoxField.stories.tsx +++ b/frontend/src/components/recruitment/CheckBoxField/CheckBoxField.stories.tsx @@ -15,14 +15,14 @@ const meta: Meta = { }, tags: ['autodocs'], argTypes: { - options: { + choices: { description: 'CheckBoxOption으로 구성된 옵션 객체 배열입니다.', control: { type: 'object' }, table: { type: { summary: 'Option[]' }, }, }, - setOptions: { + setChoices: { description: '옵션 객체 배열을 설정하는 함수입니다.', action: 'optionsChanged', table: { @@ -35,11 +35,11 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const defaultOptions = [{ value: '' }]; +const defaultOptions = [{ choice: '' }]; export const Default: Story = { args: { - options: defaultOptions, + choices: defaultOptions, }, decorators: [ (Child) => { @@ -49,8 +49,8 @@ export const Default: Story = { { - updateArgs({ options }); + setChoices: (choices) => { + updateArgs({ choices }); }, }} /> diff --git a/frontend/src/components/recruitment/CheckBoxField/index.tsx b/frontend/src/components/recruitment/CheckBoxField/index.tsx index e0447f09f..173a3274e 100644 --- a/frontend/src/components/recruitment/CheckBoxField/index.tsx +++ b/frontend/src/components/recruitment/CheckBoxField/index.tsx @@ -1,39 +1,40 @@ import React, { useCallback, useEffect, useRef } from 'react'; +import { QuestionOptionValue } from '@customTypes/dashboard'; +import CheckBoxOption from '../CheckBoxOption'; import S from './style'; -import CheckBoxOption from '../CheckBoxOption'; -interface Option { - value: string; +interface ChoiceOption { + choice: string; } interface Props { - options: Option[]; - setOptions: React.Dispatch>; + choices: ChoiceOption[]; + setChoices: (newChoices: QuestionOptionValue[]) => void; } -export default function CheckBoxField({ options, setOptions }: Props) { +export default function CheckBoxField({ choices, setChoices }: Props) { const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const handleOptionChange = (index: number, value: string) => { - const newOptions = [...options]; - newOptions[index].value = value; - setOptions(newOptions); + const newOptions = [...choices]; + newOptions[index].choice = value; + setChoices(newOptions); }; const addOption = () => { - setOptions([...options, { value: '' }]); + setChoices([...choices, { choice: '' }]); }; const deleteOption = (index: number) => { - const newOptions = options.slice(); + const newOptions = choices.slice(); newOptions.splice(index, 1); - setOptions(newOptions); + setChoices(newOptions); }; const handleOptionBlur = (index: number) => { - const isLastOption = index === options.length - 1; - const isEmptyValue = options[index].value.trim() === ''; + const isLastOption = index === choices.length - 1; + const isEmptyValue = choices[index].choice.trim() === ''; if (!isLastOption && isEmptyValue) { deleteOption(index); } @@ -43,13 +44,13 @@ export default function CheckBoxField({ options, setOptions }: Props) { }; const focusLastOption = useCallback(() => { - inputRefs.current[options.length - 1]?.focus(); - }, [options.length]); + inputRefs.current[choices.length - 1]?.focus(); + }, [choices.length]); const handleKeyDown = (e: React.KeyboardEvent, index: number) => { if ((e.key === 'Tab' || e.key === 'Enter') && !e.shiftKey) { e.preventDefault(); - if (index === options.length - 1) { + if (index === choices.length - 1) { addOption(); } focusLastOption(); @@ -58,7 +59,7 @@ export default function CheckBoxField({ options, setOptions }: Props) { useEffect(() => { focusLastOption(); - }, [options.length, focusLastOption]); + }, [choices.length, focusLastOption]); const setInputRefCallback = (index: number) => (node: HTMLInputElement) => { inputRefs.current[index] = node; @@ -66,15 +67,15 @@ export default function CheckBoxField({ options, setOptions }: Props) { return ( - {options.map((option, index) => ( + {choices.map((choice, index) => ( deleteOption(index)} inputAttrs={{ - value: option.value, + value: choice.choice, ref: setInputRefCallback(index), onChange: (e) => handleOptionChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(e, index), diff --git a/frontend/src/components/recruitment/RadioInputField/RadioInputField.stories.tsx b/frontend/src/components/recruitment/RadioInputField/RadioInputField.stories.tsx index 6d1552bb6..4ec522228 100644 --- a/frontend/src/components/recruitment/RadioInputField/RadioInputField.stories.tsx +++ b/frontend/src/components/recruitment/RadioInputField/RadioInputField.stories.tsx @@ -15,14 +15,14 @@ const meta: Meta = { }, tags: ['autodocs'], argTypes: { - options: { + choices: { description: 'RadioInputOption으로 구성된 옵션 객체 배열입니다.', control: { type: 'object' }, table: { type: { summary: 'Option[]' }, }, }, - setOptions: { + setChoices: { description: '옵션 객체 배열을 설정하는 함수입니다.', action: 'optionsChanged', table: { @@ -35,11 +35,11 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const defaultOptions = [{ value: '' }]; +const defaultOptions = [{ choice: '' }]; export const Default: Story = { args: { - options: defaultOptions, + choices: defaultOptions, }, decorators: [ (Child) => { @@ -49,8 +49,8 @@ export const Default: Story = { { - updateArgs({ options }); + setChoices: (choices) => { + updateArgs({ choices }); }, }} /> diff --git a/frontend/src/components/recruitment/RadioInputField/index.tsx b/frontend/src/components/recruitment/RadioInputField/index.tsx index ffb085956..957e71c9d 100644 --- a/frontend/src/components/recruitment/RadioInputField/index.tsx +++ b/frontend/src/components/recruitment/RadioInputField/index.tsx @@ -1,39 +1,40 @@ import React, { useCallback, useEffect, useRef } from 'react'; +import { QuestionOptionValue } from '@customTypes/dashboard'; import S from './style'; import RadioInputOption from '../RadioInputOption'; -interface Option { - value: string; +interface ChoiceOption { + choice: string; } interface Props { - options: Option[]; - setOptions: React.Dispatch>; + choices: ChoiceOption[]; + setChoices: (newChoices: QuestionOptionValue[]) => void; } -export default function RadioInputField({ options, setOptions }: Props) { +export default function RadioInputField({ choices, setChoices }: Props) { const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const handleOptionChange = (index: number, value: string) => { - const newOptions = [...options]; - newOptions[index].value = value; - setOptions(newOptions); + const newOptions = [...choices]; + newOptions[index].choice = value; + setChoices(newOptions); }; const addOption = () => { - setOptions([...options, { value: '' }]); + setChoices([...choices, { choice: '' }]); }; const deleteOption = (index: number) => { - const newOptions = options.slice(); + const newOptions = choices.slice(); newOptions.splice(index, 1); - setOptions(newOptions); + setChoices(newOptions); }; const handleOptionBlur = (index: number) => { - const isLastOption = index === options.length - 1; - const isEmptyValue = options[index].value.trim() === ''; + const isLastOption = index === choices.length - 1; + const isEmptyValue = choices[index].choice.trim() === ''; if (!isLastOption && isEmptyValue) { deleteOption(index); } @@ -43,13 +44,15 @@ export default function RadioInputField({ options, setOptions }: Props) { }; const focusLastOption = useCallback(() => { - inputRefs.current[options.length - 1]?.focus(); - }, [options.length]); + inputRefs.current[choices.length - 1]?.focus(); + }, [choices.length]); const handleKeyDown = (e: React.KeyboardEvent, index: number) => { + if (e.nativeEvent.isComposing) return; + if ((e.key === 'Tab' || e.key === 'Enter') && !e.shiftKey) { e.preventDefault(); - if (index === options.length - 1) { + if (index === choices.length - 1) { addOption(); } focusLastOption(); @@ -58,7 +61,7 @@ export default function RadioInputField({ options, setOptions }: Props) { useEffect(() => { focusLastOption(); - }, [options.length, focusLastOption]); + }, [choices.length, focusLastOption]); const setInputRefCallback = (index: number) => (node: HTMLInputElement) => { inputRefs.current[index] = node; @@ -66,15 +69,15 @@ export default function RadioInputField({ options, setOptions }: Props) { return ( - {options.map((option, index) => ( + {choices.map((choice, index) => ( deleteOption(index)} inputAttrs={{ - value: option.value, + value: choice.choice, ref: setInputRefCallback(index), onChange: (e) => handleOptionChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(e, index), diff --git a/frontend/src/hooks/useDashboardCreateForm/index.tsx b/frontend/src/hooks/useDashboardCreateForm/index.tsx index 769a34004..e49587ce3 100644 --- a/frontend/src/hooks/useDashboardCreateForm/index.tsx +++ b/frontend/src/hooks/useDashboardCreateForm/index.tsx @@ -64,7 +64,6 @@ export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { ...recruitmentInfoState, questions: applyState.slice(3).map(({ id, ...value }) => { const temp = { ...value }; - temp.choices = value.choices.map((choice, index) => ({ ...choice, orderIndex: index })); return { ...temp, orderIndex: id }; }), }, @@ -110,7 +109,12 @@ export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { setApplyState((prevState) => { const questionsCopy = [...prevState]; questionsCopy[index].type = type; - questionsCopy[index].choices = []; + if (type === 'SINGLE_CHOICE' || type === 'MULTIPLE_CHOICE') { + questionsCopy[index].choices = [{ choice: '', orderIndex: 0 }]; + } else { + questionsCopy[index].choices = []; + } + console.log(questionsCopy); return questionsCopy; }); }; @@ -118,7 +122,7 @@ export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { const setQuestionOptions = (index: number) => (options: QuestionOptionValue[]) => { setApplyState((prevState) => { const questionsCopy = [...prevState]; - questionsCopy[index].choices = options.map(({ value }, i) => ({ choice: value, orderIndex: i })); + questionsCopy[index].choices = options.map(({ choice }, i) => ({ choice, orderIndex: i })); return questionsCopy; }); }; diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts index 7d56a6ce8..1340bbd5f 100644 --- a/frontend/src/types/dashboard.ts +++ b/frontend/src/types/dashboard.ts @@ -32,7 +32,7 @@ export interface QuestionOption { export type QuestionControlActionType = 'moveUp' | 'moveDown' | 'delete'; export interface QuestionOptionValue { - value: string; + choice: string; } interface Stats { From e3b5ab382eb90a2aadaa05e81b833f2fb3383595 Mon Sep 17 00:00:00 2001 From: Jeongwoo Park <121204715+lurgi@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:04:47 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EA=B3=B5=EA=B3=A0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=8B=A4=EC=9D=8C=20=EB=B2=84=ED=8A=BC=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/DashboardCreate/Apply/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/dashboard/DashboardCreate/Apply/index.tsx b/frontend/src/components/dashboard/DashboardCreate/Apply/index.tsx index cacc633ea..d319183b3 100644 --- a/frontend/src/components/dashboard/DashboardCreate/Apply/index.tsx +++ b/frontend/src/components/dashboard/DashboardCreate/Apply/index.tsx @@ -37,6 +37,12 @@ export default function Apply({ prevStep, nextStep, }: ApplyProps) { + const isNextBtnValid = + applyState.length === DEFAULT_QUESTION_LENGTH || + applyState + .slice(DEFAULT_QUESTION_LENGTH) + .every((question) => question.question.trim() && question.choices.length !== 1); + return ( @@ -106,7 +112,7 @@ export default function Apply({