'use client'; /** * 카드 내역 조회 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링/페이지네이션 * - dateRangeSelector (헤더 액션) * - beforeTableContent: 계정과목명 선택 + 저장 버튼 + 새로고침 * - tableHeaderActions: 2개 Select 필터 (카드명, 정렬) * - tableFooter: 합계 행 * - showRowNumber={false} * - 상세 모달 (수정 기능) * - 계정과목명 일괄 저장 다이얼로그 */ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { format, startOfMonth, endOfMonth } from 'date-fns'; import { CreditCard, Plus, RefreshCw, Save, Loader2, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { TableRow, TableCell } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { MobileCard } from '@/components/organisms/MobileCard'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import type { CardTransaction, SortOption } from './types'; import { SORT_OPTIONS, ACCOUNT_SUBJECT_OPTIONS, USAGE_TYPE_OPTIONS } from './types'; import { getCardTransactionList, getCardTransactionSummary, bulkUpdateAccountCode, } from './actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'card', label: '카드' }, { key: 'cardName', label: '카드명' }, { key: 'user', label: '사용자' }, { key: 'usedAt', label: '사용일시' }, { key: 'merchantName', label: '가맹점명' }, { key: 'amount', label: '사용금액', className: 'text-right' }, { key: 'usageType', label: '사용유형' }, ]; // ===== Props ===== interface CardTransactionInquiryProps { initialData?: CardTransaction[]; initialSummary?: { previousMonthTotal: number; currentMonthTotal: number; }; initialPagination?: { currentPage: number; lastPage: number; perPage: number; total: number; }; } export function CardTransactionInquiry({ initialData = [], initialSummary, initialPagination, }: CardTransactionInquiryProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [data, setData] = useState(initialData); const [summary, setSummary] = useState( initialSummary || { previousMonthTotal: 0, currentMonthTotal: 0 } ); const [pagination, setPagination] = useState( initialPagination || { currentPage: 1, lastPage: 1, perPage: 20, total: 0 } ); // 필터 상태 const [searchQuery, setSearchQuery] = useState(''); const [sortOption, setSortOption] = useState('latest'); const [cardFilter, setCardFilter] = useState('all'); const [currentPage, setCurrentPage] = useState(initialPagination?.currentPage || 1); const [isLoading, setIsLoading] = useState(!initialData.length); const itemsPerPage = 20; // 상단 계정과목명 선택 (저장용) const [selectedAccountSubject, setSelectedAccountSubject] = useState('unset'); // 계정과목명 저장 다이얼로그 const [showSaveDialog, setShowSaveDialog] = useState(false); const [isSaving, setIsSaving] = useState(false); // 선택 필요 알림 다이얼로그 const [showSelectWarningDialog, setShowSelectWarningDialog] = useState(false); // 상세 모달 상태 const [showDetailModal, setShowDetailModal] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [detailFormData, setDetailFormData] = useState({ memo: '', usageType: 'unset', }); const [isDetailSaving, setIsDetailSaving] = useState(false); // 선택된 항목 (외부 관리) const [selectedItems, setSelectedItems] = useState>(new Set()); // 날짜 범위 상태 const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd')); const [endDate, setEndDate] = useState(() => format(endOfMonth(new Date()), 'yyyy-MM-dd')); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { const sortMapping: Record = { latest: { sortBy: 'used_at', sortDir: 'desc' }, oldest: { sortBy: 'used_at', sortDir: 'asc' }, amountHigh: { sortBy: 'amount', sortDir: 'desc' }, amountLow: { sortBy: 'amount', sortDir: 'asc' }, }; const sortParams = sortMapping[sortOption]; const [listResult, summaryResult] = await Promise.all([ getCardTransactionList({ page: currentPage, perPage: itemsPerPage, startDate, endDate, search: searchQuery || undefined, sortBy: sortParams.sortBy, sortDir: sortParams.sortDir, }), getCardTransactionSummary({ startDate, endDate }), ]); if (listResult.success) { setData(listResult.data); setPagination(listResult.pagination); } if (summaryResult.success && summaryResult.data) { setSummary({ previousMonthTotal: summaryResult.data.previousMonthTotal, currentMonthTotal: summaryResult.data.currentMonthTotal, }); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[CardTransactionInquiry] loadData error:', error); } finally { setIsLoading(false); } }, [currentPage, startDate, endDate, searchQuery, sortOption]); // 데이터 로드 (필터 변경 시) useEffect(() => { loadData(); }, [loadData]); // ===== 카드명 옵션 ===== const cardOptions = useMemo(() => { const uniqueCards = [...new Set(data.map((d) => d.cardName))]; return [ { value: 'all', label: '전체' }, ...uniqueCards.map((card) => ({ value: card, label: card })), ]; }, [data]); // ===== 필터링된 데이터 ===== const filteredData = useMemo(() => { let result = data.filter( (item) => item.card.includes(searchQuery) || item.cardName.includes(searchQuery) || item.user.includes(searchQuery) || item.merchantName.includes(searchQuery) ); // 카드명 필터 if (cardFilter !== 'all') { result = result.filter((item) => item.cardName === cardFilter); } // 정렬 switch (sortOption) { case 'oldest': result.sort((a, b) => new Date(a.usedAt).getTime() - new Date(b.usedAt).getTime()); break; case 'amountHigh': result.sort((a, b) => b.amount - a.amount); break; case 'amountLow': result.sort((a, b) => a.amount - b.amount); break; default: // latest result.sort((a, b) => new Date(b.usedAt).getTime() - new Date(a.usedAt).getTime()); break; } return result; }, [data, searchQuery, cardFilter, sortOption]); // ===== 핸들러 ===== const handleRowClick = useCallback((item: CardTransaction) => { setSelectedItem(item); setDetailFormData({ memo: item.memo || '', usageType: item.usageType || 'unset', }); setShowDetailModal(true); }, []); const handleDetailSave = useCallback(async () => { if (!selectedItem) return; setIsDetailSaving(true); try { // 임시: 로컬 데이터 업데이트 setData((prev) => prev.map((item) => item.id === selectedItem.id ? { ...item, memo: detailFormData.memo, usageType: detailFormData.usageType } : item ) ); setShowDetailModal(false); setSelectedItem(null); } catch (error) { console.error('[CardTransactionInquiry] handleDetailSave error:', error); } finally { setIsDetailSaving(false); } }, [selectedItem, detailFormData]); const handleRefresh = useCallback(() => { loadData(); }, [loadData]); // ===== 계정과목명 저장 핸들러 ===== const handleSaveAccountSubject = useCallback(() => { if (selectedItems.size === 0) { setShowSelectWarningDialog(true); return; } setShowSaveDialog(true); }, [selectedItems.size]); const handleConfirmSaveAccountSubject = useCallback(async () => { if (selectedAccountSubject === 'unset') return; setIsSaving(true); try { const ids = Array.from(selectedItems).map((id) => parseInt(id, 10)); const result = await bulkUpdateAccountCode(ids, selectedAccountSubject); if (result.success) { await loadData(); setSelectedItems(new Set()); setSelectedAccountSubject('unset'); } else { console.error('[CardTransactionInquiry] bulkUpdate error:', result.error); } } catch (error) { console.error('[CardTransactionInquiry] bulkUpdate error:', error); } finally { setIsSaving(false); setShowSaveDialog(false); } }, [selectedAccountSubject, selectedItems, loadData]); // ===== 사용유형 라벨 변환 함수 ===== const getUsageTypeLabel = useCallback((value: string) => { return USAGE_TYPE_OPTIONS.find((opt) => opt.value === value)?.label || '미설정'; }, []); // ===== 테이블 합계 계산 ===== const totalAmount = useMemo(() => { return filteredData.reduce((sum, item) => sum + item.amount, 0); }, [filteredData]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '카드 내역 조회', description: '법인카드 사용 내역을 조회합니다', icon: CreditCard, basePath: '/accounting/card-transactions', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { return { success: true, data: filteredData, totalCount: filteredData.length, }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage, // 행 번호 숨기기 showRowNumber: false, // 검색 searchPlaceholder: '카드, 카드명, 사용자, 가맹점명 검색...', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.card.toLowerCase().includes(search) || item.cardName.toLowerCase().includes(search) || item.user.toLowerCase().includes(search) || item.merchantName.toLowerCase().includes(search) ); }, // 필터 설정 (모바일용) filterConfig: [ { key: 'card', label: '카드', type: 'single', options: cardOptions.filter((o) => o.value !== 'all'), }, { key: 'sortBy', label: '정렬', type: 'single', options: SORT_OPTIONS.map((o) => ({ value: o.value, label: o.label, })), }, ], initialFilters: { card: 'all', sortBy: 'latest', }, filterTitle: '카드 필터', // 헤더 액션: 계정과목명 Select + 저장 + 새로고침 headerActions: () => (
계정과목명
), // 등록 버튼 createButton: { label: '카드내역 등록', icon: Plus, onClick: () => router.push('/ko/accounting/card-transactions?mode=new'), }, // 커스텀 필터 함수 customFilterFn: (items) => { if (!items || items.length === 0) return items; let result = [...items]; // 검색어 필터 if (searchQuery) { const search = searchQuery.toLowerCase(); result = result.filter((item) => item.card.toLowerCase().includes(search) || item.cardName.toLowerCase().includes(search) || item.user.toLowerCase().includes(search) || item.merchantName.toLowerCase().includes(search) ); } // 카드명 필터 if (cardFilter !== 'all') { result = result.filter((item) => item.cardName === cardFilter); } return result; }, // 커스텀 정렬 함수 customSortFn: (items) => { const sorted = [...items]; switch (sortOption) { case 'oldest': sorted.sort((a, b) => new Date(a.usedAt).getTime() - new Date(b.usedAt).getTime()); break; case 'amountHigh': sorted.sort((a, b) => b.amount - a.amount); break; case 'amountLow': sorted.sort((a, b) => a.amount - b.amount); break; default: // latest sorted.sort((a, b) => new Date(b.usedAt).getTime() - new Date(a.usedAt).getTime()); break; } return sorted; }, // 날짜 선택기 (헤더 액션) // 검색창 (공통 컴포넌트에서 자동 생성) hideSearch: true, searchValue: searchQuery, onSearchChange: setSearchQuery, searchPlaceholder: '카드, 카드명, 사용자, 가맹점명 검색...', dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 선택 항목 변경 콜백 onSelectionChange: setSelectedItems, // 테이블 헤더 액션 (2개 필터) tableHeaderActions: () => (
{/* 카드명 필터 */} {/* 정렬 */}
), // 테이블 푸터 (합계 행) tableFooter: ( 합계 {totalAmount.toLocaleString()} ), // Stats 카드 computeStats: (): StatCard[] => [ { label: '전월 사용액', value: `${summary.previousMonthTotal.toLocaleString()}원`, icon: CreditCard, iconColor: 'text-gray-500', }, { label: '당월 사용액', value: `${summary.currentMonthTotal.toLocaleString()}원`, icon: CreditCard, iconColor: 'text-blue-500', }, ], // 테이블 행 렌더링 renderTableRow: ( item: CardTransaction, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} > {/* 체크박스 */} e.stopPropagation()}> {/* 카드 */} {item.card} {/* 카드명 */} {item.cardName} {/* 사용자 */} {item.user} {/* 사용일시 */} {item.usedAt} {/* 가맹점명 */} {item.merchantName} {/* 사용금액 */} {item.amount.toLocaleString()} {/* 사용유형 */} {getUsageTypeLabel(item.usageType)} ), // 모바일 카드 렌더링 renderMobileCard: ( item: CardTransaction, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '가맹점명', value: item.merchantName }, { label: '사용금액', value: `${item.amount.toLocaleString()}원` }, { label: '사용유형', value: getUsageTypeLabel(item.usageType) }, ]} /> ), }), [ filteredData, cardOptions, cardFilter, sortOption, startDate, endDate, summary, totalAmount, selectedAccountSubject, isLoading, router, handleRowClick, handleRefresh, handleSaveAccountSubject, getUsageTypeLabel, ] ); return ( <> {/* 계정과목명 저장 확인 다이얼로그 */} 계정과목명 변경 {selectedItems.size}개의 카드 사용 내역을{' '} {ACCOUNT_SUBJECT_OPTIONS.find((o) => o.value === selectedAccountSubject)?.label} (으)로 모두 변경하시겠습니까? {/* 선택 필요 알림 다이얼로그 */} 항목 선택 필요 변경할 카드 사용 내역을 먼저 선택해주세요. setShowSelectWarningDialog(false)}> 확인 {/* 카드 내역 상세 모달 */} 카드 내역 상세 카드 사용 상세 내역을 등록합니다 {selectedItem && (

기본 정보

{selectedItem.usedAt}

{selectedItem.card} ({selectedItem.cardName})

{selectedItem.user}

{selectedItem.amount.toLocaleString()}원

setDetailFormData((prev) => ({ ...prev, memo: e.target.value })) } placeholder="적요" className="mt-1" />

{selectedItem.merchantName}

)}
); }