Skip to content

[Feat] WTH-410: 어드민 회비 등록 온보딩 UI 구현#133

Open
JIN921 wants to merge 23 commits into
developfrom
feat/WTH-410-어드민-회비-등록-온보딩-UI-구현

Hidden character warning

The head ref may contain hidden characters: "feat/WTH-410-\uc5b4\ub4dc\ubbfc-\ud68c\ube44-\ub4f1\ub85d-\uc628\ubcf4\ub529-UI-\uad6c\ud604"
Open

[Feat] WTH-410: 어드민 회비 등록 온보딩 UI 구현#133
JIN921 wants to merge 23 commits into
developfrom
feat/WTH-410-어드민-회비-등록-온보딩-UI-구현

Conversation

@JIN921

@JIN921 JIN921 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호

  • Closed #410

✅ Key Changes

회비 설정 온보딩 5단계 (/[clubId]/admin/dues/setup/1~5/)

  • Step1: 온보딩 안내 (튜토리얼 이미지 포함)
  • Step2: 납부 대상 설정 — DuesMemberTable + PaymentTargetModal (멤버 선택)
  • Step3: 납부 금액 설정
  • Step4: 납부 기간 설정 (시작/마감일 입력)
  • Step5: 설정 결과 확인 — SettingResultCardGrid로 결과 요약 표시

설정 공통 컴포넌트

  • DuesSetupStepIndicator — 현재 진행 단계 시각 표시기
  • DuesMemberTable — 멤버 선택 테이블 (검색, 페이지네이션, 체크박스)
  • NextButton / PrevButton — 단계 이동 버튼
  • FormCard / CarryOverCard — 입력 폼 카드
  • DuesPagination — 멤버 테이블 페이지네이션
  • DuesTabs — 납부 대상 탭 컴포넌트
  • SettingResultCardGrid — 설정 결과 요약 그리드
  • BackButton — 회비 메인으로 돌아가는 뒤로가기 버튼

모달

  • DuesTutorialModal — 회비 최초 진입 시 설정 안내 모달
  • PaymentTargetModal — Step2에서 납부 대상 멤버를 선택하는 모달

공유 UI 컴포넌트 추가

  • Checkbox (src/components/ui/checkbox.tsx) — shadcn/ui 기반 체크박스
  • Pagination (src/components/ui/pagination.tsx) — 공용 페이지네이션

상태 관리

  • useDuesSetupStore (Zustand) — 설정 전체 상태 전역 관리
  • useDuesSetupNavigation — 설정 단계 간 라우팅 훅

📸 스크린샷 or 실행영상


🎸 기타 사항 or 추가 코멘트

제가 또 실수를 했습니다.... 납부 현황 브랜치에서 작업을 하고 push 한 다음에 알아차려서 복구를 열심히 했습니다...... 그래서 이게 #131 PR을 베이스로 만들어졋습니다.. 걔가 머지되어야 file changed가 저렇게 날뛰지 않을 것 입니다.. ㅜㅜㅜ #131 PR 머지가 끝나면 리뷰 부탁드려용...

머지 완료.. 온보딩 페이지가 많아서 변경사항이 많앗던 것이엇네요... 쪼개서 작업하기두 애매해 가지고... ㅜㅜ 지송합니다

Summary by CodeRabbit

  • New Features

    • 회비 설정을 5단계 안내형 화면으로 새롭게 제공해, 기본 정보 입력부터 대상 선택, 이월 설정, 계좌 공개, 최종 확인까지 이어지는 흐름을 지원합니다.
    • 납부 대상 목록에서 검색, 탭 필터, 페이지 이동, 선택/제외 확인이 가능해졌습니다.
    • 회비 설정 시작을 돕는 안내 모달과 뒤로/다음 이동 버튼이 추가되었습니다.
  • Bug Fixes

    • 랜딩 페이지 테스트 대기 로직을 개선해 화면 안정화 후 더 신뢰성 있게 동작하도록 조정했습니다.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

회비 설정 5단계 화면, 공통 UI, 설정 스토어/라우팅 헬퍼, 대시보드 튜토리얼·납부 현황 연결이 추가되었습니다. 세션 로그 문서와 랜딩 로그인 테스트 대기 로직도 함께 변경되었습니다.

Changes

회비 설정 및 납부 화면

Layer / File(s) Summary
설정 상태와 경로 헬퍼
src/constants/mock.ts, src/stores/useDuesSetupStore.ts, src/stores/index.ts, src/components/admin/dues/setup/useDuesSetupNavigation.ts
MOCK_PAYMENT_TARGETSMOCK_PREVIOUS_BALANCE가 추가되고, useDuesSetupStore의 persist 액션과 설정 경로 헬퍼가 노출된다.
공통 입력과 UI 기본 요소
src/app/globals.css, src/components/admin/schedule/general/ScheduleTextField.tsx, src/components/ui/Checkbox.tsx, src/components/ui/pagination.tsx, src/components/ui/table.tsx, src/components/ui/index.ts
tag-base 유틸, 오류 표시가 가능한 ScheduleTextField, Checkbox, 페이지네이션, 테이블 기본 스타일, UI 배럴 export가 추가된다.
설정 공용 컨트롤
src/components/admin/dues/BackButton.tsx, src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx, src/components/admin/dues/setup/components/FormCard.tsx, src/components/admin/dues/setup/components/NextButton.tsx, src/components/admin/dues/setup/components/PrevButton.tsx, src/components/admin/dues/setup/components/CarryOverCard.tsx, src/components/admin/dues/setup/components/DuesTabs.tsx, src/components/admin/dues/setup/components/DuesSearchBar.tsx
뒤로가기 버튼, 단계 표시기, 폼 카드, 이전·다음 버튼, 이월 카드, 탭, 검색바가 추가된다.
납부 대상 목록 도구
src/components/admin/dues/setup/components/DuesMemberTable.tsx, src/components/admin/dues/setup/components/DuesPagination.tsx
선택 상태를 반영하는 멤버 표와 설정 전용 페이지네이션이 추가된다.
1·2단계 화면
src/components/admin/dues/setup/DuesSetupStep1.tsx, src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx, src/components/admin/dues/setup/DuesSetupStep2.tsx, src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx
기본 정보 입력과 납부 대상 선택 화면이 스토어 검증, 최초 선택 초기화, 검색, 탭, 페이지네이션, 단계 이동과 함께 추가된다.
3·4단계 화면
src/components/admin/dues/setup/DuesSetupStep3.tsx, src/app/(private)/[clubId]/admin/dues/setup/3/page.tsx, src/components/admin/dues/setup/DuesSetupStep4.tsx, src/app/(private)/[clubId]/admin/dues/setup/4/page.tsx
이월 옵션 초기값, 설명 입력, 계좌 정보 검증, 공개 여부 토글, 단계 이동이 추가된다.
최종 확인
src/components/admin/dues/modal/PaymentTargetModal.tsx, src/components/admin/dues/setup/components/SettingResultCardGrid.tsx, src/components/admin/dues/setup/DuesSetupStep5.tsx, src/app/(private)/[clubId]/admin/dues/setup/5/page.tsx, src/components/admin/dues/setup/components/index.ts, src/components/admin/dues/setup/index.ts
예상 금액 요약, 납부 대상 모달, 결과 카드 그리드, 완료 후 초기화와 대시보드 이동, setup 배럴 export가 추가된다.
대시보드 진입과 납부 현황
src/components/admin/dues/DuesPageContent.tsx, src/components/admin/dues/DuesTutorialModal.tsx, src/components/admin/dues/DuesPaymentStatusPageContent.tsx, src/components/admin/dues/DuesSearchBar.tsx, src/components/admin/dues/DuesMemberPaymentTable.tsx, src/components/admin/dues/DuesTransactionTable.tsx, src/components/admin/dues/index.ts
튜토리얼 모달, 설정 진입, 뒤로가기, 납부 현황 검색, 멤버 표, 거래 태그, top-level export가 갱신된다.

세션 로그 문서

Layer / File(s) Summary
세션 로그 기록
docs/로그/세션로그-JIN921-2026-06-25.md
회비 설정 Step 1~4 구현 내용, mock 데이터, 라우팅, 공유 컴포넌트, TODO가 기록된다.

랜딩 테스트 대기

Layer / File(s) Summary
로그인 링크 대기
e2e/specs/landing.spec.ts
로그인 링크 조회 전에 networkidle 대기가 추가되고 기존 visible 대기가 제거된다.

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant DuesPageContent
  participant DuesTutorialModal
  participant useDuesSetupNavigation
  participant DuesSetupStep1
  participant useDuesSetupStore
  participant DuesSetupStep5

  Admin->>DuesPageContent: 총 회비 정보 입력 시작 클릭
  DuesPageContent->>DuesTutorialModal: open=true
  Admin->>DuesTutorialModal: 총 회비 정보 입력 시작하기 클릭
  DuesTutorialModal->>DuesPageContent: onStart()
  DuesPageContent->>useDuesSetupNavigation: goToStep(1)
  Admin->>DuesSetupStep1: 기본 정보 입력
  DuesSetupStep1->>useDuesSetupStore: setField(...)
  Admin->>DuesSetupStep5: 저장하고 완료하기 클릭
  DuesSetupStep5->>useDuesSetupStore: reset()
  DuesSetupStep5->>useDuesSetupNavigation: goToDues()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🎨 Html&css, 🔨 Refactor

Suggested reviewers

  • nabbang6
  • dalzzy

Poem

🐰 폴짝폴짝 회비 길이 열렸네
스토어엔 숫자와 이름이 반짝
탭과 검색창도 귀를 쫑긋 세우고
마지막엔 reset! 하고 다시 hop
달빛 아래 다음 step으로 뛰어간다

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 핵심 변경인 어드민 회비 온보딩 UI 구현을 짧고 명확하게 요약해 제목 기준에 부합합니다.
Description check ✅ Passed 필수 섹션(PR 유형, 이슈번호, Key Changes, 스크린샷, 기타사항)을 갖춘 구조로, 내용도 전반적으로 충분합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/WTH-410-어드민-회비-등록-온보딩-UI-구현

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

❤️ Share

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

@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@JIN921 JIN921 requested review from dalzzy, nabbang6 and woneeeee June 25, 2026 15:53
@JIN921 JIN921 self-assigned this Jun 25, 2026
@JIN921 JIN921 added 🐞 BugFix Something isn't working ✨ Feature 기능 개발 labels Jun 25, 2026
@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 실패
ESLint: 통과
Prettier: 실패
Build: 실패

⚠️ 일부 검증에 실패했습니다. 확인 후 수정해주세요.

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 실패

⚠️ E2E 테스트에 실패했습니다. 확인 후 수정해주세요.

@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 통과

🎉 E2E 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 실패
Build: 통과

⚠️ 일부 검증에 실패했습니다. 확인 후 수정해주세요.

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-hfpu5gb9v-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 통과

🎉 E2E 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-q5x9ps4ft-weethsite-4975s-projects.vercel.app

@JIN921 JIN921 force-pushed the feat/WTH-410-어드민-회비-등록-온보딩-UI-구현 branch from d7538ae to 8f4611e Compare June 26, 2026 06:04
@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 통과

🎉 E2E 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-a46kw4i7m-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-p4bq8rn34-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 실패

⚠️ E2E 테스트에 실패했습니다. 확인 후 수정해주세요.

framer-motion 헤더 애니메이션이 hydration 전에 시작되어
클릭 시점에 요소가 이동 중일 수 있는 race condition 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

PR 테스트 결과

Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

PR 검증 결과

TypeScript: 통과
ESLint: 통과
Prettier: 통과
Build: 통과

🎉 모든 검증을 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-7p6dqj7qy-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR E2E 테스트 결과

Playwright: 실패

⚠️ E2E 테스트에 실패했습니다. 확인 후 수정해주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/admin/schedule/general/ScheduleTextField.tsx (1)

26-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

오류 상태가 보조기술에 전달되지 않고 기본 포커스 표시도 사라집니다.

Line 33에서 focus:outline-none만 적용돼 정상 상태의 키보드 포커스가 보이지 않고, error가 있어도 aria-invalid/aria-describedby가 없어 스크린리더는 실패한 필드로 인지하지 못합니다. 이 컴포넌트가 Step 4 검증 입력에 바로 재사용되므로 접근성 처리를 여기서 같이 넣는 편이 안전합니다.

🔧 수정 예시
+import { useId } from 'react';
 import { cn } from '`@/lib/cn`';

 function ScheduleTextField({
   label,
   value,
   onChange,
   placeholder,
   maxLength,
   className,
   error,
 }: ScheduleTextFieldProps) {
+  const errorId = useId();
+
   return (
     <ScheduleFormField label={label}>
       <input
         type="text"
         value={value}
         onChange={(e) => onChange(e.target.value)}
         placeholder={placeholder}
         maxLength={maxLength}
+        aria-invalid={!!error}
+        aria-describedby={error ? errorId : undefined}
         className={cn(
-          'bg-container-neutral typo-body1 placeholder:text-text-alternative text-text-normal h-12 w-full rounded-sm px-400 py-300 focus:outline-none',
+          'bg-container-neutral typo-body1 placeholder:text-text-alternative text-text-normal h-12 w-full rounded-sm px-400 py-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-brand-primary',
           error && 'ring-state-error ring-1',
           className,
         )}
       />
       {(error || maxLength !== undefined) && (
         <div className="mt-100 flex items-center justify-between px-100">
-          {error ? <span className="typo-caption2 text-state-error">{error}</span> : <span />}
+          {error ? (
+            <span id={errorId} className="typo-caption2 text-state-error">
+              {error}
+            </span>
+          ) : (
+            <span />
+          )}
           {maxLength !== undefined && (
             <span className="typo-caption2 text-text-alternative">
               {value.length}/{maxLength}
             </span>
           )}
         </div>
       )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/schedule/general/ScheduleTextField.tsx` around lines 26
- 46, The ScheduleTextField input is missing accessibility state and hides the
default keyboard focus. In ScheduleTextField, keep a visible focus treatment
instead of relying on focus:outline-none alone, and add aria-invalid plus an
appropriate aria-describedby link when error text is rendered so assistive tech
can announce the invalid state. Use the input element and the error/message
block in ScheduleTextField to wire this up, and preserve the existing maxLength
counter behavior.
🧹 Nitpick comments (8)
docs/로그/세션로그-JIN921-2026-06-25.md (1)

72-80: 🔒 Security & Privacy | 🔵 Trivial

localStorage 지속화된 계좌 정보의 보안 고려사항

useDuesSetupStorepersist로 인해 계좌번호, 은행, 예금주 등의 금융 계좌 정보가 localStorage에 평문으로 저장됩니다. 브라우저 공유 환경에서 민감 데이터 노출 위험이 있으므로, 온보딩 완료 후 reset() 호출뿐 아니라 저장 시점 자체를 최소화하거나 세션 스토리지 대안을 검토하세요. 또한 reset()이 실제로 호출되는지 Step 5 구현에서 반드시 검증해야 합니다.

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

In `@docs/로그/세션로그-JIN921-2026-06-25.md` around lines 72 - 80, useDuesSetupStore의
persist로 인해 계좌번호/은행/예금주 같은 민감 정보가 localStorage에 평문 저장되는 문제가 있습니다.
useDuesSetupStore와 Step 5 완료 흐름을 점검해 저장 시점을 최소화하고, 가능하면 sessionStorage 대안이나 민감
필드 비지속화 방식으로 바꾸세요. 또한 온보딩 완료 시 reset()이 실제로 호출되는지 최종 확인 로직에서 반드시 검증해, 저장된 금융 정보가
남지 않도록 처리하세요.
src/components/admin/dues/modal/PaymentTargetModal.tsx (1)

50-51: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

렌더 시 pagetotalPages 범위로 클램프하는 것을 고려해주세요.

page는 탭/검색 변경 시에만 1로 초기화되므로, 모달이 열린 상태에서 selectedMemberIds prop이 줄어들면 pagetotalPages를 초과해 빈 페이지가 노출될 수 있습니다.

♻️ 제안
   const totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
-  const pagedTargets = filteredTargets.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
+  const safePage = Math.min(page, totalPages);
+  const pagedTargets = filteredTargets.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/modal/PaymentTargetModal.tsx` around lines 50 - 51,
Clamp the current page to the valid range before slicing targets, because `page`
can become larger than `totalPages` when `selectedMemberIds` shrinks while the
modal stays open. Update `PaymentTargetModal` where `totalPages` and
`pagedTargets` are derived so `page` is bounded to 1..`totalPages` on render,
and use that safe page value for the slice and any pagination UI state.
src/components/admin/dues/DuesPageContent.tsx (1)

141-142: 📐 Maintainability & Code Quality | 🔵 Trivial

TODO: 튜토리얼 모달 표시 조건 처리.

현재 tutorialOpenuseState(true)라 페이지 진입 시마다 모달이 항상 표시됩니다. 총 회비 미설정 상태에서만 노출되도록 게이팅이 필요합니다.

회비 설정 여부에 따라 모달 초기 상태를 결정하는 로직 구현을 도와드릴까요? 원하시면 추적용 이슈를 열어드리겠습니다.

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

In `@src/components/admin/dues/DuesPageContent.tsx` around lines 141 - 142, The
tutorial modal is always opening because tutorialOpen is initialized to true in
DuesPageContent. Update the initial state and/or add a conditional gate so the
modal only opens when the total dues information is not configured, using the
existing state/control flow around tutorialOpen and setTutorialOpen. Make the
visibility decision based on the dues setup status before rendering or on mount,
rather than unconditionally showing it on page entry.
src/components/admin/dues/setup/DuesSetupStep4.tsx (1)

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial

은행 선택 드롭다운 TODO.

bankName이 자유 입력 텍스트라 오타/표기 불일치로 이후 계좌 검증·매칭에 영향이 있을 수 있습니다. 드롭다운 구현을 도와드릴까요? 원하시면 이슈를 생성하겠습니다.

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

In `@src/components/admin/dues/setup/DuesSetupStep4.tsx` at line 84, Replace the
free-text bankName input in DuesSetupStep4 with a bank selection dropdown so
values are standardized and less error-prone. Update the DuesSetupStep4
component’s bankName handling to use a predefined list of banks instead of
arbitrary text entry, and keep the existing form state/validation flow intact
when the selected value changes.
src/components/admin/dues/setup/useDuesSetupNavigation.ts (1)

7-10: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

step을 1~5로 제한해 잘못된 setup 경로 생성을 막아주세요.

지금 goToStep(step: number)0이나 6도 그대로 허용해서 존재하지 않는 /setup/{step}로 push할 수 있습니다. 이 훅이 공용 진입점이라 여기서 막아두는 편이 안전합니다.

제안 코드
+const DUES_SETUP_STEPS = [1, 2, 3, 4, 5] as const;
+type DuesSetupStep = (typeof DUES_SETUP_STEPS)[number];
+
 function useDuesSetupNavigation() {
   const router = useRouter();
   const { clubId } = useParams<{ clubId: string }>();
 
-  const goToStep = (step: number) => router.push(`/${clubId}/admin/dues/setup/${step}`);
+  const goToStep = (step: DuesSetupStep) =>
+    router.push(`/${clubId}/admin/dues/setup/${step}`);
   const goToDues = () => router.push(`/${clubId}/admin/dues`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/useDuesSetupNavigation.ts` around lines 7 -
10, `useDuesSetupNavigation`의 `goToStep`가 모든 숫자를 그대로 `router.push`해서 존재하지 않는
setup 경로를 만들 수 있습니다. `goToStep(step: number)`에서 1~5 범위만 허용하도록 검증하고, 범위를 벗어나면
이동하지 않거나 안전한 기본 경로로 처리하세요. `goToStep`와 `goToDues`는 그대로 두되, 공용 진입점인 이 훅 내부에서 잘못된
`/${clubId}/admin/dues/setup/${step}` 생성이 차단되도록 수정하세요.
src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx (1)

6-19: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

currentStep도 실제 단계 값으로 고정해 주세요.

지금은 number0이나 6이 들어와도 전부 비활성 상태로 렌더링됩니다. STEPS에서 타입을 파생시키면 호출부 실수를 컴파일 타임에 막을 수 있습니다.

제안 코드
-const STEPS = [
+const STEPS = [
   { step: 1, label: '기본 정보' },
   { step: 2, label: '납부 대상' },
   { step: 3, label: '이월 설정' },
   { step: 4, label: '계좌 공개' },
   { step: 5, label: '최종 확인' },
-];
+] as const;
+
+type DuesSetupStep = (typeof STEPS)[number]['step'];
 
 interface DuesSetupStepIndicatorProps {
-  currentStep: number;
+  currentStep: DuesSetupStep;
   className?: string;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx` around
lines 6 - 19, `DuesSetupStepIndicator`의 `currentStep` 타입이 너무 넓어서 `0`이나 `6` 같은
잘못된 값이 들어와도 컴파일에서 잡히지 않습니다. `STEPS` 배열에서 단계 값 타입을 파생해
`DuesSetupStepIndicatorProps.currentStep`을 실제 허용 단계로 제한하고, 컴포넌트와 호출부가 그 타입을 따르도록
정리해 주세요.
src/app/globals.css (1)

559-564: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

공용 유틸에 새 px 상수를 직접 넣지 않는 편이 좋겠습니다.

tag-base가 공용 배지 스타일로 재사용되기 시작했는데 24px5px를 여기서 직접 고정하면 토큰 체계를 우회하게 됩니다. globals.css에 의미 있는 CSS 변수를 추가하고 이 유틸은 그 변수를 참조하도록 맞춰두는 편이 안전합니다.

As per coding guidelines, **/*.{ts,tsx,css}: Never hardcode design token values; always use token classes from design tokens, and src/app/globals.css: Define design tokens as CSS variables in src/app/globals.css.

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

In `@src/app/globals.css` around lines 559 - 564, The tag-base utility in
globals.css is hardcoding design values, so update it to use CSS variables
instead of direct px values. Add meaningful design token variables in
globals.css for the badge height and border radius, then change the tag-base
styles to reference those variables. Keep the change localized to the shared
utility and preserve the reusable styling through the existing tag-base
selector.

Source: Coding guidelines

src/components/ui/table.tsx (1)

81-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

bg-white 대신 토큰 배경 클래스를 쓰는 편이 좋겠습니다.

이 셀은 공용 table primitive라서 이후 테마 변경 영향을 크게 받습니다. bg-white는 디자인 토큰 체계를 우회하니, globals.css에 매핑된 배경 토큰 클래스 중 하나로 맞춰두는 편이 안전합니다.

As per coding guidelines, **/*.{ts,tsx,css}: Never hardcode design token values; always use token classes from design tokens.

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

In `@src/components/ui/table.tsx` at line 81, The table cell styling in the shared
table primitive currently hardcodes a white background, which bypasses the
design token system. Update the class list in the table component to use the
existing background token class from globals.css instead of bg-white, keeping
the same component structure and styling intent while aligning with the design
token guidelines.

Source: Coding guidelines

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

Inline comments:
In `@e2e/specs/landing.spec.ts`:
- Around line 13-22: The landing spec is waiting on
page.waitForLoadState('networkidle'), which is not a reliable signal for
framer-motion or UI readiness. Remove that wait in landing.spec.ts and instead
wait for the actual interactive element to be visible/ready before clicking,
using the existing getByRole selectors around the login flow (and the mobile
menu button when isMobile is true). Keep the Promise.all navigation pattern with
loginLink and page.waitForURL, but base readiness on UI visibility rather than
network idleness.

In `@src/app/globals.css`:
- Around line 557-570: The new `@utility` rule in globals.css is being flagged as
an unknown at-rule by Stylelint, so update the Stylelint configuration to allow
Tailwind v4’s `@utility` syntax. Adjust the relevant config entry that controls
at-rule validation, such as at-rule-no-unknown or its ignoreAtRules list, so
globals.css passes lint without treating `@utility` as an error.

In `@src/components/admin/dues/DuesSearchBar.tsx`:
- Around line 15-21: The search input in DuesSearchBar lacks an accessible name
because placeholder text is not reliable for assistive technologies. Update the
input element in DuesSearchBar to include an aria-label that describes the
search purpose, keeping the existing placeholder if desired so screen readers
can announce the field clearly.

In `@src/components/admin/dues/setup/components/CarryOverCard.tsx`:
- Around line 12-34: The carry-over choice UI is using plain buttons for
mutually exclusive selection, so the selected state is not exposed to assistive
tech. Update CarryOverCard to announce its state via the appropriate selection
semantics on the interactive element, and make the parent Step3 wrapper use a
radiogroup role so screen readers understand the relationship between options.
Use the existing selected, onClick, and title/description rendering in
CarryOverCard and the surrounding container to wire this up consistently.

In `@src/components/admin/dues/setup/components/DuesMemberTable.tsx`:
- Around line 65-80: The checkbox click in DuesMemberTable is triggering
toggleMember twice because both TableRow’s onClick and Checkbox’s
onCheckedChange fire for the same interaction. Update the DuesMemberTable row so
the checkbox interaction does not bubble to the row handler, using the existing
toggleMember and Checkbox/TableRow structure to locate the fix. Keep the row
click behavior for non-checkbox clicks, but stop event propagation from the
Checkbox cell so selection only toggles once.

In `@src/components/admin/dues/setup/components/DuesSearchBar.tsx`:
- Around line 11-20: Replace the hardcoded sizing classes in DuesSearchBar with
design token classes or CSS variables defined in src/app/globals.css, since
h-[48px], max-w-[339px], and pl-[52px] violate the token-based styling rule.
Update the wrapper div and input in DuesSearchBar to use existing spacing/sizing
tokens where possible; if no token exists, add the needed token in globals.css
first and then reference it from the TSX. Keep the same layout behavior while
removing all arbitrary px values.
- Around line 15-20: The search input in DuesSearchBar currently relies only on
placeholder text and removes the default focus ring with outline-none, so update
the input to include an accessible name via aria-label or a connected label and
restore a visible focus indicator using focus-visible styles while keeping the
existing search behavior intact.

In `@src/components/admin/dues/setup/DuesSetupStep2.tsx`:
- Around line 55-56: The pagination state in DuesSetupStep2 can drift past the
available results when filteredTargets shrinks, causing pagedTargets to become
empty and the table to look blank. Clamp page to the computed totalPages before
slicing, and use the normalized value consistently in DuesSetupStep2 so
DuesPagination receives the same currentPage value.

In `@src/components/admin/dues/setup/DuesSetupStep3.tsx`:
- Around line 35-42: The default carry-over selection in DuesSetupStep3’s
useEffect is inverted: it currently sets carryOverOption to 'none' when
hasPreviousBalance is true and 'carry' otherwise. Update the conditional in the
DuesSetupStep3 component so that a previous balance defaults to 'carry' and no
previous balance defaults to 'none', while keeping carryOverInitialized handling
unchanged.

In `@src/components/ui/pagination.tsx`:
- Around line 54-67: `PaginationPrevious`/`PaginationNext`가 시각적으로만 비활성화되고 키보드
포커스는 계속 가능한 상태입니다. `PaginationLink` 계층에서 `disabled`를 받을 수 있게 확장하고, 해당 상태일 때
`aria-disabled`와 `tabIndex={-1}`를 함께 적용하도록 처리하세요. `PaginationPrevious`,
`PaginationNext`, and `PaginationLink`의 props 전달 흐름을 정리해 재사용 가능한 비활성 링크 계약으로 맞추면
됩니다.

In `@src/stores/useDuesSetupStore.ts`:
- Around line 29-35: The persisted dues setup state is shared across clubs
because the store uses a fixed persist key in useDuesSetupStore, so draft
selections and initialization flags can leak between different clubId flows.
Update the persist configuration to scope the storage key by clubId (or another
club-specific identifier) so each club has isolated setup state, and ensure
transient fields like selectedMemberIds, account inputs, memberIdsInitialized,
and carryOverInitialized are either separated per club or excluded from
persistence as needed. Keep the fix within the persist/combine setup in
useDuesSetupStore so the Step 3 initialization logic remains correct per club.

---

Outside diff comments:
In `@src/components/admin/schedule/general/ScheduleTextField.tsx`:
- Around line 26-46: The ScheduleTextField input is missing accessibility state
and hides the default keyboard focus. In ScheduleTextField, keep a visible focus
treatment instead of relying on focus:outline-none alone, and add aria-invalid
plus an appropriate aria-describedby link when error text is rendered so
assistive tech can announce the invalid state. Use the input element and the
error/message block in ScheduleTextField to wire this up, and preserve the
existing maxLength counter behavior.

---

Nitpick comments:
In `@docs/로그/세션로그-JIN921-2026-06-25.md`:
- Around line 72-80: useDuesSetupStore의 persist로 인해 계좌번호/은행/예금주 같은 민감 정보가
localStorage에 평문 저장되는 문제가 있습니다. useDuesSetupStore와 Step 5 완료 흐름을 점검해 저장 시점을
최소화하고, 가능하면 sessionStorage 대안이나 민감 필드 비지속화 방식으로 바꾸세요. 또한 온보딩 완료 시 reset()이 실제로
호출되는지 최종 확인 로직에서 반드시 검증해, 저장된 금융 정보가 남지 않도록 처리하세요.

In `@src/app/globals.css`:
- Around line 559-564: The tag-base utility in globals.css is hardcoding design
values, so update it to use CSS variables instead of direct px values. Add
meaningful design token variables in globals.css for the badge height and border
radius, then change the tag-base styles to reference those variables. Keep the
change localized to the shared utility and preserve the reusable styling through
the existing tag-base selector.

In `@src/components/admin/dues/DuesPageContent.tsx`:
- Around line 141-142: The tutorial modal is always opening because tutorialOpen
is initialized to true in DuesPageContent. Update the initial state and/or add a
conditional gate so the modal only opens when the total dues information is not
configured, using the existing state/control flow around tutorialOpen and
setTutorialOpen. Make the visibility decision based on the dues setup status
before rendering or on mount, rather than unconditionally showing it on page
entry.

In `@src/components/admin/dues/modal/PaymentTargetModal.tsx`:
- Around line 50-51: Clamp the current page to the valid range before slicing
targets, because `page` can become larger than `totalPages` when
`selectedMemberIds` shrinks while the modal stays open. Update
`PaymentTargetModal` where `totalPages` and `pagedTargets` are derived so `page`
is bounded to 1..`totalPages` on render, and use that safe page value for the
slice and any pagination UI state.

In `@src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx`:
- Around line 6-19: `DuesSetupStepIndicator`의 `currentStep` 타입이 너무 넓어서 `0`이나 `6`
같은 잘못된 값이 들어와도 컴파일에서 잡히지 않습니다. `STEPS` 배열에서 단계 값 타입을 파생해
`DuesSetupStepIndicatorProps.currentStep`을 실제 허용 단계로 제한하고, 컴포넌트와 호출부가 그 타입을 따르도록
정리해 주세요.

In `@src/components/admin/dues/setup/DuesSetupStep4.tsx`:
- Line 84: Replace the free-text bankName input in DuesSetupStep4 with a bank
selection dropdown so values are standardized and less error-prone. Update the
DuesSetupStep4 component’s bankName handling to use a predefined list of banks
instead of arbitrary text entry, and keep the existing form state/validation
flow intact when the selected value changes.

In `@src/components/admin/dues/setup/useDuesSetupNavigation.ts`:
- Around line 7-10: `useDuesSetupNavigation`의 `goToStep`가 모든 숫자를 그대로
`router.push`해서 존재하지 않는 setup 경로를 만들 수 있습니다. `goToStep(step: number)`에서 1~5 범위만
허용하도록 검증하고, 범위를 벗어나면 이동하지 않거나 안전한 기본 경로로 처리하세요. `goToStep`와 `goToDues`는 그대로 두되,
공용 진입점인 이 훅 내부에서 잘못된 `/${clubId}/admin/dues/setup/${step}` 생성이 차단되도록 수정하세요.

In `@src/components/ui/table.tsx`:
- Line 81: The table cell styling in the shared table primitive currently
hardcodes a white background, which bypasses the design token system. Update the
class list in the table component to use the existing background token class
from globals.css instead of bg-white, keeping the same component structure and
styling intent while aligning with the design token guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d729dd7e-5107-482d-9e16-3ea8a6f0617c

📥 Commits

Reviewing files that changed from the base of the PR and between 57528b3 and ac32654.

⛔ Files ignored due to path filters (1)
  • src/assets/image/dues_tutorial.png is excluded by !**/*.png
📒 Files selected for processing (48)
  • docs/.obsidian/app.json
  • docs/.obsidian/appearance.json
  • docs/.obsidian/core-plugins.json
  • docs/.obsidian/graph.json
  • docs/.obsidian/workspace.json
  • docs/로그/세션로그-JIN921-2026-06-25.md
  • e2e/specs/landing.spec.ts
  • src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx
  • src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx
  • src/app/(private)/[clubId]/admin/dues/setup/3/page.tsx
  • src/app/(private)/[clubId]/admin/dues/setup/4/page.tsx
  • src/app/(private)/[clubId]/admin/dues/setup/5/page.tsx
  • src/app/globals.css
  • src/components/admin/dues/BackButton.tsx
  • src/components/admin/dues/DuesMemberPaymentTable.tsx
  • src/components/admin/dues/DuesPageContent.tsx
  • src/components/admin/dues/DuesPaymentStatusPageContent.tsx
  • src/components/admin/dues/DuesSearchBar.tsx
  • src/components/admin/dues/DuesTransactionTable.tsx
  • src/components/admin/dues/index.ts
  • src/components/admin/dues/modal/DuesTutorialModal.tsx
  • src/components/admin/dues/modal/PaymentTargetModal.tsx
  • src/components/admin/dues/setup/DuesSetupStep1.tsx
  • src/components/admin/dues/setup/DuesSetupStep2.tsx
  • src/components/admin/dues/setup/DuesSetupStep3.tsx
  • src/components/admin/dues/setup/DuesSetupStep4.tsx
  • src/components/admin/dues/setup/DuesSetupStep5.tsx
  • src/components/admin/dues/setup/components/CarryOverCard.tsx
  • src/components/admin/dues/setup/components/DuesMemberTable.tsx
  • src/components/admin/dues/setup/components/DuesPagination.tsx
  • src/components/admin/dues/setup/components/DuesSearchBar.tsx
  • src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx
  • src/components/admin/dues/setup/components/DuesTabs.tsx
  • src/components/admin/dues/setup/components/FormCard.tsx
  • src/components/admin/dues/setup/components/NextButton.tsx
  • src/components/admin/dues/setup/components/PrevButton.tsx
  • src/components/admin/dues/setup/components/SettingResultCardGrid.tsx
  • src/components/admin/dues/setup/components/index.ts
  • src/components/admin/dues/setup/index.ts
  • src/components/admin/dues/setup/useDuesSetupNavigation.ts
  • src/components/admin/schedule/general/ScheduleTextField.tsx
  • src/components/ui/Checkbox.tsx
  • src/components/ui/index.ts
  • src/components/ui/pagination.tsx
  • src/components/ui/table.tsx
  • src/constants/mock.ts
  • src/stores/index.ts
  • src/stores/useDuesSetupStore.ts
💤 Files with no reviewable changes (5)
  • docs/.obsidian/app.json
  • docs/.obsidian/appearance.json
  • docs/.obsidian/graph.json
  • docs/.obsidian/workspace.json
  • docs/.obsidian/core-plugins.json

Comment thread e2e/specs/landing.spec.ts
Comment on lines +13 to 22
// hydration 완료 후 framer-motion 헤더 애니메이션이 안정화될 때까지 대기
await page.waitForLoadState('networkidle');

if (isMobile) {
// 모바일: 로그인 링크가 Sheet 안에 있으므로 햄버거 메뉴 먼저 오픈
await page.getByRole('button', { name: '메뉴 열기' }).click();
}

const loginLink = page.getByRole('link', { name: '로그인' });
await loginLink.waitFor({ state: 'visible' });
await Promise.all([page.waitForURL(/\/login/), loginLink.click()]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

waitForLoadState('networkidle') 제거하고 UI 가시성 대기로 전환 필요

Line 14 의 waitForLoadState('networkidle')는 네트워크 유휴 상태만 판단할 뿐, 주석의 의도인 framer-motion 애니메이션 완료나 UI 상호작용 준비와는 직접적인 연관성이 없습니다. 네트워크 요청이 계속되거나 prefetch 등으로 인해 불필요한 대기가 발생하면 테스트가 불안정해질 수 있습니다. Playwright 의 자동 대기 기능을 활용하여 실제 조작 대상의 가시성을 기다리는 방식이 더 안정적입니다.

제안된 수정
-    // hydration 완료 후 framer-motion 헤더 애니메이션이 안정화될 때까지 대기
-    await page.waitForLoadState('networkidle');
-
-    if (isMobile) {
+    const loginLink = page.getByRole('link', { name: '로그인' });
+
+    if (isMobile) {
       // 모바일: 로그인 링크가 Sheet 안에 있으므로 햄버거 메뉴 먼저 오픈
-      await page.getByRole('button', { name: '메뉴 열기' }).click();
+      const menuButton = page.getByRole('button', { name: '메뉴 열기' });
+      await expect(menuButton).toBeVisible();
+      await menuButton.click();
     }

-    const loginLink = page.getByRole('link', { name: '로그인' });
+    await expect(loginLink).toBeVisible();
     await Promise.all([page.waitForURL(/\/login/), loginLink.click()]);
📝 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
// hydration 완료 후 framer-motion 헤더 애니메이션이 안정화될 때까지 대기
await page.waitForLoadState('networkidle');
if (isMobile) {
// 모바일: 로그인 링크가 Sheet 안에 있으므로 햄버거 메뉴 먼저 오픈
await page.getByRole('button', { name: '메뉴 열기' }).click();
}
const loginLink = page.getByRole('link', { name: '로그인' });
await loginLink.waitFor({ state: 'visible' });
await Promise.all([page.waitForURL(/\/login/), loginLink.click()]);
const loginLink = page.getByRole('link', { name: '로그인' });
if (isMobile) {
// 모바일: 로그인 링크가 Sheet 안에 있으므로 햄버거 메뉴 먼저 오픈
const menuButton = page.getByRole('button', { name: '메뉴 열기' });
await expect(menuButton).toBeVisible();
await menuButton.click();
}
await expect(loginLink).toBeVisible();
await Promise.all([page.waitForURL(/\/login/), loginLink.click()]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/specs/landing.spec.ts` around lines 13 - 22, The landing spec is waiting
on page.waitForLoadState('networkidle'), which is not a reliable signal for
framer-motion or UI readiness. Remove that wait in landing.spec.ts and instead
wait for the actual interactive element to be visible/ready before clicking,
using the existing getByRole selectors around the login flow (and the mobile
menu button when isMobile is true). Keep the Promise.all navigation pattern with
loginLink and page.waitForURL, but base readiness on UI visibility rather than
network idleness.

Comment thread src/app/globals.css
Comment on lines +557 to +570
@utility tag-base {
display: inline-flex;
height: 24px;
align-items: center;
justify-content: center;
border-radius: 5px;
padding-inline: var(--spacing-200);
padding-block: var(--spacing-100);
white-space: nowrap;
font-size: var(--caption1-size);
line-height: var(--caption1-line-height);
font-weight: var(--font-weight-semibold);
letter-spacing: var(--letter-spacing);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

@utility 추가만으로는 이 파일이 현재 lint-broken 상태입니다.

Tailwind v4 문법 의도는 이해되지만, 현재 정적 분석이 Line 557의 @utility를 unknown at-rule로 실패시키고 있습니다. Stylelint 설정에서 이 at-rule을 허용하지 않으면 PR이 계속 검사 단계에서 막힙니다.

#!/bin/bash
fd -HI 'stylelint*' . -x sed -n '1,220p' {}
rg -n '`@utility`|at-rule-no-unknown|scss/at-rule-no-unknown|ignoreAtRules' .
🧰 Tools
🪛 Stylelint (17.13.0)

[error] 557-557: Unexpected unknown at-rule "@utility" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

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

In `@src/app/globals.css` around lines 557 - 570, The new `@utility` rule in
globals.css is being flagged as an unknown at-rule by Stylelint, so update the
Stylelint configuration to allow Tailwind v4’s `@utility` syntax. Adjust the
relevant config entry that controls at-rule validation, such as
at-rule-no-unknown or its ignoreAtRules list, so globals.css passes lint without
treating `@utility` as an error.

Comment on lines +15 to +21
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="이름으로 검색하기"
className="typo-body2 placeholder:text-text-alternative text-text-strong h-full w-full bg-transparent pr-400 pl-[52px] outline-none"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

검색 입력에 접근 가능한 레이블이 없습니다.

placeholder는 보조기기에서 신뢰할 수 있는 접근성 이름으로 취급되지 않습니다. 스크린리더 사용자를 위해 aria-label을 추가하세요.

♻️ 제안 수정
       <input
         type="text"
         value={searchQuery}
         onChange={(e) => setSearchQuery(e.target.value)}
         placeholder="이름으로 검색하기"
+        aria-label="이름으로 검색하기"
         className="typo-body2 placeholder:text-text-alternative text-text-strong h-full w-full bg-transparent pr-400 pl-[52px] outline-none"
       />
📝 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
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="이름으로 검색하기"
className="typo-body2 placeholder:text-text-alternative text-text-strong h-full w-full bg-transparent pr-400 pl-[52px] outline-none"
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="이름으로 검색하기"
aria-label="이름으로 검색하기"
className="typo-body2 placeholder:text-text-alternative text-text-strong h-full w-full bg-transparent pr-400 pl-[52px] outline-none"
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/DuesSearchBar.tsx` around lines 15 - 21, The search
input in DuesSearchBar lacks an accessible name because placeholder text is not
reliable for assistive technologies. Update the input element in DuesSearchBar
to include an aria-label that describes the search purpose, keeping the existing
placeholder if desired so screen readers can announce the field clearly.

Comment on lines +12 to +34
<button
type="button"
onClick={onClick}
className={cn(
'flex flex-1 cursor-pointer items-center justify-between rounded-lg border p-400 text-left transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
<div className="flex flex-col gap-100">
<span className={cn('typo-sub3', selected ? 'text-brand-primary' : 'text-text-normal')}>
{title}
</span>
{selected && <span className="typo-caption2 text-text-alternative">{description}</span>}
</div>
<div
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
{selected && <div className="bg-brand-primary size-2.5 rounded-full" />}
</div>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

선택 상태를 보조기기에 전달해 주세요.

이 컴포넌트는 Step3에서 상호배타 선택지로 쓰이는데 지금은 단순 button이라 스크린리더가 어떤 항목이 선택됐는지 알 수 없습니다. 최소한 선택 상태를 노출하고, 상위 컨테이너도 radiogroup으로 맞춰주는 게 좋습니다.

제안 코드
     <button
       type="button"
       onClick={onClick}
+      role="radio"
+      aria-checked={selected}
       className={cn(
         'flex flex-1 cursor-pointer items-center justify-between rounded-lg border p-400 text-left transition-colors',
         selected ? 'border-brand-primary' : 'border-border',
       )}

상위 래퍼에는 role="radiogroup"도 함께 추가해 주세요.

📝 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
<button
type="button"
onClick={onClick}
className={cn(
'flex flex-1 cursor-pointer items-center justify-between rounded-lg border p-400 text-left transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
<div className="flex flex-col gap-100">
<span className={cn('typo-sub3', selected ? 'text-brand-primary' : 'text-text-normal')}>
{title}
</span>
{selected && <span className="typo-caption2 text-text-alternative">{description}</span>}
</div>
<div
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
{selected && <div className="bg-brand-primary size-2.5 rounded-full" />}
</div>
</button>
<button
type="button"
onClick={onClick}
role="radio"
aria-checked={selected}
className={cn(
'flex flex-1 cursor-pointer items-center justify-between rounded-lg border p-400 text-left transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
<div className="flex flex-col gap-100">
<span className={cn('typo-sub3', selected ? 'text-brand-primary' : 'text-text-normal')}>
{title}
</span>
{selected && <span className="typo-caption2 text-text-alternative">{description}</span>}
</div>
<div
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors',
selected ? 'border-brand-primary' : 'border-border',
)}
>
{selected && <div className="bg-brand-primary size-2.5 rounded-full" />}
</div>
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/components/CarryOverCard.tsx` around lines 12
- 34, The carry-over choice UI is using plain buttons for mutually exclusive
selection, so the selected state is not exposed to assistive tech. Update
CarryOverCard to announce its state via the appropriate selection semantics on
the interactive element, and make the parent Step3 wrapper use a radiogroup role
so screen readers understand the relationship between options. Use the existing
selected, onClick, and title/description rendering in CarryOverCard and the
surrounding container to wire this up consistently.

Comment on lines +65 to +80
<TableRow
key={targetId}
className={readOnly ? undefined : 'cursor-pointer'}
onClick={readOnly ? undefined : () => toggleMember?.(clubMemberId)}
>
{!readOnly && (
<TableCell className="text-center">
<Checkbox
color="primary"
id={`select-member-${clubMemberId}`}
name={`select-member-${clubMemberId}`}
checked={isSelected}
onCheckedChange={() => toggleMember?.(clubMemberId)}
/>
</TableCell>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'Checkbox.tsx' src/components/ui --exec cat -n {}

Repository: Team-Weeth/weeth-client

Length of output: 1997


체크박스 클릭 시 토글이 두 번 실행되어 선택 상태가 변경되지 않습니다.

TableRowonClickCheckboxonCheckedChange가 동일한 toggleMember를 호출합니다. 체크박스를 클릭하면 이벤트가 셀을 거쳐 TableRow로 전파되므로 핸들러가 두 번 실행되어 선택 상태가 반전되다가 다시 원상복구됩니다.

src/components/ui/Checkbox.tsx는 Radix UI 위rapper로 내부에서 이벤트 전파를 차단하지 않습니다. 따라서 체크박스 셀 단에서 이벤트 전파를 막아야 합니다.

🐛 제안 수정
               {!readOnly && (
-                <TableCell className="text-center">
+                <TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
                   <Checkbox
                     color="primary"
                     id={`select-member-${clubMemberId}`}
                     name={`select-member-${clubMemberId}`}
                     checked={isSelected}
                     onCheckedChange={() => toggleMember?.(clubMemberId)}
                   />
                 </TableCell>
               )}
📝 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
<TableRow
key={targetId}
className={readOnly ? undefined : 'cursor-pointer'}
onClick={readOnly ? undefined : () => toggleMember?.(clubMemberId)}
>
{!readOnly && (
<TableCell className="text-center">
<Checkbox
color="primary"
id={`select-member-${clubMemberId}`}
name={`select-member-${clubMemberId}`}
checked={isSelected}
onCheckedChange={() => toggleMember?.(clubMemberId)}
/>
</TableCell>
)}
<TableRow
key={targetId}
className={readOnly ? undefined : 'cursor-pointer'}
onClick={readOnly ? undefined : () => toggleMember?.(clubMemberId)}
>
{!readOnly && (
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox
color="primary"
id={`select-member-${clubMemberId}`}
name={`select-member-${clubMemberId}`}
checked={isSelected}
onCheckedChange={() => toggleMember?.(clubMemberId)}
/>
</TableCell>
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/components/DuesMemberTable.tsx` around lines
65 - 80, The checkbox click in DuesMemberTable is triggering toggleMember twice
because both TableRow’s onClick and Checkbox’s onCheckedChange fire for the same
interaction. Update the DuesMemberTable row so the checkbox interaction does not
bubble to the row handler, using the existing toggleMember and Checkbox/TableRow
structure to locate the fix. Keep the row click behavior for non-checkbox
clicks, but stop event propagation from the Checkbox cell so selection only
toggles once.

Comment on lines +15 to +20
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="이름으로 검색하기"
className="typo-body2 placeholder:text-text-alternative text-text-strong h-full w-full bg-transparent pr-400 pl-[52px] outline-none"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

검색 입력의 접근성 이름과 포커스 표시가 없습니다.

현재 입력은 placeholder만 있고 outline-none으로 기본 포커스 표시까지 제거해서, 스크린리더와 키보드 사용자 모두 탐색성이 떨어집니다. aria-label(또는 연결된 <label>)을 추가하고, 토큰 기반의 focus-visible 스타일을 남겨 주세요.

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

In `@src/components/admin/dues/setup/components/DuesSearchBar.tsx` around lines 15
- 20, The search input in DuesSearchBar currently relies only on placeholder
text and removes the default focus ring with outline-none, so update the input
to include an accessible name via aria-label or a connected label and restore a
visible focus indicator using focus-visible styles while keeping the existing
search behavior intact.

Comment on lines +55 to +56
const totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
const pagedTargets = filteredTargets.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

필터 결과가 줄어들면 빈 페이지가 표시될 수 있습니다.

pagetotalPages로 제한되지 않습니다. 예를 들어 '선택됨' 탭에서 멤버를 해제하면 filteredTargets가 줄어들어 현재 pagetotalPages를 초과할 수 있고, 이때 pagedTargets가 빈 배열이 되어 테이블이 비어 보입니다.

pagetotalPages 범위로 보정하세요.

🐛 제안 수정
   const totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
-  const pagedTargets = filteredTargets.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
+  const currentPage = Math.min(page, totalPages);
+  const pagedTargets = filteredTargets.slice(
+    (currentPage - 1) * PAGE_SIZE,
+    currentPage * PAGE_SIZE,
+  );

DuesPagination에 전달하는 page 값도 currentPage로 맞추는 것을 함께 고려하세요.

📝 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 totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
const pagedTargets = filteredTargets.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
const totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
const currentPage = Math.min(page, totalPages);
const pagedTargets = filteredTargets.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/DuesSetupStep2.tsx` around lines 55 - 56, The
pagination state in DuesSetupStep2 can drift past the available results when
filteredTargets shrinks, causing pagedTargets to become empty and the table to
look blank. Clamp page to the computed totalPages before slicing, and use the
normalized value consistently in DuesSetupStep2 so DuesPagination receives the
same currentPage value.

Comment on lines +35 to +42
useEffect(() => {
if (!carryOverInitialized) {
setField({
carryOverOption: hasPreviousBalance ? 'none' : 'carry',
carryOverInitialized: true,
});
}
}, [carryOverInitialized, hasPreviousBalance, setField]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

이월 기본값 로직이 반전되어 있어 수정이 필요합니다.

현재 useEffect 는 잔액이 있을 때 (hasPreviousBalance === true) 기본값을 'none'(이월하지 않음) 으로, 잔액이 없을 때 'carry'(이월함) 로 설정합니다.

잔액이 존재하는 상황에서 이월을 기본값으로 두는 것이 일반적인 비즈니스 로직이며, 잔액이 없는 경우 이월은 무의미합니다. 따라서 조건식을 반전시켜야 합니다.

- carryOverOption: hasPreviousBalance ? 'none' : 'carry',
+ carryOverOption: hasPreviousBalance ? 'carry' : 'none',
📝 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
useEffect(() => {
if (!carryOverInitialized) {
setField({
carryOverOption: hasPreviousBalance ? 'none' : 'carry',
carryOverInitialized: true,
});
}
}, [carryOverInitialized, hasPreviousBalance, setField]);
useEffect(() => {
if (!carryOverInitialized) {
setField({
carryOverOption: hasPreviousBalance ? 'carry' : 'none',
carryOverInitialized: true,
});
}
}, [carryOverInitialized, hasPreviousBalance, setField]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/dues/setup/DuesSetupStep3.tsx` around lines 35 - 42, The
default carry-over selection in DuesSetupStep3’s useEffect is inverted: it
currently sets carryOverOption to 'none' when hasPreviousBalance is true and
'carry' otherwise. Update the conditional in the DuesSetupStep3 component so
that a previous balance defaults to 'carry' and no previous balance defaults to
'none', while keeping carryOverInitialized handling unchanged.

Comment on lines +54 to +67
function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink aria-label="이전 페이지" className={cn('size-8', className)} {...props}>
<ChevronLeftIcon className="size-4" />
</PaginationLink>
);
}

function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink aria-label="다음 페이지" className={cn('size-8', className)} {...props}>
<ChevronRightIcon className="size-4" />
</PaginationLink>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

비활성 페이지 링크가 여전히 포커스 가능한 링크로 남습니다.

현재 src/components/admin/dues/setup/components/DuesPagination.tsx는 첫/마지막 페이지에서 pointer-events-none opacity-40만 넘겨 이전/다음 버튼을 막고 있습니다. 그래서 이 버튼들은 시각적으로만 비활성화되고, 키보드 포커스와 스크린리더에는 계속 활성 링크로 노출됩니다. PaginationLink 계층에서 disabled를 받아 aria-disabledtabIndex={-1}까지 같이 처리하는 쪽이 재사용 컴포넌트 계약에 맞습니다.

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

In `@src/components/ui/pagination.tsx` around lines 54 - 67,
`PaginationPrevious`/`PaginationNext`가 시각적으로만 비활성화되고 키보드 포커스는 계속 가능한 상태입니다.
`PaginationLink` 계층에서 `disabled`를 받을 수 있게 확장하고, 해당 상태일 때 `aria-disabled`와
`tabIndex={-1}`를 함께 적용하도록 처리하세요. `PaginationPrevious`, `PaginationNext`, and
`PaginationLink`의 props 전달 흐름을 정리해 재사용 가능한 비활성 링크 계약으로 맞추면 됩니다.

Comment on lines +29 to +35
devtools(
persist(
combine(initialState, (set) => ({
setField: (field: Partial<DuesSetupState>) => set(field, false, 'setField'),
reset: () => set(initialState, false, 'reset'),
})),
{ name: 'duesSetup' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

클럽 간에 설정 초안이 섞일 수 있습니다.

persist 키가 고정(duesSetup)이라 /[clubId]/admin/dues/setup/*의 진행 상태가 클럽별로 분리되지 않습니다. 지금처럼 selectedMemberIds, 계좌 정보, memberIdsInitialized, carryOverInitialized까지 함께 저장하면 다른 clubId로 들어가도 이전 클럽의 선택값이 재사용되고, Step 3의 초기화 가드도 다시 돌지 않습니다. 이 플로우는 clubId 기준으로 저장 키를 분리하거나, 최소한 초기화 플래그/임시 입력값은 persist 대상에서 제외해야 합니다.

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

In `@src/stores/useDuesSetupStore.ts` around lines 29 - 35, The persisted dues
setup state is shared across clubs because the store uses a fixed persist key in
useDuesSetupStore, so draft selections and initialization flags can leak between
different clubId flows. Update the persist configuration to scope the storage
key by clubId (or another club-specific identifier) so each club has isolated
setup state, and ensure transient fields like selectedMemberIds, account inputs,
memberIdsInitialized, and carryOverInitialized are either separated per club or
excluded from persistence as needed. Keep the fix within the persist/combine
setup in useDuesSetupStore so the Step 3 initialization logic remains correct
per club.

@nabbang6 nabbang6 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

확인했습니다~~! 고생하셨어용 👍👍
회비 페이지 온보딩까지 넘 깔끔하고 예뿌네요 짱짱....

);

const displayedAvatars = selectedTargets.slice(0, MAX_AVATAR_DISPLAY);
const remainingCount = selectedTargets.length - MAX_AVATAR_DISPLAY;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요기 remainingCount에 음수는 들어가지 못하게 Math.max(0, ...) 처리해주면 쪼금 더 안전하게 쓸 수 있을 것 같습니당!

} from '@/components/admin/dues/setup/components';
import { useDuesSetupNavigation } from '@/components/admin/dues/setup/useDuesSetupNavigation';

import { ScheduleTextareaField } from '@/components/admin/schedule/general/ScheduleTextareaField';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기 미사용된 임포트는 제거해주셔도 될 것 같아요~!

Comment on lines +1 to +11
import {
Avatar,
AvatarFallback,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui';
import { Checkbox } from '@/components/ui';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

별건 아니지만,,, 여기 Checkbox 임포트 윗줄이랑 합쳐서 적어주면 좋을 것 같아용!

Comment on lines +42 to +61
const byTab =
tab === 'selected'
? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
: MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId));
const filteredTargets = search.trim()
? byTab.filter((t) => t.paymentTargetInfo.name.includes(search.trim()))
: byTab;

const totalPages = Math.max(1, Math.ceil(filteredTargets.length / PAGE_SIZE));
const pagedTargets = filteredTargets.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

const handleTabChange = (next: TabType) => {
setTab(next);
setPage(1);
};

const handleSearch = (value: string) => {
setSearch(value);
setPage(1);
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

해당 코드에서 탭 필터링 + 검색 + 페이지네이션 로직은 DuesSetupStep2에서도 유사하게 사용되고 잇는 것 같습니다! usePaymentTargetFilter 같은 훅으로 추출해서 사용하게 해주면 더 깔끔할 것 같아용,, b

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

온보딩 스텝 상태 관리 로직인 만큼 중요도가 높은 것 같아서,, 요 부분은 테스트 작성해두면 쪼금 더 안전하게 사용할 수 있을 것 같아요!
다만 추후 api 연동하시묜서 변동 잇을 수도 잇으니,,, 그때 작성해주셔도 괜찮을 것 같긴 합니당 ,,👍

Comment on lines +22 to +27
className={cn(
'typo-body2 cursor-pointer rounded-sm border px-300 py-200 transition-colors',
activeTab === key
? 'bg-container-neutral-alternative text-text-strong border-transparent'
: 'border-border text-text-alternative bg-transparent',
)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요기 피그마에선 타이포가 button2로 되어 잇는 것 같아요!

Comment on lines +71 to +83
<ScheduleTextField
label="계좌번호"
value={accountNumber}
onChange={(value) => {
setField({ accountNumber: value });
if (errors.accountNumber)
setErrors((prev) => ({ ...prev, accountNumber: undefined }));
}}
placeholder="계좌번호를 입력해주세요"
maxLength={20}
error={errors.accountNumber}
className="bg-container-neutral-alternative"
/>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요기 계좌번호 입력란에 숫자랑 '-' 기호만 입력 가능하도록 필터링 추가해주심 좋을 것 같아요!

return (
<Card className="shadow-none">
<div className="flex items-center justify-between">
<span className="typo-sub3 text-text-strong">{title}</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기 인포카드 타이틀도 typo-sub1, text-normal 색상으로 바꿔주셔야 될 것 같습니당!

Comment on lines +16 to +18
<span className="typo-body2 text-text-alternative">{label}</span>
<span className={cn('typo-body2 text-text-strong', valueClassName)}>{value}</span>
</div>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

label 타이포는 sub3으로, value 타이포도 sub3 + text-alternative로 바꿔주심 좋을 것 같아용!

Comment on lines +150 to +152
{remainingCount > 0 && (
<AvatarGroupCount className="size-6 text-xs">+{remainingCount}</AvatarGroupCount>
)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Image

피그마에는 이렇게 ... 표시로 되어 잇긴 한데 요렇게 인원 숫자 보여주니까 직관적이라 더 조은 것 같긴 하네용 흠...
일단 유지하고 담주에 디자인 분들께 요런 건 우떤지 여쭤봐도 좋을 것 같은... ㅎ_ㅎ

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

Labels

🐞 BugFix Something isn't working ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants