From 2c1fd7357e342d83a4659b1add594bd0026532d1 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Tue, 23 Apr 2024 16:08:31 +0900 Subject: [PATCH 01/40] =?UTF-8?q?refactor:=20errMsg=20->=20errorMessage?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CardNumberInput/CardNumberInput.tsx | 4 ++-- .../ExpirationDateInput/ExpirationDateInput.tsx | 4 ++-- src/components/OwnernameInput/OwnerNameInput.tsx | 4 ++-- src/components/common/Field/Field.module.css | 2 +- src/components/common/Field/Field.tsx | 6 +++--- src/hooks/useAddCardInput.tsx | 13 ++++++------- src/stories/Field.stories.tsx | 6 +++--- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/CardNumberInput/CardNumberInput.tsx b/src/components/CardNumberInput/CardNumberInput.tsx index 346896ff08..01adb10d7b 100644 --- a/src/components/CardNumberInput/CardNumberInput.tsx +++ b/src/components/CardNumberInput/CardNumberInput.tsx @@ -44,7 +44,7 @@ export default function CardNumberInput({ setCardData }: CardNumberInputProps) { const { values: cardNumbers, - errMsg, + errorMessage, isError, onChange, onBlur, @@ -60,7 +60,7 @@ export default function CardNumberInput({ setCardData }: CardNumberInputProps) { title={title} description={description} labelText={labelText} - errMsg={errMsg} + errorMessage={errorMessage} > {Object.keys(cardNumbers).map((n) => { const name = n as keyof CardNumbers; diff --git a/src/components/ExpirationDateInput/ExpirationDateInput.tsx b/src/components/ExpirationDateInput/ExpirationDateInput.tsx index 949875b3ee..29b3ca159d 100644 --- a/src/components/ExpirationDateInput/ExpirationDateInput.tsx +++ b/src/components/ExpirationDateInput/ExpirationDateInput.tsx @@ -53,7 +53,7 @@ const ExpirationDateInput = ({ setCardData }: ExpirationDateInputProps) => { const { values: expirationDate, - errMsg, + errorMessage, isError, onChange, onBlur, @@ -69,7 +69,7 @@ const ExpirationDateInput = ({ setCardData }: ExpirationDateInputProps) => { title={title} description={description} labelText={labelText} - errMsg={errMsg} + errorMessage={errorMessage} > {Object.keys(expirationDate).map((n) => { const name = n as keyof ExpirationDate; diff --git a/src/components/OwnernameInput/OwnerNameInput.tsx b/src/components/OwnernameInput/OwnerNameInput.tsx index 8fa94b1726..7e4c30a620 100644 --- a/src/components/OwnernameInput/OwnerNameInput.tsx +++ b/src/components/OwnernameInput/OwnerNameInput.tsx @@ -34,7 +34,7 @@ function OwnerNameInput({ setCardData }: OwnerNameInputProps) { const { values: ownerName, - errMsg, + errorMessage, isError, onChange, onBlur, @@ -45,7 +45,7 @@ function OwnerNameInput({ setCardData }: OwnerNameInputProps) { }); return ( - + {Object.keys(ownerName).map((n) => { const name = n as keyof OwnerName; return ( diff --git a/src/components/common/Field/Field.module.css b/src/components/common/Field/Field.module.css index ffc0c98d65..f942bbc6d1 100644 --- a/src/components/common/Field/Field.module.css +++ b/src/components/common/Field/Field.module.css @@ -38,7 +38,7 @@ margin-bottom: 8px; } -.errMsg { +.errorMsg { color: var(--color-red); font-weight: 400; font-size: 10px; diff --git a/src/components/common/Field/Field.tsx b/src/components/common/Field/Field.tsx index e9d2b1f49b..44eef22f86 100644 --- a/src/components/common/Field/Field.tsx +++ b/src/components/common/Field/Field.tsx @@ -6,7 +6,7 @@ interface FieldProps { title: string; description?: string; labelText: string; - errMsg: string; + errorMessage: string; children: React.ReactNode; } @@ -15,7 +15,7 @@ export default function Field({ description, labelText, children, - errMsg, + errorMessage, }: FieldProps) { return (
@@ -27,7 +27,7 @@ export default function Field({ {children} -

{errMsg}

+

{errorMessage}

); } diff --git a/src/hooks/useAddCardInput.tsx b/src/hooks/useAddCardInput.tsx index 78813813a4..b0f4f08795 100644 --- a/src/hooks/useAddCardInput.tsx +++ b/src/hooks/useAddCardInput.tsx @@ -28,21 +28,20 @@ export default function useAddCardInput({ updateCardData, }: UseAddCardInputProps) { const [values, setValues] = useState(initialValues); + const [errorMessage, setErrorMessage] = useState(''); const [isError, setIsError] = useState( createObjectWithKeys(Object.keys(initialValues), false) ); - const [errMsg, setErrMsg] = useState(''); - const onChange = (event: React.ChangeEvent) => { const { name, value } = event.target; const validation = validateInputOnChange({ name, value }); if (!validation.isValid) { - setErrMsg(validation.errorMsg); + setErrorMessage(validation.errorMsg); setIsError({ ...isError, [name]: true }); } else { - setErrMsg(''); + setErrorMessage(''); setIsError({ ...isError, [name]: false }); setValues({ ...values, @@ -58,10 +57,10 @@ export default function useAddCardInput({ const validation = validateInputOnBlur({ name, value }); if (!validation.isValid) { - setErrMsg(validation.errorMsg); + setErrorMessage(validation.errorMsg); setIsError({ ...isError, [name]: true }); } else { - setErrMsg(''); + setErrorMessage(''); setIsError({ ...isError, [name]: false }); updateCardData(); } @@ -72,7 +71,7 @@ export default function useAddCardInput({ return { values, - errMsg, + errorMessage, isError, onChange, onBlur, diff --git a/src/stories/Field.stories.tsx b/src/stories/Field.stories.tsx index eb27872c54..4b5edadcfd 100644 --- a/src/stories/Field.stories.tsx +++ b/src/stories/Field.stories.tsx @@ -27,7 +27,7 @@ export const CardNumbers: Story = { title: '결제할 카드 번호를 입력해 주세요', description: '본인 명의의 카드만 결제 가능합니다.', labelText: '카드 번호', - errMsg: '', + errorMessage: '', children: ( <> {CARD_NUMBERS_FIELDS.map((name, index) => ( @@ -57,7 +57,7 @@ export const ExpirationDate: Story = { title: '카드 유효기간을 입력해 주세요', description: '월/년도(MMYY)를 순서대로 입력해 주세요', labelText: '유효기간', - errMsg: '', + errorMessage: '', children: ( <> {EXPIRATION_DATE_FIELDS.map((name, index) => ( @@ -85,7 +85,7 @@ export const OwnerName: Story = { args: { title: '카드 소유자 이름을 입력해 주세요', labelText: '소유자 이름', - errMsg: '', + errorMessage: '', children: ( <> {Array.from({ length: 1 }).map((_, index) => ( From 1b9ab8c699520860aeb2216e87909620e7bb4a63 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Wed, 24 Apr 2024 00:02:15 +0900 Subject: [PATCH 02/40] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B4=80=EB=A6=AC=EB=90=98=EB=8D=98=20state=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20validation=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 43 +++++---- .../CardNumberInput/CardNumberInput.tsx | 72 +++++++-------- src/components/CardPreview/CardPreview.tsx | 12 ++- .../ExpirationDateInput.tsx | 92 +++++++++---------- .../OwnernameInput/OwnerNameInput.tsx | 71 ++++++++------ src/constants/form.ts | 17 ++++ src/constants/messages.ts | 5 +- src/domain/validators.ts | 4 + src/hooks/useAddCardFormField.ts | 67 ++++++++++++++ src/hooks/useAddCardInput.tsx | 79 ---------------- src/types/CardInfo.d.ts | 45 +++++++-- src/utils/validateInput.ts | 11 +++ 12 files changed, 292 insertions(+), 226 deletions(-) create mode 100644 src/constants/form.ts create mode 100644 src/hooks/useAddCardFormField.ts delete mode 100644 src/hooks/useAddCardInput.tsx create mode 100644 src/utils/validateInput.ts diff --git a/src/App.tsx b/src/App.tsx index 0942e2e0d5..1aaa29b6d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,41 +1,44 @@ import './index.css'; import styles from './App.module.css'; -import { useState } from 'react'; - -import CardPreview from './components/CardPreview/CardPreview'; +import useFormField from './hooks/useAddCardFormField'; import CardNumberInput from './components/CardNumberInput/CardNumberInput'; +import CardPreview from './components/CardPreview/CardPreview'; import ExpirationDateInput from './components/ExpirationDateInput/ExpirationDateInput'; import OwnerNameInput from './components/OwnerNameInput/OwnerNameInput'; +import { INITIAL_VALUES } from './constants/form'; + function App() { - const [cardInfo, setCardInfo] = useState({ - cardNumbers: ['', '', '', ''], - expirationDate: ['', ''], - ownerName: '', + const { values: cardNumbers, ...cardNumbersProps } = + useFormField({ + initialValues: INITIAL_VALUES.cardNumbers, + }); + const { values: expirationDate, ...expirationDateProps } = + useFormField({ + initialValues: INITIAL_VALUES.expirationDate, + }); + const { values: ownerName, ...ownerNameProps } = useFormField({ + initialValues: INITIAL_VALUES.ownerName, }); - const setCardData = ( - key: keyof CardInfo, - newData: CardInfo[keyof CardInfo] - ) => { - setCardInfo({ ...cardInfo, [key]: newData }); - }; - return (

카드 추가

- - - + + +
); diff --git a/src/components/CardNumberInput/CardNumberInput.tsx b/src/components/CardNumberInput/CardNumberInput.tsx index 01adb10d7b..180b29ebd2 100644 --- a/src/components/CardNumberInput/CardNumberInput.tsx +++ b/src/components/CardNumberInput/CardNumberInput.tsx @@ -1,60 +1,52 @@ -import Input from '../common/Input/Input'; +import { Fragment } from 'react/jsx-runtime'; import Field from '../common/Field/Field'; +import Input from '../common/Input/Input'; import Label from '../common/Label/Label'; import { hasFourDigit, isInteger } from '../../domain/validators'; -import useAddCardInput, { InputType } from '../../hooks/useAddCardInput'; - import { ADD_CARD_FORM_FIELDS, ERRORS } from '../../constants/messages'; -import { Fragment } from 'react/jsx-runtime'; +import { CustomInputHandlerProps } from '../../hooks/useAddCardFormField'; +import { validateInput } from '../../utils/validateInput'; interface CardNumberInputProps { - setCardData: (key: keyof CardInfo, newData: CardInfo[keyof CardInfo]) => void; + cardNumbers: CardNumbers; + errorMessage: string; + isError: Record; + onChange: (props: CustomInputHandlerProps) => void; + onBlur: (props: CustomInputHandlerProps) => void; } const { title, description, labelText, placeholder, inputLabelText } = ADD_CARD_FORM_FIELDS.CARD_NUMBER; -export default function CardNumberInput({ setCardData }: CardNumberInputProps) { - const initialValues = { - first: '', - second: '', - third: '', - fourth: '', - }; +export default function CardNumberInput({ + cardNumbers, + errorMessage, + isError, + onChange, + onBlur, +}: CardNumberInputProps) { + const handleOnChange = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as CardNumbersKey; - const validateInputOnChange = ({ value }: InputType) => { - if (!isInteger(value)) { - return { isValid: false, errorMsg: ERRORS.isNotInteger }; - } - return { isValid: true, errorMsg: '' }; + const validators = [{ test: isInteger, errorMessage: ERRORS.isNotInteger }]; + const result = validateInput(value, validators); + onChange({ ...result, name, value }); }; - const validateInputOnBlur = ({ value }: InputType) => { - if (!hasFourDigit(value)) { - return { isValid: false, errorMsg: ERRORS.isNotFourDigit }; - } - return { isValid: true, errorMsg: '' }; - }; + const handleOnBlur = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as CardNumbersKey; - const updateCardData = () => { - setCardData('cardNumbers', Object.values(cardNumbers)); + const validators = [ + { test: hasFourDigit, errorMessage: ERRORS.isNotFourDigit }, + ]; + const result = validateInput(value, validators); + onBlur({ ...result, name, value }); }; - const { - values: cardNumbers, - errorMessage, - isError, - onChange, - onBlur, - } = useAddCardInput({ - initialValues, - validateInputOnChange, - validateInputOnBlur, - updateCardData, - }); - return ( diff --git a/src/components/CardPreview/CardPreview.tsx b/src/components/CardPreview/CardPreview.tsx index 51c82cbfd4..e0963e906d 100644 --- a/src/components/CardPreview/CardPreview.tsx +++ b/src/components/CardPreview/CardPreview.tsx @@ -8,9 +8,10 @@ import { } from '../../constants/cardInfo'; type Brand = 'visa' | 'master'; +type CardPreviewProps = Record; const getCardbrand = ( - cardNumbers: CardInfo['cardNumbers'] + cardNumbers: CardPreviewProps['cardNumbers'] ): Nullable => { const { visa, master } = CARD_BRAND; @@ -25,7 +26,14 @@ const getCardbrand = ( return null; }; -const CardPreview = ({ cardNumbers, expirationDate, ownerName }: CardInfo) => { +const CardPreview = ({ + cardNumbers, + expirationDate, + ownerName, + cardIssuer, + cvc, + password, +}: CardPreviewProps) => { const brand = getCardbrand(cardNumbers); return ( diff --git a/src/components/ExpirationDateInput/ExpirationDateInput.tsx b/src/components/ExpirationDateInput/ExpirationDateInput.tsx index 29b3ca159d..25c589d6af 100644 --- a/src/components/ExpirationDateInput/ExpirationDateInput.tsx +++ b/src/components/ExpirationDateInput/ExpirationDateInput.tsx @@ -1,69 +1,67 @@ -import Input from '../common/Input/Input'; +import { Fragment } from 'react/jsx-runtime'; import Field from '../common/Field/Field'; +import Input from '../common/Input/Input'; import Label from '../common/Label/Label'; import { - isInteger, hasTwoDigit, - isValidMonth, + isInteger, isValidDate, + isValidMonth, } from '../../domain/validators'; -import useAddCardInput, { InputType } from '../../hooks/useAddCardInput'; - -import { ERRORS, ADD_CARD_FORM_FIELDS } from '../../constants/messages'; -import { Fragment } from 'react/jsx-runtime'; +import { ADD_CARD_FORM_FIELDS, ERRORS } from '../../constants/messages'; +import { CustomInputHandlerProps } from '../../hooks/useAddCardFormField'; +import { validateInput } from '../../utils/validateInput'; const { title, description, labelText, placeholder, inputLabelText } = ADD_CARD_FORM_FIELDS.EXPIRATION_DATE; interface ExpirationDateInputProps { - setCardData: (key: keyof CardInfo, newData: CardInfo[keyof CardInfo]) => void; + expirationDate: ExpirationDate; + errorMessage: string; + isError: Record; + onChange: (props: CustomInputHandlerProps) => void; + onBlur: (props: CustomInputHandlerProps) => void; } -const ExpirationDateInput = ({ setCardData }: ExpirationDateInputProps) => { - const initialValues = { - month: '', - year: '', - }; +const ExpirationDateInput = ({ + expirationDate, + errorMessage, + isError, + onChange, + onBlur, +}: ExpirationDateInputProps) => { + const handleOnChange = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as ExpirationDateKey; - const validateInputOnChange = ({ value }: { value: string }) => { - if (!isInteger(value)) { - return { isValid: false, errorMsg: ERRORS.isNotInteger }; - } - return { isValid: true, errorMsg: '' }; + const validations = [ + { test: isInteger, errorMessage: ERRORS.isNotInteger }, + ]; + const result = validateInput(value, validations); + onChange({ ...result, name, value }); }; - const validateInputOnBlur = ({ value }: InputType) => { - if (!hasTwoDigit(value)) { - return { isValid: false, errorMsg: ERRORS.isNotTwoDigit }; - } - if (!isValidMonth(expirationDate.month)) { - return { isValid: false, errorMsg: ERRORS.inValidMonth }; - } - if (!isValidDate(expirationDate)) { - return { isValid: false, errorMsg: ERRORS.deprecatedCard }; - } - return { isValid: true, errorMsg: '' }; - }; + const handleOnBlur = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as ExpirationDateKey; - const updateCardData = () => { - setCardData('expirationDate', Object.values(expirationDate)); + const validations = [ + { test: hasTwoDigit, errorMessage: ERRORS.isNotTwoDigit }, + { + test: () => isValidMonth(expirationDate.month), + errorMessage: ERRORS.inValidMonth, + }, + { + test: () => isValidDate(expirationDate), + errorMessage: ERRORS.deprecatedCard, + }, + ]; + const result = validateInput(value, validations); + onBlur({ ...result, name, value }); }; - const { - values: expirationDate, - errorMessage, - isError, - onChange, - onBlur, - } = useAddCardInput({ - initialValues, - validateInputOnChange, - validateInputOnBlur, - updateCardData, - }); - return ( { } value={expirationDate[name]} isError={isError[name]} - handleChange={onChange} - handleOnBlur={onBlur} + handleChange={handleOnChange} + handleOnBlur={handleOnBlur} maxLength={2} /> diff --git a/src/components/OwnernameInput/OwnerNameInput.tsx b/src/components/OwnernameInput/OwnerNameInput.tsx index 7e4c30a620..0745e01bd0 100644 --- a/src/components/OwnernameInput/OwnerNameInput.tsx +++ b/src/components/OwnernameInput/OwnerNameInput.tsx @@ -1,48 +1,59 @@ +import { Fragment } from 'react'; import Field from '../common/Field/Field'; import Input from '../common/Input/Input'; import Label from '../common/Label/Label'; -import useAddCardInput, { InputType } from '../../hooks/useAddCardInput'; - -import { isEnglishCharacter } from '../../domain/validators'; +import { isNotEmptyString, isEnglishCharacter } from '../../domain/validators'; -import { Fragment } from 'react'; import { ADD_CARD_FORM_FIELDS, ERRORS } from '../../constants/messages'; +import { CustomInputHandlerProps } from '../../hooks/useAddCardFormField'; +import { validateInput } from '../../utils/validateInput'; const { title, labelText, placeholder, inputLabelText } = ADD_CARD_FORM_FIELDS.OWNER_NAME; interface OwnerNameInputProps { - setCardData: (key: keyof CardInfo, newData: CardInfo[keyof CardInfo]) => void; + ownerName: OwnerName; + errorMessage: string; + isError: Record; + onChange: (props: CustomInputHandlerProps) => void; + onBlur: (props: CustomInputHandlerProps) => void; } -function OwnerNameInput({ setCardData }: OwnerNameInputProps) { - const initialValues = { - ownerName: '', - }; +function OwnerNameInput({ + ownerName, + errorMessage, + isError, + onChange, + onBlur, +}: OwnerNameInputProps) { + const handleOnChange = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as OwnerNameKey; - const validateInputOnChange = ({ value }: InputType) => { - if (value !== '' && !isEnglishCharacter(value)) { - return { isValid: false, errorMsg: ERRORS.isNotAlphabet }; - } - return { isValid: true, errorMsg: '' }; + const validators = [ + { + test: () => value === '' || isEnglishCharacter(value), + errorMessage: ERRORS.isNotAlphabet, + }, + ]; + const result = validateInput(value, validators); + onChange({ ...result, name, value }); }; - const updateCardData = () => { - setCardData('ownerName', Object.values(ownerName)); - }; + const handleOnBlur = (event: React.FocusEvent) => { + const { value } = event.target; + const name = event.target.name as OwnerNameKey; - const { - values: ownerName, - errorMessage, - isError, - onChange, - onBlur, - } = useAddCardInput({ - initialValues, - validateInputOnChange, - updateCardData, - }); + const validators = [ + { + test: isNotEmptyString, + errorMessage: ERRORS.invalidOwnerName, + }, + ]; + const result = validateInput(value, validators); + onBlur({ ...result, name, value }); + }; return ( @@ -58,8 +69,8 @@ function OwnerNameInput({ setCardData }: OwnerNameInputProps) { value={ownerName[name]} isError={isError[name]} isRequired - handleChange={onChange} - handleOnBlur={onBlur} + handleChange={handleOnChange} + handleOnBlur={handleOnBlur} maxLength={26} /> diff --git a/src/constants/form.ts b/src/constants/form.ts new file mode 100644 index 0000000000..e64c273a0b --- /dev/null +++ b/src/constants/form.ts @@ -0,0 +1,17 @@ +export const INITIAL_VALUES = { + cardNumbers: { + first: '', + second: '', + third: '', + fourth: '', + }, + + expirationDate: { + month: '', + year: '', + }, + + ownerName: { + ownerName: '', + }, +}; diff --git a/src/constants/messages.ts b/src/constants/messages.ts index 3da625206d..0d8c1d6c23 100644 --- a/src/constants/messages.ts +++ b/src/constants/messages.ts @@ -1,10 +1,11 @@ export const ERRORS = { isNotInteger: '숫자만 입력 가능합니다.', isNotFourDigit: '4자리 숫자를 입력해 주세요.', - isNotTwoDigit: '2자리 숫자를 입력해주세요', - inValidMonth: '1에서 12사이의 숫자를 입력해주세요', + isNotTwoDigit: '2자리 숫자를 입력해 주세요.', + inValidMonth: '1에서 12사이의 숫자를 입력해 주세요.', deprecatedCard: '만료된 카드는 사용할 수 없습니다.', isNotAlphabet: '알파벳만 입력 가능합니다.', + invalidOwnerName: '이름을 입력해 주세요.', } as const; const CARD_NUMBER = { diff --git a/src/domain/validators.ts b/src/domain/validators.ts index 432412856d..7194b9a79f 100644 --- a/src/domain/validators.ts +++ b/src/domain/validators.ts @@ -24,3 +24,7 @@ export const isEnglishCharacter = (value: string) => { const regex = /^[a-zA-Z\s]+$/; return regex.test(value); }; + +export const isNotEmptyString = (value: string) => { + return value !== ''; +}; diff --git a/src/hooks/useAddCardFormField.ts b/src/hooks/useAddCardFormField.ts new file mode 100644 index 0000000000..3e68ae6282 --- /dev/null +++ b/src/hooks/useAddCardFormField.ts @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import { createObjectWithKeys } from '../utils/createObjectWithKeys'; + +type InitialValuesType = CardNumbers | ExpirationDate | OwnerName; + +export interface CustomInputHandlerProps { + isValid: boolean; + errorMessage: string; + name: keyof T; + value: string; +} + +interface UseAddCardFormFieldProps { + initialValues: T; +} + +export default function useAddCardFormField({ + initialValues, +}: UseAddCardFormFieldProps) { + const [values, setValues] = useState(initialValues); + const [errorMessage, setErrorMessage] = useState(''); + const [isError, setIsError] = useState( + createObjectWithKeys(Object.keys(initialValues), false) + ); + + const onChange = ({ + isValid, + errorMessage, + name, + value, + }: CustomInputHandlerProps) => { + if (!isValid) { + setErrorMessage(errorMessage); + setIsError({ ...isError, [name]: true }); + } else { + setErrorMessage(''); + setIsError({ ...isError, [name]: false }); + + setValues({ + ...values, + [name]: name === 'ownerName' ? value.toUpperCase() : value, + }); + } + }; + + const onBlur = ({ + isValid, + errorMessage, + name, + }: CustomInputHandlerProps) => { + if (!isValid) { + setErrorMessage(errorMessage); + setIsError({ ...isError, [name]: true }); + } else { + setErrorMessage(''); + setIsError({ ...isError, [name]: false }); + } + }; + + return { + values, + errorMessage, + isError, + onChange, + onBlur, + }; +} diff --git a/src/hooks/useAddCardInput.tsx b/src/hooks/useAddCardInput.tsx deleted file mode 100644 index b0f4f08795..0000000000 --- a/src/hooks/useAddCardInput.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState } from 'react'; -import { createObjectWithKeys } from '../utils/createObjectWithKeys'; - -type InitialValuesType = CardNumbers | ExpirationDate | OwnerName; - -export type InputType = { - name?: string; - value: string; -}; - -interface UseAddCardInputProps { - initialValues: T; - validateInputOnChange: ({ name, value }: InputType) => { - isValid: boolean; - errorMsg: string; - }; - validateInputOnBlur?: ({ name, value }: InputType) => { - isValid: boolean; - errorMsg: string; - }; - updateCardData: () => void; -} - -export default function useAddCardInput({ - initialValues, - validateInputOnBlur, - validateInputOnChange, - updateCardData, -}: UseAddCardInputProps) { - const [values, setValues] = useState(initialValues); - const [errorMessage, setErrorMessage] = useState(''); - const [isError, setIsError] = useState( - createObjectWithKeys(Object.keys(initialValues), false) - ); - - const onChange = (event: React.ChangeEvent) => { - const { name, value } = event.target; - const validation = validateInputOnChange({ name, value }); - - if (!validation.isValid) { - setErrorMessage(validation.errorMsg); - setIsError({ ...isError, [name]: true }); - } else { - setErrorMessage(''); - setIsError({ ...isError, [name]: false }); - setValues({ - ...values, - [name]: name === 'ownerName' ? value.toUpperCase() : value, - }); - } - }; - - const onBlur = (event: React.FocusEvent) => { - const { name, value } = event.target; - - if (validateInputOnBlur) { - const validation = validateInputOnBlur({ name, value }); - - if (!validation.isValid) { - setErrorMessage(validation.errorMsg); - setIsError({ ...isError, [name]: true }); - } else { - setErrorMessage(''); - setIsError({ ...isError, [name]: false }); - updateCardData(); - } - } else { - updateCardData(); - } - }; - - return { - values, - errorMessage, - isError, - onChange, - onBlur, - }; -} diff --git a/src/types/CardInfo.d.ts b/src/types/CardInfo.d.ts index 1880a6773d..8dc92cd0e9 100644 --- a/src/types/CardInfo.d.ts +++ b/src/types/CardInfo.d.ts @@ -1,9 +1,3 @@ -interface CardInfo { - cardNumbers: string[]; - expirationDate: string[]; - ownerName: string; -} - interface CardNumbers { first: string; second: string; @@ -11,11 +5,50 @@ interface CardNumbers { fourth: string; } +type CardNumbersKey = keyof CardNumbers; + interface ExpirationDate { month: string; year: string; } +type ExpirationDateKey = keyof ExpirationDate; + interface OwnerName { ownerName: string; } + +type OwnerNameKey = keyof OwnerName; + +type Companies = + | 'BC카드' + | '신한카드' + | '카카오뱅크' + | '현대카드' + | '우리카드' + | '롯데카드' + | '하나카드' + | '국민카드'; + +interface CardIssuer { + cardIssuer: Nullable; +} + +interface CVC { + cvc: string; +} + +interface Password { + password: string; +} + +interface CardInfo { + cardNumbers: CardNumbers; + expirationDate: ExpirationDate; + ownerName: OwnerName; + cardIssuer: CardIssuer; + cvc: CVC; + password: Password; +} + +type CardInfoKeys = keyof CardInfo; diff --git a/src/utils/validateInput.ts b/src/utils/validateInput.ts new file mode 100644 index 0000000000..83fd709e0b --- /dev/null +++ b/src/utils/validateInput.ts @@ -0,0 +1,11 @@ +export const validateInput = ( + value: string, + validations: Array<{ test: (value: string) => boolean; errorMessage: string }> +) => { + const failedTest = validations.find((validation) => !validation.test(value)); + + if (failedTest) { + return { isValid: false, errorMessage: failedTest.errorMessage }; + } + return { isValid: true, errorMessage: '' }; +}; From 738d50ccf621b4c7eb071c2d7d2daf27308b9757 Mon Sep 17 00:00:00 2001 From: Kim Da Eun Date: Wed, 24 Apr 2024 17:21:45 +0900 Subject: [PATCH 03/40] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=EC=82=AC=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 44 ++++++------ .../CardIssuerInput/CardIssuerInput.tsx | 72 +++++++++++++++++++ .../CardNumberInput/CardNumberInput.tsx | 18 ++--- .../ExpirationDateInput.tsx | 18 ++--- .../OwnerNameInput}/OwnerNameInput.tsx | 23 +++--- src/components/common/Field/Field.tsx | 4 +- src/components/common/Input/Input.tsx | 6 +- .../common/Select/Select.module.css | 29 ++++++++ src/components/common/Select/Select.tsx | 53 ++++++++++++++ src/constants/form.ts | 4 ++ src/constants/messages.ts | 21 ++++++ src/hooks/useAddCardFormField.ts | 8 ++- src/types/CardInfo.d.ts | 32 ++++----- 13 files changed, 262 insertions(+), 70 deletions(-) create mode 100644 src/components/AddCardFormInput/CardIssuerInput/CardIssuerInput.tsx rename src/components/{ => AddCardFormInput}/CardNumberInput/CardNumberInput.tsx (80%) rename src/components/{ => AddCardFormInput}/ExpirationDateInput/ExpirationDateInput.tsx (84%) rename src/components/{OwnernameInput => AddCardFormInput/OwnerNameInput}/OwnerNameInput.tsx (78%) create mode 100644 src/components/common/Select/Select.module.css create mode 100644 src/components/common/Select/Select.tsx diff --git a/src/App.tsx b/src/App.tsx index 1aaa29b6d7..503066cb18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,24 +2,27 @@ import './index.css'; import styles from './App.module.css'; import useFormField from './hooks/useAddCardFormField'; -import CardNumberInput from './components/CardNumberInput/CardNumberInput'; +import CardNumberInput from './components/AddCardFormInput/CardNumberInput/CardNumberInput'; import CardPreview from './components/CardPreview/CardPreview'; -import ExpirationDateInput from './components/ExpirationDateInput/ExpirationDateInput'; -import OwnerNameInput from './components/OwnerNameInput/OwnerNameInput'; +import ExpirationDateInput from './components/AddCardFormInput/ExpirationDateInput/ExpirationDateInput'; +import OwnerNameInput from './components/AddCardFormInput/OwnerNameInput/OwnerNameInput'; +import CardIssuerInput from './components/AddCardFormInput/CardIssuerInput/CardIssuerInput'; import { INITIAL_VALUES } from './constants/form'; function App() { - const { values: cardNumbers, ...cardNumbersProps } = - useFormField({ - initialValues: INITIAL_VALUES.cardNumbers, - }); - const { values: expirationDate, ...expirationDateProps } = - useFormField({ - initialValues: INITIAL_VALUES.expirationDate, - }); - const { values: ownerName, ...ownerNameProps } = useFormField({ - initialValues: INITIAL_VALUES.ownerName, + const { cardNumbers, expirationDate, ownerName, cardIssuer } = INITIAL_VALUES; + const cardNumbersProps = useFormField({ + initialValues: cardNumbers, + }); + const expirationDateProps = useFormField({ + initialValues: expirationDate, + }); + const ownerNameProps = useFormField({ + initialValues: ownerName, + }); + const cardIssuerProps = useFormField({ + initialValues: cardIssuer, }); return ( @@ -27,18 +30,19 @@ function App() {

카드 추가

- - - + + + + ); diff --git a/src/components/AddCardFormInput/CardIssuerInput/CardIssuerInput.tsx b/src/components/AddCardFormInput/CardIssuerInput/CardIssuerInput.tsx new file mode 100644 index 0000000000..1c1a53adad --- /dev/null +++ b/src/components/AddCardFormInput/CardIssuerInput/CardIssuerInput.tsx @@ -0,0 +1,72 @@ +import React, { Fragment } from 'react'; + +import Label from '../../common/Label/Label'; +import Field from '../../common/Field/Field'; +import Select from '../../common/Select/Select'; + +import { validateInput } from '../../../utils/validateInput'; +import { isNotEmptyString } from '../../../domain/validators'; + +import { CustomInputHandlerProps } from '../../../hooks/useAddCardFormField'; +import { ADD_CARD_FORM_FIELDS, ERRORS } from '../../../constants/messages'; + +const { title, description, inputLabelText, defaultText, options } = + ADD_CARD_FORM_FIELDS.CARD_ISSUER; + +interface CardIssueInputProps { + values: CardIssuer; + errorMessage: string; + isError: Record; + onChange: (props: CustomInputHandlerProps) => void; + onBlur: (props: CustomInputHandlerProps) => void; +} + +export default function CardIssuerInput({ + values: cardIssuer, + errorMessage, + isError, + onChange, + onBlur, +}: CardIssueInputProps) { + const handleOnSelect = (event: React.ChangeEvent) => { + const { value } = event.target; + const name = event.target.name as CardIssuerKey; + + onChange({ isValid: true, errorMessage: '', name, value }); + }; + + const handleOnBlur = (event: React.FocusEvent) => { + const { value } = event.target; + const name = event.target.name as CardIssuerKey; + + const validators = [ + { test: isNotEmptyString, errorMessage: ERRORS.invalidCardIssuer }, + ]; + const result = validateInput(value, validators); + onBlur({ ...result, name, value }); + }; + + return ( + + {Object.keys(cardIssuer).map((n) => { + const name = n as CardIssuerKey; + return ( + +