input('date', date('Y-m-d')); $transactions = DailyFundTransaction::forTenant($tenantId) ->where('transaction_date', $date) ->orderBy('time') ->get() ->map(function ($tx) { return [ 'id' => $tx->id, 'type' => $tx->type, 'time' => $tx->time, 'accountId' => $tx->account_id, 'accountName' => $tx->account_name, 'description' => $tx->description, 'amount' => $tx->amount, 'category' => $tx->category, 'note' => $tx->note, ]; }); $income = $transactions->where('type', 'income')->values(); $expense = $transactions->where('type', 'expense')->values(); // 메모 $memoRecord = DailyFundMemo::forTenant($tenantId) ->where('memo_date', $date) ->first(); // 전일 잔액 계산: 해당 날짜 이전 모든 거래의 합 $previousBalance = DailyFundTransaction::forTenant($tenantId) ->where('transaction_date', '<', $date) ->selectRaw(" COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as balance ") ->value('balance') ?? 0; return response()->json([ 'success' => true, 'data' => [ 'income' => $income, 'expense' => $expense, 'previousBalance' => (int) $previousBalance, 'memo' => $memoRecord?->memo ?? '', 'author' => $memoRecord?->author ?? '', 'updatedAt' => $memoRecord?->updated_at?->format('Y-m-d H:i') ?? '', ], ]); } public function store(Request $request): JsonResponse { $request->validate([ 'transactionDate' => 'required|date', 'type' => 'required|in:income,expense', 'description' => 'required|string|max:200', 'amount' => 'required|integer|min:1', ]); $tenantId = session('selected_tenant_id', 1); $tx = DailyFundTransaction::create([ 'tenant_id' => $tenantId, 'transaction_date' => $request->input('transactionDate'), 'type' => $request->input('type'), 'time' => $request->input('time'), 'account_id' => $request->input('accountId'), 'account_name' => $request->input('accountName'), 'description' => $request->input('description'), 'amount' => $request->input('amount'), 'category' => $request->input('category'), 'note' => $request->input('note'), ]); return response()->json([ 'success' => true, 'message' => '거래가 등록되었습니다.', 'data' => ['id' => $tx->id], ]); } public function update(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $tx = DailyFundTransaction::forTenant($tenantId)->findOrFail($id); $request->validate([ 'description' => 'required|string|max:200', 'amount' => 'required|integer|min:1', ]); $tx->update([ 'type' => $request->input('type', $tx->type), 'time' => $request->input('time'), 'account_id' => $request->input('accountId'), 'account_name' => $request->input('accountName'), 'description' => $request->input('description'), 'amount' => $request->input('amount'), 'category' => $request->input('category'), 'note' => $request->input('note'), ]); return response()->json([ 'success' => true, 'message' => '거래가 수정되었습니다.', ]); } public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $tx = DailyFundTransaction::forTenant($tenantId)->findOrFail($id); $tx->delete(); return response()->json([ 'success' => true, 'message' => '거래가 삭제되었습니다.', ]); } public function saveMemo(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $request->validate([ 'date' => 'required|date', ]); $memo = DailyFundMemo::updateOrCreate( ['tenant_id' => $tenantId, 'memo_date' => $request->input('date')], [ 'memo' => $request->input('memo', ''), 'author' => $request->input('author', ''), ] ); return response()->json([ 'success' => true, 'message' => '메모가 저장되었습니다.', 'data' => [ 'updatedAt' => $memo->updated_at->format('Y-m-d H:i'), ], ]); } /** * 기간별 자금일보 (바로빌 계좌 거래내역 기반) */ public function periodReport(Request $request): JsonResponse { $tenantId = session('selected_tenant_id', 1); $startDate = $request->input('start_date', now()->subMonth()->format('Ymd')); $endDate = $request->input('end_date', now()->format('Ymd')); // YYYYMMDD 형식으로 변환 $startDateYmd = str_replace('-', '', $startDate); $endDateYmd = str_replace('-', '', $endDate); // 기간 내 거래내역 조회 (중복 제거: 동일 trans_dt + 금액 조합이면 최신 id(정확한 잔액)만 사용) $transactions = BarobillBankTransaction::where('tenant_id', $tenantId) ->whereBetween('trans_date', [$startDateYmd, $endDateYmd]) ->orderBy('id', 'desc') // 최신 ID 우선 (올바른 잔액) ->get() ->unique(fn($tx) => $tx->bank_account_num . '|' . $tx->trans_dt . '|' . (int)$tx->deposit . '|' . (int)$tx->withdraw) ->sortByDesc('trans_date') ->sortByDesc(fn($tx) => $tx->trans_date . $tx->trans_time) ->values(); // 오버라이드 데이터 병합 (수정된 적요/내용) if ($transactions->isNotEmpty()) { $uniqueKeys = $transactions->map(fn($t) => $t->unique_key)->toArray(); $overrides = BankTransactionOverride::getByUniqueKeys($tenantId, $uniqueKeys); $transactions = $transactions->map(function ($tx) use ($overrides) { $override = $overrides->get($tx->unique_key); if ($override) { if ($override->modified_summary) { $tx->summary = $override->modified_summary; } if ($override->modified_cast) { $tx->cast = $override->modified_cast; } } return $tx; }); } // 일별로 그룹핑 $dailyData = []; $accountBalances = []; // 계좌별 최신 잔액 추적 foreach ($transactions as $tx) { $date = $tx->trans_date; $accountNum = $tx->bank_account_num; if (!isset($dailyData[$date])) { $dailyData[$date] = [ 'date' => $date, 'dateFormatted' => $this->formatDateKorean($date), 'accounts' => [], 'deposits' => [], 'withdrawals' => [], 'totalDeposit' => 0, 'totalWithdraw' => 0, ]; } // 계좌별 데이터 집계 if (!isset($dailyData[$date]['accounts'][$accountNum])) { $dailyData[$date]['accounts'][$accountNum] = [ 'bankName' => $tx->bank_name, 'accountNum' => $accountNum, 'deposit' => 0, 'withdraw' => 0, 'balance' => $tx->balance, ]; } // 입출금 내역 추가 if ($tx->deposit > 0) { $dailyData[$date]['deposits'][] = [ 'time' => $tx->trans_time, 'bankName' => $tx->bank_name, 'summary' => $tx->summary, 'cast' => $tx->cast, 'amount' => $tx->deposit, 'balance' => $tx->balance, ]; $dailyData[$date]['accounts'][$accountNum]['deposit'] += $tx->deposit; $dailyData[$date]['totalDeposit'] += $tx->deposit; } if ($tx->withdraw > 0) { $dailyData[$date]['withdrawals'][] = [ 'time' => $tx->trans_time, 'bankName' => $tx->bank_name, 'summary' => $tx->summary, 'cast' => $tx->cast, 'amount' => $tx->withdraw, 'balance' => $tx->balance, ]; $dailyData[$date]['accounts'][$accountNum]['withdraw'] += $tx->withdraw; $dailyData[$date]['totalWithdraw'] += $tx->withdraw; } // 잔액은 첫 번째 만나는 거래(DESC 순이므로 가장 늦은 시간)의 값을 유지 // 계좌가 처음 생성될 때 이미 설정되므로 여기서 업데이트하지 않음 } // accounts를 배열로 변환 foreach ($dailyData as $date => &$data) { $data['accounts'] = array_values($data['accounts']); } // 날짜 내림차순 정렬 (최신 일자가 위) krsort($dailyData); return response()->json([ 'success' => true, 'data' => [ 'startDate' => $startDate, 'endDate' => $endDate, 'dailyReports' => array_values($dailyData), ], ]); } private function formatDateKorean(string $dateYmd): string { $year = substr($dateYmd, 0, 4); $month = (int) substr($dateYmd, 4, 2); $day = (int) substr($dateYmd, 6, 2); $date = \Carbon\Carbon::createFromFormat('Ymd', $dateYmd); $dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][$date->dayOfWeek]; return "{$year}년 {$month}월 {$day}일 {$dayOfWeek}요일"; } }