From ced9110f3b94055d3fceec6923bdefb01f7e33a5 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 16:43:02 +0900 Subject: [PATCH 01/61] =?UTF-8?q?refactor:=EC=98=81=EC=97=85=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=EB=84=88=20=EC=97=AD=ED=95=A0=202=EA=B0=9C=EB=A1=9C?= =?UTF-8?q?=20=EB=8B=A8=EC=88=9C=ED=99=94=20(recruiter=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 역할: sales(영업파트너), manager(상담매니저) 2개만 유지 - recruiter(유치담당) 역할 완전 제거 - 역할 레이블 변경: 영업→영업파트너, 매니저→상담매니저 - 통계, 필터, 역할관리 UI 모두 업데이트 Co-Authored-By: Claude Opus 4.5 --- .../Sales/AdminProspectController.php | 2 +- .../Sales/SalesManagerController.php | 12 ++++----- app/Services/Sales/SalesManagerService.php | 5 +--- database/seeders/SalesRoleSeeder.php | 10 +++---- .../views/sales/managers/approvals.blade.php | 12 +++------ .../views/sales/managers/index.blade.php | 21 +++++---------- .../managers/partials/show-modal.blade.php | 11 +++----- resources/views/sales/managers/show.blade.php | 26 +++++++------------ 8 files changed, 35 insertions(+), 64 deletions(-) diff --git a/app/Http/Controllers/Sales/AdminProspectController.php b/app/Http/Controllers/Sales/AdminProspectController.php index 4360dd7d..ead104c8 100644 --- a/app/Http/Controllers/Sales/AdminProspectController.php +++ b/app/Http/Controllers/Sales/AdminProspectController.php @@ -82,7 +82,7 @@ private function getIndexData(Request $request): array // 영업 역할을 가진 사용자 목록 (영업파트너) $salesPartners = User::whereHas('userRoles', function ($q) { $q->whereHas('role', function ($rq) { - $rq->whereIn('name', ['sales', 'manager', 'recruiter']); + $rq->whereIn('name', ['sales', 'manager']); }); })->orderBy('name')->get(); diff --git a/app/Http/Controllers/Sales/SalesManagerController.php b/app/Http/Controllers/Sales/SalesManagerController.php index 3d0e1e3b..c78ec003 100644 --- a/app/Http/Controllers/Sales/SalesManagerController.php +++ b/app/Http/Controllers/Sales/SalesManagerController.php @@ -261,7 +261,7 @@ public function delegateRole(Request $request, int $id) { $validated = $request->validate([ 'to_user_id' => 'required|exists:users,id', - 'role_name' => 'required|string|in:manager,recruiter', + 'role_name' => 'required|string|in:manager', ]); $fromUser = User::findOrFail($id); @@ -270,7 +270,7 @@ public function delegateRole(Request $request, int $id) try { $this->service->delegateRole($fromUser, $toUser, $validated['role_name']); - $roleLabel = $validated['role_name'] === 'manager' ? '매니저' : '유치담당'; + $roleLabel = '상담매니저'; return redirect()->back() ->with('success', "{$roleLabel} 역할이 {$toUser->name}님에게 위임되었습니다."); } catch (\InvalidArgumentException $e) { @@ -285,13 +285,13 @@ public function delegateRole(Request $request, int $id) public function assignRole(Request $request, int $id) { $validated = $request->validate([ - 'role_name' => 'required|string|in:sales,manager,recruiter', + 'role_name' => 'required|string|in:sales,manager', ]); $partner = User::findOrFail($id); $this->service->assignRole($partner, $validated['role_name']); - $roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당']; + $roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저']; return redirect()->back() ->with('success', "{$roleLabels[$validated['role_name']]} 역할이 부여되었습니다."); } @@ -302,13 +302,13 @@ public function assignRole(Request $request, int $id) public function removeRole(Request $request, int $id) { $validated = $request->validate([ - 'role_name' => 'required|string|in:sales,manager,recruiter', + 'role_name' => 'required|string|in:sales,manager', ]); $partner = User::findOrFail($id); $this->service->removeRole($partner, $validated['role_name']); - $roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당']; + $roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저']; return redirect()->back() ->with('success', "{$roleLabels[$validated['role_name']]} 역할이 제거되었습니다."); } diff --git a/app/Services/Sales/SalesManagerService.php b/app/Services/Sales/SalesManagerService.php index c111b3e6..d3d1d54d 100644 --- a/app/Services/Sales/SalesManagerService.php +++ b/app/Services/Sales/SalesManagerService.php @@ -19,7 +19,7 @@ class SalesManagerService /** * 영업파트너 역할 이름 목록 */ - public const SALES_ROLES = ['sales', 'manager', 'recruiter']; + public const SALES_ROLES = ['sales', 'manager']; /** * 영업파트너 생성 @@ -510,9 +510,6 @@ public function getStats(?int $parentId = null): array 'manager' => (clone $baseQuery) ->whereHas('userRoles.role', fn($q) => $q->where('name', 'manager')) ->count(), - 'recruiter' => (clone $baseQuery) - ->whereHas('userRoles.role', fn($q) => $q->where('name', 'recruiter')) - ->count(), ]; } diff --git a/database/seeders/SalesRoleSeeder.php b/database/seeders/SalesRoleSeeder.php index 3fc3e9a4..1744bc5b 100644 --- a/database/seeders/SalesRoleSeeder.php +++ b/database/seeders/SalesRoleSeeder.php @@ -20,15 +20,11 @@ public function run(): void $roles = [ [ 'name' => 'sales', - 'description' => '영업 - 가망고객 발굴, 상담, 계약 체결', + 'description' => '영업파트너 - 고객 발굴, 계약 체결', ], [ 'name' => 'manager', - 'description' => '매니저 - 하위 파트너 관리, 실적 취합, 승인 처리', - ], - [ - 'name' => 'recruiter', - 'description' => '유치담당 - 새로운 영업파트너 유치 활동', + 'description' => '상담매니저 - 고객 상담, 인터뷰 정리', ], ]; @@ -45,6 +41,6 @@ public function run(): void ); } - $this->command->info('영업파트너 역할이 생성되었습니다: sales, manager, recruiter'); + $this->command->info('영업파트너 역할이 생성되었습니다: sales, manager'); } } diff --git a/resources/views/sales/managers/approvals.blade.php b/resources/views/sales/managers/approvals.blade.php index 0b177676..2561433e 100644 --- a/resources/views/sales/managers/approvals.blade.php +++ b/resources/views/sales/managers/approvals.blade.php @@ -80,13 +80,11 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp @@ -177,13 +175,11 @@ class="px-2 py-1 bg-gray-400 hover:bg-gray-500 text-white text-xs font-medium ro $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp diff --git a/resources/views/sales/managers/index.blade.php b/resources/views/sales/managers/index.blade.php index 577c5245..5b3aee2c 100644 --- a/resources/views/sales/managers/index.blade.php +++ b/resources/views/sales/managers/index.blade.php @@ -20,7 +20,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition -
+
전체
{{ number_format($stats['total']) }}명
@@ -34,17 +34,13 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
{{ number_format($stats['approved']) }}명
-
영업
+
영업파트너
{{ number_format($stats['sales']) }}명
-
매니저
+
상담매니저
{{ number_format($stats['manager']) }}명
-
-
유치담당
-
{{ number_format($stats['recruiter']) }}명
-
@@ -87,9 +83,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none foc
@@ -135,13 +130,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none foc $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp diff --git a/resources/views/sales/managers/partials/show-modal.blade.php b/resources/views/sales/managers/partials/show-modal.blade.php index c03f17bb..fe4d941e 100644 --- a/resources/views/sales/managers/partials/show-modal.blade.php +++ b/resources/views/sales/managers/partials/show-modal.blade.php @@ -151,11 +151,10 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-

역할 관리

@php $currentRoles = $partner->userRoles->pluck('role.name')->toArray(); - $roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당']; + $roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저']; $roleColors = [ 'sales' => 'bg-blue-100 text-blue-800 border-blue-200', 'manager' => 'bg-purple-100 text-purple-800 border-purple-200', - 'recruiter' => 'bg-green-100 text-green-800 border-green-200', ]; @endphp
@@ -180,7 +179,7 @@ class="ml-1 text-gray-400 hover:text-red-500"> @endforelse
- @foreach(['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당'] as $roleName => $label) + @foreach(['sales' => '영업파트너', 'manager' => '상담매니저'] as $roleName => $label) @if(!in_array($roleName, $currentRoles))
@csrf @@ -229,13 +228,11 @@ class="text-xs text-blue-600 hover:underline">다운로드 $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp diff --git a/resources/views/sales/managers/show.blade.php b/resources/views/sales/managers/show.blade.php index 0956df2c..8223a702 100644 --- a/resources/views/sales/managers/show.blade.php +++ b/resources/views/sales/managers/show.blade.php @@ -21,13 +21,11 @@ $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp @@ -169,11 +167,10 @@ class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition te
@php $currentRoles = $partner->userRoles->pluck('role.name')->toArray(); - $roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당']; + $roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저']; $roleColors = [ 'sales' => 'bg-blue-100 text-blue-800 border-blue-200', 'manager' => 'bg-purple-100 text-purple-800 border-purple-200', - 'recruiter' => 'bg-green-100 text-green-800 border-green-200', ]; @endphp @forelse($currentRoles as $roleName) @@ -202,7 +199,7 @@ class="ml-1 text-gray-400 hover:text-red-500">

역할 부여

- @foreach(['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당'] as $roleName => $label) + @foreach(['sales' => '영업파트너', 'manager' => '상담매니저'] as $roleName => $label) @if(!in_array($roleName, $currentRoles)) @csrf @@ -217,8 +214,8 @@ class="px-3 py-1 text-sm border border-gray-300 rounded-full hover:bg-gray-50 tr
- - @if((in_array('manager', $currentRoles) || in_array('recruiter', $currentRoles)) && $delegationCandidates->isNotEmpty()) + + @if(in_array('manager', $currentRoles) && $delegationCandidates->isNotEmpty())

역할 위임

보유 중인 역할을 하위 파트너에게 위임할 수 있습니다. 위임하면 해당 역할이 제거됩니다.

@@ -230,10 +227,7 @@ class="px-3 py-1 text-sm border border-gray-300 rounded-full hover:bg-gray-50 tr
@@ -322,13 +316,11 @@ class="text-sm text-blue-600 hover:underline">다운로드 $roleColor = match($userRole->role->name ?? '') { 'sales' => 'bg-blue-100 text-blue-800', 'manager' => 'bg-purple-100 text-purple-800', - 'recruiter' => 'bg-green-100 text-green-800', default => 'bg-gray-100 text-gray-800', }; $roleLabel = match($userRole->role->name ?? '') { - 'sales' => '영업', - 'manager' => '매니저', - 'recruiter' => '유치담당', + 'sales' => '영업파트너', + 'manager' => '상담매니저', default => $userRole->role->name ?? '-', }; @endphp From c5f2957ab77b3671fd3baff4f0aab9eab72889f1 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 16:45:59 +0900 Subject: [PATCH 02/61] =?UTF-8?q?fix:recruiter=20=EC=97=AD=ED=95=A0=20DB?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=99=84=EC=A0=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시더에서 recruiter 역할 및 할당 강제 삭제 로직 추가 - soft delete 레코드 포함 forceDelete 처리 Co-Authored-By: Claude Opus 4.5 --- database/seeders/SalesRoleSeeder.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/database/seeders/SalesRoleSeeder.php b/database/seeders/SalesRoleSeeder.php index 1744bc5b..ca731dd7 100644 --- a/database/seeders/SalesRoleSeeder.php +++ b/database/seeders/SalesRoleSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\Role; +use App\Models\UserRole; use Illuminate\Database\Seeder; class SalesRoleSeeder extends Seeder @@ -41,6 +42,23 @@ public function run(): void ); } + // recruiter 역할 삭제 (더 이상 사용하지 않음) + $recruiterRole = Role::where('tenant_id', $tenantId) + ->where('name', 'recruiter') + ->first(); + + if ($recruiterRole) { + // 해당 역할을 가진 user_roles 강제 삭제 (soft delete 포함) + UserRole::withTrashed() + ->where('role_id', $recruiterRole->id) + ->forceDelete(); + $this->command->info('recruiter 역할 할당이 삭제되었습니다.'); + + // 역할 강제 삭제 + $recruiterRole->forceDelete(); + $this->command->info('recruiter 역할이 삭제되었습니다.'); + } + $this->command->info('영업파트너 역할이 생성되었습니다: sales, manager'); } } From 8574a9116f484b54cfa23d48ec7a29846a6f03b1 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 16:53:07 +0900 Subject: [PATCH 03/61] =?UTF-8?q?feat:=ED=8C=8C=ED=8A=B8=EB=84=88=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A9=94=EB=89=B4=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 영업파트너 관리 → 파트너 관리 Co-Authored-By: Claude Opus 4.5 --- database/seeders/PartnerMenuRenameSeeder.php | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 database/seeders/PartnerMenuRenameSeeder.php diff --git a/database/seeders/PartnerMenuRenameSeeder.php b/database/seeders/PartnerMenuRenameSeeder.php new file mode 100644 index 00000000..2b2efa90 --- /dev/null +++ b/database/seeders/PartnerMenuRenameSeeder.php @@ -0,0 +1,31 @@ +where('name', '영업파트너 관리') + ->first(); + + if ($menu) { + $menu->name = '파트너 관리'; + $menu->save(); + $this->command->info('메뉴 이름 변경: 영업파트너 관리 → 파트너 관리'); + } else { + $this->command->warn('영업파트너 관리 메뉴를 찾을 수 없습니다.'); + } + } +} From 8303f6fd40fc43c07d0c63c42dafc40a082d589b 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 16:58:30 +0900 Subject: [PATCH 04/61] =?UTF-8?q?fix:=EC=98=81=EC=97=85=EA=B6=8C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B6=8C=ED=95=9C=EC=9D=84=20admin?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 뷰에서 삭제 버튼 admin만 표시 - 컨트롤러에서 삭제 권한 체크 추가 Co-Authored-By: Claude Opus 4.5 --- app/Http/Controllers/Sales/TenantProspectController.php | 7 ++++--- resources/views/sales/prospects/index.blade.php | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Sales/TenantProspectController.php b/app/Http/Controllers/Sales/TenantProspectController.php index b0f7c30a..1a707b45 100644 --- a/app/Http/Controllers/Sales/TenantProspectController.php +++ b/app/Http/Controllers/Sales/TenantProspectController.php @@ -171,9 +171,10 @@ public function destroy(int $id) ->with('error', '이미 테넌트로 전환된 영업권은 삭제할 수 없습니다.'); } - // 본인 또는 관리자만 삭제 가능 - if ($prospect->registered_by !== auth()->id()) { - // TODO: 관리자 권한 체크 추가 + // 관리자만 삭제 가능 + if (!auth()->user()->isAdmin()) { + return redirect()->route('sales.prospects.index') + ->with('error', '삭제 권한이 없습니다. 본사 운영팀에 문의하세요.'); } $prospect->delete(); diff --git a/resources/views/sales/prospects/index.blade.php b/resources/views/sales/prospects/index.blade.php index d36c66ba..02f7a34d 100644 --- a/resources/views/sales/prospects/index.blade.php +++ b/resources/views/sales/prospects/index.blade.php @@ -117,6 +117,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc @if(!$prospect->isConverted()) + @if(auth()->user()->isAdmin()) @csrf @@ -124,6 +125,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc @endif + @endif @empty From 87e3e9d75acdac321ce1641640d0060a933ffec6 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 17:08:24 +0900 Subject: [PATCH 05/61] =?UTF-8?q?feat:=EC=83=81=EB=8B=B4=EB=A7=A4=EB=8B=88?= =?UTF-8?q?=EC=A0=80=20=EC=84=A0=ED=83=9D=20UI=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B2=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 드롭다운 선택 방식에서 실시간 검색 UI로 변경 - getAllManagerUsers() 메서드 추가 (manager 역할 사용자 조회) - searchManagers() API 추가 (이름/이메일 검색) - 자신이 유치한 파트너뿐만 아니라 모든 상담매니저 역할 사용자 선택 가능 Co-Authored-By: Claude Opus 4.5 --- .../Sales/SalesDashboardController.php | 61 +++++- .../partials/manager-dropdown.blade.php | 176 +++++++++++++----- routes/web.php | 1 + 3 files changed, 183 insertions(+), 55 deletions(-) diff --git a/app/Http/Controllers/Sales/SalesDashboardController.php b/app/Http/Controllers/Sales/SalesDashboardController.php index 53b523cd..f075ea39 100644 --- a/app/Http/Controllers/Sales/SalesDashboardController.php +++ b/app/Http/Controllers/Sales/SalesDashboardController.php @@ -251,10 +251,8 @@ private function getDashboardData(Request $request): array ->get() ->keyBy('tenant_id'); - // 내가 유치한 영업파트너 목록 (드롭다운용) - $allManagers = auth()->user()->children() - ->where('is_active', true) - ->get(['id', 'name', 'email']); + // 상담매니저 역할을 가진 모든 사용자 (드롭다운용) + $allManagers = $this->getAllManagerUsers(); // 내가 매니저로만 참여하는 건 (다른 사람이 등록, 내가 매니저) $managerOnlyProspects = $this->getManagerOnlyProspects($currentUserId); @@ -412,10 +410,8 @@ public function refreshTenantList(Request $request): View ->get() ->keyBy('tenant_id'); - // 내가 유치한 영업파트너 목록 (드롭다운용) - $allManagers = auth()->user()->children() - ->where('is_active', true) - ->get(['id', 'name', 'email']); + // 상담매니저 역할을 가진 모든 사용자 (드롭다운용) + $allManagers = $this->getAllManagerUsers(); return view('sales.dashboard.partials.tenant-list', compact( 'tenants', @@ -768,6 +764,55 @@ private function getCommissionData(): array return compact('commissionSummary', 'recentCommissions', 'partner'); } + /** + * 상담매니저 역할을 가진 모든 사용자 조회 + */ + private function getAllManagerUsers() + { + $tenantId = session('selected_tenant_id', 1); + + return User::whereHas('userRoles', function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId) + ->whereHas('role', function ($rq) { + $rq->where('name', 'manager'); + }); + }) + ->where('is_active', true) + ->where('id', '!=', auth()->id()) // 본인 제외 + ->get(['id', 'name', 'email']); + } + + /** + * 매니저 검색 API (AJAX) + */ + public function searchManagers(Request $request): JsonResponse + { + $query = $request->input('q', ''); + $tenantId = session('selected_tenant_id', 1); + + $managers = User::whereHas('userRoles', function ($q) use ($tenantId) { + $q->where('tenant_id', $tenantId) + ->whereHas('role', function ($rq) { + $rq->where('name', 'manager'); + }); + }) + ->where('is_active', true) + ->where('id', '!=', auth()->id()) + ->when($query, function ($q) use ($query) { + $q->where(function ($subQ) use ($query) { + $subQ->where('name', 'like', "%{$query}%") + ->orWhere('email', 'like', "%{$query}%"); + }); + }) + ->limit(10) + ->get(['id', 'name', 'email']); + + return response()->json([ + 'success' => true, + 'managers' => $managers, + ]); + } + /** * 영업파트너 가이드북 도움말 모달 */ diff --git a/resources/views/sales/dashboard/partials/manager-dropdown.blade.php b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php index 6f4ba732..a3251dfb 100644 --- a/resources/views/sales/dashboard/partials/manager-dropdown.blade.php +++ b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php @@ -1,4 +1,4 @@ -{{-- 매니저 드롭다운 컴포넌트 (테넌트 또는 가망고객용) --}} +{{-- 매니저 검색 컴포넌트 (테넌트 또는 가망고객용) --}} @once @endonce @@ -16,26 +16,65 @@ $assignedManager = $management?->manager; $isSelf = !$assignedManager || $assignedManager->id === auth()->id(); $managerName = $assignedManager?->name ?? '본인'; - $managersJson = $allManagers->map(fn($m) => ['id' => $m->id, 'name' => $m->name, 'email' => $m->email])->values()->toJson(); - $currentManagerJson = json_encode($assignedManager ? ['id' => $assignedManager->id, 'name' => $assignedManager->name, 'is_self' => $isSelf] : null); - - // API 엔드포인트 결정 - $apiEndpoint = $isProspect ? '/sales/prospects/' : '/sales/tenants/'; + $currentManagerJson = json_encode($assignedManager ? ['id' => $assignedManager->id, 'name' => $assignedManager->name, 'email' => $assignedManager->email ?? '', 'is_self' => $isSelf] : null); @endphp
- {{-- 본인 옵션 --}} - +
- {{-- 구분선 (다른 매니저가 있을 때만) --}} - - - {{-- 다른 매니저 목록 --}} - - {{-- 매니저가 없을 때 --}} - + + {{-- 검색 결과 --}} + + + {{-- 검색어가 있지만 결과 없음 --}} + + + {{-- 검색어 없을 때 안내 --}} + +
diff --git a/routes/web.php b/routes/web.php index 7434b830..bdc08b41 100644 --- a/routes/web.php +++ b/routes/web.php @@ -977,6 +977,7 @@ // 매니저 목록 조회 (드롭다운용) Route::get('/managers/list', [\App\Http\Controllers\Sales\SalesDashboardController::class, 'getManagers'])->name('managers.list'); + Route::get('/managers/search', [\App\Http\Controllers\Sales\SalesDashboardController::class, 'searchManagers'])->name('managers.search'); // 상품관리 (HQ 전용) Route::prefix('products')->name('products.')->group(function () { From 646cd7d6ef3a281dba467eaeba2f9912a6d5b5f2 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 17:15:19 +0900 Subject: [PATCH 06/61] =?UTF-8?q?feat:=EC=9D=B8=EA=B3=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=ED=95=AD=EB=AA=A9=20=EC=98=81=EC=97=85/=EB=A7=A4?= =?UTF-8?q?=EB=8B=88=EC=A0=80=20=EA=B8=B0=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=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 - 인계 완료 섹션에 영업/매니저 기록 조회 버튼 추가 - readonly 모드로 열어 수정 불가, 조회만 가능 - prospectManagerScenario에 readonly 파라미터 지원 추가 - 단계 이동 시 readonly 파라미터 유지 - 마지막 단계 버튼 텍스트 조건부 표시 (완료/닫기) Co-Authored-By: Claude Opus 4.5 --- .../Sales/SalesScenarioController.php | 3 ++ .../dashboard/partials/tenant-list.blade.php | 30 +++++++++++++++++-- .../sales/modals/scenario-step.blade.php | 9 ++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Sales/SalesScenarioController.php b/app/Http/Controllers/Sales/SalesScenarioController.php index fee9fe69..d2acd2bd 100644 --- a/app/Http/Controllers/Sales/SalesScenarioController.php +++ b/app/Http/Controllers/Sales/SalesScenarioController.php @@ -228,6 +228,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View $steps = config('sales_scenario.manager_steps'); $currentStep = (int) $request->input('step', 1); $icons = config('sales_scenario.icons'); + $readonly = $request->boolean('readonly', false); // 가망고객 영업 관리 정보 조회 또는 생성 $management = SalesTenantManagement::findOrCreateByProspect($prospectId); @@ -250,6 +251,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View 'icons' => $icons, 'management' => $management, 'isProspect' => true, + 'readonly' => $readonly, ]); } @@ -262,6 +264,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View 'icons' => $icons, 'management' => $management, 'isProspect' => true, + 'readonly' => $readonly, ]); } diff --git a/resources/views/sales/dashboard/partials/tenant-list.blade.php b/resources/views/sales/dashboard/partials/tenant-list.blade.php index 4722095a..051a2c67 100644 --- a/resources/views/sales/dashboard/partials/tenant-list.blade.php +++ b/resources/views/sales/dashboard/partials/tenant-list.blade.php @@ -9,10 +9,11 @@ swap: 'innerHTML' }); }, - openProspectScenarioModal(prospectId, type) { + openProspectScenarioModal(prospectId, type, readonly = false) { + const readonlyParam = readonly ? '?readonly=1' : ''; const url = type === 'sales' - ? `/sales/scenarios/prospect/${prospectId}/sales` - : `/sales/scenarios/prospect/${prospectId}/manager`; + ? `/sales/scenarios/prospect/${prospectId}/sales${readonlyParam}` + : `/sales/scenarios/prospect/${prospectId}/manager${readonlyParam}`; htmx.ajax('GET', url, { target: '#scenario-modal-container', swap: 'innerHTML' @@ -329,6 +330,29 @@ class="inline-flex items-center gap-1 px-3 py-1.5 rounded text-xs font-medium bg {{ $prospect->business_number ?? '-' }} + {{-- 영업/매니저 기록 조회 버튼 (읽기 전용) --}} +
+ + +
diff --git a/resources/views/sales/modals/scenario-step.blade.php b/resources/views/sales/modals/scenario-step.blade.php index db860d10..6ad337f0 100644 --- a/resources/views/sales/modals/scenario-step.blade.php +++ b/resources/views/sales/modals/scenario-step.blade.php @@ -202,12 +202,15 @@ class="border-t border-gray-100"> $routeName = $isProspectMode ? 'sales.scenarios.prospect.' . $scenarioType : 'sales.scenarios.' . $scenarioType; + + // readonly 파라미터 + $readonlyParam = $isReadonly ? '&readonly=1' : ''; @endphp
{{-- 이전 단계 버튼 --}} @if($currentStepId > 1) @else + +
@@ -267,10 +283,32 @@ class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" // 드래그 앤 드롭 초기화 function initDropZone(dropZone) { - const fileInput = dropZone.querySelector('input[type="file"]'); + const fileInput = dropZone.querySelector('.doc-file-input'); + const cameraInput = dropZone.querySelector('.doc-camera-input'); + const fileBtn = dropZone.querySelector('.doc-file-btn'); + const cameraBtn = dropZone.querySelector('.doc-camera-btn'); + // 파일 선택 버튼 클릭 + if (fileBtn) { + fileBtn.addEventListener('click', (e) => { + e.stopPropagation(); + fileInput.click(); + }); + } + + // 카메라 촬영 버튼 클릭 + if (cameraBtn) { + cameraBtn.addEventListener('click', (e) => { + e.stopPropagation(); + cameraInput.click(); + }); + } + + // 드롭존 영역 클릭 (버튼 외 영역) dropZone.addEventListener('click', (e) => { - if (!e.target.closest('.doc-preview-remove')) { + if (!e.target.closest('.doc-preview-remove') && + !e.target.closest('.doc-file-btn') && + !e.target.closest('.doc-camera-btn')) { fileInput.click(); } }); @@ -292,17 +330,29 @@ function initDropZone(dropZone) { } }); + // 파일 input change 이벤트 fileInput.addEventListener('change', (e) => { if (e.target.files.length) { handleFile(dropZone, e.target.files[0]); } }); + + // 카메라 input change 이벤트 (카메라로 촬영한 이미지 처리) + if (cameraInput) { + cameraInput.addEventListener('change', (e) => { + if (e.target.files.length) { + handleFile(dropZone, e.target.files[0]); + // 카메라 input 초기화 (같은 이미지 다시 촬영 가능하도록) + cameraInput.value = ''; + } + }); + } } function handleFile(dropZone, file) { - const fileInput = dropZone.querySelector('input[type="file"]'); + const fileInput = dropZone.querySelector('.doc-file-input'); - // DataTransfer로 파일 설정 + // DataTransfer로 파일 설정 (메인 file input에 저장) const dt = new DataTransfer(); dt.items.add(file); fileInput.files = dt.files; @@ -314,11 +364,13 @@ function handleFile(dropZone, file) { const existingPreview = dropZone.querySelector('.doc-preview'); if (existingPreview) existingPreview.remove(); - // 아이콘과 텍스트 숨기기 + // 아이콘, 텍스트, 버튼 숨기기 const icon = dropZone.querySelector('.doc-drop-zone-icon'); const text = dropZone.querySelector('p'); + const buttons = dropZone.querySelector('.doc-buttons'); if (icon) icon.style.display = 'none'; if (text) text.style.display = 'none'; + if (buttons) buttons.style.display = 'none'; // 미리보기 생성 const preview = document.createElement('div'); @@ -365,10 +417,11 @@ function handleFile(dropZone, file) { function removeFile(btn) { const dropZone = btn.closest('.doc-drop-zone'); - const fileInput = dropZone.querySelector('input[type="file"]'); + const fileInput = dropZone.querySelector('.doc-file-input'); const preview = dropZone.querySelector('.doc-preview'); const icon = dropZone.querySelector('.doc-drop-zone-icon'); const text = dropZone.querySelector('p'); + const buttons = dropZone.querySelector('.doc-buttons'); // 파일 input 초기화 fileInput.value = ''; @@ -376,9 +429,10 @@ function removeFile(btn) { // 미리보기 제거 if (preview) preview.remove(); - // 아이콘과 텍스트 다시 표시 + // 아이콘, 텍스트, 버튼 다시 표시 if (icon) icon.style.display = ''; if (text) text.style.display = ''; + if (buttons) buttons.style.display = ''; dropZone.classList.remove('has-file'); } @@ -413,11 +467,27 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none foc
- + +

클릭 또는 드래그하여 업로드

+
+ + +
From d9b0c058a962470344dda04e383635503d6311ff 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 19:43:58 +0900 Subject: [PATCH 13/61] =?UTF-8?q?feat:=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20UI=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20-=20=EA=B2=80=EC=83=89=EA=B3=BC=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 드롭다운 열릴 때 자동으로 전체 매니저 목록 로드 - 검색어 입력 시 로컬에서 즉시 필터링 - 목록 카운트 표시 추가 Co-Authored-By: Claude Opus 4.5 --- .../partials/manager-dropdown.blade.php | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/resources/views/sales/dashboard/partials/manager-dropdown.blade.php b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php index a3251dfb..854dc545 100644 --- a/resources/views/sales/dashboard/partials/manager-dropdown.blade.php +++ b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php @@ -29,6 +29,9 @@ currentManager: {{ $currentManagerJson }}, searchTimeout: null, + allManagers: [], + hasLoadedAll: false, + toggle() { this.isOpen = !this.isOpen; if (this.isOpen) { @@ -36,27 +39,21 @@ this.$refs.searchInput?.focus(); }); this.searchQuery = ''; - this.searchResults = []; + // 처음 열릴 때 전체 목록 로드 + if (!this.hasLoadedAll) { + this.loadAllManagers(); + } else { + this.searchResults = this.allManagers; + } } }, close() { this.isOpen = false; this.searchQuery = ''; - this.searchResults = []; }, - search() { - clearTimeout(this.searchTimeout); - this.searchTimeout = setTimeout(() => { - this.performSearch(); - }, 300); - }, - performSearch() { - if (this.searchQuery.length < 1) { - this.searchResults = []; - return; - } + loadAllManagers() { this.isLoading = true; - fetch('/sales/managers/search?q=' + encodeURIComponent(this.searchQuery), { + fetch('/sales/managers/search?q=', { headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content, @@ -66,14 +63,35 @@ .then(result => { this.isLoading = false; if (result.success) { + this.allManagers = result.managers; this.searchResults = result.managers; + this.hasLoadedAll = true; } }) .catch(error => { this.isLoading = false; - console.error('검색 실패:', error); + console.error('매니저 목록 로드 실패:', error); }); }, + search() { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + this.performSearch(); + }, 200); + }, + performSearch() { + if (this.searchQuery.length < 1) { + // 검색어 없으면 전체 목록 표시 + this.searchResults = this.allManagers; + return; + } + // 로컬에서 필터링 (이미 로드된 목록에서) + const query = this.searchQuery.toLowerCase(); + this.searchResults = this.allManagers.filter(m => + m.name.toLowerCase().includes(query) || + (m.email && m.email.toLowerCase().includes(query)) + ); + }, selectManager(managerId, managerName, managerEmail) { const endpoint = this.isProspect ? '/sales/prospects/' : '/sales/tenants/'; fetch(endpoint + this.entityId + '/assign-manager', { @@ -181,10 +199,13 @@ class="w-full flex items-center gap-3 px-4 py-2.5 text-left text-sm hover:bg-gra
검색 중...
- {{-- 검색 결과 --}} + {{-- 검색 결과 (매니저 목록) --}}