Skip to content

[2단계 - 페이먼츠 모듈] 에리얼(정예지) 미션 제출합니다.#124

Merged
onschan merged 38 commits intowoowacourse:yeji0214from
yeji0214:step2
May 18, 2025
Merged

[2단계 - 페이먼츠 모듈] 에리얼(정예지) 미션 제출합니다.#124
onschan merged 38 commits intowoowacourse:yeji0214from
yeji0214:step2

Conversation

@yeji0214
Copy link
Copy Markdown
Member

🎯 페이먼츠 모듈

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

1단계 - Module

  • 재사용 가능한 모듈화된 컴포넌트를 개발하고 npm에 배포할 수 있다.
  • 재사용 가능한 커스텀 훅을 개발하고 npm에 배포할 수 있다.
  • Storybook을 사용하여 컴포넌트의 다양한 상태를 시각적으로 문서화하고 테스트할 수 있다.
  • React Testing Library를 사용하여 컴포넌트와 커스텀 훅에 대해 폭넓은 테스트를 작성할 수 있다.

2단계 - Refactoring

  • 모듈화된 컴포넌트의 재사용성 및 확장성을 경험할 수 있다.
  • 요구사항 변경에 따른 컴포넌트 리팩터링 및 개선을 할 수 있다.
  • 선택) 실제 프로젝트에서의 컴포넌트 통합 및 활용할 수 있다.

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

  • 기능 요구 사항(모달 컴포넌트 및 커스텀 훅 개발, npm 배포)을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
  • Storybook과 RTL 테스트 케이스를 모두 작성했나요?
  • 코드 컨벤션을 지키고, 에러 핸들링을 고려했나요?
  • 배포한 npm 패키지가 정상적으로 설치 및 작동하나요?
  • Storybook 데모 페이지에 정상적으로 접근할 수 있나요?

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

온스타 안녕하세요 !
휴일마다 비가 오는 것 같네요 ☔️🐟
이번 리뷰도 잘 부탁드립니다 !

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

✅ 모달 컴포넌트 사용성 고려

이번 미션에서 모달의 형태와 크기에 대한 확장성을 고려하면서, 그만큼 모달이 받는 props의 개수가 늘어났습니다.
필수 입력 값으로 고정하는 값이 생기면 사용자의 사용성이 저해될 것 같다는 생각이 들어서,
대부분의 props를 옵셔널로 설정하여 사용자가 많은 props를 넘기지 않아도 쉽게 사용할 수 있도록 하였습니다.

세 종류의 모달이 모두 확인 버튼이 있고(확인 버튼이 없는 모달을 원하더라도 정상 처리에 관련된 동작을 위한 핸들러는 무조건 필요할 것이라고 생각),
모달이라면 닫는 이벤트는 반드시 필요하다고 판단하여 onCloseonConfirm만 필수로 받도록 하였습니다.


✅ 카드 번호 인풋 개수에 관한 고민

카드사별로 다른 인풋 개수를 제공해야 할지, 하나의 인풋으로 모든 카드 번호를 받아야 할지 고민이 되었습니다.
전자의 경우 구현이 복잡해질 뿐 아니라, 사용자의 입력에 따라 카드사가 달라진다면 계속 인풋 구조가 바뀌어 UX가 오히려 저해될 수도 있다고 판단했습니다.

그래서 인풋을 하나로 고정하되, 카드사에 따라 공백 포맷을 다르게 적용하여 가독성을 높이는 방식을 채택했습니다.


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

❓ 로컬 환경과 배포 환경에서의 레이아웃 차이

모달 컴포넌트를 개발하면서 reset CSS 적용 여부에 따라 레이아웃이 달라지는 문제를 경험했습니다.
로컬 개발 환경에서는 reset이 적용된 상태였지만, 다른 환경(예: Storybook, 외부 프로젝트)에서는 적용되지 않아
margin/padding 등의 기본 브라우저 스타일 차이로 인해 레이아웃 불일치가 발생했습니다.

🧐 reset 없이 브라우저 기본 스타일을 고려하면서 개발하는 게 더 재사용성이나 예측 가능성 측면에서 바람직할까요?


❓ useMemo의 활용 범위

useCardNumberInput 훅에서 formattedCardNumber 등은 매번 렌더링 시 다시 계산되고 있습니다.
이 과정에서 useMemo를 적용하면 불필요한 재계산을 줄일 수 있지 않을까 하는 고민이 들었습니다.

🧐 useMemo는 어느 정도의 연산 비용부터 사용하는 게 적절할까요?
현재처럼 단순한 문자열 조작/검사 수준에서도 성능을 고려해 사용하는 것이 좋은지,
아니면 렌더링에 눈에 띄는 비용이 발생하지 않는 이상, 지나치게 사용하는 것을 피하는 게 더 나은지 궁금합니다.


✅ 리뷰어 체크 포인트

1. 모달 컴포넌트의 유연성

  • 다양한 위치(중앙, 하단 등)와 내용을 prop으로 받아 모달을 유연하게 구성할 수 있는가?
  • 열기, 닫기, 확인 등의 사용자 정의 이벤트 핸들러가 자연스럽게 동작하는가?
  • Storybook을 통해 다양한 시나리오를 명확히 테스트했는가?

2. 커스텀 훅의 역할과 모듈화

  • 카드 정보 입력 유효성 검사 훅이 각각 명확한 책임을 가지도록 분리되어 있는가?
  • 훅이 유효성 검사 결과와 에러 메시지를 명확히 반환하는가?
  • 독립적인 테스트 케이스를 작성하여 정상/비정상 입력 모두를 폭넓게 검증했는가?

3. 모듈 배포

  • npm 배포를 위한 설정이 적절히 구성되어 있는가?

yeji0214 added 23 commits May 7, 2025 15:46
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented May 10, 2025

Summary by CodeRabbit

  • 새로운 기능

    • 모달 컴포넌트에 "alert", "confirm", "prompt" 세 가지 타입과 "small", "medium", "large" 크기 옵션이 추가되어 다양한 상황에 맞게 사용할 수 있습니다.
    • 모달 내 입력 필드(Prompt 타입)와 버튼 커스터마이징 기능이 추가되었습니다.
    • 키보드 포커스 트래핑 등 접근성이 개선되었습니다.
    • 카드 번호 입력 훅이 카드 브랜드 자동 감지, 포맷팅, 유효성 검사, 에러 메시지 제공 등으로 대폭 개선되었습니다.
  • 문서화

    • 모달 컴포넌트 및 카드 번호 입력 훅의 README가 상세 사용법, 예시, 지원 옵션, 반환값 설명 등으로 대폭 보강되었습니다.
    • Storybook에서 다양한 모달 타입과 크기 예제가 추가되고 컨트롤 옵션이 확장되었습니다.
  • 버그 수정

    • 모달의 백드롭 클릭 시 닫힘 기능이 제거되어, 의도치 않은 닫힘을 방지합니다.
  • 리팩터

    • 모달 버튼 컴포넌트가 통합되고, 스타일 및 구조가 개선되었습니다.
    • 카드 번호 입력 훅의 내부 상태 관리 방식이 단순화되었습니다.
  • 테스트

    • 카드 번호 입력 훅의 테스트가 다양한 카드 브랜드 및 엣지 케이스를 포괄하도록 확장되었습니다.
  • 기타

    • 패키지 버전이 업데이트되었습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 모달 컴포넌트에 "alert", "confirm", "prompt" 타입 및 크기(small, medium, large) 옵션이 추가되었습니다.
    • 모달 내 입력 필드 및 버튼 구성이 타입에 따라 다르게 표시됩니다.
    • 키보드 포커스 트랩 기능이 추가되어 접근성이 향상되었습니다.
    • 카드 번호 입력 훅이 단일 입력 필드와 카드 브랜드 자동 인식, 포맷팅, 유효성 검사를 지원합니다.
  • 버그 수정

    • 모달에서 배경 클릭 시 닫히는 동작이 제거되었습니다.
  • 문서화

    • 모달과 카드 번호 입력 훅의 README가 상세 사용법, 예제, API 설명, 지원 브랜드 정보 등으로 대폭 보강되었습니다.
  • 스타일

    • 버튼 스타일이 개선되고, 버튼 컴포넌트가 새롭게 도입되었습니다.
  • 테스트

    • 카드 번호 입력 훅의 다양한 카드 브랜드 및 엣지 케이스에 대한 테스트가 추가 및 개선되었습니다.
  • 기타

    • 패키지 버전이 업데이트되었습니다.

Walkthrough

이번 변경 사항은 @yeji0214/modal 컴포넌트의 모달 기능 확장, 접근성 개선, 다양한 타입 및 크기 지원, 그리고 문서 및 스토리북 스토리의 대대적인 보강에 초점을 맞추고 있습니다. 모달 컴포넌트는 "alert", "confirm", "prompt" 타입을 지원하도록 리팩터링되었고, 크기(small, medium, large)와 취소 버튼 텍스트 등 다양한 옵션이 추가되었습니다. 키보드 포커스 트랩 기능이 도입되어 접근성이 향상되었습니다. 기존의 ConfirmButton 컴포넌트는 삭제되고, 공통 Button 컴포넌트가 새롭게 도입되었습니다. 입력 필드용 Input 컴포넌트도 추가되었습니다. 문서와 스토리북 스토리는 새로운 API와 사용 예시에 맞게 전면적으로 갱신되었습니다.

한편, @yeji0214/hooks 패키지의 카드번호 입력 훅(useCardNumberInput)은 단일 입력 필드 및 브랜드 자동 감지/포맷팅/유효성 검증 방식으로 리팩터링되었습니다. 카드 브랜드별 포맷 및 정규식, 유틸 함수, 에러 메시지 처리 등이 새롭게 도입되었고, 이에 맞춰 테스트와 문서도 모두 갱신되었습니다. 패키지 버전 정보도 각각 업데이트되었습니다.


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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 10

🧹 Nitpick comments (24)
components/src/lib/components/Input/Input.tsx (1)

28-43: Input 컴포넌트의 오류 상태 지원 추가를 고려하세요.

현재 Input 컴포넌트는 오류 상태를 지원하지 않습니다. 유효성 검사를 통한 오류 상태 표시 기능을 추가하면 사용자 경험을 향상시킬 수 있습니다.

다음과 같이 오류 상태를 지원하도록 컴포넌트를 확장하는 것을 고려해보세요:

type InputProps = {
  title: string;
+  error?: string;
};

const Input = ({ title, error }: InputProps) => {
  const inputId = `input-${title.replace(/\s+/g, '-').toLowerCase()}`;
  return (
    <Wrapper>
      <Label htmlFor={inputId}>{title}</Label>
-      <StyledInput id={inputId} />
+      <StyledInput id={inputId} error={!!error} />
+      {error && <ErrorMessage>{error}</ErrorMessage>}
    </Wrapper>
  );
};

// ... 

const StyledInput = styled.input<{ error?: boolean }>`
  width: 100%;
  padding: 8px 12px;
  font-size: 14px;
-  border: 1px solid #ccc;
+  border: 1px solid ${props => props.error ? '#ff0000' : '#ccc'};
  border-radius: 6px;
  box-sizing: border-box;

  background: white;
  color: black;

  &:focus {
    outline: none;
-    border-color: #333;
+    border-color: ${props => props.error ? '#ff0000' : '#333'};
  }
`;

+const ErrorMessage = styled.p`
+  color: #ff0000;
+  font-size: 12px;
+  margin-top: 4px;
+`;
components/src/lib/components/ConfirmButton/ConfirmButton.tsx (2)

17-17: 색상에 대한 상수 또는 테마 변수 사용을 고려하세요.

컬러 코드 #333333를 하드코딩하는 대신, 테마 시스템이나 상수를 사용하면 일관성 있는 디자인과 향후 쉬운 변경이 가능합니다.

색상을 테마 객체나 상수로 분리하는 것이 좋습니다:

+ const COLORS = {
+   confirmButton: '#333333',
+   confirmText: 'white',
+ };

const Button = styled.button`
  width: 100%;
  padding: 8px 16px;
-  background-color: #333333;
+  background-color: ${COLORS.confirmButton};
-  color: white;
+  color: ${COLORS.confirmText};
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
`;

3-11: 버튼 컴포넌트 확장성 개선이 필요합니다.

현재 ConfirmButton은 제한된 스타일 옵션만 제공합니다. 재사용 가능한 버튼으로서 더 많은 유연성을 제공하는 것이 좋습니다.

버튼 크기, 비활성화 상태 등의 지원을 추가할 수 있습니다:

type ConfirmButtonProps = {
  confirmText: string;
  onClick: () => void;
+  disabled?: boolean;
+  size?: 'small' | 'medium' | 'large';
};

-const ConfirmButton = ({ confirmText, onClick }: ConfirmButtonProps) => {
-  return <Button onClick={onClick}>{confirmText}</Button>;
+const ConfirmButton = ({ 
+  confirmText, 
+  onClick, 
+  disabled = false,
+  size = 'medium'
+}: ConfirmButtonProps) => {
+  return (
+    <Button 
+      onClick={onClick} 
+      disabled={disabled} 
+      size={size}
+    >
+      {confirmText}
+    </Button>
+  );
};

그리고 styled 컴포넌트를 확장:

-const Button = styled.button`
+const Button = styled.button<{ size: 'small' | 'medium' | 'large' }>`
-  width: 100%;
+  width: ${({ size }) => size === 'small' ? 'auto' : '100%'};
-  padding: 8px 16px;
+  padding: ${({ size }) => {
+    switch(size) {
+      case 'small': return '4px 8px';
+      case 'large': return '12px 24px';
+      default: return '8px 16px';
+    }
+  }};
  background-color: #333333;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
-  font-size: 14px;
+  font-size: ${({ size }) => size === 'large' ? '16px' : '14px'};
+  opacity: ${({ disabled }) => disabled ? 0.6 : 1};
+  cursor: ${({ disabled }) => disabled ? 'not-allowed' : 'pointer'};
`;
components/package.json (1)

3-3: 버전 관리 정책에 대해 고려하세요.

버전이 0.0.20에서 0.0.29로 크게 증가했습니다. 이는 여러 번의 패치 릴리스가 있었음을 의미합니다. 의미있는 버전 관리(Semantic Versioning)를 사용하여 메이저, 마이너, 패치 버전을 적절히 관리하는 것이 좋습니다.

기능 추가가 있다면 0.1.0과 같은 마이너 버전으로 올리는 것을 고려하세요.

components/src/App.tsx (1)

24-25: 주석 처리된 코드는 제거하거나 문서화하는 것이 좋습니다.

size="medium" prop이 주석 처리되어 있습니다. 이는 코드의 명확성을 떨어뜨립니다. 만약 이 prop이 중요하다면 활성화하고, 그렇지 않다면 완전히 제거하는 것이 좋습니다.

-          // size="medium"

또는 왜 주석 처리되었는지 설명을 추가하세요:

+          // size prop은 필요시 활성화할 수 있음 (기본값: medium)
components/src/lib/components/CancelButton/CancelButton.tsx (1)

8-10: 버튼 타입 지정 및 추가 속성 전달을 허용해 주세요

폼 내부에서 <button> 요소는 기본적으로 type="submit"으로 동작합니다. 모달 안에 포함될 여지가 높은 CancelButton이 의도치 않은 폼 제출을 유발하지 않도록 type="button"을 명시하고, 향후 disabled, aria- 등의 속성을 전달할 수 있도록 나머지 프로퍼티를 확장하는 편이 좋습니다.

-type CancelButtonProps = {
-  cancelText: string;
-  onClick: () => void;
-};
+type CancelButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
+  cancelText: string;
+  onClick: () => void;
+};

-const CancelButton = ({ cancelText, onClick }: CancelButtonProps) => {
-  return <Button onClick={onClick}>{cancelText}</Button>;
+const CancelButton = ({
+  cancelText,
+  onClick,
+  type = "button",
+  ...rest
+}: CancelButtonProps) => {
+  return (
+    <Button type={type} onClick={onClick} {...rest}>
+      {cancelText}
+    </Button>
+  );
+};
components/README.md (2)

41-43: 예시 코드 내 핸들러 이름 오타

handelClosehandleClose로 수정해 주세요. 예제 코드의 사소한 오타도 사용자를 혼란스럽게 할 수 있습니다.


102-103: size 기본값 표기 오타

"lage""large" 로 수정해야 합니다. 타이핑 오류가 문서 검색 시 필터링을 방해할 수 있습니다.

components/src/stories/Modal.stories.ts (2)

108-119: 다양한 크기와 위치 조합의 예제가 잘 구성되었습니다

다양한 크기("small", "medium", "large")와 위치("center", "top", "bottom")의 조합을 보여주는 예제들이 잘 구성되어 있습니다. 이를 통해 사용자는 여러 상황에 맞는 모달 설정을 쉽게 찾을 수 있습니다.

다만, LargePromptModal 예제에서는 titlecontent 속성이 사용되고 있는데, 이는 앞서 argTypes 설명에서 "prompt 타입 제외"라고 명시된 것과 일치하지 않습니다. 일관성을 위해 이 부분을 수정하거나, prompt 타입에서도 이 속성들을 사용할 수 있다면 argTypes의 설명을 수정하는 것이 좋겠습니다.

- title: "빅 모달",
- content: "이것은 큰 크기의 입력 모달입니다.",
+ inputTitle: "이름을 입력해주세요",

또한, 모든 예제에서 hasCloseButtontrue로 설정되어 있는데, false인 경우의 예제도 추가하면 컴포넌트의 다양한 상태를 더 잘 보여줄 수 있을 것입니다.

Also applies to: 121-133, 135-147, 149-161


1-162: 개발자의 사고 과정 분석

코드의 변경 패턴을 분석해보면, 개발자가 모달 컴포넌트를 확장하는 과정에서 체계적인 접근법을 취했음을 알 수 있습니다:

  1. 먼저 모달의 기본 타입("alert", "confirm", "prompt")을 정의하고, 각 타입에 필요한 속성들을 명확히 했습니다.
  2. 다음으로 위치와 크기 옵션을 추가하여 다양한 상황에 유연하게 대응할 수 있게 했습니다.
  3. 마지막으로 다양한 조합의 예제를 만들어 컴포넌트의 유연성을 보여주었습니다.

이러한 접근 방식은 React 컴포넌트 설계의 모범 사례와 일치합니다. 특히 컴포넌트의 API를 확장하면서 기존 사용 방식과의 호환성을 유지한 점이 인상적입니다.

하지만 개선할 점으로는, 타입과 관련된 속성들(title, content, inputTitle)의 사용 규칙이 예제와 설명 사이에 일관성이 부족한 점을 들 수 있습니다. 이는 개발 과정에서 요구사항이 변경되었거나, 문서와 코드 간의 동기화가 완전히 이루어지지 않았을 가능성을 시사합니다.

ReactJS 공식 문서의 Composition vs Inheritance 섹션에서 언급하는 것처럼, 컴포넌트의 유연성과 재사용성을 높이기 위해 합성(composition)을 활용하는 것이 좋습니다. 현재의 Modal 컴포넌트 설계는 이러한 원칙을 잘 따르고 있으며, 다양한 상황에 적응할 수 있는 유연한 API를 제공하고 있습니다.

hooks/package.json (1)

3-3: 패키지 버전 관리에 관한 제안

버전이 0.0.2에서 0.0.5로 변경된 것을 확인했습니다. 의미론적 버전 관리(Semantic Versioning) 원칙에 따라 버전을 관리하는 것을 권장합니다. 기능 추가는 마이너 버전(0.1.0)을, 버그 수정은 패치 버전(0.0.x)을 증가시키는 것이 좋습니다.

다음 릴리스에서는 변경 내용의 중요도에 따라 0.1.0과 같은 마이너 버전 업데이트를 고려해보세요. 자세한 내용은 semver.org를 참조하세요.

hooks/src/lib/hooks/constants/cardBrandInfo.ts (1)

1-10: 인터페이스 정의와 상수 확장성 개선

카드 브랜드별 포맷팅을 위한 상수 정의는 좋은 구조입니다. 그러나 타입 안전성과 확장성을 더 강화할 수 있습니다.

TypeScript의 장점을 더 활용하기 위해 브랜드명에 대한 타입 정의를 추가하고, 내보내기 방식을 조정하세요:

-const CARD_BRAND_FORMAT: Record<string, number[]> = {
+export type CardBrand = 'VISA' | 'MASTERCARD' | 'AMEX' | 'DINERS' | 'UNIONPAY' | 'DEFAULT';

+export const CARD_BRAND_FORMAT: Record<CardBrand, number[]> = {
  VISA: [4, 4, 4, 4],
  MASTERCARD: [4, 4, 4, 4],
  AMEX: [4, 6, 5],
  DINERS: [4, 6, 4],
  UNIONPAY: [4, 4, 4, 4],
  DEFAULT: [4, 4, 4, 4],
};

-export default CARD_BRAND_FORMAT;

이렇게 하면 타입 검사가 향상되고, 다른 파일에서 CardBrand 타입을 재사용할 수 있습니다.

hooks/src/lib/hooks/utils/validate.ts (1)

30-42: 검증 결과 객체 구조가 일관성 있게 설계되었습니다만, 타입 불일치에 주의하세요.

유효성 검증 결과 객체에서 유효하지 않은 경우 idx: 0을, 유효한 경우 idx: null을 반환하는 패턴은 타입 불일치로 인해 혼란을 줄 수 있습니다. 다음과 같이 개선할 수 있습니다:

  validateFirstCardNumbers(number: string) {
    if (!validator.isValidCardStartNumber(number))
      return {
        isValidCardCompany: false,
-       idx: 0,
+       idx: 0, // Consider using a consistent type, e.g., number | null -> number
        helperText: ERROR_MESSAGE.CARD_NUMBER.INVALID,
      };
    return {
      isValidCardCompany: true,
      idx: null,
      helperText: "",
    };
  },

타입스크립트의 타입 안전성을 유지하려면 idx 속성의 타입을 명시적으로 정의하는 인터페이스를 만드는 것이 좋습니다.

hooks/src/lib/hooks/utils/cardBrandUtils.ts (1)

1-14: 카드사별 최대 길이 정의가 깔끔하게 구현되었습니다.

카드사 그룹화와 switch 문을 통한 최대 길이 정의가 명확합니다. 다만, 이 상수 값들은 다른 곳에서도 재사용될 가능성이 높으므로 상수 파일로 분리하는 것을 고려해보세요.

+// 별도의 상수 파일로 분리 예시: constants/cardBrandInfo.ts
+export const CARD_LENGTH = {
+  AMEX: 15,
+  DINERS: 14,
+  UNIONPAY: 16,
+  VISA: 16,
+  MASTERCARD: 16,
+  DEFAULT: 19
+};

export const getCardNumberMaxLength = (brand: string | null): number => {
  switch (brand) {
    case "AMEX":
-     return 15;
+     return CARD_LENGTH.AMEX;
    // 나머지 케이스도 동일하게 적용

이렇게 하면 카드 길이 정보가 필요한 다른 위치에서도 일관되게 재사용할 수 있습니다.

hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)

1-23: 카드사 감지 로직이 포괄적이고 명확합니다.

다양한 카드사를 식별하기 위한 정규식 패턴이 잘 구현되어 있습니다. 다만, 이러한 패턴들은 중앙화된 상수 파일로 분리하면 유지보수성이 더 향상될 것입니다:

+// constants/cardBrandPatterns.ts
+export const CARD_PATTERNS = {
+  VISA: /^4/,
+  MASTERCARD: /^5[1-5]/,
+  DINERS: /^36/,
+  AMEX: /^3[47]/,
+  // 다른 패턴들...
+};

export const detectCardCompany = (firstDigits: string): string | null => {
  const length = firstDigits.length;

-  if (length >= 1 && /^4/.test(firstDigits)) return "VISA";
+  if (length >= 1 && CARD_PATTERNS.VISA.test(firstDigits)) return "VISA";
  // 나머지 조건들도 동일하게 적용

이렇게 하면 패턴이 변경되거나 새로운 카드사가 추가될 때 한 곳에서만 수정하면 되어 유지보수가 더 쉬워집니다.

hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.test.ts (4)

45-96: 다양한 카드사에 대한 테스트가 포괄적입니다.

DINERS, AMEX, UNIONPAY 등 다양한 카드사에 대한 테스트가 잘 구현되어 있습니다. 특히 UNIONPAY와 같이 복잡한 패턴을 가진 카드사에 대해 다양한 케이스를 테스트하는 것이 인상적입니다.

테스트 케이스의 가독성을 더 높이기 위해 테스트 설명에 더 구체적인 내용을 추가하는 것이 좋습니다:

-test.each([
+test.each([
  ["6221261234567890", "6221 2612 3456 7890"],
  ["6229251234567890", "6229 2512 3456 7890"],
  // ...
-])("UNIONPAY (%s)", (input, expectedFormat) => {
+])("UNIONPAY: 입력값 %s는 %s로 포맷팅되어야 함", (input, expectedFormat) => {

이렇게 하면 테스트 실패 시 더 명확한 오류 메시지를 얻을 수 있습니다.


98-140: 엣지 케이스 테스트가 잘 구현되어 있습니다.

불완전한 카드 번호, 너무 긴 입력값, 지원되지 않는 카드사와 같은 엣지 케이스에 대한 테스트가 포괄적으로 구현되어 있습니다. 이는 훅의 견고성을 보장하는 데 매우 중요합니다.

다만, 모든 엣지 케이스에 대해 명확한 메시지를 추가하면 테스트 목적이 더 명확해질 것입니다:

-it("카드사 조건은 충족했지만, 요구 길이 만큼 숫자를 입력하지 않은 경우", () => {
+it("카드사 조건은 충족했지만 요구 길이를 충족하지 않으면 유효하지 않음을 반환해야 함", () => {

또한 전체적인 테스트 커버리지를 높이기 위해 입력값에 비숫자 문자가 포함된 경우나 포맷팅 로직에 대한 더 세부적인 테스트 케이스를 추가하는 것이 좋을 것 같습니다.


5-11: 테스트 구조와 초기 상태 검증이 개선되었습니다.

훅의 초기 상태가 예상대로 동작하는지 확인하는 테스트가 적절하게 구현되어 있습니다. 테스트 구조도 논리적으로 잘 나뉘어져 있어 가독성이 좋습니다.

한 가지 개선할 점은 cardNumberformattedCardNumber의 초기 상태도 명시적으로 검증하는 것입니다:

it("초기 상태는 빈 문자열이며 유효하지 않음", () => {
  const { result } = renderHook(() => useCardNumberInput());

  expect(result.current.isValid).toEqual(false);
+ expect(result.current.cardNumber).toEqual("");
+ expect(result.current.formattedCardNumber).toEqual("");
+ expect(result.current.cardBrand).toBeNull();
  expect(result.current.errorMessage).toBe("");
});

이렇게 하면 훅의 모든 반환 값이 초기 상태에서 예상대로 설정되었는지 확인할 수 있습니다.


1-4: 리뷰 종합: 카드 번호 처리 모듈의 개선된 구현

이번 PR에서는 카드 번호 입력과 유효성 검증을 위한 기능들이 크게 개선되었습니다. 특히 단일 입력 필드를 사용하고 카드사를 자동으로 감지하는 방식은 사용자 경험을 크게 향상시킬 것입니다. 전반적인 구현이 견고하고 테스트도 포괄적입니다.

학습 포인트:

  1. React 훅과 유틸리티 함수를 적절히 분리하여 관심사 분리가 잘 이루어졌습니다
  2. 정규식을 활용한 카드사 패턴 감지는 효율적인 접근법입니다
  3. 비즈니스 로직(카드 유효성 검증)을 명확하게 모듈화한 점이 인상적입니다

다음과 같은 개선 사항을 고려해보세요:

  • 하드코딩된 상수와 패턴을 별도의 파일로 분리하여 유지보수성 향상
  • useCardBrand 훅을 더 의미 있는 React 훅으로 확장
  • 일부 타입 불일치(null vs 숫자)를 개선하여 타입 안전성 강화
  • Luhn 알고리즘 등의 체크섬 검증으로 카드 유효성 검증 강화
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (2)

31-47: 성능 최적화를 위한 useMemo 적용을 고려해보세요.

formattedNumber 함수는 카드 번호가 변경될 때마다 호출되며, 포맷팅 로직(특히 여러 카드 브랜드를 처리하는 경우)이 복잡할 수 있습니다. 이를 useMemo로 최적화하면 불필요한 재계산을 방지할 수 있습니다.

- const formattedNumber = (): string => {
+ const formattedCardNumber = useMemo((): string => {
    if (!cardBrand) return cardNumber;

    const format = CARD_BRAND_FORMAT[cardBrand] || CARD_BRAND_FORMAT.DEFAULT;
    const digits = cardNumber.replace(/\D/g, "");
    const groups = [];
    let cursor = 0;

    for (const len of format) {
      groups.push(digits.slice(cursor, cursor + len));
      cursor += len;
    }

    return groups.filter(Boolean).join(" ");
- };
+ }, [cardNumber, cardBrand]);

- const formattedCardNumber = formattedNumber();

61-61: 반복되는 정규식은 상수로 추출하세요.

/\D/g 정규식이 여러 곳에서 반복되고 있습니다. 이를 상수로 추출하면 코드 가독성이 향상되고 유지보수가 용이해집니다. React 컴포넌트 외부에 상수를 정의해 불필요한 재생성을 방지하세요.

+ const NON_DIGIT_REGEX = /\D/g;

const useCardNumberInput = (): UseCardNumberInputResult => {
  // ...

  const formattedNumber = (): string => {
    if (!cardBrand) return cardNumber;

    const format = CARD_BRAND_FORMAT[cardBrand] || CARD_BRAND_FORMAT.DEFAULT;
-   const digits = cardNumber.replace(/\D/g, "");
+   const digits = cardNumber.replace(NON_DIGIT_REGEX, "");
    // ...
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-   const raw = e.target.value.replace(/\D/g, "");
+   const raw = e.target.value.replace(NON_DIGIT_REGEX, "");
    // ...
  };

Also applies to: 35-35

hooks/README.md (3)

37-45: useCardNumberInput 훅의 반환값 사용이 명확하게 개선되었습니다.

이전 분리된 입력 필드 관리 방식에서 단일 입력으로의 전환이 예제 코드에 잘 반영되었습니다. 이는 훅 API의 간소화를 보여주는 좋은 예시입니다. 다만, 각 반환값의 구체적인 사용 목적을 주석으로 추가하면 사용자가 더 쉽게 이해할 수 있을 것입니다.

  const {
    cardNumber,        // 원본 카드 번호 (숫자만)
    formattedCardNumber, // 포맷팅된 카드 번호 (화면에 표시용)
    cardBrand,         // 감지된 카드사 정보
    isValid,           // 카드 번호 유효성 여부
    errorMessage,      // 오류 메시지
    handleChange,      // 입력 이벤트 핸들러
  } = useCardNumberInput();

49-58: 카드 번호 입력 예제를 풍부하게 보완할 수 있습니다.

현재 예제는 기본적인 사용법은 잘 보여주고 있지만, 다음과 같은 실용적인 내용을 추가하면 더욱 유용할 것입니다:

  1. 카드 브랜드에 따라 다른 스타일링을 적용하는 예시
  2. 카드 브랜드 로고를 표시하는 방법
  3. 유효하지 않은 입력에 대한 시각적 피드백
// 카드 브랜드에 따른 스타일링 예시
{cardBrand && (
  <div className={`card-brand ${cardBrand.toLowerCase()}`}>
    <img 
      src={`/images/${cardBrand.toLowerCase()}.svg`} 
      alt={`${cardBrand} 로고`} 
    />
    {cardBrand}
  </div>
)}

37-59: 개발자의 사고 과정이 문서에 잘 반영되었습니다.

API 설계 변화에서 개발자의 사고 과정이 잘 드러납니다. 초기에는 분리된 입력 필드 관리를 위한 복잡한 상태 구조를 사용했다가, 사용자 경험과 구현 복잡성을 고려하여 단일 입력 방식으로 전환한 것으로 보입니다.

특히 카드 브랜드 자동 감지와 그에 따른 동적 포맷팅은 사용자 경험을 개선하면서도 API는 단순화하는 균형 잡힌 접근법을 보여줍니다. 이는 React 생태계에서 권장하는 "사용하기 쉬운 API, 구현은 복잡하더라도" 원칙을 잘 따르고 있습니다.

하나 제안하자면, 구현 과정에서 맞닥뜨린 도전과 해결 방법에 대한 짧은 설명(예: "왜 단일 입력 방식을 선택했는지")을 추가하면 다른 개발자들에게 더 유용한 인사이트를 제공할 수 있을 것입니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba7d520 and 1d0a04d.

⛔ Files ignored due to path filters (2)
  • components/package-lock.json is excluded by !**/package-lock.json
  • hooks/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • components/README.md (3 hunks)
  • components/package.json (2 hunks)
  • components/src/App.tsx (1 hunks)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
  • components/src/lib/components/CancelButton/CancelButton.tsx (1 hunks)
  • components/src/lib/components/ConfirmButton/ConfirmButton.tsx (1 hunks)
  • components/src/lib/components/Input/Input.tsx (1 hunks)
  • components/src/stories/Modal.stories.ts (2 hunks)
  • hooks/README.md (1 hunks)
  • hooks/package.json (1 hunks)
  • hooks/src/App.tsx (1 hunks)
  • hooks/src/lib/hooks/constants/cardBrandInfo.ts (1 hunks)
  • hooks/src/lib/hooks/constants/cardValidationInfo.ts (1 hunks)
  • hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1 hunks)
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.test.ts (1 hunks)
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1 hunks)
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts (1 hunks)
  • hooks/src/lib/hooks/utils/validate.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/components/ConfirmButton/ConfirmButton.tsx
  • components/src/lib/components/Input/Input.tsx
  • hooks/src/App.tsx
  • components/src/lib/components/CancelButton/CancelButton.tsx
  • components/src/App.tsx
  • components/src/lib/Modal/Modal.tsx
`**/*.ts`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, re...

**/*.ts: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • hooks/src/lib/hooks/constants/cardValidationInfo.ts
  • hooks/src/lib/hooks/constants/cardBrandInfo.ts
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.test.ts
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts
  • hooks/src/lib/hooks/useCardBrand/useCardBrand.ts
  • hooks/src/lib/hooks/utils/validate.ts
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts
  • components/src/stories/Modal.stories.ts
🧬 Code Graph Analysis (1)
hooks/src/lib/hooks/utils/validate.ts (1)
hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)
  • detectCardCompany (1-23)
🔇 Additional comments (12)
components/package.json (1)

39-39: prepublishOnly 스크립트 추가는 좋은 개선입니다.

배포 전 자동으로 빌드 과정을 실행하는 prepublishOnly 스크립트를 추가한 것은 매우 좋은 개선입니다. 이는 배포된 패키지가 항상 최신 빌드를 포함하도록 보장합니다.

components/src/App.tsx (1)

23-28: 모달 컴포넌트의 간소화된 API는 좋은 개선입니다.

이전에는 여러 개의 prop을 명시적으로 제공해야 했던 반면, 이제는 type="confirm"과 필수 핸들러만 제공하면 됩니다. 이는 사용성을 크게 향상시킵니다.

components/src/stories/Modal.stories.ts (2)

16-54: 모달 타입에 따른 속성 구성이 잘 정의되었습니다

모달 컴포넌트의 type, position, size 등의 속성에 대한 제어 옵션이 명확하게 정의되어 있어 좋습니다. 특히 각 속성에 대한 설명과 기본값을 지정한 점이 사용자 경험을 향상시킵니다.

한 가지 더 개선할 점이 있다면, 모달 타입별로 어떤 속성이 필수이고 어떤 속성이 선택적인지를 문서에 명확히 하면 더 좋을 것 같습니다. 예를 들어, titlecontent는 "prompt" 타입에서는 사용되지 않는다고 설명되어 있지만, 이후 LargePromptModal 예제에서는 이 속성들이 사용되고 있습니다.


68-79: 모달 타입별 예제가 체계적으로 구성되었습니다

세 가지 모달 타입("alert", "confirm", "prompt")에 대한 예제가 명확하게 구성되어 있어 사용자가 각 타입의 차이점을 이해하기 쉽습니다. 특히 각 타입에 맞는 적절한 속성(confirmText, cancelText, inputTitle 등)이 설정되어 있어 좋습니다.

이벤트 핸들러(onClose, onConfirm)를 빈 함수(() => {})로 처리한 것은 Storybook에서는 적절하지만, 실제 사용 예시를 주석으로 추가하면 개발자들에게 더 도움이 될 것 같습니다. 예를 들어:

// 실제 사용 예시:
// onConfirm: (value?: string) => {
//   if (value) {
//     console.log(`입력된 값: ${value}`);
//   }
// }

Also applies to: 81-93, 95-106

hooks/src/App.tsx (1)

3-42: 카드 입력 상태 피드백 및 개발자 사고 과정 분석

카드 번호 입력 시 즉각적인 피드백 제공이 잘 구현되어 있습니다. 개발 과정에서 다중 입력 필드에서 단일 입력 필드로 전환한 접근 방식이 인상적입니다.

개발자가 기존 분리된 입력 방식에서 단일 입력 필드와 자동 포맷팅으로 전환한 것은 이상적인 리팩토링 패턴을 보여줍니다. 사용자 경험 개선을 위해 카드 브랜드 감지와, 유효성 검사를 통합한 접근은 React 모범 사례에 부합합니다.

이 접근은 모듈식 설계와 책임 분리 원칙을 잘 구현하고 있으며, 커스텀 훅 내부에 복잡한 로직을 캡슐화하여 컴포넌트를 간결하게 유지했습니다. React 팀이 권장하는 "관심사의 분리" 패턴이 잘 적용되어 있습니다.

hooks/src/lib/hooks/utils/validate.ts (1)

25-28: 카드사 번호 유효성 검증 방식이 명확합니다.

isValidCardStartNumber 함수는 detectCardCompany를 효과적으로 활용하여 카드 번호의 시작 숫자가 유효한 카드사 패턴과 일치하는지 확인합니다. 이는 코드 재사용성 측면에서 좋은 접근법입니다.

hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.test.ts (2)

13-26: 테스트 케이스가 명확하게 작성되었습니다.

유효한 VISA 카드 번호에 대한 테스트가 잘 구현되어 있습니다. 포맷팅, 에러 메시지, 카드사 식별이 모두 적절하게 검증되고 있습니다.


28-43: test.each를 사용한 테스트 패턴이 효율적입니다.

여러 테스트 케이스를 test.each로 구현하여 코드 중복을 줄인 접근 방식이 매우 좋습니다. 마스터카드 테스트 케이스가 다양한 패턴을 포괄하고 있어 테스트의 견고성이 높습니다.

hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (2)

23-28: 새로 추가된 카드 브랜드 감지 및 유효성 검사 기능은 좋은 개선입니다.

카드 번호 입력에서 브랜드를 자동으로 감지하고 해당 브랜드의 규칙에 따라 유효성을 검사하는 로직으로 변경한 것은 사용자 경험을 크게 향상시키는 좋은 접근법입니다. 분리된 유틸리티 함수들도 재사용성과 가독성 측면에서 잘 설계되었습니다. 👍


1-8: React 커스텀 훅의 마인드셋 변화가 잘 보입니다.

코드를 살펴보면 개발자의 사고 과정이 잘 드러납니다. 초기에는 분리된 입력 필드와 상태로 접근했다가, 사용자 경험과 유지보수성을 고려하여 단일 상태로 리팩토링하는 방향으로 전환한 것으로 보입니다.

특히 카드 브랜드 감지, 카드 번호 유효성 검사, 브랜드별 포맷팅 등의 복잡한 로직을 분리된 모듈로 추출하여 응집도를 높이고, 해당 관심사에 따라 코드를 구성한 점이 인상적입니다. 이는 React의 컴포지션 철학과 단일 책임 원칙을 잘 따른 접근법입니다.

다음 단계로 고려할 점은 테스트 용이성과 성능 최적화입니다. useMemouseCallback을 전략적으로 사용하고, 순수 함수를 활용하여 코드의 테스트 용이성과 예측 가능성을 더욱 높일 수 있을 것입니다.

hooks/README.md (2)

77-88: 카드사 지원 정보 섹션이 매우 명확하게 작성되었습니다.

지원되는 카드사, 식별 조건, 포맷 구조 등을 표 형식으로 정리한 것이 매우 유용합니다. 이러한 명확한 문서화는 훅의 사용자가 카드 번호 처리 로직을 이해하는 데 큰 도움이 됩니다. 또한 경고 메시지를 통해 입력 제한사항과 오류 처리를 명확히 설명한 점도 좋습니다.

이 섹션은 모듈의 사용자에게 정확한 정보를 제공하고 있으며, 카드 번호 처리의 복잡성을 잘 설명하고 있습니다. 👍


66-74: 반환값 테이블이 매우 자세하게 업데이트되었습니다.

새로운 API 구조에 맞게 반환값 테이블을 명확하게 업데이트하여 각 값의 유형과 용도를 잘 설명하고 있습니다. 특히 formattedCardNumbercardBrand의 설명이 구체적이어서 사용자가 이해하기 쉽습니다.

훅의 사용자가 API를 이해하고 활용하는 데 필요한 모든 정보가 깔끔하게 정리되어 있어, 문서화 측면에서 매우 잘 작성되었습니다. 이는 코드의 변화가 문서에도 일관되게 반영된 좋은 예시입니다.

Comment on lines +22 to +26
const Label = styled.p`
margin-top: 20px;
margin-bottom: 8px;
font-size: 14px;
`;
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

의미론적으로 올바른 HTML 요소 사용이 필요합니다.

Label 컴포넌트가 p 태그 대신 label 태그로 구현되어야 합니다. 이는 의미론적으로 더 정확하며 접근성을 향상시킵니다.

-const Label = styled.p`
+const Label = styled.label`
  margin-top: 20px;
  margin-bottom: 8px;
  font-size: 14px;
`;
📝 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
const Label = styled.p`
margin-top: 20px;
margin-bottom: 8px;
font-size: 14px;
`;
const Label = styled.label`
margin-top: 20px;
margin-bottom: 8px;
font-size: 14px;
`;
🤖 Prompt for AI Agents (early access)
In components/src/lib/components/Input/Input.tsx around lines 22 to 26, the
Label component is currently implemented using a `p` tag, which is semantically
incorrect. Change the styled component to use a `label` tag instead of `p` to
improve semantic correctness and accessibility.

Comment on lines +3 to +5
type InputProps = {
title: string;
};
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

Input 컴포넌트의 속성을 확장하는 것이 필요합니다.

현재 Input 컴포넌트는 title 속성만 받고 있어, 사용자 입력값을 제어하거나 상태를 관리할 수 없습니다. 재사용 가능한 컴포넌트로 더 많은 사용 사례를 지원하기 위해 value, onChange, placeholder, type 등 추가 속성을 허용하는 것이 좋습니다.

type InputProps = {
  title: string;
+  value?: string;
+  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+  placeholder?: string;
+  type?: string;
};
📝 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
type InputProps = {
title: string;
};
type InputProps = {
title: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
type?: string;
};
🤖 Prompt for AI Agents (early access)
In components/src/lib/components/Input/Input.tsx around lines 3 to 5, the
InputProps type currently only includes a title property, limiting the
component's usability. Extend the InputProps type to include common input
attributes such as value, onChange, placeholder, and type to allow controlled
input behavior and better state management. This will make the Input component
more reusable and flexible for various use cases.

Comment on lines +7 to +14
const Input = ({ title }: InputProps) => {
return (
<Wrapper>
<Label>{title}</Label>
<StyledInput />
</Wrapper>
);
};
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

접근성을 향상시키기 위한 label과 input 연결이 필요합니다.

현재 구현에서는 Label이 실제 요소가 아닌

태그로 구현되어 있고, input과 연결되어 있지 않습니다. 이는 스크린 리더 사용자에게 접근성 문제를 일으킬 수 있습니다.

const Input = ({ title }: InputProps) => {
+  const inputId = `input-${title.replace(/\s+/g, '-').toLowerCase()}`;
  return (
    <Wrapper>
-      <Label>{title}</Label>
-      <StyledInput />
+      <Label htmlFor={inputId}>{title}</Label>
+      <StyledInput id={inputId} />
    </Wrapper>
  );
};
📝 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
const Input = ({ title }: InputProps) => {
return (
<Wrapper>
<Label>{title}</Label>
<StyledInput />
</Wrapper>
);
};
const Input = ({ title }: InputProps) => {
const inputId = `input-${title.replace(/\s+/g, '-').toLowerCase()}`;
return (
<Wrapper>
<Label htmlFor={inputId}>{title}</Label>
<StyledInput id={inputId} />
</Wrapper>
);
};
🤖 Prompt for AI Agents (early access)
In components/src/lib/components/Input/Input.tsx around lines 7 to 14, the Label
component is currently rendered as a <p> tag and is not associated with the
input element, causing accessibility issues. Change the Label to render as a
proper <label> element and link it to the input by adding a unique id to the
input and setting the label's htmlFor attribute to that id. This will ensure
screen readers correctly associate the label with the input field.

Comment on lines 91 to +97
<ModalTitle>{title}</ModalTitle>
<CloseButtonWrapper>
{hasCloseButton && <CloseButton onClose={onClose} />}
</CloseButtonWrapper>
</ModalHeader>
<ModalContent>{content}</ModalContent>
{type !== "prompt" && <ModalContent>{content}</ModalContent>}

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

문서와 구현 불일치 & UI 혼선 해결 필요

  1. README에는 prompt 타입에서 제목이 보이지 않는다고 명시되어 있으나, 현재 구현은 항상 title을 렌더링합니다.
  2. sizeMap 값이 README 표( small : 304px, medium : 600px, large : 100% )와 다릅니다. 특히 large 가 70 %로 제한되어 있어 전체 폭 사용이 불가능합니다.
-<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+<ModalHeader>
+  {type !== "prompt" && <ModalTitle>{title}</ModalTitle>}
   <CloseButtonWrapper>
     {hasCloseButton && <CloseButton onClose={onClose} />}
   </CloseButtonWrapper>
 </ModalHeader>
 const sizeMap: Record<"small" | "medium" | "large", string> = {
   small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
 };

위와 같이 수정하면 문서와 컴포넌트가 일치하고, 사용자-입장에서 예측 가능한 동작을 제공합니다.

Also applies to: 137-140

🤖 Prompt for AI Agents (early access)
In components/src/lib/Modal/Modal.tsx around lines 91 to 97 and also lines 137
to 140, the implementation always renders the title regardless of the modal
type, which conflicts with the README stating the title should be hidden for
"prompt" type. Update the code to conditionally render the title only when the
type is not "prompt". Additionally, adjust the sizeMap values to match the
README specifications by setting small to 304px, medium to 600px, and large to
100% width instead of the current 70% for large. This will align the component
behavior with the documentation and improve UI consistency.

Comment on lines +3 to +42
function App() {
const {
formattedCardNumber,
cardBrand,
isValid,
errorMessage,
handleChange,
} = useCardNumberInput();

return (
<div style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h2>카드 번호 입력</h2>

<input
type="text"
value={formattedCardNumber}
onChange={handleChange}
placeholder="카드 번호를 입력하세요"
style={{
padding: "12px",
fontSize: "18px",
width: "300px",
border: `2px solid ${isValid ? "#ccc" : "red"}`,
borderRadius: "4px",
letterSpacing: "1px",
}}
/>

{cardBrand && (
<p style={{ marginTop: "12px" }}>
카드사: <strong>{cardBrand}</strong>
</p>
)}

{!isValid && errorMessage && (
<p style={{ color: "red", marginTop: "4px" }}>{errorMessage}</p>
)}
</div>
);
}
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

유연한 컴포넌트 설계와 스타일링 개선

useCardNumberInput 훅을 활용한 구현이 잘 되어 있습니다. 그러나 인라인 스타일링은 유지보수와 재사용성 측면에서 개선이 필요합니다.

인라인 스타일 대신 CSS 모듈이나 스타일드 컴포넌트를 사용하여 스타일을 관리하는 것을 권장합니다:

+import styles from './App.module.css';
// 또는
+import styled from 'styled-components';

+const Container = styled.div`
+  padding: 24px;
+  font-family: sans-serif;
+`;
+
+const Input = styled.input<{ isValid: boolean }>`
+  padding: 12px;
+  font-size: 18px;
+  width: 300px;
+  border: 2px solid ${props => props.isValid ? '#ccc' : 'red'};
+  border-radius: 4px;
+  letter-spacing: 1px;
+`;

function App() {
  // ...

  return (
-    <div style={{ padding: "24px", fontFamily: "sans-serif" }}>
+    <Container>
      <h2>카드 번호 입력</h2>

-      <input
-        type="text"
-        value={formattedCardNumber}
-        onChange={handleChange}
-        placeholder="카드 번호를 입력하세요"
-        style={{
-          padding: "12px",
-          fontSize: "18px",
-          width: "300px",
-          border: `2px solid ${isValid ? "#ccc" : "red"}`,
-          borderRadius: "4px",
-          letterSpacing: "1px",
-        }}
-      />
+      <Input
+        type="text"
+        value={formattedCardNumber}
+        onChange={handleChange}
+        placeholder="카드 번호를 입력하세요"
+        isValid={isValid}
+      />
      
      // 나머지 코드...
-    </div>
+    </Container>
  );
}

또한, 접근성을 위해 aria- 속성과 레이블을 추가하고, 사용자 경험을 개선하기 위해 포커스 스타일도 고려하세요.

📝 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
function App() {
const {
formattedCardNumber,
cardBrand,
isValid,
errorMessage,
handleChange,
} = useCardNumberInput();
return (
<div style={{ padding: "24px", fontFamily: "sans-serif" }}>
<h2>카드 번호 입력</h2>
<input
type="text"
value={formattedCardNumber}
onChange={handleChange}
placeholder="카드 번호를 입력하세요"
style={{
padding: "12px",
fontSize: "18px",
width: "300px",
border: `2px solid ${isValid ? "#ccc" : "red"}`,
borderRadius: "4px",
letterSpacing: "1px",
}}
/>
{cardBrand && (
<p style={{ marginTop: "12px" }}>
카드사: <strong>{cardBrand}</strong>
</p>
)}
{!isValid && errorMessage && (
<p style={{ color: "red", marginTop: "4px" }}>{errorMessage}</p>
)}
</div>
);
}
import styled from 'styled-components';
// 또는 import styles from './App.module.css';
const Container = styled.div`
padding: 24px;
font-family: sans-serif;
`;
const Input = styled.input<{ isValid: boolean }>`
padding: 12px;
font-size: 18px;
width: 300px;
border: 2px solid ${props => (props.isValid ? '#ccc' : 'red')};
border-radius: 4px;
letter-spacing: 1px;
`;
function App() {
const {
formattedCardNumber,
cardBrand,
isValid,
errorMessage,
handleChange,
} = useCardNumberInput();
return (
<Container>
<h2>카드 번호 입력</h2>
<Input
type="text"
value={formattedCardNumber}
onChange={handleChange}
placeholder="카드 번호를 입력하세요"
isValid={isValid}
/>
{cardBrand && (
<p style={{ marginTop: '12px' }}>
카드사: <strong>{cardBrand}</strong>
</p>
)}
{!isValid && errorMessage && (
<p style={{ color: 'red', marginTop: '4px' }}>{errorMessage}</p>
)}
</Container>
);
}
🤖 Prompt for AI Agents (early access)
In hooks/src/App.tsx around lines 3 to 42, the component uses inline styles
which hinder maintainability and reusability. Refactor by moving all inline
styles into CSS modules or styled-components for better style management.
Additionally, enhance accessibility by adding appropriate aria- attributes and a
label linked to the input element. Improve user experience by including visible
focus styles on the input field to aid keyboard navigation.

Comment on lines +16 to +33
export const validateCardNumberForBrand = (
number: string,
brand: string,
): boolean => {
const length = number.length;
switch (brand) {
case "VISA":
case "MASTERCARD":
case "UNIONPAY":
return length === 16;
case "AMEX":
return length === 15;
case "DINERS":
return length === 14;
default:
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.

🛠️ Refactor suggestion

카드 번호 유효성 검증 로직이 명확합니다.

카드사별 유효성 검증 로직이 잘 구현되어 있습니다. 다만, default 케이스에서 false를 반환하는 것은 의도하지 않은 동작을 유발할 수 있습니다:

export const validateCardNumberForBrand = (
  number: string,
  brand: string,
): boolean => {
  const length = number.length;
  switch (brand) {
    // 케이스들...
    default:
-     return false;
+     // 지원되지 않는 브랜드에 대한 처리를 명확히 하거나 에러를 던지는 것이 더 명시적입니다
+     console.warn(`지원되지 않는 카드사: ${brand}`);
+     return false;
  }
};

또한 기능 확장성을 고려하여 Luhn 알고리즘과 같은 체크섬 검증도 추가하면 더 강력한 유효성 검증이 가능할 것입니다.

📝 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
export const validateCardNumberForBrand = (
number: string,
brand: string,
): boolean => {
const length = number.length;
switch (brand) {
case "VISA":
case "MASTERCARD":
case "UNIONPAY":
return length === 16;
case "AMEX":
return length === 15;
case "DINERS":
return length === 14;
default:
return false;
}
};
export const validateCardNumberForBrand = (
number: string,
brand: string,
): boolean => {
const length = number.length;
switch (brand) {
case "VISA":
case "MASTERCARD":
case "UNIONPAY":
return length === 16;
case "AMEX":
return length === 15;
case "DINERS":
return length === 14;
default:
// 지원되지 않는 카드사에 대한 처리를 명확히 하거나 에러를 던지는 것이 더 명시적입니다
console.warn(`지원되지 않는 카드사: ${brand}`);
return false;
}
};
🤖 Prompt for AI Agents (early access)
In hooks/src/lib/hooks/utils/cardBrandUtils.ts between lines 16 and 33, the
current validateCardNumberForBrand function returns false by default for unknown
brands, which might cause unintended behavior. Modify the function to handle
unknown brands more explicitly, possibly by throwing an error or returning a
distinct value to indicate unsupported brands. Additionally, enhance the
validation by implementing a Luhn algorithm checksum verification after length
checks to improve the robustness of card number validation.

Comment on lines +25 to +29
const useCardBrand = (firstDigits: string) => {
return detectCardCompany(firstDigits);
};

export default useCardBrand;
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

useCardBrand 훅 구현이 단순합니다.

현재 useCardBrand 훅은 단순히 detectCardCompany 함수를 래핑하고 있습니다. React 훅으로서 더 많은 가치를 제공하려면 다음과 같은 개선을 고려해보세요:

-const useCardBrand = (firstDigits: string) => {
-  return detectCardCompany(firstDigits);
-};
+const useCardBrand = (cardNumber: string) => {
+  // 상태 관리를 통해 카드 번호가 변경될 때만 재계산
+  const cardBrand = useMemo(() => {
+    return detectCardCompany(cardNumber);
+  }, [cardNumber]);
+  
+  // 카드사에 따른 추가 정보 제공
+  const cardInfo = useMemo(() => {
+    if (!cardBrand) return null;
+    
+    // 카드사별 추가 정보(로고 URL, 포맷팅 패턴 등)
+    return {
+      brand: cardBrand,
+      // 기타 유용한 정보들...
+    };
+  }, [cardBrand]);
+  
+  return { cardBrand, cardInfo };
+};

이렇게 하면 단순 함수 호출 이상의 가치를 제공하는 진정한 React 훅이 됩니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents (early access)
In hooks/src/lib/hooks/useCardBrand/useCardBrand.ts around lines 25 to 29, the
useCardBrand hook currently just wraps the detectCardCompany function without
leveraging React hook features. Refactor useCardBrand to use React's useState
and useEffect to track and update the card brand dynamically when firstDigits
changes, providing a reactive value rather than a simple function call. This
will make it a true React hook that manages state and side effects.

Comment on lines +60 to 72
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/\D/g, "");
const maxLength = getCardNumberMaxLength(cardBrand);
const trimmed = raw.slice(0, maxLength);

setCardNumber(trimmed);

if (cardBrand && !validateCardNumberForBrand(trimmed, cardBrand)) {
setErrorMessage(ERROR_MESSAGE.CARD_NUMBER.INVALID);
} else {
setErrorMessage("");
}
};
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

handleChange 함수의 관심사를 분리하는 것이 좋습니다.

현재 handleChange 함수는 입력값 처리와 오류 상태 업데이트 두 가지 작업을 모두 수행합니다. 이러한 관심사 혼합은 코드 가독성과 유지보수성을 저하시킬 수 있습니다. useEffect가 이미 카드 번호와 브랜드 변경에 따라 오류 상태를 업데이트하므로, handleChange에서 오류 메시지 설정 로직을 제거하는 것이 좋습니다.

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const maxLength = getCardNumberMaxLength(cardBrand);
  const trimmed = raw.slice(0, maxLength);

  setCardNumber(trimmed);
-
-  if (cardBrand && !validateCardNumberForBrand(trimmed, cardBrand)) {
-    setErrorMessage(ERROR_MESSAGE.CARD_NUMBER.INVALID);
-  } else {
-    setErrorMessage("");
-  }
};

useEffect가 이미 cardNumbercardBrand 변경을 감지하여 오류 메시지를 설정하므로, 이 중복 로직은 제거해도 됩니다.

📝 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
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/\D/g, "");
const maxLength = getCardNumberMaxLength(cardBrand);
const trimmed = raw.slice(0, maxLength);
setCardNumber(trimmed);
if (cardBrand && !validateCardNumberForBrand(trimmed, cardBrand)) {
setErrorMessage(ERROR_MESSAGE.CARD_NUMBER.INVALID);
} else {
setErrorMessage("");
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/\D/g, "");
const maxLength = getCardNumberMaxLength(cardBrand);
const trimmed = raw.slice(0, maxLength);
setCardNumber(trimmed);
};
🤖 Prompt for AI Agents (early access)
In hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts around lines 60
to 72, the handleChange function currently handles both input value processing
and error message updates, causing mixed concerns. To fix this, remove the error
message setting logic from handleChange and rely solely on the existing
useEffect hook that updates the error state based on changes to cardNumber and
cardBrand. This separation improves code clarity and maintainability.

@onschan onschan self-requested a review May 12, 2025 17:05
Copy link
Copy Markdown

@onschan onschan left a comment

Choose a reason for hiding this comment

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

안녕하세요. 에리얼!
미션 진행하시느라 수고 많으셨어요 😊
리뷰 남긴 내용 확인해주시면 좋을 것 같아요!

환절기 감기 조심하세요 🤧 저를 포함해서 제 주변에도 감기 걸리신 분들이 꽤 많네요 🥲

질문에 대한 답변

❓ 로컬 환경과 배포 환경에서의 레이아웃 차이
모달 컴포넌트를 개발하면서 reset CSS 적용 여부에 따라 레이아웃이 달라지는 문제를 경험했습니다.
로컬 개발 환경에서는 reset이 적용된 상태였지만, 다른 환경(예: Storybook, 외부 프로젝트)에서는 적용되지 않아
margin/padding 등의 기본 브라우저 스타일 차이로 인해 레이아웃 불일치가 발생했습니다.
🧐 reset 없이 브라우저 기본 스타일을 고려하면서 개발하는 게 더 재사용성이나 예측 가능성 측면에서 바람직할까요?

유의미한 추론이네요! 이런 UI 툴킷을 제공할때는 사용처의 reset에 영향을 받지 않도록 내부적인 처리가 우선되어야하기 때문에 로컬 환경에서의 reset은 적용하지 않고 개발해주시는게 더 좋을거에요. 컨테이너 레벨에서 적용되어야하는 태그들의 reset 코드를 부분적으로 적용하는 것도 하나의 방법일 수 있구요!

❓ useMemo의 활용 범위
useCardNumberInput 훅에서 formattedCardNumber 등은 매번 렌더링 시 다시 계산되고 있습니다.
이 과정에서 useMemo를 적용하면 불필요한 재계산을 줄일 수 있지 않을까 하는 고민이 들었습니다.
🧐 useMemo는 어느 정도의 연산 비용부터 사용하는 게 적절할까요?
현재처럼 단순한 문자열 조작/검사 수준에서도 성능을 고려해 사용하는 것이 좋은지,
아니면 렌더링에 눈에 띄는 비용이 발생하지 않는 이상, 지나치게 사용하는 것을 피하는 게 더 나은지 궁금합니다.

말씀하신 것처럼 계산 비용을 줄이기 위한 목적으로 useMemo를 사용할 수는 있지만, 일반적으로는 굳이 필요하지 않은 경우가 많습니다.

다만 프로젝트마다 성능 지표는 다를 수 있기 때문에, 병목이 발생했을 때 React Profiler 등으로 확인한 후 도입하는 것도 좋은 방법입니다.
보통 수천 건의 데이터에서 N^2 이상의 연산, 수백 건 이상에서 N^3 수준의 중첩 루프가 필요한 경우에는 사전에 useMemo로 계산을 캐싱해두는 것도 나쁘지 않다고 생각합니다.
지금과 같은 단순 포매팅의 경우 굳이 useMemo를 사용하실 필요는 없어요. 오히려 가독성을 해치기도 하니깐요!

{
"name": "@yeji0214/hooks",
"version": "0.0.2",
"version": "0.0.5",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

버저닝도 꼼꼼하게 해주고 계시네요.
메이저, 마이너, 패치 등에 대한 구분도 한번 학습하고 가셔도 좋을 것 같아요!

Comment on lines +21 to +23
switch (brand) {
case "VISA":
case "MASTERCARD":
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

export const validateCardNumberForBrand = (
  number: string,
  brand: string,
): boolean => number.length === getCardNumberMaxLength(brand);

처럼 단순화할 수 있어보여요!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

훨씬 간결해지네요! 반영했습니당
85c07a9

Comment on lines +30 to +42
validateFirstCardNumbers(number: string) {
if (!validator.isValidCardStartNumber(number))
return {
isValidCardCompany: false,
idx: 0,
helperText: ERROR_MESSAGE.CARD_NUMBER.INVALID,
};
return {
isValidCardCompany: true,
idx: null,
helperText: "",
};
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 친구만 성격이 조금 달라보이네요. 해당 레벨에서 같이 관리하는 이유는 무엇일까요??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

validateFirstCardNumbers는 과거 카드 번호를 4칸으로 나눠 입력받던 구조에서 사용되던 유효성 검사 함수였으나, 현재는 단일 입력창으로 통합되면서 더 이상 사용되지 않아 삭제하였습니다!

interface UseCardNumberInputResult {
cardNumber: string;
formattedCardNumber: string;
cardBrand: string | null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cardBrand가 단순히 string이 아니라 조금 더 좁은 타입으로 관리되도 좋을 것 같아요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

유니온 타입을 사용해서 CardBrand의 범위를 좁혔습니다!
9005581

position: "center" | "bottom" | "top";
title: string;
content: React.ReactNode;
type?: "alert" | "confirm" | "prompt";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

한 컴포넌트 내에서 UI가 분기가 필요한 경우 variant 같은 prop 명으로도 많이 사용해요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

새롭게 알아갑니당 !

Comment on lines +17 to +19
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

모든 prop을 열거하기보다는 ButtonProps같은 공통화된 버튼 Prop이 선언되어있다면

cancelButton?: ButtonProps;
confirmButton?: ButtonProps;

처럼 사용해볼수도 있을거에요.

Component 자체를 prop으로 받아올 수도 있을거구요. (이건 그냥 생각의 폭을 넓혀드리기 위해 언급만 드리는 부분이에요)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

모든 prop을 열거하다보면 컴포넌트가 받는 prop의 개수가 너무 많아지기도 하고, 실제로 그런 문제를 겪기도 해서 개선하면 좋을 것 같다는 생각을 했어요 !

Button 컴포넌트의 ModalButtonProps를 export해서 외부에서도 참조할 수 있도록 하였습니다. Modal 컴포넌트에서는 prop으로 넘겨준 버튼 속성이 존재하면 해당 속성을 가진 Button 컴포넌트를 호출하고, 존재하지 않다면 default 속성을 가진 Button 컴포넌트를 호출하도록 하였습니다.

b1b1600


return (
<Overlay>
<Wrapper position={position} onClick={handleBackdropClick}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

handleBackdropClick에 close 로직을 주입해야만 모달이 닫혀지는 구조일까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

X 버튼 클릭과 배경 클릭에 서로 다른 동작을 적용하고 싶은 경우도 있을 수 있다고 생각해 분리해두었는데,
기본적으로 모달은 배경 클릭 시 닫히는 게 자연스럽다는 점을 고려하여,
기본적으로는 onClose를 호출하고 필요한 경우 handleBackdropClick을 넘기면 override할 수 있도록 개선하였습니다.
fa8afe2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

배경 영역 클릭 시 닫히지 않길 기대하는 동작도 있을 수도 있다는 건 동의해요!
해당 사용 케이스까지 고려해본다면 옵셔널의 boolean 값 처럼 제공해볼 수도 있을 것 같아요!


first?.focus();

const handleKeyDown = (e: KeyboardEvent) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

key에 대한 접근성에 대해 처리하고 있는 만큼 'esc'등에 대한 처리도 추가되면 좋을 것 같네요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

추가했습니다!
d557f98

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

기본적으로 모달이 열렸을때 body 영역에 대한 스크롤 blocking을 해주는 작업을 기본적으로 해주는데, 해당 부분에 대한 고려가 반영되지 않은 것 같아요!
반영해주셔도 좋을 것 같아요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

반영했습니당
df0f99c

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: 0

♻️ Duplicate comments (1)
hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)

10-12: 🛠️ Refactor suggestion

useCardBrand 훅의 기능 확장 필요

현재 useCardBrand 훅은 단순히 detectCardCompany 함수를 래핑하고 있어 React 훅으로서의 가치가 제한적입니다. 이전 리뷰 피드백에서도 지적된 바와 같이, 다음과 같은 개선을 고려해보세요:

  1. useMemo를 사용하여 카드 번호가 변경될 때만 재계산하도록 최적화
  2. 카드 브랜드에 따른 추가 정보(로고, 포맷팅 패턴 등)를 제공
  3. 상태 관리 기능 추가
-const useCardBrand = (cardNumber: string) => {
-  return detectCardCompany(cardNumber);
+import { useMemo } from "react";
+
+const useCardBrand = (cardNumber: string) => {
+  const brand = useMemo(() => {
+    return detectCardCompany(cardNumber);
+  }, [cardNumber]);
+  
+  // 카드사에 따른 추가 정보 제공
+  const cardInfo = useMemo(() => {
+    if (!brand) return null;
+    
+    // 브랜드별 추가 정보
+    return {
+      brand,
+      // 여기에 로고 URL, 포맷팅 패턴 등 추가 정보
+    };
+  }, [brand]);
+  
+  return { brand, cardInfo };
};

이렇게 하면 훅을 사용하는 측에서 카드 브랜드뿐만 아니라 관련 정보도 함께 활용할 수 있습니다.

🧹 Nitpick comments (2)
hooks/src/lib/hooks/constants/cardValidationInfo.ts (1)

22-33: 카드 브랜드 정규식 패턴 추가 - 좋은 개선점입니다

카드 브랜드를 감지하기 위한 정규식 패턴을 추가한 것은 이전 피드백을 잘 반영한 개선입니다. VISA, MASTERCARD 외에도 AMEX, DINERS, UNIONPAY 등 다양한 카드사를 지원하도록 구현하셨네요.

다만 몇 가지 개선할 점이 있습니다:

  1. 브랜드명에 대한 타입 정의가 없습니다. 문자열 리터럴 유니온 타입을 사용하면 더 안정적인 타입 체크가 가능합니다.
  2. Discover, JCB 등 다른 주요 카드 브랜드도 포함하면 좋을 것 같습니다.
+export type CardBrand = "VISA" | "MASTERCARD" | "AMEX" | "DINERS" | "UNIONPAY";

-export const CARD_BRAND_REGEX = [
+export const CARD_BRAND_REGEX: Array<{ brand: CardBrand; pattern: RegExp }> = [
  { brand: "VISA", pattern: /^4/ },
  { brand: "MASTERCARD", pattern: /^5[1-5]/ },
  { brand: "AMEX", pattern: /^3[47]/ },
  { brand: "DINERS", pattern: /^36/ },
  { brand: "UNIONPAY", pattern: /^62[4-6]/ },
  { brand: "UNIONPAY", pattern: /^628[2-8]/ },
  {
    brand: "UNIONPAY",
    pattern: /^622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5])/,
  },
+  { brand: "DISCOVER", pattern: /^6(?:011|5)/ },
+  { brand: "JCB", pattern: /^35/ },
];
hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)

3-8: detectCardCompany 함수 구현이 깔끔합니다

카드 번호를 기반으로 카드사를 감지하는 로직이 간결하고 이해하기 쉽게 구현되었습니다. 정규식 패턴을 순회하면서 일치하는 패턴을 찾는 방식이 효율적입니다.

다만 반환 타입을 구체적인 유니온 타입으로 정의하면 타입 안정성이 더 향상될 것입니다.

+import { CARD_BRAND_REGEX, CardBrand } from "../constants/cardValidationInfo";

-export const detectCardCompany = (cardNumber: string): string | null => {
+export const detectCardCompany = (cardNumber: string): CardBrand | null => {
  for (const { brand, pattern } of CARD_BRAND_REGEX) {
    if (pattern.test(cardNumber)) return brand;
  }
  return null;
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 1d0a04d and be8a6c1.

📒 Files selected for processing (2)
  • hooks/src/lib/hooks/constants/cardValidationInfo.ts (1 hunks)
  • hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, re...

**/*.ts: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • hooks/src/lib/hooks/useCardBrand/useCardBrand.ts
  • hooks/src/lib/hooks/constants/cardValidationInfo.ts
🧬 Code Graph Analysis (1)
hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)
hooks/src/lib/hooks/constants/cardValidationInfo.ts (1)
  • CARD_BRAND_REGEX (22-33)

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: 0

♻️ Duplicate comments (1)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)

63-74: 🛠️ Refactor suggestion

useEffect와 handleChange에서의 중복 유효성 검사를 최적화해보세요.

현재 useEffect와 handleChange 둘 다 validateAndSetError를 호출하고 있어 중복 검증이 발생할 수 있습니다. 하나로 통합하는 것이 좋습니다.

두 가지 접근법을 고려해 볼 수 있습니다:

  1. useEffect만 사용하기:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);
-
-  const nextBrand = detectCardCompany(trimmed);
-  validateAndSetError(trimmed, nextBrand);
};
  1. handleChange에서만 처리하고 useEffect 제거하기:
- useEffect(() => {
-   validateAndSetError(cardNumber, cardBrand);
- }, [cardNumber, cardBrand]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

  const nextBrand = detectCardCompany(trimmed);
  validateAndSetError(trimmed, nextBrand);
};

두 번째 접근법이 더 효율적일 수 있으며, 이전 리뷰어도 useEffect의 필요성에 의문을 제기했습니다.

🧹 Nitpick comments (2)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (2)

13-13: cardBrand를 더 구체적인 타입으로 정의해 보세요.

이전 리뷰에서 제안된 대로, cardBrand가 단순 string이 아닌 구체적인 유니온 타입으로 정의되면 타입 안전성이 향상됩니다.

-  cardBrand: string | null;
+  cardBrand: "VISA" | "MASTERCARD" | "AMEX" | "DINERS" | "UNIONPAY" | null;

이렇게 하면 컴파일 타임에 지원되는 카드 브랜드만 사용할 수 있도록 제한되어 타입 오류를 줄일 수 있습니다.


47-61: formattedCardNumber 계산에 useMemo 적용을 고려해보세요.

현재 IIFE를 사용하여 포맷팅된 카드 번호를 계산하고 있지만, 이 로직은 렌더링마다 재계산됩니다. 루프와 문자열 조작이 포함된 이 계산은 useMemo를 사용하여 최적화할 수 있습니다.

- const formattedCardNumber = (() => {
+ const formattedCardNumber = useMemo(() => {
    if (!cardBrand) return cardNumber;

    const format = CARD_BRAND_FORMAT[cardBrand] || CARD_BRAND_FORMAT.DEFAULT;
    const digits = cardNumber.replace(/\D/g, "");
    const groups = [];
    let cursor = 0;

    for (const len of format) {
      groups.push(digits.slice(cursor, cursor + len));
      cursor += len;
    }

    return groups.filter(Boolean).join(" ");
- })();
+ }, [cardNumber, cardBrand]);

이전 리뷰에서 언급된 것처럼, 현재 복잡도에서는 성능 상 큰 차이가 없을 수 있지만, 재랜더링이 자주 발생하는 컴포넌트에서는 이점이 있을 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between be8a6c1 and fa0c295.

📒 Files selected for processing (1)
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, re...

**/*.ts: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts
🧬 Code Graph Analysis (1)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (2)
hooks/src/lib/hooks/useCardBrand/useCardBrand.ts (1)
  • detectCardCompany (3-8)
hooks/src/lib/hooks/utils/cardBrandUtils.ts (2)
  • validateCardNumberForBrand (16-33)
  • getCardNumberMaxLength (1-14)
🔇 Additional comments (4)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (4)

2-7: 가져오기 구조가 잘 개선되었습니다.

카드 브랜드 감지, 유효성 검사, 포맷팅 관련 기능들이 적절하게 분리되어 가져오기 되었습니다. 코드의 책임 분리가 잘 이루어졌습니다.


25-41: 유효성 검사 로직 추출이 잘 이루어졌습니다.

이전 리뷰 피드백을 반영하여 중복된 유효성 검사 로직을 validateAndSetError 함수로 추출한 것이 좋습니다. 함수가 불리언 값을 반환하여 재사용성도 높아졌습니다.


19-84: 코드가 전반적으로 잘 리팩토링되었습니다.

카드 번호 입력 처리, 브랜드 감지, 포맷팅 및 유효성 검사를 위한 로직이 매우 체계적으로 구현되었습니다. 몇 가지 개선 사항이 있지만, 전체적인 구조와 구현은 훌륭합니다.

카드 번호를 단일 문자열로 관리하고 브랜드별 포맷팅을 적용하는 접근 방식은 사용자 경험을 향상시킬 것입니다. 특히 validateAndSetError 함수를 통한 유효성 검사 로직 추출은 코드 가독성과 유지보수성을 크게 향상시켰습니다.


72-73: handleChange에서 실시간 브랜드 감지가 적절히 구현되었습니다.

사용자가 입력하는 동안 카드 번호의 변경에 따라 브랜드를 실시간으로 감지하고 유효성을 검사하는 것은 좋은 사용자 경험을 제공합니다.

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: 0

♻️ Duplicate comments (1)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)

65-76: 🛠️ Refactor suggestion

useEffect와 handleChange 간 경합 상태 가능성.

현재 useEffecthandleChange 함수 모두에서 validateAndSetError를 호출하고 있어 경합 상태(race condition)가 발생할 수 있습니다.

두 가지 방법으로 해결할 수 있습니다:

  1. useEffect 제거하기:
- useEffect(() => {
-   validateAndSetError(cardNumber, cardBrand);
- }, [cardNumber, cardBrand]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

  const nextBrand = detectCardCompany(trimmed);
  validateAndSetError(trimmed, nextBrand);
};
  1. handleChange에서 검증 로직 제거하기:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);
-
-  const nextBrand = detectCardCompany(trimmed);
-  validateAndSetError(trimmed, nextBrand);
};

첫 번째 방법이 더 효율적입니다. cardNumbercardBrand는 이미 상태에 저장되어 있으므로, useEffect를 통한 반응형 검증은 불필요합니다.

🧹 Nitpick comments (4)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (4)

27-43: 유효성 검증 로직 분리 개선.

유효성 검증 로직을 validateAndSetError 함수로 분리한 것은 좋은 개선입니다. 하지만 반복적인 에러 메시지 설정 로직을 더 줄일 수 있습니다.

다음과 같이 코드를 더 간결하게 만들 수 있습니다:

const validateAndSetError = (
  value: string,
  brand: string | null
): boolean => {
-  if (!brand && value.length > 0) {
-    setErrorMessage(ERROR_MESSAGE.CARD_NUMBER.INVALID);
-    return false;
-  }
-
-  if (brand && !validateCardNumberForBrand(value, brand)) {
-    setErrorMessage(ERROR_MESSAGE.CARD_NUMBER.INVALID);
-    return false;
-  }
-
-  setErrorMessage("");
-  return true;
+  const isValid = !((!brand && value.length > 0) || 
+                   (brand && !validateCardNumberForBrand(value, brand)));
+  
+  setErrorMessage(isValid ? "" : ERROR_MESSAGE.CARD_NUMBER.INVALID);
+  return isValid;
};

49-63: formattedCardNumber 계산 최적화 필요.

현재 formattedCardNumber는 IIFE 패턴을 사용하고 있어 렌더링마다 재계산됩니다. 이 부분은 useMemo를 사용하여 최적화할 수 있습니다.

useMemo를 사용하여 성능을 개선할 수 있습니다:

- const formattedCardNumber = (() => {
+ const formattedCardNumber = useMemo(() => {
    if (!cardBrand) return cardNumber;

    const format = CARD_BRAND_FORMAT[cardBrand] || CARD_BRAND_FORMAT.DEFAULT;
    const digits = cardNumber.replace(/\D/g, "");
    const groups = [];
    let cursor = 0;

    for (const len of format) {
      groups.push(digits.slice(cursor, cursor + len));
      cursor += len;
    }

    return groups.filter(Boolean).join(" ");
- })();
+ }, [cardNumber, cardBrand]);

이는 카드 번호나 브랜드가 변경될 때만 재계산이 이루어지므로, 특히 다른 상태 변경으로 인한 렌더링에서 성능이 향상됩니다.


74-75: 중복 브랜드 감지 로직.

detectCardCompany가 두 번 호출됩니다: 한 번은 상태를 계산할 때, 다른 한 번은 handleChange 내에서입니다.

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

- const nextBrand = detectCardCompany(trimmed);
- validateAndSetError(trimmed, nextBrand);
+ validateAndSetError(trimmed, detectCardCompany(trimmed));
};

또는 useEffect를 제거하는 경우:

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);
};

1-89: 개발자 사고 과정 분석

이 코드를 통해 개발자의 사고 과정을 분석해보면:

  1. 초기에는 입력 세그먼트를 개별적으로 관리하는 복잡한 접근 방식을 택했습니다.
  2. 리뷰 피드백을 통해 중복 코드를 식별하고 이를 단일 함수로 추출했습니다.
  3. 카드 번호를 단일 상태로 통합하고 카드 브랜드 감지와 유효성 검사를 자동화했습니다.
  4. 포맷팅 로직을 분리하여 모듈성을 향상시켰습니다.

이러한 접근 방식은 React의 "단일 진실 원천" 원칙을 잘 따르고 있으며, 상태 관리 복잡성을 줄이는 방향으로 발전했습니다. 다만 useEffect와 이벤트 핸들러 간의 관계, 그리고 성능 최적화 측면에서 추가 개선의 여지가 있습니다.

더 나아가, 이 훅은 다른 입력 필드(예: 만료일, CVC)와 결합하여 완전한 카드 결제 폼의 일부로 사용될 수 있습니다. 각 필드를 개별 훅으로 유지하면서 상위 수준의 통합 훅을 만드는 것을 고려해보세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed27f0 and 9005581.

📒 Files selected for processing (3)
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1 hunks)
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts (1 hunks)
  • hooks/src/lib/hooks/utils/validate.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • hooks/src/lib/hooks/utils/validate.ts
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.ts`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, re...

**/*.ts: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts
🧬 Code Graph Analysis (1)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)
hooks/src/lib/hooks/utils/cardBrandUtils.ts (3)
  • detectCardCompany (24-29)
  • validateCardNumberForBrand (19-22)
  • getCardNumberMaxLength (4-17)
🔇 Additional comments (1)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)

10-10: 타입 정의 개선.

CardBrand 타입을 유니온 타입으로 정의한 것은 좋은 접근입니다. 이제 타입 안전성이 향상되어 코드 자동완성과 타입 체크가 더 강력해졌습니다.

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: 1

♻️ Duplicate comments (5)
components/src/lib/components/Input/Input.tsx (1)

14-14: 🛠️ Refactor suggestion

의미론적 HTML 구조와 접근성 개선이 필요합니다.

Label 컴포넌트가 현재 p 태그로 구현되어 있어 의미론적으로 올바르지 않습니다. 이는 스크린 리더 사용자의 접근성을 저해할 수 있습니다. 또한 Label과 Input 간 연결이 없어서 스크린 리더가 이들을 연관 요소로 인식하지 못합니다.

-const Label = styled.p`
+const Label = styled.label`
  margin-top: 20px;
  margin-bottom: 8px;
  font-size: 14px;
`;

그리고 Label과 Input 연결을 위해 다음과 같이 수정해 주세요:

-      <Label>{title}</Label>
+      <Label htmlFor={name || title.replace(/\s+/g, '-').toLowerCase()}>{title}</Label>
       <StyledInput
-        id={name}
+        id={name || title.replace(/\s+/g, '-').toLowerCase()}
         name={name}

Also applies to: 32-36

components/src/lib/Modal/Modal.tsx (4)

42-82: 🛠️ Refactor suggestion

키보드 접근성 개선이 필요합니다.

현재 구현된 포커스 트랩은 Tab 키를 통한 포커스 순환을 잘 구현했으나, 모달 UI에서 중요한 접근성 기능인 ESC 키를 통한 모달 닫기 기능이 누락되어 있습니다. 사용자 경험과 접근성 표준(WAI-ARIA)에 따르면, 모달은 ESC 키로 닫을 수 있어야 합니다.

useEffect(() => {
  const modal = modalRef.current;
  if (!modal) return;

  const focusableSelectors = [
    "button",
    "a[href]",
    "input",
    "textarea",
    "select",
    '[tabindex]:not([tabindex="-1"])',
  ];

  const focusableElements = modal.querySelectorAll<HTMLElement>(
    focusableSelectors.join(",")
  );
  const first = focusableElements[0];
  const last = focusableElements[focusableElements.length - 1];

  first?.focus();

  const handleKeyDown = (e: KeyboardEvent) => {
+   if (e.key === "Escape") {
+     onClose();
+     return;
+   }
    
    if (e.key !== "Tab") return;
    if (!first || !last) return;

    if (e.shiftKey) {
      if (document.activeElement === first) {
        e.preventDefault();
        last.focus();
      }
    } else {
      if (document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
  };

  modal.addEventListener("keydown", handleKeyDown);
  return () => modal.removeEventListener("keydown", handleKeyDown);
}, [onClose]);

또한, 포커스 트랩 효과를 모달이 열릴 때마다 올바르게 작동하도록 의존성 배열에 onClose를 추가하는 것이 좋습니다.


94-98: 🛠️ Refactor suggestion

모달 타입에 따른 제목 표시 일관성이 필요합니다.

README 문서에 따르면, prompt 타입에서는 제목이 표시되지 않아야 합니다. 그러나 현재 구현에서는 모든 타입에서 제목을 표시하고 있습니다. 사용자에게 일관된 경험을 제공하기 위해 문서와 구현을 일치시켜야 합니다.

<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {type !== "prompt" && <ModalTitle>{title}</ModalTitle>}
  <CloseButtonWrapper>
    {hasCloseButton && <CloseButton onClose={onClose} />}
  </CloseButtonWrapper>
</ModalHeader>

또한, prompt 타입에서는 제목 대신 입력 필드가 주요 포커스이므로, 이러한 변경은 사용자 경험 측면에서도 타당합니다.

Also applies to: 99-111


145-149: 🛠️ Refactor suggestion

사이즈맵과 문서 간의 일관성이 필요합니다.

README 문서에는 모달 크기가 다음과 같이 명시되어 있습니다:

  • small: 304px
  • medium: 600px
  • large: 100%

그러나 현재 구현된 사이즈맵은 이와 다릅니다:

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
};

이 불일치는 사용자에게 혼란을 줄 수 있으므로, 문서와 구현을 일치시켜야 합니다.


1-6: 🛠️ Refactor suggestion

body 스크롤 방지 기능 추가가 필요합니다.

모달이 열려 있을 때 배경(body) 스크롤을 방지하는 것은 모달 컴포넌트의 기본적인 기능입니다. 이는 사용자가 모달에 집중할 수 있도록 하고, 모달 배경의 콘텐츠가 스크롤되어 혼란을 주는 것을 방지합니다.

import styled from "@emotion/styled";
import CloseButton from "../components/CloseButton/CloseButton";
import ConfirmButton from "../components/ConfirmButton/ConfirmButton";
import CancelButton from "../components/CancelButton/CancelButton";
import Input from "../components/Input/Input";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useLayoutEffect, useRef, useState } from "react";

그리고 모달 컴포넌트 내에 다음 useEffect를 추가하세요:

+useLayoutEffect(() => {
+  // 모달이 열릴 때 body 스크롤 방지
+  const originalStyle = window.getComputedStyle(document.body).overflow;
+  document.body.style.overflow = 'hidden';
+
+  // 컴포넌트가 언마운트될 때 원래 스타일로 복원
+  return () => {
+    document.body.style.overflow = originalStyle;
+  };
+}, []);

이렇게 하면 모달이 열려 있는 동안 배경 스크롤이 방지되어 더 나은 사용자 경험을 제공할 수 있습니다.

🧹 Nitpick comments (3)
components/src/lib/components/Input/Input.tsx (1)

38-53: 입력 필드 상태 처리 개선이 필요합니다.

현재 입력 필드의 Focus 상태일 때 스타일 변경은 잘 구현되어 있지만, 오류 상태(Error state)에 대한 스타일 처리가 없습니다. 재사용 가능한 입력 컴포넌트가 되기 위해서는 다양한 상태(오류, 비활성화, 읽기 전용 등)를 지원하는 것이 좋습니다.

type InputProps = {
  title: string;
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  name?: string;
  placeholder?: string;
+  error?: string;
+  disabled?: boolean;
};

const Input = ({ 
  title, 
  value, 
  onChange, 
  name, 
  placeholder,
+  error,
+  disabled 
}: InputProps) => {
  return (
    <Wrapper>
      <Label>{title}</Label>
      <StyledInput
        id={name}
        name={name}
        value={value}
        onChange={onChange}
        placeholder={placeholder}
+        disabled={disabled}
+        aria-invalid={!!error}
      />
+      {error && <ErrorMessage>{error}</ErrorMessage>}
    </Wrapper>
  );
};

+const ErrorMessage = styled.p`
+  color: #ff0000;
+  font-size: 12px;
+  margin-top: 4px;
+`;

const StyledInput = styled.input`
  // 기존 스타일 유지
  
  &:focus {
    outline: none;
    border-color: #333;
  }
  
+  &[aria-invalid="true"] {
+    border-color: #ff0000;
+  }
+  
+  &:disabled {
+    background-color: #f5f5f5;
+    cursor: not-allowed;
+  }
`;
components/README.md (1)

63-85: Prompt 모달 예시의 개선 필요

현재 Prompt 모달 예시는 inputValue 상태를 설정하는 기능은 있지만, 모달을 닫는 기능(onClose)이 누락되어 있습니다. 사용자가 취소하려고 할 때 모달을 닫는 방법이 없으면 불편할 수 있습니다.

function App() {
  const [inputValue, setInputValue] = useState("");

  return (
    <Modal
      type="prompt"
      inputTitle="이름을 입력해주세요"
      value={inputValue}
      onChange={(e) => setInputValue(e.target.value)}
      confirmText="제출"
+     onClose={() => {
+       setInputValue("");  // 모달 닫을 때 입력값 초기화
+     }}
      onConfirm={() => {
        console.log("입력된 값:", inputValue);
+       setInputValue("");  // 제출 후 입력값 초기화
+       // 여기서 실제 데이터 처리 로직을 추가할 수 있습니다
      }}
    />
  );
}

이 수정은 사용자에게 더 완전한 사용법을 보여주며, 취소 시 상태 관리 방법과 제출 후 정리 방법도 함께 제시합니다.

components/src/lib/Modal/Modal.tsx (1)

1-219: 개발자의 문제해결 과정 분석

이 PR의 커밋 히스토리를 분석해 보면, 개발자는 먼저 기본적인 모달 기능을 구현한 후 점진적으로 기능을 확장하는 접근 방식을 취했습니다. 특히 모달 타입(alert, confirm, prompt)에 따른 조건부 렌더링을 구현하고, 접근성을 개선하기 위한 포커스 트랩을 추가하는 과정에서 React의 useRef와 useEffect를 효과적으로 활용했습니다.

다만 몇 가지 개선점이 있습니다:

  1. 상태 끌어올리기(State Lifting): 모달 내부에서 상태를 관리하는 대신, 부모 컴포넌트에서 상태를 제어할 수 있도록 설계하면 더 유연한 컴포넌트가 될 것입니다. React의 제어 컴포넌트 패턴을 참고하세요.

  2. 사용자 경험 고려: ESC 키 처리나 body 스크롤 방지와 같은 모달의 기본적인 UX 패턴을 놓친 점이 아쉽습니다.

  3. 문서와 구현의 일관성: README와 실제 구현 간의 불일치는 라이브러리 사용자에게 혼란을 줄 수 있습니다.

이러한 개선점을 적용하면 더 강력하고 사용자 친화적인 모달 컴포넌트가 될 것입니다. 특히 React 공식 문서의 AccessibilityCompound Components 패턴을 참고하시면 도움이 될 것입니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9005581 and 413f3bc.

📒 Files selected for processing (3)
  • components/README.md (3 hunks)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
  • components/src/lib/components/Input/Input.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/components/Input/Input.tsx
  • components/src/lib/Modal/Modal.tsx
🔇 Additional comments (2)
components/README.md (2)

107-109: 문서와 구현 간의 일관성 확인이 필요합니다.

README에서는 large 사이즈가 100% 너비를 가진다고 명시되어 있지만, 실제 구현에서는 70%로 설정되어 있습니다. 문서와 구현 사이의 불일치는 사용자를 혼란스럽게 할 수 있습니다.

// components/src/lib/Modal/Modal.tsx
const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
  medium: "40%",
  large: "70%", // 문서에는 100%로 명시됨
};

또한, medium 사이즈도 문서에는 600px로 명시되어 있지만 실제로는 40%로 구현되어 있습니다.

실제 구현과 문서 중 어느 쪽을 기준으로 일치시킬지 확인해 주세요. 일반적으로는 문서를 구현에 맞추거나, 구현을 문서에 맞추는 방식으로 일관성을 유지하는 것이 좋습니다.


110-112: 문서와 구현 간 불일치: 모달 타입별 제목 표시 여부

README에 따르면 prompt 타입에서는 제목이 표시되지 않는다고 되어 있지만(titleprompt 타입에서는 표시되지 않습니다), 실제 Modal 컴포넌트 구현에서는 모든 타입에서 제목을 표시하고 있습니다.

// components/src/lib/Modal/Modal.tsx
<ModalHeader>
  <ModalTitle>{title}</ModalTitle>
  <CloseButtonWrapper>
    {hasCloseButton && <CloseButton onClose={onClose} />}
  </CloseButtonWrapper>
</ModalHeader>

문서와 구현을 일관되게 맞추는 것이 필요합니다:

  1. 구현을 문서에 맞추려면, prompt 타입에서 제목을 숨기도록 수정
  2. 문서를 구현에 맞추려면, README에서 해당 설명을 수정

이 불일치 사항을 확인하고 해결해 주세요.

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: 3

♻️ Duplicate comments (4)
components/src/lib/Modal/Modal.tsx (4)

38-39: 상태 관리 개선이 필요합니다.

현재 inputValue 상태가 모달 내부에서 관리되고 있어, 부모 컴포넌트에서 입력값을 제어하거나 초기값을 설정하는 것이 불가능합니다. 이는 재사용성과 확장성을 제한합니다.


92-97: 문서와 구현 불일치 해결 필요

README에는 prompt 타입에서 제목이 보이지 않는다고 명시되어 있으나, 현재 구현은 항상 title을 렌더링합니다.


143-148: 사이즈 맵이 README와 일치하지 않습니다.

README에 명시된 값과 실제 구현의 sizeMap 값이 다릅니다. 특히 large 가 70%로 제한되어 있어 전체 폭 사용이 불가능합니다.


41-81: 🛠️ Refactor suggestion

접근성 개선 - 키보드 포커스 트랩 기능이 잘 구현되었습니다.

모달 내에서 키보드 포커스를 트랩하는 기능을 잘 구현하셨습니다. 하지만 몇 가지 개선사항이 있습니다:

  1. ESC 키 처리가 빠져 있습니다.
  2. 모달이 열릴 때 body 스크롤 차단 기능이 없습니다.

ESC 키를 처리하는 로직을 추가하세요:

 const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === "Escape") {
+    onClose();
+    return;
+  }
   if (e.key !== "Tab") return;
   if (!first || !last) return;
   
   // ...existing Tab handling code
 };

또한 useEffect에서 body 스크롤을 차단하는 로직을 추가하세요:

 useEffect(() => {
   const modal = modalRef.current;
   if (!modal) return;
   
+  // body 스크롤 차단
+  document.body.style.overflow = "hidden";
+  
   // ...existing focus trap code
   
   modal.addEventListener("keydown", handleKeyDown);
-  return () => modal.removeEventListener("keydown", handleKeyDown);
+  return () => {
+    modal.removeEventListener("keydown", handleKeyDown);
+    document.body.style.overflow = "";
+  };
 }, []);
🧹 Nitpick comments (2)
components/src/lib/components/Button/Button.tsx (1)

19-31: 더 정교한 스타일링과 상태 처리가 필요합니다.

현재 스타일링은 기본적인 variant 구분만 되어 있으며, 버튼의 다양한 상태(hover, active, disabled 등)에 대한 스타일링이 부족합니다.

다음과 같이 수정하여 더 완성도 높은 버튼 컴포넌트를 만들 수 있습니다:

-const StyledButton = styled.button<{ variant: "confirm" | "cancel" }>`
+const StyledButton = styled.button<{ 
+  variant: "confirm" | "cancel";
+}>`
   width: 100%;
   padding: 8px 16px;
   font-size: 14px;
   border-radius: 4px;
   cursor: pointer;
   border: ${({ variant }) =>
     variant === "cancel" ? "1px solid gray" : "none"};
   color: ${({ variant }) =>
     variant === "cancel" ? "#333333" : "white"};
   background-color: ${({ variant }) =>
     variant === "cancel" ? "white" : "#333333"};
+  transition: all 0.2s ease-in-out;
+  
+  &:hover {
+    background-color: ${({ variant }) =>
+      variant === "cancel" ? "#f5f5f5" : "#555555"};
+  }
+  
+  &:active {
+    background-color: ${({ variant }) =>
+      variant === "cancel" ? "#e0e0e0" : "#222222"};
+  }
+  
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
`;

이렇게 하면 사용자 경험이 향상되고 인터랙션이 더 자연스러워집니다.

components/src/lib/Modal/Modal.tsx (1)

1-5: 모달 컴포넌트 개발에 대한 사고 과정 분석

PR 내용과 코드 변경사항을 분석해보면 개발자는 모달 컴포넌트를 점진적으로 발전시켰습니다:

  1. 초기에는 기본적인 모달 구현에 집중하고, confirm 타입의 모달만 제공했습니다.
  2. 이후 더 다양한 사용 사례(alert, confirm, prompt)를 지원하기 위해 컴포넌트를 확장했습니다.
  3. 접근성을 개선하기 위해 키보드 포커스 트랩을 추가했습니다.
  4. 재사용성을 높이기 위해 다양한 크기와 위치 지원을 추가했습니다.

이러한 과정은 'Component-Driven Development'와 'Progressive Enhancement' 접근 방식을 따르는 좋은 예시입니다. 리액트 생태계에서 권장되는 방식대로 단계적으로 기능을 확장하고, 접근성과 사용성을 개선하는 노력이 보입니다.

다만, 마무리 단계에서 사용자 입력 처리 및 문서와의 일치성 확인이 더 필요했습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 413f3bc and 63215da.

📒 Files selected for processing (3)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
  • components/src/lib/components/Button/Button.tsx (1 hunks)
  • components/src/lib/components/ConfirmButton/ConfirmButton.tsx (0 hunks)
💤 Files with no reviewable changes (1)
  • components/src/lib/components/ConfirmButton/ConfirmButton.tsx
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/components/Button/Button.tsx
  • components/src/lib/Modal/Modal.tsx

Comment on lines +9 to +15
const Button = ({ text, onClick, variant = "confirm" }: ButtonProps) => {
return (
<StyledButton variant={variant} onClick={onClick}>
{text}
</StyledButton>
);
};
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

접근성 개선이 필요합니다.

현재 버튼 컴포넌트는 기본적인 기능만 구현되어 있어 접근성(a11y) 측면에서 개선이 필요합니다.

다음과 같이 수정하여 접근성을 향상시킬 수 있습니다:

-const Button = ({ text, onClick, variant = "confirm" }: ButtonProps) => {
+const Button = ({ 
+  text, 
+  onClick, 
+  variant = "confirm", 
+  disabled = false,
+  ...restProps 
+}: ButtonProps) => {
   return (
-    <StyledButton variant={variant} onClick={onClick}>
+    <StyledButton 
+      variant={variant} 
+      onClick={onClick}
+      disabled={disabled}
+      {...restProps}
+    >
       {text}
     </StyledButton>
   );
 };

위 변경사항은 HTML 버튼 속성을 모두 전달하여 더 유연한 사용성을 제공합니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/src/lib/components/Button/Button.tsx around lines 9 to 15, the
Button component currently only accepts limited props and does not forward all
native button attributes, which limits accessibility and flexibility. Modify the
component to extend native button attributes and forward all received props to
the underlying button element, ensuring proper handling of accessibility-related
attributes like aria-label, disabled, and type. This will improve accessibility
and allow more flexible usage of the Button component.

Comment on lines +3 to +7
type ButtonProps = {
text: string;
onClick: () => void;
variant?: "confirm" | "cancel";
};
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

TypeScript 타입 정의 강화가 필요합니다.

ButtonProps 타입은 기본적인 속성들만 정의되어 있습니다. 재사용성과 접근성을 높이기 위해 다음과 같은 개선이 필요합니다:

  1. HTML 버튼 속성 확장 (type, disabled 등)
  2. 접근성 속성 추가 (aria-label, aria-pressed 등)

다음과 같이 수정하여 더 견고한 타입 정의를 만들 수 있습니다:

-type ButtonProps = {
+type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
   text: string;
   onClick: () => void;
   variant?: "confirm" | "cancel";
+  disabled?: boolean;
 };
📝 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
type ButtonProps = {
text: string;
onClick: () => void;
variant?: "confirm" | "cancel";
};
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
text: string;
onClick: () => void;
variant?: "confirm" | "cancel";
disabled?: boolean;
};
🤖 Prompt for AI Agents
In components/src/lib/components/Button/Button.tsx around lines 3 to 7, the
ButtonProps type only defines basic properties and lacks HTML button attributes
and accessibility props. To fix this, extend the ButtonProps type from React's
built-in button attributes interface to include standard HTML button props like
type and disabled, and explicitly add accessibility attributes such as
aria-label and aria-pressed. This will improve reusability and accessibility
compliance.

Comment on lines +99 to +110

{type === "prompt" && (
<InputContainer>
<Input
title={inputTitle}
name={inputName}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="이름을 입력하세요"
/>
</InputContainer>
)}
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

Prompt 모달의 입력값 처리 개선 필요

현재 입력값은 내부 상태로만 관리되고 있으며, onConfirm 호출 시 입력값이 전달되지 않습니다. 사용자 입장에서는 입력한 값을 확인 버튼 클릭 시 받아볼 수 없습니다.

다음과 같이 수정하여 입력값을 함께 전달하세요:

type ModalProps = {
  // ...existing props
-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;
  // ...remaining props
};

// ...in Modal component
<Button 
  text={confirmText} 
-  onClick={onConfirm} 
+  onClick={() => type === "prompt" ? onConfirm(inputValue) : onConfirm()} 
/>

이렇게 하면 사용자가 입력한 값을 확인 버튼 클릭 시 함께 전달받을 수 있습니다.

🤖 Prompt for AI Agents
In components/src/lib/Modal/Modal.tsx around lines 99 to 110, the input value
for the prompt modal is managed only internally and not passed to the onConfirm
callback, so the user cannot receive the entered value on confirmation. Modify
the onConfirm handler to accept the current inputValue as an argument and ensure
that when the confirm button is clicked, the inputValue is passed to onConfirm,
allowing the parent component to receive and handle the user's input.

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: 0

♻️ Duplicate comments (6)
components/src/lib/Modal/Modal.tsx (6)

7-21: 🛠️ Refactor suggestion

타입 설계가 명확하게 확장되었습니다.

모달 컴포넌트의 타입 시스템이 새로운 기능을 지원하도록 확장되었습니다. 다양한 모달 타입, 위치, 크기를 지원하며 버튼과 입력 필드에 대한 속성이 추가되었습니다.

그러나 다음과 같은 개선 사항을 고려해보세요:

type ModalProps = {
  type?: "alert" | "confirm" | "prompt";
  position?: "center" | "bottom" | "top";
  size?: "small" | "medium" | "large";
  title?: string;
  content?: React.ReactNode;
  handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
  hasCloseButton?: boolean;
  onClose: () => void;
-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;
  confirmButton?: ModalButtonProps;
  cancelButton?: ModalButtonProps;
  inputTitle?: string;
  inputName?: string;
};

prompt 타입에서 입력값을 onConfirm 콜백에 전달할 수 있도록 수정하면 사용성이 향상됩니다.


38-40: 🛠️ Refactor suggestion

입력 상태 관리 개선이 필요합니다.

현재 입력값이 내부 상태로만 관리되고 있어 부모 컴포넌트에서 제어할 수 없습니다. 제어 컴포넌트 패턴을 적용하여 상태 관리를 개선하세요.

-const [inputValue, setInputValue] = useState("");
+const [internalInputValue, setInternalInputValue] = useState("");
+
+// Props로 전달된 value가 있으면 그것을 사용하고, 없으면 내부 상태 사용
+const inputValue = value !== undefined ? value : internalInputValue;
+const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  if (onChange) {
+    onChange(e);
+  } else {
+    setInternalInputValue(e.target.value);
+  }
+};

ModalProps 타입에도 다음을 추가해야 합니다:

type ModalProps = {
  // 기존 props...
+  value?: string;
+  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

53-93: 🛠️ Refactor suggestion

접근성이 크게 향상되었습니다.

키보드 포커스 트래핑을 구현하여 모달 내에서 포커스가 유지되도록 했습니다. 이는 스크린 리더 사용자를 포함한 접근성 측면에서 중요한 개선입니다.

그러나 ESC 키로 모달을 닫는 기능이 누락되어 있습니다. 다음과 같이 추가하면 접근성이 더욱 향상됩니다:

const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === "Escape") {
+    onClose();
+    return;
+  }
  if (e.key !== "Tab") return;
  if (!first || !last) return;

  // 기존 코드...
};

또한 모달이 열렸을 때 body 스크롤을 방지하는 기능이 누락되어 있습니다:

useEffect(() => {
+  // Prevent body scrolling
+  document.body.style.overflow = 'hidden';
+  
  const modal = modalRef.current;
  if (!modal) return;
  
  // 기존 코드...
  
  return () => {
+    document.body.style.overflow = '';
    modal.removeEventListener("keydown", handleKeyDown);
  };
}, []);

104-109: ⚠️ Potential issue

문서와 구현 불일치를 해결해야 합니다.

README에는 prompt 타입에서 제목이 보이지 않는다고 명시되어 있으나, 현재 코드는 항상 제목을 렌더링하고 있습니다.

<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {type !== "prompt" && <ModalTitle>{title}</ModalTitle>}
  <CloseButtonWrapper>
    {hasCloseButton && <CloseButton onClose={onClose} />}
  </CloseButtonWrapper>
</ModalHeader>

110-122: ⚠️ Potential issue

사용자 입력값 처리가 개선되어야 합니다.

prompt 모달에서 사용자 입력값이 onConfirm 콜백에 전달되지 않고 있습니다. 이는 사용자가 입력한 값을 받아볼 수 없는 문제를 야기합니다.

버튼 클릭 핸들러를 수정하여 입력값을 전달해야 합니다:

// ModalProps 타입 수정
type ModalProps = {
  // ...
-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;
  // ...
};

// Prompt 모달의 Input 구현 부분
{type === "prompt" && (
  <InputContainer>
    <Input
      title={inputTitle}
      name={inputName}
      value={inputValue}
      onChange={(e) => setInputValue(e.target.value)}
      placeholder="이름을 입력하세요"
    />
  </InputContainer>
)}

// 버튼 클릭 시 입력값 전달
const defaultConfirmButton = {
  text: "확인",
-  onClick: onConfirm,
+  onClick: () => type === "prompt" ? onConfirm(inputValue) : onConfirm(),
  variant: "confirm" as const,
};

144-160: ⚠️ Potential issue

스타일 맵 값과 문서 간 불일치를 해결해야 합니다.

sizeMap 값이 README에 명시된 규격(small: 304px, medium: 600px, large: 100%)과 다릅니다. 특히 medium과 large 값이 다릅니다.

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
};

이러한 불일치는 사용자가 예상치 못한 UI 결과를 경험하게 할 수 있으며, 문서와 일치시키는 것이 중요합니다.

🧹 Nitpick comments (1)
components/src/lib/Modal/Modal.tsx (1)

97-103: 이벤트 버블링 처리가 개선되었습니다.

모달 컨테이너 클릭 시 이벤트 버블링을 중단하도록 e.stopPropagation()을 추가한 것은 좋은 방식입니다. 이를 통해 배경 클릭 시 모달이 닫히는 동작이 예상대로 작동합니다.

하지만 handleBackdropClick에 대한 의존성이 강하게 느껴집니다. 좀 더 명시적인 방식으로 배경 클릭을 처리하는 것이 좋을 것 같습니다:

-<Wrapper position={position} onClick={handleBackdropClick}>
+<Wrapper 
+  position={position} 
+  onClick={(e) => {
+    if (e.target === e.currentTarget) {
+      handleBackdropClick?.(e);
+      if (closeOnBackdropClick) {
+        onClose();
+      }
+    }
+  }}
+>

ModalProps에 다음을 추가:

type ModalProps = {
  // 기존 props...
+  closeOnBackdropClick?: boolean;
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 63215da and b1b1600.

📒 Files selected for processing (2)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
  • components/src/lib/components/Button/Button.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/src/lib/components/Button/Button.tsx
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (1)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)
🔇 Additional comments (5)
components/src/lib/Modal/Modal.tsx (5)

3-5: 의존성 관리가 적절하게 이루어졌습니다.

Button, Input 컴포넌트와 필요한 리액트 훅을 적절히 import하여 모달 구현에 활용했습니다.


23-37: 기본값 설정으로 사용성이 향상되었습니다.

모달 컴포넌트의 대부분의 속성에 기본값을 제공함으로써 사용자가 최소한의 prop만 전달해도 작동하도록 했습니다. 이는 컴포넌트 사용성을 크게 향상시킵니다.


41-51: 버튼 기본값 설정이 명확합니다.

확인 및 취소 버튼의 기본값을 제공하여 사용자가 버튼 속성을 전달하지 않아도 기본 동작이 가능하도록 설계되었습니다. 타입 캐스팅(as const)을 사용하여 타입 안전성을 보장한 점도 좋습니다.


124-135: 모달 타입에 따른 버튼 렌더링이 적절합니다.

모달 타입에 따라 다른 버튼 구성을 보여주도록 조건부 렌더링을 사용한 것은 좋은 접근 방식입니다. 이를 통해 사용자는 각 모달 타입에 맞는 UI를 경험할 수 있습니다.

alert: 확인 버튼만 표시
confirm/prompt: 취소 및 확인 버튼 모두 표시

이는 모달 컴포넌트의 유연성과 재사용성을 높이는 좋은 설계입니다.


222-230: 스타일링이 잘 구조화되었습니다.

버튼과 입력 필드를 위한 스타일 컴포넌트가 잘 구조화되어 있습니다. 특히 버튼 사이의 간격을 지정하기 위한 gap 속성 사용은 현대적인 CSS 접근 방식입니다.

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: 2

♻️ Duplicate comments (6)
components/src/lib/Modal/Modal.tsx (6)

38-40: ⚠️ Potential issue

상태 관리 개선 필요

현재 inputValue 상태가 내부적으로만 관리되므로 부모 컴포넌트에서 입력값을 제어하거나 초기값을 설정할 수 없습니다. 이는 재사용성과 확장성을 제한합니다.

다음과 같이 제어 컴포넌트 패턴을 구현하세요:

-const [inputValue, setInputValue] = useState("");
+const [internalInputValue, setInternalInputValue] = useState("");
+
+// 부모로부터 받은 value가 있으면 그것을 사용하고, 없으면 내부 상태 사용
+const inputValue = value !== undefined ? value : internalInputValue;
+const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  if (onChange) {
+    onChange(e);
+  } else {
+    setInternalInputValue(e.target.value);
+  }
+};

그리고 ModalProps 타입에 다음을 추가하세요:

type ModalProps = {
  // 기존 props...
+  value?: string;
+  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

104-109: ⚠️ Potential issue

문서와 구현 불일치 개선 필요

README에는 prompt 타입에서 제목이 보이지 않는다고 명시되어 있으나, 현재 구현은 항상 title을 렌더링합니다. 이는 사용자에게 혼란을 줄 수 있습니다.

다음과 같이 수정하여 문서와 구현을 일치시키세요:

<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {type !== "prompt" && <ModalTitle>{title}</ModalTitle>}
   <CloseButtonWrapper>
     {hasCloseButton && <CloseButton onClose={onClose} />}
   </CloseButtonWrapper>
</ModalHeader>

117-118: ⚠️ Potential issue

Prompt 모달의 입력값 처리 개선 필요

현재 입력값은 내부 상태로만 관리되고 있으며, onConfirm 호출 시 입력값이 전달되지 않습니다. 이로 인해 사용자는 입력한 값을 확인 버튼 클릭 시 받아볼 수 없습니다.

다음과 같이 수정하여 입력값을 함께 전달하세요:

type ModalProps = {
  // ...existing props
-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;
  // ...remaining props
};

// 버튼 클릭 이벤트 핸들러 수정
const defaultConfirmButton = {
  text: "확인",
-  onClick: onConfirm,
+  onClick: () => type === "prompt" ? onConfirm(inputValue) : onConfirm(),
  variant: "confirm" as const,
};

156-160: ⚠️ Potential issue

사이즈맵 수치 문서와 일치 필요

sizeMap 값이 README 표(small: 304px, medium: 600px, large: 100%)와 다릅니다. 특히 large가 70%로 제한되어 있어 전체 폭 사용이 불가능합니다.

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
};

92-93: 🛠️ Refactor suggestion

body 스크롤 방지 처리 필요

모달이 열렸을 때 배경(body)의 스크롤을 방지하는 기능이 구현되어 있지 않습니다. 이는 모달 UI의 중요한 기능으로, 사용자가 모달 외부 콘텐츠와 상호작용하는 것을 방지합니다.

다음과 같이 useEffect를 추가하여 body 스크롤을 관리하세요:

 useEffect(() => {
   const modal = modalRef.current;
   if (!modal) return;

   // ... 기존 코드 ...

   modal.addEventListener("keydown", handleKeyDown);
   return () => modal.removeEventListener("keydown", handleKeyDown);
 }, []);

+useEffect(() => {
+  // 모달이 열릴 때 body 스크롤 방지
+  document.body.style.overflow = "hidden";
+  
+  // 컴포넌트가 언마운트될 때 body 스크롤 복원
+  return () => {
+    document.body.style.overflow = "auto";
+  };
+}, []);

74-89: 🛠️ Refactor suggestion

키보드 접근성 개선 - ESC 키 처리 추가 필요

Tab/Shift+Tab 처리가 잘 구현되었습니다만, 모달의 접근성을 더 향상시키기 위해 ESC 키를 눌렀을 때 모달이 닫히도록 하는 기능이 필요합니다. 이는 모달 UI에서 표준적인 사용자 경험이며, 이전 리뷰에서도 지적되었습니다.

 const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === "Escape") {
+    onClose();
+    return;
+  }
+
   if (e.key !== "Tab") return;
   if (!first || !last) return;

   if (e.shiftKey) {
🧹 Nitpick comments (2)
components/src/lib/Modal/Modal.tsx (1)

8-8: 모달 타입 prop 이름 고려사항

type 대신 variant와 같은 prop 이름을 사용하는 것을 고려해 보세요. 이는 일반적인 React 컴포넌트 관례와 일치하며 다른 reviewer의 피드백과도 일치합니다. UI 분기가 필요한 경우 variant가 더 명확하게 목적을 전달할 수 있습니다.

-  type?: "alert" | "confirm" | "prompt";
+  variant?: "alert" | "confirm" | "prompt";

또한 사용하는 부분도 변경되어야 합니다:

-  type = "alert",
+  variant = "alert",

Also applies to: 24-24

components/README.md (1)

109-109: 타이포 수정 필요

"lage" 대신 "large"로 수정해주세요.

- | `size`                | `"small"` \| `"medium"` \| `"lage"`             | ❌       | `"small"`                                                                                 | 모달 크기를 지정합니다.                                      |
+ | `size`                | `"small"` \| `"medium"` \| `"large"`             | ❌       | `"small"`                                                                                 | 모달 크기를 지정합니다.                                      |
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between b1b1600 and fa8afe2.

📒 Files selected for processing (2)
  • components/README.md (3 hunks)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (1)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)

Comment on lines +116 to +117
| `confirmText` | `string` | ❌ | `"확인"` | 확인 버튼 텍스트입니다. |
| `cancelText` | `string` | ❌ | `"취소"` | 취소 버튼 텍스트입니다. (confirm, prompt 타입에서 사용) |
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

확인/취소 버튼 관련 문서 업데이트 필요

현재 코드에서는 confirmTextcancelText 대신 confirmButtoncancelButton 객체를 사용하고 있습니다. 문서를 실제 구현과 일치하도록 업데이트해주세요.

-| `confirmText`         | `string`                                        | ❌       | `"확인"`                                                                                  | 확인 버튼 텍스트입니다.                                      |
-| `cancelText`          | `string`                                        | ❌       | `"취소"`                                                                                  | 취소 버튼 텍스트입니다. (confirm, prompt 타입에서 사용)      |
+| `confirmButton`       | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` | ❌ | `{ text: "확인", onClick: onConfirm, variant: "confirm" }` | 확인 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. |
+| `cancelButton`        | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` | ❌ | `{ text: "취소", onClick: onClose, variant: "cancel" }` | 취소 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. (confirm, prompt 타입에서 사용) |
📝 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
| `confirmText` | `string` || `"확인"` | 확인 버튼 텍스트입니다. |
| `cancelText` | `string` || `"취소"` | 취소 버튼 텍스트입니다. (confirm, prompt 타입에서 사용) |
| `confirmButton` | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` || `{ text: "확인", onClick: onConfirm, variant: "confirm" }` | 확인 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. |
| `cancelButton` | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` || `{ text: "취소", onClick: onClose, variant: "cancel" }` | 취소 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. (confirm, prompt 타입에서 사용) |
🤖 Prompt for AI Agents
In components/README.md around lines 116 to 117, the documentation still
references `confirmText` and `cancelText` string props, but the current
implementation uses `confirmButton` and `cancelButton` objects instead. Update
the documentation to replace `confirmText` and `cancelText` with `confirmButton`
and `cancelButton` respectively, describing them as objects and detailing their
structure and usage to match the actual code.

Comment on lines +76 to +81
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
confirmText="제출"
onConfirm={() => {
console.log("입력된 값:", inputValue);
}}
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

실제 코드와 문서의 불일치 수정 필요

현재 Modal 컴포넌트는 다음 props를 지원하지 않습니다:

  • value
  • onChange
  • confirmText (대신 confirmButton.text를 사용)

실제 구현된 코드와 문서가 일치하도록 수정해주세요.

다음과 같이 예시를 수정하는 것이 좋습니다:

<Modal
  type="prompt"
  inputTitle="이름을 입력해주세요"
- value={inputValue}
- onChange={(e) => setInputValue(e.target.value)}
- confirmText="제출"
+ confirmButton={{
+   text: "제출",
+   onClick: () => {
+     console.log("입력된 값:", /* 입력값을 받을 수 없음 - API 개선 필요 */);
+   }
+ }}
  onConfirm={() => {
    console.log("입력된 값:", inputValue);
  }}
/>

또한 Modal 컴포넌트에 valueonChange props를 추가하는 것을 고려해보세요.

📝 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
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
confirmText="제출"
onConfirm={() => {
console.log("입력된 값:", inputValue);
}}
<Modal
type="prompt"
inputTitle="이름을 입력해주세요"
confirmButton={{
text: "제출",
onClick: () => {
console.log(
"입력된 값:",
/* 입력값을 받을 수 없음 – API 개선 필요 */
);
}
}}
onConfirm={() => {
console.log("입력된 값:", inputValue);
}}
/>
🤖 Prompt for AI Agents
In components/README.md around lines 76 to 81, the documented usage of the Modal
component includes unsupported props like value, onChange, and confirmText,
which do not match the actual implementation. Update the example to remove these
unsupported props and instead use confirmButton.text for the confirm button
label. Additionally, revise the example to handle input state changes outside
the Modal or consider adding value and onChange props to the Modal component if
input control inside the Modal is needed.

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: 0

♻️ Duplicate comments (6)
components/src/lib/Modal/Modal.tsx (6)

16-16: ⚠️ Potential issue

onConfirm 함수 타입이 입력값을 지원하지 않습니다.

현재 "prompt" 유형의 모달에서 사용자가 입력한 값이 onConfirm 함수로 전달되지 않아 부모 컴포넌트에서 입력값에 접근할 수 없습니다.

onConfirm 함수 타입을 다음과 같이 수정하세요:

-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;

그리고 확인 버튼 클릭 시 입력값을 전달하세요:

-<Button {...(confirmButton ? confirmButton : defaultConfirmButton)} />
+<Button 
+  {...{
+    ...defaultConfirmButton,
+    onClick: () => variant === "prompt" ? onConfirm(inputValue) : onConfirm(),
+    ...confirmButton,
+  }} 
+/>

38-39: 🛠️ Refactor suggestion

입력값 상태 관리 개선이 필요합니다.

현재 inputValue 상태가 모달 내부에서만 관리되고 있어, 부모 컴포넌트에서 입력값을 제어하거나 초기값을 설정할 수 없습니다.

다음과 같이 상태 관리를 개선하세요:

-  const [inputValue, setInputValue] = useState("");
+  const [internalInputValue, setInternalInputValue] = useState("");
+
+  // Props로 전달된 value와 onChange가 있으면 사용하고, 없으면 내부 상태 사용
+  const inputValue = confirmButton?.value !== undefined ? confirmButton.value : internalInputValue;
+  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (confirmButton?.onChange) {
+      confirmButton.onChange(e);
+    } else {
+      setInternalInputValue(e.target.value);
+    }
+  };

또한 ModalButtonProps 타입을 확장해야 합니다:

// Button.tsx 파일에서
export type ModalButtonProps = {
  text: string;
  onClick: () => void;
  variant?: "confirm" | "cancel";
  value?: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

1-5: 🛠️ Refactor suggestion

모달 열림 시 body 스크롤 차단 기능이 필요합니다.

모달이 열렸을 때 배경 콘텐츠가 스크롤되지 않도록 body 스크롤을 차단하는 기능이 구현되어 있지 않습니다.

다음과 같이 useEffect를 추가하여 body 스크롤을 관리하세요:

  useEffect(() => {
    const modal = modalRef.current;
    if (!modal) return;

    // 포커스 핸들링 코드...

    modal.addEventListener("keydown", handleKeyDown);
    return () => modal.removeEventListener("keydown", handleKeyDown);
  }, []);
  
+  useEffect(() => {
+    // 모달이 열릴 때 body 스크롤 차단
+    document.body.style.overflow = "hidden";
+    
+    // 컴포넌트 언마운트 시 원래 상태로 복원
+    return () => {
+      document.body.style.overflow = "";
+    };
+  }, []);

이렇게 하면 모달이 열려 있는 동안 배경 콘텐츠가 스크롤되지 않아 사용자 경험이 향상됩니다.


104-105: ⚠️ Potential issue

prompt 유형에서 제목 렌더링 문제.

README에 따르면 "prompt" 유형의 모달에서는 제목이 표시되지 않아야 하지만, 현재 구현은 모든 유형에서 제목을 렌더링합니다.

다음과 같이 수정하여 문서와 일치시키세요:

<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {variant !== "prompt" && <ModalTitle>{title}</ModalTitle>}
  <CloseButtonWrapper>
    {hasCloseButton && <CloseButton onClose={onClose} />}
  </CloseButtonWrapper>
</ModalHeader>

156-160: ⚠️ Potential issue

sizeMap 값이 문서화된 내용과 일치하지 않습니다.

README에서는 크기가 small: 304px, medium: 600px, large: 100%로 명시되어 있지만, 현재 구현은 다른 값을 사용하고 있습니다.

다음과 같이 수정하여 문서와 일치시키세요:

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
};

74-89: 🛠️ Refactor suggestion

ESC 키 처리가 누락되었습니다.

현재 Tab 키 처리는 구현되어 있지만, 모달 UI에서 일반적으로 기대되는 ESC 키를 눌러 닫는 기능이 누락되었습니다.

다음과 같이 ESC 키 처리를 추가하세요:

  const handleKeyDown = (e: KeyboardEvent) => {
+    if (e.key === "Escape") {
+      onClose();
+      return;
+    }
     if (e.key !== "Tab") return;
     if (!first || !last) return;

     // ...existing Tab handling code
  };

이렇게 하면 사용자가 ESC 키를 눌러 모달을 쉽게 닫을 수 있어 접근성이 향상됩니다.

🧹 Nitpick comments (2)
components/src/lib/Modal/Modal.tsx (2)

24-37: 모달 변형(variant)에 대한 더 명확한 명명이 필요할 수 있습니다.

현재 "alert", "confirm", "prompt"로 지정된 variant 속성은 혼란을 줄 수 있습니다. HTML 네이티브 dialog 인터페이스(window.alert, window.confirm, window.prompt)와 유사한 이름을 사용하지만 동작은 다를 수 있기 때문입니다.

다음과 같은 대안을 고려해 보세요:

-  variant?: "alert" | "confirm" | "prompt";
+  type?: "alert" | "confirm" | "prompt";

또는

-  variant?: "alert" | "confirm" | "prompt";
+  variant?: "single" | "double" | "input";

이렇게 하면 native dialog 인터페이스와의 혼동을 줄이고 컴포넌트의 실제 용도를 더 명확하게 설명할 수 있습니다.


1-140: 모달 컴포넌트 설계에 대한 전반적인 고려사항

현재 모달 컴포넌트는 여러 역할을 수행하고 있어 단일 책임 원칙(SRP)을 위반할 수 있습니다. 다양한 유형의 모달, 입력 관리, 포커스 트래핑 등 다양한 기능이 하나의 컴포넌트에 집중되어 있습니다.

컴포넌트 설계를 개선하기 위한 몇 가지 아이디어:

  1. 기본 모달 구성 요소 분리: 기본 모달 컨테이너, 헤더, 바디, 푸터로 분리하여 재사용성을 높이세요.
// 예시 구조
<Modal onClose={handleClose}>
  <Modal.Header title="제목" />
  <Modal.Body>{content}</Modal.Body>
  <Modal.Footer>
    <Button onClick={handleCancel}>취소</Button>
    <Button onClick={handleConfirm}>확인</Button>
  </Modal.Footer>
</Modal>
  1. 커스텀 훅 사용: 포커스 트래핑과 같은 로직을 별도의 커스텀 훅으로 분리하세요.
// useFocusTrap.ts
export function useFocusTrap(ref) {
  // 포커스 트래핑 로직
}

// Modal 컴포넌트에서
const modalRef = useRef(null);
useFocusTrap(modalRef);
  1. 컴포넌트 확장성 고려: 버튼과 입력 필드를 단일 props 객체로 전달하는 대신, 더 유연한 확장을 위해 React 컴포넌트 합성(composition)을 사용하세요.

이러한 설계 접근 방식은 코드를 더 유지보수하기 쉽고 테스트하기 쉽게 만들며, 향후 요구사항 변경에 더 유연하게 대응할 수 있게 합니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between fa8afe2 and f329484.

📒 Files selected for processing (2)
  • components/src/App.tsx (1 hunks)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/src/App.tsx
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (1)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)

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: 0

♻️ Duplicate comments (5)
components/src/lib/Modal/Modal.tsx (5)

38-39: 상태 관리 패턴 개선이 필요합니다

현재 inputValue 상태는 모달 내부에서만 관리되고 있어 부모 컴포넌트에서 초기값을 설정하거나 값을 제어할 수 없습니다. 이는 컴포넌트의 재사용성과 유연성을 제한합니다.

React에서는 제어 컴포넌트(controlled component) 패턴이 권장됩니다. 이 패턴을 적용하여 부모 컴포넌트가 필요에 따라 입력값을 제어할 수 있도록 개선해보세요.

다음과 같은 질문을 고려해보시기 바랍니다:

  • 부모 컴포넌트에서 초기값을 설정할 방법이 있나요?
  • 부모 컴포넌트에서 입력값을 실시간으로 확인하거나 조작할 수 있나요?
  • 부모 컴포넌트에서 유효성 검사를 수행하고 그 결과를 반영할 수 있나요?

16-16: Prompt 모달의 입력값 활용이 누락되었습니다

현재 onConfirm 함수는 매개변수 없이 호출되므로, prompt 모달에서 사용자가 입력한 값이 부모 컴포넌트로 전달되지 않습니다. 이로 인해 prompt 모달의 주요 목적인 사용자 입력 수집 기능이 제대로 작동하지 않습니다.

onConfirm 타입을 수정하여 입력값을 매개변수로 받을 수 있게 하고, prompt 모달에서는 이 값을 전달하도록 개선해보세요.

// 타입 수정 예시
onConfirm: (value?: string) => void;

110-110: 문서와 구현의 일관성 문제

README에 따르면 prompt 타입의 모달에서는 제목이 표시되지 않아야 하지만, 현재 구현에서는 모든 모달 타입에서 항상 제목이 표시됩니다. 이는 사용자에게 혼란을 줄 수 있습니다.

조건부 렌더링을 사용하여 prompt 타입일 때는 제목을 숨기도록 수정해보세요. 문서와 구현이 일치하면 사용자가 컴포넌트의 동작을 더 쉽게 예측할 수 있습니다.


161-165: 문서와 사이즈 매핑 값이 불일치합니다

현재 구현된 사이즈 매핑 값이 README에 명시된 값과 다릅니다:

  • README: small(304px), medium(600px), large(100%)
  • 현재 구현: small(304px), medium(40%), large(70%)

특히 large 사이즈가 70%로 제한되어 있어 전체 너비를 사용해야 하는 경우에 적합하지 않습니다. README와 일치하도록 값을 수정하여 사용자가 예상하는 동작을 제공해주세요.


1-5: 모달 오픈 시 body 스크롤 방지 기능이 필요합니다

현재 구현에서는 모달이 열렸을 때 배경(body)의 스크롤을 방지하는 기능이 누락되어 있습니다. 이는 사용자 경험을 저하시킬 수 있는 요소입니다.

useEffect를 사용하여 모달이 열릴 때 document.body.style.overflow = 'hidden'을 설정하고, 닫힐 때 원래 값으로 복원하는 패턴을 적용해보세요.

React 18에서는 이런 side effect를 관리하기 위한 새로운 접근 방식도 있으니, React 공식 문서의 useEffect 섹션을 참고해보세요.

🧹 Nitpick comments (5)
components/src/lib/Modal/Modal.tsx (5)

53-98: 접근성 개선을 위한 키보드 트랩 구현이 훌륭합니다

모달 내에서 키보드 포커스를 트랩하는 기능을 구현하여 접근성을 크게 향상시켰습니다. 특히 Tab 키와 Shift+Tab 키를 사용한 포커스 순환과 Escape 키를 통한 모달 닫기 기능이 잘 구현되어 있습니다.

한 가지 개선점으로는 useEffect의 의존성 배열에 onClose를 추가하는 것을 고려해보세요. 현재는 의존성 배열이 비어있어 onClose 함수가 변경되더라도 이벤트 핸들러는 초기 참조값을 유지하게 됩니다.

-  }, []);
+  }, [onClose]);

41-51: 버튼 구성에 대한 개선 제안

현재 defaultConfirmButtondefaultCancelButton은 하드코딩된 variant 값을 사용하고 있습니다. 이 방식보다 props에서 전달받은 variant 값을 우선적으로 사용하고, 없을 경우에만 기본값을 적용하는 방식이 더 유연할 수 있습니다.

또한, 버튼 스타일과 동작을 더 세밀하게 제어할 수 있도록 아래 예시처럼 확장해 볼 수 있습니다:

const mergedConfirmButton = {
  ...defaultConfirmButton,
  ...confirmButton
};

이렇게 하면 부분적인 props 오버라이딩이 가능해집니다.


102-102: 백드롭 클릭 핸들러의 개선 필요

현재 handleBackdropClick이 제공되지 않으면 기본적으로 onClose를 사용하는 방식은 조건 연산자를 사용하여 매번 평가하는 구조입니다. 이 부분은 성능과 가독성을 위해 컴포넌트 초기화 시 한 번만 결정되도록 개선할 수 있습니다.

const backdropClickHandler = handleBackdropClick || onClose;

이와 같이 미리 핸들러를 결정해두면 렌더링 시마다 조건을 평가하지 않아도 됩니다.


130-139: 조건부 렌더링 로직 간소화 제안

현재 Modal 푸터의 조건부 렌더링 로직이 두 부분으로 나뉘어 있어 코드가 다소 복잡합니다. 이 로직은 삼항 연산자나 변수를 활용하여 간소화할 수 있습니다.

예를 들어, variant에 따라 렌더링할 컴포넌트를 미리 결정한 후 한 번에 렌더링하는 방식을 고려해볼 수 있습니다. 이렇게 하면 코드의 가독성이 향상되고 유지보수가 용이해집니다.


8-20: 타입 정의 및 인터페이스 설계에 대한 고민

현재 ModalProps 타입은 모든 variant에 대한 속성을 하나의 타입으로 정의하고 있습니다. 이런 방식은 코드를 간단하게 유지할 수 있지만, 특정 variant에만 필요한 속성이 다른 variant에서는 불필요하게 노출되는 단점이 있습니다.

타입 안전성과 개발자 경험을 더 개선하기 위해 다음과 같은 접근 방식을 고려해보세요:

  1. 기본 공통 속성 타입을 정의
  2. variant별로 특화된 속성 타입을 정의
  3. 판별 유니온(discriminated union)을 사용하여 variant에 따라 적절한 속성이 요구되도록 설계

예를 들어, TypeScript의 조건부 타입이나 판별 유니온을 활용하면 variant에 따라 필요한 속성만 자동완성으로 제공받을 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between f329484 and d557f98.

📒 Files selected for processing (1)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (1)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)

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: 1

♻️ Duplicate comments (4)
components/src/lib/Modal/Modal.tsx (4)

38-40: 🛠️ Refactor suggestion

상태 관리 개선이 필요합니다.

현재 inputValue 상태는 모달 내부에서만 관리되고 있어, 부모 컴포넌트에서 입력 값을 제어하거나 초기값을 설정할 수 없습니다. 이는 재사용성과 확장성을 제한합니다.

다음과 같이 수정하여 제어 컴포넌트 패턴을 적용하세요:

-const [inputValue, setInputValue] = useState("");
+const [internalInputValue, setInternalInputValue] = useState("");
+
+// Props로 전달된 value가 있으면 그것을 사용하고, 없으면 내부 상태 사용
+const inputValue = value !== undefined ? value : internalInputValue;
+const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  if (onChange) {
+    onChange(e);
+  } else {
+    setInternalInputValue(e.target.value);
+  }
+};

또한 ModalProps 타입에 다음을 추가해야 합니다:

type ModalProps = {
  // 기존 props...
+  value?: string;
+  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

118-123: ⚠️ Potential issue

문서와 구현 불일치 문제가 남아있습니다.

README에 따르면 "prompt" 타입에서는 제목이 보이지 않아야 하는데, 현재 구현은 모든 variant에서 항상 title을 렌더링하고 있습니다.

다음과 같이 수정하여 문서와 구현을 일치시키세요:

<ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {variant !== "prompt" && <ModalTitle>{title}</ModalTitle>}
  <CloseButtonWrapper>
    {hasCloseButton && <CloseButton onClose={onClose} />}
  </CloseButtonWrapper>
</ModalHeader>

126-136: ⚠️ Potential issue

Prompt 모달의 입력값 처리 개선 필요

현재 입력값은 내부 상태로만 관리되고 있으며, onConfirm 호출 시 입력값이 전달되지 않습니다. 사용자 입장에서는 입력한 값을 확인 버튼 클릭 시 받아볼 수 없습니다.

다음과 같이 수정하여 입력값을 함께 전달하세요:

<Button
-  {...(confirmButton ? confirmButton : defaultConfirmButton)}
+  {...(confirmButton ? 
+    confirmButton : 
+    {
+      ...defaultConfirmButton,
+      onClick: () => variant === "prompt" ? onConfirm(inputValue) : onConfirm()
+    }
+  )}
/>

또는 defaultConfirmButton 정의 부분을 수정하세요:

const defaultConfirmButton = {
  text: "확인",
-  onClick: onConfirm,
+  onClick: () => variant === "prompt" ? onConfirm(inputValue) : onConfirm(),
  variant: "confirm" as const,
};

176-180: ⚠️ Potential issue

문서와 일치하지 않는 크기 설정

sizeMap 값이 README 표(small: 304px, medium: 600px, large: 100%)와 다릅니다. 특히 large가 70%로 제한되어 있어 전체 폭 사용이 불가능합니다.

다음과 같이 수정하여 문서와 일치시키세요:

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
};
🧹 Nitpick comments (1)
components/src/lib/Modal/Modal.tsx (1)

3-104: 전반적인 컴포넌트 아키텍처 개선 제안

모달 컴포넌트가 다양한 기능(alert, confirm, prompt)을 지원하도록 확장된 것은 훌륭하지만, 현재 구현은 단일 컴포넌트가 너무 많은 역할을 담당하고 있습니다. 이는 유지보수와 확장성에 어려움을 줄 수 있습니다.

다음 아키텍처 개선을 고려해보세요:

  1. 컴포넌트 분리: 각 모달 타입별로 별도의 컴포넌트(AlertModal, ConfirmModal, PromptModal)를 만들고, 공통 로직은 useModal 훅이나 HOC로 추출하는 것이 좋습니다.

  2. 상태 관리 최적화: 현재 모든 상태와 이벤트 핸들러가 모달 컴포넌트 내부에 있어 복잡성이 증가하고 있습니다. Context API나 Recoil, Zustand 같은 상태 관리 라이브러리를 도입하여 상태 로직을 분리하는 것을 고려해보세요.

  3. 타입 안전성 강화: 현재 variant에 따라 다른 props가 필요하지만, 타입 시스템이 이를 완전히 반영하지 못하고 있습니다. 다음과 같은 타입 개선을 고려해보세요:

type BaseModalProps = {
  position?: "center" | "bottom" | "top";
  size?: "small" | "medium" | "large";
  handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
  hasCloseButton?: boolean;
  onClose: () => void;
};

type AlertModalProps = BaseModalProps & {
  variant: "alert";
  title?: string;
  content?: React.ReactNode;
  onConfirm: () => void;
  confirmButton?: ModalButtonProps;
};

type ConfirmModalProps = BaseModalProps & {
  variant: "confirm";
  title?: string;
  content?: React.ReactNode;
  onConfirm: () => void;
  confirmButton?: ModalButtonProps;
  cancelButton?: ModalButtonProps;
};

type PromptModalProps = BaseModalProps & {
  variant: "prompt";
  inputTitle?: string;
  inputName?: string;
  value?: string;
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onConfirm: (value: string) => void;
  confirmButton?: ModalButtonProps;
  cancelButton?: ModalButtonProps;
};

type ModalProps = AlertModalProps | ConfirmModalProps | PromptModalProps;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between d557f98 and df0f99c.

📒 Files selected for processing (1)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (1)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)
🔇 Additional comments (1)
components/src/lib/Modal/Modal.tsx (1)

53-104: 접근성 개선이 잘 구현되었습니다.

모달 내 키보드 포커스 트랩 구현과 body 스크롤 방지 기능은 접근성 측면에서 훌륭한 개선입니다. ESC 키를 통한 모달 닫기 기능도 적절히 구현되었습니다.

특히 다음 부분이 잘 구현되었습니다:

  1. 포커스 가능한 요소 선택 및 관리
  2. Tab/Shift+Tab 키 핸들링으로 포커스 순환
  3. ESC 키로 모달 닫기
  4. 컴포넌트 언마운트 시 body 스타일 복원

이러한 접근성 개선은 사용자 경험과 웹 표준 준수에 큰 도움이 됩니다.

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: 4

♻️ Duplicate comments (4)
components/src/lib/Modal/Modal.tsx (2)

24-37: props 이름 일관성 및 기본값 업데이트 필요

컴포넌트 정의에서 variant로 선언된 prop을 사용하고 있지만, README에서는 type으로 문서화되어 있습니다. 일관성을 유지하기 위해 이름을 변경해야 합니다.

 const Modal = ({
-  variant = "alert",
+  type = "alert",
   position = "center",
   size = "small",
   title = "알림",
   content = "내용이 없습니다.",
   handleBackdropClick,
   hasCloseButton = true,
   onClose,
   onConfirm,
   confirmButton,
   cancelButton,
   inputTitle = "입력해주세요.",
   inputName = "input",
 }: ModalProps) => {

124-136: prompt 모달에서 사용자 입력 처리 개선 필요

현재 prompt 모달의 입력값은 내부 상태로만 관리되고 있고, onConfirm 호출 시 이 값이 전달되지 않습니다. 사용자 관점에서 입력한 값을 받아볼 수 없는 상황입니다.

다음과 같이 Input 컴포넌트 사용 부분을 수정하세요:

<Input
  title={inputTitle}
  name={inputName}
  value={inputValue}
-  onChange={(e) => setInputValue(e.target.value)}
+  onChange={handleInputChange}
  placeholder="이름을 입력하세요"
/>

그리고 확인 버튼 클릭 시 입력값을 전달하도록 수정하세요:

<Button
-  onClick={onConfirm}
+  onClick={() => variant === "prompt" ? onConfirm(inputValue) : onConfirm()}
  text={confirmButton?.text || "확인"}
  variant="confirm"
/>
components/README.md (2)

37-62: 코드 예시에서의 prop 이름 불일치

코드에서는 variant를 사용하지만 예시에서는 type을 사용하고 있습니다. 일관성을 위해 모든 예제에서 동일한 prop 이름을 사용해야 합니다.

다음과 같이 수정하세요:

 <Modal
-  type="confirm"
+  variant="confirm"
   title="삭제하시겠습니까?"
   content="삭제된 데이터는 복구할 수 없습니다."
   onClose={handleCancel}
   onConfirm={handleConfirm}
 />

105-119: ⚠️ Potential issue

Props 테이블이 실제 구현과 일치하지 않습니다.

현재 코드에서는 confirmTextcancelText 대신 confirmButtoncancelButton 객체를 사용하고 있습니다. 또한 코드는 variant를 사용하지만 문서는 type을 사용하고 있어 혼란을 줍니다.

다음과 같이 수정하세요:

 | Prop                  | Type                                            | Required | Default                                                                                   | Description                                                  |
 | --------------------- | ----------------------------------------------- | -------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
-| `type`                | `"alert"` \| `"confirm"` \| `"prompt"`          | ❌       | `"alert"`                                                                                 | 모달의 형태를 지정합니다.                                    |
+| `variant`             | `"alert"` \| `"confirm"` \| `"prompt"`          | ❌       | `"alert"`                                                                                 | 모달의 형태를 지정합니다.                                    |
 | `position`            | `"center"` \| `"bottom"` \| `"top"`             | ❌       | `"center"`                                                                                | 모달 위치를 지정합니다.                                      |
 | `size`                | `"small"` \| `"medium"` \| `"lage"`             | ❌       | `"small"`                                                                                 | 모달 크기를 지정합니다.                                      |
 | `title`               | `string`                                        | ❌       | `"알림"`                                                                                  | 모달 제목입니다. `prompt` 타입에서는 표시되지 않습니다.      |
 | `content`             | `React.ReactNode`                               | ❌       | `"내용이 없습니다."`                                                                      | 모달 본문 내용입니다. `prompt` 타입에서는 표시되지 않습니다. |
 | `inputTitle`          | `string`                                        | ❌       | `"입력해주세요."`                                                                         | `prompt` 타입에서 입력 필드 상단의 안내 문구입니다.          |
 | `hasCloseButton`      | `boolean`                                       | ❌       | `true`                                                                                    | 우측 상단 닫기 버튼 표시 여부입니다.                         |
 | `onClose`             | `() => void`                                    | ✅       | –                                                                                         | 닫기 버튼, 백드롭 클릭 시 실행되는 콜백입니다.               |
-| `onConfirm`           | `() => void`                                    | ✅       | –                                                                                         | 확인 버튼 클릭 시 실행되는 콜백입니다.                       |
-| `confirmText`         | `string`                                        | ❌       | `"확인"`                                                                                  | 확인 버튼 텍스트입니다.                                      |
-| `cancelText`          | `string`                                        | ❌       | `"취소"`                                                                                  | 취소 버튼 텍스트입니다. (confirm, prompt 타입에서 사용)      |
+| `onConfirm`           | `(value?: string) => void`                      | ✅       | –                                                                                         | 확인 버튼 클릭 시 실행되는 콜백입니다. prompt 타입에서는 입력값을 전달받습니다.  |
+| `confirmButton`       | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` | ❌ | `{ text: "확인", onClick: onConfirm, variant: "confirm" }` | 확인 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. |
+| `cancelButton`        | `{ text: string; onClick: () => void; variant?: "confirm" \| "cancel" }` | ❌ | `{ text: "취소", onClick: onClose, variant: "cancel" }` | 취소 버튼의 텍스트, 이벤트 핸들러, 스타일 등을 설정합니다. (confirm, prompt 타입에서 사용) |
 | `handleBackdropClick` | `(e: React.MouseEvent<HTMLDivElement>) => void` | ❌       | 모달 배경 클릭 시 실행할 커스텀 핸들러입니다. 전달하지 않으면 기본적으로 모달이 닫힙니다. |
 |                       |
🧹 Nitpick comments (3)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (3)

49-63: 성능 최적화를 위한 useMemo 적용을 고려해보세요.

현재 IIFE 패턴을 사용하여 formattedCardNumber를 계산하고 있습니다. 이 계산 로직은 문자열 처리와 반복문을 포함하고 있어 렌더링마다 실행될 경우 성능에 영향을 줄 수 있습니다. useMemo를 사용하여 cardNumbercardBrand가 변경될 때만 재계산되도록 최적화하는 것이 좋겠습니다.

- const formattedCardNumber = (() => {
+ const formattedCardNumber = useMemo(() => {
    if (!cardBrand) return cardNumber;

    const format = CARD_BRAND_FORMAT[cardBrand] || CARD_BRAND_FORMAT.DEFAULT;
    const digits = cardNumber.replace(/\D/g, "");
    const groups = [];
    let cursor = 0;

    for (const len of format) {
      groups.push(digits.slice(cursor, cursor + len));
      cursor += len;
    }

    return groups.filter(Boolean).join(" ");
- })();
+ }, [cardNumber, cardBrand]);

10-10: 타입 불일치를 수정해주세요.

validateAndSetError 함수는 두 번째 매개변수로 brand: string | null을 받고 있지만, 실제로는 더 구체적인 CardBrand | null 타입을 사용해야 합니다. 이는 타입 안전성을 높이고 오류를 방지하는 데 도움이 됩니다.

const validateAndSetError = (
  value: string,
- brand: string | null,
+ brand: CardBrand | null,
): boolean => {
  // ...
};

Also applies to: 15-15, 30-30


25-25: 중복 함수 호출을 최적화하세요.

현재 코드에서는 detectCardCompany 함수가 두 번 호출되고 있습니다: 한 번은 cardBrand 변수를 설정할 때, 다른 한 번은 handleChange 내에서 입력이 변경될 때입니다. 이런 중복 호출은 불필요하며, 특히 detectCardCompany 함수가 복잡한 패턴 매칭을 수행한다면 성능에 영향을 줄 수 있습니다.

- const cardBrand = detectCardCompany(cardNumber);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

- const nextBrand = detectCardCompany(trimmed);
- validateAndSetError(trimmed, nextBrand);
+ validateAndSetError(trimmed, cardBrand);
};

또는 cardBrand를 상태로 관리하면서 handleChange에서 업데이트하는 방식도 고려해볼 수 있습니다:

- const cardBrand = detectCardCompany(cardNumber);
+ const [cardBrand, setCardBrand] = useState<CardBrand | null>(null);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

  const nextBrand = detectCardCompany(trimmed);
+ setCardBrand(nextBrand);
  validateAndSetError(trimmed, nextBrand);
};

useEffect(() => {
  const brand = detectCardCompany(cardNumber);
  setCardBrand(brand);
}, [cardNumber]);

Also applies to: 74-74

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
Knowledge Base: Disabled due to Reviews > Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between df0f99c and c5d66f3.

⛔ Files ignored due to path filters (1)
  • components/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • components/README.md (3 hunks)
  • components/package.json (2 hunks)
  • components/src/lib/Modal/Modal.tsx (5 hunks)
  • components/src/lib/components/Button/Button.tsx (1 hunks)
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1 hunks)
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • hooks/src/lib/hooks/utils/cardBrandUtils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/package.json
  • components/src/lib/components/Button/Button.tsx
🧰 Additional context used
📓 Path-based instructions (2)
`**/*.ts`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, re...

**/*.ts: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts
`**/*.tsx`: You MUST follow these guidelines: - Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, r...

**/*.tsx: You MUST follow these guidelines:

  • Please leave 1–2 key review comments that address recurring mistakes or pain points the code author frequently makes. If possible, reference official documentation that can help resolve these issues
  • Analyze the commit history to infer the cognitive process the developer went through while solving the problem. Summarize their thought progression: how they understood the problem, iterated on solutions, and refined the code. Focus on their reasoning patterns, learning moments, or shifts in approach. If applicable, relate their process to established best practices in the React ecosystem.
  • Do not provide complete solutions unless explicitly requested.
  • Guide the programmer in problem-solving instead of providing direct answers.
  • When asked about programming concepts (e.g., "What is a hook?"), give direct and clear explanations.
  • Break problems into smaller, manageable steps to help think through them logically.
  • Ask leading questions and provide hints instead of just telling the answer.
  • Encourage independent debugging before offering suggestions.
  • Refer to relevant documentation instead of providing ready-made solutions.
  • Promote modular thinking—breaking problems into reusable components.
  • Remind the programmer to reflect on lessons learned after solving an issue.
  • If explicitly asked for code (e.g., "Give me the code"), then provide it.
  • components/src/lib/Modal/Modal.tsx
🧬 Code Graph Analysis (2)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)
hooks/src/lib/hooks/utils/cardBrandUtils.ts (3)
  • detectCardCompany (24-29)
  • validateCardNumberForBrand (19-22)
  • getCardNumberMaxLength (4-17)
components/src/lib/Modal/Modal.tsx (1)
components/src/lib/components/Button/Button.tsx (1)
  • ModalButtonProps (3-7)
🔇 Additional comments (6)
hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts (1)

27-43: 유효성 검증 함수의 추출로 코드 개선이 이루어졌습니다.

중복된 유효성 검사 로직을 validateAndSetError 함수로 추출하여 코드의 가독성과 유지보수성이 크게 향상되었습니다. 이전 리뷰 피드백을 잘 반영해 주셨네요.

components/src/lib/Modal/Modal.tsx (4)

53-104: 접근성 및 스크롤 관리에 대한 개선 👍

모달이 열렸을 때 배경 스크롤을 차단하고, 키보드 포커스를 트랩하는 구현이 잘 되어있습니다. 특히 Tab/Shift+Tab 키를 통한 포커스 순환 및 Escape 키로 모달 닫기 기능이 접근성 측면에서 큰 개선입니다.

다만, useEffect의 의존성 배열이 비어있어 모달 내용이 변경되어도 포커스 가능한 요소 목록이 업데이트되지 않습니다. content나 variant가 변경될 경우 포커스 가능한 요소 목록을 갱신하는 것이 좋습니다.

-  }, []);
+  }, [variant, content]);

138-154: 모달 타입에 따른 버튼 렌더링 논리가 잘 구현되었습니다.

타입(variant)에 따라 다른 버튼 구성을 보여주는 조건부 렌더링이 명확하게 구현되었습니다. 각 버튼에 적절한 기본값을 제공하면서도 커스터마이징 가능하도록 한 점이 좋습니다.

그러나 타입이 변경됐을 때 버튼의 onClick 핸들러가 일관되게 동작하는지 확인이 필요합니다. prompt 타입의 경우 onConfirm에 입력값을 전달하도록 수정하세요.

<Button
-  {...(confirmButton ? confirmButton : defaultConfirmButton)}
+  {...(confirmButton ? {
+    ...confirmButton,
+    onClick: variant === "prompt" ? 
+      () => confirmButton.onClick(inputValue) : 
+      confirmButton.onClick
+  } : {
+    ...defaultConfirmButton,
+    onClick: variant === "prompt" ? 
+      () => onConfirm(inputValue) : 
+      onConfirm
+  })}
/>

176-180: 사이즈 설정이 문서와 일치하지 않습니다.

sizeMap 값이 README에 명시된 값(small: 304px, medium: 600px, large: 100%)과 다릅니다. 특히 large가 70%로 제한되어 있어 전체 폭 사용이 불가능합니다.

README와 일치하도록 다음과 같이 수정하세요:

 const sizeMap: Record<"small" | "medium" | "large", string> = {
   small: "304px",
-  medium: "40%",
-  large: "70%",
+  medium: "600px",
+  large: "100%",
 };

Likely an incorrect or invalid review comment.


118-119: 타이틀 렌더링 로직이 문서화와 불일치

README에는 prompt 타입에서 제목이 보이지 않는다고 명시되어 있으나, 현재 구현은 항상 title을 렌더링하고 있습니다.

다음과 같이 수정하여 문서와 일치시키세요:

 <ModalHeader>
-  <ModalTitle>{title}</ModalTitle>
+  {variant !== "prompt" && <ModalTitle>{title}</ModalTitle>}
   <CloseButtonWrapper>
     {hasCloseButton && <CloseButton onClose={onClose} />}
   </CloseButtonWrapper>
 </ModalHeader>

Likely an incorrect or invalid review comment.

components/README.md (1)

63-85: Prompt 모달 예시가 실제 구현과 일치하지 않습니다.

현재 Modal 컴포넌트는 다음 props를 지원하지 않습니다:

  • value
  • onChange
  • confirmText (대신 confirmButton.text를 사용)

실제 구현된 코드와 문서가 일치하도록 수정해주세요.

다음과 같이 예시를 수정하는 것이 좋습니다:

 function App() {
   const [inputValue, setInputValue] = useState("");
 
   return (
     <Modal
-      type="prompt"
+      variant="prompt"
       inputTitle="이름을 입력해주세요"
-      value={inputValue}
-      onChange={(e) => setInputValue(e.target.value)}
-      confirmText="제출"
+      confirmButton={{
+        text: "제출",
+        onClick: () => console.log("확인 버튼 클릭됨")
+      }}
       onConfirm={() => {
-        console.log("입력된 값:", inputValue);
+        // 현재는 입력값을 받을 수 없음 - API 개선 필요
+        console.log("확인 버튼 클릭됨");
       }}
     />
   );
 }

Likely an incorrect or invalid review comment.

Comment on lines +65 to +67
useEffect(() => {
validateAndSetError(cardNumber, cardBrand);
}, [cardNumber, cardBrand]);
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

useEffect와 handleChange에서의 중복 검증을 개선해보세요.

현재 코드는 useEffect에서 카드 번호와 브랜드가 변경될 때마다 유효성 검사를 수행하고, handleChange 함수에서도 동일한 유효성 검사를 수행합니다. 이는 중복 작업일 뿐만 아니라 상태 업데이트의 타이밍 차이로 인해 경쟁 조건(race condition)이 발생할 수 있습니다.

handleChange 내에서만 유효성 검사를 수행하고 useEffect를 제거하거나, 반대로 handleChange에서 유효성 검사 로직을 제거하고 useEffect만 사용하는 방식으로 일관성을 유지하는 것이 좋습니다.

옵션 1: useEffect 제거하기 (추천)

- useEffect(() => {
-   validateAndSetError(cardNumber, cardBrand);
- }, [cardNumber, cardBrand]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);

  const nextBrand = detectCardCompany(trimmed);
  validateAndSetError(trimmed, nextBrand);
};

옵션 2: handleChange에서 검증 로직 제거하기

useEffect(() => {
  validateAndSetError(cardNumber, cardBrand);
}, [cardNumber, cardBrand]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const raw = e.target.value.replace(/\D/g, "");
  const trimmed = raw.slice(0, getCardNumberMaxLength(cardBrand));
  setCardNumber(trimmed);
-
-  const nextBrand = detectCardCompany(trimmed);
-  validateAndSetError(trimmed, nextBrand);
};

Also applies to: 69-76

🤖 Prompt for AI Agents
In hooks/src/lib/hooks/useCardNumberInput/useCardNumberInput.ts around lines 65
to 67 and 69 to 76, there is duplicated validation logic both in useEffect and
handleChange causing redundant work and potential race conditions. To fix this,
choose one approach: either remove the useEffect entirely and keep validation
only inside handleChange, or remove the validation from handleChange and rely
solely on useEffect for validation. The recommended fix is to remove the
useEffect validation calls and perform validation exclusively within
handleChange to maintain consistency and avoid duplication.

Comment on lines +38 to +39
const modalRef = useRef<HTMLDivElement | null>(null);
const [inputValue, setInputValue] = useState("");
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

입력 상태 관리 개선 필요

현재 inputValue 상태가 모달 내부에서만 관리되고 있어, 부모 컴포넌트에서 입력값을 제어하거나 초기값을 설정할 수 없습니다. 입력 값이 onConfirm에 전달되지 않아 사용자의 입력을 받아볼 수 없는 문제가 있습니다.

아래와 같이 수정하여 입력값을 제어 가능하게 하고, 확인 버튼 클릭 시 입력값을 전달하세요:

-const [inputValue, setInputValue] = useState("");
+const [internalInputValue, setInternalInputValue] = useState("");
+// value prop이 있으면 사용하고, 없으면 내부 상태 사용
+const inputValue = value !== undefined ? value : internalInputValue;
+
+const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  if (onChange) {
+    onChange(e);
+  } else {
+    setInternalInputValue(e.target.value);
+  }
+};

또한 ModalProps 타입에 다음을 추가해야 합니다:

value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
🤖 Prompt for AI Agents
In components/src/lib/Modal/Modal.tsx around lines 38 to 39, the inputValue
state is currently managed only inside the modal, preventing the parent
component from controlling or initializing the input value and causing the input
not to be passed to onConfirm. To fix this, lift the inputValue state up by
adding optional value and onChange props to ModalProps, use these props to
control the input value and handle changes, and ensure the onConfirm callback
receives the current input value when the confirm button is clicked.

Comment on lines +8 to 21
variant?: "alert" | "confirm" | "prompt";
position?: "center" | "bottom" | "top";
size?: "small" | "medium" | "large";
title?: string;
content?: React.ReactNode;
handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
hasCloseButton?: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
onConfirm: () => void;
confirmButton?: ModalButtonProps;
cancelButton?: ModalButtonProps;
inputTitle?: string;
inputName?: string;
};
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

타입과 변형 이름에 대한 일관성 필요

현재 컴포넌트에서는 모달의 형태를 variant라는 prop으로 정의하고 있으나, README에서는 type으로 문서화되어 있습니다. 이는 개발자에게 혼란을 줄 수 있습니다.

또한, prompt 타입의 모달에서 입력값을 onConfirm 콜백으로 전달하지 않고 있어 사용자 입력을 받을 방법이 없습니다.

다음과 같이 수정해보세요:

 type ModalProps = {
-  variant?: "alert" | "confirm" | "prompt";
+  type?: "alert" | "confirm" | "prompt";
   position?: "center" | "bottom" | "top";
   size?: "small" | "medium" | "large";
   title?: string;
   content?: React.ReactNode;
   handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
   hasCloseButton?: boolean;
   onClose: () => void;
-  onConfirm: () => void;
+  onConfirm: (value?: string) => void;
   confirmButton?: ModalButtonProps;
   cancelButton?: ModalButtonProps;
   inputTitle?: string;
   inputName?: string;
 };
📝 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
variant?: "alert" | "confirm" | "prompt";
position?: "center" | "bottom" | "top";
size?: "small" | "medium" | "large";
title?: string;
content?: React.ReactNode;
handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
hasCloseButton?: boolean;
onClose: () => void;
onConfirm?: () => void;
confirmText?: string;
onConfirm: () => void;
confirmButton?: ModalButtonProps;
cancelButton?: ModalButtonProps;
inputTitle?: string;
inputName?: string;
};
type ModalProps = {
type?: "alert" | "confirm" | "prompt";
position?: "center" | "bottom" | "top";
size?: "small" | "medium" | "large";
title?: string;
content?: React.ReactNode;
handleBackdropClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
hasCloseButton?: boolean;
onClose: () => void;
onConfirm: (value?: string) => void;
confirmButton?: ModalButtonProps;
cancelButton?: ModalButtonProps;
inputTitle?: string;
inputName?: string;
};
🤖 Prompt for AI Agents
In components/src/lib/Modal/Modal.tsx lines 8 to 21, rename the prop 'variant'
to 'type' to match the README documentation for consistency. Additionally,
update the onConfirm callback to accept and handle the input value from the
'prompt' type modal, ensuring user input is passed correctly when confirming.

Comment on lines +91 to +95
| 사이즈 | 설명 | 실제 너비 |
| -------- | ----------------------- | --------- |
| `small` | 기본 사이즈 (디폴트) | `304px` |
| `medium` | 넓은 콘텐츠용 | `600px` |
| `large` | 전체 너비 사용 (반응형) | `100%` |
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

사이즈 표에 오타가 있습니다.

사이즈 옵션 중 large의 실제 구현은 70%인데, 문서에는 100%라고 표기되어 있습니다. 또한 medium도 실제 구현은 40%인데 문서에는 600px로 표기되어 있습니다.

실제 구현에 맞게 문서를 수정하거나, 문서에 맞게 구현을 수정해야 합니다.

 | 사이즈   | 설명                    | 실제 너비 |
 | -------- | ----------------------- | --------- |
 | `small`  | 기본 사이즈 (디폴트)    | `304px`   |
-| `medium` | 넓은 콘텐츠용           | `600px`   |
-| `large`  | 전체 너비 사용 (반응형) | `100%`    |
+| `medium` | 넓은 콘텐츠용           | `40%`     |
+| `large`  | 넓은 화면 사용 (반응형) | `70%`     |

또는 구현을 문서에 맞게 수정:

const sizeMap: Record<"small" | "medium" | "large", string> = {
  small: "304px",
  medium: "600px",
  large: "100%",
};
🤖 Prompt for AI Agents
In components/README.md around lines 91 to 95, the size table documentation does
not match the actual implementation: 'large' is documented as 100% but
implemented as 70%, and 'medium' is documented as 600px but implemented as 40%.
To fix this, either update the documentation to reflect the actual implemented
sizes (large: 70%, medium: 40%) or modify the implementation to match the
documented sizes (large: 100%, medium: 600px). Choose one approach and ensure
both the code and documentation are consistent.

@yeji0214
Copy link
Copy Markdown
Member Author

안녕하세요 온스타 !
컨디션은 좀 괜찮아지셨나요 ?!

다음 미션을 먼저 진행하느라 리팩토링 반영이 조금 늦어졌네요오 🫠
미처 생각하지 못했던 부분들을 많이 배워가는 것 같아요 감사합니다 🙃

📦 리팩토링 내역

🧱 상수 처리

  • 카드 브랜드 식별 조건 상수 개선
    • 기존의 CARD_BRAND_START_NUMBER를 사용하지 않도록 수정
    • 카드 브랜드 정규식을 객체 형태로 관리하는 CARD_BRAND_REGEX 도입
    • detectCardCompany 함수는 해당 정규식을 순회하며 카드 브랜드를 탐지

🧼 중복 로직 분리

  • useCardNumberInput 내부의 중복 유효성 검사 로직을 validateAndSetErrorMessage로 분리
  • 카드 번호 변경 시 즉각적인 유효성 검사가 필요하여 useEffect를 유지

🧩 코드 구조 개선

  • 사용되지 않는 유효성 검사 함수 validateFirstCardNumbers 삭제
  • useCardBrand는 내부에 훅이 없어 유틸 함수 성격에 가깝기 때문에 해당 파일 제거 후 detectCardCompanycardBrandUtils로 이동
  • 삼항 연산자 방식으로 isValidCardNumber 계산 간소화

🧾 타입 정교화

  • 카드 브랜드 타입을 "VISA" | "MASTERCARD" | "DINERS" | "AMEX" | "UNIONPAY" 유니온 타입으로 한정

🧪 Input 컴포넌트 개선

  • value, onChange 등 추가 props를 받을 수 있도록 수정
  • 외부 상태로 제어 가능한 Controlled 컴포넌트 형태로 리팩토링

🧱 Button 컴포넌트 통합

  • ConfirmButton, CancelButton → 공통 Button 컴포넌트로 통합
  • Emotion 조건 분기(variant)를 통해 스타일 차이를 유연하게 처리

🧩 버튼 prop 구조화

  • confirmButton, cancelButton props는 ModalButtonProps 타입의 객체로 통합
  • 개별 prop 열거 방식 대신 하나의 버튼 속성 객체로 관리
  • ModalButtonProps 타입을 Button.tsx에서 export하여 외부 참조 가능하게 구성
  • 존재하지 않는 경우 기본 버튼 속성을 모달 내부에서 처리

🧠 handleBackdropClick에 기본 핸들러 부여

  • handleBackdropClick이 존재하지 않는 경우 기본적으로 onClose() 실행되도록 개선
  • 사용자 정의 핸들러를 통해 override 가능하도록 설계

💡 모달 접근성 및 UX 개선

  • ESC 키를 눌렀을 때 모달이 닫히도록 keydown 이벤트 처리 추가 (e.key === "Escape")
  • 모달이 열릴 때 document.body.style.overflow = "hidden"으로 body 스크롤 차단
  • 모달이 닫힐 때 기존 overflow 값으로 복원 (useEffect 클린업 함수 사용)

Copy link
Copy Markdown

@onschan onschan left a comment

Choose a reason for hiding this comment

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

안녕하세요. 에리얼!
주말 잘보내고 계신가요? 제가 의도드린 리뷰대로 반영 작업을 잘 진행해주셨네요!
이번 미션은 여기서 Appove 하도록 하겠습니다! 수고 많으셨어요 😊

@onschan onschan merged commit 0320b7a into woowacourse:yeji0214 May 18, 2025
1 check passed
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.

2 participants