refactor: [ecard] 분리 항목별 개별 분개 생성 구조로 변경

- 기존 multi-split 번들 분개를 단일 split 개별 분개로 교체
- 원본 행 분개 열에 split별 집계 상태 표시 (N/M, 완료)
- 각 분리 행에 독립 분개 버튼 추가 (splitSourceKey 기반)
- handleOpenJournalModal에 singleSplit 파라미터 추가
- CardJournalModal 거래 정보에 개별 split 정보 표시
- 합계금액 열에 split 분개 진행률 표시
This commit is contained in:
김보곤
2026-02-24 19:53:46 +09:00
parent c0b350fa22
commit 6eff3e0d55

View File

@@ -703,58 +703,56 @@ className={`px-3 py-1.5 text-sm cursor-pointer ${index === highlightIndex ? 'bg-
// 기본 분개 라인
const getDefaultLines = () => {
// splits 데이터가 있으면 분리 항목 기반으로 라인 생성
const splits = log._splits || [];
if (splits.length > 0) {
const debitLines = [];
// 단일 split 분개 (분리 항목별 개별 분개)
const singleSplit = log._split;
if (singleSplit) {
const splitSupply = Math.round(parseFloat(singleSplit.split_supply_amount ?? singleSplit.supplyAmount ?? singleSplit.split_amount ?? singleSplit.amount ?? 0));
const splitTax = Math.round(parseFloat(singleSplit.split_tax ?? singleSplit.tax ?? 0));
const splitDeductionType = singleSplit.deduction_type || singleSplit.deductionType || 'non_deductible';
const splitAccountCode = singleSplit.account_code || singleSplit.accountCode || '826';
const splitAccountName = singleSplit.account_name || singleSplit.accountName || '잡비';
const lines = [];
let totalDebitSum = 0;
splits.forEach(split => {
const splitSupply = Math.round(parseFloat(split.split_supply_amount ?? split.supplyAmount ?? split.split_amount ?? split.amount ?? 0));
const splitTax = Math.round(parseFloat(split.split_tax ?? split.tax ?? 0));
const splitDeductionType = split.deduction_type || split.deductionType || 'non_deductible';
const splitAccountCode = split.account_code || split.accountCode || '826';
const splitAccountName = split.account_name || split.accountName || '잡비';
if (splitDeductionType === 'deductible') {
// 공제: 비용 계정 = 공급가액, 부가세대급금 = 세액
debitLines.push({
dc_type: 'debit', account_code: splitAccountCode, account_name: splitAccountName,
debit_amount: splitSupply, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: split.memo || ''
if (splitDeductionType === 'deductible') {
// 공제: 비용 계정 = 공급가액, 부가세대급금 = 세액
lines.push({
dc_type: 'debit', account_code: splitAccountCode, account_name: splitAccountName,
debit_amount: splitSupply, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: singleSplit.memo || ''
});
totalDebitSum += splitSupply;
if (splitTax > 0) {
lines.push({
dc_type: 'debit', account_code: '135', account_name: '부가세대급금',
debit_amount: splitTax, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: ''
});
totalDebitSum += splitSupply;
if (splitTax > 0) {
debitLines.push({
dc_type: 'debit', account_code: '135', account_name: '부가세대급금',
debit_amount: splitTax, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: ''
});
totalDebitSum += splitTax;
}
} else {
// 불공제: 비용 계정 = 공급가액 + 세액
const combined = splitSupply + splitTax;
debitLines.push({
dc_type: 'debit', account_code: splitAccountCode, account_name: splitAccountName,
debit_amount: combined, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: split.memo || ''
});
totalDebitSum += combined;
totalDebitSum += splitTax;
}
});
} else {
// 불공제: 비용 계정 = 공급가액 + 세액
const combined = splitSupply + splitTax;
lines.push({
dc_type: 'debit', account_code: splitAccountCode, account_name: splitAccountName,
debit_amount: combined, credit_amount: 0,
trading_partner_id: null, trading_partner_name: '', description: singleSplit.memo || ''
});
totalDebitSum += combined;
}
// 대변: 미지급비용 = 전체 합계
debitLines.push({
// 대변: 미지급비용 = 합계
lines.push({
dc_type: 'credit', account_code: '205', account_name: '미지급비용',
debit_amount: 0, credit_amount: totalDebitSum,
trading_partner_id: null, trading_partner_name: '', description: ''
});
return debitLines;
return lines;
}
// splits가 없으면 기존 로직 (원본 금액 기반)
// splits가 없으면 기존 로직 (원본 금액 기반, 분리 없는 거래용)
const expenseCode = log.accountCode || '826';
const expenseName = log.accountName || '잡비';
@@ -915,14 +913,22 @@ className={`px-3 py-1.5 text-sm cursor-pointer ${index === highlightIndex ? 'bg-
<div><span className="text-stone-500">공급가액: </span><span className="font-medium">{formatCurrency(supplyAmount)}</span></div>
<div><span className="text-stone-500">세액: </span><span className="font-medium">{formatCurrency(taxAmount)}</span></div>
</div>
{(log._splits || []).length > 0 && (
<div className="mt-3 flex items-center gap-2">
<span className="inline-flex items-center px-2.5 py-1 bg-amber-100 text-amber-700 rounded-lg text-xs font-bold">
분리 데이터 기반 ({log._splits.length})
</span>
{isEditMode && (
<span className="text-xs text-stone-400">저장된 분개가 있어 참고용으로 표시됩니다</span>
)}
{log._split && (
<div className="mt-3">
<div className="flex items-center gap-2 mb-2">
<span className="inline-flex items-center px-2.5 py-1 bg-amber-100 text-amber-700 rounded-lg text-xs font-bold">
분리 항목 분개
</span>
{isEditMode && (
<span className="text-xs text-stone-400">저장된 분개가 있어 참고용으로 표시됩니다</span>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm bg-amber-50 rounded-lg p-3">
<div><span className="text-stone-500">계정: </span><span className="font-medium">{log._split.account_name || log._split.accountName || '미지정'}</span></div>
<div><span className="text-stone-500">공제: </span><span className="font-medium">{(log._split.deduction_type || log._split.deductionType) === 'deductible' ? '공제' : '불공제'}</span></div>
<div><span className="text-stone-500">공급가액: </span><span className="font-medium">{formatCurrency(Math.round(parseFloat(log._split.split_supply_amount ?? log._split.supplyAmount ?? log._split.split_amount ?? log._split.amount ?? 0)))}</span></div>
<div><span className="text-stone-500">세액: </span><span className="font-medium">{formatCurrency(Math.round(parseFloat(log._split.split_tax ?? log._split.tax ?? 0)))}</span></div>
</div>
</div>
)}
</div>
@@ -1573,6 +1579,23 @@ className="p-1.5 text-amber-500 hover:bg-amber-100 rounded-lg transition-colors"
{/* 분개 열 */}
<td className="px-3 py-3 text-center">
{(() => {
if (hasSplits) {
// 분리 항목별 분개 집계
const splitJournalCount = logSplits.filter(s => journalMap[`${uniqueKey}|split:${s.id}`]).length;
if (splitJournalCount === logSplits.length) {
return (
<span className="px-1.5 py-0.5 bg-emerald-100 text-emerald-700 rounded text-[10px] font-bold">
완료
</span>
);
} else {
return (
<span className={`text-[10px] font-bold ${splitJournalCount > 0 ? 'text-purple-600' : 'text-stone-400'}`}>
{splitJournalCount}/{logSplits.length}
</span>
);
}
}
const jInfo = journalMap[uniqueKey];
if (jInfo) {
return (
@@ -1584,16 +1607,6 @@ className="px-1.5 py-0.5 bg-emerald-100 text-emerald-700 rounded text-[10px] fon
완료
</button>
);
} else if (hasSplits) {
return (
<button
onClick={() => onOpenJournalModal(log, uniqueKey, false, logSplits)}
className="px-1.5 py-0.5 bg-purple-100 text-purple-700 rounded text-[10px] font-bold hover:bg-purple-200 transition-colors"
title="분리 데이터 기반 분개 생성"
>
분개
</button>
);
} else {
return (
<button
@@ -1691,12 +1704,16 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:outlin
</div>
);
})()}
{journalMap[uniqueKey] && (
{!hasSplits && journalMap[uniqueKey] && (
<div className="text-xs text-emerald-600 mt-1">{journalMap[uniqueKey].entry_no}</div>
)}
{!journalMap[uniqueKey] && hasSplits && (
<div className="text-xs text-amber-600 mt-1">분리됨({logSplits.length})</div>
)}
{hasSplits && (() => {
const splitJournalCount = logSplits.filter(s => journalMap[`${uniqueKey}|split:${s.id}`]).length;
if (splitJournalCount > 0) {
return <div className="text-xs text-emerald-600 mt-1">분개 {splitJournalCount}/{logSplits.length}</div>;
}
return <div className="text-xs text-amber-600 mt-1">분리됨({logSplits.length})</div>;
})()}
</td>
<td className={`px-4 py-3 text-right ${log.isAmountModified && log.modifiedSupplyAmount !== null ? 'bg-orange-50' : ''}`}>
{hasSplits ? (
@@ -1827,7 +1844,33 @@ className="p-1 text-stone-400 hover:text-red-500 hover:bg-red-50 rounded transit
<td className="px-3 py-2 text-center">
<div className="w-4 h-4 border-l-2 border-b-2 border-amber-300 ml-2"></div>
</td>
<td></td>
<td className="px-3 py-2 text-center">
{(() => {
const splitSourceKey = `${uniqueKey}|split:${split.id}`;
const sjInfo = journalMap[splitSourceKey];
if (sjInfo) {
return (
<button
onClick={() => onOpenJournalModal(log, splitSourceKey, true, logSplits, split)}
className="px-1.5 py-0.5 bg-emerald-100 text-emerald-700 rounded text-[10px] font-bold hover:bg-emerald-200 transition-colors"
title={`전표: ${sjInfo.entry_no}`}
>
완료
</button>
);
} else {
return (
<button
onClick={() => onOpenJournalModal(log, splitSourceKey, false, logSplits, split)}
className="px-1.5 py-0.5 bg-purple-100 text-purple-700 rounded text-[10px] font-bold hover:bg-purple-200 transition-colors"
title="분리 항목 분개"
>
분개
</button>
);
}
})()}
</td>
<td colSpan="2" className="px-4 py-2 text-xs text-stone-600">
분리 #{splitIdx + 1} {split.memo && `- ${split.memo}`}
</td>
@@ -2105,13 +2148,14 @@ className="px-3 py-1 bg-green-500 text-white text-xs rounded-lg hover:bg-green-6
};
// 복식부기 분개 모달 열기
const handleOpenJournalModal = (log, uniqueKey, hasJournal, logSplits) => {
const handleOpenJournalModal = (log, sourceKey, hasJournal, logSplits, singleSplit) => {
const logWithJournalInfo = {
...log,
uniqueKey,
uniqueKey: sourceKey,
_hasJournal: hasJournal,
_journalData: null,
_splits: logSplits || [],
_split: singleSplit || null,
};
setJournalModalLog(logWithJournalInfo);
setJournalModalOpen(true);