refactor: [ecard] 분리 항목별 개별 분개 생성 구조로 변경
- 기존 multi-split 번들 분개를 단일 split 개별 분개로 교체 - 원본 행 분개 열에 split별 집계 상태 표시 (N/M, 완료) - 각 분리 행에 독립 분개 버튼 추가 (splitSourceKey 기반) - handleOpenJournalModal에 singleSplit 파라미터 추가 - CardJournalModal 거래 정보에 개별 split 정보 표시 - 합계금액 열에 split 분개 진행률 표시
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user