tenantId(); $recentYear = $params['recent_year'] ?? false; $year = $params['year'] ?? date('Y'); $search = $params['search'] ?? null; // 월 기간 생성 (동적 월 지원) $periods = $this->generateMonthPeriods($recentYear, $year); $monthLabels = array_map(fn ($p) => $p['label'], $periods); // 이월잔액 기준일 (첫번째 월의 시작일 전날) $carryForwardDate = Carbon::parse($periods[0]['start'])->subDay()->format('Y-m-d'); // 거래처 목록 조회 $clientsQuery = Client::where('tenant_id', $tenantId) ->where('is_active', true); if ($search) { $clientsQuery->where('name', 'like', "%{$search}%"); } $clients = $clientsQuery->orderBy('name')->get(); $result = []; foreach ($clients as $client) { // 이월잔액 계산 (기준일 이전까지의 누적 미수금) $carryForwardBalance = $this->getCarryForwardBalance($tenantId, $client->id, $carryForwardDate); // 월별 데이터 수집 (년-월 키 기반) $salesByPeriod = $this->getSalesByPeriods($tenantId, $client->id, $periods); $depositsByPeriod = $this->getDepositsByPeriods($tenantId, $client->id, $periods); $billsByPeriod = $this->getBillsByPeriods($tenantId, $client->id, $periods); // 누적 미수금 계산 $receivablesByPeriod = $this->calculateCumulativeReceivables( $carryForwardBalance, $salesByPeriod, $depositsByPeriod, $billsByPeriod, count($periods) ); // 카테고리별 데이터 생성 (배열 형태) $categories = [ [ 'category' => 'sales', 'amounts' => $this->formatPeriodAmounts($salesByPeriod, count($periods)), ], [ 'category' => 'deposit', 'amounts' => $this->formatPeriodAmounts($depositsByPeriod, count($periods)), ], [ 'category' => 'bill', 'amounts' => $this->formatPeriodAmounts($billsByPeriod, count($periods)), ], [ 'category' => 'receivable', 'amounts' => $this->formatReceivableAmounts($receivablesByPeriod), ], ]; // 연체 여부: 최종 미수금이 양수인 경우 $finalReceivable = end($receivablesByPeriod); $isOverdue = $client->is_overdue ?? ($finalReceivable > 0); $result[] = [ 'id' => (string) $client->id, 'vendor_id' => $client->id, 'vendor_name' => $client->name, 'is_overdue' => $isOverdue, 'memo' => $client->memo ?? '', 'carry_forward_balance' => $carryForwardBalance, 'month_labels' => $monthLabels, 'categories' => $categories, ]; } // 미수금이 있는 거래처만 필터링 (선택적) if (! empty($params['has_receivable'])) { $result = array_filter($result, function ($item) { $receivableCat = collect($item['categories'])->firstWhere('category', 'receivable'); return $receivableCat && $receivableCat['amounts']['total'] > 0; }); $result = array_values($result); } // 공통 월 레이블 추가 (프론트엔드에서 헤더로 사용) return [ 'month_labels' => $monthLabels, 'items' => $result, ]; } /** * 요약 통계 조회 */ public function summary(array $params): array { $tenantId = $this->tenantId(); $recentYear = $params['recent_year'] ?? false; $year = $params['year'] ?? date('Y'); // 월 기간 생성 $periods = $this->generateMonthPeriods($recentYear, $year); $startDate = $periods[0]['start']; $endDate = end($periods)['end']; // 이월잔액 기준일 $carryForwardDate = Carbon::parse($startDate)->subDay()->format('Y-m-d'); // 전체 이월잔액 (모든 거래처) $totalCarryForward = $this->getTotalCarryForwardBalance($tenantId, $carryForwardDate); // 기간 내 총 매출 $totalSales = Sale::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->whereBetween('sale_date', [$startDate, $endDate]) ->sum('total_amount'); // 기간 내 총 입금 $totalDeposits = Deposit::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->whereBetween('deposit_date', [$startDate, $endDate]) ->sum('amount'); // 기간 내 총 어음 $totalBills = Bill::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->where('bill_type', 'received') ->whereBetween('issue_date', [$startDate, $endDate]) ->sum('amount'); // 총 미수금 (이월잔액 + 매출 - 입금 - 어음) $totalReceivables = $totalCarryForward + $totalSales - $totalDeposits - $totalBills; // 거래처 수 $vendorCount = Client::where('tenant_id', $tenantId) ->where('is_active', true) ->count(); // 연체 거래처 수 (미수금이 양수인 거래처) $overdueVendorCount = Client::where('tenant_id', $tenantId) ->where('is_active', true) ->where('is_overdue', true) ->count(); return [ 'total_carry_forward' => (float) $totalCarryForward, 'total_sales' => (float) $totalSales, 'total_deposits' => (float) $totalDeposits, 'total_bills' => (float) $totalBills, 'total_receivables' => (float) $totalReceivables, 'vendor_count' => $vendorCount, 'overdue_vendor_count' => $overdueVendorCount, ]; } /** * 월 기간 배열 생성 * * @return array [['start' => 'Y-m-d', 'end' => 'Y-m-d', 'label' => 'YY.MM', 'year' => Y, 'month' => M], ...] */ private function generateMonthPeriods(bool $recentYear, string $year): array { $periods = []; if ($recentYear) { // 최근 1년: 현재 월 기준으로 12개월 전부터 $current = Carbon::now()->startOfMonth(); $start = $current->copy()->subMonths(11); for ($i = 0; $i < 12; $i++) { $month = $start->copy()->addMonths($i); $periods[] = [ 'start' => $month->format('Y-m-01'), 'end' => $month->endOfMonth()->format('Y-m-d'), 'label' => $month->format('y.m'), 'year' => (int) $month->format('Y'), 'month' => (int) $month->format('n'), ]; } } else { // 특정 연도: 1월~12월 for ($month = 1; $month <= 12; $month++) { $date = Carbon::createFromDate($year, $month, 1); $periods[] = [ 'start' => $date->format('Y-m-01'), 'end' => $date->endOfMonth()->format('Y-m-d'), 'label' => "{$month}월", 'year' => (int) $year, 'month' => $month, ]; } } return $periods; } /** * 이월잔액 계산 (기준일 이전까지의 누적 미수금) */ private function getCarryForwardBalance(int $tenantId, int $clientId, string $beforeDate): float { // 기준일 이전 총 매출 $totalSales = Sale::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->where('sale_date', '<=', $beforeDate) ->sum('total_amount'); // 기준일 이전 총 입금 $totalDeposits = Deposit::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->where('deposit_date', '<=', $beforeDate) ->sum('amount'); // 기준일 이전 총 어음 $totalBills = Bill::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->where('bill_type', 'received') ->where('issue_date', '<=', $beforeDate) ->sum('amount'); return (float) ($totalSales - $totalDeposits - $totalBills); } /** * 전체 거래처 이월잔액 합계 */ private function getTotalCarryForwardBalance(int $tenantId, string $beforeDate): float { $totalSales = Sale::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->where('sale_date', '<=', $beforeDate) ->sum('total_amount'); $totalDeposits = Deposit::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->where('deposit_date', '<=', $beforeDate) ->sum('amount'); $totalBills = Bill::where('tenant_id', $tenantId) ->whereNotNull('client_id') ->where('bill_type', 'received') ->where('issue_date', '<=', $beforeDate) ->sum('amount'); return (float) ($totalSales - $totalDeposits - $totalBills); } /** * 기간별 매출 조회 */ private function getSalesByPeriods(int $tenantId, int $clientId, array $periods): array { $result = []; foreach ($periods as $index => $period) { $total = Sale::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->whereBetween('sale_date', [$period['start'], $period['end']]) ->sum('total_amount'); $result[$index] = (float) $total; } return $result; } /** * 기간별 입금 조회 */ private function getDepositsByPeriods(int $tenantId, int $clientId, array $periods): array { $result = []; foreach ($periods as $index => $period) { $total = Deposit::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->whereBetween('deposit_date', [$period['start'], $period['end']]) ->sum('amount'); $result[$index] = (float) $total; } return $result; } /** * 기간별 어음 조회 */ private function getBillsByPeriods(int $tenantId, int $clientId, array $periods): array { $result = []; foreach ($periods as $index => $period) { $total = Bill::where('tenant_id', $tenantId) ->where('client_id', $clientId) ->where('bill_type', 'received') ->whereBetween('issue_date', [$period['start'], $period['end']]) ->sum('amount'); $result[$index] = (float) $total; } return $result; } /** * 누적 미수금 계산 * 1월: 이월잔액 + 1월 매출 - 1월 입금 - 1월 어음 * 2월: 1월 미수금 + 2월 매출 - 2월 입금 - 2월 어음 * ... */ private function calculateCumulativeReceivables( float $carryForward, array $sales, array $deposits, array $bills, int $periodCount ): array { $result = []; $cumulative = $carryForward; for ($i = 0; $i < $periodCount; $i++) { $monthSales = $sales[$i] ?? 0; $monthDeposits = $deposits[$i] ?? 0; $monthBills = $bills[$i] ?? 0; $cumulative = $cumulative + $monthSales - $monthDeposits - $monthBills; $result[$i] = $cumulative; } return $result; } /** * 기간별 금액을 프론트엔드 형식으로 변환 (매출, 입금, 어음용) */ private function formatPeriodAmounts(array $periodData, int $periodCount): array { $amounts = []; $total = 0; for ($i = 0; $i < $periodCount; $i++) { $amount = $periodData[$i] ?? 0; $amounts[] = $amount; $total += $amount; } return [ 'values' => $amounts, 'total' => $total, ]; } /** * 미수금 금액을 프론트엔드 형식으로 변환 (누적이므로 total = 마지막 값) */ private function formatReceivableAmounts(array $receivables): array { $values = array_values($receivables); $total = ! empty($values) ? end($values) : 0; return [ 'values' => $values, 'total' => $total, ]; } /** * 연체 상태 일괄 업데이트 */ public function updateOverdueStatus(array $updates): int { $tenantId = $this->tenantId(); $updatedCount = 0; foreach ($updates as $update) { $clientId = (int) $update['id']; $isOverdue = (bool) $update['is_overdue']; $affected = Client::where('tenant_id', $tenantId) ->where('id', $clientId) ->update(['is_overdue' => $isOverdue]); $updatedCount += $affected; } return $updatedCount; } /** * 거래처 메모 업데이트 */ public function updateMemo(int $clientId, string $memo): bool { $tenantId = $this->tenantId(); $affected = Client::where('tenant_id', $tenantId) ->where('id', $clientId) ->update(['memo' => $memo]); return $affected > 0; } /** * 거래처 메모 일괄 업데이트 */ public function updateMemos(array $memos): int { $tenantId = $this->tenantId(); $updatedCount = 0; foreach ($memos as $item) { $clientId = (int) $item['id']; $memo = $item['memo'] ?? ''; $affected = Client::where('tenant_id', $tenantId) ->where('id', $clientId) ->update(['memo' => $memo]); $updatedCount += $affected; } return $updatedCount; } }