- 계정관리 상세/폼 개선 (AccountDetail, AccountDetailForm) - 근태설정, 휴가정책 관리 개선 - 바로빌 연동 회원가입 모달 개선 - 알림설정, 결제이력, 권한관리 UI 개선 - 직급/직책 관리 UI 개선 (RankManagement, TitleManagement) - 구독관리, 근무스케줄 관리 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
384 lines
13 KiB
TypeScript
384 lines
13 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 계좌관리 - 종합 계좌 관리 목록 페이지
|
|
*
|
|
* - 통계카드 5개 (전체/은행/대출/증권/보험)
|
|
* - 구분/금융기관 필터
|
|
* - 수기 계좌 등록 버튼
|
|
* - 범례 (수기/연동)
|
|
* - 체크박스 없음
|
|
*/
|
|
|
|
import { useState, useMemo, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
|
import {
|
|
Landmark,
|
|
Building2,
|
|
CreditCard,
|
|
TrendingUp,
|
|
Shield,
|
|
Plus,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { TableRow, TableCell } from '@/components/ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
UniversalListPage,
|
|
type UniversalListConfig,
|
|
type SelectionHandlers,
|
|
type RowClickHandlers,
|
|
type ListParams,
|
|
type StatCard,
|
|
} from '@/components/templates/UniversalListPage';
|
|
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
|
import type { Account, AccountCategory } from './types';
|
|
import {
|
|
BANK_LABELS,
|
|
ACCOUNT_CATEGORY_LABELS,
|
|
ACCOUNT_CATEGORY_FILTER_OPTIONS,
|
|
ACCOUNT_TYPE_LABELS,
|
|
ACCOUNT_STATUS_LABELS,
|
|
ACCOUNT_STATUS_COLORS,
|
|
ALL_FINANCIAL_INSTITUTION_OPTIONS,
|
|
} from './types';
|
|
import { getBankAccounts } from './actions';
|
|
|
|
// ===== 계좌번호 마스킹 =====
|
|
const maskAccountNumber = (accountNumber: string): string => {
|
|
if (!accountNumber || accountNumber.length <= 8) return accountNumber || '';
|
|
const parts = accountNumber.split('-');
|
|
if (parts.length >= 3) {
|
|
return parts.map((part, idx) => {
|
|
if (idx === 0 || idx === parts.length - 1) return part;
|
|
return '****';
|
|
}).join('-');
|
|
}
|
|
const first = accountNumber.slice(0, 4);
|
|
const last = accountNumber.slice(-4);
|
|
return `${first}-****-${last}`;
|
|
};
|
|
|
|
export function AccountManagement() {
|
|
const router = useRouter();
|
|
const itemsPerPage = 20;
|
|
|
|
// ===== 날짜 범위 상태 =====
|
|
const today = new Date();
|
|
const [startDate, setStartDate] = useState(() => format(startOfMonth(today), 'yyyy-MM-dd'));
|
|
const [endDate, setEndDate] = useState(() => format(endOfMonth(today), 'yyyy-MM-dd'));
|
|
|
|
// ===== 필터 상태 =====
|
|
const [categoryFilter, setCategoryFilter] = useState<string>('all');
|
|
const [institutionFilter, setInstitutionFilter] = useState<string>('all');
|
|
|
|
// ===== 핸들러 =====
|
|
const handleRowClick = useCallback((item: Account) => {
|
|
router.push(`/ko/settings/accounts/${item.id}?mode=view`);
|
|
}, [router]);
|
|
|
|
const handleCreate = useCallback(() => {
|
|
router.push('/ko/settings/accounts?mode=new');
|
|
}, [router]);
|
|
|
|
// ===== 금융기관 필터 옵션 =====
|
|
const institutionFilterOptions = useMemo(() => [
|
|
{ value: 'all', label: '전체' },
|
|
...ALL_FINANCIAL_INSTITUTION_OPTIONS,
|
|
], []);
|
|
|
|
// ===== UniversalListPage Config =====
|
|
const config: UniversalListConfig<Account> = useMemo(
|
|
() => ({
|
|
title: '계좌 관리',
|
|
description: '계좌 목록을 관리합니다',
|
|
icon: Landmark,
|
|
basePath: '/settings/accounts',
|
|
|
|
idField: 'id',
|
|
getItemId: (item: Account) => String(item.id),
|
|
|
|
// 체크박스 없음
|
|
showCheckbox: false,
|
|
|
|
// 날짜 범위 선택기 + 프리셋 버튼
|
|
dateRangeSelector: {
|
|
enabled: true,
|
|
showPresets: true,
|
|
presets: ['thisMonth', 'lastMonth', 'twoMonthsAgo', 'threeMonthsAgo', 'fourMonthsAgo', 'fiveMonthsAgo'],
|
|
presetLabels: {
|
|
thisMonth: '이번달',
|
|
lastMonth: '지난달',
|
|
},
|
|
startDate,
|
|
endDate,
|
|
onStartDateChange: setStartDate,
|
|
onEndDateChange: setEndDate,
|
|
},
|
|
|
|
// API 액션
|
|
actions: {
|
|
getList: async (params?: ListParams) => {
|
|
try {
|
|
const result = await getBankAccounts();
|
|
if (result.success && result.data) {
|
|
let filteredData = result.data;
|
|
|
|
// 구분 필터
|
|
if (categoryFilter && categoryFilter !== 'all') {
|
|
filteredData = filteredData.filter(item => item.category === categoryFilter);
|
|
}
|
|
// 금융기관 필터
|
|
if (institutionFilter && institutionFilter !== 'all') {
|
|
filteredData = filteredData.filter(item => item.bankCode === institutionFilter);
|
|
}
|
|
// 검색 필터
|
|
if (params?.search) {
|
|
const s = params.search.toLowerCase();
|
|
filteredData = filteredData.filter(item =>
|
|
item.accountName?.toLowerCase().includes(s) ||
|
|
item.accountNumber?.includes(s) ||
|
|
item.accountHolder?.toLowerCase().includes(s) ||
|
|
BANK_LABELS[item.bankCode]?.toLowerCase().includes(s)
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: filteredData,
|
|
totalCount: filteredData.length,
|
|
totalPages: Math.ceil(filteredData.length / itemsPerPage),
|
|
};
|
|
}
|
|
return { success: false, error: result.error || '계좌 목록을 불러오는데 실패했습니다.' };
|
|
} catch {
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
},
|
|
},
|
|
|
|
// 통계카드
|
|
computeStats: (data: Account[], totalCount: number): StatCard[] => {
|
|
// 전체 데이터 기준 (필터 무관하게)
|
|
return [
|
|
{
|
|
label: '전체계좌',
|
|
value: totalCount,
|
|
icon: Landmark,
|
|
iconColor: 'text-blue-500',
|
|
onClick: () => setCategoryFilter('all'),
|
|
isActive: categoryFilter === 'all',
|
|
},
|
|
{
|
|
label: '은행계좌',
|
|
value: data.filter(a => a.category === 'bank_account').length,
|
|
icon: Building2,
|
|
iconColor: 'text-green-500',
|
|
onClick: () => setCategoryFilter('bank_account'),
|
|
isActive: categoryFilter === 'bank_account',
|
|
},
|
|
{
|
|
label: '대출계좌',
|
|
value: data.filter(a => a.category === 'loan_account').length,
|
|
icon: CreditCard,
|
|
iconColor: 'text-orange-500',
|
|
onClick: () => setCategoryFilter('loan_account'),
|
|
isActive: categoryFilter === 'loan_account',
|
|
},
|
|
{
|
|
label: '증권계좌',
|
|
value: data.filter(a => a.category === 'securities_account').length,
|
|
icon: TrendingUp,
|
|
iconColor: 'text-purple-500',
|
|
onClick: () => setCategoryFilter('securities_account'),
|
|
isActive: categoryFilter === 'securities_account',
|
|
},
|
|
{
|
|
label: '보험계좌',
|
|
value: data.filter(a => a.category === 'insurance_account').length,
|
|
icon: Shield,
|
|
iconColor: 'text-red-500',
|
|
onClick: () => setCategoryFilter('insurance_account'),
|
|
isActive: categoryFilter === 'insurance_account',
|
|
},
|
|
];
|
|
},
|
|
|
|
// 테이블 컬럼
|
|
columns: [
|
|
{ key: 'no', label: 'No.', className: 'text-center w-[60px]' },
|
|
{ key: 'category', label: '구분', className: 'min-w-[80px]' },
|
|
{ key: 'accountType', label: '유형', className: 'min-w-[80px]' },
|
|
{ key: 'institution', label: '금융기관', className: 'min-w-[100px]' },
|
|
{ key: 'accountNumber', label: '계좌번호', className: 'min-w-[160px]' },
|
|
{ key: 'accountName', label: '계좌명', className: 'min-w-[120px]' },
|
|
{ key: 'status', label: '상태', className: 'min-w-[70px]' },
|
|
],
|
|
|
|
clientSideFiltering: true,
|
|
itemsPerPage,
|
|
searchPlaceholder: '금융기관, 계좌번호, 계좌명 검색...',
|
|
searchFilter: (item: Account, search: string) => {
|
|
const s = search.toLowerCase();
|
|
return (
|
|
item.bankName?.toLowerCase().includes(s) ||
|
|
item.accountNumber?.toLowerCase().includes(s) ||
|
|
item.accountName?.toLowerCase().includes(s) ||
|
|
item.accountHolder?.toLowerCase().includes(s) ||
|
|
false
|
|
);
|
|
},
|
|
|
|
// 헤더 액션 - 수기 계좌 등록 버튼
|
|
headerActions: () => (
|
|
<Button
|
|
className="ml-auto bg-orange-500 hover:bg-orange-600 text-white"
|
|
onClick={handleCreate}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
수기 계좌 등록
|
|
</Button>
|
|
),
|
|
|
|
// 테이블 카드 내부 필터 (구분, 금융기관 Select) - "총 N건" 옆에 배치
|
|
tableHeaderActions: (
|
|
<div className="flex items-center gap-2">
|
|
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
|
<SelectTrigger className="min-w-[130px] w-auto h-9">
|
|
<SelectValue placeholder="구분" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{ACCOUNT_CATEGORY_FILTER_OPTIONS.map(opt => (
|
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={institutionFilter} onValueChange={setInstitutionFilter}>
|
|
<SelectTrigger className="min-w-[150px] w-auto h-9">
|
|
<SelectValue placeholder="금융기관" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{institutionFilterOptions.map(opt => (
|
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
),
|
|
|
|
// 테이블 행 렌더링
|
|
renderTableRow: (
|
|
item: Account,
|
|
_index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Account>
|
|
) => {
|
|
return (
|
|
<TableRow
|
|
key={item.id}
|
|
className="hover:bg-muted/50 cursor-pointer"
|
|
onClick={() => handleRowClick(item)}
|
|
>
|
|
<TableCell className="text-muted-foreground text-center">{globalIndex}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="text-xs">
|
|
{ACCOUNT_CATEGORY_LABELS[item.category] || item.category}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">
|
|
{ACCOUNT_TYPE_LABELS[item.accountType] || item.accountType || '-'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1.5">
|
|
<span
|
|
className={`inline-block w-2 h-2 rounded-full flex-shrink-0 ${
|
|
item.isManual ? 'bg-orange-400' : 'bg-blue-400'
|
|
}`}
|
|
/>
|
|
{BANK_LABELS[item.bankCode] || item.bankCode}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-sm">{maskAccountNumber(item.accountNumber)}</TableCell>
|
|
<TableCell>{item.accountName}</TableCell>
|
|
<TableCell>
|
|
<Badge className={ACCOUNT_STATUS_COLORS[item.status]}>
|
|
{ACCOUNT_STATUS_LABELS[item.status]}
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
},
|
|
|
|
// 모바일 카드
|
|
renderMobileCard: (
|
|
item: Account,
|
|
_index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Account>
|
|
) => {
|
|
return (
|
|
<ListMobileCard
|
|
key={item.id}
|
|
id={String(item.id)}
|
|
title={item.accountName}
|
|
headerBadges={
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<Badge variant="outline" className="text-xs">
|
|
{ACCOUNT_CATEGORY_LABELS[item.category]}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
{BANK_LABELS[item.bankCode] || item.bankCode}
|
|
</span>
|
|
</div>
|
|
}
|
|
statusBadge={
|
|
<Badge className={ACCOUNT_STATUS_COLORS[item.status]}>
|
|
{ACCOUNT_STATUS_LABELS[item.status]}
|
|
</Badge>
|
|
}
|
|
showCheckbox={false}
|
|
isSelected={false}
|
|
onToggleSelection={() => {}}
|
|
onClick={() => handleRowClick(item)}
|
|
infoGrid={
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
<InfoField label="유형" value={ACCOUNT_TYPE_LABELS[item.accountType] || '-'} />
|
|
<InfoField label="계좌번호" value={maskAccountNumber(item.accountNumber)} />
|
|
</div>
|
|
}
|
|
/>
|
|
);
|
|
},
|
|
|
|
// 테이블 카드 내부 하단 - 범례 (수기/연동)
|
|
tableFooter: (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="border-0">
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="inline-block w-3 h-3 rounded-full bg-orange-400" />
|
|
수기 계좌
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="inline-block w-3 h-3 rounded-full bg-blue-400" />
|
|
연동 계좌
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
}),
|
|
[handleCreate, handleRowClick, categoryFilter, institutionFilter, institutionFilterOptions, startDate, endDate]
|
|
);
|
|
|
|
return <UniversalListPage config={config} />;
|
|
}
|