From 55d04537fc67213e5381b3485fb2ec0a8044ca2d Mon Sep 17 00:00:00 2001 From: pro Date: Wed, 28 Jan 2026 16:49:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=8B=A0=EC=9A=A9=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=9A=8C=EC=88=98=20=EC=A7=91=EA=B3=84=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreditUsageController: 조회회수 집계 컨트롤러 신규 생성 - credit/usage/index.blade.php: 집계 화면 (월별/연간/기간별) - 과금 정책: 월 5건 무료, 추가건당 2,000원 - 본사(tenant_id=1)는 전체 테넌트 조회 가능 - CreditInquiry 모델에 tenant_id 필드 추가 - 신용평가 조회 시 tenant_id 저장하도록 수정 Co-Authored-By: Claude Opus 4.5 --- .../Controllers/Credit/CreditController.php | 5 +- .../Credit/CreditUsageController.php | 262 ++++++++++++++++++ app/Models/Credit/CreditInquiry.php | 6 +- resources/views/credit/usage/index.blade.php | 254 +++++++++++++++++ routes/web.php | 3 + 5 files changed, 527 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Credit/CreditUsageController.php create mode 100644 resources/views/credit/usage/index.blade.php diff --git a/app/Http/Controllers/Credit/CreditController.php b/app/Http/Controllers/Credit/CreditController.php index e7aeb8cc..eae3efd7 100644 --- a/app/Http/Controllers/Credit/CreditController.php +++ b/app/Http/Controllers/Credit/CreditController.php @@ -95,12 +95,13 @@ public function search(Request $request): JsonResponse $ntsService = new NtsBusinessService(); $ntsResult = $ntsService->getBusinessStatus($companyKey); - // DB에 저장 + // DB에 저장 (tenant_id는 세션에서 가져옴) $inquiry = CreditInquiry::createFromApiResponse( $companyKey, $apiResult, $ntsResult, - auth()->id() + auth()->id(), + session('selected_tenant_id') ); return response()->json([ diff --git a/app/Http/Controllers/Credit/CreditUsageController.php b/app/Http/Controllers/Credit/CreditUsageController.php new file mode 100644 index 00000000..9c4fed19 --- /dev/null +++ b/app/Http/Controllers/Credit/CreditUsageController.php @@ -0,0 +1,262 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('credit.usage.index')); + } + + $user = auth()->user(); + $selectedTenantId = session('selected_tenant_id'); + $isHQ = $selectedTenantId == 1; // 본사(코드브릿지엑스) + + // 기간 필터 (기본값: 현재 월) + $year = $request->input('year', date('Y')); + $month = $request->input('month', date('m')); + $viewType = $request->input('view_type', 'monthly'); // monthly, yearly, custom + + // 기간 설정 + if ($viewType === 'yearly') { + $startDate = "{$year}-01-01 00:00:00"; + $endDate = "{$year}-12-31 23:59:59"; + } elseif ($viewType === 'custom') { + $startDate = $request->input('start_date', date('Y-m-01')) . ' 00:00:00'; + $endDate = $request->input('end_date', date('Y-m-t')) . ' 23:59:59'; + } else { + $startDate = "{$year}-{$month}-01 00:00:00"; + $endDate = date('Y-m-t 23:59:59', strtotime($startDate)); + } + + // 본사는 전체 테넌트 조회, 일반 테넌트는 자기 것만 + if ($isHQ) { + $usageData = $this->getAllTenantsUsage($startDate, $endDate, $viewType, $year); + $tenants = Tenant::whereNull('deleted_at') + ->orderBy('company_name') + ->get(['id', 'company_name', 'code']); + } else { + $usageData = $this->getSingleTenantUsage($selectedTenantId, $startDate, $endDate, $viewType, $year); + $tenants = collect(); + } + + // 선택된 테넌트 필터 + $filterTenantId = $request->input('tenant_id'); + if ($isHQ && $filterTenantId) { + $usageData['details'] = collect($usageData['details'])->filter(function ($item) use ($filterTenantId) { + return $item['tenant_id'] == $filterTenantId; + })->values()->all(); + } + + return view('credit.usage.index', [ + 'isHQ' => $isHQ, + 'usageData' => $usageData, + 'tenants' => $tenants, + 'filters' => [ + 'year' => $year, + 'month' => $month, + 'view_type' => $viewType, + 'start_date' => substr($startDate, 0, 10), + 'end_date' => substr($endDate, 0, 10), + 'tenant_id' => $filterTenantId, + ], + 'policy' => [ + 'free_quota' => self::FREE_MONTHLY_QUOTA, + 'additional_fee' => self::ADDITIONAL_FEE_PER_INQUIRY, + ], + ]); + } + + /** + * 전체 테넌트 사용량 조회 (본사용) + */ + private function getAllTenantsUsage(string $startDate, string $endDate, string $viewType, string $year): array + { + // 테넌트별 조회 건수 + $query = CreditInquiry::select( + 'tenant_id', + DB::raw('COUNT(*) as total_count'), + DB::raw('DATE_FORMAT(inquired_at, "%Y-%m") as month') + ) + ->whereBetween('inquired_at', [$startDate, $endDate]) + ->whereNotNull('tenant_id') + ->groupBy('tenant_id', DB::raw('DATE_FORMAT(inquired_at, "%Y-%m")')); + + $rawData = $query->get(); + + // 테넌트 정보 조회 + $tenantIds = $rawData->pluck('tenant_id')->unique(); + $tenants = Tenant::whereIn('id', $tenantIds)->get()->keyBy('id'); + + // 월별로 그룹화하여 계산 + $monthlyData = []; + foreach ($rawData as $row) { + $tenantId = $row->tenant_id; + $month = $row->month; + + if (!isset($monthlyData[$tenantId])) { + $monthlyData[$tenantId] = []; + } + $monthlyData[$tenantId][$month] = $row->total_count; + } + + // 결과 데이터 생성 + $details = []; + $totalCount = 0; + $totalFee = 0; + + foreach ($monthlyData as $tenantId => $months) { + $tenant = $tenants->get($tenantId); + $tenantTotalCount = 0; + $tenantTotalFee = 0; + + foreach ($months as $month => $count) { + $fee = $this->calculateFee($count); + $tenantTotalCount += $count; + $tenantTotalFee += $fee; + + if ($viewType === 'yearly') { + // 연간 조회 시 월별 상세 표시 + $details[] = [ + 'tenant_id' => $tenantId, + 'tenant_name' => $tenant?->company_name ?? '(삭제됨)', + 'tenant_code' => $tenant?->code ?? '-', + 'month' => $month, + 'count' => $count, + 'free_count' => min($count, self::FREE_MONTHLY_QUOTA), + 'paid_count' => max(0, $count - self::FREE_MONTHLY_QUOTA), + 'fee' => $fee, + ]; + } + } + + if ($viewType !== 'yearly') { + // 월간/기간 조회 시 테넌트별 합계만 + $totalMonthCount = array_sum($months); + $fee = $this->calculateFee($totalMonthCount); + + $details[] = [ + 'tenant_id' => $tenantId, + 'tenant_name' => $tenant?->company_name ?? '(삭제됨)', + 'tenant_code' => $tenant?->code ?? '-', + 'count' => $totalMonthCount, + 'free_count' => min($totalMonthCount, self::FREE_MONTHLY_QUOTA), + 'paid_count' => max(0, $totalMonthCount - self::FREE_MONTHLY_QUOTA), + 'fee' => $fee, + ]; + } + + $totalCount += $tenantTotalCount; + $totalFee += $tenantTotalFee; + } + + // 정렬: 조회 건수 내림차순 + usort($details, fn($a, $b) => $b['count'] - $a['count']); + + return [ + 'total_count' => $totalCount, + 'total_fee' => $totalFee, + 'details' => $details, + ]; + } + + /** + * 단일 테넌트 사용량 조회 + */ + private function getSingleTenantUsage(int $tenantId, string $startDate, string $endDate, string $viewType, string $year): array + { + $tenant = Tenant::find($tenantId); + + // 월별 조회 건수 + $query = CreditInquiry::select( + DB::raw('COUNT(*) as total_count'), + DB::raw('DATE_FORMAT(inquired_at, "%Y-%m") as month') + ) + ->where('tenant_id', $tenantId) + ->whereBetween('inquired_at', [$startDate, $endDate]) + ->groupBy(DB::raw('DATE_FORMAT(inquired_at, "%Y-%m")')) + ->orderBy('month'); + + $rawData = $query->get(); + + $details = []; + $totalCount = 0; + $totalFee = 0; + + foreach ($rawData as $row) { + $count = $row->total_count; + $fee = $this->calculateFee($count); + + $details[] = [ + 'tenant_id' => $tenantId, + 'tenant_name' => $tenant?->company_name ?? '(삭제됨)', + 'month' => $row->month, + 'count' => $count, + 'free_count' => min($count, self::FREE_MONTHLY_QUOTA), + 'paid_count' => max(0, $count - self::FREE_MONTHLY_QUOTA), + 'fee' => $fee, + ]; + + $totalCount += $count; + $totalFee += $fee; + } + + // 연간 조회 시 없는 월도 표시 + if ($viewType === 'yearly') { + $existingMonths = collect($details)->pluck('month')->toArray(); + for ($m = 1; $m <= 12; $m++) { + $monthKey = sprintf('%s-%02d', $year, $m); + if (!in_array($monthKey, $existingMonths)) { + $details[] = [ + 'tenant_id' => $tenantId, + 'tenant_name' => $tenant?->company_name ?? '(삭제됨)', + 'month' => $monthKey, + 'count' => 0, + 'free_count' => 0, + 'paid_count' => 0, + 'fee' => 0, + ]; + } + } + // 월 순서로 정렬 + usort($details, fn($a, $b) => strcmp($a['month'], $b['month'])); + } + + return [ + 'total_count' => $totalCount, + 'total_fee' => $totalFee, + 'details' => $details, + ]; + } + + /** + * 요금 계산 + */ + private function calculateFee(int $count): int + { + $paidCount = max(0, $count - self::FREE_MONTHLY_QUOTA); + return $paidCount * self::ADDITIONAL_FEE_PER_INQUIRY; + } +} diff --git a/app/Models/Credit/CreditInquiry.php b/app/Models/Credit/CreditInquiry.php index 97edc1c6..374d6011 100644 --- a/app/Models/Credit/CreditInquiry.php +++ b/app/Models/Credit/CreditInquiry.php @@ -12,6 +12,7 @@ class CreditInquiry extends Model { protected $fillable = [ + 'tenant_id', 'inquiry_key', 'company_key', 'company_name', @@ -175,12 +176,14 @@ public function getNtsStatusLabelAttribute(): string * @param array $apiResult 쿠콘 API 결과 * @param array|null $ntsResult 국세청 API 결과 * @param int|null $userId 조회자 ID + * @param int|null $tenantId 테넌트 ID */ public static function createFromApiResponse( string $companyKey, array $apiResult, ?array $ntsResult = null, - ?int $userId = null + ?int $userId = null, + ?int $tenantId = null ): self { // 요약 정보에서 건수 추출 $summaryData = $apiResult['summary']['data'] ?? []; @@ -238,6 +241,7 @@ public static function createFromApiResponse( }; return self::create([ + 'tenant_id' => $tenantId, 'company_key' => $companyKey, 'user_id' => $userId, 'inquired_at' => now(), diff --git a/resources/views/credit/usage/index.blade.php b/resources/views/credit/usage/index.blade.php new file mode 100644 index 00000000..10e6334a --- /dev/null +++ b/resources/views/credit/usage/index.blade.php @@ -0,0 +1,254 @@ +@extends('layouts.app') + +@section('title', '조회회수 집계') + +@section('content') +
+ +
+

신용평가 조회회수 집계

+

+ @if($isHQ) + 전체 테넌트의 신용평가 조회 현황을 확인합니다 + @else + 월별/기간별 신용평가 조회 현황과 요금을 확인합니다 + @endif +

+
+ + +
+

+ + + + 과금 정책 +

+
    +
  • 월 기본 제공: {{ $policy['free_quota'] }}건 무료
  • +
  • 추가 조회: 건당 {{ number_format($policy['additional_fee']) }}원
  • +
+
+ + +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + ~ + +
+
+ + @if($isHQ && $tenants->isNotEmpty()) + +
+ + +
+ @endif + + +
+
+ + +
+
+
+
+

총 조회 건수

+

{{ number_format($usageData['total_count']) }}건

+
+
+ + + +
+
+
+ +
+
+
+

유료 조회 건수

+

+ {{ number_format(max(0, $usageData['total_count'] - (count($usageData['details']) * $policy['free_quota']))) }}건 +

+
+
+ + + +
+
+
+ +
+
+
+

예상 청구 금액

+

{{ number_format($usageData['total_fee']) }}원

+
+
+ + + +
+
+
+
+ + +
+
+

+ @if($isHQ) + 테넌트별 조회 현황 + @else + 월별 조회 현황 + @endif +

+
+ + @if(count($usageData['details']) > 0) +
+ + + + @if($isHQ) + + @endif + @if($filters['view_type'] === 'yearly' || !$isHQ) + + @endif + + + + + + + + @foreach($usageData['details'] as $row) + + @if($isHQ) + + @endif + @if($filters['view_type'] === 'yearly' || !$isHQ) + + @endif + + + + + + @endforeach + + + + + + + + + + +
테넌트기간총 조회무료 ({{ $policy['free_quota'] }}건)유료요금
+
{{ $row['tenant_name'] }}
+ @if(isset($row['tenant_code'])) +
{{ $row['tenant_code'] }}
+ @endif +
+ {{ $row['month'] ?? '-' }} + + {{ number_format($row['count']) }}건 + + {{ number_format($row['free_count']) }}건 + + {{ number_format($row['paid_count']) }}건 + + {{ number_format($row['fee']) }}원 +
+ 합계 + {{ number_format($usageData['total_count']) }}건 + {{ number_format(array_sum(array_column($usageData['details'], 'free_count'))) }}건 + + {{ number_format(array_sum(array_column($usageData['details'], 'paid_count'))) }}건 + {{ number_format($usageData['total_fee']) }}원
+
+ @else +
+ + + +

조회 내역이 없습니다.

+
+ @endif +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/routes/web.php b/routes/web.php index ac51433e..77abd39c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -420,6 +420,9 @@ Route::get('/inquiry/{inquiryKey}/report', [\App\Http\Controllers\Credit\CreditController::class, 'getReportData'])->name('inquiry.report'); Route::delete('/inquiry/{id}', [\App\Http\Controllers\Credit\CreditController::class, 'deleteInquiry'])->name('inquiry.destroy'); + // 조회회수 집계 + Route::get('/usage', [\App\Http\Controllers\Credit\CreditUsageController::class, 'index'])->name('usage.index'); + // 설정 관리 Route::get('/settings', [\App\Http\Controllers\Credit\CreditController::class, 'settings'])->name('settings.index'); Route::get('/settings/create', [\App\Http\Controllers\Credit\CreditController::class, 'createConfig'])->name('settings.create');