Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
89dffb6
feat: 페어 프로그래밍 초기 세팅
JeLee-river Apr 15, 2025
b584488
docs: 미션의 기능목록, 설계 구조 등 작성
JeLee-river Apr 15, 2025
54cd47d
style: 전역 스타일링 적용
JeLee-river Apr 15, 2025
c5aeb04
feat: Input 컴포넌트 생성
JeLee-river Apr 15, 2025
6c97deb
feat: InputForm 컴포넌트 생성
JeLee-river Apr 15, 2025
d21b7c2
feat: 카드 번호 입력 컴포넌트 작성
JeLee-river Apr 15, 2025
ab65cb5
feat: 카드 유효기간 입력 컴포넌트 작성
JeLee-river Apr 15, 2025
6736ff9
feat: 카드 CVC 입력 컴포넌트 작성
JeLee-river Apr 15, 2025
2f38071
feat: 카드 입력 form 컴포넌트 작성
JeLee-river Apr 15, 2025
9f3ca2d
refactor: InputForm이 Input을 children으로 받도록 리팩토링
JeLee-river Apr 15, 2025
590cea9
style: 전역 변수로 관리하는 색상 추가
JeLee-river Apr 15, 2025
6a939ca
feat: 카드 브랜드 로고 이미지 생성
JeLee-river Apr 15, 2025
dd0545e
style: 카드 입력 form 컴포넌트 스타일 적용
JeLee-river Apr 15, 2025
99221c1
style: 기본 스타일을 초기화하는 파일 수정
JeLee-river Apr 15, 2025
4c78448
style: 카드 입력 컴포넌트의 스타일 수정
JeLee-river Apr 15, 2025
8b02e89
feat: 카드 정보를 입력하는 UI를 렌더링하는 함수 생성
JeLee-river Apr 15, 2025
a0369d1
style: 카드 정보를 입력하는 UI에 스타일 추가
JeLee-river Apr 15, 2025
d6e5927
style: 카드 입력 form의 스타일 조정
JeLee-river Apr 16, 2025
65d9928
feat: 사용자가 입력한 카드 정보를 프리뷰에 렌더링하는 기능 구현
JeLee-river Apr 16, 2025
1d09069
feat: 카드 번호와 유효기간 정보를 상태관리 하도록 코드 추가
JeLee-river Apr 16, 2025
33bb1e8
refactor: 사용자 카드 정보 등록 페이지가 렌더링되도록 app 수정
JeLee-river Apr 16, 2025
b75a883
feat: 사용자 입력값의 예외 처리 로직 생성
JeLee-river Apr 17, 2025
461a369
feat: 사용자 입력값에 대한 피드백 메시지 렌더링 로직 추가
JeLee-river Apr 17, 2025
b3c73c8
refactor: 유효기간 검증 로직 수정
JeLee-river Apr 17, 2025
b339891
feat: 카드 유효기간 검증 로직 추가
JeLee-river Apr 17, 2025
a5b3347
feat: 카드 CVC 검증 로직 추가
JeLee-river Apr 17, 2025
5acd51b
feat: 카드 번호 검증 로직 추가
JeLee-river Apr 17, 2025
34e372b
docs: 구현한 기능 목록 체크
JeLee-river Apr 17, 2025
391c1b9
refactor: 상태 변경 함수의 타입 추가
JeLee-river Apr 17, 2025
514e201
style: storybook에 css 적용
JeLee-river Apr 17, 2025
13ff1bd
refactor: 불필요한 props 제거
JeLee-river Apr 17, 2025
464572b
test: storybook 비주얼 테스트 추가
JeLee-river Apr 17, 2025
f92ac28
fix: 웹 배포 환경에 따라 이미지 경로 수정
dev-dino22 Apr 17, 2025
a208999
refactor: 불필요한 fragment 제거
dev-dino22 Apr 21, 2025
5f94959
refactor: 카드 정보 상수 분리
dev-dino22 Apr 21, 2025
d34ae3d
refactor: checkBrand 조건문 startsWith()로 가독성 개선
dev-dino22 Apr 21, 2025
c1bf6f8
refactor: 다시 유효한 값 입력 시 오류 스타일 삭제
dev-dino22 Apr 21, 2025
5ed8cc4
refactor: 유효기간 month, year로 나누고 전용 validator 분리
dev-dino22 Apr 21, 2025
b61f061
refactor: 전역 상태 대신 ref로 유효기간 검증 관리
dev-dino22 Apr 21, 2025
d476f0d
refactor: 카드 유효기간입력 컴포넌트의 inputs를 map 함수 렌더링으로 변경
dev-dino22 Apr 21, 2025
736fd2a
refactor: CardExpirationDateInput 의 onChangeHandler 함수/상수 분리 및 추상화
dev-dino22 Apr 21, 2025
eee598a
refactor: 카드프리뷰의 let 을 사용한 함수 내 상태관리에서 useMemo()훅 활용
dev-dino22 Apr 21, 2025
3d12de1
refactor: Input 컴포넌트에서 이제 불필요한 forwardRef 제거 및 props 전개 방식으로 변경
dev-dino22 Apr 21, 2025
92ee2c2
chore: Input컴포넌트 폴명 파스칼케이스 수정을 위한 중간임시 폴더명
dev-dino22 Apr 22, 2025
f177789
feat: input 폴더명 카멜케이스 수정
dev-dino22 Apr 22, 2025
7f491da
refactor: validateCardInput.ts 의 숫자문자열 검사 메서드명 수정
dev-dino22 Apr 22, 2025
6a493e1
refactor: validateCardInput.ts의 반환값 에러결과불리언객체로 통일
dev-dino22 Apr 22, 2025
337f2d1
refactor: Input 컴포넌트는 isValid 프롭을 전달받고 각 유효 상태를 사용처에서 제어하도록 변경 및 에러메세…
dev-dino22 Apr 22, 2025
ce26f82
chore: 불필요한 import 문 제거
dev-dino22 Apr 22, 2025
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
12 changes: 12 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link href="../src/index.css" rel="stylesheet" />

<title>Document</title>
</head>
<body></body>
</html>
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,124 @@
# react-payments

# 기능 목록

## 사용자 입력

- [x] 사용자는 카드 번호를 입력할 수 있다.
- [x] 16자리의 입력 번호를 실시간 감지하여 알맞는 브랜드 로고를 UI에 표시한다. (Visa: 4로 시작 / MasterCard: 51~55로 시작 / 그외 로고 렌더링 x )
- [x] 예외: 숫자만 가능 / 16자리 / 한칸에 네 자리 -> 피드백: 빨간 border & 포커스 & 밑에 피드백 문구 출력
- [x] 사용자는 카드 유효기간을 입력할 수 있다.
- [x] 예외: 숫자만 가능 / 2자리 / 월은 1~12 까지 연도 25~99까지 / 오늘 날짜를 기준으로 유효한 기간인지 검증 -> 피드백: 빨간 border & 포커스 & 밑에 피드백 문구 출력
- [x] 사용자는 CVC 번호를 입력할 수 있다.
- [x] 예외: 숫자만 가능 / 3자리 -> 피드백: 빨간 border & 포커스 & 밑에 피드백 문구 출력

## UI 업데이트

- [x] 사용자 입력에 따라 동시에 프리뷰를 업데이트한다.
- [x] 입력할 때마다 한자리 단위로 실시간 업데이트
- [x] 카드번호에 유효한 로고 실시간 업데이트
- [x] 카드번호 뒷 8자리는 마스킹 처리

# 테스트 목록 (Storybook)

## 결제 입력 테스트 파일

- ### Describe

- 사용자가 카드 정보를 입력 시

- ### Context

- 사용자가 카드 번호를 입력할 경우
- ### It

- 사용자가 16자리를 다 채우지 않은 채 카드번호 인풋 포커스를 이탈하면 "유효하지 않은 카드번호입니다. 16자리를 입력해주세요." 피드백을 출력한다.
- 한 칸에 4자리를 다 채우지 않은 채 다음 포커스 이동하면 "유효하지 않은 번호입니다. 4자리를 입력해주세요." 피드백을 출력한다.
- 사용자가 숫자 이외를 입력하려고 하면 "유효하지 않은 입력입니다. 숫자만 입력해주세요." 피드백을 출력한다.

- ### Context
- 사용자가 카드 유효 기간을 입력할 경우
- ### It

- 사용자가 4자리를 다 채우지 않은 채 유효기간 인풋 포커스를 이탈하면 "유효하지 않은 카드번호입니다. 16자리를 입력해주세요." 피드백을 출력한다.
- 한 칸에 2자리를 다 채우지 않은 채 다음 포커스 이동하면 "유효하지 않은 번호입니다. 2자리를 입력해주세요." 피드백을 출력한다.
- 사용자가 숫자 이외를 입력하려고 하면 "유효하지 않은 입력입니다. 숫자만 입력해주세요." 피드백을 출력한다.
- 사용자가 현재 날짜 이전 기간을 입력할 경우 "유효하지 않은 카드입니다. 유효 기간을 확인해주세요." 피드백을 출력한다.

- ### Context
- 사용자가 CVC를 입력할 경우
- ### It
- 사용자가 3자리를 다 채우지 않은 채 CVC 인풋 포커스를 이탈하면 "유효하지 않은 CVC입니다. 3자리를 입력해주세요." 피드백을 출력한다.
- 사용자가 숫자 이외를 입력하려고 하면 "유효하지 않은 입력입니다. 숫자만 입력해주세요." 피드백을 출력한다.

## 카드 프리뷰 UI 테스트 파일

- ### Describe
- 사용자가 카드 정보를 입력 시
- ### Context
- 사용자가 카드 번호를 입력할 경우
- ### It
- 카드 번호 첫 자리가 4로 시작하면 VISA 로고가 렌더링된다.
- 카드 번호 첫 자리가 51~55 로 시작하면 MasterCard 로고가 렌더링된다.
- 사용자가 카드번호를 입력할 시 실시간으로 카드 번호가 업데이트된다.
- 카드번호 뒷 8자리는 마스킹 처리하여 표시한다.
- ### Context
- 사용자가 유효 기간을 입력할 경우
- ### It
- 사용자가 유효 기간을 입력할 시 실시간으로 카드 번호가 업데이트된다.

# 설계 구조

## 디렉터리 구조

src/utils
src/constants
src/styles
src/components

src/components/abc/constants
src/components/abc/styles

Dom 트리구조와 비슷한 디렉터리 설계
src/components/paymentsLayout/paymentInput/
src/components/common

## 컴포넌트 분리

```js
src/
├── components/
│ ├── common/
│ │ ├── input.ts # validator 콜백을 받아서 input 하나를 검증
│ │ └── inputForm.ts # input 개수만큼 input 생성 + validator 전달
│ │
│ ├── paymentsInputPage/
│ │ ├── paymentsInputPage.ts # 카드 정보 입력 페이지의 메인 컴포넌트
│ │ │
│ │ ├── cardInputForm/ # 카드 입력 폼 영역
│ │ │ ├── cardInputForm.ts # 카드 번호, 유효기간, CVC를 포함하는 폼 컴포넌트
│ │ │ ├── cardNumberInput/
│ │ │ │ └── cardNumberInput.ts # 카드 번호 입력 필드
│ │ │ ├── cardExpirationDateInput/
│ │ │ │ └── cardExpirationDateInput.ts # 유효기간 입력 필드
│ │ │ └── cardCVCInput/
│ │ │ └── cardCVCInput.ts # CVC 입력 필드
│ │ │
│ │ └── cardPreview/ # 카드 미리보기 영역
│ │ └── cardPreview.ts # 카드 정보 시각화 (title, description, label, inputs)
```

## 상태 관리

paymentsInputPage는 cardInputForm의 상태를 cardPreview에 반영

- cardInputForm 은 paymentsInputPage의 setState 콜백을 받아 자식들(card~input 컴포넌트들) 상태를 변경

- paymentsInputPage는 state를 cardPreview에 전달
- cardPreview는 state에 따라 실시간 변경

# 컨벤션

## CSS 작성 방식

- Module CSS
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite dev --host 0.0.0.0",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
Expand Down
Binary file added public/Mastercard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/Visa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import "./App.css";

import './App.css';
import PaymentInputPage from './components/paymentInputPage/PaymentInputPage';
function App() {
return (
<>
<h1>React Payments</h1>
<PaymentInputPage />
</>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/color.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:root {
--grey: #acacac;
--black: #000000;
--white: #ffffff;
--red: #ff3d3d;
--gold: #ddcd78;
}
17 changes: 17 additions & 0 deletions src/components/common/InputForm/Input/Input.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.input {
width: 100%;
border: 1px solid var(--grey);
border-radius: 4px;
color: var(--black);
padding: 12px 8px;
&::placeholder {
color: var(--grey);
}

&:focus {
border: 1px solid var(--black);
}
&.isNotValid {
border: 1px solid var(--red);
}
}
34 changes: 34 additions & 0 deletions src/components/common/InputForm/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ComponentProps, Dispatch, SetStateAction, useState } from 'react';
import styles from './Input.module.css';

export interface InputProps extends Omit<ComponentProps<'input'>, 'onChange'> {
onChange: (
event: React.ChangeEvent<HTMLInputElement>,
setState: Dispatch<SetStateAction<boolean>>
) => void;
}

function Input({
type,
onChange,
name,
id,
placeholder,
maxLength,
}: InputProps) {
const [isValid, setIsValid] = useState<boolean>(true);

return (
<input
type={type}
name={name}
id={id}
placeholder={placeholder}
maxLength={maxLength}
onChange={(e) => onChange(e, setIsValid)}
className={`${styles.input} ${!isValid && styles.isNotValid} tx-md`}
/>
);
}

export default Input;
43 changes: 43 additions & 0 deletions src/components/common/InputForm/InputForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.container {
display: flex;
flex-direction: column;
gap: 16px;
}

.inputForm {
display: flex;
justify-content: space-between;
align-items: center;
}

.inputContainer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}

.inputBox {
display: flex;
flex-direction: column;
gap: 8px;
}

label {
color: var(--black);
}

.titleBox {
display: flex;
flex-direction: column;
gap: 8px;

& p {
color: var(--grey);
}
}

.feedbackMessage {
color: var(--red);
height: 18px;
}
38 changes: 38 additions & 0 deletions src/components/common/InputForm/InputForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import styles from './InputForm.module.css';

export interface InputFormProps {
title: string;
label: string;
description?: string;
feedbackMessage?: string;
children: React.ReactNode;
}

function InputForm(props: InputFormProps) {
const { title, label, description, feedbackMessage } = props;

return (
<div className={styles.container}>
<div className={styles.titleBox}>
<h3 className='tx-xl'>{title}</h3>
{description && <p className='tx-md'>{description}</p>}
</div>
<div className={styles.inputBox}>
<label className='tx-lg'>{label}</label>
<div className={styles.inputContainer}>{props.children}</div>
<p
style={
feedbackMessage
? { visibility: 'visible' }
: { visibility: 'hidden' }
}
className={`${styles.feedbackMessage}`}
>
{feedbackMessage}
</p>
</div>
</div>
);
}

export default InputForm;
16 changes: 16 additions & 0 deletions src/components/paymentInputPage/PaymentInputPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.section {
display: flex;
width: 100%;
justify-content: center;
}
.container {
width: 390px;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid var(--grey);
border-radius: 4px;
padding: 28px;
gap: 40px;
}
26 changes: 26 additions & 0 deletions src/components/paymentInputPage/PaymentInputPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import CardPreview from './cardPreview/CardPreview';
import CardInputForm from './cardInputForm/CardInputForm';
import styles from './PaymentInputPage.module.css';
import { useState } from 'react';

function PaymentInputPage() {
const [cardNumbers, setCardNumbers] = useState<string[]>([]);
const [expirationDate, setExpirationDate] = useState<string[]>([]);

return (
<section className={styles.section}>
<div className={styles.container}>
<CardPreview
cardNumbers={cardNumbers}
expirationDate={expirationDate}
/>
<CardInputForm
setCardNumbers={setCardNumbers}
setExpirationDate={setExpirationDate}
/>
</div>
</section>
);
}

export default PaymentInputPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.cardInputForm {
display: flex;
flex-direction: column;
gap: 26px;
}
24 changes: 24 additions & 0 deletions src/components/paymentInputPage/cardInputForm/CardInputForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Dispatch, SetStateAction } from 'react';
import CardCVCInput from './cardInput/CardCVCInput';
import CardExpirationDateInput from './cardInput/CardExpirationDateInput';
import CardNumberInput from './cardInput/CardNumberInput';
import styles from './cardInputForm.module.css';

interface CardInputFormProps {
setCardNumbers: Dispatch<SetStateAction<string[]>>;
setExpirationDate: Dispatch<SetStateAction<string[]>>;
}

function CardInputForm({
setCardNumbers,
setExpirationDate,
}: CardInputFormProps) {
return (
<div className={styles.cardInputForm}>
<CardNumberInput setCardNumbers={setCardNumbers} />
<CardExpirationDateInput setExpirationDate={setExpirationDate} />
<CardCVCInput />
</div>
);
}
export default CardInputForm;
Loading