From 11bacef55c1db07a3c02e63f5c50fa2a1a083360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 2 Feb 2026 11:54:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=98=81=EC=97=85=ED=8C=8C=ED=8A=B8?= =?UTF-8?q?=EB=84=88=20=EA=B3=A0=EA=B0=9D=EA=B4=80=EB=A6=AC=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=B6=94=EA=B0=80=20(=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20=EC=A0=84=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminProspectController 생성 (관리자/슈퍼관리자만 접근) - 전체 영업파트너의 고객 현황을 한눈에 파악 - 영업파트너별 필터, 상태별 필터 제공 - 영업/매니저 진행률 및 개발 상태 표시 - 상세 모달에서 담당자 정보 및 진행 현황 확인 - AdminProspectMenuSeeder 생성 (메뉴 추가용) Co-Authored-By: Claude Opus 4.5 --- .../Sales/AdminProspectController.php | 133 +++++++++ database/seeders/AdminProspectMenuSeeder.php | 73 +++++ .../sales/admin-prospects/index.blade.php | 259 ++++++++++++++++++ .../partials/show-modal.blade.php | 105 +++++++ routes/web.php | 4 + 5 files changed, 574 insertions(+) create mode 100644 app/Http/Controllers/Sales/AdminProspectController.php create mode 100644 database/seeders/AdminProspectMenuSeeder.php create mode 100644 resources/views/sales/admin-prospects/index.blade.php create mode 100644 resources/views/sales/admin-prospects/partials/show-modal.blade.php diff --git a/app/Http/Controllers/Sales/AdminProspectController.php b/app/Http/Controllers/Sales/AdminProspectController.php new file mode 100644 index 00000000..063cb895 --- /dev/null +++ b/app/Http/Controllers/Sales/AdminProspectController.php @@ -0,0 +1,133 @@ +middleware(function ($request, $next) { + if (!auth()->user()->isAdmin() && !auth()->user()->isSuperAdmin()) { + abort(403, '관리자만 접근할 수 있습니다.'); + } + return $next($request); + }); + } + + /** + * 전체 고객 목록 페이지 + */ + public function index(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.admin-prospects.index')); + } + + // 영업 역할을 가진 사용자 목록 (영업파트너) + $salesPartners = User::whereHas('userRoles', function ($q) { + $q->whereHas('role', function ($rq) { + $rq->whereIn('name', ['sales', 'manager', 'recruiter']); + }); + })->orderBy('name')->get(); + + // 필터 + $filters = [ + 'search' => $request->get('search'), + 'status' => $request->get('status'), + 'registered_by' => $request->get('registered_by'), // 특정 영업파트너 필터 + ]; + + // 쿼리 빌드 + $query = TenantProspect::with(['registeredBy', 'tenant']); + + // 검색 + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('company_name', 'like', "%{$search}%") + ->orWhere('business_number', 'like', "%{$search}%") + ->orWhere('ceo_name', 'like', "%{$search}%") + ->orWhere('contact_phone', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + // 영업파트너 필터 + if (!empty($filters['registered_by'])) { + $query->where('registered_by', $filters['registered_by']); + } + + $prospects = $query->orderByDesc('created_at')->paginate(20); + + // 각 가망고객의 진행률 계산 + foreach ($prospects as $prospect) { + $progress = SalesScenarioChecklist::getProspectProgress($prospect->id); + $prospect->sales_progress = $progress['sales']['percentage']; + $prospect->manager_progress = $progress['manager']['percentage']; + + // management 정보 + $management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first(); + $prospect->hq_status = $management?->hq_status ?? 'pending'; + $prospect->hq_status_label = $management?->hq_status_label ?? '대기'; + $prospect->manager_user = $management?->manager; + } + + // 전체 통계 + $stats = [ + 'total' => TenantProspect::count(), + 'active' => TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)->count(), + 'expired' => TenantProspect::where('status', TenantProspect::STATUS_EXPIRED)->count(), + 'converted' => TenantProspect::where('status', TenantProspect::STATUS_CONVERTED)->count(), + ]; + + // 영업파트너별 통계 + $partnerStats = TenantProspect::selectRaw('registered_by, COUNT(*) as total') + ->groupBy('registered_by') + ->with('registeredBy') + ->get() + ->map(function ($item) { + return [ + 'user' => $item->registeredBy, + 'total' => $item->total, + ]; + }); + + return view('sales.admin-prospects.index', compact('prospects', 'stats', 'salesPartners', 'partnerStats', 'filters')); + } + + /** + * 고객 상세 모달 + */ + public function modalShow(int $id): View + { + $prospect = TenantProspect::with(['registeredBy', 'tenant'])->findOrFail($id); + + // 진행률 + $progress = SalesScenarioChecklist::getProspectProgress($prospect->id); + $prospect->sales_progress = $progress['sales']['percentage']; + $prospect->manager_progress = $progress['manager']['percentage']; + + // management 정보 + $management = SalesTenantManagement::findOrCreateByProspect($prospect->id); + + return view('sales.admin-prospects.partials.show-modal', compact('prospect', 'management', 'progress')); + } +} diff --git a/database/seeders/AdminProspectMenuSeeder.php b/database/seeders/AdminProspectMenuSeeder.php new file mode 100644 index 00000000..5f7194cc --- /dev/null +++ b/database/seeders/AdminProspectMenuSeeder.php @@ -0,0 +1,73 @@ +where('name', '영업관리') + ->whereNull('parent_id') + ->value('id'); + + if (!$salesParentId) { + $this->command->error('영업관리 메뉴를 찾을 수 없습니다.'); + return; + } + + // 고객 관리 메뉴 찾기 (sort_order 확인) + $prospectMenu = Menu::where('tenant_id', $tenantId) + ->where('parent_id', $salesParentId) + ->where('name', '고객 관리') + ->first(); + + $sortOrder = ($prospectMenu?->sort_order ?? 2) + 1; + + // 영업파트너 고객관리 메뉴 존재 여부 확인 + $existingMenu = Menu::where('tenant_id', $tenantId) + ->where('parent_id', $salesParentId) + ->where('name', '영업파트너 고객관리') + ->first(); + + if ($existingMenu) { + $this->command->info('영업파트너 고객관리 메뉴가 이미 존재합니다.'); + return; + } + + // 메뉴 생성 + Menu::create([ + 'tenant_id' => $tenantId, + 'parent_id' => $salesParentId, + 'name' => '영업파트너 고객관리', + 'url' => '/sales/admin-prospects', + 'icon' => 'users', + 'sort_order' => $sortOrder, + 'is_active' => true, + 'required_roles' => json_encode(['admin', 'super_admin']), // 관리자만 접근 가능 + ]); + + $this->command->info('영업파트너 고객관리 메뉴 생성 완료 (sort_order: ' . $sortOrder . ')'); + + // 결과 출력 + $this->command->info(''); + $this->command->info('=== 영업관리 하위 메뉴 ==='); + $children = Menu::where('parent_id', $salesParentId) + ->orderBy('sort_order') + ->get(['name', 'url', 'sort_order']); + + foreach ($children as $child) { + $this->command->info("{$child->sort_order}. {$child->name} ({$child->url})"); + } + } +} diff --git a/resources/views/sales/admin-prospects/index.blade.php b/resources/views/sales/admin-prospects/index.blade.php new file mode 100644 index 00000000..b22e1d1f --- /dev/null +++ b/resources/views/sales/admin-prospects/index.blade.php @@ -0,0 +1,259 @@ +@extends('layouts.app') + +@section('title', '영업파트너 고객관리') + +@section('content') +
+ +
+
+

영업파트너 고객관리

+

전체 영업파트너의 고객 현황을 관리합니다 (관리자 전용)

+
+
+ + +
+
+
전체 고객
+
{{ number_format($stats['total']) }}건
+
+
+
영업 진행중
+
{{ number_format($stats['active']) }}건
+
+
+
영업권 만료
+
{{ number_format($stats['expired']) }}건
+
+
+
계약 완료
+
{{ number_format($stats['converted']) }}건
+
+
+ + +
+
+ + + + + + + + @if(request('status')) + + @endif + + +
+ +
+ + +
+ +
+ + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + @forelse($prospects as $prospect) + + + + + + + + + + + + @empty + + + + @endforelse + +
업체명담당 파트너담당 매니저영업 진행률매니저 진행률개발 상태상태등록일관리
+
{{ $prospect->company_name }}
+
{{ $prospect->business_number }}
+
+ @if($prospect->registeredBy) + {{ $prospect->registeredBy->name }} + @else + - + @endif + + @if($prospect->manager_user) + {{ $prospect->manager_user->name }} + @else + 미지정 + @endif + +
+
+
+
+ {{ $prospect->sales_progress }}% +
+
+
+
+
+
+ {{ $prospect->manager_progress }}% +
+
+ + {{ $prospect->hq_status_label }} + + + + {{ $prospect->status_label }} + + + {{ $prospect->created_at->format('Y-m-d') }} + + +
+ 등록된 고객이 없습니다. +
+
+ + + @if($prospects->hasPages()) +
+ {{ $prospects->withQueryString()->links() }} +
+ @endif +
+
+ + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/sales/admin-prospects/partials/show-modal.blade.php b/resources/views/sales/admin-prospects/partials/show-modal.blade.php new file mode 100644 index 00000000..f06fdab9 --- /dev/null +++ b/resources/views/sales/admin-prospects/partials/show-modal.blade.php @@ -0,0 +1,105 @@ +{{-- 상세 모달 내용 --}} +
+ +
+
+

{{ $prospect->company_name }}

+

{{ $prospect->business_number }}

+
+ +
+ + +
+
+

대표자

+

{{ $prospect->ceo_name ?? '-' }}

+
+
+

연락처

+

{{ $prospect->contact_phone ?? '-' }}

+
+
+

이메일

+

{{ $prospect->contact_email ?? '-' }}

+
+
+

상태

+ + {{ $prospect->status_label }} + +
+
+ + +
+

담당자 정보

+
+
+

담당 파트너

+

{{ $prospect->registeredBy?->name ?? '-' }}

+
+
+

담당 매니저

+

{{ $management->manager?->name ?? '미지정' }}

+
+
+
+ + +
+

영업 진행 현황

+
+
+
+ 영업 시나리오 + {{ $progress['sales']['percentage'] }}% +
+
+
+
+
+
+
+ 매니저 시나리오 + {{ $progress['manager']['percentage'] }}% +
+
+
+
+
+
+
+ + +
+

개발 진행 상태

+
+ + {{ $management->hq_status_label }} + +
+
+ + +
+

등록일: {{ $prospect->created_at->format('Y-m-d H:i') }}

+ @if($prospect->expires_at) +

영업권 만료: {{ $prospect->expires_at->format('Y-m-d') }}

+ @endif +
+ + +
+ +
+
diff --git a/routes/web.php b/routes/web.php index f8a737eb..2400bbf5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -928,6 +928,10 @@ Route::get('prospects/{id}/modal-show', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalShow'])->name('prospects.modal-show'); Route::get('prospects/{id}/modal-edit', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalEdit'])->name('prospects.modal-edit'); + // 관리자용 전체 고객 관리 (관리자/슈퍼관리자 전용) + Route::get('admin-prospects', [\App\Http\Controllers\Sales\AdminProspectController::class, 'index'])->name('admin-prospects.index'); + Route::get('admin-prospects/{id}/modal-show', [\App\Http\Controllers\Sales\AdminProspectController::class, 'modalShow'])->name('admin-prospects.modal-show'); + // 영업 시나리오 관리 Route::prefix('scenarios')->name('scenarios.')->group(function () { // 테넌트 기반 (기존)