From 4fc2de471af0b60a19e5cdd88480fb42469e3eb9 Mon Sep 17 00:00:00 2001 From: bh2980 Date: Wed, 5 Jun 2024 17:22:44 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20shared-ui=EB=A1=9C=20common=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/component/common/Input.tsx | 34 ---- .../component/editor/AnswerItemManager.tsx | 148 ++++++++++-------- .../src/component/editor/ChooseAnswer.tsx | 34 ++-- .../src/component/editor/QuestionBlock.tsx | 99 ++++++++---- .../src/component/editor/TitleBlock.tsx | 23 +-- .../component/preview/AnswerItemManager.tsx | 10 +- .../src/component/preview/QuestionBlock.tsx | 2 +- .../src/component/preview/TitleBlock.tsx | 2 +- .../component/preview/answer/LongAnswer.tsx | 3 +- .../component/preview/answer/ShortAnswer.tsx | 2 +- apps/googleFormClone/src/pages/Editor.tsx | 2 +- apps/googleFormClone/src/pages/Preview.tsx | 2 +- apps/googleFormClone/src/pages/Result.tsx | 3 +- library/shared-ui/.babelrc | 12 ++ library/shared-ui/.eslintrc.json | 18 +++ library/shared-ui/README.md | 7 + library/shared-ui/project.json | 9 ++ .../shared-ui/src/components}/Block.tsx | 4 +- .../shared-ui/src/components}/Checkbox.tsx | 2 +- .../shared-ui/src/components}/Divider.tsx | 7 +- .../shared-ui/src/components}/Dropdown.tsx | 6 +- .../shared-ui/src/components}/IconButton.tsx | 4 +- library/shared-ui/src/components/Input.tsx | 40 +++++ .../shared-ui/src/components}/Radio/Radio.tsx | 2 +- .../src/components}/Radio/RadioGroup.tsx | 18 ++- .../shared-ui/src/components}/Switch.tsx | 4 +- .../shared-ui/src/components}/TextArea.tsx | 6 +- .../shared-ui/src/hooks}/useFluidTextArea.ts | 4 +- library/shared-ui/src/index.ts | 12 ++ .../shared-ui}/src/utils/classMerge.ts | 4 +- library/shared-ui/src/utils/iconClass.ts | 1 + library/shared-ui/tsconfig.json | 12 ++ library/shared-ui/tsconfig.lib.json | 24 +++ library/shared-ui/tsconfig.spec.json | 27 ++++ library/shared-ui/vite.config.ts | 28 ++++ nx.json | 3 +- package.json | 3 + tsconfig.base.json | 4 +- vitest.workspace.ts | 1 + yarn.lock | 55 +++++-- 40 files changed, 481 insertions(+), 200 deletions(-) delete mode 100644 apps/googleFormClone/src/component/common/Input.tsx create mode 100644 library/shared-ui/.babelrc create mode 100644 library/shared-ui/.eslintrc.json create mode 100644 library/shared-ui/README.md create mode 100644 library/shared-ui/project.json rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Block.tsx (92%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Checkbox.tsx (91%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Divider.tsx (67%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Dropdown.tsx (96%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/IconButton.tsx (86%) create mode 100644 library/shared-ui/src/components/Input.tsx rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Radio/Radio.tsx (93%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Radio/RadioGroup.tsx (60%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/Switch.tsx (94%) rename {apps/googleFormClone/src/component/common => library/shared-ui/src/components}/TextArea.tsx (83%) rename {apps/googleFormClone/src/hook/headless => library/shared-ui/src/hooks}/useFluidTextArea.ts (91%) create mode 100644 library/shared-ui/src/index.ts rename {apps/googleFormClone => library/shared-ui}/src/utils/classMerge.ts (60%) create mode 100644 library/shared-ui/src/utils/iconClass.ts create mode 100644 library/shared-ui/tsconfig.json create mode 100644 library/shared-ui/tsconfig.lib.json create mode 100644 library/shared-ui/tsconfig.spec.json create mode 100644 library/shared-ui/vite.config.ts create mode 100644 vitest.workspace.ts diff --git a/apps/googleFormClone/src/component/common/Input.tsx b/apps/googleFormClone/src/component/common/Input.tsx deleted file mode 100644 index d83b25c..0000000 --- a/apps/googleFormClone/src/component/common/Input.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import classMerge from "../../utils/classMerge"; - -interface InputProps extends React.ComponentPropsWithRef<"input"> { - innerRef?: React.ComponentPropsWithRef<"input">["ref"]; -} - -const Input = ({ className, innerRef, ...props }: InputProps) => { - return ( -
- -
-
- ); -}; - -export default Input; diff --git a/apps/googleFormClone/src/component/editor/AnswerItemManager.tsx b/apps/googleFormClone/src/component/editor/AnswerItemManager.tsx index 14bc0c8..a3dce40 100644 --- a/apps/googleFormClone/src/component/editor/AnswerItemManager.tsx +++ b/apps/googleFormClone/src/component/editor/AnswerItemManager.tsx @@ -3,18 +3,21 @@ * answer 상태에서 answerIDLIst를 순회하면서 answer을 뽑아 알맞게 렌더링한다 */ -import { useEffect, useRef } from "react"; -import { EDITOR_QUESTION_TYPE } from "../../constants"; -import { useAppDispatch, useAppSelector } from "../../hook/useRedux"; -import { AnswerInterface, addAnswer, editAnswer, removeAnswer } from "../../store/reducer/answerSlice"; -import ChooseAnswer from "./ChooseAnswer"; -import { v4 as uuidv4 } from "uuid"; -import classMerge from "../../utils/classMerge"; -import useDnDList from "../../hook/headless/useDnDList"; -import { editAnswerOrder } from "../../store/reducer/questionSlice"; -import useChangeEditBlockID from "../../hook/useChangeEditBlockID"; -import Checkbox from "../common/Checkbox"; -import Radio from "../common/Radio/Radio"; +import { useEffect, useRef } from 'react'; +import { EDITOR_QUESTION_TYPE } from '../../constants'; +import { useAppDispatch, useAppSelector } from '../../hook/useRedux'; +import { + AnswerInterface, + addAnswer, + editAnswer, + removeAnswer, +} from '../../store/reducer/answerSlice'; +import ChooseAnswer from './ChooseAnswer'; +import { v4 as uuidv4 } from 'uuid'; +import useDnDList from '../../hook/headless/useDnDList'; +import { editAnswerOrder } from '../../store/reducer/questionSlice'; +import useChangeEditBlockID from '../../hook/useChangeEditBlockID'; +import { Checkbox, Radio, classMerge } from '@google-form-clone/shared-ui'; interface AnswerManagerProps { questionID: string; @@ -23,7 +26,9 @@ interface AnswerManagerProps { // TODO 기타 추가 구현 const AnswerManager = ({ questionID }: AnswerManagerProps) => { const dispatch = useAppDispatch(); - const { type, answerIDList } = useAppSelector((store) => store.question[questionID]); + const { type, answerIDList } = useAppSelector( + (store) => store.question[questionID] + ); const answerMap = useAppSelector((store) => store.answer); const prevAnswerLength = useRef(answerIDList.length); @@ -37,18 +42,24 @@ const AnswerManager = ({ questionID }: AnswerManagerProps) => { const chooseAnswerRef = useRef<(null | HTMLInputElement)[]>([]); - const changeAnswer = (e: React.ChangeEvent, { answerID, questionID }: AnswerInterface) => { + const changeAnswer = ( + e: React.ChangeEvent, + { answerID, questionID }: AnswerInterface + ) => { dispatch(editAnswer({ answerID, content: e.target.value, questionID })); }; const addAnswerItem = () => { const NEW_ANSWER_ID = uuidv4(); - dispatch(addAnswer({ answerID: NEW_ANSWER_ID, content: "", questionID })); + dispatch(addAnswer({ answerID: NEW_ANSWER_ID, content: '', questionID })); }; const removeAnswerItem = (aID: string, idx: number) => { if (chooseAnswerRef && chooseAnswerRef.current) { - if (idx < chooseAnswerRef.current.length && chooseAnswerRef.current[idx + 1] instanceof HTMLInputElement) { + if ( + idx < chooseAnswerRef.current.length && + chooseAnswerRef.current[idx + 1] instanceof HTMLInputElement + ) { chooseAnswerRef.current[idx + 1]!.focus(); } else { chooseAnswerRef.current[idx - 1]!.focus(); @@ -56,7 +67,9 @@ const AnswerManager = ({ questionID }: AnswerManagerProps) => { } if (chooseAnswerRef && chooseAnswerRef.current) { - chooseAnswerRef.current = chooseAnswerRef.current.filter((_, i) => i !== idx); + chooseAnswerRef.current = chooseAnswerRef.current.filter( + (_, i) => i !== idx + ); } dispatch(removeAnswer(answerMap[aID])); @@ -75,56 +88,67 @@ const AnswerManager = ({ questionID }: AnswerManagerProps) => { return (
- {type === EDITOR_QUESTION_TYPE.short &&
단답형 메시지
} - {type === EDITOR_QUESTION_TYPE.long &&
장문형 메시지
} - {type !== EDITOR_QUESTION_TYPE.short && type !== EDITOR_QUESTION_TYPE.long && ( - <> -
- {answerIDList.map((aID, idx) => { - const answerInfo = answerMap[aID]; - return ( - changeAnswer(e, answerInfo)} - isEditing={isEditing} - deletable={answerIDList.length > 1} - onDeleteButton={() => removeAnswerItem(aID, idx)} - handleDrag={handleDrag} - innerRef={(el) => (chooseAnswerRef.current[idx] = el)} - /> - ); - })} -
- {isEditing && ( -
-
- {type === EDITOR_QUESTION_TYPE.radio && } - {type === EDITOR_QUESTION_TYPE.checkbox && } - {type === EDITOR_QUESTION_TYPE.dropdown &&
} - - 옵션 추가 - -
- {/* 또는 + {type === EDITOR_QUESTION_TYPE.short && ( +
단답형 메시지
+ )} + {type === EDITOR_QUESTION_TYPE.long && ( +
장문형 메시지
+ )} + {type !== EDITOR_QUESTION_TYPE.short && + type !== EDITOR_QUESTION_TYPE.long && ( + <> +
+ {answerIDList.map((aID, idx) => { + const answerInfo = answerMap[aID]; + return ( + changeAnswer(e, answerInfo)} + isEditing={isEditing} + deletable={answerIDList.length > 1} + onDeleteButton={() => removeAnswerItem(aID, idx)} + handleDrag={handleDrag} + innerRef={(el) => (chooseAnswerRef.current[idx] = el)} + /> + ); + })} +
+ {isEditing && ( +
+
+ {type === EDITOR_QUESTION_TYPE.radio && } + {type === EDITOR_QUESTION_TYPE.checkbox && ( + + )} + {type === EDITOR_QUESTION_TYPE.dropdown && ( +
+ )} + + 옵션 추가 + +
+ {/* 또는 */} -
- )} - - )} +
+ )} + + )}
); }; diff --git a/apps/googleFormClone/src/component/editor/ChooseAnswer.tsx b/apps/googleFormClone/src/component/editor/ChooseAnswer.tsx index 04edb12..4da5f19 100644 --- a/apps/googleFormClone/src/component/editor/ChooseAnswer.tsx +++ b/apps/googleFormClone/src/component/editor/ChooseAnswer.tsx @@ -1,19 +1,25 @@ -import { RiCloseFill, RiDraggable } from "react-icons/ri"; -import IconButton from "../common/IconButton"; -import { EDITOR_QUESTION_TYPE, ICON_CLASS } from "../../constants"; -import Checkbox from "../common/Checkbox"; -import Radio from "../common/Radio/Radio"; -import Input from "../common/Input"; -import classMerge from "../../utils/classMerge"; -import { isTouchScreen } from "../../hook/headless/useDnDList"; +import { RiCloseFill, RiDraggable } from 'react-icons/ri'; +import { EDITOR_QUESTION_TYPE, ICON_CLASS } from '../../constants'; +import { isTouchScreen } from '../../hook/headless/useDnDList'; +import { + Checkbox, + IconButton, + Input, + Radio, + classMerge, +} from '@google-form-clone/shared-ui'; -interface RadioAnswerProps extends Omit, "type"> { - type: EDITOR_QUESTION_TYPE.radio | EDITOR_QUESTION_TYPE.checkbox | EDITOR_QUESTION_TYPE.dropdown; +interface RadioAnswerProps + extends Omit, 'type'> { + type: + | EDITOR_QUESTION_TYPE.radio + | EDITOR_QUESTION_TYPE.checkbox + | EDITOR_QUESTION_TYPE.dropdown; idx?: number; deletable?: boolean; isEditing?: boolean; onDeleteButton?: (...params: unknown[]) => unknown; - innerRef?: React.ComponentPropsWithRef<"input">["ref"]; + innerRef?: React.ComponentPropsWithRef<'input'>['ref']; handleDrag?: (e: React.MouseEvent) => void; } @@ -37,7 +43,7 @@ const ChooseAnswer = ({ tabIndex={0} className={classMerge([ `absolute outline-none cursor-move ${ICON_CLASS}`, - !isTouchScreen && "hidden group-hover/item:flex", + !isTouchScreen && 'hidden group-hover/item:flex', ])} /> )} @@ -45,7 +51,9 @@ const ChooseAnswer = ({ {type === EDITOR_QUESTION_TYPE.radio && } {type === EDITOR_QUESTION_TYPE.checkbox && } {type === EDITOR_QUESTION_TYPE.dropdown && ( -
{idx}
+
+ {idx} +
)} {deletable && isEditing && ( diff --git a/apps/googleFormClone/src/component/editor/QuestionBlock.tsx b/apps/googleFormClone/src/component/editor/QuestionBlock.tsx index d899156..fe7b54f 100644 --- a/apps/googleFormClone/src/component/editor/QuestionBlock.tsx +++ b/apps/googleFormClone/src/component/editor/QuestionBlock.tsx @@ -1,28 +1,42 @@ -import { RiDeleteBin6Line, RiDraggable, RiFileCopyLine } from "react-icons/ri"; -import Block from "../common/Block"; -import Input from "../common/Input"; -import Divider from "../common/Divider"; -import { EDITOR_DROPDOWN_LIST, EDITOR_QUESTION_TYPE, ICON_CLASS } from "../../constants"; -import IconButton from "../common/IconButton"; -import Switch from "../common/Switch"; - -import { removeQuestion, editQuestion, copyQuestion } from "../../store/reducer/questionSlice"; -import { useAppDispatch, useAppSelector } from "../../hook/useRedux"; -import AnswerManager from "./AnswerItemManager"; -import { addAnswer, removeAnswer } from "../../store/reducer/answerSlice"; -import { v4 } from "uuid"; -import Dropdown from "../common/Dropdown"; -import useChangeEditBlockID from "../../hook/useChangeEditBlockID"; -import useBlockAutoFocus from "../../hook/useBlockAutoFocus"; -import classMerge from "../../utils/classMerge"; -import { isTouchScreen } from "../../hook/headless/useDnDList"; - -interface QuestionBlockProps extends React.ComponentPropsWithRef<"div"> { +import { RiDeleteBin6Line, RiDraggable, RiFileCopyLine } from 'react-icons/ri'; +import { + EDITOR_DROPDOWN_LIST, + EDITOR_QUESTION_TYPE, + ICON_CLASS, +} from '../../constants'; + +import { + removeQuestion, + editQuestion, + copyQuestion, +} from '../../store/reducer/questionSlice'; +import { useAppDispatch, useAppSelector } from '../../hook/useRedux'; +import AnswerManager from './AnswerItemManager'; +import { addAnswer, removeAnswer } from '../../store/reducer/answerSlice'; +import { v4 } from 'uuid'; +import useChangeEditBlockID from '../../hook/useChangeEditBlockID'; +import useBlockAutoFocus from '../../hook/useBlockAutoFocus'; +import { isTouchScreen } from '../../hook/headless/useDnDList'; +import { + Block, + Divider, + Dropdown, + IconButton, + Input, + Switch, + classMerge, +} from '@google-form-clone/shared-ui'; + +interface QuestionBlockProps extends React.ComponentPropsWithRef<'div'> { questionID: string; handleDrag?: (e: React.MouseEvent | React.TouchEvent) => void; } -const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) => { +const QuestionBlock = ({ + questionID, + handleDrag, + ...props +}: QuestionBlockProps) => { const dispatch = useAppDispatch(); const { changeEditingBlockID, isEditing } = useChangeEditBlockID(questionID); const { containerRef, questionInputRef } = useBlockAutoFocus(questionID); @@ -42,16 +56,29 @@ const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) const NEW_QUESTION_ID = v4(); dispatch( - copyQuestion({ ...questionInfo, questionID: NEW_QUESTION_ID, answerIDList: [], parentQuestionID: questionID }) + copyQuestion({ + ...questionInfo, + questionID: NEW_QUESTION_ID, + answerIDList: [], + parentQuestionID: questionID, + }) ); answerIDList.map((aID) => { - dispatch(addAnswer({ ...answerMap[aID], answerID: v4(), questionID: NEW_QUESTION_ID })); + dispatch( + addAnswer({ + ...answerMap[aID], + answerID: v4(), + questionID: NEW_QUESTION_ID, + }) + ); }); }; const changeQuestionType = (idx: number) => { - dispatch(editQuestion({ ...questionInfo, type: EDITOR_DROPDOWN_LIST[idx].type })); + dispatch( + editQuestion({ ...questionInfo, type: EDITOR_DROPDOWN_LIST[idx].type }) + ); if ( EDITOR_DROPDOWN_LIST[idx].type === EDITOR_QUESTION_TYPE.long || @@ -61,12 +88,14 @@ const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) dispatch(removeAnswer(answerMap[aID])); }); - dispatch(addAnswer({ answerID: v4(), content: "", questionID })); + dispatch(addAnswer({ answerID: v4(), content: '', questionID })); } }; const changeQuestionContent = (e: React.ChangeEvent) => { - dispatch(editQuestion({ ...questionInfo, questionContent: e.target.value })); + dispatch( + editQuestion({ ...questionInfo, questionContent: e.target.value }) + ); }; const changeRequired = () => { @@ -87,7 +116,10 @@ const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) onTouchStart={handleDrag} >
@@ -110,8 +142,10 @@ const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) ) : (
- {questionContent.length === 0 ? "질문" : questionContent} - {required && *} + {questionContent.length === 0 ? '질문' : questionContent} + {required && ( + * + )}
)}
@@ -127,7 +161,12 @@ const QuestionBlock = ({ questionID, handleDrag, ...props }: QuestionBlockProps) - + )} diff --git a/apps/googleFormClone/src/component/editor/TitleBlock.tsx b/apps/googleFormClone/src/component/editor/TitleBlock.tsx index 7a095bd..7f0242b 100644 --- a/apps/googleFormClone/src/component/editor/TitleBlock.tsx +++ b/apps/googleFormClone/src/component/editor/TitleBlock.tsx @@ -1,17 +1,16 @@ -import { useAppDispatch, useAppSelector } from "../../hook/useRedux"; -import useChangeEditBlockID from "../../hook/useChangeEditBlockID"; -import { editContent, editTitle } from "../../store/reducer/docsSlice"; -import Block from "../common/Block"; -import Input from "../common/Input"; -import TextArea from "../common/TextArea"; -import useBlockAutoFocus from "../../hook/useBlockAutoFocus"; +import { useAppDispatch, useAppSelector } from '../../hook/useRedux'; +import useChangeEditBlockID from '../../hook/useChangeEditBlockID'; +import { editContent, editTitle } from '../../store/reducer/docsSlice'; +import useBlockAutoFocus from '../../hook/useBlockAutoFocus'; +import { Block, Input, TextArea } from '@google-form-clone/shared-ui'; const TitleBlock = () => { - const TITLE_BLOCK_ID = "title"; + const TITLE_BLOCK_ID = 'title'; const dispatch = useAppDispatch(); const { title, content } = useAppSelector((store) => store.docs); - const { changeEditingBlockID, isEditing } = useChangeEditBlockID(TITLE_BLOCK_ID); + const { changeEditingBlockID, isEditing } = + useChangeEditBlockID(TITLE_BLOCK_ID); const { containerRef, questionInputRef } = useBlockAutoFocus(TITLE_BLOCK_ID); const changeTitle = (e: React.ChangeEvent) => { @@ -37,7 +36,11 @@ const TitleBlock = () => { placeholder="제목을 입력하세요" innerRef={questionInputRef} /> -