diff --git a/app/Http/Controllers/Barobill/EaccountController.php b/app/Http/Controllers/Barobill/EaccountController.php index 26d0dcc4..3cf7c92c 100644 --- a/app/Http/Controllers/Barobill/EaccountController.php +++ b/app/Http/Controllers/Barobill/EaccountController.php @@ -7,6 +7,7 @@ use App\Models\Barobill\BarobillConfig; use App\Models\Barobill\BarobillMember; use App\Models\Barobill\BankTransaction; +use App\Models\Barobill\BankTransactionOverride; use App\Models\Tenants\Tenant; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -309,9 +310,12 @@ public function transactions(Request $request): JsonResponse // DB에서 저장된 계정과목 데이터 조회 $savedData = BankTransaction::getByDateRange($tenantId, $startDate, $endDate, $bankAccountNum ?: null); + // 오버라이드 데이터 (수정된 적요/내용) 조회 + $overrideData = null; + // 전체 계좌 조회: 빈 값이면 모든 계좌의 거래 내역 조회 if (empty($bankAccountNum)) { - return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData); + return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData, $overrideData, $tenantId); } // 단일 계좌 조회 @@ -358,8 +362,8 @@ public function transactions(Request $request): JsonResponse ]); } - // 데이터 파싱 (저장된 계정과목 병합) - $logs = $this->parseTransactionLogs($resultData, '', $savedData); + // 데이터 파싱 (저장된 계정과목 + 오버라이드 병합) + $logs = $this->parseTransactionLogs($resultData, '', $savedData, $tenantId); return response()->json([ 'success' => true, @@ -386,7 +390,7 @@ public function transactions(Request $request): JsonResponse /** * 전체 계좌의 거래 내역 조회 */ - private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null): JsonResponse + private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null, $overrideData = null, int $tenantId = 1): JsonResponse { // 먼저 계좌 목록 조회 $accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]); @@ -432,7 +436,7 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s $errorCode = $this->checkErrorCode($accData); if (!$errorCode || in_array($errorCode, [-25005, -25001])) { - $parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? '', $savedData); + $parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? '', $savedData, $tenantId); foreach ($parsed['logs'] as $log) { $log['bankName'] = $acc->BankName ?? $this->getBankName($acc->BankCode ?? ''); $allLogs[] = $log; @@ -476,11 +480,12 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s } /** - * 거래 내역 파싱 (저장된 계정과목 병합) + * 거래 내역 파싱 (저장된 계정과목 + 오버라이드 병합) */ - private function parseTransactionLogs($resultData, string $defaultBankName = '', $savedData = null): array + private function parseTransactionLogs($resultData, string $defaultBankName = '', $savedData = null, int $tenantId = 1): array { $logs = []; + $uniqueKeys = []; $totalDeposit = 0; $totalWithdraw = 0; @@ -491,6 +496,22 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', : [$resultData->BankAccountLogList->BankAccountTransLog]; } + // 1단계: 모든 고유 키 수집 + foreach ($rawLogs as $log) { + $bankAccountNum = $log->BankAccountNum ?? ''; + $transDT = $log->TransDT ?? ''; + $deposit = (int) floatval($log->Deposit ?? 0); + $withdraw = (int) floatval($log->Withdraw ?? 0); + $balance = (int) floatval($log->Balance ?? 0); + + $uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance]); + $uniqueKeys[] = $uniqueKey; + } + + // 2단계: 오버라이드 데이터 일괄 조회 + $overrides = BankTransactionOverride::getByUniqueKeys($tenantId, $uniqueKeys); + + // 3단계: 각 로그 처리 foreach ($rawLogs as $log) { $deposit = floatval($log->Deposit ?? 0); $withdraw = floatval($log->Withdraw ?? 0); @@ -525,6 +546,15 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', // 고유 키 생성하여 저장된 데이터와 매칭 (숫자는 정수로 변환하여 형식 통일) $uniqueKey = implode('|', [$bankAccountNum, $transDT, (int) $deposit, (int) $withdraw, (int) $balance]); $savedItem = $savedData?->get($uniqueKey); + $override = $overrides->get($uniqueKey); + + // 원본 적요/내용 + $originalSummary = $fullSummary; + $originalCast = $savedItem?->cast ?? ''; + + // 오버라이드 적용 (수정된 값이 있으면 사용) + $displaySummary = $override?->modified_summary ?? $originalSummary; + $displayCast = $override?->modified_cast ?? $originalCast; $logItem = [ 'transDate' => $transDate, @@ -538,15 +568,18 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '', 'withdrawFormatted' => number_format($withdraw), 'balance' => $balance, 'balanceFormatted' => number_format($balance), - 'summary' => $fullSummary, - // 저장된 상대계좌예금주명 우선 사용 (직접 입력 가능) - 'cast' => $savedItem?->cast ?? '', + 'summary' => $displaySummary, + 'originalSummary' => $originalSummary, + 'cast' => $displayCast, + 'originalCast' => $originalCast, 'memo' => $log->Memo ?? '', 'transOffice' => $log->TransOffice ?? '', // 저장된 계정과목 정보 병합 'accountCode' => $savedItem?->account_code ?? '', 'accountName' => $savedItem?->account_name ?? '', 'isSaved' => $savedItem !== null, + 'isOverridden' => $override !== null, + 'uniqueKey' => $uniqueKey, ]; $logs[] = $logItem; @@ -965,6 +998,53 @@ public function exportExcel(Request $request): StreamedResponse|JsonResponse } } + /** + * 거래내역 적요/내용 오버라이드 저장 + */ + public function saveOverride(Request $request): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + + $validated = $request->validate([ + 'uniqueKey' => 'required|string|max:100', + 'modifiedSummary' => 'nullable|string|max:200', + 'modifiedCast' => 'nullable|string|max:200', + ]); + + $result = BankTransactionOverride::saveOverride( + $tenantId, + $validated['uniqueKey'], + $validated['modifiedSummary'] ?? null, + $validated['modifiedCast'] ?? null + ); + + if ($result === null) { + return response()->json([ + 'success' => true, + 'message' => '오버라이드가 삭제되었습니다.', + 'deleted' => true + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '오버라이드가 저장되었습니다.', + 'data' => [ + 'id' => $result->id, + 'modifiedSummary' => $result->modified_summary, + 'modifiedCast' => $result->modified_cast, + ] + ]); + } catch (\Throwable $e) { + Log::error('오버라이드 저장 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '저장 오류: ' . $e->getMessage() + ], 500); + } + } + /** * SOAP 호출 */ diff --git a/app/Models/Barobill/BankTransactionOverride.php b/app/Models/Barobill/BankTransactionOverride.php new file mode 100644 index 00000000..f370f4d3 --- /dev/null +++ b/app/Models/Barobill/BankTransactionOverride.php @@ -0,0 +1,80 @@ +where('tenant_id', $tenantId); + } + + /** + * 고유키로 조회 + */ + public function scopeByUniqueKey($query, string $uniqueKey) + { + return $query->where('unique_key', $uniqueKey); + } + + /** + * 여러 고유키에 대한 오버라이드 조회 + * @return Collection key가 unique_key인 컬렉션 + */ + public static function getByUniqueKeys(int $tenantId, array $uniqueKeys): Collection + { + if (empty($uniqueKeys)) { + return collect(); + } + + return static::forTenant($tenantId) + ->whereIn('unique_key', $uniqueKeys) + ->get() + ->keyBy('unique_key'); + } + + /** + * 오버라이드 저장 또는 업데이트 + */ + public static function saveOverride( + int $tenantId, + string $uniqueKey, + ?string $modifiedSummary, + ?string $modifiedCast + ): ?self { + // 둘 다 null이거나 빈 문자열이면 기존 레코드 삭제 + if (empty($modifiedSummary) && empty($modifiedCast)) { + static::forTenant($tenantId)->byUniqueKey($uniqueKey)->delete(); + return null; + } + + return static::updateOrCreate( + [ + 'tenant_id' => $tenantId, + 'unique_key' => $uniqueKey, + ], + [ + 'modified_summary' => $modifiedSummary ?: null, + 'modified_cast' => $modifiedCast ?: null, + ] + ); + } +} diff --git a/resources/views/barobill/eaccount/index.blade.php b/resources/views/barobill/eaccount/index.blade.php index 9ee294bc..6567c3cb 100644 --- a/resources/views/barobill/eaccount/index.blade.php +++ b/resources/views/barobill/eaccount/index.blade.php @@ -76,6 +76,7 @@ accountCodesDestroy: (id) => `/barobill/eaccount/account-codes/${id}`, save: '{{ route("barobill.eaccount.save") }}', export: '{{ route("barobill.eaccount.export") }}', + saveOverride: '{{ route("barobill.eaccount.save-override") }}', }; const CSRF_TOKEN = '{{ csrf_token() }}'; @@ -596,6 +597,197 @@ className="px-6 py-2 bg-stone-600 text-white rounded-lg hover:bg-stone-700 font- ); }; + // 적요/내용 수정 모달 컴포넌트 + const TransactionEditModal = ({ isOpen, onClose, log, onSave }) => { + const [modifiedSummary, setModifiedSummary] = useState(''); + const [modifiedCast, setModifiedCast] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (isOpen && log) { + // 현재 표시되는 값으로 초기화 (수정된 값이 있으면 그 값, 없으면 원본) + setModifiedSummary(log.summary || ''); + setModifiedCast(log.cast || ''); + } + }, [isOpen, log]); + + const handleSave = async () => { + if (!log?.uniqueKey) { + notify('고유 키가 없습니다.', 'error'); + return; + } + + setSaving(true); + try { + const res = await fetch(API.saveOverride, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': CSRF_TOKEN, + }, + body: JSON.stringify({ + uniqueKey: log.uniqueKey, + modifiedSummary: modifiedSummary !== log.originalSummary ? modifiedSummary : null, + modifiedCast: modifiedCast !== log.originalCast ? modifiedCast : null, + }), + }); + + const data = await res.json(); + if (data.success) { + notify(data.message, 'success'); + onSave(modifiedSummary, modifiedCast); + onClose(); + } else { + notify(data.error || '저장 실패', 'error'); + } + } catch (err) { + notify('저장 오류: ' + err.message, 'error'); + } finally { + setSaving(false); + } + }; + + const handleReset = async () => { + if (!confirm('원본 값으로 되돌리시겠습니까?')) return; + + setSaving(true); + try { + const res = await fetch(API.saveOverride, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': CSRF_TOKEN, + }, + body: JSON.stringify({ + uniqueKey: log.uniqueKey, + modifiedSummary: null, + modifiedCast: null, + }), + }); + + const data = await res.json(); + if (data.success) { + notify('원본으로 복원되었습니다.', 'success'); + onSave(log.originalSummary, log.originalCast); + onClose(); + } else { + notify(data.error || '복원 실패', 'error'); + } + } catch (err) { + notify('복원 오류: ' + err.message, 'error'); + } finally { + setSaving(false); + } + }; + + if (!isOpen || !log) return null; + + return ( +
+
+ {/* Header */} +
+

적요/내용 수정

+ +
+ + {/* 거래 정보 */} +
+
+
+ 거래일시: + {log.transDateTime} +
+
+ 계좌: + {log.bankName} +
+
+ 입금: + {log.deposit > 0 ? log.depositFormatted + '원' : '-'} +
+
+ 출금: + {log.withdraw > 0 ? log.withdrawFormatted + '원' : '-'} +
+
+
+ + {/* 수정 폼 */} +
+
+ + setModifiedSummary(e.target.value)} + className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none" + placeholder="적요 입력" + /> + {log.originalSummary && modifiedSummary !== log.originalSummary && ( +

원본: {log.originalSummary}

+ )} +
+ +
+ + setModifiedCast(e.target.value)} + className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none" + placeholder="내용 입력" + /> + {log.originalCast && modifiedCast !== log.originalCast && ( +

원본: {log.originalCast}

+ )} +
+
+ + {/* Footer */} +
+
+ {log.isOverridden && ( + + )} +
+
+ + +
+
+
+
+ ); + }; + // TransactionTable Component const TransactionTable = ({ logs, @@ -615,7 +807,8 @@ className="px-6 py-2 bg-stone-600 text-white rounded-lg hover:bg-stone-700 font- onExport, onOpenSettings, saving, - hasChanges + hasChanges, + onEditTransaction }) => { const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원'; @@ -750,7 +943,7 @@ className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded ) : ( logs.map((log, index) => ( - +
{log.transDateTime || '-'}
@@ -760,8 +953,19 @@ className="flex items-center gap-2 px-4 py-2 bg-stone-100 text-stone-700 rounded {log.bankAccountNum ? '****' + log.bankAccountNum.slice(-4) : '-'} - -
{log.summary || '-'}
+ onEditTransaction(index)} + > +
+
{log.summary || '-'}
+ {log.isOverridden && ( + 수정 + )} + + + +
{log.memo &&
{log.memo}
} @@ -818,6 +1022,8 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2 const [accountCodes, setAccountCodes] = useState([]); const [hasChanges, setHasChanges] = useState(false); const [showSettingsModal, setShowSettingsModal] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [editingLogIndex, setEditingLogIndex] = useState(null); // 날짜 필터 상태 (기본: 현재 월) const currentMonth = getMonthDates(0); @@ -915,6 +1121,28 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2 setHasChanges(true); }, []); + // 거래 수정 모달 열기 + const handleEditTransaction = useCallback((index) => { + setEditingLogIndex(index); + setEditModalOpen(true); + }, []); + + // 오버라이드 저장 후 로그 업데이트 + const handleSaveOverride = useCallback((newSummary, newCast) => { + if (editingLogIndex !== null) { + setLogs(prevLogs => { + const newLogs = [...prevLogs]; + newLogs[editingLogIndex] = { + ...newLogs[editingLogIndex], + summary: newSummary, + cast: newCast, + isOverridden: true + }; + return newLogs; + }); + } + }, [editingLogIndex]); + // 저장 핸들러 const handleSave = async () => { if (logs.length === 0) return; @@ -1072,9 +1300,21 @@ className="w-full px-2 py-1 text-sm border border-stone-200 rounded focus:ring-2 onOpenSettings={() => setShowSettingsModal(true)} saving={saving} hasChanges={hasChanges} + onEditTransaction={handleEditTransaction} /> )} + {/* Transaction Edit Modal */} + { + setEditModalOpen(false); + setEditingLogIndex(null); + }} + log={editingLogIndex !== null ? logs[editingLogIndex] : null} + onSave={handleSaveOverride} + /> + {/* Account Code Settings Modal */} name('account-codes.destroy'); Route::post('/save', [\App\Http\Controllers\Barobill\EaccountController::class, 'save'])->name('save'); Route::get('/export', [\App\Http\Controllers\Barobill\EaccountController::class, 'exportExcel'])->name('export'); + Route::post('/save-override', [\App\Http\Controllers\Barobill\EaccountController::class, 'saveOverride'])->name('save-override'); }); // 카드 사용내역 (React 페이지)