From 31bb2c249df197e9ccc8cb285476dbc62ad0afb7 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 24 Jun 2026 17:54:58 +0900 Subject: [PATCH 01/28] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20ste?= =?UTF-8?q?p1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[clubId]/admin/dues/setup/1/page.tsx | 5 + src/components/admin/dues/BackButton.tsx | 21 ++ src/components/admin/dues/DuesPageContent.tsx | 3 +- .../dues/DuesPaymentStatusPageContent.tsx | 10 +- src/components/admin/dues/index.ts | 2 + .../admin/dues/setup/DuesSetupStep1.tsx | 180 ++++++++++++++++++ .../dues/setup/DuesSetupStepIndicator.tsx | 59 ++++++ src/components/admin/dues/setup/FormCard.tsx | 20 ++ src/components/admin/dues/setup/index.ts | 2 + src/stores/index.ts | 6 + src/stores/useDuesSetupStore.ts | 44 +++++ 11 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx create mode 100644 src/components/admin/dues/BackButton.tsx create mode 100644 src/components/admin/dues/setup/DuesSetupStep1.tsx create mode 100644 src/components/admin/dues/setup/DuesSetupStepIndicator.tsx create mode 100644 src/components/admin/dues/setup/FormCard.tsx create mode 100644 src/components/admin/dues/setup/index.ts create mode 100644 src/stores/useDuesSetupStore.ts diff --git a/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx b/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx new file mode 100644 index 00000000..019e89cd --- /dev/null +++ b/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx @@ -0,0 +1,5 @@ +import { DuesSetupStep1 } from '@/components/admin/dues/setup'; + +export default function DuesSetupStep1Page() { + return ; +} diff --git a/src/components/admin/dues/BackButton.tsx b/src/components/admin/dues/BackButton.tsx new file mode 100644 index 00000000..83c09543 --- /dev/null +++ b/src/components/admin/dues/BackButton.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { BackIcon } from '@/assets/icons'; +import { Icon } from '@/components/ui'; +import { useRouter } from 'next/router'; + +function BackButton() { + const router = useRouter(); + return ( + + ); +} + +export { BackButton }; diff --git a/src/components/admin/dues/DuesPageContent.tsx b/src/components/admin/dues/DuesPageContent.tsx index 135c288c..73f45581 100644 --- a/src/components/admin/dues/DuesPageContent.tsx +++ b/src/components/admin/dues/DuesPageContent.tsx @@ -9,13 +9,14 @@ import { useCardinalSelector } from '@/hooks'; import { DuesTopBar } from './DuesTopBar'; import { DuesBalanceCard } from './DuesBalanceCard'; import { DuesChart } from './DuesChart'; -import { DuesTransactionTable } from './DuesTransactionTable'; + import { DuesGenerationFilter } from './DuesGenerationFilter'; import { AddTransactionModal } from './modal/AddTransactionModal'; import { EditTransactionModal } from './modal/EditTransactionModal'; import { TransactionDetailModal } from './modal/TransactionDetailModal'; import type { TransactionDetail } from './modal/TransactionDetailModal'; import type { TransactionFormData } from './modal/TransactionForm'; +import { DuesTransactionTable } from './DuesTransactionTable'; const MOCK_MONTHLY_DATA: MonthlyData[] = [ { month: '3월', amount: 1425000 }, diff --git a/src/components/admin/dues/DuesPaymentStatusPageContent.tsx b/src/components/admin/dues/DuesPaymentStatusPageContent.tsx index ff5863c0..ee637ddc 100644 --- a/src/components/admin/dues/DuesPaymentStatusPageContent.tsx +++ b/src/components/admin/dues/DuesPaymentStatusPageContent.tsx @@ -12,6 +12,7 @@ import { useCardinalSelector } from '@/hooks'; import { DuesMemberPaymentTable, type DuesMember } from './DuesMemberPaymentTable'; import { DuesPaymentSummaryCard } from './DuesPaymentSummaryCard'; +import { BackButton } from './BackButton'; const MOCK_MEMBERS: DuesMember[] = [ { id: 1, name: '김위드', major: '경영학과', phone: '010-1234-1234', status: 'unpaid' }, @@ -158,14 +159,7 @@ function DuesPaymentStatusPageContent() {
{/* 헤더 */}
- +

{generationLabel} 회비 납부 현황

diff --git a/src/components/admin/dues/index.ts b/src/components/admin/dues/index.ts index f80d554b..e5ffe9f2 100644 --- a/src/components/admin/dues/index.ts +++ b/src/components/admin/dues/index.ts @@ -27,3 +27,5 @@ export { type TransactionDetailModalProps, } from './modal/TransactionDetailModal'; export { type TransactionFormData } from './modal/TransactionForm'; + +export { BackButton } from './BackButton'; diff --git a/src/components/admin/dues/setup/DuesSetupStep1.tsx b/src/components/admin/dues/setup/DuesSetupStep1.tsx new file mode 100644 index 00000000..848e4b16 --- /dev/null +++ b/src/components/admin/dues/setup/DuesSetupStep1.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useState } from 'react'; + +import Image from 'next/image'; +import { useRouter, useParams } from 'next/navigation'; + +import { ArrowRightIcon } from '@/assets/icons'; +import { cn } from '@/lib/cn'; +import { useDuesSetupActions, useDuesSetupValues } from '@/stores/useDuesSetupStore'; + +import { DuesSetupStepIndicator } from './DuesSetupStepIndicator'; +import { BackButton } from '../BackButton'; + +const NAME_MAX = 30; +const DESCRIPTION_MAX = 30; + +function DuesSetupStep1() { + const router = useRouter(); + const { clubId } = useParams<{ clubId: string }>(); + const { amount, name, description, generationNumber } = useDuesSetupValues(); + const { setField } = useDuesSetupActions(); + + const [errors, setErrors] = useState<{ amount?: string; name?: string }>({}); + + const handleNext = () => { + const next: { amount?: string; name?: string } = {}; + if (!amount || Number(amount) === 0) next.amount = '회비 금액을 입력해주세요'; + if (!name.trim()) next.name = '회비 이름을 입력해주세요'; + setErrors(next); + if (Object.keys(next).length > 0) return; + router.push(`/${clubId}/admin/dues/setup/2`); + }; + + return ( +
+ {/* 헤더 */} +
+ +

{generationNumber}기 총 회비 설정

+
+ +
+ {/* 스텝 인디케이터 */} + + + {/* 폼 카드 */} +
+ {/* 섹션 헤더 */} +
+ 기본 정보 (1/5) +

총 회비의 기본 정보를 입력해주세요

+
+ + {/* 필드 행: 회비금액 + 회비 이름 */} +
+ {/* 1인당 회비금액 */} +
+ +
+
+ { + const raw = e.target.value.replace(/\D/g, ''); + setField({ amount: raw }); + if (errors.amount) setErrors((prev) => ({ ...prev, amount: undefined })); + }} + placeholder="0" + className="typo-sub3 placeholder:text-text-alternative text-text-alternative min-w-0 flex-1 bg-transparent focus:outline-none" + /> + +
+
+ {errors.amount ? ( + {errors.amount} + ) : ( + + 회비 금액은 등록 후에도 수정할 수 있습니다. + + )} +
+
+
+ + {/* 회비 이름 */} +
+ +
+ { + setField({ name: e.target.value.slice(0, NAME_MAX) }); + if (errors.name) setErrors((prev) => ({ ...prev, name: undefined })); + }} + placeholder={`${generationNumber}기 정기회비`} + className={cn( + 'bg-container-neutral-alternative typo-body1 placeholder:text-text-alternative text-text-normal h-12 w-full rounded-sm px-400 py-300 focus:outline-none', + errors.name && 'ring-state-error ring-1', + )} + /> +
+ {errors.name ? ( + {errors.name} + ) : ( + + )} + + {name.length}/{NAME_MAX} + +
+
+
+
+ + {/* 회비 설명 (선택) */} +
+ +
+ + setField({ description: e.target.value.slice(0, DESCRIPTION_MAX) }) + } + placeholder="설명을 작성해주세요" + className="bg-container-neutral-alternative typo-body1 placeholder:text-text-alternative text-text-normal h-12 w-full rounded-sm px-400 py-300 focus:outline-none" + /> +
+ + {description.length}/{DESCRIPTION_MAX} + +
+
+
+
+
+ + {/* 다음으로 버튼 */} +
+ +
+
+ ); +} + +export { DuesSetupStep1 }; diff --git a/src/components/admin/dues/setup/DuesSetupStepIndicator.tsx b/src/components/admin/dues/setup/DuesSetupStepIndicator.tsx new file mode 100644 index 00000000..5ce2a592 --- /dev/null +++ b/src/components/admin/dues/setup/DuesSetupStepIndicator.tsx @@ -0,0 +1,59 @@ +import { cn } from '@/lib/cn'; + +const STEPS = [ + { step: 1, label: '기본 정보' }, + { step: 2, label: '납부 대상' }, + { step: 3, label: '이월 설정' }, + { step: 4, label: '계좌 공개' }, + { step: 5, label: '최종 확인' }, +]; + +interface DuesSetupStepIndicatorProps { + currentStep: number; + className?: string; +} + +function DuesSetupStepIndicator({ currentStep, className }: DuesSetupStepIndicatorProps) { + return ( +
+ {STEPS.map(({ step, label }) => { + const isActive = step === currentStep; + const isCompleted = step < currentStep; + const isHighlighted = isActive || isCompleted; + + return ( +
+
+
+ {step} +
+ + {label} + +
+
+
+
+
+ ); + })} +
+ ); +} + +export { DuesSetupStepIndicator, type DuesSetupStepIndicatorProps }; diff --git a/src/components/admin/dues/setup/FormCard.tsx b/src/components/admin/dues/setup/FormCard.tsx new file mode 100644 index 00000000..77ef4792 --- /dev/null +++ b/src/components/admin/dues/setup/FormCard.tsx @@ -0,0 +1,20 @@ +interface FormCardProps { + title: string; + step:number; + description:string; + children:Node; +} +function FormCard({title,step,,description,children}:FormCardProps){ + return ( +
+ {/* 섹션 헤더 */} +
+ {title} ({step}/5) +

{description}

+
+ {children} +
+ + ) + +} \ No newline at end of file diff --git a/src/components/admin/dues/setup/index.ts b/src/components/admin/dues/setup/index.ts new file mode 100644 index 00000000..efad8780 --- /dev/null +++ b/src/components/admin/dues/setup/index.ts @@ -0,0 +1,2 @@ +export { DuesSetupStepIndicator, type DuesSetupStepIndicatorProps } from './DuesSetupStepIndicator'; +export { DuesSetupStep1 } from './DuesSetupStep1'; diff --git a/src/stores/index.ts b/src/stores/index.ts index cb9335f4..0ce095da 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -24,6 +24,12 @@ export { useBoardNavReset, } from './useBoardNavStore'; export { useCreateClubDraftStore } from './useCreateClubDraftStore'; +export { + useDuesSetupStore, + useDuesSetupValues, + useDuesSetupActions, + type DuesSetupState, +} from './useDuesSetupStore'; export { useAdminHeaderStore, useIsAdminEditMode, diff --git a/src/stores/useDuesSetupStore.ts b/src/stores/useDuesSetupStore.ts new file mode 100644 index 00000000..bc290aec --- /dev/null +++ b/src/stores/useDuesSetupStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand'; +import { combine, devtools, persist } from 'zustand/middleware'; +import { useShallow } from 'zustand/react/shallow'; + +const initialState = { + generationNumber: 0, + // Step 1: 기본 정보 + amount: '', + name: '', + description: '', +}; + +export type DuesSetupState = typeof initialState; + +export const useDuesSetupStore = create( + devtools( + persist( + combine(initialState, (set) => ({ + setField: (field: Partial) => set(field, false, 'setField'), + reset: () => set(initialState, false, 'reset'), + })), + { name: 'duesSetup' }, + ), + { name: 'DuesSetupStore' }, + ), +); + +export const useDuesSetupValues = () => + useDuesSetupStore( + useShallow((state) => ({ + generationNumber: state.generationNumber, + amount: state.amount, + name: state.name, + description: state.description, + })), + ); + +export const useDuesSetupActions = () => + useDuesSetupStore( + useShallow((state) => ({ + setField: state.setField, + reset: state.reset, + })), + ); From 37e8ab18a92e0077ebe119157cb65407df66754e Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 24 Jun 2026 18:07:24 +0900 Subject: [PATCH 02/28] =?UTF-8?q?fix:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/dues/BackButton.tsx | 2 +- .../admin/dues/setup/DuesSetupStep1.tsx | 26 ++++---------- src/components/admin/dues/setup/FormCard.tsx | 34 +++++++++++-------- .../admin/dues/setup/NextButton.tsx | 19 +++++++++++ 4 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 src/components/admin/dues/setup/NextButton.tsx diff --git a/src/components/admin/dues/BackButton.tsx b/src/components/admin/dues/BackButton.tsx index 83c09543..da42404e 100644 --- a/src/components/admin/dues/BackButton.tsx +++ b/src/components/admin/dues/BackButton.tsx @@ -2,7 +2,7 @@ import { BackIcon } from '@/assets/icons'; import { Icon } from '@/components/ui'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; function BackButton() { const router = useRouter(); diff --git a/src/components/admin/dues/setup/DuesSetupStep1.tsx b/src/components/admin/dues/setup/DuesSetupStep1.tsx index 848e4b16..798eb448 100644 --- a/src/components/admin/dues/setup/DuesSetupStep1.tsx +++ b/src/components/admin/dues/setup/DuesSetupStep1.tsx @@ -10,7 +10,9 @@ import { cn } from '@/lib/cn'; import { useDuesSetupActions, useDuesSetupValues } from '@/stores/useDuesSetupStore'; import { DuesSetupStepIndicator } from './DuesSetupStepIndicator'; -import { BackButton } from '../BackButton'; +import { BackButton } from '@/components/admin/dues'; +import { FormCard } from './FormCard'; +import { NextButton } from './NextButton'; const NAME_MAX = 30; const DESCRIPTION_MAX = 30; @@ -44,14 +46,7 @@ function DuesSetupStep1() { {/* 스텝 인디케이터 */} - {/* 폼 카드 */} -
- {/* 섹션 헤더 */} -
- 기본 정보 (1/5) -

총 회비의 기본 정보를 입력해주세요

-
- + {/* 필드 행: 회비금액 + 회비 이름 */}
{/* 1인당 회비금액 */} @@ -159,20 +154,11 @@ function DuesSetupStep1() {
-
+
{/* 다음으로 버튼 */} -
- -
+ ); } diff --git a/src/components/admin/dues/setup/FormCard.tsx b/src/components/admin/dues/setup/FormCard.tsx index 77ef4792..b04edae1 100644 --- a/src/components/admin/dues/setup/FormCard.tsx +++ b/src/components/admin/dues/setup/FormCard.tsx @@ -1,20 +1,24 @@ +import { ReactNode } from 'react'; + interface FormCardProps { title: string; - step:number; - description:string; - children:Node; + step: number; + description: string; + children: ReactNode; } -function FormCard({title,step,,description,children}:FormCardProps){ +function FormCard({ title, step, description, children }: FormCardProps) { return ( -
- {/* 섹션 헤더 */} -
- {title} ({step}/5) -

{description}

-
- {children} -
- - ) +
+ {/* 섹션 헤더 */} +
+ + {title} ({step}/5) + +

{description}

+
+ {children} +
+ ); +} -} \ No newline at end of file +export { FormCard, type FormCardProps }; diff --git a/src/components/admin/dues/setup/NextButton.tsx b/src/components/admin/dues/setup/NextButton.tsx new file mode 100644 index 00000000..2284d62f --- /dev/null +++ b/src/components/admin/dues/setup/NextButton.tsx @@ -0,0 +1,19 @@ +import { ArrowRightIcon } from '@/assets/icons'; +import { Icon } from '@/components/ui'; + +function NextButton({ handleNext }: { handleNext: () => void }) { + return ( +
+ +
+ ); +} + +export { NextButton }; From 970390ff1273ca882ae48371ddeebc35c9daa7b3 Mon Sep 17 00:00:00 2001 From: JIN921 Date: Wed, 24 Jun 2026 22:35:34 +0900 Subject: [PATCH 03/28] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20ste?= =?UTF-8?q?p2=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[clubId]/admin/dues/setup/1/page.tsx | 2 +- .../[clubId]/admin/dues/setup/2/page.tsx | 5 + .../admin/dues/setup/DuesSetupStep1.tsx | 6 +- .../admin/dues/setup/DuesSetupStep2.tsx | 301 ++++++++++++++++++ .../DuesSetupStepIndicator.tsx | 0 .../dues/setup/{ => components}/FormCard.tsx | 0 .../setup/{ => components}/NextButton.tsx | 0 .../dues/setup/components/PrevButton.tsx | 19 ++ .../admin/dues/setup/components/index.ts | 5 + src/components/admin/dues/setup/index.ts | 2 +- src/components/ui/index.ts | 10 + src/components/ui/pagination.tsx | 127 ++++++++ src/stores/useDuesSetupStore.ts | 5 + 13 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx create mode 100644 src/components/admin/dues/setup/DuesSetupStep2.tsx rename src/components/admin/dues/setup/{ => components}/DuesSetupStepIndicator.tsx (100%) rename src/components/admin/dues/setup/{ => components}/FormCard.tsx (100%) rename src/components/admin/dues/setup/{ => components}/NextButton.tsx (100%) create mode 100644 src/components/admin/dues/setup/components/PrevButton.tsx create mode 100644 src/components/admin/dues/setup/components/index.ts create mode 100644 src/components/ui/pagination.tsx diff --git a/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx b/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx index 019e89cd..dbffb288 100644 --- a/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx +++ b/src/app/(private)/[clubId]/admin/dues/setup/1/page.tsx @@ -1,4 +1,4 @@ -import { DuesSetupStep1 } from '@/components/admin/dues/setup'; +import { DuesSetupStep1 } from '@/components/admin/dues/setup/components'; export default function DuesSetupStep1Page() { return ; diff --git a/src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx b/src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx new file mode 100644 index 00000000..8ee29e86 --- /dev/null +++ b/src/app/(private)/[clubId]/admin/dues/setup/2/page.tsx @@ -0,0 +1,5 @@ +import { DuesSetupStep2 } from '@/components/admin/dues/setup/components'; + +export default function DuesSetupStep2Page() { + return ; +} diff --git a/src/components/admin/dues/setup/DuesSetupStep1.tsx b/src/components/admin/dues/setup/DuesSetupStep1.tsx index 798eb448..da4ec914 100644 --- a/src/components/admin/dues/setup/DuesSetupStep1.tsx +++ b/src/components/admin/dues/setup/DuesSetupStep1.tsx @@ -2,17 +2,13 @@ import { useState } from 'react'; -import Image from 'next/image'; import { useRouter, useParams } from 'next/navigation'; -import { ArrowRightIcon } from '@/assets/icons'; import { cn } from '@/lib/cn'; import { useDuesSetupActions, useDuesSetupValues } from '@/stores/useDuesSetupStore'; -import { DuesSetupStepIndicator } from './DuesSetupStepIndicator'; import { BackButton } from '@/components/admin/dues'; -import { FormCard } from './FormCard'; -import { NextButton } from './NextButton'; +import { DuesSetupStepIndicator, FormCard, NextButton } from './components'; const NAME_MAX = 30; const DESCRIPTION_MAX = 30; diff --git a/src/components/admin/dues/setup/DuesSetupStep2.tsx b/src/components/admin/dues/setup/DuesSetupStep2.tsx new file mode 100644 index 00000000..890f8bf9 --- /dev/null +++ b/src/components/admin/dues/setup/DuesSetupStep2.tsx @@ -0,0 +1,301 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; + +import { useRouter, useParams } from 'next/navigation'; +import { useQuery } from '@tanstack/react-query'; +import Image from 'next/image'; + +import { SearchIcon, ArrowLeftIcon, ArrowRightIcon } from '@/assets/icons'; +import { BackButton } from '@/components/admin/dues'; +import { + Avatar, + AvatarFallback, + Icon, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui'; +import { cn } from '@/lib/cn'; +import { adminMemberApi } from '@/lib/apis/adminMember'; +import { useDuesSetupValues, useDuesSetupActions } from '@/stores/useDuesSetupStore'; + +import { DuesSetupStepIndicator } from './component/DuesSetupStepIndicator'; + +type TabType = 'all' | 'selected' | 'excluded'; + +const PAGE_SIZE = 10; + +const ROLE_LABEL: Record = { + LEAD: '리더', + ADMIN: '관리자', + USER: '일반멤버', +}; + +function DuesSetupStep2() { + const router = useRouter(); + const { clubId } = useParams<{ clubId: string }>(); + + const { generationNumber, selectedMemberIds, memberIdsInitialized } = useDuesSetupValues(); + const { setField } = useDuesSetupActions(); + + const [tab, setTab] = useState('all'); + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + + const { data: members = [] } = useQuery({ + queryKey: ['admin', 'members', clubId], + queryFn: () => adminMemberApi.getMembers(clubId).then((res) => res.data.data), + staleTime: 5 * 60 * 1000, + }); + + // 첫 진입 시 ACTIVE 멤버 전체 선택으로 초기화 + useEffect(() => { + if (!memberIdsInitialized && members.length > 0) { + const activeIds = members + .filter((m) => m.memberStatus === 'ACTIVE') + .map((m) => m.clubMemberId); + setField({ selectedMemberIds: activeIds, memberIdsInitialized: true }); + } + }, [members, memberIdsInitialized, setField]); + + const selectedSet = useMemo(() => new Set(selectedMemberIds), [selectedMemberIds]); + + const filteredMembers = useMemo(() => { + const active = members.filter((m) => m.memberStatus === 'ACTIVE'); + const byTab = + tab === 'selected' + ? active.filter((m) => selectedSet.has(m.clubMemberId)) + : tab === 'excluded' + ? active.filter((m) => !selectedSet.has(m.clubMemberId)) + : active; + return search.trim() ? byTab.filter((m) => m.name.includes(search.trim())) : byTab; + }, [members, tab, search, selectedSet]); + + const totalPages = Math.max(1, Math.ceil(filteredMembers.length / PAGE_SIZE)); + const pagedMembers = filteredMembers.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + const totalCount = members.filter((m) => m.memberStatus === 'ACTIVE').length; + const selectedCount = selectedMemberIds.length; + const excludedCount = totalCount - selectedCount; + + const toggleMember = (id: number) => { + const next = selectedSet.has(id) + ? selectedMemberIds.filter((x) => x !== id) + : [...selectedMemberIds, id]; + setField({ selectedMemberIds: next }); + }; + + const handleTabChange = (next: TabType) => { + setTab(next); + setPage(1); + }; + + const handleSearch = (value: string) => { + setSearch(value); + setPage(1); + }; + + return ( +
+ {/* 헤더 */} +
+ +

{generationNumber}기 총 회비 설정

+
+ +
+ + +
+ {/* 섹션 헤더 */} +
+ 납부 대상 (2/5) +

이번 회비를 납부할 멤버를 선택해주세요

+
+ + {/* 탭 + 검색 */} +
+
+ {( + [ + { key: 'all', label: `전체 ${totalCount}` }, + { key: 'selected', label: `선택됨 ${selectedCount}` }, + { key: 'excluded', label: `제외됨 ${excludedCount}` }, + ] as const + ).map(({ key, label }) => ( + + ))} +
+ + {/* 검색바 */} +
+ + handleSearch(e.target.value)} + placeholder="이름으로 검색하기" + className="typo-body2 placeholder:text-text-alternative text-text-normal min-w-0 flex-1 bg-transparent focus:outline-none" + /> +
+
+ + {/* 테이블 */} + + + + + 선택 + + 이름 + 학과 + 직급 + + 납부 현황 + + + + + {pagedMembers.map((member) => { + const isSelected = selectedSet.has(member.clubMemberId); + return ( + toggleMember(member.clubMemberId)} + > + + toggleMember(member.clubMemberId)} + onClick={(e) => e.stopPropagation()} + className="accent-brand-primary size-4 cursor-pointer" + /> + + +
+ + {member.name[0]} + + {member.name} +
+
+ + {member.department} + + + {ROLE_LABEL[member.memberRole] ?? member.memberRole} + + + + {isSelected ? '선택됨' : '제외됨'} + + +
+ ); + })} + {pagedMembers.length === 0 && ( + + + 멤버가 없습니다 + + + )} +
+
+ + {/* 페이지네이션 */} + {totalPages > 1 && ( +
+ + {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( + + ))} + +
+ )} +
+
+ + {/* 하단 네비게이션 */} +
+ + +
+
+ ); +} + +export { DuesSetupStep2 }; diff --git a/src/components/admin/dues/setup/DuesSetupStepIndicator.tsx b/src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx similarity index 100% rename from src/components/admin/dues/setup/DuesSetupStepIndicator.tsx rename to src/components/admin/dues/setup/components/DuesSetupStepIndicator.tsx diff --git a/src/components/admin/dues/setup/FormCard.tsx b/src/components/admin/dues/setup/components/FormCard.tsx similarity index 100% rename from src/components/admin/dues/setup/FormCard.tsx rename to src/components/admin/dues/setup/components/FormCard.tsx diff --git a/src/components/admin/dues/setup/NextButton.tsx b/src/components/admin/dues/setup/components/NextButton.tsx similarity index 100% rename from src/components/admin/dues/setup/NextButton.tsx rename to src/components/admin/dues/setup/components/NextButton.tsx diff --git a/src/components/admin/dues/setup/components/PrevButton.tsx b/src/components/admin/dues/setup/components/PrevButton.tsx new file mode 100644 index 00000000..56795a1a --- /dev/null +++ b/src/components/admin/dues/setup/components/PrevButton.tsx @@ -0,0 +1,19 @@ +import { ArrowLeftIcon } from '@/assets/icons'; +import { Icon } from '@/components/ui'; + +function PrevButton({ handlePrev }: { handlePrev: () => void }) { + return ( +
+ +
+ ); +} + +export { PrevButton }; diff --git a/src/components/admin/dues/setup/components/index.ts b/src/components/admin/dues/setup/components/index.ts new file mode 100644 index 00000000..23384648 --- /dev/null +++ b/src/components/admin/dues/setup/components/index.ts @@ -0,0 +1,5 @@ +export { DuesSetupStepIndicator, type DuesSetupStepIndicatorProps } from './DuesSetupStepIndicator'; + +export { NextButton } from './NextButton'; +export { PrevButton } from './PrevButton'; +export { FormCard } from './FormCard'; diff --git a/src/components/admin/dues/setup/index.ts b/src/components/admin/dues/setup/index.ts index efad8780..d433fe8d 100644 --- a/src/components/admin/dues/setup/index.ts +++ b/src/components/admin/dues/setup/index.ts @@ -1,2 +1,2 @@ -export { DuesSetupStepIndicator, type DuesSetupStepIndicatorProps } from './DuesSetupStepIndicator'; export { DuesSetupStep1 } from './DuesSetupStep1'; +export { DuesSetupStep2 } from './DuesSetupStep2'; diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 5bd7f515..9f84981e 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -164,3 +164,13 @@ export { MobileBlocker, type MobileBlockerProps } from './MobileBlocker'; export { BackOrHomeButton, type BackOrHomeButtonProps } from './BackOrHomeButton'; export { ChartContainer, type ChartConfig } from './chart'; + +export { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationPrevious, + PaginationNext, + PaginationEllipsis, +} from './pagination'; diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx new file mode 100644 index 00000000..bcc5685a --- /dev/null +++ b/src/components/ui/pagination.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import { + ChevronLeftIcon, + ChevronRightIcon, + MoreHorizontalIcon, +} from "lucide-react" + +import { cn } from "@/lib/cn" +import { buttonVariants, type Button } from "@/components/ui/button" + +function Pagination({ className, ...props }: React.ComponentProps<"nav">) { + return ( +