Skip to content

Commit

Permalink
feat: 공통 input 코드를 useInput hook으로 분리 (#379)
Browse files Browse the repository at this point in the history
* feat: 반복적으로 사용하는 Input 코드를 useInput 훅으로 분리

* refactor: errorMessage 상수화

* fix: useInput 분리에 따라 발생한 useSearchInMemberList 에러 해결

* feat: input에 빈문자열이 들어온다면 canSubmit을 false로 invalid한 입력값은 지우고 canSubmit은 true로 변경

* refactor: Input이 하나 인 곳에서 index를 입력하지 않아도 작동되도록 수정
  • Loading branch information
soi-ha authored Aug 16, 2024
1 parent 7d54702 commit a5bf87a
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) => {
handleCurrentInputIndex(inputIndex);
Expand Down
1 change: 1 addition & 0 deletions client/src/constants/errorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const ERROR_MESSAGE = {
purchasePrice: '10,000,000원 이하의 숫자만 입력이 가능해요',
purchaseTitle: '지출 이름은 30자 이하의 한글, 영어, 숫자만 가능해요',
preventEmpty: '값은 비어있을 수 없어요',
invalidInput: '올바르지 않은 입력이에요.',
};

export const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
89 changes: 21 additions & 68 deletions client/src/hooks/useDynamicInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<HTMLInputElement>) => void;
deleteEmptyInputElementOnBlur: () => void;
errorMessage: string | null;
getFilledInputList: (list?: InputValue[]) => InputValue[];
focusNextInputOnEnter: (e: React.KeyboardEvent<HTMLInputElement>, 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<InputValue[]>([{index: 0, value: ''}]);
const initialInputList = [{index: 0, value: ''}];
const inputRefList = useRef<(HTMLInputElement | null)[]>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [errorIndexList, setErrorIndexList] = useState<number[]>([]);
const [canSubmit, setCanSubmit] = useState(false);

const {inputList, errorMessage, errorIndexList, canSubmit, handleChange, setInputList} = useInput({
validateFunc,
initialInputList,
});

useEffect(() => {
if (inputRefList.current.length <= 0) return;
Expand All @@ -37,14 +42,13 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult): Return
}
}, [inputList]);

// event에서 value를 받아와서 새 인풋을 만들고 검증 후 set 하는 함수
const handleInputChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
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) {
// 마지막 인풋이 한 자라도 채워진다면 새로운 인풋을 생성해 간편한 다음 입력을 유도합니다.
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -131,30 +96,18 @@ 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<HTMLInputElement>, index: number) => {
if (e.nativeEvent.isComposing) return;

if (e.key === 'Enter') {
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);
};
Expand All @@ -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,
};
};

Expand Down
93 changes: 93 additions & 0 deletions client/src/hooks/useInput.tsx
Original file line number Diff line number Diff line change
@@ -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<T = InputValue> = {
inputList: T[];
errorMessage: string;
errorIndexList: number[];
canSubmit: boolean;
handleChange: (index: number, value: string) => void;
setInputList: React.Dispatch<React.SetStateAction<T[]>>;
addErrorIndex: (index: number) => void;
setCanSubmit: React.Dispatch<React.SetStateAction<boolean>>;
};

type UseInputProps<T = InputValue> = {
validateFunc: (value: string) => ValidateResult;
initialInputList: T[];
};

const useInput = <T extends InputValue>({validateFunc, initialInputList}: UseInputProps<T>): UseInputReturn<T> => {
const [inputList, setInputList] = useState<T[]>(initialInputList);
const [errorMessage, setErrorMessage] = useState('');
const [errorIndexList, setErrorIndexList] = useState<number[]>([]);
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;
6 changes: 2 additions & 4 deletions client/src/hooks/useSearchInMemberList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -47,7 +45,7 @@ const useSearchInMemberList = (

const chooseMember = (inputIndex: number, name: string) => {
setFilteredInMemberList([]);
validateAndSetTargetInput(inputIndex, name);
handleChange(inputIndex, name);
};

const searchCurrentInMember = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
Loading

0 comments on commit a5bf87a

Please sign in to comment.