"use client"; /** * 세금계산서 목록 컴포넌트 * 세금계산서 목록 조회, 검색, 필터링 기능 */ import { useState, useEffect, useCallback } from "react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { Plus, Search, Filter, FileText, Eye, Edit, Trash2, Send, CheckCircle, XCircle, Clock, RefreshCw, Paperclip, Image, File, ArrowUpDown, ArrowUp, ArrowDown, X, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { toast } from "sonner"; import { getTaxInvoiceList, deleteTaxInvoice, issueTaxInvoice, cancelTaxInvoice, TaxInvoice, TaxInvoiceListParams, CostType, costTypeLabels, } from "@/lib/api/taxInvoice"; import { TaxInvoiceForm } from "./TaxInvoiceForm"; import { TaxInvoiceDetail } from "./TaxInvoiceDetail"; // 상태 뱃지 색상 const statusBadgeVariant: Record = { draft: "outline", issued: "default", sent: "secondary", cancelled: "destructive", }; // 상태 라벨 const statusLabels: Record = { draft: "임시저장", issued: "발행완료", sent: "전송완료", cancelled: "취소됨", }; // 유형 라벨 const typeLabels: Record = { sales: "매출", purchase: "매입", }; // 컬럼 정의 interface ColumnDef { key: string; label: string; sortable?: boolean; filterable?: boolean; filterType?: "text" | "select"; filterOptions?: { value: string; label: string }[]; width?: string; align?: "left" | "center" | "right"; } const columns: ColumnDef[] = [ { key: "invoice_number", label: "계산서번호", sortable: true, filterable: true, filterType: "text", width: "120px" }, { key: "invoice_type", label: "유형", sortable: true, filterable: true, filterType: "select", filterOptions: [{ value: "sales", label: "매출" }, { value: "purchase", label: "매입" }], width: "70px" }, { key: "cost_type", label: "비용유형", sortable: true, filterable: true, filterType: "select", filterOptions: Object.entries(costTypeLabels).map(([value, label]) => ({ value, label })), width: "90px" }, { key: "invoice_status", label: "상태", sortable: true, filterable: true, filterType: "select", filterOptions: [ { value: "draft", label: "임시저장" }, { value: "issued", label: "발행완료" }, { value: "sent", label: "전송완료" }, { value: "cancelled", label: "취소됨" } ], width: "90px" }, { key: "invoice_date", label: "작성일", sortable: true, filterable: true, filterType: "text", width: "100px" }, { key: "buyer_name", label: "공급받는자", sortable: true, filterable: true, filterType: "text" }, { key: "attachments", label: "첨부", sortable: false, filterable: false, width: "50px", align: "center" }, { key: "supply_amount", label: "공급가액", sortable: true, filterable: false, align: "right" }, { key: "tax_amount", label: "세액", sortable: true, filterable: false, align: "right" }, { key: "total_amount", label: "합계", sortable: true, filterable: false, align: "right" }, ]; export function TaxInvoiceList() { // 상태 const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(false); const [pagination, setPagination] = useState({ page: 1, pageSize: 20, total: 0, totalPages: 0, }); // 필터 상태 const [filters, setFilters] = useState({ page: 1, pageSize: 20, }); const [searchText, setSearchText] = useState(""); // 정렬 상태 const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); // 컬럼별 필터 상태 const [columnFilters, setColumnFilters] = useState>({}); const [activeFilterColumn, setActiveFilterColumn] = useState(null); // 모달 상태 const [showForm, setShowForm] = useState(false); const [showDetail, setShowDetail] = useState(false); const [selectedInvoice, setSelectedInvoice] = useState(null); const [editMode, setEditMode] = useState(false); // 확인 다이얼로그 상태 const [confirmDialog, setConfirmDialog] = useState<{ open: boolean; type: "delete" | "issue" | "cancel"; invoice: TaxInvoice | null; }>({ open: false, type: "delete", invoice: null, }); // 데이터 로드 const loadData = useCallback(async () => { setLoading(true); try { // 컬럼 필터를 API 파라미터에 추가 const apiFilters: TaxInvoiceListParams = { ...filters, invoice_type: columnFilters.invoice_type as "sales" | "purchase" | undefined, invoice_status: columnFilters.invoice_status, cost_type: columnFilters.cost_type as CostType | undefined, search: columnFilters.invoice_number || columnFilters.buyer_name || searchText || undefined, }; const response = await getTaxInvoiceList(apiFilters); if (response.success) { let data = response.data; // 클라이언트 사이드 정렬 적용 if (sortConfig) { data = [...data].sort((a, b) => { const aVal = a[sortConfig.key as keyof TaxInvoice]; const bVal = b[sortConfig.key as keyof TaxInvoice]; if (aVal === null || aVal === undefined) return 1; if (bVal === null || bVal === undefined) return -1; // 숫자 비교 if (typeof aVal === "number" && typeof bVal === "number") { return sortConfig.direction === "asc" ? aVal - bVal : bVal - aVal; } // 문자열 비교 const strA = String(aVal).toLowerCase(); const strB = String(bVal).toLowerCase(); if (sortConfig.direction === "asc") { return strA.localeCompare(strB, "ko"); } return strB.localeCompare(strA, "ko"); }); } // 클라이언트 사이드 필터 적용 (날짜 필터) if (columnFilters.invoice_date) { data = data.filter((item) => item.invoice_date?.includes(columnFilters.invoice_date) ); } setInvoices(data); setPagination(response.pagination); } } catch (error: any) { toast.error("데이터 로드 실패", { description: error.message }); } finally { setLoading(false); } }, [filters, sortConfig, columnFilters, searchText]); useEffect(() => { loadData(); }, [loadData]); // 정렬 핸들러 const handleSort = (columnKey: string) => { setSortConfig((prev) => { if (prev?.key === columnKey) { // 같은 컬럼 클릭: asc -> desc -> null 순환 if (prev.direction === "asc") return { key: columnKey, direction: "desc" }; return null; } // 새 컬럼: asc로 시작 return { key: columnKey, direction: "asc" }; }); }; // 컬럼 필터 핸들러 const handleColumnFilter = (columnKey: string, value: string) => { setColumnFilters((prev) => { if (!value) { const { [columnKey]: _, ...rest } = prev; return rest; } return { ...prev, [columnKey]: value }; }); setFilters((prev) => ({ ...prev, page: 1 })); // 필터 변경 시 첫 페이지로 }; // 필터 초기화 const clearColumnFilter = (columnKey: string) => { setColumnFilters((prev) => { const { [columnKey]: _, ...rest } = prev; return rest; }); setActiveFilterColumn(null); }; // 모든 필터 초기화 const clearAllFilters = () => { setColumnFilters({}); setSortConfig(null); setSearchText(""); setFilters({ page: 1, pageSize: 20 }); }; // 정렬 아이콘 렌더링 const renderSortIcon = (columnKey: string) => { if (sortConfig?.key !== columnKey) { return ; } return sortConfig.direction === "asc" ? : ; }; // 검색 const handleSearch = () => { setFilters((prev) => ({ ...prev, search: searchText, page: 1 })); }; // 필터 변경 const handleFilterChange = (key: keyof TaxInvoiceListParams, value: string | undefined) => { setFilters((prev) => ({ ...prev, [key]: value === "all" ? undefined : value, page: 1, })); }; // 새 세금계산서 const handleNew = () => { setSelectedInvoice(null); setEditMode(false); setShowForm(true); }; // 상세 보기 const handleView = (invoice: TaxInvoice) => { setSelectedInvoice(invoice); setShowDetail(true); }; // 수정 const handleEdit = (invoice: TaxInvoice) => { if (invoice.invoice_status !== "draft") { toast.warning("임시저장 상태의 세금계산서만 수정할 수 있습니다."); return; } setSelectedInvoice(invoice); setEditMode(true); setShowForm(true); }; // 삭제 확인 const handleDeleteConfirm = (invoice: TaxInvoice) => { if (invoice.invoice_status !== "draft") { toast.warning("임시저장 상태의 세금계산서만 삭제할 수 있습니다."); return; } setConfirmDialog({ open: true, type: "delete", invoice }); }; // 발행 확인 const handleIssueConfirm = (invoice: TaxInvoice) => { if (invoice.invoice_status !== "draft") { toast.warning("임시저장 상태의 세금계산서만 발행할 수 있습니다."); return; } setConfirmDialog({ open: true, type: "issue", invoice }); }; // 취소 확인 const handleCancelConfirm = (invoice: TaxInvoice) => { if (!["draft", "issued"].includes(invoice.invoice_status)) { toast.warning("취소할 수 없는 상태입니다."); return; } setConfirmDialog({ open: true, type: "cancel", invoice }); }; // 확인 다이얼로그 실행 const handleConfirmAction = async () => { const { type, invoice } = confirmDialog; if (!invoice) return; try { if (type === "delete") { const response = await deleteTaxInvoice(invoice.id); if (response.success) { toast.success("세금계산서가 삭제되었습니다."); loadData(); } } else if (type === "issue") { const response = await issueTaxInvoice(invoice.id); if (response.success) { toast.success("세금계산서가 발행되었습니다."); loadData(); } } else if (type === "cancel") { const response = await cancelTaxInvoice(invoice.id); if (response.success) { toast.success("세금계산서가 취소되었습니다."); loadData(); } } } catch (error: any) { toast.error("작업 실패", { description: error.message }); } finally { setConfirmDialog({ open: false, type: "delete", invoice: null }); } }; // 폼 저장 완료 const handleFormSave = () => { setShowForm(false); setSelectedInvoice(null); loadData(); }; // 금액 포맷 const formatAmount = (amount: number) => { return new Intl.NumberFormat("ko-KR").format(amount); }; // 날짜 포맷 const formatDate = (dateString: string) => { try { return format(new Date(dateString), "yyyy-MM-dd", { locale: ko }); } catch { return dateString; } }; return (
{/* 헤더 */}

세금계산서 관리

{/* 필터 영역 */}
{/* 검색 */}
setSearchText(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} className="h-9" />
{/* 유형 필터 */}
{/* 상태 필터 */}
{/* 새로고침 */} {/* 필터 초기화 */} {(Object.keys(columnFilters).length > 0 || sortConfig) && ( )}
{/* 활성 필터 표시 */} {Object.keys(columnFilters).length > 0 && (
{Object.entries(columnFilters).map(([key, value]) => { const column = columns.find((c) => c.key === key); let displayValue = value; if (column?.filterOptions) { displayValue = column.filterOptions.find((o) => o.value === value)?.label || value; } return ( {column?.label}: {displayValue} ); })}
)}
{/* 테이블 */} {columns.map((column) => (
{/* 컬럼 필터 (filterable인 경우) */} {column.filterable && ( setActiveFilterColumn(open ? column.key : null)} >
{column.label} 필터
{column.filterType === "select" ? ( ) : ( handleColumnFilter(column.key, e.target.value)} onKeyDown={(e) => e.key === "Enter" && setActiveFilterColumn(null)} className="h-8 text-xs" autoFocus /> )} {columnFilters[column.key] && ( )}
)} {/* 컬럼 라벨 + 정렬 */} {column.sortable ? ( ) : ( {column.label} )}
))} 작업
{loading ? ( 로딩 중... ) : invoices.length === 0 ? ( 세금계산서가 없습니다. ) : ( invoices.map((invoice) => ( {invoice.invoice_number} {typeLabels[invoice.invoice_type]} {invoice.cost_type ? ( {costTypeLabels[invoice.cost_type as CostType]} ) : ( - )} {statusLabels[invoice.invoice_status]} {formatDate(invoice.invoice_date)} {invoice.buyer_name || "-"} {invoice.attachments && invoice.attachments.length > 0 ? (
{invoice.attachments.length}
) : ( - )}
{formatAmount(invoice.supply_amount)} {formatAmount(invoice.tax_amount)} {formatAmount(invoice.total_amount)}
{invoice.invoice_status === "draft" && ( <> )} {invoice.invoice_status === "issued" && ( )}
)) )}
{/* 페이지네이션 */} {pagination.totalPages > 1 && (
총 {pagination.total}건 중 {(pagination.page - 1) * pagination.pageSize + 1}- {Math.min(pagination.page * pagination.pageSize, pagination.total)}건
)} {/* 세금계산서 작성/수정 폼 */} {showForm && ( setShowForm(false)} onSave={handleFormSave} invoice={editMode ? selectedInvoice : null} /> )} {/* 세금계산서 상세 */} {showDetail && selectedInvoice && ( setShowDetail(false)} invoiceId={selectedInvoice.id} /> )} {/* 확인 다이얼로그 */} !open && setConfirmDialog({ ...confirmDialog, open: false })} > {confirmDialog.type === "delete" && "세금계산서 삭제"} {confirmDialog.type === "issue" && "세금계산서 발행"} {confirmDialog.type === "cancel" && "세금계산서 취소"} {confirmDialog.type === "delete" && "이 세금계산서를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다."} {confirmDialog.type === "issue" && "이 세금계산서를 발행하시겠습니까? 발행 후에는 수정할 수 없습니다."} {confirmDialog.type === "cancel" && "이 세금계산서를 취소하시겠습니까?"}
); }