tenantId(); $page = (int) ($params['page'] ?? 1); $size = (int) ($params['size'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $onlyActive = $params['only_active'] ?? null; $query = Client::query()->where('tenant_id', $tenantId); if ($q !== '') { $query->where(function ($qq) use ($q) { $qq->where('name', 'like', "%{$q}%") ->orWhere('client_code', 'like', "%{$q}%") ->orWhere('contact_person', 'like', "%{$q}%"); }); } if ($onlyActive !== null) { $query->where('is_active', (bool) $onlyActive); } $query->orderBy('client_code')->orderBy('id'); $paginator = $query->paginate($size, ['*'], 'page', $page); // 미수금 계산: 매출 합계 - 입금 합계 $clientIds = $paginator->pluck('id')->toArray(); if (! empty($clientIds)) { // 거래처별 매출 합계 $salesByClient = Sale::where('tenant_id', $tenantId) ->whereIn('client_id', $clientIds) ->whereNull('deleted_at') ->groupBy('client_id') ->select('client_id', DB::raw('SUM(total_amount) as total_sales')) ->pluck('total_sales', 'client_id'); // 거래처별 입금 합계 $depositsByClient = Deposit::where('tenant_id', $tenantId) ->whereIn('client_id', $clientIds) ->whereNull('deleted_at') ->groupBy('client_id') ->select('client_id', DB::raw('SUM(amount) as total_deposits')) ->pluck('total_deposits', 'client_id'); // 거래처별 활성 악성채권 합계 (추심중, 법적조치) $badDebtsByClient = BadDebt::where('tenant_id', $tenantId) ->whereIn('client_id', $clientIds) ->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION]) ->where('is_active', true) ->whereNull('deleted_at') ->groupBy('client_id') ->select('client_id', DB::raw('SUM(debt_amount) as total_bad_debt')) ->pluck('total_bad_debt', 'client_id'); // 각 거래처에 미수금/악성채권 정보 추가 $paginator->getCollection()->transform(function ($client) use ($salesByClient, $depositsByClient, $badDebtsByClient) { $totalSales = $salesByClient[$client->id] ?? 0; $totalDeposits = $depositsByClient[$client->id] ?? 0; $client->outstanding_amount = max(0, $totalSales - $totalDeposits); $client->bad_debt_total = $badDebtsByClient[$client->id] ?? 0; $client->has_bad_debt = ($badDebtsByClient[$client->id] ?? 0) > 0; return $client; }); } return $paginator; } /** 단건 */ public function show(int $id) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } // 미수금 계산 $totalSales = Sale::where('tenant_id', $tenantId) ->where('client_id', $id) ->whereNull('deleted_at') ->sum('total_amount'); $totalDeposits = Deposit::where('tenant_id', $tenantId) ->where('client_id', $id) ->whereNull('deleted_at') ->sum('amount'); $client->outstanding_amount = max(0, $totalSales - $totalDeposits); // 악성채권 정보 $badDebtTotal = BadDebt::where('tenant_id', $tenantId) ->where('client_id', $id) ->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION]) ->where('is_active', true) ->whereNull('deleted_at') ->sum('debt_amount'); $client->bad_debt_total = $badDebtTotal; $client->has_bad_debt = $badDebtTotal > 0; return $client; } /** 생성 */ public function store(array $data) { $tenantId = $this->tenantId(); // client_code 자동 생성 (프론트에서 보내도 무시) $data['client_code'] = $this->generateClientCode($tenantId); $data['tenant_id'] = $tenantId; $data['is_active'] = $data['is_active'] ?? true; return Client::create($data); } /** * 유니크한 client_code 자동 생성 * 형식: 8자리 영숫자 (예: A3B7X9K2) */ private function generateClientCode(int $tenantId): string { $maxAttempts = 10; $attempt = 0; do { // 8자리 영숫자 코드 생성 (대문자 + 숫자) $code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8)); // 중복 검사 $exists = Client::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('client_code', $code) ->exists(); $attempt++; } while ($exists && $attempt < $maxAttempts); if ($exists) { // 극히 드문 경우: timestamp 추가하여 유니크성 보장 $code = strtoupper(substr(bin2hex(random_bytes(2)), 0, 4).dechex(time() % 0xFFFF)); } return $code; } /** 수정 */ public function update(int $id, array $data) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } // client_code 변경 불가 (프론트에서 보내도 무시) unset($data['client_code']); $client->update($data); return $client->refresh(); } /** 삭제 */ public function destroy(int $id) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } // 주문 존재 검사 if ($client->orders()->exists()) { throw new BadRequestHttpException(__('error.has_orders')); } $client->delete(); return 'success'; } /** 활성/비활성 토글 */ public function toggle(int $id) { $tenantId = $this->tenantId(); $client = Client::where('tenant_id', $tenantId)->find($id); if (! $client) { throw new NotFoundHttpException(__('error.not_found')); } $client->is_active = ! $client->is_active; $client->save(); return $client->refresh(); } }