diff --git a/app/Http/Controllers/Barobill/EaccountController.php b/app/Http/Controllers/Barobill/EaccountController.php index 266d5102..9c75a735 100644 --- a/app/Http/Controllers/Barobill/EaccountController.php +++ b/app/Http/Controllers/Barobill/EaccountController.php @@ -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) 정렬 상태로 전달됨 diff --git a/app/Models/Barobill/BankTransactionSplit.php b/app/Models/Barobill/BankTransactionSplit.php new file mode 100644 index 00000000..509d68a1 --- /dev/null +++ b/app/Models/Barobill/BankTransactionSplit.php @@ -0,0 +1,125 @@ + '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(); + } +} diff --git a/resources/views/barobill/eaccount/index.blade.php b/resources/views/barobill/eaccount/index.blade.php index 78a36332..99fc44e0 100644 --- a/resources/views/barobill/eaccount/index.blade.php +++ b/resources/views/barobill/eaccount/index.blade.php @@ -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 ( +
| 분개 | 거래일시 | 계좌정보 | 적요/내용 | @@ -1370,13 +1610,40 @@ className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded|||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + | 해당 기간에 조회된 입출금 내역이 없습니다. | |||||||||||||||||||
| + {!hasSplits ? ( + + ) : ( + + )} + |
{log.transDateTime || '-'}
{log.isManual && (
@@ -1426,13 +1693,24 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2
/>
|
- {log.accountName}
+ {hasSplits ? (
+
+ ) : (
+ <>
+ {log.accountName}
+ )}
+ >
)}
|
@@ -1460,7 +1738,39 @@ className="p-1 text-red-500 hover:bg-red-50 rounded transition-colors" )} | |||||||||||||||||
| + + | ++ 분개 #{sIdx + 1} + {split.memo && - {split.memo}} + | +{split.description || '-'} | ++ {log.deposit > 0 ? new Intl.NumberFormat('ko-KR').format(split.split_amount || 0) + '원' : '-'} + | ++ {log.withdraw > 0 ? new Intl.NumberFormat('ko-KR').format(split.split_amount || 0) + '원' : '-'} + | ++ | + | + | + {split.account_name && ( + {split.account_code} {split.account_name} + )} + | ++ | |||||||||||