Skip to content

[페이먼츠 2단계 - hooks & state] 루멘(이운태) 미션 제출합니다.#545

Open
bigcloud07 wants to merge 74 commits into
woowacourse:bigcloud07from
bigcloud07:step2
Open

[페이먼츠 2단계 - hooks & state] 루멘(이운태) 미션 제출합니다.#545
bigcloud07 wants to merge 74 commits into
woowacourse:bigcloud07from
bigcloud07:step2

Conversation

@bigcloud07
Copy link
Copy Markdown

@bigcloud07 bigcloud07 commented May 12, 2026

🎯 페이먼츠

이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.

1단계

  • 재사용 가능한 Input Component를 개발한다.
  • Storybook을 사용하여 컴포넌트의 다양한 상태를 시각적으로 테스트한다.
  • 카드 정보를 효과적으로 렌더링 하기 위한 상태 관리를 경험한다.

2단계

  • 다양한 Form 구성 요소들간의 상태를 효율적으로 관리한다.
  • hooks API를 이용하여 상태 관리 로직을 구현한다.
  • custom hooks를 생성하여 Form 관리 로직을 컴포넌트에서 분리하고 재사용한다.
  • Controlled & Uncontrolled Components에 입각하여 Form을 핸들링한다.

3단계

  • MSW로 네트워크 경계를 모킹하고, 프론트엔드가 보는 서버의 모습을 설계한다.
  • 비동기 상태를 idle | loading | success | error 네 가지로 명시적으로 관리한다.
  • 실제 서버에 보낼 요청·받을 응답 형식을 설계하여 서버-클라이언트 계약을 경험한다.
  • React Testing Library와 MSW로 사용자 관점의 통합 테스트를 작성한다.

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

  • 기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
  • 기본적인 프로그래밍 요구 사항(코드 컨벤션, 에러 핸들링 등)을 준수했나요?
  • 배포한 데모 페이지에 정상적으로 접근할 수 있나요?

리뷰 요청 & 논의하고 싶은 내용

1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점

  • 현재 컴포넌트 구조
graph TD
    App[App]
    App --> BR[BrowserRouter / Routes]
    BR -->|path: /| RC[RegisterCard 페이지]
    BR -->|path: /complete| RCM[RegisterComplete 페이지]

    RC --> CP[CardPreview]
    RC --> CI[CardInput]
    RC --> BTN[확인 버튼]

    CI --> CN[CardNumber]
    CI --> CED[CardExpiryDate]
    CI --> CC[CardCompany]
    CI --> CCV[CardCvc]
    CI --> CPW[CardPassword]

    RC -.hook.-> H1[useCardNumber]
    RC -.hook.-> H2[useExpiryDate]
    RC -.hook.-> H3[useCardCvc]
    RC -.hook.-> H4[useCardCompany]
    RC -.hook.-> H5[useCardPassword]

    H1 & H2 & H3 & H4 & H5 -.상태 전달.-> CI
    H1 & H2 & H4 -.상태 전달.-> CP
Loading
  1. 언제 Custom Hook으로 추출해야 하는가

이번 요구사항에 “처음부터 훅 구조를 설계하지 않고, 먼저 1단계 스타일로 완성한 뒤 중복을 확인하고 추출한다”는 내용이 있어서 처음에는 상태와 핸들러를 컴포넌트 안에서 직접 관리하는 방식으로 구현했습니다.

그런데 구현을 진행하면서 카드 번호, 카드사, 유효기간, CVC, 비밀번호 입력이 모두 비슷한 구조를 가진다는 점이 보였습니다. 각 필드는 공통적으로 useState로 입력값을 관리하고, onChange로 값을 갱신하며, onBlur 시점에 검증을 수행하고, 에러 상태를 따로 관리하는 흐름을 가지고 있었습니다.

처음에는 이 로직을 useCardForm 같은 하나의 훅으로 묶는 것도 고민했습니다. 하지만 그렇게 할 경우 한 훅 안에 여러 입력값 상태와 많은 핸들러가 모이게 되어, 오히려 훅 하나가 너무 많은 책임을 갖게 된다고 느꼈습니다.

결국 각 입력 필드의 상태와 검증 책임을 기준으로 useCardNumber, useCardCompany, useExpiryDate, useCardCvc, useCardPassword처럼 필드 단위로 Custom Hook을 분리했습니다. 이 과정을 통해 Custom Hook은 단순히 코드 길이를 줄이기 위한 수단이 아니라, 반복되는 구조와 명확한 책임 단위가 보일 때 추출하는 것이 더 적절하다는 점을 배웠습니다.

  1. 카드 번호 입력 시 카드 브랜드를 구분하는 로직

이번 미션에서는 카드 번호를 입력하는 과정에서 Visa, Mastercard, Diners, AMEX, UnionPay 같은 카드 브랜드를 자동으로 판별하고, 해당 브랜드 이미지를 카드 프리뷰에 보여주는 기능도 구현했습니다.

처음에는 카드 번호 앞자리 조건을 CardNumber 컴포넌트 안에서 직접 처리할 수도 있다고 생각했습니다. 하지만 카드 브랜드 판별은 UI를 그리는 책임이라기보다는 도메인 규칙에 가까운 로직이라고 판단했습니다. 그래서 브랜드별 prefix 규칙과 카드 번호 그룹 길이 정보는 상수로 분리하고, 실제 판별 로직은 utils/cardBrand.ts로 분리했습니다.

특히 브랜드마다 카드 번호 길이와 그룹 구성이 다르다는 점이 고민 지점이었습니다. Visa, Mastercard, UnionPay는 16자리지만, Diners는 14자리, AMEX는 15자리라서 입력칸 개수와 각 칸의 최대 길이도 달라져야 했습니다. 따라서 단순히 앞자리만 보고 브랜드 이미지를 바꾸는 것이 아니라, 브랜드에 따라 카드 번호 입력 UI와 검증 규칙까지 함께 달라지도록 처리했습니다.

이 작업을 하면서 카드 브랜드 판별 로직은 단순한 화면 표시용 로직이 아니라 입력 UX와 유효성 검증에도 영향을 주는 핵심 규칙이라는 것을 알게 되었습니다. 그래서 컴포넌트 내부에 흩어두기보다, 상수와 유틸 함수로 분리해 재사용 가능하게 만드는 것이 더 적절하다고 느꼈습니다.

또 카드 번호를 입력하는 도중에는 아직 브랜드를 확정할 수 없는 상태도 존재하기 때문에, “완성된 카드 번호가 유효한지”와 “현재 입력값이 특정 브랜드가 될 가능성이 있는지”를 구분해서 생각해야 했습니다. 이를 통해 검증 로직을 만들 때 최종 결과만 판단하는 것이 아니라, 사용자가 입력 중인 중간 상태까지 고려해야 더 자연스러운 입력 경험을 만들 수 있다는 점을 배웠습니다.

2) 이번 리뷰를 통해 논의하고 싶은 부분

  1. 커스텀 훅의 분리 단위

이번 구현에서도 step1의 구현과 비슷하게 Form 상태와 검증 로직을 UI 컴포넌트에서 분리하기 위해 useCardNumber, useCardCompany, useExpiryDate, useCardCvc, useCardPassword처럼 입력 항목별로 Custom Hook을 나누었습니다.

현재 구조는 각 입력 컴포넌트가 필요한 상태와 핸들러만 받아서 사용한다는 점에서는 명확하다고 느꼈지만, 카드 등록 전체의 완료 여부나 다음 입력 단계 노출 조건은 여러 훅의 상태를 조합해서 판단하고 있습니다.

이런 구조에서 입력 항목별로 훅을 분리한 현재 방식이 적절한지 궁금합니다. 혹시 useRegisterCardForm처럼 카드 등록 Form 전체의 흐름과 완료 조건을 관리하는 상위 훅을 추가하는 편이 더 나을지, 아니면 현재처럼 작은 단위의 훅들을 페이지에서 조합하는 방식이 더 유지보수하기 좋은 구조인지 우디의 의견을 듣고 싶습니다.

  1. 에러 상태 모델링에 대한 고민

현재 각 Custom Hook에서는 cardCvcErrorMode: 'cvcCount' | 'notNumber' | null처럼 에러 상태를 문자열 리터럴 타입으로 관리하고 있고, 실제 에러 메시지 매핑은 컴포넌트에서 처리하고 있습니다.

이 구조는 훅이 “어떤 에러가 발생했는지”만 알려주고, UI는 그 에러를 어떻게 보여줄지 결정한다는 점에서 역할 분리가 명확하다고 생각했습니다. 다만 컴포넌트가 에러 키와 메시지 매핑 구조를 알아야 하기 때문에, 호출부와 에러 타입 사이의 결합이 남아있는 것 같다는 고민도 들었습니다.

우디는 이런 경우에 훅이 현재처럼 에러 종류만 노출하는 방식이 적절하다고 보시는지 궁금합니다. 아니면 훅에서 바로 에러 메시지 문자열이나 hasError 같은 boolean 값을 함께 제공해서 컴포넌트가 에러 타입을 직접 알 필요 없도록 만드는 편이 더 나은 구조일지 의견을
듣고 싶습니다.

  1. 유효성 검증 책임의 위치

현재 각 Custom Hook은 자신이 담당하는 입력값의 상태와 필드 단위 검증만 관리하고 있습니다. 예를 들어 useCardCvc는 CVC 값과 CVC 에러 상태를 관리하고, useCardPassword는 비밀번호 값과 비밀번호 에러 상태를 관리합니다.

반면 isCvcComplete, isPasswordComplete처럼 카드 등록 Form 전체 흐름에서 “다음 단계로 넘어갈 수 있는지”, “확인 버튼을 보여줄 수 있는지”를 판단하는 로직은 RegisterCard 페이지 컴포넌트 안에서 여러 훅의 상태를 조합해 계산하고 있습니다.

현재 구조는 페이지가 전체 입력 흐름을 조율한다는 점에서는 자연스럽다고 생각했습니다. 하지만 입력 단계가 카드 번호 → 카드사 → 유효기간 → CVC → 비밀번호처럼 순차적으로 이어지고, 각 단계의 완료 여부가 다음 UI 노출 조건에 영향을 주다 보니 페이지 컴포넌트에 Form 흐름 판단 로직이 점점 모이는 느낌도 있었습니다.

우디는 이런 경우에 현재처럼 페이지에서 각 훅의 상태를 조합해 전체 완료 여부를 판단하는 방식이 적절하다고 보시는지 궁금합니다. 아니면 useCardFormValidation이나 useRegisterCardForm처럼 카드 등록 전체의 단계 진행 상태와 제출 가능 여부를 계산하는 별도 훅으로 분리하는 편이 더 유지보수하기 좋은 구조일지 의견을 듣고 싶습니다.

  1. 카드 브랜드 판별 로직에 대해

이번 구현에서는 카드 번호 입력값을 기반으로 Visa, Mastercard, Diners, AMEX, UnionPay 브랜드를 판별하도록 했습니다. 브랜드별 prefix 조건과 카드 번호 그룹 길이는 상수로 분리하고, 실제 판별 및 검증 로직은 utils/cardBrand.ts에서 관리하도록 구현했습니다.

이 구조는 컴포넌트에서 브랜드 판별 규칙을 직접 알지 않아도 된다는 점에서는 괜찮다고 느꼈습니다. 다만 카드 브랜드 규칙이 입력 UI, 카드 프리뷰 이미지, 카드 번호 유효성 검증에 모두 영향을 주기 때문에 현재처럼 상수와 유틸 함수로만 관리하는 것이 충분한지 고민이 되었습니다.

우디는 카드 브랜드별 prefix, 전체 길이, 입력 그룹 길이, 이미지 정보처럼 서로 관련 있는 정보를 각각 분리해서 관리하는 현재 방식이 적절하다고 보시는지 궁금합니다. 아니면 브랜드별 설정을 하나의 객체로 모아서, 예를 들어 CARD_BRANDS 안에 prefixRules, groupLengths, image, validator 같은 정보를 함께 두는 방식이 더 확장성 있는 구조일지도 의견을 듣고 싶습니다.

또 카드 번호 입력 중에는 아직 브랜드가 확정되지 않은 중간 상태가 존재합니다. 현재는 입력값이 늘어남에 따라 가능한 브랜드를 추론하고, 최종 길이에 도달했을 때 유효성 검증을 수행하는 방식인데, 이런 “입력 중 상태”와 “최종 검증 상태”를 분리해서 관리하는 현재 접근이 적절한지도 함께 리뷰받고 싶습니다.


✅ 리뷰어 체크 포인트

1. Form 상태 관리 & Custom Hook 분리

  • 반복되는 로직을 custom hook으로 분리했는가?
  • hook 내부와 UI 컴포넌트의 역할이 명확하게 분리되어 있는가?
  • 상태 흐름이 직관적이며, Form 전체를 일관되게 관리할 수 있는 구조인가?

2. 입력 UI 흐름과 UX

  • 카드 번호 입력 시 필드 간 자동 포커싱 이동이 자연스럽게 동작하는가?
  • 숫자만 입력 가능한 필드에서 제한, 에러 메시지, 유효성 피드백 등 사용자 경험이 충분히 고려되었는가?

3. 컴포넌트 구조 및 재사용성

  • 컴포넌트가 명확한 역할과 책임을 가지며 과도하게 분리되거나 중첩되어 있지 않은가?
  • Input, Button 등 재사용 가능한 컴포넌트가 잘 정의되어 있는가?

4. 상태 기반 유효성 검사 및 확인 버튼 활성화

  • 모든 필드가 유효할 때만 확인 버튼이 정확히 활성화/비활성화되는가?
  • 유효성 검사의 기준이 명확하고, 상태 변경에 따른 UI 반응이 잘 연결되어 있는가?

5. 비동기 상태 · 네트워크 경계 · 통합 테스트

  • 비동기 상태를 idle | loading | success | error 네 가지로 명시적으로 관리하고, isLoading/error를 별도 boolean으로 쪼개지 않았는가?
  • MSW handler가 POST/GET/DELETE /cards와 400 시나리오까지 포함하여 네트워크 경계에서 동작하는가?
  • 통합 테스트가 fetch·axios를 모킹하지 않고, MSW + RTL로 사용자 관점에서 작성되었는가?
  • RTL 요소 탐색이 getByRole → getByText → getByLabelText → getByTestId 우선순위를 따르고, 비동기 요소에 findBy*를 사용했는가?

bigcloud07 and others added 30 commits April 28, 2026 15:13
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
Co-authored-by: DongEun02 <ehddms0517@naver.com>
Co-authored-by: eunnn <DongEun02@users.noreply.github.com>
bigcloud07 and others added 25 commits May 1, 2026 15:48
Co-authored-by: DongEun02 <ehddms0517@naver.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Review Change Stack

전체 개요

이 PR은 React와 Emotion을 사용한 카드 결제 입력 애플리케이션의 전체 초기 구현입니다. Vite, TypeScript, Storybook을 포함한 현대적인 개발 환경을 구축하며, 카드 번호 브랜드 인식, 유효기간/CVC 검증, 실시간 카드 미리보기 기능을 제공합니다. 다섯 가지 카드 브랜드(Visa, Mastercard, Diners, AMEX, UnionPay) 지원 및 한글 에러 메시지를 포함하며, 모든 UI 컴포넌트가 Storybook을 통해 문서화되어 있습니다.

예상 코드 리뷰 시간

🎯 3 (Moderate) | ⏱️ ~25분

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 페이먼츠 2단계 hooks & state 구현이라는 명확한 주제를 담고 있으며, 변경 사항의 핵심을 잘 나타냅니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션을 포함하고 있으며, 학습 목표, 셀프 리뷰 체크리스트, 리뷰 요청 항목, 리뷰어 체크 포인트가 완벽히 채워져 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🧹 Nitpick comments (38)
src/styles/globalStyles.ts (1)

16-16: ⚡ Quick win

테마 토큰을 직접 사용해 스타일 일관성을 맞추는 편이 좋습니다.

body 배경색을 하드코딩하지 말고 theme.colors.pageBackground를 참조하면, 테마 변경 시 전역 스타일 드리프트를 막을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/globalStyles.ts` at line 16, The body background is hardcoded to
rgb(245, 245, 245); update the globalStyles definition to use the theme token
instead: replace the literal in the body style with theme.colors.pageBackground
so it reads the color from the theme (refer to the body selector in
src/styles/globalStyles.ts and the theme.colors.pageBackground token).
package.json (1)

6-13: ⚡ Quick win

테스트 실행 스크립트가 빠져 있어 검증 루프가 약합니다.

Vitest/Playwright를 도입했는데 test 스크립트가 없어 로컬/CI에서 일관된 실행 진입점이 없습니다. scripts에 최소 test(예: vitest run) 하나는 두는 게 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 6 - 13, The package.json scripts block is missing
a test entry; add a "test" script under "scripts" (e.g., "test": "vitest run")
so local and CI runs have a single canonical entry point, and optionally add
"test:e2e" (e.g., "playwright test") if you want a separate Playwright entry;
update the "scripts" object to include these new keys so tooling and CI can
invoke npm run test consistently.
src/constants/cardCompanies.ts (1)

14-20: ⚡ Quick win

성능 관점에서 개선할 수 있는 패턴입니다.

reduce 내부에서 스프레드 연산자(...)를 사용하면 매 반복마다 새로운 객체를 생성하게 되어 O(n²) 복잡도를 가지게 됩니다. 현재 항목이 8개로 적어 실질적 영향은 미미하지만, 이 패턴은 확장성 측면에서 권장되지 않습니다.

더 효율적인 방법을 고려해보세요:

  • Object.fromEntriesmap을 조합하는 방법
  • 또는 reduce 내부에서 가변 객체를 직접 수정하는 방법

이 패턴을 개선하면 어떤 이점이 있을지 생각해보시고, 더 나은 대안을 찾아보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants/cardCompanies.ts` around lines 14 - 20, The current
CARD_COMPANY_LABEL creation uses CARD_COMPANIES.reduce with a spread inside the
reducer causing repeated object allocation and O(n²) behavior; change it to an
O(n) pattern by either using Object.fromEntries(CARD_COMPANIES.map(c =>
[c.value, c.label])) or by mutating the accumulator in the reducer (e.g., let
labels = {} and set labels[company.value] = company.label then return labels)
when building CARD_COMPANY_LABEL from CARD_COMPANIES to avoid repeated object
copies.
src/components/CardPreview.tsx (2)

80-89: ⚡ Quick win

배열 접근 안전성을 검토해보세요.

cardExpiryDate 타입이 string[]로 선언되어 있지만, 코드는 최소 2개의 요소가 있다고 가정하고 있습니다(Line 80, 87). 만약 빈 배열이나 요소가 1개만 있는 배열이 전달되면 런타임 오류가 발생할 수 있습니다.

다음 방법들을 고려해보세요:

  • Props 타입을 [string, string] 같은 튜플로 더 명확하게 정의
  • 또는 cardExpiryDate.length >= 2 체크를 추가하여 방어 코드 작성
  • 상위 컴포넌트에서 이미 보장되는 사항인지 확인

타입 안정성과 방어적 프로그래밍 관점에서 검토해보시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPreview.tsx` around lines 80 - 89, The code assumes
cardExpiryDate has two elements but may be a shorter array; update the
CardPreview component to guard access by either changing the prop type to a
tuple [string, string] (so TypeScript enforces two elements) or add a runtime
check (e.g., verify cardExpiryDate.length >= 2) before using cardExpiryDate[0]
and cardExpiryDate[1]; adjust the JSX conditional that currently checks
cardExpiryDate[0] !== '' && cardExpiryDate[1] !== '' to use the length check or
the new tuple typing so you avoid potential out-of-bounds access.

61-78: ⚡ Quick win

동적 너비 계산이 레이아웃 변화를 일으킬 수 있습니다.

Line 71에서 minWidth를 카드 번호 길이에 따라 동적으로 계산하고 있습니다. 이는 사용자가 입력하는 동안 레이아웃이 움직이는 현상(layout shift)을 유발할 수 있습니다.

고려해볼 점들:

  • 고정된 너비를 사용하여 안정적인 레이아웃을 유지할 수 있는지
  • 매직 넘버(34, 10)의 의미를 상수로 추출하거나 주석으로 설명할 수 있는지
  • 현재 UX가 의도된 것인지, 아니면 개선의 여지가 있는지

사용자 입력 시 레이아웃 안정성에 대해 검토해보시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPreview.tsx` around lines 61 - 78, The dynamic minWidth
calculation in the CardPreview span (inside the cardNumbers.map callback) causes
layout shifts; replace the per-digit-based minWidth with a stable fixed width
(or a fixed width derived from clearly named constants like CARD_CELL_MIN_WIDTH
and CARD_DIGIT_PX) and extract the magic numbers 34 and 10 into those constants
(or add comments) so the span uses a consistent width (e.g., width:
CARD_CELL_MIN_WIDTH) to prevent layout movement during input while preserving
the visual spacing logic in the map rendering.
src/hooks/useCardPassword.tsx (1)

19-25: ⚡ Quick win

최대 길이 검증도 고려해보세요.

현재 handlePasswordBlur는 최소 길이(2자리)만 검증하고 있습니다. 카드 비밀번호는 일반적으로 2-4자리로 제한됩니다.

다음을 검토해보세요:

  • 최대 길이 제한이 다른 곳(예: 입력 컴포넌트의 maxLength 속성)에서 이미 처리되고 있나요?
  • 아니면 이 훅에서도 최대 길이 검증을 추가해야 할까요?
  • 사용자가 4자리를 초과하여 입력하면 어떤 동작이 예상되나요?

입력 유효성 검증의 책임이 어디에 있는지 명확히 하고, 필요하다면 최대 길이 검증도 추가하는 것을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardPassword.tsx` around lines 19 - 25, handlePasswordBlur
currently only checks minimum length; add a maximum-length validation so card
passwords are constrained to 2–4 digits. In the handlePasswordBlur function,
check cardPassword.length > 4 and call setCardPasswordErrorMode('passwordCount'
or a new 'passwordTooLong' mode) (and keep the existing min-length branch), and
ensure consistency with any maxLength on the input component; update
setCardPasswordErrorMode usage so the hook enforces the 2–4 length rule for
cardPassword.
src/hooks/useCardCompany.tsx (1)

7-9: ⚡ Quick win

타입 단언(type assertion) 사용에 대해 생각해볼 점이 있습니다.

Line 8에서 e.target.valueCardCompanyStatus['cardCompany'] 타입으로 단언하고 있습니다. 현재 코드는 <select>가 제어 컴포넌트이고 옵션이 CARD_COMPANIES에서 제공되므로 실행 시점에는 안전합니다.

하지만 타입 단언은 컴파일러의 타입 체크를 우회하는 방법이기 때문에, 향후 코드 변경 시 안전성을 보장하기 어려울 수 있습니다.

다음을 고민해보세요:

  • 타입 단언 대신 타입 가드를 사용하여 런타임 검증을 추가하는 방법
  • 또는 현재 패턴이 제어 컴포넌트에서 충분히 안전한 이유

이 패턴이 프로젝트의 다른 훅들과 일관성이 있는지도 확인해보시면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardCompany.tsx` around lines 7 - 9, The handler
handleCardCompany currently force-casts e.target.value to
CardCompanyStatus['cardCompany']; instead, validate the incoming value at
runtime against the allowed options (CARD_COMPANIES) before calling
setCardCompany: read const value = e.target.value and if
CARD_COMPANIES.includes(value as typeof CARD_COMPANIES[number]) then
setCardCompany(value) else ignore/log/error; alternatively make the select
strongly typed so the change event provides the specific union type rather than
asserting. Ensure you reference handleCardCompany, setCardCompany,
CardCompanyStatus['cardCompany'], and CARD_COMPANIES when applying the fix.
src/components/CardCvc.tsx (3)

1-2: ⚡ Quick win

TypeScript import 경로에서 파일 확장자를 제거하는 것을 고려해보세요.

.ts 또는 .tsx 확장자를 import 문에 명시하는 것은 일반적으로 권장되지 않습니다. 대부분의 TypeScript 프로젝트와 번들러는 확장자 없이 동작하며, 이는 다음과 같은 이유로 선호됩니다:

  • 파일 확장자 변경 시 import 문 수정 불필요
  • 표준 관행과의 일관성
  • 일부 도구에서 문제를 일으킬 수 있음

이 프로젝트의 다른 파일들과 일관성을 확인하고, tsconfig.json의 moduleResolution 설정을 검토해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardCvc.tsx` around lines 1 - 2, The imports at the top of
CardCvc.tsx include explicit .ts extensions (CVC_ERROR_MESSAGE and the
Cvc/CvcHandler types); remove the file extensions so the imports read from
'../constants/messages' and '../types/cardStausTypes' (i.e., drop ".ts") to
match project convention, then verify moduleResolution in tsconfig.json and
other files for consistency and update any other files that import these modules
with extensions so all imports follow the same pattern.

30-48: ⚡ Quick win

입력 요소에 접근성 속성 추가를 고려해보세요.

현재 input 요소에는 접근성 속성이 없습니다. 스크린 리더 사용자를 위해 어떤 속성들을 추가하면 좋을까요?

힌트:

  • aria-label 또는 aria-labelledby로 입력 필드의 목적 명시
  • aria-invalid 속성으로 에러 상태 전달
  • aria-describedby로 에러 메시지와 연결
  • autoComplete 속성 (예: "cc-csc")

Web Content Accessibility Guidelines (WCAG)를 참고하여 적절한 속성들을 탐구해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardCvc.tsx` around lines 30 - 48, Add accessibility
attributes to the CardCvc input: add an aria-label (or aria-labelledby)
describing the field (e.g., "Card CVC"), set aria-invalid to true when
cardCvc.cardCvcErrorMode is non-null, add aria-describedby pointing to the ID of
the related error message element (create an id like "card-cvc-error" where the
error is rendered), and set autoComplete="cc-csc"; keep existing props and
handlers (value={cardCvc.cardCvc}, onChange={setCardCvc.handleCardCvc},
onBlur={setCardCvc.handleCvcBlur}) so the input remains functionally unchanged.

42-45: 💤 Low value

조건부 스타일링 로직을 단순화할 수 있는지 검토해보세요.

템플릿 리터럴 내부의 삼항 연산자가 복잡해 보입니다. 이를 더 읽기 쉽게 만드는 방법을 생각해보세요:

  • 조건을 변수로 추출하기
  • Emotion의 다른 기능 활용하기 (예: css 배열, 조건부 스타일 병합)
  • 테마에 상태별 색상 getter 함수 추가하기

어떤 방법이 가장 유지보수하기 쉬울까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardCvc.tsx` around lines 42 - 45, The borderColor expression
in the CardCvc component (currently using cardCvc.cardCvcErrorMode !== null
inside a template literal) is hard to read—extract a descriptive boolean like
const hasError = cardCvc.cardCvcErrorMode !== null and use that in the style,
then replace the inline ternary with a clearer conditional style merge (e.g.,
pass an array or spread a conditional object) that sets borderColor to hasError
? theme.colors.error : theme.colors.inactiveBorder; update the style block that
contains border and borderColor so it references hasError and keep border as the
static value while computing only the varying color to improve readability and
maintainability.
src/components/CardNumber.tsx (5)

62-62: ⚡ Quick win

매직 넘버를 상수로 추출하는 것을 고려해보세요.

Line 62의 17.75는 각 문자의 픽셀 너비를 나타내는 것으로 보입니다.

다음을 고민해보세요:

  • 이 값은 어떻게 결정되었나요? (폰트 크기, 패딩 등과의 관계)
  • 다른 컴포넌트에서도 비슷한 계산이 필요하지 않을까요?
  • 테마나 디자인 토큰에 이 값을 포함시켜야 하지 않을까요?
  • CHAR_WIDTH_PX 같은 명확한 이름으로 상수화하면 어떨까요?

상수 이름만으로 "왜 17.75인가?"를 설명할 수 있도록 주석이나 명명을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardNumber.tsx` at line 62, Extract the magic number 17.75
used in the width calculation inside CardNumber.tsx (where width: `${maxLength *
17.75}px`) into a clearly named constant (e.g., CHAR_WIDTH_PX) and replace the
inline literal with maxLength * CHAR_WIDTH_PX; add a one-line comment by the
constant explaining how 17.75 was derived (font-size, padding,
monospace/letter-spacing assumptions) and consider exporting or moving
CHAR_WIDTH_PX to a shared theme/design-tokens module if other components may
reuse the same character width.

1-3: ⚡ Quick win

import 경로의 파일 확장자를 제거하세요.

이전 컴포넌트들과 동일한 패턴입니다. TypeScript import에서 .ts 확장자를 제거하여 프로젝트 전체의 일관성을 유지하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardNumber.tsx` around lines 1 - 3, The import statements in
CardNumber.tsx include explicit .ts extensions which breaks project import
style; update the imports that reference '../constants/messages.ts',
'../types/cardStausTypes.ts', and '../utils/cardBrand.ts' to remove the '.ts'
suffix so they become '../constants/messages', '../types/cardStausTypes', and
'../utils/cardBrand' while keeping the imported symbols (CARD_ERROR_MESSAGE,
CardHandler, CardStatus, getCardNumberGroupLengths) unchanged.

48-74: ⚡ Quick win

입력 필드들의 접근성을 개선해보세요.

다른 컴포넌트들과 마찬가지로 접근성 속성이 부족합니다. 특히 여러 입력 필드가 하나의 카드 번호를 구성하므로:

  • 각 필드가 전체 카드 번호의 일부임을 어떻게 전달할까요?
  • aria-label에 "첫 번째 그룹", "두 번째 그룹" 등을 포함해야 할까요?
  • autoComplete="cc-number"는 어떻게 작동할까요? (여러 필드로 나뉘어 있을 때)
  • 포커스 관리는 적절한가요? (한 필드가 채워지면 자동으로 다음 필드로 이동?)

카드 번호 입력의 UX 베스트 프랙티스를 조사하고, 포커스 자동 이동 같은 기능도 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardNumber.tsx` around lines 48 - 74, Wrap the set of inputs
(cardStatus.cardNumbers) in an accessible group (e.g., a container with
role="group" and an aria-label like "Credit card number") and update each input
to expose its position and state: add aria-label={`Card number group ${index +
1} of ${cardNumberGroupLengths.length}`} (or localized text), aria-invalid when
cardStatus.cardNumberErrorMode is set, and only include autoComplete="cc-number"
on the first input to help browsers aggregate the full number; enhance
setCardStatus.handleCardNumbers and setCardStatus.handleCardNumbersBlur to
implement focus management (auto-advance to the next input once value.length ===
maxLength, move focus back on backspace when empty, and merge values for form
submit) and ensure the inputs keep inputMode="numeric" and appropriate
placeholders so screen readers and autofill work correctly.

55-55: 💤 Low value

placeholder 생성 로직을 개선할 방법을 생각해보세요.

'123456'.slice(0, maxLength)는 각 그룹의 placeholder를 생성합니다.

더 나은 접근을 고려해보세요:

  • 각 브랜드별로 적절한 placeholder를 상수로 정의
  • '0'.repeat(maxLength) 사용
  • 실제 카드 번호 형식과 유사하게 표시

어떤 방법이 사용자에게 가장 직관적인 힌트를 제공할까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardNumber.tsx` at line 55, The placeholder generation using
'123456'.slice(0, maxLength) in the CardNumber component is brittle—replace it
with a small helper (e.g., getPlaceholder) used where placeholder is set instead
of the current expression; implement getPlaceholder(brand, maxLength) to return
either a brand-specific constant placeholder (map card brand to a typical
pattern), or a neutral repeat-based hint like '0'.repeat(maxLength), or
preferably a formatted group-style string matching real card layout (e.g., "0000
0000 0000 0000" trimmed to maxLength); update the placeholder prop usage in
CardNumber to call this helper so placeholders are descriptive and consistent
across brands.

66-69: ⚡ Quick win

테두리 색상 조건 로직을 단순화할 방법을 탐구해보세요.

현재 에러 테두리를 표시하는 조건이 두 가지를 확인합니다:

  1. 현재 입력의 길이가 maxLength보다 작은가
  2. 전체 카드 번호에 에러 모드가 있는가

이 로직의 의도를 생각해보세요:

  • cardNumber.length < maxLength 조건이 필요한가요?
  • 이미 완전히 채워진 필드는 에러 표시를 하지 않겠다는 의도인가요?
  • 하지만 cardStatus.cardNumberErrorMode가 있다면 모든 필드가 에러 상태 아닌가요?

다음을 검토해보세요:

  • 각 입력 필드가 개별적인 에러 상태를 가져야 하는지
  • 아니면 전체 카드 번호에 대한 에러만 표시하면 되는지
  • 사용자 경험 관점에서 어떤 접근이 더 명확한지

실제 동작을 테스트하고 의도한 UX와 일치하는지 확인해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardNumber.tsx` around lines 66 - 69, The borderColor
conditional can be simplified: remove the redundant length check and base the
error border solely on the centralized error state or implement per-field error
flags; specifically, replace the current expression that uses cardNumber.length
< maxLength && cardStatus.cardNumberErrorMode !== null with a single check of
cardStatus.cardNumberErrorMode !== null (or, if you intend per-input errors, add
a per-field flag in cardStatus like cardStatus.partErrors[index] and use that
instead); update the component logic that reads cardNumber, maxLength and
cardStatus.cardNumberErrorMode (and any rendering that depends on the old
length-based behavior) to match the chosen approach.
src/hooks/useExpiryDate.tsx (3)

30-44: ⚡ Quick win

유효기간 검증 로직의 구조를 재고해보세요.

handleYearBlur 함수의 검증 순서와 조건들을 살펴보세요:

  1. 현재는 여러 조건을 순차적으로 체크하고 있습니다
  2. 조건들 간의 관계를 파악하기 어렵습니다
  3. 각 에러 상태가 언제 발생하는지 명확하지 않습니다

다음 질문들을 고민해보세요:

  • 검증 순서가 논리적인가요? (빈 값 체크 → 개별 필드 체크 → 전체 유효성 순서가 자연스러울까요?)
  • Early return 패턴이 효과적으로 사용되고 있나요?
  • handleMonthBlur와 중복되는 로직은 없나요?

로직을 다이어그램으로 그려보면 구조를 개선할 포인트가 보일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useExpiryDate.tsx` around lines 30 - 44, The handleYearBlur
validation logic is hard to follow and duplicates checks from handleMonthBlur;
refactor by consolidating expiry validation into a single helper (e.g.,
validateExpiry or isExpiryEmpty/hasValidMonth/hasValidYear) and then have
handleYearBlur call that helper: first check the combined empty state (both
month and year) and set 'emptyBoth', then check empty month -> 'emptyMonth',
then check invalid/short year length -> 'emptyYear', else clear error via
setCardExpiryDateErrorMode(null); remove overlapping logic from handleMonthBlur
and use the same helper to keep behavior consistent.

19-24: ⚡ Quick win

매직 넘버를 상수로 추출하는 것을 고려해보세요.

Line 20의 12'00'은 월(month) 유효성 검사에 사용되는 중요한 값입니다. 이런 값들을 상수로 정의하면 어떤 이점이 있을까요?

생각해볼 점:

  • 코드의 의도가 더 명확해지지 않을까요?
  • 다른 곳에서도 같은 검증 로직이 필요하다면?
  • MAX_MONTH, MIN_MONTH 같은 이름이 가독성에 어떻게 기여할까요?

src/constants/ 디렉토리에 카드 관련 상수들을 모아두는 파일이 있는지 확인해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useExpiryDate.tsx` around lines 19 - 24, Replace the magic literals
used in useExpiryDate.tsx with named constants: extract the month bounds (e.g.,
MAX_MONTH = 12) and the invalid zero-string (e.g., INVALID_MONTH_STRING = '00'
or MIN_MONTH_STRING = '01') into a card constants module (under src/constants/
like cardConstants or export from an existing card constants file), then update
the validation in the function using next and index to compare against those
constants and keep the call to setCardExpiryDateErrorMode('notMonthRange')
unchanged; reference the useExpiryDate hook and setCardExpiryDateErrorMode when
making the change.

6-66: 🏗️ Heavy lift

만료된 카드 유효기간 검증을 추가해볼까요?

현재 훅은 월/연도의 형식만 검사하고 있습니다. 하지만 실제로 과거 날짜인지는 검증하지 않습니다.

사용자가 이미 만료된 유효기간을 입력했을 때:

  • 어느 시점에 검증하는 것이 좋을까요? (onChange vs onBlur vs form submit)
  • 현재 년도/월과 어떻게 비교할 수 있을까요?
  • 새로운 에러 타입이 필요할까요? (src/types/errorTypes.ts 확인)

이런 비즈니스 로직 추가는 백엔드 검증과도 연관될 수 있으니, 서버 측 검증과의 역할 분담도 함께 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useExpiryDate.tsx` around lines 6 - 66, Add validation to detect
past (expired) card expiry dates: in useExpiryDate, after existing format checks
in handleCardExpiryDate and in the blur handlers (handleYearBlur and
handleMonthBlur) compare the entered month/year (cardExpiryDate[0],
cardExpiryDate[1]) against the current month/year (use new Date()) and set
cardExpiryDateErrorMode to a new error type (e.g., 'expiredDate' added in
src/types/errorTypes.ts) when the entered year < current year or year == current
year and month < current month; perform this check on blur
(handleYearBlur/handleMonthBlur) and optionally after both fields are populated
in handleCardExpiryDate to give immediate feedback. Ensure the comparison uses
numeric values (parseInt) and that existing empty/format checks short-circuit
before the expired check; update any callers/tests to expect the new
'expiredDate' error state.
src/hooks/useCardCvc.tsx (2)

27-36: 💤 Low value

반환 타입으로 객체 구조를 고려해보세요.

현재 배열 튜플 [Cvc, CvcHandler]로 반환하고 있는데, 객체 형태로 반환하면 어떤 장점이 있을지 생각해보세요:

  • 구조 분해 시 순서에 의존하지 않음
  • 사용하는 쪽에서 필요한 것만 선택적으로 가져올 수 있음
  • 반환 값의 의미가 더 명확해짐

예: { state: Cvc, handlers: CvcHandler } 또는 { cardCvc, cardCvcErrorMode, handleCardCvc, handleCvcBlur }

어떤 구조가 이 프로젝트의 다른 훅들과 일관성을 유지하면서도 사용성을 높일 수 있을까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardCvc.tsx` around lines 27 - 36, The hook useCardCvc currently
returns a tuple array; change its return to a descriptive object (e.g., return {
cardCvc, cardCvcErrorMode, handleCardCvc, handleCvcBlur }) so consumers can
destructure by name without relying on order; update the hook's return
type/signature accordingly and update any callers of useCardCvc to use object
destructuring (search for useCardCvc, cardCvc, handleCardCvc, handleCvcBlur to
find usages) to maintain consistency with other hooks.

19-25: ⚡ Quick win

빈 입력과 불완전한 입력을 구분하여 처리해보세요.

현재 blur 핸들러는 길이가 3 미만일 때 모두 'cvcCount' 에러로 처리합니다. 하지만 사용자가 아무것도 입력하지 않은 경우와 1~2자리만 입력한 경우를 다르게 처리하면 더 나은 UX를 제공할 수 있습니다.

다음 질문들을 고려해보세요:

  • 빈 입력 상태에서 blur가 발생했을 때 에러를 표시해야 할까요?
  • 사용자가 입력을 시작했지만 완료하지 않은 경우만 에러를 표시하는 것이 더 자연스럽지 않을까요?
  • cardCvc.trim().length를 사용하여 공백 입력도 체크해야 하지 않을까요?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardCvc.tsx` around lines 19 - 25, handleCvcBlur currently
treats any length < 3 as 'cvcCount'; change it to distinguish empty vs
incomplete input by using cardCvc.trim().length: if trimmed length === 0 set
setCardCvcErrorMode('cvcEmpty') (or null depending on UX decision), else if
trimmed length > 0 && trimmed length < 3 set setCardCvcErrorMode('cvcCount'),
otherwise setCardCvcErrorMode(null); update the logic inside the handleCvcBlur
function accordingly so blank/whitespace and partial input are handled
separately.
src/components/CardExpiryDate.tsx (4)

1-7: ⚡ Quick win

import 경로의 파일 확장자 제거를 고려해보세요.

CardCvc.tsx에서와 동일한 이슈입니다. TypeScript import 문에서 .ts 확장자를 제거하는 것이 표준 관행입니다.

프로젝트 전체에서 일관된 import 스타일을 유지하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardExpiryDate.tsx` around lines 1 - 7, The imports in
CardExpiryDate.tsx include explicit .ts file extensions (e.g.
'../constants/messages.ts', '../utils/util.ts', '../types/cardStausTypes') which
should be removed to match the project's import style and the pattern used in
CardCvc.tsx; update those import statements to '../constants/messages',
'../utils/util', and '../types/cardStausTypes' respectively so TypeScript
resolves modules without extensions and keeps imports consistent across the
codebase.

56-97: 💤 Low value

중복된 스타일링 로직을 추출하는 방법을 탐구해보세요.

두 개의 input 요소(월, 년도)가 거의 동일한 스타일 속성을 가지고 있습니다. 특히 Lines 64-76과 Lines 85-96이 유사합니다.

코드 중복을 줄이는 방법들을 고려해보세요:

  • 공통 스타일 객체를 컴포넌트 외부나 상단에 정의
  • 스타일을 반환하는 헬퍼 함수 생성
  • 두 input을 배열로 렌더링 (map 사용)

각 방법의 장단점은 무엇이며, 이 컴포넌트의 맥락에서 어떤 것이 가장 적합할까요?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardExpiryDate.tsx` around lines 56 - 97, The two input
elements for month and year duplicate nearly identical styling; extract the
shared CSS into a single helper (e.g., create a getExpiryInputStyle(theme,
isError) function) or a shared style object defined above the component and
reuse it for both inputs, while keeping the per-field conditional borderColor
logic by passing isMonthError(cardExpiry.cardExpiryDateErrorMode) or
isYearError(...) into that helper; update the month/year inputs that call
setCardExpiry.handleCardExpiryDate(0)/handleCardExpiryDate(1) and onBlur
handlers handleMonthBlur/handleYearBlur to use the shared style helper to remove
the duplicated lines and improve maintainability.

14-18: 💤 Low value

객체 스프레드 시 키 충돌 가능성을 검토해보세요.

세 개의 에러 메시지 객체를 스프레드로 병합하고 있습니다.

다음 질문들을 고민해보세요:

  • 세 객체(DATE_ERROR_MESSAGE, MONTH_ERROR_MESSAGE, YEAR_ERROR_MESSAGE)에 중복되는 키가 있을 가능성은 없나요?
  • 있다면 어떤 값이 최종적으로 사용될까요? (스프레드 순서의 중요성)
  • TypeScript가 이런 잠재적 충돌을 감지할 수 있나요?

src/constants/messages.ts 파일을 열어 실제로 키들이 어떻게 정의되어 있는지 확인해보고, 타입 정의가 이런 병합을 안전하게 만들어주는지 검증해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardExpiryDate.tsx` around lines 14 - 18, EXPIRY_ERROR_MESSAGE
is created by spreading DATE_ERROR_MESSAGE, MONTH_ERROR_MESSAGE, and
YEAR_ERROR_MESSAGE which can silently overwrite duplicate keys depending on
spread order; inspect src/constants/messages.ts to see if keys collide and
either (a) ensure all three objects have disjoint keys (rename keys if needed),
or (b) enforce typings by declaring explicit interfaces/union types for
DATE_ERROR_MESSAGE, MONTH_ERROR_MESSAGE, YEAR_ERROR_MESSAGE and typing
EXPIRY_ERROR_MESSAGE so TypeScript will catch overlaps, or (c) merge explicitly
with Object.assign or explicit property picks to make precedence intentional
(reference symbols: EXPIRY_ERROR_MESSAGE, DATE_ERROR_MESSAGE,
MONTH_ERROR_MESSAGE, YEAR_ERROR_MESSAGE).

56-97: ⚡ Quick win

접근성 개선을 위한 속성 추가를 고려해보세요.

CardCvc 컴포넌트와 마찬가지로, 이 입력 필드들도 접근성 속성이 부족합니다.

월/년도 입력이 별도의 필드로 나뉘어 있으므로:

  • 각 필드가 무엇을 입력하는 곳인지 명확하게 전달되고 있나요?
  • 스크린 리더가 "MM" / "YY" placeholder만으로 충분히 이해할 수 있을까요?
  • 에러가 발생했을 때 어느 필드에 문제가 있는지 명확한가요?

ARIA 관련 문서를 참고하여 신용카드 입력 폼의 접근성 베스트 프랙티스를 학습해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardExpiryDate.tsx` around lines 56 - 97, The month/year
inputs in the CardExpiryDate component lack ARIA/accessibility attributes;
update the two input elements (the ones using
value={cardExpiry.cardExpiryDate[0]} and value={cardExpiry.cardExpiryDate[1]})
to include clear accessible identifiers and error linking: add semantic
attributes such as aria-label (e.g., "Card expiration month" / "Card expiration
year") or corresponding <label> with htmlFor and id, set aria-required="true",
set aria-invalid based on isMonthError(cardExpiry.cardExpiryDateErrorMode) and
isYearError(...), and add aria-describedby that points to a unique error message
element rendered when errors exist (use the same error state from
cardExpiry.cardExpiryDateErrorMode); keep existing handlers
(setCardExpiry.handleCardExpiryDate(0/1), setCardExpiry.handleMonthBlur,
setCardExpiry.handleYearBlur) unchanged while adding these attributes so screen
readers can announce field purpose and error details.
src/hooks/useCardNumber.tsx (3)

13-22: 💤 Low value

헬퍼 함수의 이름이 동작을 정확히 반영하는지 검토해보세요.

matchCardNumberGroupCount 함수는 배열의 길이를 조정하고 있습니다. 하지만 "match"라는 이름이 이 동작을 충분히 설명하고 있나요?

다음을 고려해보세요:

  • 이 함수는 새 배열을 생성하나요, 아니면 기존 배열을 수정하나요? (Line 15의 slice를 확인)
  • "Normalize", "Adjust", "Resize" 같은 동사가 더 명확할까요?
  • 빈 문자열을 채우는 동작(Line 17-19)도 함수명에 반영되어야 하나요?

좋은 함수 이름은 코드를 읽는 사람이 구현을 보지 않고도 무엇을 하는지 이해할 수 있게 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardNumber.tsx` around lines 13 - 22, The helper
matchCardNumberGroupCount does not clearly describe its behavior: it creates a
new array (uses slice) resized to match
getCardNumberGroupLengths(cardBrand).length and pads missing entries with empty
strings; rename it to something explicit like resizeAndPadCardNumberGroups or
normalizeCardNumberGroupCount, update any related JSDoc/comments to state
"returns a new array sized to the card brand's group lengths and pads with ''
for missing groups", and update all call sites to use the new name (reference
symbols: matchCardNumberGroupCount, getCardNumberGroupLengths, CardStatus,
cardNumbers).

40-45: ⚡ Quick win

카드 번호 초기화 로직을 명확히 해보세요.

첫 번째 그룹이 비어있을 때 모든 상태를 초기화하는 특별한 처리가 있습니다.

이 설계 결정에 대해 생각해보세요:

  • 사용자가 첫 번째 입력 필드를 지웠을 때 모든 필드를 초기화하는 것이 의도된 UX인가요?
  • 사용자가 뒤쪽 필드에 입력한 값들이 있어도 모두 지워지나요?
  • 이것이 사용자에게 혼란을 줄 수 있나요?

실제 사용자 시나리오를 상상해보고:

  1. 사용자가 카드 번호를 모두 입력
  2. 첫 번째 그룹에서 오타를 발견하고 전체 삭제
  3. 이때 나머지 그룹들도 함께 초기화되는 것이 자연스러운가요?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardNumber.tsx` around lines 40 - 45, The current logic in
useCardNumber.tsx resets all card state whenever the first group (next[0]) is
empty, which unintentionally clears later groups; change the condition so you
only fully reset state (calling setCardBrand(''), setCardNumberErrorMode(null),
setCardNumbers(...)) when the entire card input is empty (e.g., all entries in
next are empty) or when an explicit clear action is triggered, otherwise
preserve the existing groups and only update the changed group; locate and
update the branch that uses next[0] === '' and the calls to setCardBrand,
setCardNumberErrorMode, and setCardNumbers (and use matchCardNumberGroupCount)
to implement this refined empty-check.

31-59: ⚖️ Poor tradeoff

상태 업데이트의 일관성을 검토해보세요.

handleCardNumbers 함수는 여러 조건문에서 상태를 업데이트합니다. 각 분기에서 어떤 상태들을 업데이트하는지 패턴을 분석해보세요:

  • Line 41-44: 3개 상태 업데이트
  • Line 48-50: 3개 상태 업데이트
  • Line 56-58: 3개 상태 업데이트

다음 질문들을 고민해보세요:

  • 각 분기에서 업데이트하는 상태들의 조합이 일관성 있나요?
  • 어떤 상태는 항상 함께 업데이트되어야 하나요?
  • setCardBrandsetCardNumberErrorMode가 항상 연관되어 있다면, 이들을 하나의 상태로 묶을 수 있을까요?
  • React의 useReducer를 사용하면 상태 전환이 더 명확해지지 않을까요?

상태 관리의 복잡도가 높아질 때 고려할 수 있는 패턴들을 학습해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useCardNumber.tsx` around lines 31 - 59, handleCardNumbers updates
multiple related states inconsistently across branches (setCardBrand,
setCardNumberErrorMode, setCardNumbers); refactor so the same state-combination
is updated predictably in every branch — either consolidate card brand and error
mode into a single state object or move all state transitions into a single
update path (or use useReducer) so each branch returns a consistent payload for
setCardBrand/setCardNumberErrorMode/setCardNumbers; locate the logic in
handleCardNumbers and replace the multiple inline set... calls with a single
coordinated state update (or reducer action) that captures brand, errorMode, and
numbers together.
src/utils/cardBrand.ts (3)

8-15: ⚡ Quick win

문자열을 숫자로 변환할 때 앞자리 0이 손실될 위험을 검토해보세요.

isPrefixInRange 함수는 카드 번호 prefix를 Number()로 변환하고 있습니다.

다음 시나리오를 고려해보세요:

  • 카드 번호가 "0"으로 시작한다면? (실제로는 드물지만 논리적으로 가능)
  • startend 값에 앞자리 0이 있다면?
  • Number("0123")은 123이 되는데, 이것이 의도한 동작인가요?

카드 브랜드 prefix 범위 데이터(CARD_BRAND_PREFIX_RANGES)를 확인하고:

  • 모든 prefix가 0이 아닌 숫자로 시작하는지 검증
  • 아니면 문자열 비교를 사용하는 방법 고려
  • 숫자 비교가 필요한 이유를 명확히 이해하고 있는지 확인
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/cardBrand.ts` around lines 8 - 15, isPrefixInRange currently
converts prefixes to Number which can drop leading zeros; change it to do
string-based comparisons to preserve leading zeros: compute prefixLength as
before, extract prefixStr = cardNumber.slice(0, prefixLength), left-pad start
and end to prefixLength (or slice their prefix parts) and then compare
lexicographically (prefixStr >= startStr && prefixStr <= endStr); also add a
validation step when loading CARD_BRAND_PREFIX_RANGES to ensure entries contain
only digits and are normalized (same format/length) or document why numeric
comparison is required if you instead choose to keep Number().

34-40: 💤 Low value

빈 문자열 처리 일관성을 검토해보세요.

hasPotentialCardBrand 함수는 빈 문자열일 때 true를 반환합니다 (Line 35-37).

이것이 의도한 동작인지 확인해보세요:

  • 빈 입력이 "잠재적 브랜드를 가질 수 있다"는 의미가 논리적으로 타당한가요?
  • 호출하는 쪽(useCardNumber.tsx Line 47)에서 이 동작을 기대하고 있나요?
  • 다른 유틸 함수들도 빈 문자열을 일관되게 처리하나요?

함수의 목적과 사용 맥락을 명확히 하기 위해 JSDoc 주석을 추가하는 것도 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/cardBrand.ts` around lines 34 - 40, The function
hasPotentialCardBrand currently returns true for an empty cardNumber which is
ambiguous; decide on the intended behavior and make it explicit: if empty should
not be treated as having a potential brand, change the initial guard in
hasPotentialCardBrand to return false for '' (instead of true), and add a JSDoc
comment to hasPotentialCardBrand explaining that it returns whether the input
prefix matches any CARD_BRAND_PREFIX_RANGES via isPrefixInRange; then update any
callers (e.g., useCardNumber) that assume the previous behavior to handle empty
strings explicitly or rely on the new semantics.

59-69: ⚖️ Poor tradeoff

함수의 단일 책임 원칙을 검토해보세요.

isValidCardNumber 함수는 여러 가지를 동시에 검증하고 있습니다:

  1. 카드 브랜드가 비어있지 않은지
  2. 추출된 브랜드가 제공된 브랜드와 일치하는지
  3. 그룹 개수가 맞는지
  4. 각 그룹의 길이가 맞는지

이런 구조의 장단점을 생각해보세요:

  • 어느 검증이 실패했는지 호출자가 알 수 있나요?
  • 에러 메시지를 제공해야 한다면 어떻게 할까요?
  • 각 검증을 독립적으로 테스트하기 쉬운가요?

다음을 고려해보세요:

  • 더 작은 검증 함수들로 분리
  • 검증 결과 객체 반환 (예: { isValid: boolean, reason?: string })
  • 또는 현재 구조가 이 유스케이스에 충분히 적합한지 평가
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/cardBrand.ts` around lines 59 - 69, The isValidCardNumber function
is doing multiple validations; refactor it into smaller, single-responsibility
validators and return a structured result so callers know which check failed.
Create helper functions like isNonEmptyBrand(brand),
matchesExtractedBrand(cardNumbers, brand) which use getCardBrand(cardNumber),
hasCorrectGroupCount(cardNumbers, brand) and
groupsHaveCorrectLengths(cardNumbers, brand) which use
getCardNumberGroupLengths(cardBrand); then replace isValidCardNumber to call
these helpers and return a result object such as { isValid: boolean, reason?:
string, failedCheck?: 'brandEmpty'|'brandMismatch'|'groupCount'|'groupLength' }
so callers can inspect the specific failure and tests can target each validator
independently.
src/components/CardPassword.tsx (2)

58-77: ⚡ Quick win

비밀번호 입력 필드의 보안 속성을 검토해보세요.

현재 type="text"로 설정되어 있고 클라이언트 측에서 마스킹을 처리하고 있습니다.

다음 보안 관련 질문들을 고려해보세요:

  • autoComplete 속성이 적절하게 설정되어 있나요? (예: "cc-csc" 또는 "off")
  • 비밀번호 관리자가 이 필드를 감지하는 것을 원하나요?
  • 개발자 도구에서 마스킹되지 않은 값이 노출되지 않나요?
  • 브라우저 히스토리나 자동완성에 저장되는 것을 방지하고 있나요?

신용카드 PIN 입력의 보안 베스트 프랙티스를 조사해보고, PCI DSS 같은 표준도 참고해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPassword.tsx` around lines 58 - 77, The input currently
uses type="text" and client-side masking (maskedPassword) which exposes the
plaintext in the DOM and risks autocomplete/history storage; change the input to
type="password", add an explicit autoComplete value (e.g., "cc-csc" or "off"),
disable spellcheck, ensure inputMode="numeric" remains, and confirm that
handlePasswordChange and setCardPassword.handlePasswordBlur do not persist the
raw PIN in an easily accessible state or attributes (keep only maskedPassword
for rendering and securely clear the real value on blur). Also verify
cardPassword.cardPasswordErrorMode still controls border color logic after these
changes.

58-77: ⚡ Quick win

접근성 속성 추가를 고려해보세요.

이전 컴포넌트들과 동일하게, 접근성 속성이 부족합니다. 특히 비밀번호 입력은 마스킹되어 있어 스크린 리더 사용자에게 더 명확한 안내가 필요합니다.

생각해볼 점:

  • 입력된 문자 수를 스크린 리더에 알려줘야 할까요?
  • role 속성이 필요할까요?
  • 에러 상태를 명확하게 전달하고 있나요?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPassword.tsx` around lines 58 - 77, The input lacks
accessibility attributes for a masked password field; update the <input> used
with maskedPassword/handlePasswordChange/setCardPassword.handlePasswordBlur to
be accessible by switching to a semantic password input and adding ARIA hooks:
use type="password" (or keep text but provide aria-hidden/aria-label if masking
visually), add a clear aria-label (e.g., "Card password (2 digits)"), set
aria-invalid based on cardPassword.cardPasswordErrorMode, add aria-describedby
pointing to an error/help element that you create (and update that element
text), and ensure error messages are announced (use an aria-live="polite" region
or role="alert" on the error container). Also ensure inputMode="numeric" remains
and update the error container to reflect the current error state so screen
readers receive it.
index.html (1)

7-7: ⚡ Quick win

더 의미있는 페이지 제목을 사용하세요.

"my-app"은 너무 일반적입니다. 페이지의 목적을 나타내는 제목(예: "카드 등록" 또는 실제 서비스명)을 사용하면 사용자 경험이 개선됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 7, 현재 <title>my-app</title>처럼 일반적인 페이지 제목을 사용하고 있으므로 더
의미있는 제목으로 변경하세요; 예를 들어 페이지 목적이나 서비스명을 반영하도록 <title> 요소의 텍스트를 "카드 등록 - {서비스명}" 또는
"{서비스명} | 카드 등록" 등으로 바꿔 사용자가 탭과 검색 결과에서 페이지 목적을 바로 알 수 있게 하세요.
src/components/CardInput.tsx (3)

47-54: ⚡ Quick win

완료 조건 로직이 부모 컴포넌트와 중복됩니다.

RegisterCard.tsx의 23-27번 라인과 유사한 완료 조건 계산이 여기에도 있습니다. 이런 중복은 유지보수 시 일관성 문제를 일으킬 수 있습니다.

이 로직을 어디에 두는 것이 더 적절할까요?

  • 부모 컴포넌트에서 계산해서 props로 전달하는 방법
  • 별도의 유틸리티 함수나 커스텀 훅으로 분리하는 방법

어떤 접근이 이 컴포넌트의 책임 분리에 더 적합할지 고민해보시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardInput.tsx` around lines 47 - 54, The completion-check
logic duplicated in CardInput.tsx (isCardNumberComplete, isCardCompanySelected,
isExpiryDateComplete, isCvcComplete) should be centralized: either compute these
in the parent RegisterCard.tsx and pass a single prop (e.g., isFormComplete or
the four booleans) to CardInput, or extract the logic into a shared utility or
custom hook (e.g., computeCardCompletion(...) or useCardCompletion(cardStatus,
cardCompanyStatus, cardExpiry, cardCvc)) and import it in both places; remove
the duplicated inline checks from CardInput.tsx and replace them with the
prop/hook call so both components use the same authoritative implementation.

65-77: ⚖️ Poor tradeoff

폼 필드의 DOM 순서가 논리적 순서와 반대입니다.

조건부 렌더링으로 인해 CardPassword가 DOM 상단에, CardNumber가 하단에 위치합니다. flexDirection: 'column'을 사용하고 있어 시각적으로는 의도한 대로 보일 수 있지만, 스크린 리더 사용자에게는 혼란스러울 수 있습니다.

DOM 순서를 논리적 흐름과 일치시키고, 시각적 순서 조정은 CSS의 order 속성이나 다른 레이아웃 기법을 사용하는 것을 고려해보세요. 이렇게 하면 접근성이 개선됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardInput.tsx` around lines 65 - 77, The conditional rendering
currently places CardPassword and later fields earlier in the DOM than
CardNumber, which breaks logical reading order for assistive tech; reorder the
JSX in CardInput so the DOM always lists CardNumber, CardCompany,
CardExpiryDate, CardCvc, CardPassword (use the existing props/is-guards:
isCardNumberComplete, isCardCompanySelected, isExpiryDateComplete,
isCvcComplete) but keep their visibility conditional, and if you need a
different visual order apply CSS (e.g., flex order or grid positioning) rather
than changing DOM order so screen readers see the logical sequence while visuals
remain the same.

57-64: ⚡ Quick win

form 엘리먼트에 submit 핸들러가 없습니다.

현재 <form> 태그를 사용하고 있지만 onSubmit 핸들러가 없어서 Enter 키로 폼을 제출할 수 없습니다. 사용자 경험 개선을 위해 Enter 키 제출을 지원하는 것을 고려해보세요.

힌트:

  • onSubmit 핸들러를 추가하고 e.preventDefault() 호출
  • 마지막 필드 완료 시 자동으로 다음 단계로 이동하는 로직 구현 가능
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardInput.tsx` around lines 57 - 64, The form in CardInput.tsx
is missing an onSubmit handler so Enter doesn't submit; add an onSubmit prop to
the <form> that calls e.preventDefault() and then invokes the component's
existing submit flow (e.g., call handleSubmit or the same function wired to the
primary action button such as onConfirm/onPrimaryAction or props.onSubmit); if
you have per-field step logic, have the handler detect whether the current field
is the last and either advance to the next step (focus next input) or perform
the final submit action so Enter behaves the same as clicking the submit button.
src/pages/RegisterCard.tsx (2)

23-27: ⚡ Quick win

완료 조건 로직이 CardInput과 중복됩니다.

이 완료 상태 계산이 CardInput.tsx의 47-54번 라인과 중복됩니다. 같은 관심사를 두 곳에서 관리하고 있습니다.

이 상태 계산을 어떻게 한 곳으로 모을 수 있을까요? 커스텀 훅을 만들거나, 기존 훅들을 조합하는 방법을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RegisterCard.tsx` around lines 23 - 27, The completion logic for
isCvcComplete and isPasswordComplete is duplicated with CardInput (see cardCvc,
cardPassword and their error modes); extract that logic into a single reusable
place (e.g., create a custom hook useCardCompletion or export a helper from
CardInput) that accepts the cardCvc and cardPassword state objects and returns
isCvcComplete and isPasswordComplete, then replace the inline calculations in
RegisterCard.tsx (remove the existing isCvcComplete/isPasswordComplete
definitions) and consume the shared hook/helper instead so both CardInput and
RegisterCard derive completion from the same implementation.

42-52: ⚡ Quick win

고정 너비 레이아웃의 반응형 대응을 고려해보세요.

컨테이너와 버튼 모두 376px로 고정되어 있습니다. 더 작은 화면에서는 레이아웃이 깨질 수 있습니다.

최대 너비를 설정하면서 작은 화면에서는 유연하게 대응하는 방법(예: max-widthwidth: 100% 조합)을 검토해보시기 바랍니다.

Also applies to: 77-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RegisterCard.tsx` around lines 42 - 52, The container CSS in
RegisterCard.tsx uses a fixed width ('376px') (and similarly the button styles
around lines 77-89) which breaks on smaller screens; change those to responsive
rules by replacing width: '376px' with maxWidth: '376px' and width: '100%'
(optionally add boxSizing: 'border-box' and sensible padding) so the
RegisterCard container and the button can shrink on small viewports; update the
same pattern in the other CSS block(s) referenced (the button style) and, if you
use a theme or Mantine breakpoint utility, consider applying an explicit media
query or theme.fn.smallerThan to adjust height or padding for very small
screens.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6f14546-06de-404f-aa2b-705c60e17aec

📥 Commits

Reviewing files that changed from the base of the PR and between 7ef814f and 07d06ee.

⛔ Files ignored due to path filters (9)
  • package-lock.json is excluded by !**/package-lock.json
  • public/favicon.svg is excluded by !**/*.svg
  • public/icons.svg is excluded by !**/*.svg
  • src/assets/AMEX.png is excluded by !**/*.png
  • src/assets/Diners.png is excluded by !**/*.png
  • src/assets/Mastercard.png is excluded by !**/*.png
  • src/assets/RegisterCheck.png is excluded by !**/*.png
  • src/assets/UnionPay.png is excluded by !**/*.png
  • src/assets/Visa.png is excluded by !**/*.png
📒 Files selected for processing (47)
  • .gitignore
  • .prettierrc
  • .storybook/main.ts
  • .storybook/preview.tsx
  • README.md
  • eslint.config.js
  • index.html
  • package.json
  • src/App.tsx
  • src/components/CardCompany.tsx
  • src/components/CardCvc.tsx
  • src/components/CardExpiryDate.tsx
  • src/components/CardInput.tsx
  • src/components/CardNumber.tsx
  • src/components/CardPassword.tsx
  • src/components/CardPreview.tsx
  • src/constants/cardBrandImages.ts
  • src/constants/cardBrands.ts
  • src/constants/cardCompanies.ts
  • src/constants/messages.ts
  • src/emotion.d.ts
  • src/hooks/useCardCompany.tsx
  • src/hooks/useCardCvc.tsx
  • src/hooks/useCardNumber.tsx
  • src/hooks/useCardPassword.tsx
  • src/hooks/useExpiryDate.tsx
  • src/main.tsx
  • src/pages/RegisterCard.tsx
  • src/pages/RegisterComplete.tsx
  • src/stories/CardCompany.stories.tsx
  • src/stories/CardCvc.stories.tsx
  • src/stories/CardExpiryDate.stories.tsx
  • src/stories/CardNumber.stories.tsx
  • src/stories/CardPassword.stories.tsx
  • src/stories/CardPreview.stories.tsx
  • src/stories/cardStoryFixtures.ts
  • src/styles/globalStyles.ts
  • src/styles/theme.ts
  • src/types/cardStausTypes.ts
  • src/types/errorTypes.ts
  • src/utils/cardBrand.ts
  • src/utils/util.ts
  • tsconfig.app.json
  • tsconfig.json
  • tsconfig.node.json
  • vite.config.ts
  • vitest.shims.d.ts

Comment thread index.html
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

언어 속성이 앱 내용과 맞지 않습니다.

HTML의 lang="en"이지만 앱은 한국어 콘텐츠("확인" 버튼 등)를 사용합니다. 접근성과 SEO를 위해 lang="ko"로 수정하는 것이 적절합니다.

수정 제안
-<html lang="en">
+<html lang="ko">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 2, HTML 루트 요소의 언어 설정이 잘못되어 있습니다: 현재 <html lang="en">로 되어
있는데 앱이 한국어 콘텐츠를 사용하므로 루트 HTML 태그의 lang 속성을 "ko"로 변경하세요 (찾기 대상: the <html
lang="en"> declaration in the file) and verify any i18n or accessibility
metadata matches 한국어.

Comment thread README.md

### 실시간 프리뷰 업데이트

- 사용자의 카드 정보 입력에 따라 카드 프리뷰를 동시에 업데이트한다..
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

문서 문장부호/문장 표현을 다듬어 주세요.

Line 20의 업데이트한다..는 마침표가 중복되어 있고, Line 47의 다양한 카드 브랜드 지원한다.는 조사 누락으로 문장이 어색합니다.

Also applies to: 47-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 20, 문서의 문장 표기 오류를 고치세요: Line 20의 "업데이트한다.."에서 마침표를 하나만 남겨
"업데이트한다."로 수정하고, Line 47의 "다양한 카드 브랜드 지원한다."에 누락된 조사를 추가해 "다양한 카드 브랜드를 지원한다."로
고쳐 문장을 자연스럽게 만드세요.

CardCompanyStatus,
CardPassword as CardPasswordType,
CardPasswordHandler,
} from '../types/cardStausTypes';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 실제 타입 파일명 확인
fd -e ts -e tsx "cardS.*Types" src/types/

Repository: woowacourse/react-payments

Length of output: 98


파일명의 오타를 수정하세요.

cardStausTypes.ts 파일명에서 'Staus'는 'Status'의 오타입니다. 현재 import 경로는 실제 파일명과 일치하므로 정상이지만, 파일명 자체가 수정되어야 합니다. 파일명을 cardStatusTypes.ts로 변경하고 이를 import하는 모든 곳에서 경로를 업데이트해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardInput.tsx` at line 17, Rename the file currently named
cardStausTypes.ts to cardStatusTypes.ts and update all imports that reference
'../types/cardStausTypes' (e.g., the import in CardInput.tsx) to
'../types/cardStatusTypes' so that the filename and import paths match; search
the repo for "cardStausTypes" and replace occurrences with "cardStatusTypes" to
ensure consistency across modules that use the types exported from that file.

Comment on lines +12 to +26
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextValue = e.target.value;
const typedValue = nextValue.replaceAll('*', '');

if (typedValue === '' && nextValue.length < cardPassword.cardPassword.length) {
setCardPassword.handleCardPassword(cardPassword.cardPassword.slice(0, nextValue.length));
return;
}

const nextPassword = nextValue.includes('*')
? `${cardPassword.cardPassword}${typedValue}`.slice(0, 2)
: typedValue.slice(0, 2);

setCardPassword.handleCardPassword(nextPassword);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

비밀번호 마스킹 로직의 복잡도를 줄일 방법을 탐구해보세요.

handlePasswordChange 함수는 여러 엣지 케이스를 처리하고 있어 복잡합니다:

  1. * 문자 제거
  2. 백스페이스/삭제 감지
  3. 새 문자 추가
  4. 2자리 제한

이 로직을 디버깅하거나 테스트해야 한다면:

  • 각 조건문이 어떤 사용자 행동에 대응하는지 명확한가요?
  • Line 16의 조건 typedValue === '' && nextValue.length < cardPassword.cardPassword.length는 정확히 언제 true일까요?
  • Line 21-23의 슬라이싱 로직을 단계별로 추적해보면 모든 경우에 올바르게 동작할까요?

다음 접근을 시도해보세요:

  1. 각 사용자 시나리오를 명시적으로 나열 (빈 상태에서 입력, 한 자리에서 입력, 백스페이스, 전체 선택 후 입력 등)
  2. 각 시나리오에 대한 테스트 케이스 작성
  3. 로직을 더 작은 함수들로 분리 가능한지 검토
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPassword.tsx` around lines 12 - 26, The
handlePasswordChange function is handling masking, deletion and length rules in
a tangled way; simplify by (1) reading current masked input nextValue and
previous real value cardPassword.cardPassword, (2) detect deletion when
nextValue.length < previousMasked.length and update via
setCardPassword.handleCardPassword(previousReal.slice(0, nextValue.length)), (3)
otherwise build the new real value by removing '*' from nextValue (typedValue)
and appending it to previousReal if nextValue includes '*' (or just using
typedValue when fully typed), then clamp to 2 chars and call
setCardPassword.handleCardPassword with that result; refactor this into clear
helper functions used by handlePasswordChange (e.g., isDeletion(nextValue,
previousMasked), extractTyped(nextValue), buildNextPassword(previousReal,
typedValue)) to make behavior for backspace, insertion, select-and-type explicit
and testable, referencing handlePasswordChange,
setCardPassword.handleCardPassword, and cardPassword.cardPassword.

cardCompany === ''
? theme.colors.cardBackground
: theme.colors.cardCompanyBackground[cardCompany],
boxShadow: '3px 3px 5px 0px #00000040;',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

CSS 속성 값에 불필요한 세미콜론이 있습니다.

boxShadow 값 끝에 세미콜론(;)이 포함되어 있습니다. JavaScript 객체의 CSS 속성 값에는 세미콜론이 필요하지 않으며, 이는 잘못된 구문입니다.

다음과 같이 수정하세요:

-        boxShadow: '3px 3px 5px 0px `#00000040`;',
+        boxShadow: '3px 3px 5px 0px `#00000040`',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
boxShadow: '3px 3px 5px 0px #00000040;',
boxShadow: '3px 3px 5px 0px `#00000040`',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/CardPreview.tsx` at line 28, CardPreview.tsx의 style 객체에서
boxShadow 속성 값 끝에 불필요한 세미콜론이 포함되어 있습니다; CardPreview 컴포넌트 내 boxShadow 속성 값을 찾아
문자열 끝의 세미콜론(;)을 제거하여 "3px 3px 5px 0px `#00000040`" 형태로 수정하세요.

}

export interface CardHandler {
handleCardNumbers: (index: number) => (e: React.ChangeEvent<HTMLInputElement>) => void;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) 해당 패턴 사용 위치 확인
rg -n "React\\.ChangeEvent<" src/types/cardStausTypes.ts

# 2) 프로젝트 tsconfig에서 React 타입/JSX 설정 확인
fd -a "tsconfig*.json" -x sh -c 'echo "### $1"; sed -n "1,220p" "$1"' sh {}

# 3) 동일 패턴이 다른 파일에도 있는지 확인
rg -n "React\\.ChangeEvent<" src

Repository: woowacourse/react-payments

Length of output: 1573


🏁 Script executed:

# 1) Check imports in the cardStausTypes.ts file
head -20 src/types/cardStausTypes.ts

# 2) Check tsconfig.app.json for React configuration
cat src/../tsconfig.app.json

# 3) Check how other files import React.ChangeEvent
head -20 src/components/CardPassword.tsx
head -20 src/hooks/useCardNumber.tsx

Repository: woowacourse/react-payments

Length of output: 2562


🏁 Script executed:

# 1) Check if there's a React global declaration file
fd -i "react" src/types | head -20

# 2) Check package.json for React types
grep -A 5 -B 5 "react\|@types" package.json | head -30

# 3) Check if full cardStausTypes.ts file uses React elsewhere
cat src/types/cardStausTypes.ts

# 4) Check if there's a vite.config or other setup that might auto-import
fd "vite.config" -o "config"

Repository: woowacourse/react-payments

Length of output: 2753


React.ChangeEvent 타입에 대한 명시적 import 추가 필요

cardStausTypes.ts 30, 39, 48, 59번 줄에서 React.ChangeEvent를 사용하고 있지만, 파일에 React 타입 import가 없습니다. tsconfig.app.json에서 types 배열에 React 타입이 포함되어 있지 않기 때문에 TypeScript 컴파일 시 Cannot find namespace 'React' 에러가 발생할 수 있습니다.

ChangeEvent 타입을 직접 import하거나 React를 명시적으로 import하여 수정하세요:

import type { ChangeEvent } from 'react';
// 또는
import React from 'react';

그 후 React.ChangeEvent 대신 ChangeEvent를 사용하거나 React.ChangeEvent 형태로 유지할 수 있습니다.

Also applies to: 39-39, 48-48, 59-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/cardStausTypes.ts` at line 30, The file uses React.ChangeEvent
types (e.g., in handleCardNumbers and other handlers referenced on lines 30, 39,
48, 59) but does not import React types, causing "Cannot find namespace
'React'"; fix by adding a top-level type import from React (for example import
type { ChangeEvent } from 'react' or import React from 'react') and then update
the signatures to use the imported ChangeEvent (or keep React.ChangeEvent if you
imported React) so TypeScript can resolve the type.

Comment thread src/utils/util.ts
Comment on lines +4 to +16
export function isMonthError(mode: DateError | MonthError | YearError | null) {
if (
mode === 'emptyBoth' ||
mode === 'emptyMonth' ||
mode === 'notMonthRange' ||
mode === 'notMonthNumber'
) {
return true;
}
if (mode === null || mode === 'emptyYear' || mode === 'notYearNumber') {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

함수가 모든 경로에서 값을 반환하지 않습니다.

isMonthError 함수는 반환 타입이 boolean이지만, 일부 코드 경로에서 명시적으로 값을 반환하지 않습니다. Line 16에서 함수가 종료되는데, if-else 체인이 모든 경우를 처리한다고 가정하고 있지만 명시적인 반환문이 없습니다.

이런 패턴의 문제점:

  • 향후 새로운 에러 타입이 추가되면 예상치 못한 undefined 반환이 발생할 수 있습니다
  • 타입 안정성이 보장되지 않습니다

다음을 고려해보세요:

  • 함수 끝에 기본 반환문을 추가
  • 또는 타입 exhaustiveness 체크를 통해 모든 케이스가 처리되었음을 보장

이 패턴이 안전한지, 개선할 수 있는 방법이 있는지 검토해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/util.ts` around lines 4 - 16, The function isMonthError currently
can fall through without returning; update isMonthError(DateError | MonthError |
YearError | null) to always return a boolean by handling all branches explicitly
— either add a final return (e.g., return false) after the existing conditionals
or replace the branching with an exhaustive switch/never-check that returns true
for 'emptyBoth'|'emptyMonth'|'notMonthRange'|'notMonthNumber', false for
null|'emptyYear'|'notYearNumber', and throws or asserts on any unexpected value;
reference the isMonthError function and the specific literal cases
('emptyBoth','emptyMonth','notMonthRange','notMonthNumber','emptyYear','notYearNumber')
when making the change so no execution path yields undefined.

Comment thread src/utils/util.ts
Comment on lines +18 to +30
export function isYearError(mode: DateError | MonthError | YearError | null) {
if (mode === 'emptyBoth' || mode === 'emptyYear' || mode === 'notYearNumber') {
return true;
}
if (
mode === null ||
mode === 'emptyMonth' ||
mode === 'notMonthRange' ||
mode === 'notMonthNumber'
) {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

함수가 모든 경로에서 값을 반환하지 않습니다.

isYearError 함수도 isMonthError와 동일한 문제가 있습니다. 반환 타입이 boolean이지만 모든 코드 경로에서 명시적으로 값을 반환하지 않습니다.

isMonthError에 대한 리뷰 코멘트를 참고하여 동일한 방식으로 개선해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/util.ts` around lines 18 - 30, The isYearError function currently
misses a return on some branches; update isYearError (and mirror isMonthError)
so it always returns a boolean by adding an explicit default return (or
exhaustive handling) for the remaining cases of DateError | MonthError |
YearError | null; ensure every conditional branch in isYearError returns true or
false (e.g., return false at the end or switch over the union) so the function's
return type is satisfied.

Comment thread src/utils/util.ts
Comment on lines +44 to +48
export function setEmptyBrand(value: string[], setCardBrand: (brand: CardBrand) => void) {
if (value[0] === '') {
setCardBrand('');
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

배열 접근 전에 경계 검사가 필요합니다.

value[0]에 접근하기 전에 배열이 비어있지 않은지 확인하는 로직이 없습니다. 만약 빈 배열이 전달되면 런타임 에러가 발생할 수 있습니다.

방어적 프로그래밍 관점에서 다음을 고려해보세요:

  • value.length > 0 체크를 추가
  • 또는 옵셔널 체이닝(value[0]?.)을 사용
  • 호출하는 쪽에서 이미 보장되는 사항인지 검토

배열 접근의 안전성을 어떻게 보장할 수 있을지 생각해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/util.ts` around lines 44 - 48, The function setEmptyBrand currently
accesses value[0] without checking array bounds; update setEmptyBrand to guard
against empty arrays by verifying value.length > 0 (or using optional chaining
like value[0]?) before comparing to '' and calling setCardBrand, ensuring safe
access to value and preserving the existing behavior of calling setCardBrand('')
when the first element is an empty string; keep references to setEmptyBrand,
setCardBrand, CardBrand and the value parameter so the fix is applied in the
correct function.

Comment thread src/utils/util.ts
Comment on lines +50 to +66
export function setNoExist<T extends string>(
value: string[],
errorMode: T,
setFunc: (mode: T) => void,
): boolean {
if (value[0].slice(0, 1) !== '4' && value[0].slice(0, 1) !== '5') {
setFunc(errorMode);
return true;
}
if (value[0].length === 2) {
if (Number(value[0].slice(0, 2)) < 51 || Number(value[0].slice(0, 2)) > 55) {
setFunc(errorMode);
return true;
}
}
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

카드 브랜드 검증 로직이 불완전하거나 데이터와 불일치합니다.

이 함수는 카드 번호 프리픽스 검증을 수행하는 것으로 보이지만, 몇 가지 문제가 있습니다:

  1. 제한적인 검증 범위: Line 55에서 첫 번째 숫자가 '4' 또는 '5'인 경우만 검증합니다. 하지만 CARD_BRAND_PREFIX_RANGES에는 Diners(36), AMEX(34, 37), UnionPay(622...) 등 다른 브랜드도 정의되어 있습니다.

  2. 하드코딩된 로직: Lines 60-61에서 51-55 범위를 하드코딩하고 있는데, 이는 CARD_BRAND_PREFIX_RANGES의 Mastercard 범위와 중복됩니다. 단일 데이터 소스를 사용하는 것이 더 안전합니다.

  3. 배열 접근 안전성: value[0]value[0].slice()를 경계 검사 없이 사용하고 있습니다.

다음을 고민해보세요:

  • 이 함수가 모든 카드 브랜드를 검증해야 하나요, 아니면 의도적으로 일부만 다루나요?
  • CARD_BRAND_PREFIX_RANGES 상수를 활용하여 검증 로직을 통합할 수 있을까요?
  • 함수명 setNoExist가 의도를 명확하게 전달하나요?

이 함수의 책임과 범위를 명확히 하고, 가능하면 기존 상수 데이터를 재사용하는 방향을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/util.ts` around lines 50 - 66, The setNoExist function currently
assumes value[0] exists and only checks prefixes starting with '4' or '5' with a
hardcoded 51-55 range; update setNoExist to first guard against empty or short
value arrays/strings, then replace the ad‑hoc checks by iterating
CARD_BRAND_PREFIX_RANGES (or the shared prefix constant) to determine if the
provided prefix matches any brand range/values, call setFunc(errorMode) when no
match is found, and keep the function signature (setNoExist) unchanged so
callers work; ensure you reference value[0] safely (length checks) and remove
the hardcoded 51-55 logic in favor of the canonical ranges in
CARD_BRAND_PREFIX_RANGES.

@bigcloud07 bigcloud07 marked this pull request as ready for review May 12, 2026 07:57
Copy link
Copy Markdown

@jw-r jw-r left a comment

Choose a reason for hiding this comment

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

수정 후 다시 리뷰요청 부탁드려요!

화이팅입니다👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants