-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 페이먼츠 구현 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
greetings1012
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제 로컬 환경에서는 npm run dev를 실행하면 import문 에러가 발생하면서 preview가 정상적으로 동작하지 않네요! 이 부분 고쳐주시면 다시 확인해보겠습니다 😭
|
@greetings1012 import 오류 수정했습니다 알려주셔서 감사해요 😊 |
greetings1012
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다!
전체적으로 폴더 구조 설계와 기능 분리에 대해 생각해서 설계한 노력이 보였어요!
몇 가지 생각할 거리를 남겨 두었으니, 시간 날 때 한번 확인해주세요 👍
| 📦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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
폴더 구조를 굉장히 깔끔하게 잘 짜주셨네요 👍
| <h1>React Payments</h1> | ||
| </> | ||
| ); | ||
| return <Home></Home>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사소한 내용이지만, 안에 children을 받지 않는 컴포넌트의 경우 아래처럼 명확히 단일 태그라는 것을 보여주면 좋아요.
| return <Home></Home>; | |
| return <Home />; |
| @@ -0,0 +1,13 @@ | |||
| export type ChangeEvent = React.ChangeEvent<HTMLInputElement>; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 타입 선언은 단순히 길게 써야 하는 선언을 줄이기 위해 만든 걸까요?
조금 더 명시적으로 타입을 선언해서
잘못된 코드를 작성하는 걸 컴파일 수준에서 방지할 수 있을 것 같아요!
| export type ChangeEvent = React.ChangeEvent<HTMLInputElement>; | |
| export type InputName = | |
| | "firstBlock" | |
| | "secondBlock" | |
| | "thirdBlock" | |
| | "fourthBlock" | |
| | "month" | |
| | "year" | |
| | "owner"; | |
| export type ChangeInputEvent = ChangeEvent<HTMLInputElement> & { | |
| target: { | |
| name: InputName; | |
| value: string; | |
| }; | |
| }; | |
| export type CardData = { | |
| numbers: { | |
| firstBlock: string; | |
| secondBlock: string; | |
| thirdBlock: string; | |
| fourthBlock: string; | |
| }; | |
| expiration: { | |
| month: string; | |
| year: string; | |
| }; | |
| owner: string; | |
| }; |
혹은... 각 필드 이름을 두 번 사용하기 싫으면 아래와 같은 방법도 있어요.
| export type ChangeEvent = React.ChangeEvent<HTMLInputElement>; | |
| export type CardData = { | |
| numbers: { | |
| firstBlock: string; | |
| secondBlock: string; | |
| thirdBlock: string; | |
| fourthBlock: string; | |
| }; | |
| expiration: { | |
| month: string; | |
| year: string; | |
| }; | |
| owner: string; | |
| }; | |
| type FlattenKeys<T> = T extends object | |
| ? { [K in keyof T]: K | FlattenKeys<T[K]> }[keyof T] | |
| : never; | |
| export type InputName = FlattenKeys<CardData>; | |
| export type ChangeInputEvent = ChangeEvent<HTMLInputElement> & { | |
| target: { | |
| name: InputName; | |
| value: string; | |
| }; | |
| }; |
제네릭을 활용한 재귀 타입 지정 방식인데, 이건 아직 생소한 개념일 수 있으니
'이런 방법도 있구나~' 라고 생각하고 넘어가도 돼요!
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기도 인터페이스를 계층화해서 관리하면 조금 더 깔끔하게 사용할 수 있을 것 같아요!
또한, CardNumberBlockError는 ErrorType을 상속받는데 서로 이름의 형식이 달라 코드를 처음 보는 사람은 정확한 상속 관계를 유추하기 어려울 수 있을 것 같아요!
| 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; | |
| } | |
| export interface ErrorType { | |
| hasError: boolean; | |
| errorMessage: string; | |
| } | |
| export interface CardNumberErrorType extends ErrorType { | |
| isDisable: boolean; | |
| } | |
| export interface CardFormError { | |
| cardNumbers: { | |
| firstBlock: CardNumberErrorType; | |
| secondBlock: CardNumberErrorType; | |
| thirdBlock: CardNumberErrorType; | |
| fourthBlock: CardNumberErrorType; | |
| }; | |
| expiration: { | |
| month: ErrorType; | |
| year: ErrorType; | |
| }; | |
| owner: ErrorType; | |
| } |
| const keys: CardNumbersKeys[] = [ | ||
| "firstBlock", | ||
| "secondBlock", | ||
| "thirdBlock", | ||
| "fourthBlock", | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 상수로 분리하거나 미리 지정해둔 type 등을 사용하면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 다음과 같이 상수로 분리했습니다!
// constants/cardConstants.ts
export const CARD_INFORMATION = {
EXPIRATION_DATE: ["month", "year"] as const,
CARD_NUMBER_BLOCK: [
"firstBlock",
"secondBlock",
"thirdBlock",
"fourthBlock",
] as const,
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| export const convertMonth = (month: string): string => { | ||
| const monthNumber = Number(month); | ||
| if (isNaN(monthNumber)) return ""; | ||
| return monthNumber < 10 ? `0${monthNumber}` : `${monthNumber}`; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
월을 썼다가 지우면 00이 계속 남아 있던데, 없앨 방법은 없을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- input의 onBlur를 사용해서 focus 해제 시에 값을 변환하는 걸로 바꾸어 보았습니다!
// pages/Home.tsx
// 숫자 한자리 입력 시 두자리로 반환 (ex. 3 입력 시 03으로 변환)
const handleExpirationDateBlur = (
date: keyof CardData["expirationDate"],
e: React.FocusEvent<HTMLInputElement>
) => {
const value = e.target.value;
if (value.length === 1) {
setExpirationDate((prev) => ({
...prev,
[date]: `0${value}`,
}));
}
};
| import type { ErrorType } from "../types"; | ||
|
|
||
| // 대문자로 변경 | ||
| export const formatStringToUpper = (text: string): string => text.toUpperCase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 친구는 validator보다는 util의 성격을 띄고 있는 것 같아요!
| 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); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 html 태그의 속성을 잘 살펴보면 훨씬 간단하게 해결할 수 있어요.
(max, maxlength 등)
MDN Docs를 한번 읽어보는 것도 좋아요!
There was a problem hiding this comment.
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....)
📍 BackGround
😎 Content
목표
파일 구조
상수화
분리
input을 각각 분리해서 만들지, 아니면 하나로 쓰되props를 활용할지였음 ➡️input을 하나로 사용하고,props를 활용하기로 결정isDisable과hasError을 사용타입
useState를 사용하여 error를 관리하기 위해CardError타입을 하나 만들어서 전체로 관리하고, 세부 error 타입을 선언ErrorType타입은hasError와errorMessage를 가지는데, 카드번호 에러는input의disable사용을 위해 확장으로isDisable속성 추가해줌📸 Screenshot
default.mp4
추가로 고려해볼 것