From 7d54702f4fd11a455323b47dd6b93b18a8fb0338 Mon Sep 17 00:00:00 2001 From: JinHo Kim <81083461+jinhokim98@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:56:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Input=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20(#376)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: errorMessage 타입을 string | undefined에서 string | null로 변경 * refactor: 에러가 아닐 때 errorMessage를 null로 리턴 * fix: 에러 import 되지 않던 현상 해결 * refactor: 에러 메시지 validate 함수 리턴하는 대로 그대로 set * feat: 에러메시지 보여지도록 추가 * refactor: 에러메시지 타입 string | null로 변경 --- .../LabelGroupInput/LabelGroupInput.type.ts | 2 +- HDesign/src/components/LabelInput/LabelInput.tsx | 8 +++++--- HDesign/src/components/LabelInput/LabelInput.type.ts | 2 +- client/src/ErrorProvider.tsx | 2 +- .../AddBillActionListModalContent.tsx | 4 +++- client/src/components/Toast/ToastProvider.tsx | 2 +- client/src/constants/errorMessage.ts | 4 +--- client/src/hooks/useDynamicBillActionInput.tsx | 9 ++++----- client/src/hooks/useDynamicInput.tsx | 11 ++++------- client/src/hooks/usePutAndDeleteBillAction.ts | 8 ++++---- client/src/hooks/useSetPassword.ts | 5 ++--- client/src/utils/validate/type.ts | 2 +- client/src/utils/validate/validateEventName.ts | 4 ++-- client/src/utils/validate/validateEventPassword.ts | 4 ++-- client/src/utils/validate/validateMemberName.ts | 2 +- client/src/utils/validate/validatePurchase.ts | 6 +++--- 16 files changed, 36 insertions(+), 39 deletions(-) diff --git a/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts b/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts index cfa938457..8507311b5 100644 --- a/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts +++ b/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts @@ -2,7 +2,7 @@ export interface LabelGroupInputStyleProps {} export interface LabelGroupInputCustomProps { labelText: string; - errorText?: string; + errorText: string | null; } export type LabelGroupInputOptionProps = LabelGroupInputStyleProps & LabelGroupInputCustomProps; diff --git a/HDesign/src/components/LabelInput/LabelInput.tsx b/HDesign/src/components/LabelInput/LabelInput.tsx index f9dd3b71a..43f60c220 100644 --- a/HDesign/src/components/LabelInput/LabelInput.tsx +++ b/HDesign/src/components/LabelInput/LabelInput.tsx @@ -29,9 +29,11 @@ const LabelInput: React.FC = forwardRef {labelText} - - {errorText} - + {errorText && ( + + {errorText} + + )} diff --git a/HDesign/src/components/LabelInput/LabelInput.type.ts b/HDesign/src/components/LabelInput/LabelInput.type.ts index ec24b3a30..83e7e9751 100644 --- a/HDesign/src/components/LabelInput/LabelInput.type.ts +++ b/HDesign/src/components/LabelInput/LabelInput.type.ts @@ -2,7 +2,7 @@ export interface LabelInputStyleProps {} export interface LabelInputCustomProps { labelText: string; - errorText?: string; + errorText: string | null; isError?: boolean; autoFocus: boolean; } diff --git a/client/src/ErrorProvider.tsx b/client/src/ErrorProvider.tsx index ef0714c7f..92b93fb60 100644 --- a/client/src/ErrorProvider.tsx +++ b/client/src/ErrorProvider.tsx @@ -1,6 +1,6 @@ import {createContext, useState, useContext, useEffect, ReactNode} from 'react'; -import SERVER_ERROR_MESSAGES from '@constants/errorMessage'; +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; // 에러 컨텍스트 생성 interface ErrorContextType { diff --git a/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx index 239bc0d2d..cc49d5953 100644 --- a/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx +++ b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx @@ -1,4 +1,5 @@ import {FixedButton, LabelGroupInput} from 'haengdong-design'; +import {useEffect} from 'react'; import validatePurchase from '@utils/validate/validatePurchase'; @@ -15,6 +16,7 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList const { inputPairList, inputRefList, + errorMessage, errorIndexList, handleInputChange, getFilledInputPairList, @@ -34,7 +36,7 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList return (
- + {inputPairList.map(({index, title, price}) => (
; -const SERVER_ERROR_MESSAGES: ErrorMessage = { +export const SERVER_ERROR_MESSAGES: ErrorMessage = { EVENT_NOT_FOUND: '존재하지 않는 행사입니다.', EVENT_NAME_LENGTH_INVALID: '행사 이름은 2자 이상 30자 이하만 입력 가능합니다.', EVENT_NAME_CONSECUTIVE_SPACES: '행사 이름에는 공백 문자가 연속될 수 없습니다.', @@ -45,5 +45,3 @@ export const ERROR_MESSAGE = { }; export const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; - -export default SERVER_ERROR_MESSAGES; diff --git a/client/src/hooks/useDynamicBillActionInput.tsx b/client/src/hooks/useDynamicBillActionInput.tsx index 252b40862..f21311de8 100644 --- a/client/src/hooks/useDynamicBillActionInput.tsx +++ b/client/src/hooks/useDynamicBillActionInput.tsx @@ -14,7 +14,7 @@ export type BillInputType = 'title' | 'price'; const useDynamicBillActionInput = (validateFunc: (inputPair: Bill) => ValidateResult) => { const [inputPairList, setInputPairList] = useState([{title: '', price: '', index: 0}]); const inputRefList = useRef<(HTMLInputElement | null)[]>([]); - const [errorMessage, setErrorMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(null); const [errorIndexList, setErrorIndexList] = useState([]); const [canSubmit, setCanSubmit] = useState(false); @@ -34,13 +34,14 @@ const useDynamicBillActionInput = (validateFunc: (inputPair: Bill) => ValidateRe [field]: value, }); + setErrorMessage(validationResultMessage); + // TODO: (@weadie) 가독성이 안좋다는 리뷰. 함수로 분리 if ( isLastInputPairFilled({index, field, value}) && targetInputPair.title.trim().length !== 0 && targetInputPair.price.trim().length !== 0 ) { - setErrorMessage(''); setInputPairList(prevInputPairList => { const updatedInputPairList = [...prevInputPairList]; @@ -52,19 +53,17 @@ const useDynamicBillActionInput = (validateFunc: (inputPair: Bill) => ValidateRe }); } else if (isValidInput) { // 입력된 값이 유효하면 데이터(inputLis)를 변경합니다. - setErrorMessage(''); if (errorIndexList.includes(index)) { setErrorIndexList(prev => prev.filter(i => i !== index)); } changeInputListValue(index, value, field); } else if (value.length === 0) { - setErrorMessage(''); + setErrorMessage(null); changeErrorIndex(index); changeInputListValue(index, value, field); } else { - setErrorMessage(validationResultMessage ?? ''); changeErrorIndex(targetInputPair.index); } diff --git a/client/src/hooks/useDynamicInput.tsx b/client/src/hooks/useDynamicInput.tsx index 3584d3a05..77e25098c 100644 --- a/client/src/hooks/useDynamicInput.tsx +++ b/client/src/hooks/useDynamicInput.tsx @@ -12,7 +12,7 @@ export type ReturnUseDynamicInput = { inputRefList: React.MutableRefObject<(HTMLInputElement | null)[]>; handleInputChange: (index: number, event: React.ChangeEvent) => void; deleteEmptyInputElementOnBlur: () => void; - errorMessage: string; + errorMessage: string | null; getFilledInputList: (list?: InputValue[]) => InputValue[]; focusNextInputOnEnter: (e: React.KeyboardEvent, index: number) => void; canSubmit: boolean; @@ -23,7 +23,7 @@ export type ReturnUseDynamicInput = { const useDynamicInput = (validateFunc: (name: string) => ValidateResult): ReturnUseDynamicInput => { const [inputList, setInputList] = useState([{index: 0, value: ''}]); const inputRefList = useRef<(HTMLInputElement | null)[]>([]); - const [errorMessage, setErrorMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(null); const [errorIndexList, setErrorIndexList] = useState([]); const [canSubmit, setCanSubmit] = useState(false); @@ -62,11 +62,10 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return // onChange와 setValue 둘 다 지원하기 위해서 validate를 분리 const validateAndSetTargetInput = (index: number, value: string) => { const {isValid: isValidInput, errorMessage: validationResultMessage} = validateFunc(value); + setErrorMessage(validationResultMessage); if (isValidInput) { // 입력된 값이 유효하면 데이터(inputList)를 변경합니다. - setErrorMessage(''); - if (errorIndexList.includes(index)) { setErrorIndexList(prev => prev.filter(i => i !== index)); } @@ -75,7 +74,7 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return } else if (value.length === 0) { // value의 값이 0이라면 errorMessage는 띄워지지 않지만 값은 변경됩니다. 또한 invalid한 값이기에 errorIndex에 추가합니다. - setErrorMessage(''); + setErrorMessage(null); changeErrorIndex(index); changeInputListValue(index, value); @@ -83,8 +82,6 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return // 유효성 검사에 실패한 입력입니다. 에러 메세지를 세팅합니다. const targetInput = findInputByIndex(index); - - setErrorMessage(validationResultMessage ?? ''); changeErrorIndex(targetInput.index); } }; diff --git a/client/src/hooks/usePutAndDeleteBillAction.ts b/client/src/hooks/usePutAndDeleteBillAction.ts index 9cb399ef8..925f591e8 100644 --- a/client/src/hooks/usePutAndDeleteBillAction.ts +++ b/client/src/hooks/usePutAndDeleteBillAction.ts @@ -12,7 +12,7 @@ import {useFetch} from '@apis/useFetch'; import getEventIdByUrl from '@utils/getEventIdByUrl'; -import ERROR_MESSAGE from '@constants/errorMessage'; +import {ERROR_MESSAGE} from '@constants/errorMessage'; const usePutAndDeleteBillAction = ( initialValue: InputPair, @@ -26,7 +26,7 @@ const usePutAndDeleteBillAction = ( const [inputPair, setInputPair] = useState(initialValue); const [canSubmit, setCanSubmit] = useState(false); const [errorInfo, setErrorInfo] = useState>({title: false, price: false}); - const [errorMessage, setErrorMessage] = useState(); + const [errorMessage, setErrorMessage] = useState(null); const handleInputChange = (field: BillInputType, event: React.ChangeEvent) => { const {value} = event.target; @@ -42,9 +42,10 @@ const usePutAndDeleteBillAction = ( const {isValid, errorMessage, errorInfo} = validateFunc(getFieldValue()); + setErrorMessage(errorMessage); + if (isValid) { // valid일 경우 에러메시지 nope, setValue, submit은 value가 비지 않았을 때 true를 설정 - setErrorMessage(undefined); setInputPair(prevInputPair => { return { ...prevInputPair, @@ -55,7 +56,6 @@ const usePutAndDeleteBillAction = ( } else { // valid하지 않으면 event.target.value 덮어쓰기 event.target.value = inputPair[field]; - setErrorMessage(errorMessage); setCanSubmit(false); } diff --git a/client/src/hooks/useSetPassword.ts b/client/src/hooks/useSetPassword.ts index 0ba5e7272..8e8cf9c8c 100644 --- a/client/src/hooks/useSetPassword.ts +++ b/client/src/hooks/useSetPassword.ts @@ -11,7 +11,7 @@ import useEvent from './useEvent'; const useSetPassword = (eventName: string) => { const {fetch} = useFetch(); const [password, setPassword] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(null); const [canSubmit, setCanSubmit] = useState(false); const {createNewEvent} = useEvent(); @@ -27,13 +27,12 @@ const useSetPassword = (eventName: string) => { const {isValid, errorMessage} = validateEventPassword(newValue); setCanSubmit(newValue.length === RULE.maxEventPasswordLength); + setErrorMessage(errorMessage); if (isValid) { setPassword(newValue); - setErrorMessage(''); } else { event.target.value = password; - setErrorMessage(errorMessage ?? ''); } }; diff --git a/client/src/utils/validate/type.ts b/client/src/utils/validate/type.ts index ba8b0953b..df81f1644 100644 --- a/client/src/utils/validate/type.ts +++ b/client/src/utils/validate/type.ts @@ -1,5 +1,5 @@ export interface ValidateResult { isValid: boolean; - errorMessage?: string; + errorMessage: string | null; errorInfo?: Record; } diff --git a/client/src/utils/validate/validateEventName.ts b/client/src/utils/validate/validateEventName.ts index b390f12b9..93f4ecef1 100644 --- a/client/src/utils/validate/validateEventName.ts +++ b/client/src/utils/validate/validateEventName.ts @@ -1,4 +1,4 @@ -import ERROR_MESSAGE from '@constants/errorMessage'; +import {ERROR_MESSAGE} from '@constants/errorMessage'; import RULE from '@constants/rule'; import {ValidateResult} from './type'; @@ -7,7 +7,7 @@ const validateEventName = (name: string): ValidateResult => { if (name.length > RULE.maxEventNameLength) { return {isValid: false, errorMessage: ERROR_MESSAGE.eventName}; } - return {isValid: true}; + return {isValid: true, errorMessage: null}; }; export default validateEventName; diff --git a/client/src/utils/validate/validateEventPassword.ts b/client/src/utils/validate/validateEventPassword.ts index 70499e393..7075e2f3e 100644 --- a/client/src/utils/validate/validateEventPassword.ts +++ b/client/src/utils/validate/validateEventPassword.ts @@ -1,4 +1,4 @@ -import ERROR_MESSAGE from '@constants/errorMessage'; +import {ERROR_MESSAGE} from '@constants/errorMessage'; import REGEXP from '@constants/regExp'; import {ValidateResult} from './type'; @@ -7,7 +7,7 @@ const validateEventPassword = (password: string): ValidateResult => { if (!REGEXP.eventPassword.test(password)) { return {isValid: false, errorMessage: ERROR_MESSAGE.eventPasswordType}; } - return {isValid: true}; + return {isValid: true, errorMessage: null}; }; export default validateEventPassword; diff --git a/client/src/utils/validate/validateMemberName.ts b/client/src/utils/validate/validateMemberName.ts index 06c7416dd..053028761 100644 --- a/client/src/utils/validate/validateMemberName.ts +++ b/client/src/utils/validate/validateMemberName.ts @@ -16,7 +16,7 @@ const validateMemberName = (name: string): ValidateResult => { }; if (validateOnlyString() && validateLength()) { - return {isValid: true}; + return {isValid: true, errorMessage: null}; } return {isValid: false, errorMessage: ERROR_MESSAGE.memberName}; diff --git a/client/src/utils/validate/validatePurchase.ts b/client/src/utils/validate/validatePurchase.ts index 7c7a4b3d6..9915ce8b7 100644 --- a/client/src/utils/validate/validatePurchase.ts +++ b/client/src/utils/validate/validatePurchase.ts @@ -1,6 +1,6 @@ import type {Bill} from 'types/serviceType'; -import ERROR_MESSAGE from '@constants/errorMessage'; +import {ERROR_MESSAGE} from '@constants/errorMessage'; import RULE from '@constants/rule'; import REGEXP from '@constants/regExp'; @@ -8,7 +8,7 @@ import {ValidateResult} from './type'; const validatePurchase = (inputPair: Bill): ValidateResult => { const {title, price} = inputPair; - let errorMessage; + let errorMessage: string | null = null; const errorInfo = { price: false, @@ -38,7 +38,7 @@ const validatePurchase = (inputPair: Bill): ValidateResult => { }; if (validatePrice() && validateTitle()) { - return {isValid: true, errorMessage: ''}; + return {isValid: true, errorMessage: null}; } return {isValid: false, errorMessage, errorInfo}; From a5bf87aa21ecc811b0b3297816d4353ef4318b37 Mon Sep 17 00:00:00 2001 From: Soyeon Choe <77609591+soi-ha@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:00:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20input=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20useInput=20hook=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20(#379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 반복적으로 사용하는 Input 코드를 useInput 훅으로 분리 * refactor: errorMessage 상수화 * fix: useInput 분리에 따라 발생한 useSearchInMemberList 에러 해결 * feat: input에 빈문자열이 들어온다면 canSubmit을 false로 invalid한 입력값은 지우고 canSubmit은 true로 변경 * refactor: Input이 하나 인 곳에서 index를 입력하지 않아도 작동되도록 수정 --- .../OutMember.tsx | 4 +- client/src/constants/errorMessage.ts | 1 + client/src/hooks/useDynamicInput.tsx | 89 +++++------------- client/src/hooks/useInput.tsx | 93 +++++++++++++++++++ client/src/hooks/useSearchInMemberList.ts | 6 +- client/src/hooks/useSetAllMemberList.tsx | 68 ++++++-------- client/src/utils/isArraysEqual.ts | 2 +- .../src/utils/validate/validateMemberName.ts | 13 ++- 8 files changed, 157 insertions(+), 119 deletions(-) create mode 100644 client/src/hooks/useInput.tsx diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx index d139261fd..949d47fa9 100644 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx @@ -15,10 +15,10 @@ const OutMember = ({dynamicProps}: OutMemberProps) => { deleteEmptyInputElementOnBlur, focusNextInputOnEnter, handleInputChange, - validateAndSetTargetInput, + handleChange, } = dynamicProps; const {currentInputIndex, filteredInMemberList, handleCurrentInputIndex, searchCurrentInMember, chooseMember} = - useSearchInMemberList(validateAndSetTargetInput); + useSearchInMemberList(handleChange); const validationAndSearchOnChange = (inputIndex: number, event: React.ChangeEvent) => { handleCurrentInputIndex(inputIndex); diff --git a/client/src/constants/errorMessage.ts b/client/src/constants/errorMessage.ts index f874f6ff4..71354bd99 100644 --- a/client/src/constants/errorMessage.ts +++ b/client/src/constants/errorMessage.ts @@ -42,6 +42,7 @@ export const ERROR_MESSAGE = { purchasePrice: '10,000,000원 이하의 숫자만 입력이 가능해요', purchaseTitle: '지출 이름은 30자 이하의 한글, 영어, 숫자만 가능해요', preventEmpty: '값은 비어있을 수 없어요', + invalidInput: '올바르지 않은 입력이에요.', }; export const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; diff --git a/client/src/hooks/useDynamicInput.tsx b/client/src/hooks/useDynamicInput.tsx index 77e25098c..cd639d770 100644 --- a/client/src/hooks/useDynamicInput.tsx +++ b/client/src/hooks/useDynamicInput.tsx @@ -2,6 +2,8 @@ import {useEffect, useRef, useState} from 'react'; import {ValidateResult} from '@utils/validate/type'; +import useInput from './useInput'; + type InputValue = { value: string; index: number; @@ -10,22 +12,25 @@ type InputValue = { export type ReturnUseDynamicInput = { inputList: InputValue[]; inputRefList: React.MutableRefObject<(HTMLInputElement | null)[]>; + errorMessage: string | null; + canSubmit: boolean; + errorIndexList: number[]; + handleChange: (index: number, value: string) => void; handleInputChange: (index: number, event: React.ChangeEvent) => void; deleteEmptyInputElementOnBlur: () => void; - errorMessage: string | null; getFilledInputList: (list?: InputValue[]) => InputValue[]; focusNextInputOnEnter: (e: React.KeyboardEvent, index: number) => void; - canSubmit: boolean; - errorIndexList: number[]; - validateAndSetTargetInput: (index: number, value: string) => void; + setInputValueTargetIndex: (index: number, value: string) => void; }; const useDynamicInput = (validateFunc: (name: string) => ValidateResult): ReturnUseDynamicInput => { - const [inputList, setInputList] = useState([{index: 0, value: ''}]); + const initialInputList = [{index: 0, value: ''}]; const inputRefList = useRef<(HTMLInputElement | null)[]>([]); - const [errorMessage, setErrorMessage] = useState(null); - const [errorIndexList, setErrorIndexList] = useState([]); - const [canSubmit, setCanSubmit] = useState(false); + + const {inputList, errorMessage, errorIndexList, canSubmit, handleChange, setInputList} = useInput({ + validateFunc, + initialInputList, + }); useEffect(() => { if (inputRefList.current.length <= 0) return; @@ -37,14 +42,13 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return } }, [inputList]); - // event에서 value를 받아와서 새 인풋을 만들고 검증 후 set 하는 함수 const handleInputChange = (index: number, event: React.ChangeEvent) => { const {value} = event.target; + makeNewInputWhenFirstCharacterInput(index, value); - validateAndSetTargetInput(index, value); + handleChange(index, value); }; - // 첫 번째 문자가 입력됐을 때 새로운 인풋이 생기는 기능 분리 const makeNewInputWhenFirstCharacterInput = (index: number, value: string) => { if (isLastInputFilled(index, value) && value.trim().length !== 0) { // 마지막 인풋이 한 자라도 채워진다면 새로운 인풋을 생성해 간편한 다음 입력을 유도합니다. @@ -59,44 +63,6 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return } }; - // onChange와 setValue 둘 다 지원하기 위해서 validate를 분리 - const validateAndSetTargetInput = (index: number, value: string) => { - const {isValid: isValidInput, errorMessage: validationResultMessage} = validateFunc(value); - setErrorMessage(validationResultMessage); - - if (isValidInput) { - // 입력된 값이 유효하면 데이터(inputList)를 변경합니다. - if (errorIndexList.includes(index)) { - setErrorIndexList(prev => prev.filter(i => i !== index)); - } - - changeInputListValue(index, value); - } else if (value.length === 0) { - // value의 값이 0이라면 errorMessage는 띄워지지 않지만 값은 변경됩니다. 또한 invalid한 값이기에 errorIndex에 추가합니다. - - setErrorMessage(null); - changeErrorIndex(index); - - changeInputListValue(index, value); - } else { - // 유효성 검사에 실패한 입력입니다. 에러 메세지를 세팅합니다. - - const targetInput = findInputByIndex(index); - changeErrorIndex(targetInput.index); - } - }; - - // inputList가 변했을 때 canSubmit이 반영되도록 - // setValue가 수행되기 전에 handleCanSubmit이 실행되어 새로운 입력값에 대한 검증이 되지 않는 버그를 해결 - useEffect(() => { - handleCanSubmit(); - }, [inputList]); - - // 현재까지 입력된 값들로 submit을 할 수 있는지 여부를 핸들합니다. - const handleCanSubmit = () => { - setCanSubmit(inputList.length > 0 && getFilledInputList().length > 0 && errorIndexList.length === 0); - }; - const deleteEmptyInputElementOnBlur = () => { // 0, 1번 input이 값이 있는 상태에서 두 input의 값을 모두 x버튼으로 제거해도 input이 2개 남아있는 문제를 위해 조건문을 추가했습니다. if (getFilledInputList().length === 0 && inputList.length > 1) { @@ -107,7 +73,6 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return // *표시 조건문은 처음에 input을 클릭했다가 블러시켰을 때 filledInputList가 아예 없어 .index에 접근할 때 오류가 납니다. 이를 위한 얼리리턴을 두었습니다. if (getFilledInputList().length === 0) return; - // * if (getFilledInputList().length !== inputList.length) { setInputList(inputList => { const filledInputList = getFilledInputList(inputList); @@ -120,7 +85,7 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return } }; - const changeInputListValue = (index: number, value: string) => { + const setInputValueTargetIndex = (index: number, value: string) => { setInputList(prevInputs => { const updatedInputList = [...prevInputs]; const targetInput = findInputByIndex(index, updatedInputList); @@ -131,15 +96,6 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return }); }; - const changeErrorIndex = (index: number) => { - setErrorIndexList(prev => { - if (!prev.includes(index)) { - return [...prev, index]; - } - return prev; - }); - }; - const focusNextInputOnEnter = (e: React.KeyboardEvent, index: number) => { if (e.nativeEvent.isComposing) return; @@ -147,14 +103,11 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return inputRefList.current[index + 1]?.focus(); } }; - // 아래부터는 이 훅에서 재사용되는 함수입니다. - // list 인자를 넘겨주면 그 인자로 찾고, 없다면 inputList state를 사용합니다. const findInputByIndex = (index: number, list?: InputValue[]) => { return (list ?? inputList).filter(input => input.index === index)[0]; }; - // list 인자를 넘겨주면 그 인자로 찾고, 없다면 inputList state를 사용합니다. const getFilledInputList = (list?: InputValue[]) => { return (list ?? inputList).filter(({value}) => value.trim().length !== 0); }; @@ -168,15 +121,15 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return return { inputList, inputRefList, + errorMessage, + canSubmit, + errorIndexList, handleInputChange, + handleChange, deleteEmptyInputElementOnBlur, - errorMessage, getFilledInputList, focusNextInputOnEnter, - canSubmit, - errorIndexList, - validateAndSetTargetInput, - // TODO: (@weadie) 네이밍 수정 + setInputValueTargetIndex, }; }; diff --git a/client/src/hooks/useInput.tsx b/client/src/hooks/useInput.tsx new file mode 100644 index 000000000..1b90805a2 --- /dev/null +++ b/client/src/hooks/useInput.tsx @@ -0,0 +1,93 @@ +import {useEffect, useState} from 'react'; + +import {ValidateResult} from '@utils/validate/type'; + +import {ERROR_MESSAGE} from '@constants/errorMessage'; + +export type InputValue = { + value: string; + index?: number; +}; + +export type UseInputReturn = { + inputList: T[]; + errorMessage: string; + errorIndexList: number[]; + canSubmit: boolean; + handleChange: (index: number, value: string) => void; + setInputList: React.Dispatch>; + addErrorIndex: (index: number) => void; + setCanSubmit: React.Dispatch>; +}; + +type UseInputProps = { + validateFunc: (value: string) => ValidateResult; + initialInputList: T[]; +}; + +const useInput = ({validateFunc, initialInputList}: UseInputProps): UseInputReturn => { + const [inputList, setInputList] = useState(initialInputList); + const [errorMessage, setErrorMessage] = useState(''); + const [errorIndexList, setErrorIndexList] = useState([]); + const [canSubmit, setCanSubmit] = useState(false); + + useEffect(() => { + changeCanSubmit(); + }, [errorMessage, errorIndexList]); + + const handleChange = (index: number = 0, value: string) => { + const {isValid, errorMessage: validationResultMessage} = validateFunc(value); + + if (validationResultMessage === ERROR_MESSAGE.preventEmpty) { + setErrorMessage(validationResultMessage); + updateInputList(index, value); + addErrorIndex(index); + } else if (isValid && value.length !== 0) { + // TODO: (@soha) 쿠키가 작업한 errorMessage를 위로 올리기 변경 추후에 merge후에 반영하기 + setErrorMessage(''); + updateInputList(index, value); + removeErrorIndex(index); + } + }; + + const updateInputList = (index: number, value: string) => { + setInputList(prev => { + const newList = [...prev]; + const targetInput = newList.find(input => input.index === index); + if (targetInput) { + targetInput.value = value; + } + return newList; + }); + }; + + const removeErrorIndex = (index: number) => { + setErrorIndexList(prev => prev.filter(i => i !== index)); + }; + + const addErrorIndex = (index: number) => { + setErrorIndexList(prev => { + if (!prev.includes(index)) { + return [...prev, index]; + } + return prev; + }); + }; + + const changeCanSubmit = () => { + setCanSubmit(errorIndexList.length || errorMessage.length ? false : true); + }; + + return { + inputList, + errorMessage, + errorIndexList, + canSubmit, + handleChange, + setInputList, + addErrorIndex, + setCanSubmit, + }; +}; + +export default useInput; diff --git a/client/src/hooks/useSearchInMemberList.ts b/client/src/hooks/useSearchInMemberList.ts index 04514d689..ed7df41a3 100644 --- a/client/src/hooks/useSearchInMemberList.ts +++ b/client/src/hooks/useSearchInMemberList.ts @@ -14,9 +14,7 @@ export type ReturnUseSearchInMemberList = { chooseMember: (inputIndex: number, name: string) => void; }; -const useSearchInMemberList = ( - validateAndSetTargetInput: (index: number, value: string) => void, -): ReturnUseSearchInMemberList => { +const useSearchInMemberList = (handleChange: (index: number, value: string) => void): ReturnUseSearchInMemberList => { const eventId = getEventIdByUrl(); const {fetch} = useFetch(); @@ -47,7 +45,7 @@ const useSearchInMemberList = ( const chooseMember = (inputIndex: number, name: string) => { setFilteredInMemberList([]); - validateAndSetTargetInput(inputIndex, name); + handleChange(inputIndex, name); }; const searchCurrentInMember = (event: React.ChangeEvent) => { diff --git a/client/src/hooks/useSetAllMemberList.tsx b/client/src/hooks/useSetAllMemberList.tsx index 44066dbf6..69fd4ba96 100644 --- a/client/src/hooks/useSetAllMemberList.tsx +++ b/client/src/hooks/useSetAllMemberList.tsx @@ -9,6 +9,7 @@ import isArraysEqual from '@utils/isArraysEqual'; import getEventIdByUrl from '@utils/getEventIdByUrl'; import {useStepList} from './useStepList'; +import useInput from './useInput'; interface UseSetAllMemberListProps { validateFunc: (name: string) => ValidateResult; @@ -16,18 +17,37 @@ interface UseSetAllMemberListProps { handleCloseAllMemberListModal: () => void; } +interface UseSetAllMemberListReturns { + editedAllMemberList: string[]; + canSubmit: boolean; + errorMessage: string; + errorIndexList: number[]; + handleNameChange: (index: number, event: React.ChangeEvent) => void; + handleClickDeleteButton: (index: number) => Promise; + handlePutAllMemberList: () => Promise; +} + const useSetAllMemberList = ({ validateFunc, allMemberList, handleCloseAllMemberListModal, -}: UseSetAllMemberListProps) => { - const [editedAllMemberList, setEditedAllMemberList] = useState(allMemberList); - const [errorMessage, setErrorMessage] = useState(''); - const [errorIndexList, setErrorIndexList] = useState([]); - const [canSubmit, setCanSubmit] = useState(false); +}: UseSetAllMemberListProps): UseSetAllMemberListReturns => { + const initialInputList = allMemberList.map((name, index) => ({index, value: name})); + const { + inputList, + errorMessage, + errorIndexList, + canSubmit, + handleChange, + setInputList: setEditedAllMemberList, + setCanSubmit, + } = useInput({validateFunc, initialInputList}); + const [deleteInOriginal, setDeleteInOriginal] = useState(allMemberList); const [deleteMemberList, setDeleteMemberList] = useState([]); + const editedAllMemberList = inputList.map(input => input.value); + const {refreshStepList} = useStepList(); const eventId = getEventIdByUrl(); const {fetch} = useFetch(); @@ -38,35 +58,8 @@ const useSetAllMemberList = ({ const handleNameChange = (index: number, event: React.ChangeEvent) => { const {value} = event.target; - const {isValid, errorMessage: validationResultMessage} = validateFunc(value); - - if (isValid && value.length !== 0) { - setErrorMessage(''); - - setEditedAllMemberList(prev => { - const newList = [...prev]; - newList[index] = value; - return newList; - }); - setErrorIndexList(prev => prev.filter(i => i !== index)); - - setCanSubmit(true); - } else if (value.length === 0) { - setErrorMessage(''); - - setEditedAllMemberList(prev => { - const newList = [...prev]; - newList[index] = value; - return newList; - }); - - changeErrorIndex(index); - } else { - setErrorMessage(validationResultMessage ?? ''); - - changeErrorIndex(index); - } + handleChange(index, value); }; const handleClickDeleteButton = async (index: number) => { @@ -110,15 +103,6 @@ const useSetAllMemberList = ({ }); }; - const changeErrorIndex = (index: number) => { - setErrorIndexList(prev => { - if (!prev.includes(index)) { - return [...prev, index]; - } - return prev; - }); - }; - return { editedAllMemberList, canSubmit, diff --git a/client/src/utils/isArraysEqual.ts b/client/src/utils/isArraysEqual.ts index 1f033eefc..df73a8552 100644 --- a/client/src/utils/isArraysEqual.ts +++ b/client/src/utils/isArraysEqual.ts @@ -1,4 +1,4 @@ -const isArraysEqual = (arr1: string[], arr2: string[]) => { +const isArraysEqual = (arr1: T[], arr2: T[]) => { if (arr1.length !== arr2.length) return false; // 배열을 정렬한 후 비교 diff --git a/client/src/utils/validate/validateMemberName.ts b/client/src/utils/validate/validateMemberName.ts index 053028761..e73f42271 100644 --- a/client/src/utils/validate/validateMemberName.ts +++ b/client/src/utils/validate/validateMemberName.ts @@ -5,6 +5,7 @@ import RULE from '@constants/rule'; import {ValidateResult} from './type'; const validateMemberName = (name: string): ValidateResult => { + let errorMessage = null; const validateOnlyString = () => { if (!REGEXP.memberName.test(name)) return false; return true; @@ -15,11 +16,19 @@ const validateMemberName = (name: string): ValidateResult => { return true; }; - if (validateOnlyString() && validateLength()) { + const validateEmpty = () => { + if (!name.trim().length) { + errorMessage = ERROR_MESSAGE.preventEmpty; + return false; + } + return true; + }; + + if (validateOnlyString() && validateLength() && validateEmpty()) { return {isValid: true, errorMessage: null}; } - return {isValid: false, errorMessage: ERROR_MESSAGE.memberName}; + return {isValid: false, errorMessage: errorMessage || ERROR_MESSAGE.memberName}; }; export default validateMemberName;