'use client'; /** * 분개 수정 팝업 * * - 세금계산서 정보 (읽기전용): 구분, 거래처, 공급가액, 세액 * - 분개 내역 테이블: 구분(차변/대변), 계정과목 Select, 차변 금액, 대변 금액 * - 동적 행 추가/삭제 + 합계 행 * - 버튼: 분개 삭제, 취소, 분개 수정 */ import { useState, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; import { formatNumber } from '@/lib/utils/amount'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableFooter, } from '@/components/ui/table'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { getJournalEntries, updateJournalEntry, deleteJournalEntry, } from './actions'; import { AccountSubjectSelect } from '@/components/accounting/common'; import type { TaxInvoiceMgmtRecord, JournalEntryRow, JournalSide } from './types'; import { TAB_OPTIONS, JOURNAL_SIDE_OPTIONS, } from './types'; interface JournalEntryModalProps { open: boolean; onOpenChange: (open: boolean) => void; invoice: TaxInvoiceMgmtRecord; onSuccess: () => void; } function createEmptyRow(): JournalEntryRow { return { id: crypto.randomUUID(), side: 'debit', accountSubject: '', debitAmount: 0, creditAmount: 0, }; } export function JournalEntryModal({ open, onOpenChange, invoice, onSuccess, }: JournalEntryModalProps) { const [rows, setRows] = useState([createEmptyRow()]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // 기존 분개 내역 로드 useEffect(() => { if (!open || !invoice) return; const loadEntries = async () => { setIsLoading(true); try { const result = await getJournalEntries(invoice.id); if (result.success && result.data && result.data.rows.length > 0) { setRows( result.data.rows.map((r) => ({ id: crypto.randomUUID(), side: r.side as JournalSide, accountSubject: r.account_subject, debitAmount: r.debit_amount, creditAmount: r.credit_amount, })) ); } else { // 기본 행 2개 (차변/대변) setRows([ { ...createEmptyRow(), side: 'debit', debitAmount: invoice.totalAmount }, { ...createEmptyRow(), side: 'credit', creditAmount: invoice.totalAmount }, ]); } } catch { setRows([ { ...createEmptyRow(), side: 'debit', debitAmount: invoice.totalAmount }, { ...createEmptyRow(), side: 'credit', creditAmount: invoice.totalAmount }, ]); } finally { setIsLoading(false); } }; loadEntries(); }, [open, invoice]); // 행 추가 const handleAddRow = useCallback(() => { setRows((prev) => [...prev, createEmptyRow()]); }, []); // 행 삭제 const handleRemoveRow = useCallback((rowId: string) => { setRows((prev) => { if (prev.length <= 1) return prev; return prev.filter((r) => r.id !== rowId); }); }, []); // 행 수정 const handleRowChange = useCallback( (rowId: string, field: keyof JournalEntryRow, value: string | number) => { setRows((prev) => prev.map((r) => { if (r.id !== rowId) return r; const updated = { ...r, [field]: value }; // 구분 변경 시 금액 초기화 if (field === 'side') { if (value === 'debit') { updated.creditAmount = 0; } else { updated.debitAmount = 0; } } return updated; }) ); }, [] ); // 합계 계산 const totals = useMemo(() => { const debitTotal = rows.reduce((sum, r) => sum + (r.debitAmount || 0), 0); const creditTotal = rows.reduce((sum, r) => sum + (r.creditAmount || 0), 0); return { debitTotal, creditTotal }; }, [rows]); // 분개 수정 저장 const handleSave = useCallback(async () => { // 유효성 검사 const hasEmptyAccount = rows.some((r) => !r.accountSubject); if (hasEmptyAccount) { toast.warning('모든 행의 계정과목을 선택해주세요.'); return; } if (totals.debitTotal !== totals.creditTotal) { toast.warning('차변 합계와 대변 합계가 일치하지 않습니다.'); return; } setIsSaving(true); try { const result = await updateJournalEntry(invoice.id, rows); if (result.success) { toast.success('분개가 수정되었습니다.'); onSuccess(); } else { toast.error(result.error || '분개 수정에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsSaving(false); } }, [invoice.id, rows, totals, onSuccess]); // 분개 삭제 const handleDelete = useCallback(async () => { setShowDeleteConfirm(false); try { const result = await deleteJournalEntry(invoice.id); if (result.success) { toast.success('분개가 삭제되었습니다.'); onSuccess(); } else { toast.error(result.error || '분개 삭제에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } }, [invoice.id, onSuccess]); const divisionLabel = TAB_OPTIONS.find((t) => t.value === invoice.division)?.label || invoice.division; return ( <> 분개 수정 {/* 세금계산서 정보 (읽기전용) */}
{divisionLabel}
{invoice.vendorName}
{formatNumber(invoice.supplyAmount)}원
{formatNumber(invoice.taxAmount)}원
{/* 분개 내역 */}
{isLoading ? (
로딩 중...
) : ( 구분 계정과목 차변 금액 대변 금액 {rows.map((row) => ( handleRowChange(row.id, 'accountSubject', v) } placeholder="선택" size="sm" /> handleRowChange( row.id, 'debitAmount', Number(e.target.value) || 0 ) } disabled={row.side === 'credit'} className="h-8 text-sm text-right" placeholder="0" /> handleRowChange( row.id, 'creditAmount', Number(e.target.value) || 0 ) } disabled={row.side === 'debit'} className="h-8 text-sm text-right" placeholder="0" /> ))} 합계 {formatNumber(totals.debitTotal)} {formatNumber(totals.creditTotal)}
)}
{/* 차대변 불일치 경고 */} {totals.debitTotal !== totals.creditTotal && (

차변 합계({formatNumber(totals.debitTotal)})와 대변 합계( {formatNumber(totals.creditTotal)})가 일치하지 않습니다.

)}
{/* 분개 삭제 확인 */} 분개 삭제 이 세금계산서의 분개 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 취소 삭제 ); }