Skip to content
Open
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
Comment on lines +5 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

폴더 구조를 굉장히 깔끔하게 잘 짜주셨네요 👍

```

## 💳 기능 요구 사항

- 카드 번호 입력 및 식별
- 카드 번호의 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></Home>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소한 내용이지만, 안에 children을 받지 않는 컴포넌트의 경우 아래처럼 명확히 단일 태그라는 것을 보여주면 좋아요.

Suggested change
return <Home></Home>;
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.
40 changes: 40 additions & 0 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<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;
43 changes: 43 additions & 0 deletions src/components/CardFormInput.tsx

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한번 input의 조건을 만족하면 다음 input으로 자동으로 포커스가 이동하게 설계해 보는 건 어떨까요?
(카드 번호 첫 네자리를 입력하면 다음 칸으로 focus, 카드 번호를 모두 입력하면 month로 focus....)

Original file line number Diff line number Diff line change
@@ -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 (
<>
<input
className={styles.inputBox}
name={name}
value={cardInput}
placeholder={cardPlaceHolder}
type="text"
onChange={handleChange}
style={{
width: width,
borderColor: hasError ? "red" : "#d5d5d5",
}}
disabled={isDisable}
></input>
</>
);
}

export default CardFormInput;
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 "./usingNumbers";
22 changes: 22 additions & 0 deletions src/constants/textConstants.ts
Original file line number Diff line number Diff line change
@@ -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 };
14 changes: 14 additions & 0 deletions src/constants/usingNumbers.ts
Original file line number Diff line number Diff line change
@@ -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 = "*";
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