diff --git a/README.md b/README.md
index 8d917806e0..3c9e33e13c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,65 @@
# 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
+```
+
+## ๐ณ ๊ธฐ๋ฅ ์๊ตฌ ์ฌํญ
+
+- ์นด๋ ๋ฒํธ ์
๋ ฅ ๋ฐ ์๋ณ
+ - ์นด๋ ๋ฒํธ์ 3~4๋ฒ ๋ธ๋ญ์ ์จ๊น ์ฒ๋ฆฌํ๋ค.
+ - ์ซ์๋ฅผ ์
๋ ฅํ์ง ์์ผ๋ฉด ์ฌ๋ฐ๋ฅด๊ฒ ์
๋ ฅํ๋ผ๋ ํผ๋๋ฐฑ์ ๋ณด์ฌ์ฃผ๊ณ , ์
๋ ฅ์ ์ ํํ๋ค.
+ - ๊ฐ ์นด๋ ๋ฒํธ ์
๋ ฅ ๋ธ๋ญ์ 0~9์ ์ซ์ 4์๋ฆฌ๋ก ์ด๋ฃจ์ด์ ธ์๋ค.
+ - ์
๋ ฅ์ ์ซ์๋ง ๊ฐ๋ฅํ๋ฉฐ, ์ ํจํ์ง ์์ ๋ฒํธ ์
๋ ฅ ์ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
+ - ์นด๋ ๋ฒํธ 4์๋ฆฌ ๋ฏธ๋ง์ผ๋ก ์
๋ ฅํ๋ ์ค์ ํฌ์ปค์ค๋ฅผ ๋๊ธฐ๋ ค๊ณ ํ๋ค๋ฉด ์๋ฌ๋ก ๋ง๋๋ค.
+- ์นด๋ ์ ํจ๊ธฐ๊ฐ ์
๋ ฅ
+ - ์
๋ ฅ์ ์ซ์๋ง ๊ฐ๋ฅํ๋ฉฐ ์ซ์๊ฐ ์๋์ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
+ - ์ ํจํ์ง ์์ ์์ ์
๋ ฅ ์(ex 13์) ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
+ - ํ์ฌ๋ณด๋ค ์ด์ ๋ ์ง๋ฅผ ์
๋ ฅ ์ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
+ - ํ ์๋ฆฌ ์ซ์๋ฅผ ์
๋ ฅ ์ ์๋์ผ๋ก ํ์์ ๋ง์ถฐ 0์ ๋ฃ์ด์ค๋ค.
+- ์นด๋ ์์ ์ ์ด๋ฆ ์
๋ ฅ
+ - ์๋ฌธ์๋ก ์
๋ ฅ ์ ๊ฐ์ ๋ก ๋๋ฌธ์๋ก ๋ณํํ๋ค.
+ - ์์ด๊ฐ ์๋ ๋ฌธ์ ์
๋ ฅ ์ ์
๋ ฅ์ ์ ํํ๊ณ ํผ๋๋ฐฑ์ ์ ๊ณตํ๋ค.
+ - (์ถ๊ฐ) ์ฌ์ฉ์ ์ด๋ฆ์ ์ต์ ๋๊ธ์ ์ด์ ์
๋ ฅํด์ผ ํ๋ค.
+- ์ค์๊ฐ ํ๋ฆฌ๋ทฐ ์
๋ฐ์ดํธ
+ - ์นด๋ ๋ฒํธ๊ฐ 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..4d46e40edd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,11 +1,7 @@
-import "./App.css";
+import Home from "../src/pages/Home";
function App() {
- return (
- <>
-
React Payments
- >
- );
+ return ;
}
export default App;
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.tsx b/src/components/Card.tsx
new file mode 100644
index 0000000000..8aca249949
--- /dev/null
+++ b/src/components/Card.tsx
@@ -0,0 +1,40 @@
+import styles from "../styles/Card.module.css";
+import type { CardNumbers, ExpirationDate } from "../types/cardTypes";
+import { hideNumber, setCardLogo } from "../utils/cardNumberValidators";
+
+interface 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.tsx b/src/components/CardForm.tsx
new file mode 100644
index 0000000000..39ad5d1477
--- /dev/null
+++ b/src/components/CardForm.tsx
@@ -0,0 +1,25 @@
+import styles from "../styles/CardForm.module.css";
+
+interface 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.tsx b/src/components/CardFormInput.tsx
new file mode 100644
index 0000000000..705321cd15
--- /dev/null
+++ b/src/components/CardFormInput.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import type { ChangeEvent } from "../types/cardTypes";
+import styles from "../styles/CardFormInput.module.css";
+
+interface CardFormProps {
+ name: string;
+ cardPlaceHolder: string;
+ cardInput: number | string;
+ handleChange: (e: ChangeEvent) => void;
+ width: string;
+ isDisable?: boolean;
+ hasError: boolean;
+}
+
+function CardFormInput({
+ name,
+ cardPlaceHolder,
+ cardInput,
+ handleChange,
+ width,
+ isDisable,
+ hasError,
+}: CardFormProps) {
+ return (
+ <>
+
+ >
+ );
+}
+
+export default CardFormInput;
diff --git a/src/constants/errorMessege.ts b/src/constants/errorMessege.ts
new file mode 100644
index 0000000000..7bce166174
--- /dev/null
+++ b/src/constants/errorMessege.ts
@@ -0,0 +1,12 @@
+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: "์ซ์ ๋ ์๋ฆฌ๋ฅผ ์
๋ ฅํ์ธ์.",
+ REQUIRE_FOUR_DIGIT_NUMBER: "4์๋ฆฌ ์ซ์๋ฅผ ์
๋ ฅํ์ธ์.",
+};
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000000..3fb2c8442e
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,3 @@
+export * from "./errorMessege";
+export * from "./textConstants";
+export * from "./usingNumbers";
diff --git a/src/constants/textConstants.ts b/src/constants/textConstants.ts
new file mode 100644
index 0000000000..d4aada3276
--- /dev/null
+++ b/src/constants/textConstants.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/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/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/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;
diff --git a/src/styles/Card.module.css b/src/styles/Card.module.css
new file mode 100644
index 0000000000..d75375ff5e
--- /dev/null
+++ b/src/styles/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/styles/CardForm.module.css b/src/styles/CardForm.module.css
new file mode 100644
index 0000000000..0818c10d67
--- /dev/null
+++ b/src/styles/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/styles/CardFormInput.module.css b/src/styles/CardFormInput.module.css
new file mode 100644
index 0000000000..290b5fea45
--- /dev/null
+++ b/src/styles/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/types/cardTypes.ts b/src/types/cardTypes.ts
new file mode 100644
index 0000000000..51113ac7ef
--- /dev/null
+++ b/src/types/cardTypes.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/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/cardNumberValidators.tsx b/src/utils/cardNumberValidators.tsx
new file mode 100644
index 0000000000..fb42e71cef
--- /dev/null
+++ b/src/utils/cardNumberValidators.tsx
@@ -0,0 +1,83 @@
+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 {
+ 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..b4c94be801
--- /dev/null
+++ b/src/utils/expirationDateValidators.tsx
@@ -0,0 +1,112 @@
+import { ERROR_MESSAGE } from "../constants";
+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/inputFilters.tsx b/src/utils/inputFilters.tsx
new file mode 100644
index 0000000000..3409721b9b
--- /dev/null
+++ b/src/utils/inputFilters.tsx
@@ -0,0 +1,28 @@
+// ์ซ์๋ง ์
๋ ฅํ๋์ง ๊ฒ์ฆ
+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, "");
+
+// ์์ด๋ง ์
๋ ฅ ๊ฐ๋ฅ
+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..cb5a0fac18
--- /dev/null
+++ b/src/utils/ownerNameValidators.tsx
@@ -0,0 +1,30 @@
+import { ERROR_MESSAGE } 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);
+
+// ์ต์ ๋๊ธ์ ์ด์์ธ์ง ๊ฒ์ฆ
+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: "",
+ };
+};