[Feat] WTH-410: 어드민 회비 등록 온보딩 UI 구현#133
Hidden character warning
Conversation
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough회비 설정 5단계 화면, 공용 UI, 설정 스토어/라우팅 헬퍼, 대시보드 연결, 세션 로그 문서와 Obsidian 정리, 랜딩 테스트 대기 로직이 변경되었습니다. Changes회비 설정 및 납부 화면
문서와 Obsidian 정리
랜딩 테스트 대기
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과❌ TypeScript: 실패 |
PR E2E 테스트 결과❌ Playwright: 실패 |
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR E2E 테스트 결과✅ Playwright: 통과 🎉 E2E 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-hfpu5gb9v-weethsite-4975s-projects.vercel.app |
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR E2E 테스트 결과✅ Playwright: 통과 🎉 E2E 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-q5x9ps4ft-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
|
구현한 기능 Preview: https://weeth-p4bq8rn34-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
PR E2E 테스트 결과❌ Playwright: 실패 |
framer-motion 헤더 애니메이션이 hydration 전에 시작되어 클릭 시점에 요소가 이동 중일 수 있는 race condition 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
|
구현한 기능 Preview: https://weeth-7p6dqj7qy-weethsite-4975s-projects.vercel.app |
PR E2E 테스트 결과❌ Playwright: 실패 |
There was a problem hiding this comment.
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 | 🔵 TriviallocalStorage 지속화된 계좌 정보의 보안 고려사항
useDuesSetupStore의persist로 인해 계좌번호, 은행, 예금주 등의 금융 계좌 정보가 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렌더 시
page를totalPages범위로 클램프하는 것을 고려해주세요.
page는 탭/검색 변경 시에만 1로 초기화되므로, 모달이 열린 상태에서selectedMemberIdsprop이 줄어들면page가totalPages를 초과해 빈 페이지가 노출될 수 있습니다.♻️ 제안
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 | 🔵 TrivialTODO: 튜토리얼 모달 표시 조건 처리.
현재
tutorialOpen이useState(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도 실제 단계 값으로 고정해 주세요.지금은
number라0이나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가 공용 배지 스타일로 재사용되기 시작했는데24px와5px를 여기서 직접 고정하면 토큰 체계를 우회하게 됩니다.globals.css에 의미 있는 CSS 변수를 추가하고 이 유틸은 그 변수를 참조하도록 맞춰두는 편이 안전합니다.As per coding guidelines,
**/*.{ts,tsx,css}: Never hardcode design token values; always use token classes from design tokens, andsrc/app/globals.css: Define design tokens as CSS variables insrc/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
⛔ Files ignored due to path filters (1)
src/assets/image/dues_tutorial.pngis excluded by!**/*.png
📒 Files selected for processing (48)
docs/.obsidian/app.jsondocs/.obsidian/appearance.jsondocs/.obsidian/core-plugins.jsondocs/.obsidian/graph.jsondocs/.obsidian/workspace.jsondocs/로그/세션로그-JIN921-2026-06-25.mde2e/specs/landing.spec.tssrc/app/(private)/[clubId]/admin/dues/setup/1/page.tsxsrc/app/(private)/[clubId]/admin/dues/setup/2/page.tsxsrc/app/(private)/[clubId]/admin/dues/setup/3/page.tsxsrc/app/(private)/[clubId]/admin/dues/setup/4/page.tsxsrc/app/(private)/[clubId]/admin/dues/setup/5/page.tsxsrc/app/globals.csssrc/components/admin/dues/BackButton.tsxsrc/components/admin/dues/DuesMemberPaymentTable.tsxsrc/components/admin/dues/DuesPageContent.tsxsrc/components/admin/dues/DuesPaymentStatusPageContent.tsxsrc/components/admin/dues/DuesSearchBar.tsxsrc/components/admin/dues/DuesTransactionTable.tsxsrc/components/admin/dues/index.tssrc/components/admin/dues/modal/DuesTutorialModal.tsxsrc/components/admin/dues/modal/PaymentTargetModal.tsxsrc/components/admin/dues/setup/DuesSetupStep1.tsxsrc/components/admin/dues/setup/DuesSetupStep2.tsxsrc/components/admin/dues/setup/DuesSetupStep3.tsxsrc/components/admin/dues/setup/DuesSetupStep4.tsxsrc/components/admin/dues/setup/DuesSetupStep5.tsxsrc/components/admin/dues/setup/components/CarryOverCard.tsxsrc/components/admin/dues/setup/components/DuesMemberTable.tsxsrc/components/admin/dues/setup/components/DuesPagination.tsxsrc/components/admin/dues/setup/components/DuesSearchBar.tsxsrc/components/admin/dues/setup/components/DuesSetupStepIndicator.tsxsrc/components/admin/dues/setup/components/DuesTabs.tsxsrc/components/admin/dues/setup/components/FormCard.tsxsrc/components/admin/dues/setup/components/NextButton.tsxsrc/components/admin/dues/setup/components/PrevButton.tsxsrc/components/admin/dues/setup/components/SettingResultCardGrid.tsxsrc/components/admin/dues/setup/components/index.tssrc/components/admin/dues/setup/index.tssrc/components/admin/dues/setup/useDuesSetupNavigation.tssrc/components/admin/schedule/general/ScheduleTextField.tsxsrc/components/ui/Checkbox.tsxsrc/components/ui/index.tssrc/components/ui/pagination.tsxsrc/components/ui/table.tsxsrc/constants/mock.tssrc/stores/index.tssrc/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
| // 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()]); |
There was a problem hiding this comment.
🩺 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.
| // 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.
| @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); | ||
| } |
There was a problem hiding this comment.
📐 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.
| <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" |
There was a problem hiding this comment.
🎯 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.
| 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> | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
비활성 페이지 링크가 여전히 포커스 가능한 링크로 남습니다.
현재 src/components/admin/dues/setup/components/DuesPagination.tsx는 첫/마지막 페이지에서 pointer-events-none opacity-40만 넘겨 이전/다음 버튼을 막고 있습니다. 그래서 이 버튼들은 시각적으로만 비활성화되고, 키보드 포커스와 스크린리더에는 계속 활성 링크로 노출됩니다. PaginationLink 계층에서 disabled를 받아 aria-disabled와 tabIndex={-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 전달 흐름을 정리해 재사용 가능한 비활성 링크 계약으로 맞추면 됩니다.
| devtools( | ||
| persist( | ||
| combine(initialState, (set) => ({ | ||
| setField: (field: Partial<DuesSetupState>) => set(field, false, 'setField'), | ||
| reset: () => set(initialState, false, 'reset'), | ||
| })), | ||
| { name: 'duesSetup' }, |
There was a problem hiding this comment.
🗄️ 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
left a comment
There was a problem hiding this comment.
확인했습니다~~! 고생하셨어용 👍👍
회비 페이지 온보딩까지 넘 깔끔하고 예뿌네요 짱짱....
There was a problem hiding this comment.
온보딩 스텝 상태 관리 로직인 만큼 중요도가 높은 것 같아서,, 요 부분은 테스트 작성해두면 쪼금 더 안전하게 사용할 수 있을 것 같아요!
다만 추후 api 연동하시묜서 변동 잇을 수도 잇으니,,, 그때 작성해주셔도 괜찮을 것 같긴 합니당 ,,👍
| <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" | ||
| /> |
There was a problem hiding this comment.
요기 계좌번호 입력란에 숫자랑 '-' 기호만 입력 가능하도록 필터링 추가해주심 좋을 것 같아요!
| {remainingCount > 0 && ( | ||
| <AvatarGroupCount className="size-6 text-xs">+{remainingCount}</AvatarGroupCount> | ||
| )} |
woneeeee
left a comment
There was a problem hiding this comment.
고생하셨습니다~!! 회비가 구현해야 할 부분들이 넘 많네요 ㅜㅜ
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR E2E 테스트 결과✅ Playwright: 통과 🎉 E2E 테스트를 통과했습니다! |
PR 검증 결과✅ TypeScript: 통과 |
|
구현한 기능 Preview: https://weeth-4bb9wpbgv-weethsite-4975s-projects.vercel.app |
There was a problem hiding this comment.
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
📒 Files selected for processing (15)
.gitignoresrc/components/admin/dues/DuesSearchBar.tsxsrc/components/admin/dues/modal/PaymentTargetModal.tsxsrc/components/admin/dues/setup/DuesSetupStep1.tsxsrc/components/admin/dues/setup/DuesSetupStep2.tsxsrc/components/admin/dues/setup/DuesSetupStep3.tsxsrc/components/admin/dues/setup/DuesSetupStep5.tsxsrc/components/admin/dues/setup/components/CarryOverCard.tsxsrc/components/admin/dues/setup/components/DuesMemberTable.tsxsrc/components/admin/dues/setup/components/DuesSetupStepIndicator.tsxsrc/components/admin/dues/setup/components/DuesTabs.tsxsrc/components/admin/dues/setup/components/SettingResultCardGrid.tsxsrc/hooks/admin/index.tssrc/hooks/admin/usePaymentTargetFilter.tssrc/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
| const byTab = | ||
| tab === 'selected' | ||
| ? MOCK_PAYMENT_TARGETS.filter((t) => selectedSet.has(t.paymentTargetInfo.clubMemberId)) | ||
| : MOCK_PAYMENT_TARGETS.filter((t) => !selectedSet.has(t.paymentTargetInfo.clubMemberId)); |
There was a problem hiding this comment.
🎯 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.
| 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.
PR 테스트 결과✅ Jest: 통과 🎉 모든 테스트를 통과했습니다! |
PR E2E 테스트 결과✅ Playwright: 통과 🎉 E2E 테스트를 통과했습니다! |
|
구현한 기능 Preview: https://weeth-9nv0bidk1-weethsite-4975s-projects.vercel.app |
PR 검증 결과✅ TypeScript: 통과 🎉 모든 검증을 통과했습니다! |
✅ PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
회비 설정 온보딩 5단계 (
/[clubId]/admin/dues/setup/1~5/)DuesMemberTable+PaymentTargetModal(멤버 선택)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