'use client'; /** * 분개 수정 팝업 * * - 거래 정보 (읽기전용): 날짜, 구분, 금액, 적요, 계좌(은행명+계좌번호) * - 전표 적요: 적요 Input * - 분개 내역 테이블: 구분(차변/대변), 계정과목, 거래처, 차변 금액, 대변 금액, 적요, 삭제 * - 합계 행 + 대차 균형 표시 * - 버튼: 분개 삭제(왼쪽), 취소, 분개 수정 */ import { useState, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2 } from 'lucide-react'; 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 { FormField } from '@/components/molecules/FormField'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { AccountSubjectSelect } from '@/components/accounting/common'; 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 { getJournalDetail, updateJournalDetail, deleteJournalDetail, getVendorList, } from './actions'; import type { GeneralJournalRecord, JournalEntryRow, JournalSide, VendorOption, } from './types'; import { JOURNAL_SIDE_OPTIONS, JOURNAL_DIVISION_LABELS } from './types'; interface JournalEditModalProps { open: boolean; onOpenChange: (open: boolean) => void; record: GeneralJournalRecord; onSuccess: () => void; } function createEmptyRow(): JournalEntryRow { return { id: crypto.randomUUID(), side: 'debit', accountSubjectId: '', accountSubjectName: '', vendorId: '', vendorName: '', debitAmount: 0, creditAmount: 0, memo: '', }; } export function JournalEditModal({ open, onOpenChange, record, onSuccess, }: JournalEditModalProps) { // 전표 적요 const [journalMemo, setJournalMemo] = useState(''); // 분개 행 const [rows, setRows] = useState([createEmptyRow()]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); // 거래 정보 (읽기전용) const [bankName, setBankName] = useState(''); const [accountNumber, setAccountNumber] = useState(''); // 옵션 데이터 const [vendors, setVendors] = useState([]); // 데이터 로드 useEffect(() => { if (!open || !record) return; const loadData = async () => { setIsLoading(true); try { const [detailRes, vendorsRes] = await Promise.all([ getJournalDetail(record.id), getVendorList(), ]); if (vendorsRes.success && vendorsRes.data) { setVendors(vendorsRes.data); } if (detailRes.success && detailRes.data) { const detail = detailRes.data; setBankName(detail.bank_name || ''); setAccountNumber(detail.account_number || ''); setJournalMemo(detail.journal_memo || ''); if (detail.rows && detail.rows.length > 0) { setRows( detail.rows.map((r) => ({ id: crypto.randomUUID(), side: r.side as JournalSide, accountSubjectId: String(r.account_subject_id), accountSubjectName: r.account_subject_name, vendorId: r.vendor_id ? String(r.vendor_id) : '', vendorName: r.vendor_name || '', debitAmount: r.debit_amount, creditAmount: r.credit_amount, memo: r.memo || '', })) ); } else { setRows([ { ...createEmptyRow(), side: 'debit', debitAmount: record.amount }, { ...createEmptyRow(), side: 'credit', creditAmount: record.amount }, ]); } } else { setRows([ { ...createEmptyRow(), side: 'debit', debitAmount: record.amount }, { ...createEmptyRow(), side: 'credit', creditAmount: record.amount }, ]); } } catch { setRows([ { ...createEmptyRow(), side: 'debit', debitAmount: record.amount }, { ...createEmptyRow(), side: 'credit', creditAmount: record.amount }, ]); } finally { setIsLoading(false); } }; loadData(); }, [open, record]); // 행 추가 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); const isBalanced = debitTotal === creditTotal; const difference = Math.abs(debitTotal - creditTotal); return { debitTotal, creditTotal, isBalanced, difference }; }, [rows]); // 분개 수정 const handleSave = useCallback(async () => { const hasEmptyAccount = rows.some((r) => !r.accountSubjectId); if (hasEmptyAccount) { toast.warning('모든 행의 계정과목을 선택해주세요.'); return; } if (!totals.isBalanced) { toast.warning('차변 합계와 대변 합계가 일치하지 않습니다.'); return; } setIsSaving(true); try { const result = await updateJournalDetail(record.id, { journalMemo, rows, }); if (result.success) { toast.success('분개가 수정되었습니다.'); onSuccess(); } else { toast.error(result.error || '분개 수정에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsSaving(false); } }, [record.id, journalMemo, rows, totals, onSuccess]); // 분개 삭제 const handleDelete = useCallback(async () => { setShowDeleteConfirm(false); try { const result = await deleteJournalDetail(record.id); if (result.success) { toast.success('분개가 삭제되었습니다.'); onSuccess(); } else { toast.error(result.error || '분개 삭제에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } }, [record.id, onSuccess]); return ( <> 분개 수정 전표의 분개 내역을 수정합니다 {/* 거래 정보 (읽기전용) */}
{record.date}
{JOURNAL_DIVISION_LABELS[record.division] || record.division}
{formatNumber(record.amount)}원
{record.description || '-'}
{bankName && accountNumber ? `${bankName} ${accountNumber}` : '-'}
{/* 전표 적요 */} {/* 분개 내역 헤더 */}
{/* 분개 테이블 */}
{isLoading ? (
로딩 중...
) : ( 구분 계정과목 거래처 차변 금액 대변 금액 적요 {rows.map((row) => (
{JOURNAL_SIDE_OPTIONS.map((opt) => ( ))}
handleRowChange(row.id, 'accountSubjectId', v) } size="sm" placeholder="선택" /> 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" /> handleRowChange(row.id, 'memo', e.target.value)} className="h-8 text-sm" placeholder="적요" />
))}
합계 {formatNumber(totals.debitTotal)} {formatNumber(totals.creditTotal)} {totals.isBalanced ? ( 대차 균형 ) : ( 차이: {formatNumber(totals.difference)}원 )}
)}
{/* 분개 삭제 확인 */} 분개 삭제 이 전표의 분개 내역을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 취소 삭제 ); }