Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 로고를 카드 프리뷰에 업데이트한다.
- 사용자가 카드 번호 입력 시 실시간으로 카드 프리뷰에 업데이트한다.
- 사용자가 카드 유효기간 입력 시 실시간으로 카드 프리뷰에 업데이트한다.
- 사용자가 카드 소유자 이름 입력 시 실시간으로 카드 프리뷰에 업데이트한다.
Empty file removed src/App.css
Empty file.
8 changes: 2 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import "./App.css";
import Home from "../src/pages/Home";

function App() {
return (
<>
<h1>React Payments</h1>
</>
);
return <Home />;
}

export default App;
6 changes: 6 additions & 0 deletions src/assets/Mastercard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/Visa.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import styles from "../styles/Card.module.css";
import type { CardData } from "../types/cardTypes";
import { hideNumber, setCardLogo } from "../utils/cardNumberValidators";
import { EMPTY_STRING } from "../constants";

interface CardProps {
cardNumbers: CardData["numbers"];
expirationDate: CardData["expirationDate"];
owner: CardData["owner"];
}

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}`
: EMPTY_STRING;
const numbers: string = `${cardNumbers.firstBlock} ${cardNumbers.secondBlock} ${hideNumber(cardNumbers.thirdBlock)} ${hideNumber(cardNumbers.fourthBlock)}`;

return (
<>
<div className={styles.card}>
<div className={styles["card-header"]}>
<div className={styles["card-ic"]}></div>
{cardLogo ? (
<img className={styles.logo} src={cardLogo} />
) : (
<div className={styles.logo}></div>
)}
</div>
<p className={styles["card-detail"]}>{numbers}</p>
<p className={styles["card-detail"]}>{date}</p>
<p className={styles["card-detail"]}>{owner}</p>
</div>
</>
);
}

export default Card;
25 changes: 25 additions & 0 deletions src/components/CardForm.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h3 className={styles.label}>{cardFormLabelText}</h3>
{cardFormLabelCaption && (
<p className={styles.caption}>{cardFormLabelCaption}</p>
)}
<p className={styles["form-label"]}>{cardLabelText}</p>
</>
);
}

export default CardForm;
67 changes: 67 additions & 0 deletions src/components/CardFormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { forwardRef } 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;
handleBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
handleOnFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
maxLength: number;
pattern?: string;
handleOnInput?: (e: React.FormEvent<HTMLInputElement>) => void;
autoFocus?: boolean;
}

const CardFormInput = forwardRef<HTMLInputElement, CardFormProps>(
(
{
name,
cardPlaceHolder,
cardInput,
handleChange,
width,
isDisable,
hasError,
handleBlur,
handleOnFocus,
maxLength,
pattern,
handleOnInput,
autoFocus,
}: CardFormProps,
ref
) => {
return (
<>
<input
className={styles.inputBox}
name={name}
value={cardInput}
placeholder={cardPlaceHolder}
type="text"
onChange={handleChange}
ref={ref}
style={{
width: width,
borderColor: hasError ? "red" : "#d5d5d5",
}}
disabled={isDisable}
onBlur={handleBlur}
maxLength={maxLength}
pattern={pattern}
onInput={handleOnInput}
onFocus={handleOnFocus}
autoFocus={autoFocus}
/>
</>
);
}
);

export default CardFormInput;
22 changes: 22 additions & 0 deletions src/constants/cardConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 CARD_INFORMATION = {
EXPIRATION_DATE: ["month", "year"] as const,
CARD_NUMBER_BLOCK: [
"firstBlock",
"secondBlock",
"thirdBlock",
"fourthBlock",
] as const,
};
12 changes: 12 additions & 0 deletions src/constants/errorMessege.ts
Original file line number Diff line number Diff line change
@@ -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자리 숫자를 입력하세요.",
};
3 changes: 3 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./errorMessege";
export * from "./textConstants";
export * from "./cardConstants";
42 changes: 42 additions & 0 deletions src/constants/textConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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: "본인 명의의 카드만 결제 가능합니다.",
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",
};

const MASK_SYMBOL = "●";

const EMPTY_STRING = "";

export {
MASK_SYMBOL,
CARD_FORM_LABELS,
CARD_LABELS,
CARD_PLACEHOLDERS,
EMPTY_STRING,
};
14 changes: 7 additions & 7 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
)
</React.StrictMode>
);
Loading