Skip to content

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

Merged
JIN921 merged 28 commits into
developfrom
feat/WTH-410-어드민-회비-등록-온보딩-UI-구현
Jun 28, 2026

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"
Merged

[Feat] WTH-410: 어드민 회비 등록 온보딩 UI 구현#133
JIN921 merged 28 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단계(기본 정보 → 납부 대상 선택 → 이월 설정 → 계좌 공개 → 최종 확인)로 안내하는 화면을 추가했습니다.
    • 납부 대상에서 탭/검색/페이지 이동 및 선택·제외 관리를 지원합니다.
    • 시작 안내 모달과 단계별 뒤로/다음 이동을 제공합니다.
  • UI Improvements
    • 회비 화면의 검색바, 결제 상태/선택 배지, 공통 버튼·태그·테이블/체크박스·페이징 UI를 개선했습니다.
  • Bug Fixes
    • 랜딩 페이지 e2e 로딩 대기 로직을 안정화했습니다.
  • Tests
    • 관련 e2e 테스트 시나리오를 업데이트했습니다.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JIN921, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 40 minutes and 40 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7030f06-11a3-4e09-b430-294002d7eeb4

📥 Commits

Reviewing files that changed from the base of the PR and between bf59bd9 and 9eab223.

📒 Files selected for processing (1)
  • src/components/admin/dues/DuesSearchBar.tsx
📝 Walkthrough

Walkthrough

회비 설정 5단계 화면, 공용 UI, 설정 스토어/라우팅 헬퍼, 대시보드 연결, 세션 로그 문서와 Obsidian 정리, 랜딩 테스트 대기 로직이 변경되었습니다.

Changes

회비 설정 및 납부 화면

Layer / File(s) Summary
상태와 경로 헬퍼
src/constants/mock.ts, src/stores/useDuesSetupStore.ts, src/stores/index.ts, src/components/admin/dues/setup/useDuesSetupNavigation.ts, src/hooks/admin/usePaymentTargetFilter.ts, src/hooks/admin/index.ts
MOCK_PAYMENT_TARGETSMOCK_PREVIOUS_BALANCE가 추가되고, useDuesSetupStore의 persist 액션, 설정 경로 헬퍼, 납부 대상 필터 훅과 관련 재export가 노출됩니다.
공통 입력과 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/index.ts
뒤로가기 버튼, 단계 표시기, 폼 카드, 이전·다음 버튼, 이월 카드, 탭, 검색바가 추가됩니다.
납부 대상 필터와 표
src/components/admin/dues/setup/components/DuesMemberTable.tsx, src/components/admin/dues/setup/components/DuesPagination.tsx, src/components/admin/dues/modal/PaymentTargetModal.tsx
선택 상태를 반영하는 멤버 표, 설정 전용 페이지네이션, 납부 대상 모달이 추가됩니다.
1~5단계 설정 화면
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, 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/setup/DuesSetupStep5.tsx, src/app/(private)/[clubId]/admin/dues/setup/5/page.tsx, src/components/admin/dues/setup/components/SettingResultCardGrid.tsx, src/components/admin/dues/setup/index.ts
기본 정보, 납부 대상, 이월 설정, 계좌 공개, 최종 확인 단계와 해당 라우트 페이지, 결과 카드 그리드, setup 배럴 export가 추가됩니다.
대시보드 진입과 납부 현황
src/components/admin/dues/DuesPageContent.tsx, src/components/admin/dues/modal/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가 갱신됩니다.

문서와 Obsidian 정리

Layer / File(s) Summary
세션 로그와 Obsidian 정리
docs/로그/세션로그-JIN921-2026-06-25.md, docs/.obsidian/app.json, docs/.obsidian/appearance.json, docs/.obsidian/core-plugins.json, docs/.obsidian/graph.json, docs/.obsidian/workspace.json, .gitignore
회비 설정 구현 내용을 기록한 세션 로그 문서가 추가되고, Obsidian 상태 JSON 파일들과 ignore 규칙이 정리됩니다.

랜딩 테스트 대기

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🎨 Html&css

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.17% 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 필수 섹션을 모두 포함하고 핵심 변경사항도 충분히 구체적으로 작성되어 템플릿 요구를 대체로 충족합니다.
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 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

@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 thread src/components/admin/dues/DuesSearchBar.tsx
Comment thread src/components/admin/dues/setup/components/CarryOverCard.tsx
Comment thread src/components/admin/dues/setup/components/DuesMemberTable.tsx
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 thread src/components/admin/dues/setup/DuesSetupStep2.tsx Outdated
Comment thread src/components/admin/dues/setup/DuesSetupStep3.tsx
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.

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

Comment thread src/components/admin/dues/setup/DuesSetupStep5.tsx Outdated
Comment thread src/components/admin/dues/setup/DuesSetupStep3.tsx Outdated
Comment thread src/components/admin/dues/setup/components/DuesMemberTable.tsx Outdated
Comment thread src/components/admin/dues/modal/PaymentTargetModal.tsx Outdated

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 thread src/components/admin/dues/setup/components/DuesTabs.tsx
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.

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

Comment thread src/components/admin/dues/setup/components/SettingResultCardGrid.tsx Outdated
Comment thread src/components/admin/dues/setup/components/SettingResultCardGrid.tsx Outdated
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

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

@woneeeee woneeeee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

고생하셨습니다~!! 회비가 구현해야 할 부분들이 넘 많네요 ㅜㅜ

Comment thread src/components/admin/dues/setup/DuesSetupStep1.tsx
Comment thread src/components/admin/dues/setup/components/DuesTabs.tsx Outdated
Comment thread src/components/admin/dues/setup/components/DuesTabs.tsx Outdated
@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-4bb9wpbgv-weethsite-4975s-projects.vercel.app

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

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/dues/modal/PaymentTargetModal.tsx (1)

53-53: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

px-[71px]는 간격 토큰 클래스로 치환해주세요.

이 파일은 전반적으로 토큰 기반 간격 클래스를 쓰고 있는데 여기만 임의 픽셀 값이 남아 있습니다. 같은 모달 계열에서 사용하는 spacing token 클래스로 맞추는 편이 안전합니다.

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/admin/dues/modal/PaymentTargetModal.tsx` at line 53,
`PaymentTargetModal`의 scroll container에서만 남아 있는 `px-[71px]` 임의 픽셀 값을 제거하고, 이 모달
계열에서 사용 중인 spacing token 클래스로 교체하세요. `PaymentTargetModal`의 outer content
wrapper(현재 `scrollbar-custom flex ... px-[71px] ...`)를 찾아 전반적인 토큰 기반 간격 패턴과
일치하도록 수정하면 됩니다.

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 `@src/hooks/admin/usePaymentTargetFilter.ts`:
- Around line 21-24: The tab filtering logic in usePaymentTargetFilter is
treating every non-selected tab as the excluded list, so the all tab incorrectly
shows only unselected members. Update the byTab branching to handle the all tab
explicitly and return the full MOCK_PAYMENT_TARGETS list there, while keeping
selected and excluded behavior separate using selectedSet and
paymentTargetInfo.clubMemberId.

---

Outside diff comments:
In `@src/components/admin/dues/modal/PaymentTargetModal.tsx`:
- Line 53: `PaymentTargetModal`의 scroll container에서만 남아 있는 `px-[71px]` 임의 픽셀 값을
제거하고, 이 모달 계열에서 사용 중인 spacing token 클래스로 교체하세요. `PaymentTargetModal`의 outer
content wrapper(현재 `scrollbar-custom flex ... px-[71px] ...`)를 찾아 전반적인 토큰 기반 간격
패턴과 일치하도록 수정하면 됩니다.
🪄 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: 7cd91cda-8cdf-493d-83fd-bf985803633c

📥 Commits

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

📒 Files selected for processing (15)
  • .gitignore
  • src/components/admin/dues/DuesSearchBar.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/DuesSetupStep5.tsx
  • src/components/admin/dues/setup/components/CarryOverCard.tsx
  • src/components/admin/dues/setup/components/DuesMemberTable.tsx
  • src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx
  • src/components/admin/dues/setup/components/DuesTabs.tsx
  • src/components/admin/dues/setup/components/SettingResultCardGrid.tsx
  • src/hooks/admin/index.ts
  • src/hooks/admin/usePaymentTargetFilter.ts
  • src/stores/__tests__/useDuesSetupStore.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/hooks/admin/index.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/components/admin/dues/setup/components/CarryOverCard.tsx
  • src/components/admin/dues/setup/components/DuesTabs.tsx
  • src/components/admin/dues/DuesSearchBar.tsx
  • src/components/admin/dues/setup/DuesSetupStep5.tsx
  • src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx
  • src/components/admin/dues/setup/components/DuesMemberTable.tsx
  • src/components/admin/dues/setup/components/SettingResultCardGrid.tsx
  • src/components/admin/dues/setup/DuesSetupStep1.tsx

Comment on lines +21 to +24
const byTab =
tab === 'selected'
? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
: MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId));

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

all 탭이 실제로는 제외 멤버만 보여줍니다.

TabType'all'이 포함되어 있는데 여기 분기는 selected만 따로 처리하고 나머지를 모두 제외 목록으로 보내고 있습니다. 그래서 Step 2의 "전체" 탭이 전체가 아니라 제외 멤버 목록으로 동작합니다.

수정 예시
  const byTab =
-    tab === 'selected'
-      ? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
-      : MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId));
+    tab === 'selected'
+      ? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
+      : tab === 'excluded'
+        ? MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId))
+        : MOCK_PAYMENT_TARGETS;
📝 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 byTab =
tab === 'selected'
? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
: MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId));
const byTab =
tab === 'selected'
? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId))
: tab === 'excluded'
? MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId))
: MOCK_PAYMENT_TARGETS;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/admin/usePaymentTargetFilter.ts` around lines 21 - 24, The tab
filtering logic in usePaymentTargetFilter is treating every non-selected tab as
the excluded list, so the all tab incorrectly shows only unselected members.
Update the byTab branching to handle the all tab explicitly and return the full
MOCK_PAYMENT_TARGETS list there, while keeping selected and excluded behavior
separate using selectedSet and paymentTargetInfo.clubMemberId.

@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-9nv0bidk1-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 검증 결과

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

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

@JIN921 JIN921 merged commit 7ec5324 into develop Jun 28, 2026
5 checks passed
@JIN921 JIN921 deleted the feat/WTH-410-어드민-회비-등록-온보딩-UI-구현 branch June 28, 2026 14:06
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.

3 participants