Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…eng-dong into feature/#319
  • Loading branch information
pakxe committed Aug 16, 2024
2 parents 0caac4a + a5bf87a commit e412f83
Show file tree
Hide file tree
Showing 20 changed files with 163 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface LabelGroupInputStyleProps {}

export interface LabelGroupInputCustomProps {
labelText: string;
errorText?: string;
errorText: string | null;
}

export type LabelGroupInputOptionProps = LabelGroupInputStyleProps & LabelGroupInputCustomProps;
Expand Down
8 changes: 5 additions & 3 deletions HDesign/src/components/LabelInput/LabelInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ const LabelInput: React.FC<LabelInputProps> = forwardRef<HTMLInputElement, Label
<Text size="caption" css={labelTextStyle(theme, hasFocus, !!htmlProps.value)}>
{labelText}
</Text>
<Text size="caption" css={errorTextStyle(theme, isError ?? false)}>
{errorText}
</Text>
{errorText && (
<Text size="caption" css={errorTextStyle(theme, isError ?? false)}>
{errorText}
</Text>
)}
</Flex>
<Flex flexDirection="column" gap="0.5rem">
<Input ref={inputRef} isError={isError} placeholder={labelText} {...htmlProps} />
Expand Down
2 changes: 1 addition & 1 deletion HDesign/src/components/LabelInput/LabelInput.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface LabelInputStyleProps {}

export interface LabelInputCustomProps {
labelText: string;
errorText?: string;
errorText: string | null;
isError?: boolean;
autoFocus: boolean;
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/ErrorProvider.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {FixedButton, LabelGroupInput} from 'haengdong-design';
import {useEffect} from 'react';

import validatePurchase from '@utils/validate/validatePurchase';
import {useStepList} from '@hooks/useStepList/useStepList';
Expand All @@ -15,6 +16,8 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList
const {
inputPairList,
inputRefList,
errorMessage,
errorIndexList,
handleInputChange,
getFilledInputPairList,
deleteEmptyInputPairElementOnBlur,
Expand All @@ -33,7 +36,7 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList
return (
<div css={style.container}>
<div css={style.inputContainer}>
<LabelGroupInput labelText="지출내역 / 금액">
<LabelGroupInput labelText="지출내역 / 금액" errorText={errorMessage}>
{inputPairList.map(({index, title, price}) => (
<div key={index} css={style.input}>
<LabelGroupInput.Element
Expand All @@ -45,6 +48,7 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList
onBlur={() => deleteEmptyInputPairElementOnBlur()} // TODO: (@weadie) 이 블러프롭이 내부적으로 index를 넘기고 있기 때문에 화살표 함수로 써야만하내요..
placeholder="지출 내역"
ref={el => (inputRefList.current[index * 2] = el)}
isError={errorIndexList.includes(index)}
/>
<LabelGroupInput.Element
elementKey={`${index}`}
Expand All @@ -55,6 +59,7 @@ const AddBillActionListModalContent = ({setIsOpenBottomSheet}: AddBillActionList
onBlur={() => deleteEmptyInputPairElementOnBlur()}
placeholder="금액"
ref={el => (inputRefList.current[index * 2 + 1] = el)}
isError={errorIndexList.includes(index)}
/>
</div>
))}
Expand Down
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
2 changes: 1 addition & 1 deletion client/src/components/Toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsxImportSource @emotion/react */
import {createContext, useContext, useEffect, useState} from 'react';

import SERVER_ERROR_MESSAGES from '@constants/errorMessage';
import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage';

import {useError} from '../../ErrorProvider';

Expand Down
5 changes: 2 additions & 3 deletions client/src/constants/errorMessage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type ErrorMessage = Record<string, string>;

const SERVER_ERROR_MESSAGES: ErrorMessage = {
export const SERVER_ERROR_MESSAGES: ErrorMessage = {
EVENT_NOT_FOUND: '존재하지 않는 행사입니다.',
EVENT_NAME_LENGTH_INVALID: '행사 이름은 2자 이상 30자 이하만 입력 가능합니다.',
EVENT_NAME_CONSECUTIVE_SPACES: '행사 이름에는 공백 문자가 연속될 수 없습니다.',
Expand Down Expand Up @@ -43,8 +43,7 @@ export const ERROR_MESSAGE = {
purchasePrice: '10,000,000원 이하의 숫자만 입력이 가능해요',
purchaseTitle: '지출 이름은 30자 이하의 한글, 영어, 숫자만 가능해요',
preventEmpty: '값은 비어있을 수 없어요',
invalidInput: '올바르지 않은 입력이에요.',
};

export const UNKNOWN_ERROR = 'UNKNOWN_ERROR';

export default SERVER_ERROR_MESSAGES;
92 changes: 21 additions & 71 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;
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('');
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,47 +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);

if (isValidInput) {
// 입력된 값이 유효하면 데이터(inputList)를 변경합니다.
setErrorMessage('');

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('');
changeErrorIndex(index);

changeInputListValue(index, value);
} else {
// 유효성 검사에 실패한 입력입니다. 에러 메세지를 세팅합니다.

const targetInput = findInputByIndex(index);

setErrorMessage(validationResultMessage ?? '');
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 @@ -110,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 @@ -123,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 @@ -134,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 @@ -171,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;
Loading

0 comments on commit e412f83

Please sign in to comment.