feat:계좌 입출금내역 분개(Split) 기능 구현

- BankTransactionSplit 모델 생성
- EaccountController에 splits/saveSplits/deleteSplits 메서드 추가
- 라우트 3개 추가 (GET/POST/DELETE splits)
- BankSplitModal React 컴포넌트 추가
- TransactionTable에 분개 컬럼/하위행 렌더링
- App 컴포넌트에 분개 상태 및 핸들러 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-06 14:43:21 +09:00
parent 6520923def
commit ce08d0110a
4 changed files with 677 additions and 12 deletions

View File

@@ -8,6 +8,7 @@
use App\Models\Barobill\BarobillMember;
use App\Models\Barobill\BankTransaction;
use App\Models\Barobill\BankTransactionOverride;
use App\Models\Barobill\BankTransactionSplit;
use App\Models\Tenants\Tenant;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -1286,6 +1287,115 @@ public function destroyManual(int $id): JsonResponse
}
}
/**
* 분개 내역 조회
*/
public function splits(Request $request): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$startDate = $request->input('startDate', date('Ymd'));
$endDate = $request->input('endDate', date('Ymd'));
$splits = BankTransactionSplit::getByDateRange($tenantId, $startDate, $endDate);
return response()->json([
'success' => true,
'data' => $splits
]);
} catch (\Throwable $e) {
Log::error('계좌 분개 내역 조회 오류: ' . $e->getMessage());
return response()->json([
'success' => false,
'error' => '조회 오류: ' . $e->getMessage()
]);
}
}
/**
* 분개 저장
*/
public function saveSplits(Request $request): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$uniqueKey = $request->input('uniqueKey');
$originalData = $request->input('originalData', []);
$splits = $request->input('splits', []);
if (empty($uniqueKey)) {
return response()->json([
'success' => false,
'error' => '고유키가 없습니다.'
]);
}
// 분개 금액 합계 검증
$originalAmount = floatval($originalData['originalAmount'] ?? 0);
$splitTotal = array_sum(array_map(function ($s) {
return floatval($s['amount'] ?? 0);
}, $splits));
if (abs($originalAmount - $splitTotal) > 0.01) {
return response()->json([
'success' => false,
'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다."
]);
}
DB::beginTransaction();
BankTransactionSplit::saveSplits($tenantId, $uniqueKey, $originalData, $splits);
DB::commit();
return response()->json([
'success' => true,
'message' => '분개가 저장되었습니다.',
'splitCount' => count($splits)
]);
} catch (\Throwable $e) {
DB::rollBack();
Log::error('계좌 분개 저장 오류: ' . $e->getMessage());
return response()->json([
'success' => false,
'error' => '저장 오류: ' . $e->getMessage()
]);
}
}
/**
* 분개 삭제 (원본으로 복원)
*/
public function deleteSplits(Request $request): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$uniqueKey = $request->input('uniqueKey');
if (empty($uniqueKey)) {
return response()->json([
'success' => false,
'error' => '고유키가 없습니다.'
]);
}
$deleted = BankTransactionSplit::deleteSplits($tenantId, $uniqueKey);
return response()->json([
'success' => true,
'message' => '분개가 삭제되었습니다.',
'deleted' => $deleted
]);
} catch (\Throwable $e) {
Log::error('계좌 분개 삭제 오류: ' . $e->getMessage());
return response()->json([
'success' => false,
'error' => '삭제 오류: ' . $e->getMessage()
]);
}
}
/**
* 병합된 로그에서 수동입력 건의 잔액을 직전 거래 기준으로 재계산
* 로그는 날짜 내림차순(DESC) 정렬 상태로 전달됨

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Models\Barobill;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\Tenants\Tenant;
/**
* 계좌 입출금 거래 분개 모델
* 하나의 입출금 거래를 여러 계정과목으로 분개하여 저장
*/
class BankTransactionSplit extends Model
{
protected $table = 'barobill_bank_transaction_splits';
protected $fillable = [
'tenant_id',
'original_unique_key',
'split_amount',
'account_code',
'account_name',
'description',
'memo',
'sort_order',
'bank_account_num',
'trans_dt',
'trans_date',
'original_deposit',
'original_withdraw',
'summary',
];
protected $casts = [
'split_amount' => 'decimal:2',
'original_deposit' => 'decimal:2',
'original_withdraw' => 'decimal:2',
'sort_order' => 'integer',
];
/**
* 테넌트 관계
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
/**
* 테넌트별 분개 내역 조회 (기간별)
* 고유키를 기준으로 그룹핑하여 반환
*/
public static function getByDateRange(int $tenantId, string $startDate, string $endDate): array
{
$splits = self::where('tenant_id', $tenantId)
->whereBetween('trans_date', [$startDate, $endDate])
->orderBy('original_unique_key')
->orderBy('sort_order')
->get();
// 고유키별로 그룹핑
$grouped = [];
foreach ($splits as $split) {
$key = $split->original_unique_key;
if (!isset($grouped[$key])) {
$grouped[$key] = [];
}
$grouped[$key][] = $split;
}
return $grouped;
}
/**
* 특정 거래의 분개 내역 조회
*/
public static function getByUniqueKey(int $tenantId, string $uniqueKey): \Illuminate\Database\Eloquent\Collection
{
return self::where('tenant_id', $tenantId)
->where('original_unique_key', $uniqueKey)
->orderBy('sort_order')
->get();
}
/**
* 특정 거래의 분개 내역 저장 (기존 분개 삭제 후 재생성)
*/
public static function saveSplits(int $tenantId, string $uniqueKey, array $originalData, array $splits): void
{
// 기존 분개 삭제
self::where('tenant_id', $tenantId)
->where('original_unique_key', $uniqueKey)
->delete();
// 새 분개 저장
foreach ($splits as $index => $split) {
self::create([
'tenant_id' => $tenantId,
'original_unique_key' => $uniqueKey,
'split_amount' => $split['amount'] ?? 0,
'account_code' => $split['accountCode'] ?? null,
'account_name' => $split['accountName'] ?? null,
'description' => $split['description'] ?? null,
'memo' => $split['memo'] ?? null,
'sort_order' => $index,
'bank_account_num' => $originalData['bankAccountNum'] ?? '',
'trans_dt' => $originalData['transDt'] ?? '',
'trans_date' => $originalData['transDate'] ?? '',
'original_deposit' => $originalData['originalDeposit'] ?? 0,
'original_withdraw' => $originalData['originalWithdraw'] ?? 0,
'summary' => $originalData['summary'] ?? '',
]);
}
}
/**
* 분개 내역 삭제 (원본으로 복원)
*/
public static function deleteSplits(int $tenantId, string $uniqueKey): int
{
return self::where('tenant_id', $tenantId)
->where('original_unique_key', $uniqueKey)
->delete();
}
}

View File

@@ -80,6 +80,9 @@
manualStore: '{{ route("barobill.eaccount.manual.store") }}',
manualUpdate: '{{ route("barobill.eaccount.manual.update", ":id") }}',
manualDestroy: '{{ route("barobill.eaccount.manual.destroy", ":id") }}',
splits: '{{ route("barobill.eaccount.splits") }}',
saveSplits: '{{ route("barobill.eaccount.splits.save") }}',
deleteSplits: '{{ route("barobill.eaccount.splits.delete") }}',
};
const CSRF_TOKEN = '{{ csrf_token() }}';
@@ -791,6 +794,239 @@ className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 t
);
};
// BankSplitModal Component - 계좌 입출금 분개 모달
const BankSplitModal = ({ isOpen, onClose, log, accountCodes, onSave, onReset, splits: existingSplits }) => {
const [splits, setSplits] = useState([]);
const [saving, setSaving] = useState(false);
const [resetting, setResetting] = useState(false);
useEffect(() => {
if (isOpen && log) {
if (existingSplits && existingSplits.length > 0) {
// 기존 분개 로드
setSplits(existingSplits.map(s => ({
amount: parseFloat(s.split_amount || s.amount || 0),
accountCode: s.account_code || s.accountCode || '',
accountName: s.account_name || s.accountName || '',
description: s.description || '',
memo: s.memo || ''
})));
} else {
// 새 분개: 원본 금액으로 1개 행 생성
const origAmount = log.deposit > 0 ? log.deposit : log.withdraw;
setSplits([{
amount: origAmount,
accountCode: log.accountCode || '',
accountName: log.accountName || '',
description: log.summary || '',
memo: ''
}]);
}
}
}, [isOpen, log, existingSplits]);
if (!isOpen || !log) return null;
const originalAmount = log.deposit > 0 ? log.deposit : log.withdraw;
const isDeposit = log.deposit > 0;
const splitTotal = splits.reduce((sum, s) => sum + (parseFloat(s.amount) || 0), 0);
const isValid = Math.abs(originalAmount - splitTotal) < 0.01;
const addSplit = () => {
const remaining = originalAmount - splitTotal;
setSplits([...splits, {
amount: remaining > 0 ? remaining : 0,
accountCode: '',
accountName: '',
description: '',
memo: ''
}]);
};
const removeSplit = (index) => {
if (splits.length <= 1) return;
setSplits(splits.filter((_, i) => i !== index));
};
const updateSplit = (index, updates) => {
const newSplits = [...splits];
newSplits[index] = { ...newSplits[index], ...updates };
setSplits(newSplits);
};
const handleSave = async () => {
if (!isValid) {
notify('분개 합계금액이 원본 금액과 일치하지 않습니다.', 'error');
return;
}
setSaving(true);
await onSave(log, splits);
setSaving(false);
onClose();
};
const handleReset = async () => {
if (!confirm('분개를 삭제하고 원본 거래로 복구하시겠습니까?')) {
return;
}
setResetting(true);
await onReset(log);
setResetting(false);
onClose();
};
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0);
const formatAmountInput = (value) => {
if (!value && value !== 0) return '';
return new Intl.NumberFormat('ko-KR').format(value);
};
const parseAmountInput = (value) => {
const cleaned = String(value).replace(/[^0-9.-]/g, '');
return parseFloat(cleaned) || 0;
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
<div className="p-6 border-b border-stone-200">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-stone-900">거래 분개</h3>
<button onClick={onClose} className="text-stone-400 hover:text-stone-600">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="mt-3 p-3 bg-stone-50 rounded-lg text-sm">
<div className="flex justify-between mb-1">
<span className="text-stone-500">적요</span>
<span className="font-medium">{log.summary || '-'}</span>
</div>
<div className="flex justify-between mb-1">
<span className="text-stone-500">거래일시</span>
<span className="font-medium">{log.transDateTime || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-stone-500">{isDeposit ? '입금액' : '출금액'}</span>
<span className={`font-bold ${isDeposit ? 'text-blue-600' : 'text-red-600'}`}>
{formatCurrency(originalAmount)}
</span>
</div>
</div>
</div>
<div className="p-6 overflow-y-auto" style={ {maxHeight: '400px'} }>
<div className="space-y-3">
{splits.map((split, index) => (
<div key={index} className="flex items-start gap-3 p-3 bg-stone-50 rounded-lg">
<div className="flex-1 grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-stone-500 mb-1">금액 <span className="text-red-500">*</span></label>
<input
type="text"
value={formatAmountInput(split.amount)}
onChange={(e) => updateSplit(index, { amount: parseAmountInput(e.target.value) })}
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm text-right focus:ring-2 focus:ring-emerald-500 outline-none"
placeholder="0"
/>
</div>
<div>
<label className="block text-xs text-stone-500 mb-1">계정과목</label>
<AccountCodeSelect
value={split.accountCode}
onChange={(code, name) => updateSplit(index, { accountCode: code, accountName: name })}
accountCodes={accountCodes}
/>
</div>
<div>
<label className="block text-xs text-stone-500 mb-1">내역</label>
<input
type="text"
value={split.description || ''}
onChange={(e) => updateSplit(index, { description: e.target.value })}
placeholder="내역"
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
/>
</div>
<div>
<label className="block text-xs text-stone-500 mb-1">메모</label>
<input
type="text"
value={split.memo || ''}
onChange={(e) => updateSplit(index, { memo: e.target.value })}
placeholder="분개 메모 (선택)"
className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none"
/>
</div>
</div>
<button
onClick={() => removeSplit(index)}
disabled={splits.length <= 1}
className="mt-6 p-2 text-red-500 hover:bg-red-50 rounded-lg disabled:opacity-30 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4" />
</svg>
</button>
</div>
))}
</div>
<button
onClick={addSplit}
className="mt-3 w-full py-2 border-2 border-dashed border-stone-300 text-stone-500 rounded-lg hover:border-emerald-400 hover:text-emerald-600 transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
분개 항목 추가
</button>
</div>
<div className="p-6 border-t border-stone-200 bg-stone-50">
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-stone-500">분개 합계</span>
<span className={`font-bold ${isValid ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(splitTotal)}
{!isValid && (
<span className="ml-2 text-xs font-normal">
(차이: {formatCurrency(originalAmount - splitTotal)})
</span>
)}
</span>
</div>
<div className="flex gap-3">
{existingSplits && existingSplits.length > 0 && (
<button
onClick={handleReset}
disabled={resetting}
className="py-2 px-4 border border-red-300 text-red-600 rounded-lg hover:bg-red-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{resetting ? '복구 중...' : '분개 복구'}
</button>
)}
<button
onClick={onClose}
className="flex-1 py-2 border border-stone-300 text-stone-700 rounded-lg hover:bg-stone-100 transition-colors"
>
취소
</button>
<button
onClick={handleSave}
disabled={!isValid || saving}
className="flex-1 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? '저장 중...' : '분개 저장'}
</button>
</div>
</div>
</div>
</div>
);
};
// ManualEntryModal Component (수동입력 모달)
const ManualEntryModal = ({ isOpen, onClose, onSave, editData, accountCodes, accounts, logs }) => {
const [form, setForm] = useState({});
@@ -1207,7 +1443,10 @@ className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 t
onEditTransaction,
onManualNew,
onManualEdit,
onManualDelete
onManualDelete,
splits,
onOpenSplitModal,
onDeleteSplits
}) => {
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원';
@@ -1355,6 +1594,7 @@ className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded
<table className="w-full text-left text-sm text-stone-600">
<thead className="bg-stone-50 text-xs uppercase font-medium text-stone-500 sticky top-0">
<tr>
<th className="px-2 py-4 bg-stone-50 text-center w-[50px]">분개</th>
<th className="px-4 py-4 bg-stone-50">거래일시</th>
<th className="px-4 py-4 bg-stone-50">계좌정보</th>
<th className="px-4 py-4 bg-stone-50">적요/내용</th>
@@ -1370,13 +1610,40 @@ className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded
<tbody className="divide-y divide-stone-100">
{logs.length === 0 ? (
<tr>
<td colSpan="10" className="px-6 py-8 text-center text-stone-400">
<td colSpan="11" className="px-6 py-8 text-center text-stone-400">
해당 기간에 조회된 입출금 내역이 없습니다.
</td>
</tr>
) : (
logs.map((log, index) => (
<tr key={index} className={`hover:bg-stone-50 transition-colors ${log.isSaved ? 'bg-green-50/30' : ''} ${log.isOverridden ? 'bg-amber-50/50' : ''}`}>
logs.map((log, index) => {
const logSplits = splits && splits[log.uniqueKey] ? splits[log.uniqueKey] : [];
const hasSplits = logSplits.length > 0;
return (
<React.Fragment key={index}>
<tr className={`hover:bg-stone-50 transition-colors ${log.isSaved ? 'bg-green-50/30' : ''} ${log.isOverridden ? 'bg-amber-50/50' : ''}`}>
<td className="px-2 py-3 text-center">
{!hasSplits ? (
<button
onClick={() => onOpenSplitModal(log, log.uniqueKey)}
className="p-1 text-purple-500 hover:bg-purple-50 rounded transition-colors"
title="분개 추가"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
</button>
) : (
<button
onClick={() => { if (confirm('분개를 삭제하시겠습니까?')) onDeleteSplits(log.uniqueKey); }}
className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
title="분개 삭제"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 12H4" />
</svg>
</button>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="font-medium text-stone-900">{log.transDateTime || '-'}</div>
{log.isManual && (
@@ -1426,13 +1693,24 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2
/>
</td>
<td className="px-4 py-3">
<AccountCodeSelect
value={log.accountCode}
onChange={(code, name) => onAccountCodeChange(index, code, name)}
accountCodes={accountCodes}
/>
{log.accountName && (
<div className="text-xs text-emerald-600 mt-1">{log.accountName}</div>
{hasSplits ? (
<button
onClick={() => onOpenSplitModal(log, log.uniqueKey, logSplits)}
className="text-sm text-emerald-600 hover:text-emerald-800 hover:underline font-medium"
>
분개 수정 ({logSplits.length})
</button>
) : (
<>
<AccountCodeSelect
value={log.accountCode}
onChange={(code, name) => onAccountCodeChange(index, code, name)}
accountCodes={accountCodes}
/>
{log.accountName && (
<div className="text-xs text-emerald-600 mt-1">{log.accountName}</div>
)}
</>
)}
</td>
<td className="px-3 py-3 text-center">
@@ -1460,7 +1738,39 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
)}
</td>
</tr>
))
{/* 분개 하위 행 */}
{hasSplits && logSplits.map((split, sIdx) => (
<tr key={`${index}-split-${sIdx}`} className="bg-amber-50/30">
<td className="px-2 py-2 text-center">
<svg className="w-4 h-4 text-amber-400 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
</svg>
</td>
<td className="px-4 py-2 text-xs text-stone-400" colSpan="2">
<span className="text-amber-600 font-medium">분개 #{sIdx + 1}</span>
{split.memo && <span className="ml-2 text-stone-400">- {split.memo}</span>}
</td>
<td className="px-4 py-2 text-xs text-stone-500">{split.description || '-'}</td>
<td className="px-4 py-2 text-right text-sm font-medium text-blue-600">
{log.deposit > 0 ? new Intl.NumberFormat('ko-KR').format(split.split_amount || 0) + '원' : '-'}
</td>
<td className="px-4 py-2 text-right text-sm font-medium text-red-600">
{log.withdraw > 0 ? new Intl.NumberFormat('ko-KR').format(split.split_amount || 0) + '원' : '-'}
</td>
<td className="px-4 py-2"></td>
<td className="px-4 py-2"></td>
<td className="px-4 py-2"></td>
<td className="px-4 py-2 text-xs">
{split.account_name && (
<span className="text-emerald-600">{split.account_code} {split.account_name}</span>
)}
</td>
<td className="px-3 py-2"></td>
</tr>
))}
</React.Fragment>
);
})
)}
</tbody>
</table>
@@ -1488,6 +1798,13 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
const [manualModalOpen, setManualModalOpen] = useState(false);
const [manualEditData, setManualEditData] = useState(null);
// 분개 관련 상태
const [splits, setSplits] = useState({});
const [splitModalOpen, setSplitModalOpen] = useState(false);
const [splitModalLog, setSplitModalLog] = useState(null);
const [splitModalKey, setSplitModalKey] = useState('');
const [splitModalExisting, setSplitModalExisting] = useState([]);
// 날짜 필터 상태 (기본: 현재 월)
const currentMonth = getMonthDates(0);
const [dateFrom, setDateFrom] = useState(currentMonth.from);
@@ -1545,6 +1862,8 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
setLogs(data.data?.logs || []);
setPagination(data.data?.pagination || {});
setSummary(data.data?.summary || {});
// 분개 데이터도 함께 로드
loadSplits();
} else {
setError(data.error || '조회 실패');
setLogs([]);
@@ -1557,6 +1876,99 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
}
};
// 분개 데이터 로드
const loadSplits = async () => {
try {
const params = new URLSearchParams({
startDate: dateFrom.replace(/-/g, ''),
endDate: dateTo.replace(/-/g, ''),
});
const response = await fetch(`${API.splits}?${params}`);
const data = await response.json();
if (data.success) {
setSplits(data.data || {});
}
} catch (err) {
console.error('분개 데이터 로드 오류:', err);
}
};
// 분개 모달 열기
const handleOpenSplitModal = (log, uniqueKey, existingSplits) => {
setSplitModalLog(log);
setSplitModalKey(uniqueKey);
setSplitModalExisting(existingSplits || []);
setSplitModalOpen(true);
};
// 분개 저장
const handleSaveSplits = async (log, splitData) => {
try {
const originalAmount = log.deposit > 0 ? log.deposit : log.withdraw;
const response = await fetch(API.saveSplits, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': CSRF_TOKEN,
'Accept': 'application/json'
},
body: JSON.stringify({
uniqueKey: log.uniqueKey,
originalData: {
bankAccountNum: log.bankAccountNum,
transDt: log.transDate + (log.transTime || ''),
transDate: log.transDate,
originalDeposit: log.deposit || 0,
originalWithdraw: log.withdraw || 0,
originalAmount: originalAmount,
summary: log.summary || '',
},
splits: splitData
})
});
const data = await response.json();
if (data.success) {
notify(data.message, 'success');
loadSplits();
} else {
notify(data.error || '분개 저장 실패', 'error');
}
} catch (err) {
notify('분개 저장 오류: ' + err.message, 'error');
}
};
// 분개 삭제
const handleDeleteSplits = async (uniqueKey) => {
try {
const response = await fetch(API.deleteSplits, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': CSRF_TOKEN,
'Accept': 'application/json'
},
body: JSON.stringify({ uniqueKey })
});
const data = await response.json();
if (data.success) {
notify(data.message, 'success');
loadSplits();
} else {
notify(data.error || '분개 삭제 실패', 'error');
}
} catch (err) {
notify('분개 삭제 오류: ' + err.message, 'error');
}
};
// 분개 복구 (모달 내 복구 버튼)
const handleResetSplits = async (log) => {
await handleDeleteSplits(log.uniqueKey);
};
// 계정과목 변경 핸들러
const handleAccountCodeChange = useCallback((index, code, name) => {
setLogs(prevLogs => {
@@ -1846,9 +2258,23 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors"
onManualNew={handleManualNew}
onManualEdit={handleManualEdit}
onManualDelete={handleManualDelete}
splits={splits}
onOpenSplitModal={handleOpenSplitModal}
onDeleteSplits={handleDeleteSplits}
/>
)}
{/* Bank Split Modal */}
<BankSplitModal
isOpen={splitModalOpen}
onClose={() => { setSplitModalOpen(false); setSplitModalLog(null); setSplitModalExisting([]); }}
log={splitModalLog}
accountCodes={accountCodes}
onSave={handleSaveSplits}
onReset={handleResetSplits}
splits={splitModalExisting}
/>
{/* Transaction Edit Modal */}
<TransactionEditModal
isOpen={editModalOpen}

View File

@@ -473,6 +473,10 @@
Route::post('/manual', [\App\Http\Controllers\Barobill\EaccountController::class, 'storeManual'])->name('manual.store');
Route::put('/manual/{id}', [\App\Http\Controllers\Barobill\EaccountController::class, 'updateManual'])->name('manual.update');
Route::delete('/manual/{id}', [\App\Http\Controllers\Barobill\EaccountController::class, 'destroyManual'])->name('manual.destroy');
// 분개 관련
Route::get('/splits', [\App\Http\Controllers\Barobill\EaccountController::class, 'splits'])->name('splits');
Route::post('/splits', [\App\Http\Controllers\Barobill\EaccountController::class, 'saveSplits'])->name('splits.save');
Route::delete('/splits', [\App\Http\Controllers\Barobill\EaccountController::class, 'deleteSplits'])->name('splits.delete');
});
// 카드 사용내역 (재무관리로 이동됨 - 데이터 API만 유지)