'use client'; import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Landmark, Pencil, Trash2, Plus, Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { IntegratedListTemplateV2, type TableColumn, } from '@/components/templates/IntegratedListTemplateV2'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import type { Account } from './types'; import { BANK_LABELS, ACCOUNT_STATUS_LABELS, ACCOUNT_STATUS_COLORS, } from './types'; import { getBankAccounts, deleteBankAccount, deleteBankAccounts } from './actions'; // ===== 계좌번호 마스킹 함수 ===== const maskAccountNumber = (accountNumber: string): string => { if (accountNumber.length <= 8) return accountNumber; const parts = accountNumber.split('-'); if (parts.length >= 3) { // 1234-****-****-1234 형태 return parts.map((part, idx) => { if (idx === 0 || idx === parts.length - 1) return part; return '****'; }).join('-'); } // 단순 형태: 앞 4자리-****-뒤 4자리 const first = accountNumber.slice(0, 4); const last = accountNumber.slice(-4); return `${first}-****-****-${last}`; }; export function AccountManagement() { const router = useRouter(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 로딩 상태 const [isLoading, setIsLoading] = useState(true); const [isDeleting, setIsDeleting] = useState(false); // 삭제 다이얼로그 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); // API 데이터 const [data, setData] = useState([]); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { const result = await getBankAccounts(); if (result.success && result.data) { setData(result.data); } else { toast.error(result.error || '계좌 목록을 불러오는데 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, []); // 초기 로드 useEffect(() => { loadData(); }, [loadData]); // ===== 체크박스 핸들러 ===== const toggleSelection = useCallback((id: number) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); return newSet; }); }, []); // ===== 필터링된 데이터 ===== const filteredData = useMemo(() => { if (!searchQuery) return data; const search = searchQuery.toLowerCase(); return data.filter(item => item.accountName.toLowerCase().includes(search) || item.accountNumber.includes(search) || item.accountHolder.toLowerCase().includes(search) || BANK_LABELS[item.bankCode]?.toLowerCase().includes(search) ); }, [data, searchQuery]); const paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredData.slice(startIndex, startIndex + itemsPerPage); }, [filteredData, currentPage, itemsPerPage]); const totalPages = Math.ceil(filteredData.length / itemsPerPage); // ===== 전체 선택 핸들러 ===== const toggleSelectAll = useCallback(() => { if (selectedItems.size === filteredData.length && filteredData.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(filteredData.map(item => item.id))); } }, [selectedItems.size, filteredData]); // ===== 액션 핸들러 ===== const handleRowClick = useCallback((item: Account) => { router.push(`/ko/settings/accounts/${item.id}`); }, [router]); const handleEdit = useCallback((item: Account) => { router.push(`/ko/settings/accounts/${item.id}?mode=edit`); }, [router]); const handleDeleteClick = useCallback((id: number) => { setDeleteTargetId(id); setShowDeleteDialog(true); }, []); const handleConfirmDelete = useCallback(async () => { if (!deleteTargetId) return; setIsDeleting(true); try { const result = await deleteBankAccount(deleteTargetId); if (result.success) { toast.success('계좌가 삭제되었습니다.'); setData(prev => prev.filter(item => item.id !== deleteTargetId)); setSelectedItems(prev => { const newSet = new Set(prev); newSet.delete(deleteTargetId); return newSet; }); } else { toast.error(result.error || '계좌 삭제에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsDeleting(false); setShowDeleteDialog(false); setDeleteTargetId(null); } }, [deleteTargetId]); const handleBulkDelete = useCallback(() => { if (selectedItems.size > 0) { setShowBulkDeleteDialog(true); } }, [selectedItems.size]); const handleConfirmBulkDelete = useCallback(async () => { const ids = Array.from(selectedItems); setIsDeleting(true); try { const result = await deleteBankAccounts(ids); if (result.success) { toast.success(`${result.deletedCount}개의 계좌가 삭제되었습니다.`); if (result.error) { toast.warning(result.error); } setData(prev => prev.filter(item => !selectedItems.has(item.id))); setSelectedItems(new Set()); } else { toast.error(result.error || '계좌 삭제에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsDeleting(false); setShowBulkDeleteDialog(false); } }, [selectedItems]); const handleCreate = useCallback(() => { router.push('/ko/settings/accounts/new'); }, [router]); // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, { key: 'bank', label: '은행', className: 'min-w-[100px]' }, { key: 'accountNumber', label: '계좌번호', className: 'min-w-[160px]' }, { key: 'accountName', label: '계좌명', className: 'min-w-[120px]' }, { key: 'accountHolder', label: '예금주', className: 'min-w-[80px]' }, { key: 'status', label: '상태', className: 'min-w-[80px]' }, { key: 'actions', label: '작업', className: 'w-[100px] text-right' }, ], []); // ===== 테이블 행 렌더링 ===== const renderTableRow = useCallback((item: Account, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); return ( handleRowClick(item)} > e.stopPropagation()}> toggleSelection(item.id)} /> {globalIndex} {BANK_LABELS[item.bankCode] || item.bankCode} {maskAccountNumber(item.accountNumber)} {item.accountName} {item.accountHolder} {ACCOUNT_STATUS_LABELS[item.status]} e.stopPropagation()}> {isSelected && (
)}
); }, [selectedItems, toggleSelection, handleRowClick, handleEdit, handleDeleteClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: Account, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void ) => { return ( #{globalIndex} {BANK_LABELS[item.bankCode] || item.bankCode} } statusBadge={ {ACCOUNT_STATUS_LABELS[item.status]} } isSelected={isSelected} onToggleSelection={onToggle} onCardClick={() => handleRowClick(item)} infoGrid={
} actions={ isSelected ? (
) : undefined } /> ); }, [handleRowClick, handleEdit, handleDeleteClick]); // ===== 헤더 액션 (카드관리와 동일한 패턴) ===== const headerActions = ( ); return ( <> item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} pagination={{ currentPage, totalPages, totalItems: filteredData.length, itemsPerPage, onPageChange: setCurrentPage, }} /> {/* 단일 삭제 확인 다이얼로그 */} 계좌 삭제 계좌를 정말 삭제하시겠습니까?
삭제된 계좌의 과거 사용 내역은 보존됩니다.
취소 {isDeleting ? ( <> 삭제 중... ) : '삭제'}
{/* 다중 삭제 확인 다이얼로그 */} 계좌 삭제 선택하신 {selectedItems.size}개의 계좌를 정말 삭제하시겠습니까?
삭제된 계좌의 과거 사용 내역은 보존됩니다.
취소 {isDeleting ? ( <> 삭제 중... ) : '삭제'}
); }