Files
sam-react-prod/src/components/settings/AccountManagement/index.tsx
유병철 23135ff01a feat: [설정] 설정 관리 전반 UI 개선
- 계정관리 상세/폼 개선 (AccountDetail, AccountDetailForm)
- 근태설정, 휴가정책 관리 개선
- 바로빌 연동 회원가입 모달 개선
- 알림설정, 결제이력, 권한관리 UI 개선
- 직급/직책 관리 UI 개선 (RankManagement, TitleManagement)
- 구독관리, 근무스케줄 관리 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:33:28 +09:00

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} />;
}