From a4742b22c8a0c24096436549986486023592d158 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 6 Apr 2025 10:33:28 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Feat:=201=EC=B0=A8=20=EA=B5=AC=ED=98=84=20-?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++ src/App.css | 0 src/App.tsx | 168 +++++++++++++++++++++++- src/assets/Mastercard.svg | 6 + src/assets/Visa.svg | 4 + src/components/Card.module.css | 37 ++++++ src/components/Card.tsx | 40 ++++++ src/components/CardForm.module.css | 21 +++ src/components/CardForm.tsx | 25 ++++ src/components/CardFormInput.module.css | 14 ++ src/components/CardFormInput.tsx | 43 ++++++ src/constants/CardConstants.ts | 22 ++++ src/constants/ErrorMessage.ts | 11 ++ src/main.tsx | 14 +- src/types/card.ts | 13 ++ src/types/error.ts | 5 + src/utils/NumberUtils.tsx | 18 +++ src/utils/ValidCardNumbers.tsx | 23 ++++ src/utils/ValidExpirationDate.tsx | 60 +++++++++ src/utils/ValidOwnerName.tsx | 28 ++++ 20 files changed, 567 insertions(+), 9 deletions(-) delete mode 100644 src/App.css create mode 100644 src/assets/Mastercard.svg create mode 100644 src/assets/Visa.svg create mode 100644 src/components/Card.module.css create mode 100644 src/components/Card.tsx create mode 100644 src/components/CardForm.module.css create mode 100644 src/components/CardForm.tsx create mode 100644 src/components/CardFormInput.module.css create mode 100644 src/components/CardFormInput.tsx create mode 100644 src/constants/CardConstants.ts create mode 100644 src/constants/ErrorMessage.ts create mode 100644 src/types/card.ts create mode 100644 src/types/error.ts create mode 100644 src/utils/NumberUtils.tsx create mode 100644 src/utils/ValidCardNumbers.tsx create mode 100644 src/utils/ValidExpirationDate.tsx create mode 100644 src/utils/ValidOwnerName.tsx diff --git a/README.md b/README.md index 8d917806e0..1a0cfb5f04 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ # react-payments + +## ๐Ÿ’ณ ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ + +- [ ] ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ ์‹๋ณ„ + - [V] ์นด๋“œ ๋ฒˆํ˜ธ์˜ 3~4๋ฒˆ ๋ธ”๋Ÿญ์€ ์ˆจ๊น€ ์ฒ˜๋ฆฌํ•œ๋‹ค. + - [V] ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋ผ๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ณด์—ฌ์ฃผ๊ณ , ์ž…๋ ฅ์„ ์ œํ•œํ•œ๋‹ค. + - [V] ๊ฐ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ธ”๋Ÿญ์€ 0~9์˜ ์ˆซ์ž 4์ž๋ฆฌ๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ๋‹ค. + - [V] ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - [ ] ์นด๋“œ ๋ฒˆํ˜ธ 4์ž๋ฆฌ ๋ฏธ๋งŒ์œผ๋กœ ์ž…๋ ฅํ•˜๋Š” ์ค‘์— ํฌ์ปค์Šค๋ฅผ ๋„˜๊ธฐ๋ ค๊ณ  ํ•œ๋‹ค๋ฉด ์—๋Ÿฌ๋กœ ๋ง‰๋Š”๋‹ค. +- [V] ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ + - [ ] ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์ˆซ์ž๊ฐ€ ์•„๋‹์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ์›”์„ ์ž…๋ ฅ ์‹œ(ex 13์›”) ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - [ ] ํ˜„์žฌ๋ณด๋‹ค ์ด์ „ ๋‚ ์งœ๋ฅผ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - [ ] ํ•œ ์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ํ˜•์‹์— ๋งž์ถฐ 0์„ ๋„ฃ์–ด์ค€๋‹ค. +- [V] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ + - [V] ์†Œ๋ฌธ์ž๋กœ ์ž…๋ ฅ ์‹œ ๊ฐ•์ œ๋กœ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. + - [V] ์˜์–ด๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž ์ž…๋ ฅ ์‹œ ์ž…๋ ฅ์„ ์ œํ•œํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - [V] (์ถ”๊ฐ€) ์‚ฌ์šฉ์ž ์ด๋ฆ„์€ ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค. +- [V] ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ์—…๋ฐ์ดํŠธ + - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 4๋กœ ์‹œ์ž‘ํ•˜๋ฉด Visa์นด๋“œ ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 51~55๋กœ ์‹œ์ž‘ํ•˜๋ฉด MasterCard ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. diff --git a/src/App.css b/src/App.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/App.tsx b/src/App.tsx index ef7e3632d2..750b4d1d18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,173 @@ -import "./App.css"; +import CardForm from "./components/CardForm"; +import styles from "./components/CardForm.module.css"; +import Card from "./components/Card"; +import { + CARD_FORM_LABELS, + CARD_LABELS, + CARD_PLACEHOLDERS, +} from "./constants/CardConstants"; +import CardFormInput from "./components/CardFormInput"; +import type { ChangeEvent, ExpirationDate, CardNumbers } from "./types/card"; +import type { CardError } from "./types/error"; +import { useState } from "react"; +import { formatStringToUpper, validOwnerName } from "./utils/ValidOwnerName"; +import { convertMonth, validExpirationDate } from "./utils/ValidExpirationDate"; +import { filterNumber } from "./utils/NumberUtils"; +import { validCardNumbersBlock } from "./utils/ValidCardNumbers"; function App() { + const [expirationDate, setExpirationDate] = useState({ + month: "", + year: "", + }); + + const [cardNumbers, setCardNumbers] = useState({ + firstBlock: "", + secondBlock: "", + thirdBlock: "", + fourthBlock: "", + }); + + const [owner, setOwner] = useState(""); + + const [cardError, setCardError] = useState({ + cardNumbersError: "", + expirationDateError: "", + ownerError: "", + }); + + const handleExpirationDateChange = (event: ChangeEvent) => { + const name = event.target.name; + const value = event.target.value; + + const filterValue = filterNumber( + name === "month" ? convertMonth(value) : value, + 2 + ); + + const newDate = { + ...expirationDate, + [name]: filterValue, + }; + + const validResult = validExpirationDate(newDate.month, newDate.year); + + setCardError((pre) => ({ + ...pre, + expirationDateError: validResult, + })); + + setExpirationDate({ + month: newDate.month, + year: newDate.year, + }); + }; + + const handleOwnerChange = (event: ChangeEvent) => { + const validResult = validOwnerName(event.target.value); + setCardError((pre) => ({ + ...pre, + ownerError: validResult, + })); + const ownerName = formatStringToUpper(event.target.value); + setOwner(ownerName); + }; + + const handleCardNumbersChange = (event: ChangeEvent) => { + let validResult = ""; + const name = event.target.name; + const value = event.target.value; + const filterValue = filterNumber(value, 4); + + const numberBlock = { + ...cardNumbers, + [name]: filterValue, + }; + + Object.entries(numberBlock).forEach(([key, value]) => { + validResult = validCardNumbersBlock(value); + }); + + setCardError((pre) => ({ + ...pre, + cardNumbersError: validResult, + })); + + setCardNumbers({ + firstBlock: numberBlock.firstBlock, + secondBlock: numberBlock.secondBlock, + thirdBlock: numberBlock.thirdBlock, + fourthBlock: numberBlock.fourthBlock, + }); + }; + return ( <> -

React Payments

+ + + + {Object.entries(cardNumbers).map(([key, value]) => ( + + ))} + + {cardError.cardNumbersError && ( +

{cardError.cardNumbersError}

+ )} + + + + {Object.entries(expirationDate).map(([key, value]) => ( + + ))} + + {cardError.expirationDateError && ( +

{cardError.expirationDateError}

+ )} + + + {cardError.ownerError && ( +

{cardError.ownerError}

+ )} ); } diff --git a/src/assets/Mastercard.svg b/src/assets/Mastercard.svg new file mode 100644 index 0000000000..dbe057ab78 --- /dev/null +++ b/src/assets/Mastercard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/Visa.svg b/src/assets/Visa.svg new file mode 100644 index 0000000000..99b2ccebd7 --- /dev/null +++ b/src/assets/Visa.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Card.module.css b/src/components/Card.module.css new file mode 100644 index 0000000000..d75375ff5e --- /dev/null +++ b/src/components/Card.module.css @@ -0,0 +1,37 @@ +.card { + width: 220px; + height: 120px; + margin-left: 40px; + background-color: #333333; + border-radius: 4px; +} + +.logo { + width: 30px; + height: 30px; + margin-right: 10px; + margin-top: 4px; +} + +.card-detail { + color: white; + margin: 0; + padding: 4px; + font-size: 14px; + margin-left: 10px; +} + +.card-ic { + width: 30px; + height: 18px; + background-color: #ddcd78; + border-radius: 3px; + margin-left: 10px; + margin-top: 4px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 0000000000..fd61af574e --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,40 @@ +import styles from "./Card.module.css"; +import type { CardNumbers, ExpirationDate } from "../types/card"; +import { hideNumber, setCardLogo } from "../utils/ValidCardNumbers"; + +type CardProps = { + cardNumbers: CardNumbers; + expirationDate: ExpirationDate; + owner: string; +}; + +function Card({ owner, expirationDate, cardNumbers }: CardProps) { + const expirationMonth = expirationDate.month; + const expirationYear = expirationDate.year; + const cardLogo = setCardLogo(cardNumbers.firstBlock); + const date = + expirationMonth.length > 0 || expirationYear.length > 0 + ? `${expirationMonth}/${expirationYear}` + : ""; + const numbers: string = `${cardNumbers.firstBlock} ${cardNumbers.secondBlock} ${hideNumber(cardNumbers.thirdBlock)} ${hideNumber(cardNumbers.fourthBlock)}`; + + return ( + <> +
+
+
+ {cardLogo ? ( + + ) : ( +
+ )} +
+

{numbers}

+

{date}

+

{owner}

+
+ + ); +} + +export default Card; diff --git a/src/components/CardForm.module.css b/src/components/CardForm.module.css new file mode 100644 index 0000000000..0818c10d67 --- /dev/null +++ b/src/components/CardForm.module.css @@ -0,0 +1,21 @@ +.label { + margin-top: 40px; + margin-bottom: 4px; +} + +.caption { + color: gray; + font-size: 12px; + margin: 0; + padding: 0; +} + +.form-label { + color: black; + font-size: 14px; +} + +.error { + color: red; + font-size: 12px; +} diff --git a/src/components/CardForm.tsx b/src/components/CardForm.tsx new file mode 100644 index 0000000000..0a20464373 --- /dev/null +++ b/src/components/CardForm.tsx @@ -0,0 +1,25 @@ +import styles from "./CardForm.module.css"; + +type CardFormProps = { + cardFormLabelText: string; + cardFormLabelCaption?: string; + cardLabelText: string; +}; + +function CardForm({ + cardFormLabelText, + cardFormLabelCaption, + cardLabelText, +}: CardFormProps) { + return ( + <> +

{cardFormLabelText}

+ {cardFormLabelCaption && ( +

{cardFormLabelCaption}

+ )} +

{cardLabelText}

+ + ); +} + +export default CardForm; diff --git a/src/components/CardFormInput.module.css b/src/components/CardFormInput.module.css new file mode 100644 index 0000000000..290b5fea45 --- /dev/null +++ b/src/components/CardFormInput.module.css @@ -0,0 +1,14 @@ +.inputBox { + padding: 8px; + border-radius: 4px; + border: 1.6px solid #ccc; + margin-right: 10px; +} + +.error-inputBox { + border-color: red; +} + +.not-error-inputBox { + border-color: #d5d5d5; +} diff --git a/src/components/CardFormInput.tsx b/src/components/CardFormInput.tsx new file mode 100644 index 0000000000..332a2fbee9 --- /dev/null +++ b/src/components/CardFormInput.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import type { ChangeEvent } from "../types/card"; +import styles from "./CardFormInput.module.css"; + +type CardFormProps = { + name: string; + cardPlaceHolder: string; + cardInput: number | string; + handleChange: (e: ChangeEvent) => void; + width?: string; + disabled?: boolean; + isError?: boolean; +}; + +function CardFormInput({ + name, + cardPlaceHolder, + cardInput, + handleChange, + width, + disabled, + isError, +}: CardFormProps) { + return ( + <> + + + ); +} + +export default CardFormInput; diff --git a/src/constants/CardConstants.ts b/src/constants/CardConstants.ts new file mode 100644 index 0000000000..d4aada3276 --- /dev/null +++ b/src/constants/CardConstants.ts @@ -0,0 +1,22 @@ +const CARD_FORM_LABELS = { + CARD_NUMBER: "๊ฒฐ์ œํ•  ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”", + CARD_NUMBER_CAPTION: "๋ณธ์ธ ๋ช…์˜์˜ ์นด๋“œ๋งŒ ๊ฒฐ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + EXPIRATION_DATE: "์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”", + EXPIRATION_DATE_CAPTION: "์›”/๋…„๋„(MMYY)๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.", + CARD_OWNER: "์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", +}; + +const CARD_LABELS = { + CARD_NUMBER: "์นด๋“œ ๋ฒˆํ˜ธ", + EXPIRATION_DATE: "์œ ํšจ๊ธฐ๊ฐ„", + CARD_OWNER: "์†Œ์œ ์ž ์ด๋ฆ„", +}; + +const CARD_PLACEHOLDERS = { + CARD_NUMBER: "1234", + EXPIRATION_MONTH: "MM", + EXPIRATION_YEAR: "YY", + CARD_OWNER: "JOHN DOE", +}; + +export { CARD_FORM_LABELS, CARD_LABELS, CARD_PLACEHOLDERS }; diff --git a/src/constants/ErrorMessage.ts b/src/constants/ErrorMessage.ts new file mode 100644 index 0000000000..c174ae9917 --- /dev/null +++ b/src/constants/ErrorMessage.ts @@ -0,0 +1,11 @@ +export const ERROR_MESSAGE = { + ONLY_NUMBER: "์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + ONLY_ENGLISH: "์˜์–ด๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + MIN_LENGTH_REQUIRED: "๋‘๊ธ€์ž ์ด์ƒ ์ž…๋ ฅํ•˜์„ธ์š”.", + MONTH_OUT_OF_RANGE: "์œ ํšจํ•œ ์›”์„ ์ž…๋ ฅํ•˜์„ธ์š”.", + EXPIRATION_DATE_IN_PAST: "์œ ํšจ๊ธฐ๊ฐ„ ๋‚ ์งœ๊ฐ€ ์ง€๋‚ฌ์Šต๋‹ˆ๋‹ค.", + VALID_MONTH_RANGE: "1์›”๋ถ€ํ„ฐ 12์›” ์‚ฌ์ด๋งŒ ์ž…๋ ฅํ•˜์„ธ์š”.", + PAST_DATE: "์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ์—ฐ๋„์ž…๋‹ˆ๋‹ค.", + OVER_MAX_VALID_YEAR: "์œ ํšจ๊ธฐ๊ฐ„์€ ์ตœ๋Œ€ 10๋…„๊นŒ์ง€๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + MIN_LENGTH_TWO: "์ˆซ์ž ๋‘ ์ž๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.", +}; diff --git a/src/main.tsx b/src/main.tsx index 3d7150da80..966f17a4b2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import "./index.css"; -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById("root")!).render( - , -) + +); diff --git a/src/types/card.ts b/src/types/card.ts new file mode 100644 index 0000000000..51113ac7ef --- /dev/null +++ b/src/types/card.ts @@ -0,0 +1,13 @@ +export type ChangeEvent = React.ChangeEvent; + +export interface ExpirationDate { + month: string; + year: string; +} + +export interface CardNumbers { + firstBlock: string; + secondBlock: string; + thirdBlock: string; + fourthBlock: string; +} diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 0000000000..841919afad --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,5 @@ +export interface CardError { + cardNumbersError: string; + expirationDateError: string; + ownerError: string; +} diff --git a/src/utils/NumberUtils.tsx b/src/utils/NumberUtils.tsx new file mode 100644 index 0000000000..a36ae0abc3 --- /dev/null +++ b/src/utils/NumberUtils.tsx @@ -0,0 +1,18 @@ +// ์ˆซ์ž๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +export const isOnlyNumber = (inputValue: string): boolean => { + return /^[0-9]+$/.test(inputValue); +}; + +// ๊ธ€์ž์ˆ˜ ์ œํ•œ +const limitToDigits = (value: string, max: number) => { + return value.length <= 2 ? value : value.slice(0, max); +}; + +// ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ +const inputOnlyNumber = (value: string): string => value.replace(/[^0-9]/g, ""); + +// ์ž…๋ ฅํ•œ max ์ˆ˜๋งŒํผ ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ +export const filterNumber = (value: string, max: number) => { + const result = limitToDigits(value, max); + return inputOnlyNumber(result); +}; diff --git a/src/utils/ValidCardNumbers.tsx b/src/utils/ValidCardNumbers.tsx new file mode 100644 index 0000000000..c0da5a3c92 --- /dev/null +++ b/src/utils/ValidCardNumbers.tsx @@ -0,0 +1,23 @@ +import { ERROR_MESSAGE } from "../constants/ErrorMessage"; +import { isOnlyNumber } from "./NumberUtils"; +import visa from "../assets/Visa.svg"; +import master from "../assets/Mastercard.svg"; + +// Visa ๋˜๋Š” Master ๋กœ๊ณ  ์„ค์ • +export const setCardLogo = (value: string): string => { + if (value.slice(0, 1) === "4") return visa; + const first = Number(value.slice(0, 2)); + if (51 <= first && first <= 55) return master; + return ""; +}; + +// ์ˆซ์ž๋ฅผ '*' ์ฒ˜๋ฆฌ +export const hideNumber = (value: string): string => + value.replace(/[0-9]/g, "*"); + +// ์นด๋“œ ๋ฒˆํ˜ธ ๊ฒ€์ฆ +export const validCardNumbersBlock = (block: string) => { + if (!isOnlyNumber(block)) return ERROR_MESSAGE.ONLY_NUMBER; + + return ""; +}; diff --git a/src/utils/ValidExpirationDate.tsx b/src/utils/ValidExpirationDate.tsx new file mode 100644 index 0000000000..937d9b8aeb --- /dev/null +++ b/src/utils/ValidExpirationDate.tsx @@ -0,0 +1,60 @@ +import { ERROR_MESSAGE } from "../constants/ErrorMessage"; +import { isOnlyNumber } from "./NumberUtils"; + +// ์˜ค๋Š˜ ๋‚ ์งœ, ์—ฐ๋„, ์›” +const today: Date = new Date(); +const currentYear: number = today.getFullYear(); +const currentMonth: number = today.getMonth() + 1; + +// ์ž…๋ ฅํ•œ 2์ž๋ฆฌ ์—ฐ๋„๋ฅผ 4์ž๋ฆฌ ์—ฐ๋„๋กœ ๋ณ€๊ฒฝ +const convertYear = (year: number): number => + Math.floor(currentYear / 100) * 100 + year; + +// ๋‘์ž๋ฆฌ๋กœ ์›”์„ ๋ฐ˜ํ™˜ (ex. 3์›” ์ž…๋ ฅ ์‹œ 03์›”๋กœ ๋ณ€ํ™˜) +export const convertMonth = (month: string): string => { + const monthNumber = Number(month); + if (isNaN(monthNumber)) return ""; + return monthNumber < 10 ? `0${monthNumber}` : `${monthNumber}`; +}; + +// 2๊ธ€์ž๋ฅผ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +const isLengthTwo = (value: string): boolean => value.length === 2; + +// ์ž…๋ ฅํ•œ ๋‹ฌ์ด 1์›”๋ถ€ํ„ฐ 12์›” ์‚ฌ์ด์ธ์ง€ ๊ฒ€์ฆ +const isInValidMonthRange = (month: number): boolean => + month >= 1 && month <= 12; + +// ์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ +const isNotPastDate = (month: number, year: number): boolean => { + const inputDate: Date = new Date(year, month - 1, 1); + return inputDate >= today; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„์ด 10๋…„ ์ด๋‚ด์ธ์ง€ ํ™•์ธ +const isOverMaxValidYear = (month: number, year: number): boolean => { + const tenYearsLaterDate = new Date(currentYear + 10, currentMonth - 1, 1); + const inputDate = new Date(year, month - 1, 1); + + return inputDate <= tenYearsLaterDate; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ +export const validExpirationDate = ( + inputMonth: string, + inputYear: string +): string => { + if (!isOnlyNumber(inputMonth) || !isOnlyNumber(inputYear)) + return ERROR_MESSAGE.ONLY_NUMBER; + + const month: number = Number(inputMonth); + const year: number = convertYear(Number(inputYear)); + + if (!isLengthTwo(convertMonth(inputMonth)) || !isLengthTwo(inputYear)) + return ERROR_MESSAGE.MIN_LENGTH_TWO; + if (!isInValidMonthRange(month)) return ERROR_MESSAGE.VALID_MONTH_RANGE; + if (!isNotPastDate(month, year)) return ERROR_MESSAGE.PAST_DATE; + if (!isOverMaxValidYear(month, year)) + return ERROR_MESSAGE.OVER_MAX_VALID_YEAR; + + return ""; +}; diff --git a/src/utils/ValidOwnerName.tsx b/src/utils/ValidOwnerName.tsx new file mode 100644 index 0000000000..e157807a67 --- /dev/null +++ b/src/utils/ValidOwnerName.tsx @@ -0,0 +1,28 @@ +{ + /* ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ•จ์ˆ˜ + 1. formatStringToUpper -> ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ + 2. isEnglishOnly -> ์˜๋ฌธ๋งŒ ์žˆ๋Š”์ง€ ๊ฒ€์ฆ + 3. isMinLength -> ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ธ์ง€ ๊ฒ€์ฆ + + validOwnerName -> ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฐ˜ํ™˜ + */ +} + +import { ERROR_MESSAGE } from "../constants/ErrorMessage"; + +// ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ +export const formatStringToUpper = (text: string): string => text.toUpperCase(); + +// ์˜๋ฌธ๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +const isEnglishOnly = (text: string): boolean => /^[A-Za-z\s]+$/.test(text); + +// ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ธ์ง€ ๊ฒ€์ฆ +const isMinLength = (text: string): boolean => text.length >= 2; + +// ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฐ˜ํ™˜ +export const validOwnerName = (text: string): string => { + if (!isEnglishOnly(text)) return ERROR_MESSAGE.ONLY_ENGLISH; + if (!isMinLength(text)) return ERROR_MESSAGE.MIN_LENGTH_REQUIRED; + + return ""; +}; From 75ff3944acd8e9f54cf3ac791b4c5e8879b9a5c6 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 6 Apr 2025 15:06:39 +0900 Subject: [PATCH 2/9] =?UTF-8?q?Fix:=202=EC=B0=A8=20=EA=B5=AC=ED=98=84=20-?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 87 +++++++++----- src/components/Card.tsx | 10 +- src/components/CardForm.tsx | 6 +- src/components/CardFormInput.tsx | 20 ++-- src/constants/ErrorMessage.ts | 1 + src/constants/index.ts | 3 + .../{CardConstants.ts => textConstants.ts} | 0 src/constants/usingNumbers.ts | 14 +++ src/{components => styles}/Card.module.css | 0 .../CardForm.module.css | 0 .../CardFormInput.module.css | 0 src/types/{card.ts => cardTypes.ts} | 0 src/types/error.ts | 5 - src/types/errorType.ts | 26 ++++ src/types/index.ts | 2 + src/utils/ValidCardNumbers.tsx | 23 ---- src/utils/ValidExpirationDate.tsx | 60 ---------- src/utils/ValidOwnerName.tsx | 28 ----- src/utils/cardNumberValidators.tsx | 83 +++++++++++++ src/utils/errorHelpers.tsx | 17 +++ src/utils/expirationDateValidators.tsx | 112 ++++++++++++++++++ src/utils/index.ts | 5 + .../{NumberUtils.tsx => inputFilters.tsx} | 10 ++ src/utils/ownerNameValidators.tsx | 30 +++++ 24 files changed, 381 insertions(+), 161 deletions(-) create mode 100644 src/constants/index.ts rename src/constants/{CardConstants.ts => textConstants.ts} (100%) create mode 100644 src/constants/usingNumbers.ts rename src/{components => styles}/Card.module.css (100%) rename src/{components => styles}/CardForm.module.css (100%) rename src/{components => styles}/CardFormInput.module.css (100%) rename src/types/{card.ts => cardTypes.ts} (100%) delete mode 100644 src/types/error.ts create mode 100644 src/types/errorType.ts create mode 100644 src/types/index.ts delete mode 100644 src/utils/ValidCardNumbers.tsx delete mode 100644 src/utils/ValidExpirationDate.tsx delete mode 100644 src/utils/ValidOwnerName.tsx create mode 100644 src/utils/cardNumberValidators.tsx create mode 100644 src/utils/errorHelpers.tsx create mode 100644 src/utils/expirationDateValidators.tsx create mode 100644 src/utils/index.ts rename src/utils/{NumberUtils.tsx => inputFilters.tsx} (65%) create mode 100644 src/utils/ownerNameValidators.tsx diff --git a/src/App.tsx b/src/App.tsx index 750b4d1d18..043a4960ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,32 @@ +import { useState } from "react"; import CardForm from "./components/CardForm"; -import styles from "./components/CardForm.module.css"; +import CardFormInput from "./components/CardFormInput"; import Card from "./components/Card"; import { CARD_FORM_LABELS, CARD_LABELS, CARD_PLACEHOLDERS, -} from "./constants/CardConstants"; -import CardFormInput from "./components/CardFormInput"; -import type { ChangeEvent, ExpirationDate, CardNumbers } from "./types/card"; -import type { CardError } from "./types/error"; -import { useState } from "react"; -import { formatStringToUpper, validOwnerName } from "./utils/ValidOwnerName"; -import { convertMonth, validExpirationDate } from "./utils/ValidExpirationDate"; -import { filterNumber } from "./utils/NumberUtils"; -import { validCardNumbersBlock } from "./utils/ValidCardNumbers"; +} from "./constants/textConstants"; +import type { + ChangeEvent, + ExpirationDate, + CardNumbers, + CardError, + ExpirationDateError, + CardNumbersError, +} from "./types"; +import { + formatStringToUpper, + validOwnerName, + convertMonth, + validExpirationDate, + filterNumber, + filterString, + getFirstExpirationErrorMessage, + getFirstErrorMessage, + validCardNumbersBlock, +} from "./utils"; +import styles from "./styles/CardForm.module.css"; function App() { const [expirationDate, setExpirationDate] = useState({ @@ -31,9 +44,17 @@ function App() { const [owner, setOwner] = useState(""); const [cardError, setCardError] = useState({ - cardNumbersError: "", - expirationDateError: "", - ownerError: "", + cardNumbersError: { + firstBlock: { hasError: false, errorMessage: "", isDisable: false }, + secondBlock: { hasError: false, errorMessage: "", isDisable: true }, + thirdBlock: { hasError: false, errorMessage: "", isDisable: true }, + fourthBlock: { hasError: false, errorMessage: "", isDisable: true }, + }, + expirationDateError: { + month: { hasError: false, errorMessage: "" }, + year: { hasError: false, errorMessage: "" }, + }, + ownerError: { hasError: false, errorMessage: "" }, }); const handleExpirationDateChange = (event: ChangeEvent) => { @@ -64,33 +85,32 @@ function App() { }; const handleOwnerChange = (event: ChangeEvent) => { + const value = filterString(event.target.value, 20); + const validResult = validOwnerName(event.target.value); + setCardError((pre) => ({ ...pre, ownerError: validResult, })); - const ownerName = formatStringToUpper(event.target.value); + const ownerName = formatStringToUpper(value); setOwner(ownerName); }; const handleCardNumbersChange = (event: ChangeEvent) => { - let validResult = ""; const name = event.target.name; const value = event.target.value; - const filterValue = filterNumber(value, 4); const numberBlock = { ...cardNumbers, - [name]: filterValue, + [name]: filterNumber(value, 4), }; - Object.entries(numberBlock).forEach(([key, value]) => { - validResult = validCardNumbersBlock(value); - }); + const result: CardNumbersError = validCardNumbersBlock(numberBlock); setCardError((pre) => ({ ...pre, - cardNumbersError: validResult, + cardNumbersError: result, })); setCardNumbers({ @@ -122,11 +142,18 @@ function App() { cardPlaceHolder={CARD_PLACEHOLDERS.CARD_NUMBER} handleChange={handleCardNumbersChange} width="50px" + hasError={ + cardError.cardNumbersError[key as keyof CardNumbersError].hasError + } + isDisable={ + cardError.cardNumbersError[key as keyof CardNumbersError].isDisable + } /> ))} - {cardError.cardNumbersError && ( -

{cardError.cardNumbersError}

+

+ {getFirstErrorMessage(cardError.cardNumbersError)} +

)} - {Object.entries(expirationDate).map(([key, value]) => ( ))} - {cardError.expirationDateError && ( -

{cardError.expirationDateError}

+

+ {getFirstExpirationErrorMessage(cardError.expirationDateError)} +

)} + {cardError.ownerError && ( -

{cardError.ownerError}

+

{cardError.ownerError.errorMessage}

)} ); diff --git a/src/components/Card.tsx b/src/components/Card.tsx index fd61af574e..8aca249949 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,12 +1,12 @@ -import styles from "./Card.module.css"; -import type { CardNumbers, ExpirationDate } from "../types/card"; -import { hideNumber, setCardLogo } from "../utils/ValidCardNumbers"; +import styles from "../styles/Card.module.css"; +import type { CardNumbers, ExpirationDate } from "../types/cardTypes"; +import { hideNumber, setCardLogo } from "../utils/cardNumberValidators"; -type CardProps = { +interface CardProps { cardNumbers: CardNumbers; expirationDate: ExpirationDate; owner: string; -}; +} function Card({ owner, expirationDate, cardNumbers }: CardProps) { const expirationMonth = expirationDate.month; diff --git a/src/components/CardForm.tsx b/src/components/CardForm.tsx index 0a20464373..39ad5d1477 100644 --- a/src/components/CardForm.tsx +++ b/src/components/CardForm.tsx @@ -1,10 +1,10 @@ -import styles from "./CardForm.module.css"; +import styles from "../styles/CardForm.module.css"; -type CardFormProps = { +interface CardFormProps { cardFormLabelText: string; cardFormLabelCaption?: string; cardLabelText: string; -}; +} function CardForm({ cardFormLabelText, diff --git a/src/components/CardFormInput.tsx b/src/components/CardFormInput.tsx index 332a2fbee9..99100acbc9 100644 --- a/src/components/CardFormInput.tsx +++ b/src/components/CardFormInput.tsx @@ -1,16 +1,16 @@ import React from "react"; -import type { ChangeEvent } from "../types/card"; -import styles from "./CardFormInput.module.css"; +import type { ChangeEvent } from "../types/cardTypes"; +import styles from "../styles/CardFormInput.module.css"; -type CardFormProps = { +interface CardFormProps { name: string; cardPlaceHolder: string; cardInput: number | string; handleChange: (e: ChangeEvent) => void; width?: string; - disabled?: boolean; - isError?: boolean; -}; + isDisable?: boolean; + hasError?: boolean; +} function CardFormInput({ name, @@ -18,8 +18,8 @@ function CardFormInput({ cardInput, handleChange, width, - disabled, - isError, + isDisable, + hasError, }: CardFormProps) { return ( <> @@ -32,9 +32,9 @@ function CardFormInput({ onChange={handleChange} style={{ width: width, - borderColor: isError ? "red" : "#d5d5d5", + borderColor: hasError ? "red" : "#d5d5d5", }} - disabled={disabled} + disabled={isDisable} > ); diff --git a/src/constants/ErrorMessage.ts b/src/constants/ErrorMessage.ts index c174ae9917..7bce166174 100644 --- a/src/constants/ErrorMessage.ts +++ b/src/constants/ErrorMessage.ts @@ -8,4 +8,5 @@ export const ERROR_MESSAGE = { PAST_DATE: "์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ์—ฐ๋„์ž…๋‹ˆ๋‹ค.", OVER_MAX_VALID_YEAR: "์œ ํšจ๊ธฐ๊ฐ„์€ ์ตœ๋Œ€ 10๋…„๊นŒ์ง€๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", MIN_LENGTH_TWO: "์ˆซ์ž ๋‘ ์ž๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.", + REQUIRE_FOUR_DIGIT_NUMBER: "4์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.", }; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000000..2532982ceb --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,3 @@ +export * from "./errorMessage"; +export * from "./textConstants"; +export * from "./usingNumbers"; diff --git a/src/constants/CardConstants.ts b/src/constants/textConstants.ts similarity index 100% rename from src/constants/CardConstants.ts rename to src/constants/textConstants.ts diff --git a/src/constants/usingNumbers.ts b/src/constants/usingNumbers.ts new file mode 100644 index 0000000000..67a5f3243b --- /dev/null +++ b/src/constants/usingNumbers.ts @@ -0,0 +1,14 @@ +export const CARD_LOGO_NUMBER = { + VISA_NUMBER: "4", + MASTER_MIN_NUMBER: 51, + MASTER_MAX_NUMBER: 55, +}; + +export const CARD_PREFIX_LENGTH = { + VISA: 1, + MASTER: 2, +}; + +export const CARD_NUMBER_MAX_LENGTH = 4; + +export const MASK_SYMBOL = "*"; diff --git a/src/components/Card.module.css b/src/styles/Card.module.css similarity index 100% rename from src/components/Card.module.css rename to src/styles/Card.module.css diff --git a/src/components/CardForm.module.css b/src/styles/CardForm.module.css similarity index 100% rename from src/components/CardForm.module.css rename to src/styles/CardForm.module.css diff --git a/src/components/CardFormInput.module.css b/src/styles/CardFormInput.module.css similarity index 100% rename from src/components/CardFormInput.module.css rename to src/styles/CardFormInput.module.css diff --git a/src/types/card.ts b/src/types/cardTypes.ts similarity index 100% rename from src/types/card.ts rename to src/types/cardTypes.ts diff --git a/src/types/error.ts b/src/types/error.ts deleted file mode 100644 index 841919afad..0000000000 --- a/src/types/error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CardError { - cardNumbersError: string; - expirationDateError: string; - ownerError: string; -} diff --git a/src/types/errorType.ts b/src/types/errorType.ts new file mode 100644 index 0000000000..6074bf525e --- /dev/null +++ b/src/types/errorType.ts @@ -0,0 +1,26 @@ +export interface CardError { + cardNumbersError: CardNumbersError; + expirationDateError: ExpirationDateError; + ownerError: ErrorType; +} + +export interface ErrorType { + hasError: boolean; + errorMessage: string; +} + +export interface CardNumberBlockError extends ErrorType { + isDisable: boolean; +} + +export interface ExpirationDateError { + month: ErrorType; + year: ErrorType; +} + +export interface CardNumbersError { + firstBlock: CardNumberBlockError; + secondBlock: CardNumberBlockError; + thirdBlock: CardNumberBlockError; + fourthBlock: CardNumberBlockError; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000000..d9674448f4 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./cardTypes"; +export * from "./errorType"; diff --git a/src/utils/ValidCardNumbers.tsx b/src/utils/ValidCardNumbers.tsx deleted file mode 100644 index c0da5a3c92..0000000000 --- a/src/utils/ValidCardNumbers.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ERROR_MESSAGE } from "../constants/ErrorMessage"; -import { isOnlyNumber } from "./NumberUtils"; -import visa from "../assets/Visa.svg"; -import master from "../assets/Mastercard.svg"; - -// Visa ๋˜๋Š” Master ๋กœ๊ณ  ์„ค์ • -export const setCardLogo = (value: string): string => { - if (value.slice(0, 1) === "4") return visa; - const first = Number(value.slice(0, 2)); - if (51 <= first && first <= 55) return master; - return ""; -}; - -// ์ˆซ์ž๋ฅผ '*' ์ฒ˜๋ฆฌ -export const hideNumber = (value: string): string => - value.replace(/[0-9]/g, "*"); - -// ์นด๋“œ ๋ฒˆํ˜ธ ๊ฒ€์ฆ -export const validCardNumbersBlock = (block: string) => { - if (!isOnlyNumber(block)) return ERROR_MESSAGE.ONLY_NUMBER; - - return ""; -}; diff --git a/src/utils/ValidExpirationDate.tsx b/src/utils/ValidExpirationDate.tsx deleted file mode 100644 index 937d9b8aeb..0000000000 --- a/src/utils/ValidExpirationDate.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { ERROR_MESSAGE } from "../constants/ErrorMessage"; -import { isOnlyNumber } from "./NumberUtils"; - -// ์˜ค๋Š˜ ๋‚ ์งœ, ์—ฐ๋„, ์›” -const today: Date = new Date(); -const currentYear: number = today.getFullYear(); -const currentMonth: number = today.getMonth() + 1; - -// ์ž…๋ ฅํ•œ 2์ž๋ฆฌ ์—ฐ๋„๋ฅผ 4์ž๋ฆฌ ์—ฐ๋„๋กœ ๋ณ€๊ฒฝ -const convertYear = (year: number): number => - Math.floor(currentYear / 100) * 100 + year; - -// ๋‘์ž๋ฆฌ๋กœ ์›”์„ ๋ฐ˜ํ™˜ (ex. 3์›” ์ž…๋ ฅ ์‹œ 03์›”๋กœ ๋ณ€ํ™˜) -export const convertMonth = (month: string): string => { - const monthNumber = Number(month); - if (isNaN(monthNumber)) return ""; - return monthNumber < 10 ? `0${monthNumber}` : `${monthNumber}`; -}; - -// 2๊ธ€์ž๋ฅผ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ -const isLengthTwo = (value: string): boolean => value.length === 2; - -// ์ž…๋ ฅํ•œ ๋‹ฌ์ด 1์›”๋ถ€ํ„ฐ 12์›” ์‚ฌ์ด์ธ์ง€ ๊ฒ€์ฆ -const isInValidMonthRange = (month: number): boolean => - month >= 1 && month <= 12; - -// ์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ -const isNotPastDate = (month: number, year: number): boolean => { - const inputDate: Date = new Date(year, month - 1, 1); - return inputDate >= today; -}; - -// ์œ ํšจ๊ธฐ๊ฐ„์ด 10๋…„ ์ด๋‚ด์ธ์ง€ ํ™•์ธ -const isOverMaxValidYear = (month: number, year: number): boolean => { - const tenYearsLaterDate = new Date(currentYear + 10, currentMonth - 1, 1); - const inputDate = new Date(year, month - 1, 1); - - return inputDate <= tenYearsLaterDate; -}; - -// ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ -export const validExpirationDate = ( - inputMonth: string, - inputYear: string -): string => { - if (!isOnlyNumber(inputMonth) || !isOnlyNumber(inputYear)) - return ERROR_MESSAGE.ONLY_NUMBER; - - const month: number = Number(inputMonth); - const year: number = convertYear(Number(inputYear)); - - if (!isLengthTwo(convertMonth(inputMonth)) || !isLengthTwo(inputYear)) - return ERROR_MESSAGE.MIN_LENGTH_TWO; - if (!isInValidMonthRange(month)) return ERROR_MESSAGE.VALID_MONTH_RANGE; - if (!isNotPastDate(month, year)) return ERROR_MESSAGE.PAST_DATE; - if (!isOverMaxValidYear(month, year)) - return ERROR_MESSAGE.OVER_MAX_VALID_YEAR; - - return ""; -}; diff --git a/src/utils/ValidOwnerName.tsx b/src/utils/ValidOwnerName.tsx deleted file mode 100644 index e157807a67..0000000000 --- a/src/utils/ValidOwnerName.tsx +++ /dev/null @@ -1,28 +0,0 @@ -{ - /* ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ•จ์ˆ˜ - 1. formatStringToUpper -> ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ - 2. isEnglishOnly -> ์˜๋ฌธ๋งŒ ์žˆ๋Š”์ง€ ๊ฒ€์ฆ - 3. isMinLength -> ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ธ์ง€ ๊ฒ€์ฆ - - validOwnerName -> ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฐ˜ํ™˜ - */ -} - -import { ERROR_MESSAGE } from "../constants/ErrorMessage"; - -// ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ -export const formatStringToUpper = (text: string): string => text.toUpperCase(); - -// ์˜๋ฌธ๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ -const isEnglishOnly = (text: string): boolean => /^[A-Za-z\s]+$/.test(text); - -// ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ธ์ง€ ๊ฒ€์ฆ -const isMinLength = (text: string): boolean => text.length >= 2; - -// ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฐ˜ํ™˜ -export const validOwnerName = (text: string): string => { - if (!isEnglishOnly(text)) return ERROR_MESSAGE.ONLY_ENGLISH; - if (!isMinLength(text)) return ERROR_MESSAGE.MIN_LENGTH_REQUIRED; - - return ""; -}; diff --git a/src/utils/cardNumberValidators.tsx b/src/utils/cardNumberValidators.tsx new file mode 100644 index 0000000000..d2c47646e9 --- /dev/null +++ b/src/utils/cardNumberValidators.tsx @@ -0,0 +1,83 @@ +import { ERROR_MESSAGE } from "../constants/errorMessage"; +import { isOnlyNumber } from "./inputFilters"; +import visa from "../assets/Visa.svg"; +import master from "../assets/Mastercard.svg"; +import { CardNumberBlockError, CardNumbers, CardNumbersError } from "../types"; +import { + CARD_LOGO_NUMBER, + MASK_SYMBOL, + CARD_PREFIX_LENGTH, + CARD_NUMBER_MAX_LENGTH, +} from "../constants"; + +// Visa ๋˜๋Š” Master ๋กœ๊ณ  ์„ค์ • +export const setCardLogo = (value: string): string => { + if (value.slice(0, CARD_PREFIX_LENGTH.VISA) === CARD_LOGO_NUMBER.VISA_NUMBER) + return visa; + const first = Number(value.slice(0, CARD_PREFIX_LENGTH.MASTER)); + if ( + CARD_LOGO_NUMBER.MASTER_MIN_NUMBER <= first && + first <= CARD_LOGO_NUMBER.MASTER_MAX_NUMBER + ) + return master; + return ""; +}; + +// ์ˆซ์ž๋ฅผ '*' ์ฒ˜๋ฆฌ +export const hideNumber = (value: string): string => + value.replace(/[0-9]/g, MASK_SYMBOL); + +// ์นด๋“œ ๋ฒˆํ˜ธ ๊ฒ€์ฆ +export const validCardNumbers = (block: string): CardNumberBlockError => { + if (!isOnlyNumber(block)) + return { + hasError: true, + errorMessage: ERROR_MESSAGE.ONLY_NUMBER, + isDisable: false, + }; + + return { + hasError: false, + errorMessage: "", + isDisable: false, + }; +}; + +// ์นด๋“œ ๋ฒˆํ˜ธ ๋ธ”๋ก ๊ฒ€์ฆ +export const validCardNumbersBlock = ( + cardNumbers: CardNumbers +): CardNumbersError => { + type CardNumbersKeys = keyof CardNumbers; + const keys: CardNumbersKeys[] = [ + "firstBlock", + "secondBlock", + "thirdBlock", + "fourthBlock", + ]; + const result: CardNumbersError = { + firstBlock: { hasError: false, errorMessage: "", isDisable: false }, + secondBlock: { hasError: false, errorMessage: "", isDisable: false }, + thirdBlock: { hasError: false, errorMessage: "", isDisable: false }, + fourthBlock: { hasError: false, errorMessage: "", isDisable: false }, + }; + + keys.forEach((key, index) => { + if (index === 0) return; + + const preValue = cardNumbers[keys[index - 1]]; + const currentValue = cardNumbers[keys[index]]; + const isPrevFilled = preValue.length === CARD_NUMBER_MAX_LENGTH; + const isCurrentFilled = currentValue.length === CARD_NUMBER_MAX_LENGTH; + + result[key] = { + hasError: !isPrevFilled || !isCurrentFilled, + errorMessage: + !isPrevFilled || !isCurrentFilled + ? ERROR_MESSAGE.REQUIRE_FOUR_DIGIT_NUMBER + : "", + isDisable: !isPrevFilled, + }; + }); + + return result; +}; diff --git a/src/utils/errorHelpers.tsx b/src/utils/errorHelpers.tsx new file mode 100644 index 0000000000..4d85d0777a --- /dev/null +++ b/src/utils/errorHelpers.tsx @@ -0,0 +1,17 @@ +import type { ExpirationDateError, CardNumbersError } from "../types"; + +// ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ์—์„œ ๊ฐ€์žฅ ์ฒซ๋ฒˆ์งธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ +export const getFirstExpirationErrorMessage = ( + error: ExpirationDateError +): string => { + if (error.month.hasError) return error.month.errorMessage; + if (error.year.hasError) return error.year.errorMessage; + + return ""; +}; + +// ์นด๋“œ๋ฒˆํ˜ธ ๊ฒ€์ฆ์—์„œ ๊ฐ€์žฅ ์ฒซ๋ฒˆ์งธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ +export const getFirstErrorMessage = (error: CardNumbersError): string => { + const errorBlock = Object.values(error).find((block) => block.hasError); + return errorBlock ? errorBlock.errorMessage : ""; +}; diff --git a/src/utils/expirationDateValidators.tsx b/src/utils/expirationDateValidators.tsx new file mode 100644 index 0000000000..ff85f82ad1 --- /dev/null +++ b/src/utils/expirationDateValidators.tsx @@ -0,0 +1,112 @@ +import { ERROR_MESSAGE } from "../constants/errorMessage"; +import { isOnlyNumber } from "./inputFilters"; +import type { ErrorType, ExpirationDateError } from "../types"; + +// ์˜ค๋Š˜ ๋‚ ์งœ, ์—ฐ๋„, ์›” +const today: Date = new Date(); +const currentYear: number = today.getFullYear(); +const currentMonth: number = today.getMonth() + 1; + +// ์ž…๋ ฅํ•œ 2์ž๋ฆฌ ์—ฐ๋„๋ฅผ 4์ž๋ฆฌ ์—ฐ๋„๋กœ ๋ณ€๊ฒฝ +const convertYear = (year: number): number => + Math.floor(currentYear / 100) * 100 + year; + +// ๋‘์ž๋ฆฌ๋กœ ์›”์„ ๋ฐ˜ํ™˜ (ex. 3์›” ์ž…๋ ฅ ์‹œ 03์›”๋กœ ๋ณ€ํ™˜) +export const convertMonth = (month: string): string => { + const monthNumber = Number(month); + if (isNaN(monthNumber)) return ""; + return monthNumber < 10 ? `0${monthNumber}` : `${monthNumber}`; +}; + +// 2๊ธ€์ž๋ฅผ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +const isLengthTwo = (value: string): boolean => value.length === 2; + +// ์ž…๋ ฅํ•œ ๋‹ฌ์ด 1์›”๋ถ€ํ„ฐ 12์›” ์‚ฌ์ด์ธ์ง€ ๊ฒ€์ฆ +const isInValidMonthRange = (month: number): boolean => + month >= 1 && month <= 12; + +// ์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ +const isNotPastDate = (month: number, year: number): boolean => { + const inputDate: Date = new Date(year, month - 1, 1); + return inputDate >= today; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„์ด 10๋…„ ์ด๋‚ด์ธ์ง€ ํ™•์ธ +const isOverMaxValidYear = (month: number, year: number): boolean => { + const tenYearsLaterDate = new Date(currentYear + 10, currentMonth - 1, 1); + const inputDate = new Date(year, month - 1, 1); + + return inputDate <= tenYearsLaterDate; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„์— ์ž…๋ ฅํ•œ '์›”(Month)' ๊ฒ€์ฆ +const validExpirationMonth = (inputMonth: string): ErrorType => { + if (!isOnlyNumber(inputMonth)) + return { hasError: true, errorMessage: ERROR_MESSAGE.ONLY_NUMBER }; + + if (!isLengthTwo(convertMonth(inputMonth))) + return { hasError: true, errorMessage: ERROR_MESSAGE.MIN_LENGTH_TWO }; + + if (!isInValidMonthRange(Number(inputMonth))) + return { hasError: true, errorMessage: ERROR_MESSAGE.VALID_MONTH_RANGE }; + + return { hasError: false, errorMessage: "" }; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„์— ์ž…๋ ฅํ•œ '์—ฐ๋„(Year)' ๊ฒ€์ฆ +const validExpirationYear = (inputYear: string): ErrorType => { + if (!isOnlyNumber(inputYear)) + return { hasError: true, errorMessage: ERROR_MESSAGE.ONLY_NUMBER }; + + if (!isLengthTwo(convertMonth(inputYear))) + return { hasError: true, errorMessage: ERROR_MESSAGE.MIN_LENGTH_TWO }; + + return { hasError: false, errorMessage: "" }; +}; + +// ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌํƒ€์ž… ๋ฐ˜ํ™˜ +export const validExpirationDate = ( + inputMonth: string, + inputYear: string +): ExpirationDateError => { + const monthError: ErrorType = validExpirationMonth(inputMonth); + const yearError: ErrorType = validExpirationYear(inputYear); + + if (monthError.hasError || yearError.hasError) + return { + month: monthError, + year: yearError, + }; + + const monthNumber: number = Number(inputMonth); + const yearNumber: number = convertYear(Number(inputYear)); + + if (!isNotPastDate(monthNumber, yearNumber)) + return { + month: { + hasError: true, + errorMessage: ERROR_MESSAGE.PAST_DATE, + }, + year: { + hasError: true, + errorMessage: ERROR_MESSAGE.PAST_DATE, + }, + }; + + if (!isOverMaxValidYear(monthNumber, yearNumber)) + return { + month: { + hasError: true, + errorMessage: ERROR_MESSAGE.OVER_MAX_VALID_YEAR, + }, + year: { + hasError: true, + errorMessage: ERROR_MESSAGE.OVER_MAX_VALID_YEAR, + }, + }; + + return { + month: monthError, + year: yearError, + }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000000..1c84796346 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./inputFilters"; +export * from "./ownerNameValidators"; +export * from "./expirationDateValidators"; +export * from "./cardNumberValidators"; +export * from "./errorHelpers"; diff --git a/src/utils/NumberUtils.tsx b/src/utils/inputFilters.tsx similarity index 65% rename from src/utils/NumberUtils.tsx rename to src/utils/inputFilters.tsx index a36ae0abc3..3409721b9b 100644 --- a/src/utils/NumberUtils.tsx +++ b/src/utils/inputFilters.tsx @@ -11,8 +11,18 @@ const limitToDigits = (value: string, max: number) => { // ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ const inputOnlyNumber = (value: string): string => value.replace(/[^0-9]/g, ""); +// ์˜์–ด๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ +const inputOnlyEnglish = (value: string): string => + value.replace(/[^a-zA-Z\s]/g, ""); + // ์ž…๋ ฅํ•œ max ์ˆ˜๋งŒํผ ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ export const filterNumber = (value: string, max: number) => { const result = limitToDigits(value, max); return inputOnlyNumber(result); }; + +// ์ž…๋ ฅํ•œ max ์ˆ˜๋งŒํผ ์˜์–ด๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ +export const filterString = (value: string, max: number) => { + const result = limitToDigits(value, max); + return inputOnlyEnglish(result); +}; diff --git a/src/utils/ownerNameValidators.tsx b/src/utils/ownerNameValidators.tsx new file mode 100644 index 0000000000..2d7da088e4 --- /dev/null +++ b/src/utils/ownerNameValidators.tsx @@ -0,0 +1,30 @@ +import { ERROR_MESSAGE } from "../constants/errorMessage"; +import type { ErrorType } from "../types"; + +// ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ +export const formatStringToUpper = (text: string): string => text.toUpperCase(); + +// ์˜๋ฌธ๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +const isEnglishOnly = (text: string): boolean => /^[A-Za-z\s]+$/.test(text); + +// ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ์ธ์ง€ ๊ฒ€์ฆ +const isMinLength = (text: string): boolean => text.length >= 2; + +// ์†Œ์œ ์ž ์ด๋ฆ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌ ๋ฐ˜ํ™˜ +export const validOwnerName = (text: string): ErrorType => { + if (!isEnglishOnly(text)) + return { + hasError: true, + errorMessage: ERROR_MESSAGE.ONLY_ENGLISH, + }; + if (!isMinLength(text)) + return { + hasError: true, + errorMessage: ERROR_MESSAGE.MIN_LENGTH_REQUIRED, + }; + + return { + hasError: false, + errorMessage: "", + }; +}; From e21351b4623b266c5c01ebb0328ccfed3354dbc8 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 6 Apr 2025 15:16:29 +0900 Subject: [PATCH 3/9] =?UTF-8?q?Fix:=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 205 +------------------------------------------- src/pages/Home.tsx | 208 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 203 deletions(-) create mode 100644 src/pages/Home.tsx diff --git a/src/App.tsx b/src/App.tsx index 043a4960ae..4d46e40edd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,208 +1,7 @@ -import { useState } from "react"; -import CardForm from "./components/CardForm"; -import CardFormInput from "./components/CardFormInput"; -import Card from "./components/Card"; -import { - CARD_FORM_LABELS, - CARD_LABELS, - CARD_PLACEHOLDERS, -} from "./constants/textConstants"; -import type { - ChangeEvent, - ExpirationDate, - CardNumbers, - CardError, - ExpirationDateError, - CardNumbersError, -} from "./types"; -import { - formatStringToUpper, - validOwnerName, - convertMonth, - validExpirationDate, - filterNumber, - filterString, - getFirstExpirationErrorMessage, - getFirstErrorMessage, - validCardNumbersBlock, -} from "./utils"; -import styles from "./styles/CardForm.module.css"; +import Home from "../src/pages/Home"; function App() { - const [expirationDate, setExpirationDate] = useState({ - month: "", - year: "", - }); - - const [cardNumbers, setCardNumbers] = useState({ - firstBlock: "", - secondBlock: "", - thirdBlock: "", - fourthBlock: "", - }); - - const [owner, setOwner] = useState(""); - - const [cardError, setCardError] = useState({ - cardNumbersError: { - firstBlock: { hasError: false, errorMessage: "", isDisable: false }, - secondBlock: { hasError: false, errorMessage: "", isDisable: true }, - thirdBlock: { hasError: false, errorMessage: "", isDisable: true }, - fourthBlock: { hasError: false, errorMessage: "", isDisable: true }, - }, - expirationDateError: { - month: { hasError: false, errorMessage: "" }, - year: { hasError: false, errorMessage: "" }, - }, - ownerError: { hasError: false, errorMessage: "" }, - }); - - const handleExpirationDateChange = (event: ChangeEvent) => { - const name = event.target.name; - const value = event.target.value; - - const filterValue = filterNumber( - name === "month" ? convertMonth(value) : value, - 2 - ); - - const newDate = { - ...expirationDate, - [name]: filterValue, - }; - - const validResult = validExpirationDate(newDate.month, newDate.year); - - setCardError((pre) => ({ - ...pre, - expirationDateError: validResult, - })); - - setExpirationDate({ - month: newDate.month, - year: newDate.year, - }); - }; - - const handleOwnerChange = (event: ChangeEvent) => { - const value = filterString(event.target.value, 20); - - const validResult = validOwnerName(event.target.value); - - setCardError((pre) => ({ - ...pre, - ownerError: validResult, - })); - const ownerName = formatStringToUpper(value); - setOwner(ownerName); - }; - - const handleCardNumbersChange = (event: ChangeEvent) => { - const name = event.target.name; - const value = event.target.value; - - const numberBlock = { - ...cardNumbers, - [name]: filterNumber(value, 4), - }; - - const result: CardNumbersError = validCardNumbersBlock(numberBlock); - - setCardError((pre) => ({ - ...pre, - cardNumbersError: result, - })); - - setCardNumbers({ - firstBlock: numberBlock.firstBlock, - secondBlock: numberBlock.secondBlock, - thirdBlock: numberBlock.thirdBlock, - fourthBlock: numberBlock.fourthBlock, - }); - }; - - return ( - <> - - - - {Object.entries(cardNumbers).map(([key, value]) => ( - - ))} - {cardError.cardNumbersError && ( -

- {getFirstErrorMessage(cardError.cardNumbersError)} -

- )} - - - {Object.entries(expirationDate).map(([key, value]) => ( - - ))} - {cardError.expirationDateError && ( -

- {getFirstExpirationErrorMessage(cardError.expirationDateError)} -

- )} - - - - {cardError.ownerError && ( -

{cardError.ownerError.errorMessage}

- )} - - ); + return ; } export default App; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000000..8007aea4cb --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,208 @@ +import { useState } from "react"; +import CardForm from "../components/CardForm"; +import CardFormInput from "../components/CardFormInput"; +import Card from "../components/Card"; +import { + CARD_FORM_LABELS, + CARD_LABELS, + CARD_PLACEHOLDERS, +} from "../constants/textConstants"; +import type { + ChangeEvent, + ExpirationDate, + CardNumbers, + CardError, + ExpirationDateError, + CardNumbersError, +} from "../types"; +import { + formatStringToUpper, + validOwnerName, + convertMonth, + validExpirationDate, + filterNumber, + filterString, + getFirstExpirationErrorMessage, + getFirstErrorMessage, + validCardNumbersBlock, +} from "../utils"; +import styles from "../styles/CardForm.module.css"; + +function Home() { + const [expirationDate, setExpirationDate] = useState({ + month: "", + year: "", + }); + + const [cardNumbers, setCardNumbers] = useState({ + firstBlock: "", + secondBlock: "", + thirdBlock: "", + fourthBlock: "", + }); + + const [owner, setOwner] = useState(""); + + const [cardError, setCardError] = useState({ + cardNumbersError: { + firstBlock: { hasError: false, errorMessage: "", isDisable: false }, + secondBlock: { hasError: false, errorMessage: "", isDisable: true }, + thirdBlock: { hasError: false, errorMessage: "", isDisable: true }, + fourthBlock: { hasError: false, errorMessage: "", isDisable: true }, + }, + expirationDateError: { + month: { hasError: false, errorMessage: "" }, + year: { hasError: false, errorMessage: "" }, + }, + ownerError: { hasError: false, errorMessage: "" }, + }); + + const handleExpirationDateChange = (event: ChangeEvent) => { + const name = event.target.name; + const value = event.target.value; + + const filterValue = filterNumber( + name === "month" ? convertMonth(value) : value, + 2 + ); + + const newDate = { + ...expirationDate, + [name]: filterValue, + }; + + const validResult = validExpirationDate(newDate.month, newDate.year); + + setCardError((pre) => ({ + ...pre, + expirationDateError: validResult, + })); + + setExpirationDate({ + month: newDate.month, + year: newDate.year, + }); + }; + + const handleOwnerChange = (event: ChangeEvent) => { + const value = filterString(event.target.value, 20); + + const validResult = validOwnerName(event.target.value); + + setCardError((pre) => ({ + ...pre, + ownerError: validResult, + })); + const ownerName = formatStringToUpper(value); + setOwner(ownerName); + }; + + const handleCardNumbersChange = (event: ChangeEvent) => { + const name = event.target.name; + const value = event.target.value; + + const numberBlock = { + ...cardNumbers, + [name]: filterNumber(value, 4), + }; + + const result: CardNumbersError = validCardNumbersBlock(numberBlock); + + setCardError((pre) => ({ + ...pre, + cardNumbersError: result, + })); + + setCardNumbers({ + firstBlock: numberBlock.firstBlock, + secondBlock: numberBlock.secondBlock, + thirdBlock: numberBlock.thirdBlock, + fourthBlock: numberBlock.fourthBlock, + }); + }; + + return ( + <> + + + + {Object.entries(cardNumbers).map(([key, value]) => ( + + ))} + {cardError.cardNumbersError && ( +

+ {getFirstErrorMessage(cardError.cardNumbersError)} +

+ )} + + + {Object.entries(expirationDate).map(([key, value]) => ( + + ))} + {cardError.expirationDateError && ( +

+ {getFirstExpirationErrorMessage(cardError.expirationDateError)} +

+ )} + + + + {cardError.ownerError && ( +

{cardError.ownerError.errorMessage}

+ )} + + ); +} + +export default Home; From c3e6810230e9b042bf22fe1098e6d7b8b75213c5 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 6 Apr 2025 15:32:15 +0900 Subject: [PATCH 4/9] =?UTF-8?q?Fix:=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 81 +++++++++++++++++++++++--------- src/components/CardFormInput.tsx | 4 +- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1a0cfb5f04..60dc9317ce 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,64 @@ # react-payments +## ๐Ÿ“‚ ํŒŒ์ผ ๊ตฌ์กฐ + +๐Ÿ“ฆsrc +โ”ฃ ๐Ÿ“‚assets +โ”ƒ โ”ฃ ๐Ÿ“œMastercard.svg +โ”ƒ โ”— ๐Ÿ“œVisa.svg +โ”ฃ ๐Ÿ“‚components +โ”ƒ โ”ฃ ๐Ÿ“œCard.tsx +โ”ƒ โ”ฃ ๐Ÿ“œCardForm.tsx +โ”ƒ โ”— ๐Ÿ“œCardFormInput.tsx +โ”ฃ ๐Ÿ“‚constants +โ”ƒ โ”ฃ ๐Ÿ“œerrorMessage.ts +โ”ƒ โ”ฃ ๐Ÿ“œindex.ts +โ”ƒ โ”ฃ ๐Ÿ“œtextConstants.ts +โ”ƒ โ”— ๐Ÿ“œusingNumbers.ts +โ”ฃ ๐Ÿ“‚pages +โ”ƒ โ”— ๐Ÿ“œHome.tsx +โ”ฃ ๐Ÿ“‚stories +โ”ƒ โ”— ๐Ÿ“œApp.stories.tsx +โ”ฃ ๐Ÿ“‚styles +โ”ƒ โ”ฃ ๐Ÿ“œCard.module.css +โ”ƒ โ”ฃ ๐Ÿ“œCardForm.module.css +โ”ƒ โ”— ๐Ÿ“œCardFormInput.module.css +โ”ฃ ๐Ÿ“‚types +โ”ƒ โ”ฃ ๐Ÿ“œcardTypes.ts +โ”ƒ โ”ฃ ๐Ÿ“œerrorType.ts +โ”ƒ โ”— ๐Ÿ“œindex.ts +โ”ฃ ๐Ÿ“‚utils +โ”ƒ โ”ฃ ๐Ÿ“œcardNumberValidators.tsx +โ”ƒ โ”ฃ ๐Ÿ“œerrorHelpers.tsx +โ”ƒ โ”ฃ ๐Ÿ“œexpirationDateValidators.tsx +โ”ƒ โ”ฃ ๐Ÿ“œindex.ts +โ”ƒ โ”ฃ ๐Ÿ“œinputFilters.tsx +โ”ƒ โ”— ๐Ÿ“œownerNameValidators.tsx +โ”ฃ ๐Ÿ“œApp.tsx +โ”ฃ ๐Ÿ“œindex.css +โ”ฃ ๐Ÿ“œmain.tsx +โ”— ๐Ÿ“œvite-env.d.ts + ## ๐Ÿ’ณ ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ -- [ ] ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ ์‹๋ณ„ - - [V] ์นด๋“œ ๋ฒˆํ˜ธ์˜ 3~4๋ฒˆ ๋ธ”๋Ÿญ์€ ์ˆจ๊น€ ์ฒ˜๋ฆฌํ•œ๋‹ค. - - [V] ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋ผ๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ณด์—ฌ์ฃผ๊ณ , ์ž…๋ ฅ์„ ์ œํ•œํ•œ๋‹ค. - - [V] ๊ฐ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ธ”๋Ÿญ์€ 0~9์˜ ์ˆซ์ž 4์ž๋ฆฌ๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ๋‹ค. - - [V] ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ 4์ž๋ฆฌ ๋ฏธ๋งŒ์œผ๋กœ ์ž…๋ ฅํ•˜๋Š” ์ค‘์— ํฌ์ปค์Šค๋ฅผ ๋„˜๊ธฐ๋ ค๊ณ  ํ•œ๋‹ค๋ฉด ์—๋Ÿฌ๋กœ ๋ง‰๋Š”๋‹ค. -- [V] ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ - - [ ] ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์ˆซ์ž๊ฐ€ ์•„๋‹์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. - - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ์›”์„ ์ž…๋ ฅ ์‹œ(ex 13์›”) ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. - - [ ] ํ˜„์žฌ๋ณด๋‹ค ์ด์ „ ๋‚ ์งœ๋ฅผ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. - - [ ] ํ•œ ์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ํ˜•์‹์— ๋งž์ถฐ 0์„ ๋„ฃ์–ด์ค€๋‹ค. -- [V] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ - - [V] ์†Œ๋ฌธ์ž๋กœ ์ž…๋ ฅ ์‹œ ๊ฐ•์ œ๋กœ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. - - [V] ์˜์–ด๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž ์ž…๋ ฅ ์‹œ ์ž…๋ ฅ์„ ์ œํ•œํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. - - [V] (์ถ”๊ฐ€) ์‚ฌ์šฉ์ž ์ด๋ฆ„์€ ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค. -- [V] ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ์—…๋ฐ์ดํŠธ - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 4๋กœ ์‹œ์ž‘ํ•˜๋ฉด Visa์นด๋“œ ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 51~55๋กœ ์‹œ์ž‘ํ•˜๋ฉด MasterCard ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. - - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. - - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. - - [ ] ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. +- ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ ์‹๋ณ„ + - ์นด๋“œ ๋ฒˆํ˜ธ์˜ 3~4๋ฒˆ ๋ธ”๋Ÿญ์€ ์ˆจ๊น€ ์ฒ˜๋ฆฌํ•œ๋‹ค. + - ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž…๋ ฅํ•˜๋ผ๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ณด์—ฌ์ฃผ๊ณ , ์ž…๋ ฅ์„ ์ œํ•œํ•œ๋‹ค. + - ๊ฐ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ธ”๋Ÿญ์€ 0~9์˜ ์ˆซ์ž 4์ž๋ฆฌ๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ๋‹ค. + - ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - ์นด๋“œ ๋ฒˆํ˜ธ 4์ž๋ฆฌ ๋ฏธ๋งŒ์œผ๋กœ ์ž…๋ ฅํ•˜๋Š” ์ค‘์— ํฌ์ปค์Šค๋ฅผ ๋„˜๊ธฐ๋ ค๊ณ  ํ•œ๋‹ค๋ฉด ์—๋Ÿฌ๋กœ ๋ง‰๋Š”๋‹ค. +- ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ + - ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์ˆซ์ž๊ฐ€ ์•„๋‹์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - ์œ ํšจํ•˜์ง€ ์•Š์€ ์›”์„ ์ž…๋ ฅ ์‹œ(ex 13์›”) ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - ํ˜„์žฌ๋ณด๋‹ค ์ด์ „ ๋‚ ์งœ๋ฅผ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - ํ•œ ์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ํ˜•์‹์— ๋งž์ถฐ 0์„ ๋„ฃ์–ด์ค€๋‹ค. +- ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ + - ์†Œ๋ฌธ์ž๋กœ ์ž…๋ ฅ ์‹œ ๊ฐ•์ œ๋กœ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. + - ์˜์–ด๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž ์ž…๋ ฅ ์‹œ ์ž…๋ ฅ์„ ์ œํ•œํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + - (์ถ”๊ฐ€) ์‚ฌ์šฉ์ž ์ด๋ฆ„์€ ์ตœ์†Œ ๋‘๊ธ€์ž ์ด์ƒ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค. +- ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ์—…๋ฐ์ดํŠธ + - ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 4๋กœ ์‹œ์ž‘ํ•˜๋ฉด Visa์นด๋“œ ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - ์นด๋“œ ๋ฒˆํ˜ธ๊ฐ€ 51~55๋กœ ์‹œ์ž‘ํ•˜๋ฉด MasterCard ๋กœ๊ณ ๋ฅผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + - ์‚ฌ์šฉ์ž๊ฐ€ ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. diff --git a/src/components/CardFormInput.tsx b/src/components/CardFormInput.tsx index 99100acbc9..705321cd15 100644 --- a/src/components/CardFormInput.tsx +++ b/src/components/CardFormInput.tsx @@ -7,9 +7,9 @@ interface CardFormProps { cardPlaceHolder: string; cardInput: number | string; handleChange: (e: ChangeEvent) => void; - width?: string; + width: string; isDisable?: boolean; - hasError?: boolean; + hasError: boolean; } function CardFormInput({ From 66e77495ec8041700fe860380ac4ac2728120ca1 Mon Sep 17 00:00:00 2001 From: DaKyung Date: Sun, 6 Apr 2025 15:36:59 +0900 Subject: [PATCH 5/9] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 60dc9317ce..3c9e33e13c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # react-payments ## ๐Ÿ“‚ ํŒŒ์ผ ๊ตฌ์กฐ - +``` ๐Ÿ“ฆsrc โ”ฃ ๐Ÿ“‚assets โ”ƒ โ”ฃ ๐Ÿ“œMastercard.svg @@ -38,6 +38,7 @@ โ”ฃ ๐Ÿ“œindex.css โ”ฃ ๐Ÿ“œmain.tsx โ”— ๐Ÿ“œvite-env.d.ts +``` ## ๐Ÿ’ณ ๊ธฐ๋Šฅ ์š”๊ตฌ ์‚ฌํ•ญ From fd390cb5d4da37cbfc07df81ce5d405fb86dc587 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 4 May 2025 09:44:48 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Fix:=20import=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/{ErrorMessage.ts => errorMessege.ts} | 0 src/constants/index.ts | 2 +- src/utils/cardNumberValidators.tsx | 2 +- src/utils/ownerNameValidators.tsx | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/constants/{ErrorMessage.ts => errorMessege.ts} (100%) diff --git a/src/constants/ErrorMessage.ts b/src/constants/errorMessege.ts similarity index 100% rename from src/constants/ErrorMessage.ts rename to src/constants/errorMessege.ts diff --git a/src/constants/index.ts b/src/constants/index.ts index 2532982ceb..3fb2c8442e 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,3 @@ -export * from "./errorMessage"; +export * from "./errorMessege"; export * from "./textConstants"; export * from "./usingNumbers"; diff --git a/src/utils/cardNumberValidators.tsx b/src/utils/cardNumberValidators.tsx index d2c47646e9..fb42e71cef 100644 --- a/src/utils/cardNumberValidators.tsx +++ b/src/utils/cardNumberValidators.tsx @@ -1,4 +1,4 @@ -import { ERROR_MESSAGE } from "../constants/errorMessage"; +import { ERROR_MESSAGE } from "../constants"; import { isOnlyNumber } from "./inputFilters"; import visa from "../assets/Visa.svg"; import master from "../assets/Mastercard.svg"; diff --git a/src/utils/ownerNameValidators.tsx b/src/utils/ownerNameValidators.tsx index 2d7da088e4..cb5a0fac18 100644 --- a/src/utils/ownerNameValidators.tsx +++ b/src/utils/ownerNameValidators.tsx @@ -1,4 +1,4 @@ -import { ERROR_MESSAGE } from "../constants/errorMessage"; +import { ERROR_MESSAGE } from "../constants"; import type { ErrorType } from "../types"; // ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ From bf5d9bf67ad95eb045b87078c0dcf3e2bb8477c6 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Sun, 4 May 2025 09:46:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?Fix:=20import=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20=ED=8C=8C=EC=9D=BC=EA=B2=BD=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/expirationDateValidators.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/expirationDateValidators.tsx b/src/utils/expirationDateValidators.tsx index ff85f82ad1..b4c94be801 100644 --- a/src/utils/expirationDateValidators.tsx +++ b/src/utils/expirationDateValidators.tsx @@ -1,4 +1,4 @@ -import { ERROR_MESSAGE } from "../constants/errorMessage"; +import { ERROR_MESSAGE } from "../constants"; import { isOnlyNumber } from "./inputFilters"; import type { ErrorType, ExpirationDateError } from "../types"; From 605e6cc426b878bbf0be14d2925d959a8e984a64 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Wed, 7 May 2025 12:38:22 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/types/cardTypes.ts | 38 +++++++++++++++++++++++++++----------- src/types/errorType.ts | 34 ++++++++++++++-------------------- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4d46e40edd..c24dfaac54 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import Home from "../src/pages/Home"; function App() { - return ; + return ; } export default App; diff --git a/src/types/cardTypes.ts b/src/types/cardTypes.ts index 51113ac7ef..170745b18b 100644 --- a/src/types/cardTypes.ts +++ b/src/types/cardTypes.ts @@ -1,13 +1,29 @@ -export type ChangeEvent = React.ChangeEvent; +export type InputName = + | "firstBlock" + | "secondBlock" + | "thirdBlock" + | "fourthBlock" + | "month" + | "year" + | "owner"; -export interface ExpirationDate { - month: string; - year: string; -} +export type ChangeEvent = React.ChangeEvent & { + target: { + name: InputName; + value: string; + }; +}; -export interface CardNumbers { - firstBlock: string; - secondBlock: string; - thirdBlock: string; - fourthBlock: string; -} +export type CardData = { + numbers: { + firstBlock: string; + secondBlock: string; + thirdBlock: string; + fourthBlock: string; + }; + expiration: { + month: string; + year: string; + }; + owner: string; +}; diff --git a/src/types/errorType.ts b/src/types/errorType.ts index 6074bf525e..089d926899 100644 --- a/src/types/errorType.ts +++ b/src/types/errorType.ts @@ -1,26 +1,20 @@ -export interface CardError { - cardNumbersError: CardNumbersError; - expirationDateError: ExpirationDateError; - ownerError: ErrorType; -} - export interface ErrorType { hasError: boolean; errorMessage: string; } - -export interface CardNumberBlockError extends ErrorType { +export interface CardNumberErrorType extends ErrorType { isDisable: boolean; } - -export interface ExpirationDateError { - month: ErrorType; - year: ErrorType; -} - -export interface CardNumbersError { - firstBlock: CardNumberBlockError; - secondBlock: CardNumberBlockError; - thirdBlock: CardNumberBlockError; - fourthBlock: CardNumberBlockError; -} +export type CardFormError = { + cardNumbers: { + firstBlock: CardNumberErrorType; + secondBlock: CardNumberErrorType; + thirdBlock: CardNumberErrorType; + fourthBlock: CardNumberErrorType; + }; + expiration: { + month: ErrorType; + year: ErrorType; + }; + owner: ErrorType; +}; From 083d93cdfcbdea7ba1803d88e512432c3b0fadf6 Mon Sep 17 00:00:00 2001 From: onegood07 Date: Wed, 14 May 2025 16:57:43 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=ED=9B=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 11 +- src/components/CardFormInput.tsx | 80 +++-- .../{usingNumbers.ts => cardConstants.ts} | 10 +- src/constants/index.ts | 2 +- src/constants/textConstants.ts | 22 +- src/pages/Home.tsx | 275 ++++++++++++------ src/styles/CardForm.module.css | 8 +- src/types/cardTypes.ts | 13 +- src/types/{errorType.ts => errorTypes.ts} | 4 +- src/types/index.ts | 2 +- src/utils/cardNumberValidators.tsx | 107 ++++--- src/utils/errorHelpers.tsx | 28 +- src/utils/expirationDateValidators.tsx | 24 +- src/utils/index.ts | 1 + src/utils/inputFilters.tsx | 32 +- src/utils/ownerNameValidators.tsx | 7 +- src/utils/validationUtils.tsx | 4 + 17 files changed, 411 insertions(+), 219 deletions(-) rename src/constants/{usingNumbers.ts => cardConstants.ts} (52%) rename src/types/{errorType.ts => errorTypes.ts} (92%) create mode 100644 src/utils/validationUtils.tsx diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 8aca249949..b07226774a 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,11 +1,12 @@ import styles from "../styles/Card.module.css"; -import type { CardNumbers, ExpirationDate } from "../types/cardTypes"; +import type { CardData } from "../types/cardTypes"; import { hideNumber, setCardLogo } from "../utils/cardNumberValidators"; +import { EMPTY_STRING } from "../constants"; interface CardProps { - cardNumbers: CardNumbers; - expirationDate: ExpirationDate; - owner: string; + cardNumbers: CardData["numbers"]; + expirationDate: CardData["expirationDate"]; + owner: CardData["owner"]; } function Card({ owner, expirationDate, cardNumbers }: CardProps) { @@ -15,7 +16,7 @@ function Card({ owner, expirationDate, cardNumbers }: CardProps) { const date = expirationMonth.length > 0 || expirationYear.length > 0 ? `${expirationMonth}/${expirationYear}` - : ""; + : EMPTY_STRING; const numbers: string = `${cardNumbers.firstBlock} ${cardNumbers.secondBlock} ${hideNumber(cardNumbers.thirdBlock)} ${hideNumber(cardNumbers.fourthBlock)}`; return ( diff --git a/src/components/CardFormInput.tsx b/src/components/CardFormInput.tsx index 705321cd15..1db33db469 100644 --- a/src/components/CardFormInput.tsx +++ b/src/components/CardFormInput.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { forwardRef } from "react"; import type { ChangeEvent } from "../types/cardTypes"; import styles from "../styles/CardFormInput.module.css"; @@ -10,34 +10,58 @@ interface CardFormProps { width: string; isDisable?: boolean; hasError: boolean; + handleBlur?: (e: React.FocusEvent) => void; + handleOnFocus?: (e: React.FocusEvent) => void; + maxLength: number; + pattern?: string; + handleOnInput?: (e: React.FormEvent) => void; + autoFocus?: boolean; } -function CardFormInput({ - name, - cardPlaceHolder, - cardInput, - handleChange, - width, - isDisable, - hasError, -}: CardFormProps) { - return ( - <> - - - ); -} +const CardFormInput = forwardRef( + ( + { + name, + cardPlaceHolder, + cardInput, + handleChange, + width, + isDisable, + hasError, + handleBlur, + handleOnFocus, + maxLength, + pattern, + handleOnInput, + autoFocus, + }: CardFormProps, + ref + ) => { + return ( + <> + + + ); + } +); export default CardFormInput; diff --git a/src/constants/usingNumbers.ts b/src/constants/cardConstants.ts similarity index 52% rename from src/constants/usingNumbers.ts rename to src/constants/cardConstants.ts index 67a5f3243b..382fd341bf 100644 --- a/src/constants/usingNumbers.ts +++ b/src/constants/cardConstants.ts @@ -11,4 +11,12 @@ export const CARD_PREFIX_LENGTH = { export const CARD_NUMBER_MAX_LENGTH = 4; -export const MASK_SYMBOL = "*"; +export const CARD_INFORMATION = { + EXPIRATION_DATE: ["month", "year"] as const, + CARD_NUMBER_BLOCK: [ + "firstBlock", + "secondBlock", + "thirdBlock", + "fourthBlock", + ] as const, +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index 3fb2c8442e..028ba410cd 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,3 @@ export * from "./errorMessege"; export * from "./textConstants"; -export * from "./usingNumbers"; +export * from "./cardConstants"; diff --git a/src/constants/textConstants.ts b/src/constants/textConstants.ts index d4aada3276..20b13d8556 100644 --- a/src/constants/textConstants.ts +++ b/src/constants/textConstants.ts @@ -1,3 +1,13 @@ +export const INPUTS = { + FIRST_BLOCK: "firstBlock", + SECOND_BLOCK: "secondBlock", + THIRD_BLOCK: "thirdBlock", + FOURTH_BLOCK: "fourthBlock", + MONTH: "month", + YEAR: "year", + OWNER: "owner", +} as const; + const CARD_FORM_LABELS = { CARD_NUMBER: "๊ฒฐ์ œํ•  ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”", CARD_NUMBER_CAPTION: "๋ณธ์ธ ๋ช…์˜์˜ ์นด๋“œ๋งŒ ๊ฒฐ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", @@ -19,4 +29,14 @@ const CARD_PLACEHOLDERS = { CARD_OWNER: "JOHN DOE", }; -export { CARD_FORM_LABELS, CARD_LABELS, CARD_PLACEHOLDERS }; +const MASK_SYMBOL = "โ—"; + +const EMPTY_STRING = ""; + +export { + MASK_SYMBOL, + CARD_FORM_LABELS, + CARD_LABELS, + CARD_PLACEHOLDERS, + EMPTY_STRING, +}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 8007aea4cb..d4dc726601 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import CardForm from "../components/CardForm"; import CardFormInput from "../components/CardFormInput"; import Card from "../components/Card"; @@ -6,92 +6,125 @@ import { CARD_FORM_LABELS, CARD_LABELS, CARD_PLACEHOLDERS, -} from "../constants/textConstants"; -import type { - ChangeEvent, - ExpirationDate, - CardNumbers, - CardError, - ExpirationDateError, - CardNumbersError, -} from "../types"; + EMPTY_STRING, + INPUTS, +} from "../constants"; +import type { ChangeEvent, CardData, CardFormError } from "../types"; import { formatStringToUpper, validOwnerName, - convertMonth, validExpirationDate, - filterNumber, - filterString, getFirstExpirationErrorMessage, getFirstErrorMessage, validCardNumbersBlock, + hideNumber, + onInputOnlyNumber, + onInputOnlyString, } from "../utils"; import styles from "../styles/CardForm.module.css"; function Home() { - const [expirationDate, setExpirationDate] = useState({ - month: "", - year: "", + const [expirationDate, setExpirationDate] = useState< + CardData["expirationDate"] + >({ + month: EMPTY_STRING, + year: EMPTY_STRING, }); - const [cardNumbers, setCardNumbers] = useState({ - firstBlock: "", - secondBlock: "", - thirdBlock: "", - fourthBlock: "", + const [cardNumbers, setCardNumbers] = useState({ + firstBlock: EMPTY_STRING, + secondBlock: EMPTY_STRING, + thirdBlock: EMPTY_STRING, + fourthBlock: EMPTY_STRING, }); - const [owner, setOwner] = useState(""); + const [owner, setOwner] = useState(EMPTY_STRING); - const [cardError, setCardError] = useState({ - cardNumbersError: { - firstBlock: { hasError: false, errorMessage: "", isDisable: false }, - secondBlock: { hasError: false, errorMessage: "", isDisable: true }, - thirdBlock: { hasError: false, errorMessage: "", isDisable: true }, - fourthBlock: { hasError: false, errorMessage: "", isDisable: true }, + const [cardError, setCardError] = useState({ + numbers: { + firstBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: false, + }, + secondBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: true, + }, + thirdBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: true, + }, + fourthBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: true, + }, }, - expirationDateError: { - month: { hasError: false, errorMessage: "" }, - year: { hasError: false, errorMessage: "" }, + expirationDate: { + month: { hasError: false, errorMessage: EMPTY_STRING }, + year: { hasError: false, errorMessage: EMPTY_STRING }, }, - ownerError: { hasError: false, errorMessage: "" }, + owner: { hasError: false, errorMessage: EMPTY_STRING }, }); + const [isAllFocusDone, setIsAllFocusDone] = useState(false); + const [isOnFocus, setIsOnFocus] = useState(false); + + const firstRef = useRef(null); + const secondRef = useRef(null); + const thirdRef = useRef(null); + const fourthRef = useRef(null); + const monthRef = useRef(null); + const yearRef = useRef(null); + const ownerRef = useRef(null); + const handleExpirationDateChange = (event: ChangeEvent) => { const name = event.target.name; const value = event.target.value; - const filterValue = filterNumber( - name === "month" ? convertMonth(value) : value, - 2 - ); - const newDate = { ...expirationDate, - [name]: filterValue, + [name]: value, }; + setExpirationDate((prev) => ({ + ...prev, + [name]: value, + })); + const validResult = validExpirationDate(newDate.month, newDate.year); setCardError((pre) => ({ ...pre, - expirationDateError: validResult, + expirationDate: validResult, })); + }; - setExpirationDate({ - month: newDate.month, - year: newDate.year, - }); + const handleExpirationDateBlur = ( + date: keyof CardData["expirationDate"], + e: React.FocusEvent + ) => { + const value = e.target.value; + + if (value.length === 1) { + setExpirationDate((prev) => ({ + ...prev, + [date]: `0${value}`, + })); + } }; const handleOwnerChange = (event: ChangeEvent) => { - const value = filterString(event.target.value, 20); + const value = event.target.value; const validResult = validOwnerName(event.target.value); setCardError((pre) => ({ ...pre, - ownerError: validResult, + owner: validResult, })); const ownerName = formatStringToUpper(value); setOwner(ownerName); @@ -103,14 +136,14 @@ function Home() { const numberBlock = { ...cardNumbers, - [name]: filterNumber(value, 4), + [name]: value, }; - const result: CardNumbersError = validCardNumbersBlock(numberBlock); + const result: CardFormError["numbers"] = validCardNumbersBlock(numberBlock); setCardError((pre) => ({ ...pre, - cardNumbersError: result, + numbers: result, })); setCardNumbers({ @@ -121,6 +154,46 @@ function Home() { }); }; + const currentInputRef = ( + key: string + ): React.RefObject | null => { + switch (key) { + case INPUTS.FIRST_BLOCK: + return firstRef; + case INPUTS.SECOND_BLOCK: + return secondRef; + case INPUTS.THIRD_BLOCK: + return thirdRef; + case INPUTS.FOURTH_BLOCK: + return fourthRef; + case INPUTS.MONTH: + return monthRef; + case INPUTS.YEAR: + return yearRef; + case INPUTS.OWNER: + return ownerRef; + default: + return null; + } + }; + + useEffect(() => { + if (isAllFocusDone) return; + if (cardNumbers.firstBlock.length === 4) secondRef.current?.focus(); + if (cardNumbers.secondBlock.length === 4) thirdRef.current?.focus(); + if (cardNumbers.thirdBlock.length === 4) fourthRef.current?.focus(); + if (cardNumbers.fourthBlock.length === 4) monthRef.current?.focus(); + }, [cardNumbers, isAllFocusDone]); + + useEffect(() => { + if (isAllFocusDone) return; + if (expirationDate.month.length === 2) yearRef.current?.focus(); + if (expirationDate.year.length === 2) { + ownerRef.current?.focus(); + setIsAllFocusDone(true); + } + }, [expirationDate, isAllFocusDone]); + return ( <> - {Object.entries(cardNumbers).map(([key, value]) => ( - - ))} - {cardError.cardNumbersError && ( -

- {getFirstErrorMessage(cardError.cardNumbersError)} + {Object.entries(cardNumbers).map(([key, value], index) => { + const isNeedHiding = + key === INPUTS.THIRD_BLOCK || key === INPUTS.FOURTH_BLOCK; + + return ( + setIsOnFocus(true)} + handleBlur={() => setIsOnFocus(false)} + autoFocus={index === 0} + /> + ); + })} + + { +

+ {cardError.numbers + ? getFirstErrorMessage(cardError.numbers) + : EMPTY_STRING}

- )} + } + {Object.entries(expirationDate).map(([key, value]) => ( + handleExpirationDateBlur( + key as keyof CardFormError["expirationDate"], + e + ) } /> ))} - {cardError.expirationDateError && ( -

- {getFirstExpirationErrorMessage(cardError.expirationDateError)} + + { +

+ {cardError.expirationDate + ? getFirstExpirationErrorMessage(cardError.expirationDate) + : EMPTY_STRING}

- )} + } - {cardError.ownerError && ( -

{cardError.ownerError.errorMessage}

- )} + + { +

+ {cardError.owner ? cardError.owner.errorMessage : EMPTY_STRING} +

+ } ); } diff --git a/src/styles/CardForm.module.css b/src/styles/CardForm.module.css index 0818c10d67..4f6ed28c87 100644 --- a/src/styles/CardForm.module.css +++ b/src/styles/CardForm.module.css @@ -17,5 +17,11 @@ .error { color: red; - font-size: 12px; + height: 0.1rem; + font-size: 0.75rem; +} + +.none { + height: 0.1rem; + font-size: 0.75rem; } diff --git a/src/types/cardTypes.ts b/src/types/cardTypes.ts index 170745b18b..0aebe7742b 100644 --- a/src/types/cardTypes.ts +++ b/src/types/cardTypes.ts @@ -1,11 +1,6 @@ -export type InputName = - | "firstBlock" - | "secondBlock" - | "thirdBlock" - | "fourthBlock" - | "month" - | "year" - | "owner"; +import { INPUTS } from "../constants"; + +export type InputName = (typeof INPUTS)[keyof typeof INPUTS]; export type ChangeEvent = React.ChangeEvent & { target: { @@ -21,7 +16,7 @@ export type CardData = { thirdBlock: string; fourthBlock: string; }; - expiration: { + expirationDate: { month: string; year: string; }; diff --git a/src/types/errorType.ts b/src/types/errorTypes.ts similarity index 92% rename from src/types/errorType.ts rename to src/types/errorTypes.ts index 089d926899..fe8aab8553 100644 --- a/src/types/errorType.ts +++ b/src/types/errorTypes.ts @@ -6,13 +6,13 @@ export interface CardNumberErrorType extends ErrorType { isDisable: boolean; } export type CardFormError = { - cardNumbers: { + numbers: { firstBlock: CardNumberErrorType; secondBlock: CardNumberErrorType; thirdBlock: CardNumberErrorType; fourthBlock: CardNumberErrorType; }; - expiration: { + expirationDate: { month: ErrorType; year: ErrorType; }; diff --git a/src/types/index.ts b/src/types/index.ts index d9674448f4..7d313ac1b1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,2 @@ export * from "./cardTypes"; -export * from "./errorType"; +export * from "./errorTypes"; diff --git a/src/utils/cardNumberValidators.tsx b/src/utils/cardNumberValidators.tsx index fb42e71cef..3560513699 100644 --- a/src/utils/cardNumberValidators.tsx +++ b/src/utils/cardNumberValidators.tsx @@ -1,14 +1,16 @@ -import { ERROR_MESSAGE } from "../constants"; -import { isOnlyNumber } from "./inputFilters"; -import visa from "../assets/Visa.svg"; -import master from "../assets/Mastercard.svg"; -import { CardNumberBlockError, CardNumbers, CardNumbersError } from "../types"; import { + ERROR_MESSAGE, CARD_LOGO_NUMBER, MASK_SYMBOL, CARD_PREFIX_LENGTH, CARD_NUMBER_MAX_LENGTH, + EMPTY_STRING, + CARD_INFORMATION, } from "../constants"; +import { isOnlyNumber } from "./validationUtils"; +import visa from "../assets/Visa.svg"; +import master from "../assets/Mastercard.svg"; +import { CardFormError, CardData, CardNumberErrorType } from "../types"; // Visa ๋˜๋Š” Master ๋กœ๊ณ  ์„ค์ • export const setCardLogo = (value: string): string => { @@ -20,15 +22,15 @@ export const setCardLogo = (value: string): string => { first <= CARD_LOGO_NUMBER.MASTER_MAX_NUMBER ) return master; - return ""; + return EMPTY_STRING; }; -// ์ˆซ์ž๋ฅผ '*' ์ฒ˜๋ฆฌ +// ์ˆซ์ž๋ฅผ 'โ—' ์ฒ˜๋ฆฌ export const hideNumber = (value: string): string => value.replace(/[0-9]/g, MASK_SYMBOL); // ์นด๋“œ ๋ฒˆํ˜ธ ๊ฒ€์ฆ -export const validCardNumbers = (block: string): CardNumberBlockError => { +export const validCardNumbers = (block: string): CardNumberErrorType => { if (!isOnlyNumber(block)) return { hasError: true, @@ -38,45 +40,76 @@ export const validCardNumbers = (block: string): CardNumberBlockError => { return { hasError: false, - errorMessage: "", + errorMessage: EMPTY_STRING, isDisable: false, }; }; +// ํ•ด๋‹น ๋ธ”๋ก์ด ์ˆซ์ž๋กœ ์ „๋ถ€ ์ฑ„์›Œ์กŒ๋Š”์ง€ ํ™•์ธ +const isBlockFilledWithNumbers = (numbers: string, length: number): boolean => { + return numbers.length === length; +}; + +// ์ด์ „ ๋ธ”๋ก์˜ ๊ฐ’์ด ์ˆซ์ž๋กœ ์ „๋ถ€ ์ฑ„์›Œ์กŒ๋Š”์ง€ ํ™•์ธ +const validBlockIsFull = ( + prevBlock: string, + currentBlock: string +): CardNumberErrorType => { + const isPrevFilled = isBlockFilledWithNumbers( + prevBlock, + CARD_NUMBER_MAX_LENGTH + ); + const isCurrentFilled = isBlockFilledWithNumbers( + currentBlock, + CARD_NUMBER_MAX_LENGTH + ); + + return { + hasError: !isPrevFilled || !isCurrentFilled, + errorMessage: + !isPrevFilled || !isCurrentFilled + ? ERROR_MESSAGE.REQUIRE_FOUR_DIGIT_NUMBER + : EMPTY_STRING, + isDisable: !isPrevFilled, + }; +}; + // ์นด๋“œ ๋ฒˆํ˜ธ ๋ธ”๋ก ๊ฒ€์ฆ export const validCardNumbersBlock = ( - cardNumbers: CardNumbers -): CardNumbersError => { - type CardNumbersKeys = keyof CardNumbers; - const keys: CardNumbersKeys[] = [ - "firstBlock", - "secondBlock", - "thirdBlock", - "fourthBlock", - ]; - const result: CardNumbersError = { - firstBlock: { hasError: false, errorMessage: "", isDisable: false }, - secondBlock: { hasError: false, errorMessage: "", isDisable: false }, - thirdBlock: { hasError: false, errorMessage: "", isDisable: false }, - fourthBlock: { hasError: false, errorMessage: "", isDisable: false }, + cardNumbers: CardData["numbers"] +): CardFormError["numbers"] => { + const result: CardFormError["numbers"] = { + firstBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: false, + }, + secondBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: false, + }, + thirdBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: false, + }, + fourthBlock: { + hasError: false, + errorMessage: EMPTY_STRING, + isDisable: false, + }, }; - keys.forEach((key, index) => { - if (index === 0) return; + const cardNumberKeys = CARD_INFORMATION.CARD_NUMBER_BLOCK; - const preValue = cardNumbers[keys[index - 1]]; - const currentValue = cardNumbers[keys[index]]; - const isPrevFilled = preValue.length === CARD_NUMBER_MAX_LENGTH; - const isCurrentFilled = currentValue.length === CARD_NUMBER_MAX_LENGTH; + cardNumberKeys.forEach((key, index) => { + if (index === 0) return; - result[key] = { - hasError: !isPrevFilled || !isCurrentFilled, - errorMessage: - !isPrevFilled || !isCurrentFilled - ? ERROR_MESSAGE.REQUIRE_FOUR_DIGIT_NUMBER - : "", - isDisable: !isPrevFilled, - }; + result[key] = validBlockIsFull( + cardNumbers[cardNumberKeys[index - 1]], + cardNumbers[cardNumberKeys[index]] + ); }); return result; diff --git a/src/utils/errorHelpers.tsx b/src/utils/errorHelpers.tsx index 4d85d0777a..004b40488d 100644 --- a/src/utils/errorHelpers.tsx +++ b/src/utils/errorHelpers.tsx @@ -1,17 +1,27 @@ -import type { ExpirationDateError, CardNumbersError } from "../types"; +import type { CardFormError } from "../types"; +import { EMPTY_STRING, CARD_INFORMATION } from "../constants"; // ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ์—์„œ ๊ฐ€์žฅ ์ฒซ๋ฒˆ์งธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ export const getFirstExpirationErrorMessage = ( - error: ExpirationDateError + error: CardFormError["expirationDate"] ): string => { - if (error.month.hasError) return error.month.errorMessage; - if (error.year.hasError) return error.year.errorMessage; - - return ""; + for (const field of CARD_INFORMATION.EXPIRATION_DATE) { + if (error[field].hasError) { + return error[field].errorMessage; + } + } + return EMPTY_STRING; }; // ์นด๋“œ๋ฒˆํ˜ธ ๊ฒ€์ฆ์—์„œ ๊ฐ€์žฅ ์ฒซ๋ฒˆ์งธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ -export const getFirstErrorMessage = (error: CardNumbersError): string => { - const errorBlock = Object.values(error).find((block) => block.hasError); - return errorBlock ? errorBlock.errorMessage : ""; +export const getFirstErrorMessage = ( + error: CardFormError["numbers"] +): string => { + for (const field of CARD_INFORMATION.CARD_NUMBER_BLOCK) { + if (error[field].hasError) { + return error[field].errorMessage; + } + } + + return EMPTY_STRING; }; diff --git a/src/utils/expirationDateValidators.tsx b/src/utils/expirationDateValidators.tsx index b4c94be801..eed9723891 100644 --- a/src/utils/expirationDateValidators.tsx +++ b/src/utils/expirationDateValidators.tsx @@ -1,6 +1,6 @@ -import { ERROR_MESSAGE } from "../constants"; -import { isOnlyNumber } from "./inputFilters"; -import type { ErrorType, ExpirationDateError } from "../types"; +import { ERROR_MESSAGE, EMPTY_STRING } from "../constants"; +import { isOnlyNumber } from "./validationUtils"; +import type { ErrorType, CardFormError } from "../types"; // ์˜ค๋Š˜ ๋‚ ์งœ, ์—ฐ๋„, ์›” const today: Date = new Date(); @@ -11,11 +11,9 @@ const currentMonth: number = today.getMonth() + 1; const convertYear = (year: number): number => Math.floor(currentYear / 100) * 100 + year; -// ๋‘์ž๋ฆฌ๋กœ ์›”์„ ๋ฐ˜ํ™˜ (ex. 3์›” ์ž…๋ ฅ ์‹œ 03์›”๋กœ ๋ณ€ํ™˜) -export const convertMonth = (month: string): string => { - const monthNumber = Number(month); - if (isNaN(monthNumber)) return ""; - return monthNumber < 10 ? `0${monthNumber}` : `${monthNumber}`; +// ์ˆซ์ž ํ•œ์ž๋ฆฌ ์ž…๋ ฅ ์‹œ ๋‘์ž๋ฆฌ ๋ฐ˜ํ™˜ +const convertTwoLength = (date: string): string => { + return date.length === 1 ? `0${date}` : date; }; // 2๊ธ€์ž๋ฅผ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ @@ -44,13 +42,13 @@ const validExpirationMonth = (inputMonth: string): ErrorType => { if (!isOnlyNumber(inputMonth)) return { hasError: true, errorMessage: ERROR_MESSAGE.ONLY_NUMBER }; - if (!isLengthTwo(convertMonth(inputMonth))) + if (!isLengthTwo(convertTwoLength(inputMonth))) return { hasError: true, errorMessage: ERROR_MESSAGE.MIN_LENGTH_TWO }; if (!isInValidMonthRange(Number(inputMonth))) return { hasError: true, errorMessage: ERROR_MESSAGE.VALID_MONTH_RANGE }; - return { hasError: false, errorMessage: "" }; + return { hasError: false, errorMessage: EMPTY_STRING }; }; // ์œ ํšจ๊ธฐ๊ฐ„์— ์ž…๋ ฅํ•œ '์—ฐ๋„(Year)' ๊ฒ€์ฆ @@ -58,17 +56,17 @@ const validExpirationYear = (inputYear: string): ErrorType => { if (!isOnlyNumber(inputYear)) return { hasError: true, errorMessage: ERROR_MESSAGE.ONLY_NUMBER }; - if (!isLengthTwo(convertMonth(inputYear))) + if (!isLengthTwo(convertTwoLength(inputYear))) return { hasError: true, errorMessage: ERROR_MESSAGE.MIN_LENGTH_TWO }; - return { hasError: false, errorMessage: "" }; + return { hasError: false, errorMessage: EMPTY_STRING }; }; // ์œ ํšจ๊ธฐ๊ฐ„ ๊ฒ€์ฆ ํ›„ ์—๋Ÿฌํƒ€์ž… ๋ฐ˜ํ™˜ export const validExpirationDate = ( inputMonth: string, inputYear: string -): ExpirationDateError => { +): CardFormError["expirationDate"] => { const monthError: ErrorType = validExpirationMonth(inputMonth); const yearError: ErrorType = validExpirationYear(inputYear); diff --git a/src/utils/index.ts b/src/utils/index.ts index 1c84796346..13251a7b58 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./ownerNameValidators"; export * from "./expirationDateValidators"; export * from "./cardNumberValidators"; export * from "./errorHelpers"; +export * from "./validationUtils"; diff --git a/src/utils/inputFilters.tsx b/src/utils/inputFilters.tsx index 3409721b9b..dea0fd899b 100644 --- a/src/utils/inputFilters.tsx +++ b/src/utils/inputFilters.tsx @@ -1,28 +1,16 @@ -// ์ˆซ์ž๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ -export const isOnlyNumber = (inputValue: string): boolean => { - return /^[0-9]+$/.test(inputValue); -}; +import { EMPTY_STRING } from "../constants"; -// ๊ธ€์ž์ˆ˜ ์ œํ•œ -const limitToDigits = (value: string, max: number) => { - return value.length <= 2 ? value : value.slice(0, max); -}; +// ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ +export const formatStringToUpper = (text: string): string => text.toUpperCase(); // ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ -const inputOnlyNumber = (value: string): string => value.replace(/[^0-9]/g, ""); - -// ์˜์–ด๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ -const inputOnlyEnglish = (value: string): string => - value.replace(/[^a-zA-Z\s]/g, ""); - -// ์ž…๋ ฅํ•œ max ์ˆ˜๋งŒํผ ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ -export const filterNumber = (value: string, max: number) => { - const result = limitToDigits(value, max); - return inputOnlyNumber(result); +export const onInputOnlyNumber = (event: React.FormEvent) => { + const input = event.currentTarget; + input.value = input.value.replace(/[^0-9]/g, EMPTY_STRING); }; -// ์ž…๋ ฅํ•œ max ์ˆ˜๋งŒํผ ์˜์–ด๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ -export const filterString = (value: string, max: number) => { - const result = limitToDigits(value, max); - return inputOnlyEnglish(result); +// ๋ฌธ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅ +export const onInputOnlyString = (event: React.FormEvent) => { + const input = event.currentTarget; + input.value = input.value.replace(/[^a-zA-Z]/g, EMPTY_STRING); }; diff --git a/src/utils/ownerNameValidators.tsx b/src/utils/ownerNameValidators.tsx index cb5a0fac18..db104d4cca 100644 --- a/src/utils/ownerNameValidators.tsx +++ b/src/utils/ownerNameValidators.tsx @@ -1,9 +1,6 @@ -import { ERROR_MESSAGE } from "../constants"; +import { ERROR_MESSAGE, EMPTY_STRING } from "../constants"; import type { ErrorType } from "../types"; -// ๋Œ€๋ฌธ์ž๋กœ ๋ณ€๊ฒฝ -export const formatStringToUpper = (text: string): string => text.toUpperCase(); - // ์˜๋ฌธ๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ const isEnglishOnly = (text: string): boolean => /^[A-Za-z\s]+$/.test(text); @@ -25,6 +22,6 @@ export const validOwnerName = (text: string): ErrorType => { return { hasError: false, - errorMessage: "", + errorMessage: EMPTY_STRING, }; }; diff --git a/src/utils/validationUtils.tsx b/src/utils/validationUtils.tsx new file mode 100644 index 0000000000..43d73eb30a --- /dev/null +++ b/src/utils/validationUtils.tsx @@ -0,0 +1,4 @@ +// ์ˆซ์ž๋งŒ ์ž…๋ ฅํ–ˆ๋Š”์ง€ ๊ฒ€์ฆ +export const isOnlyNumber = (inputValue: string): boolean => { + return /^[0-9]+$/.test(inputValue); +};