'use client'; /** * 세금계산서 관리 - 메인 리스트 페이지 * * - 매출/매입 탭 (카운트 표시) * - 일자유형 Select + 날짜범위 + 분기버튼 + 거래처 검색 * - 요약 카드 (매출/매입 공급가액/세액/합계) * - 테이블 + 범례 + 기간 요약 * - 분개 버튼 → JournalEntryModal * - 수기 입력 버튼 → ManualEntryModal */ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import dynamic from 'next/dynamic'; import { toast } from 'sonner'; import { FileText, Download, PenLine, Search, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { StatCards } from '@/components/organisms/StatCards'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; 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, } from '@/components/templates/UniversalListPage'; import { MobileCard } from '@/components/organisms/MobileCard'; import { getTaxInvoices, getTaxInvoiceSummary, downloadTaxInvoiceExcel, } from './actions'; const ManualEntryModal = dynamic( () => import('./ManualEntryModal').then(mod => ({ default: mod.ManualEntryModal })), ); const JournalEntryModal = dynamic( () => import('./JournalEntryModal').then(mod => ({ default: mod.JournalEntryModal })), ); import type { TaxInvoiceMgmtRecord, InvoiceTab, TaxInvoiceSummary, } from './types'; import { TAB_OPTIONS, DATE_TYPE_OPTIONS, TAX_TYPE_LABELS, RECEIPT_TYPE_LABELS, INVOICE_STATUS_MAP, INVOICE_SOURCE_LABELS, } from './types'; import { formatNumber } from '@/lib/utils/amount'; // ===== 분기 옵션 ===== const QUARTER_BUTTONS = [ { value: 'Q1', label: '1분기', startMonth: 1, endMonth: 3 }, { value: 'Q2', label: '2분기', startMonth: 4, endMonth: 6 }, { value: 'Q3', label: '3분기', startMonth: 7, endMonth: 9 }, { value: 'Q4', label: '4분기', startMonth: 10, endMonth: 12 }, ]; // ===== 테이블 컬럼 ===== const tableColumns = [ { key: 'writeDate', label: '작성일자', className: 'text-center', sortable: true }, { key: 'issueDate', label: '발급일자', className: 'text-center', sortable: true }, { key: 'vendorName', label: '거래처', sortable: true }, { key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center', sortable: true }, { key: 'taxType', label: '과세형태', className: 'text-center', sortable: true }, { key: 'itemName', label: '품목', sortable: true }, { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, { key: 'taxAmount', label: '세액', className: 'text-right', sortable: true }, { key: 'totalAmount', label: '합계', className: 'text-right', sortable: true }, { key: 'receiptType', label: '영수청구', className: 'text-center', sortable: true }, { key: 'documentType', label: '문서형태', className: 'text-center', sortable: true }, { key: 'issueType', label: '발급형태', className: 'text-center', sortable: true }, { key: 'status', label: '상태', className: 'text-center', sortable: true }, { key: 'journal', label: '분개', className: 'text-center w-[80px]' }, ]; // ===== 날짜 헬퍼 ===== function getQuarterDates(year: number, quarter: string) { const q = QUARTER_BUTTONS.find((b) => b.value === quarter); if (!q) return { start: '', end: '' }; const start = `${year}-${String(q.startMonth).padStart(2, '0')}-01`; const lastDay = new Date(year, q.endMonth, 0).getDate(); const end = `${year}-${String(q.endMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`; return { start, end }; } function getCurrentQuarter(): string { const month = new Date().getMonth() + 1; if (month <= 3) return 'Q1'; if (month <= 6) return 'Q2'; if (month <= 9) return 'Q3'; return 'Q4'; } export function TaxInvoiceManagement() { // ===== 필터 상태 ===== const currentYear = new Date().getFullYear(); const [activeTab, setActiveTab] = useState('sales'); const [dateType, setDateType] = useState('write_date'); const [selectedQuarter, setSelectedQuarter] = useState(getCurrentQuarter()); const [startDate, setStartDate] = useState(() => { const q = getQuarterDates(currentYear, getCurrentQuarter()); return q.start; }); const [endDate, setEndDate] = useState(() => { const q = getQuarterDates(currentYear, getCurrentQuarter()); return q.end; }); const [vendorSearch, setVendorSearch] = useState(''); // ===== 데이터 상태 ===== const [invoiceData, setInvoiceData] = useState([]); const [isLoading, setIsLoading] = useState(false); const isInitialLoadDone = useRef(false); const [currentPage, setCurrentPage] = useState(1); const [pagination, setPagination] = useState({ currentPage: 1, lastPage: 1, perPage: 20, total: 0, }); // ===== 요약 상태 ===== const [summary, setSummary] = useState({ salesSupplyAmount: 0, salesTaxAmount: 0, salesTotalAmount: 0, salesCount: 0, purchaseSupplyAmount: 0, purchaseTaxAmount: 0, purchaseTotalAmount: 0, purchaseCount: 0, }); // ===== 모달 상태 ===== const [showManualEntry, setShowManualEntry] = useState(false); const [journalTarget, setJournalTarget] = useState(null); // ===== 분기 버튼 클릭 ===== const handleQuarterClick = useCallback((quarter: string) => { setSelectedQuarter(quarter); const dates = getQuarterDates(currentYear, quarter); setStartDate(dates.start); setEndDate(dates.end); setCurrentPage(1); }, [currentYear]); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { if (!isInitialLoadDone.current) { setIsLoading(true); } try { const [listResult, summaryResult] = await Promise.all([ getTaxInvoices({ division: activeTab, dateType, startDate, endDate, vendorSearch, page: currentPage, perPage: 20, }), getTaxInvoiceSummary({ dateType, startDate, endDate, vendorSearch, }), ]); if (listResult.success) { setInvoiceData(listResult.data); setPagination(listResult.pagination); } else { toast.error(listResult.error || '목록 조회에 실패했습니다.'); } if (summaryResult.success && summaryResult.data) { setSummary(summaryResult.data); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsLoading(false); isInitialLoadDone.current = true; } }, [activeTab, dateType, startDate, endDate, vendorSearch, currentPage]); useEffect(() => { loadData(); }, [loadData]); // ===== 탭 변경 ===== const handleTabChange = useCallback((tab: InvoiceTab) => { setActiveTab(tab); setCurrentPage(1); }, []); // ===== 조회 버튼 ===== const handleSearch = useCallback(() => { setCurrentPage(1); loadData(); }, [loadData]); // ===== 엑셀 다운로드 ===== const handleExcelDownload = useCallback(async () => { const result = await downloadTaxInvoiceExcel({ division: activeTab, dateType, startDate, endDate, vendorSearch, }); if (result.success && result.data) { window.open(result.data.url, '_blank'); } else { toast.error(result.error || '엑셀 다운로드에 실패했습니다.'); } }, [activeTab, dateType, startDate, endDate, vendorSearch]); // ===== 수기 등록 완료 ===== const handleManualEntrySuccess = useCallback(() => { setShowManualEntry(false); loadData(); toast.success('세금계산서가 등록되었습니다.'); }, [loadData]); // ===== 분개 완료 ===== const handleJournalSuccess = useCallback(() => { setJournalTarget(null); loadData(); }, [loadData]); // ===== 기간 요약 계산 ===== const periodDifference = useMemo(() => { return summary.salesTotalAmount - summary.purchaseTotalAmount; }, [summary]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ title: '세금계산서 관리', description: '홈택스에 신고된 세금계산서 매입/매출 내역을 조회하고 관리합니다', icon: FileText, basePath: '/accounting/tax-invoices', idField: 'id', actions: { getList: async () => ({ success: true, data: invoiceData, totalCount: pagination.total, }), }, columns: tableColumns, clientSideFiltering: false, itemsPerPage: 20, hideSearch: true, showCheckbox: false, // ===== 검색 영역 (beforeTableContent) ===== beforeTableContent: (
{/* 검색 필터 카드 */} {/* Row1: 일자타입 + 날짜범위 + 분기 버튼 + 조회 */}
{QUARTER_BUTTONS.map((q) => ( ))}
{/* Row2: 거래처 검색 (아이콘 포함) */}
setVendorSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder="사업자 번호 또는 사업자명" className="pl-9 h-9" />
{/* 요약 카드 5개 */} {/* 매출/매입 탭 + 액션 버튼 */}
handleTabChange(v as InvoiceTab)}> {TAB_OPTIONS.map((t) => ( {t.label} {t.value === 'sales' ? summary.salesCount : summary.purchaseCount} ))}
), // 탭은 beforeTableContent에서 수동 렌더링 (카드와 테이블 사이) // ===== 테이블 행 렌더링 ===== renderTableRow: ( item: TaxInvoiceMgmtRecord, _index: number, _globalIndex: number, _handlers: SelectionHandlers & RowClickHandlers ) => ( {item.writeDate} {item.issueDate || '-'} {item.vendorName} {item.vendorBusinessNumber} {TAX_TYPE_LABELS[item.taxType]} {item.itemName || '-'} {formatNumber(item.supplyAmount)} {formatNumber(item.taxAmount)} {formatNumber(item.totalAmount)} {RECEIPT_TYPE_LABELS[item.receiptType]} {item.documentNumber || '-'} {INVOICE_SOURCE_LABELS[item.source]} {INVOICE_STATUS_MAP[item.status].label} ), // ===== 모바일 카드 렌더링 ===== renderMobileCard: ( item: TaxInvoiceMgmtRecord, _index: number, _globalIndex: number, _handlers: SelectionHandlers & RowClickHandlers ) => ( setJournalTarget(item)} details={[ { label: '작성일자', value: item.writeDate }, { label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` }, { label: '세액', value: `${formatNumber(item.taxAmount)}원` }, { label: '합계', value: `${formatNumber(item.totalAmount)}원` }, { label: '과세여부', value: TAX_TYPE_LABELS[item.taxType] }, { label: '소스', value: INVOICE_SOURCE_LABELS[item.source] }, ]} /> ), // ===== 범례 (테이블 안) ===== tableFooter: (
수기 세금계산서
홈택스 연동 세금계산서
), // ===== 기간 요약 (테이블 뒤) ===== afterTableContent: () => (
기간 요약
{/* 모바일: 세로 스택, sm 이상: 가로 배치 */}
매출 합계 (공급가액 + 세액)
{formatNumber(summary.salesTotalAmount)}원
매입 합계 (공급가액 + 세액)
{formatNumber(summary.purchaseTotalAmount)}원
=
예상 부가세
= 0 ? 'text-blue-700' : 'text-red-700'}`}> {formatNumber(periodDifference)}원
), }), [ invoiceData, pagination, summary, dateType, startDate, endDate, selectedQuarter, vendorSearch, periodDifference, handleQuarterClick, handleSearch, handleExcelDownload, ] ); return ( <> {/* 수기 입력 팝업 */} {/* 분개 수정 팝업 */} {journalTarget && ( !open && setJournalTarget(null)} invoice={journalTarget} onSuccess={handleJournalSuccess} /> )} ); }