From 2f381b2285ce0946b5680202b23e569b7c2bfca3 Mon Sep 17 00:00:00 2001 From: pro Date: Wed, 28 Jan 2026 21:45:11 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EB=A0=88=EA=B1=B0=EC=8B=9C=20=EC=98=81?= =?UTF-8?q?=EC=97=85=EA=B4=80=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20MNG?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 영업/매니저 시나리오 모달 구현 (6단계 체크리스트) - 상담 기록 기능 (텍스트, 음성, 첨부파일) - 음성 녹음 + Speech-to-Text 변환 - 첨부파일 Drag & Drop 업로드 - 매니저 지정 드롭다운 Co-Authored-By: Claude Opus 4.5 --- .../Sales/ConsultationController.php | 286 +++++++++++ .../Sales/SalesDashboardController.php | 67 +++ .../Sales/SalesScenarioController.php | 212 +++++++++ config/sales_scenario.php | 447 ++++++++++++++++++ .../views/sales/dashboard/index.blade.php | 26 + .../partials/manager-dropdown.blade.php | 170 +++++++ .../dashboard/partials/tenant-list.blade.php | 131 ++--- .../sales/modals/consultation-log.blade.php | 201 ++++++++ .../sales/modals/file-uploader.blade.php | 260 ++++++++++ .../sales/modals/scenario-modal.blade.php | 185 ++++++++ .../sales/modals/scenario-step.blade.php | 184 +++++++ .../sales/modals/voice-recorder.blade.php | 363 ++++++++++++++ routes/web.php | 21 + 13 files changed, 2497 insertions(+), 56 deletions(-) create mode 100644 app/Http/Controllers/Sales/ConsultationController.php create mode 100644 app/Http/Controllers/Sales/SalesScenarioController.php create mode 100644 config/sales_scenario.php create mode 100644 resources/views/sales/dashboard/partials/manager-dropdown.blade.php create mode 100644 resources/views/sales/modals/consultation-log.blade.php create mode 100644 resources/views/sales/modals/file-uploader.blade.php create mode 100644 resources/views/sales/modals/scenario-modal.blade.php create mode 100644 resources/views/sales/modals/scenario-step.blade.php create mode 100644 resources/views/sales/modals/voice-recorder.blade.php diff --git a/app/Http/Controllers/Sales/ConsultationController.php b/app/Http/Controllers/Sales/ConsultationController.php new file mode 100644 index 00000000..fea2eade --- /dev/null +++ b/app/Http/Controllers/Sales/ConsultationController.php @@ -0,0 +1,286 @@ +input('scenario_type', 'sales'); + $stepId = $request->input('step_id'); + + // 캐시에서 상담 기록 조회 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 특정 단계 필터링 + if ($stepId) { + $consultations = array_filter($consultations, fn($c) => ($c['step_id'] ?? null) == $stepId); + } + + // 최신순 정렬 + usort($consultations, fn($a, $b) => strtotime($b['created_at']) - strtotime($a['created_at'])); + + return view('sales.modals.consultation-log', [ + 'tenant' => $tenant, + 'consultations' => $consultations, + 'scenarioType' => $scenarioType, + 'stepId' => $stepId, + ]); + } + + /** + * 텍스트 상담 기록 저장 + */ + public function store(Request $request): JsonResponse + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + 'step_id' => 'nullable|integer', + 'content' => 'required|string|max:5000', + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + $stepId = $request->input('step_id'); + $content = $request->input('content'); + + // 캐시 키 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 새 상담 기록 추가 + $consultation = [ + 'id' => uniqid('cons_'), + 'type' => 'text', + 'content' => $content, + 'step_id' => $stepId, + 'created_by' => auth()->id(), + 'created_by_name' => auth()->user()->name, + 'created_at' => now()->toDateTimeString(), + ]; + + $consultations[] = $consultation; + + // 캐시에 저장 (90일 유지) + cache()->put($cacheKey, $consultations, now()->addDays(90)); + + return response()->json([ + 'success' => true, + 'consultation' => $consultation, + ]); + } + + /** + * 상담 기록 삭제 + */ + public function destroy(string $consultationId, Request $request): JsonResponse + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + + // 캐시 키 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 상담 기록 찾기 및 삭제 + $found = false; + foreach ($consultations as $index => $consultation) { + if ($consultation['id'] === $consultationId) { + // 파일이 있으면 삭제 + if (isset($consultation['file_path'])) { + Storage::delete($consultation['file_path']); + } + unset($consultations[$index]); + $found = true; + break; + } + } + + if (!$found) { + return response()->json([ + 'success' => false, + 'message' => '상담 기록을 찾을 수 없습니다.', + ], 404); + } + + // 캐시에 저장 + cache()->put($cacheKey, array_values($consultations), now()->addDays(90)); + + return response()->json([ + 'success' => true, + ]); + } + + /** + * 음성 파일 업로드 + */ + public function uploadAudio(Request $request): JsonResponse + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + 'step_id' => 'nullable|integer', + 'audio' => 'required|file|mimes:webm,mp3,wav,ogg|max:51200', // 50MB + 'transcript' => 'nullable|string|max:10000', + 'duration' => 'nullable|integer', + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + $stepId = $request->input('step_id'); + $transcript = $request->input('transcript'); + $duration = $request->input('duration'); + + // 파일 저장 + $file = $request->file('audio'); + $fileName = 'audio_' . now()->format('Ymd_His') . '_' . uniqid() . '.' . $file->getClientOriginalExtension(); + $path = $file->storeAs("tenant/consultations/{$tenantId}", $fileName, 'local'); + + // 캐시 키 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 새 상담 기록 추가 + $consultation = [ + 'id' => uniqid('cons_'), + 'type' => 'audio', + 'file_path' => $path, + 'file_name' => $fileName, + 'transcript' => $transcript, + 'duration' => $duration, + 'step_id' => $stepId, + 'created_by' => auth()->id(), + 'created_by_name' => auth()->user()->name, + 'created_at' => now()->toDateTimeString(), + ]; + + $consultations[] = $consultation; + + // 캐시에 저장 + cache()->put($cacheKey, $consultations, now()->addDays(90)); + + return response()->json([ + 'success' => true, + 'consultation' => $consultation, + ]); + } + + /** + * 첨부파일 업로드 + */ + public function uploadFile(Request $request): JsonResponse + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + 'step_id' => 'nullable|integer', + 'file' => 'required|file|max:20480', // 20MB + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + $stepId = $request->input('step_id'); + + // 파일 저장 + $file = $request->file('file'); + $originalName = $file->getClientOriginalName(); + $fileName = now()->format('Ymd_His') . '_' . uniqid() . '_' . $originalName; + $path = $file->storeAs("tenant/attachments/{$tenantId}", $fileName, 'local'); + + // 캐시 키 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 새 상담 기록 추가 + $consultation = [ + 'id' => uniqid('cons_'), + 'type' => 'file', + 'file_path' => $path, + 'file_name' => $originalName, + 'file_size' => $file->getSize(), + 'file_type' => $file->getMimeType(), + 'step_id' => $stepId, + 'created_by' => auth()->id(), + 'created_by_name' => auth()->user()->name, + 'created_at' => now()->toDateTimeString(), + ]; + + $consultations[] = $consultation; + + // 캐시에 저장 + cache()->put($cacheKey, $consultations, now()->addDays(90)); + + return response()->json([ + 'success' => true, + 'consultation' => $consultation, + ]); + } + + /** + * 파일 삭제 + */ + public function deleteFile(string $fileId, Request $request): JsonResponse + { + return $this->destroy($fileId, $request); + } + + /** + * 파일 다운로드 URL 생성 + */ + public function getDownloadUrl(string $consultationId, Request $request): JsonResponse + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + + // 캐시 키 + $cacheKey = "consultations:{$tenantId}:{$scenarioType}"; + $consultations = cache()->get($cacheKey, []); + + // 상담 기록 찾기 + $consultation = collect($consultations)->firstWhere('id', $consultationId); + + if (!$consultation || !isset($consultation['file_path'])) { + return response()->json([ + 'success' => false, + 'message' => '파일을 찾을 수 없습니다.', + ], 404); + } + + // 임시 다운로드 URL 생성 (5분 유효) + $url = Storage::temporaryUrl($consultation['file_path'], now()->addMinutes(5)); + + return response()->json([ + 'success' => true, + 'url' => $url, + ]); + } +} diff --git a/app/Http/Controllers/Sales/SalesDashboardController.php b/app/Http/Controllers/Sales/SalesDashboardController.php index 4b63a661..5e4c41dc 100644 --- a/app/Http/Controllers/Sales/SalesDashboardController.php +++ b/app/Http/Controllers/Sales/SalesDashboardController.php @@ -4,6 +4,8 @@ use App\Http\Controllers\Controller; use App\Models\Tenants\Tenant; +use App\Models\User; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -114,4 +116,69 @@ private function getDashboardData(Request $request): array 'endDate' ); } + + /** + * 매니저 지정 변경 + */ + public function assignManager(int $tenantId, Request $request): JsonResponse + { + $request->validate([ + 'manager_id' => 'required|integer', + ]); + + $tenant = Tenant::findOrFail($tenantId); + $managerId = $request->input('manager_id'); + + // 캐시 키 + $cacheKey = "tenant_manager:{$tenantId}"; + + if ($managerId === 0) { + // 본인으로 설정 (현재 로그인 사용자) + $manager = auth()->user(); + cache()->put($cacheKey, [ + 'id' => $manager->id, + 'name' => $manager->name, + 'is_self' => true, + ], now()->addDays(365)); + } else { + // 특정 매니저 지정 + $manager = User::find($managerId); + if (!$manager) { + return response()->json([ + 'success' => false, + 'message' => '매니저를 찾을 수 없습니다.', + ], 404); + } + + cache()->put($cacheKey, [ + 'id' => $manager->id, + 'name' => $manager->name, + 'is_self' => $manager->id === auth()->id(), + ], now()->addDays(365)); + } + + return response()->json([ + 'success' => true, + 'manager' => [ + 'id' => $manager->id, + 'name' => $manager->name, + ], + ]); + } + + /** + * 매니저 목록 조회 (드롭다운용) + */ + public function getManagers(Request $request): JsonResponse + { + // HQ 테넌트의 사용자 중 매니저 역할이 있는 사용자 조회 + $managers = User::whereHas('tenants', function ($query) { + $query->where('tenant_type', 'HQ'); + })->get(['id', 'name', 'email']); + + return response()->json([ + 'success' => true, + 'managers' => $managers, + ]); + } } diff --git a/app/Http/Controllers/Sales/SalesScenarioController.php b/app/Http/Controllers/Sales/SalesScenarioController.php new file mode 100644 index 00000000..43874a75 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesScenarioController.php @@ -0,0 +1,212 @@ +input('step', 1); + $icons = config('sales_scenario.icons'); + + // 체크리스트 진행 상태 조회 + $progress = $this->getChecklistProgress($tenantId, 'sales'); + + // HTMX 요청이면 단계 콘텐츠만 반환 + if ($request->header('HX-Request') && $request->has('step')) { + return view('sales.modals.scenario-step', [ + 'tenant' => $tenant, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'step' => collect($steps)->firstWhere('id', $currentStep), + 'progress' => $progress, + 'scenarioType' => 'sales', + 'icons' => $icons, + ]); + } + + return view('sales.modals.scenario-modal', [ + 'tenant' => $tenant, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'progress' => $progress, + 'scenarioType' => 'sales', + 'icons' => $icons, + ]); + } + + /** + * 매니저 시나리오 모달 뷰 + */ + public function managerScenario(int $tenantId, Request $request): View|Response + { + $tenant = Tenant::findOrFail($tenantId); + $steps = config('sales_scenario.manager_steps'); + $currentStep = (int) $request->input('step', 1); + $icons = config('sales_scenario.icons'); + + // 체크리스트 진행 상태 조회 + $progress = $this->getChecklistProgress($tenantId, 'manager'); + + // HTMX 요청이면 단계 콘텐츠만 반환 + if ($request->header('HX-Request') && $request->has('step')) { + return view('sales.modals.scenario-step', [ + 'tenant' => $tenant, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'step' => collect($steps)->firstWhere('id', $currentStep), + 'progress' => $progress, + 'scenarioType' => 'manager', + 'icons' => $icons, + ]); + } + + return view('sales.modals.scenario-modal', [ + 'tenant' => $tenant, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'progress' => $progress, + 'scenarioType' => 'manager', + 'icons' => $icons, + ]); + } + + /** + * 체크리스트 항목 토글 (HTMX) + */ + public function toggleChecklist(Request $request): Response + { + $request->validate([ + 'tenant_id' => 'required|integer|exists:tenants,id', + 'scenario_type' => 'required|in:sales,manager', + 'step_id' => 'required|integer', + 'checkpoint_id' => 'required|string', + 'checked' => 'required|boolean', + ]); + + $tenantId = $request->input('tenant_id'); + $scenarioType = $request->input('scenario_type'); + $stepId = $request->input('step_id'); + $checkpointId = $request->input('checkpoint_id'); + $checked = $request->boolean('checked'); + + // 캐시 키 생성 + $cacheKey = "scenario_checklist:{$tenantId}:{$scenarioType}"; + + // 현재 체크리스트 상태 조회 + $checklist = cache()->get($cacheKey, []); + + // 체크리스트 상태 업데이트 + $key = "{$stepId}_{$checkpointId}"; + if ($checked) { + $checklist[$key] = [ + 'checked_at' => now()->toDateTimeString(), + 'checked_by' => auth()->id(), + ]; + } else { + unset($checklist[$key]); + } + + // 캐시에 저장 (30일 유지) + cache()->put($cacheKey, $checklist, now()->addDays(30)); + + // 진행률 계산 + $progress = $this->calculateProgress($tenantId, $scenarioType); + + return response()->json([ + 'success' => true, + 'progress' => $progress, + 'checked' => $checked, + ]); + } + + /** + * 진행률 조회 + */ + public function getProgress(int $tenantId, string $type): Response + { + $progress = $this->calculateProgress($tenantId, $type); + + return response()->json([ + 'success' => true, + 'progress' => $progress, + ]); + } + + /** + * 체크리스트 진행 상태 조회 + */ + private function getChecklistProgress(int $tenantId, string $scenarioType): array + { + $cacheKey = "scenario_checklist:{$tenantId}:{$scenarioType}"; + + return cache()->get($cacheKey, []); + } + + /** + * 진행률 계산 + */ + private function calculateProgress(int $tenantId, string $scenarioType): array + { + $steps = config("sales_scenario.{$scenarioType}_steps"); + $checklist = $this->getChecklistProgress($tenantId, $scenarioType); + + $totalCheckpoints = 0; + $completedCheckpoints = 0; + $stepProgress = []; + + foreach ($steps as $step) { + $stepCompleted = 0; + $stepTotal = count($step['checkpoints']); + $totalCheckpoints += $stepTotal; + + foreach ($step['checkpoints'] as $checkpoint) { + $key = "{$step['id']}_{$checkpoint['id']}"; + if (isset($checklist[$key])) { + $completedCheckpoints++; + $stepCompleted++; + } + } + + $stepProgress[$step['id']] = [ + 'total' => $stepTotal, + 'completed' => $stepCompleted, + 'percentage' => $stepTotal > 0 ? round(($stepCompleted / $stepTotal) * 100) : 0, + ]; + } + + return [ + 'total' => $totalCheckpoints, + 'completed' => $completedCheckpoints, + 'percentage' => $totalCheckpoints > 0 ? round(($completedCheckpoints / $totalCheckpoints) * 100) : 0, + 'steps' => $stepProgress, + ]; + } + + /** + * 특정 단계의 체크포인트 체크 여부 확인 + */ + public function isCheckpointChecked(int $tenantId, string $scenarioType, int $stepId, string $checkpointId): bool + { + $checklist = $this->getChecklistProgress($tenantId, $scenarioType); + $key = "{$stepId}_{$checkpointId}"; + + return isset($checklist[$key]); + } +} diff --git a/config/sales_scenario.php b/config/sales_scenario.php new file mode 100644 index 00000000..dd51bfcd --- /dev/null +++ b/config/sales_scenario.php @@ -0,0 +1,447 @@ + [ + [ + 'id' => 1, + 'title' => '사전 준비', + 'subtitle' => 'Preparation', + 'icon' => 'search', + 'color' => 'blue', + 'bg_class' => 'bg-blue-100', + 'text_class' => 'text-blue-600', + 'description' => '고객사를 만나기 전, 철저한 분석을 통해 성공 확률을 높이는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'prep_1', + 'title' => '고객사 심층 분석', + 'detail' => '홈페이지, 뉴스 등을 통해 이슈와 비전을 파악하세요.', + 'pro_tip' => '직원들의 불만 사항을 미리 파악하면 미팅 시 강력한 무기가 됩니다.', + ], + [ + 'id' => 'prep_2', + 'title' => '재무 건전성 확인', + 'detail' => '매출액, 영업이익 추이를 확인하고 IT 투자 여력을 가늠해보세요.', + 'pro_tip' => '성장 추세라면 \'확장성\'과 \'관리 효율\'을 강조하세요.', + ], + [ + 'id' => 'prep_3', + 'title' => '경쟁사 및 시장 동향', + 'detail' => '핵심 기능에 집중하여 도입 속도가 빠르다는 점을 정리하세요.', + 'pro_tip' => '경쟁사를 비방하기보다 차별화된 가치를 제시하세요.', + ], + [ + 'id' => 'prep_4', + 'title' => '가설 수립 (Hypothesis)', + 'detail' => '구체적인 페인포인트 가설을 세우고 질문을 준비하세요.', + 'pro_tip' => '\'만약 ~하다면\' 화법으로 고객의 \'Yes\'를 유도하세요.', + ], + ], + ], + [ + 'id' => 2, + 'title' => '접근 및 탐색', + 'subtitle' => 'Approach', + 'icon' => 'phone', + 'color' => 'indigo', + 'bg_class' => 'bg-indigo-100', + 'text_class' => 'text-indigo-600', + 'description' => '담당자와의 첫 접점을 만들고, 미팅 기회를 확보하는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'approach_1', + 'title' => 'Key-man 식별 및 컨택', + 'detail' => '실무 책임자(팀장급)와 의사결정권자(임원급) 라인을 파악하세요.', + 'pro_tip' => '전달드릴 자료가 있다고 하여 Gatekeeper를 통과하세요.', + ], + [ + 'id' => 'approach_2', + 'title' => '맞춤형 콜드메일/콜', + 'detail' => '사전 조사 내용을 바탕으로 해결 방안을 제안하세요.', + 'pro_tip' => '제목에 고객사 이름을 넣어 클릭률을 높이세요.', + ], + [ + 'id' => 'approach_3', + 'title' => '미팅 일정 확정', + 'detail' => '인사이트 공유를 목적으로 미팅을 제안하세요.', + 'pro_tip' => '두 가지 시간대를 제시하여 양자택일을 유도하세요.', + ], + ], + ], + [ + 'id' => 3, + 'title' => '현장 진단', + 'subtitle' => 'Diagnosis', + 'icon' => 'clipboard-check', + 'color' => 'purple', + 'bg_class' => 'bg-purple-100', + 'text_class' => 'text-purple-600', + 'description' => '고객의 업무 현장을 직접 확인하고 진짜 문제를 찾아내는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'diag_1', + 'title' => 'AS-IS 프로세스 맵핑', + 'detail' => '고객과 함께 업무 흐름도를 그리며 병목을 찾으세요.', + 'pro_tip' => '고객 스스로 문제를 깨닫게 하는 것이 가장 효과적입니다.', + ], + [ + 'id' => 'diag_2', + 'title' => '비효율/리스크 식별', + 'detail' => '데이터 누락, 중복 입력 등 리스크를 수치화하세요.', + 'pro_tip' => '불편함을 시간과 비용으로 환산하여 설명하세요.', + ], + [ + 'id' => 'diag_3', + 'title' => 'To-Be 이미지 스케치', + 'detail' => '도입 후 업무가 어떻게 간소화될지 시각화하세요.', + 'pro_tip' => '비포/애프터의 극명한 차이를 보여주세요.', + ], + ], + ], + [ + 'id' => 4, + 'title' => '솔루션 제안', + 'subtitle' => 'Proposal', + 'icon' => 'presentation', + 'color' => 'pink', + 'bg_class' => 'bg-pink-100', + 'text_class' => 'text-pink-600', + 'description' => 'SAM을 통해 고객의 문제를 어떻게 해결할 수 있는지 증명하는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'proposal_1', + 'title' => '맞춤형 데모 시연', + 'detail' => '핵심 기능을 위주로 고객사 데이터를 넣어 시연하세요.', + 'pro_tip' => '고객사 로고를 넣어 \'이미 우리 것\'이라는 느낌을 주세요.', + ], + [ + 'id' => 'proposal_2', + 'title' => 'ROI 분석 보고서', + 'detail' => '비용 대비 절감 가능한 수치를 산출하여 증명하세요.', + 'pro_tip' => '보수적인 ROI가 훨씬 더 높은 신뢰를 줍니다.', + ], + [ + 'id' => 'proposal_3', + 'title' => '단계별 도입 로드맵', + 'detail' => '부담을 줄이기 위해 단계적 확산 방안을 제시하세요.', + 'pro_tip' => '1단계는 핵심 문제 해결에만 집중하세요.', + ], + ], + ], + [ + 'id' => 5, + 'title' => '협상 및 조율', + 'subtitle' => 'Negotiation', + 'icon' => 'scale', + 'color' => 'orange', + 'bg_class' => 'bg-orange-100', + 'text_class' => 'text-orange-600', + 'description' => '도입을 가로막는 장애물을 제거하고 조건을 합의하는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'nego_1', + 'title' => '가격/조건 협상', + 'detail' => '할인 대신 범위나 기간 조정 등으로 합의하세요.', + 'pro_tip' => 'Give & Take 원칙을 지키며 기대를 관리하세요.', + ], + [ + 'id' => 'nego_2', + 'title' => '의사결정권자 설득', + 'detail' => 'CEO/CFO의 관심사에 맞는 보고용 장표를 제공하세요.', + 'pro_tip' => '실무자가 내부 보고 사업을 잘하게 돕는 것이 핵심입니다.', + ], + ], + ], + [ + 'id' => 6, + 'title' => '계약 체결', + 'subtitle' => 'Closing', + 'icon' => 'file-signature', + 'color' => 'green', + 'bg_class' => 'bg-green-100', + 'text_class' => 'text-green-600', + 'description' => '공식적인 파트너십을 맺고 법적 효력을 발생시키는 단계입니다.', + 'checkpoints' => [ + [ + 'id' => 'close_1', + 'title' => '계약서 날인 및 교부', + 'detail' => '전자계약 등을 통해 체결 시간을 단축하세요.', + 'pro_tip' => '원본은 항상 안전하게 보관하고 백업하세요.', + ], + [ + 'id' => 'close_2', + 'title' => '세금계산서 발행', + 'detail' => '정확한 수금 일정을 확인하고 발행하세요.', + 'pro_tip' => '가입비 입금이 완료되어야 다음 단계가 시작됩니다.', + ], + [ + 'id' => 'close_3', + 'title' => '계약 완료 (확정)', + 'detail' => '축하 인사를 전하고 후속 지원 일정을 잡으세요.', + 'pro_tip' => '계약은 진정한 서비스의 시작임을 강조하세요.', + ], + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | 매니저 시나리오 단계 (MANAGER_SCENARIO_STEPS) + |-------------------------------------------------------------------------- + | 매니저가 프로젝트를 인수받아 착수하기까지의 6단계 프로세스 + */ + 'manager_steps' => [ + [ + 'id' => 1, + 'title' => '영업 이관', + 'subtitle' => 'Handover', + 'icon' => 'arrow-right-left', + 'color' => 'blue', + 'bg_class' => 'bg-blue-100', + 'text_class' => 'text-blue-600', + 'description' => '영업팀으로부터 고객 정보를 전달받고, 프로젝트의 배경과 핵심 요구사항을 파악하는 단계입니다.', + 'tips' => '잘못된 시작은 엉뚱한 결말을 낳습니다. 영업팀의 약속을 검증하세요.', + 'checkpoints' => [ + [ + 'id' => 'handover_1', + 'title' => '영업 히스토리 리뷰', + 'detail' => '영업 담당자가 작성한 미팅록, 고객의 페인포인트, 예산 범위, 예상 일정 등을 꼼꼼히 확인하세요.', + 'pro_tip' => '영업 담당자에게 \'고객이 가장 꽂힌 포인트\'가 무엇인지 꼭 물어보세요. 그게 프로젝트의 CSF입니다.', + ], + [ + 'id' => 'handover_2', + 'title' => '고객사 기본 정보 파악', + 'detail' => '고객사의 업종, 규모, 주요 경쟁사 등을 파악하여 커뮤니케이션 톤앤매너를 준비하세요.', + 'pro_tip' => 'IT 지식이 부족한 고객이라면 전문 용어 사용을 자제하고 쉬운 비유를 준비해야 합니다.', + ], + [ + 'id' => 'handover_3', + 'title' => 'RFP/요구사항 문서 분석', + 'detail' => '고객이 전달한 요구사항 문서(RFP 등)가 있다면 기술적으로 실현 가능한지 1차 검토하세요.', + 'pro_tip' => '모호한 문장을 찾아내어 구체적인 수치나 기능으로 정의할 준비를 하세요.', + ], + [ + 'id' => 'handover_4', + 'title' => '내부 킥오프 (영업-매니저)', + 'detail' => '영업팀과 함께 프로젝트의 리스크 요인(까다로운 담당자 등)을 사전에 공유받으세요.', + 'pro_tip' => '영업 단계에서 \'무리하게 약속한 기능\'이 있는지 반드시 체크해야 합니다.', + ], + ], + ], + [ + 'id' => 2, + 'title' => '요구사항 파악', + 'subtitle' => 'Requirements', + 'icon' => 'search', + 'color' => 'indigo', + 'bg_class' => 'bg-indigo-100', + 'text_class' => 'text-indigo-600', + 'description' => '고객과 직접 만나 구체적인 니즈를 청취하고, 숨겨진 요구사항까지 발굴하는 단계입니다.', + 'tips' => '고객은 자기가 뭘 원하는지 모를 때가 많습니다. 질문으로 답을 찾아주세요.', + 'checkpoints' => [ + [ + 'id' => 'req_1', + 'title' => '고객 인터뷰 및 실사', + 'detail' => '현업 담당자를 만나 실제 업무 프로세스를 확인하고 시스템이 필요한 진짜 이유를 찾으세요.', + 'pro_tip' => '\'왜 이 기능이 필요하세요?\'라고 3번 물어보세요(5 Whys). 목적을 찾아야 합니다.', + ], + [ + 'id' => 'req_2', + 'title' => '요구사항 구체화 (Scope)', + 'detail' => '고객의 요구사항을 기능 단위로 쪼개고 우선순위(Must/Should/Could)를 매기세요.', + 'pro_tip' => '\'오픈 시점에 반드시 필요한 기능\'과 \'추후 고도화할 기능\'을 명확히 구분해 주세요.', + ], + [ + 'id' => 'req_3', + 'title' => '제약 사항 확인', + 'detail' => '예산, 일정, 레거시 시스템 연동, 보안 규정 등 프로젝트의 제약 조건을 명확히 하세요.', + 'pro_tip' => '특히 \'데이터 이관\' 이슈를 조심하세요. 엑셀 데이터가 엉망인 경우가 태반입니다.', + ], + [ + 'id' => 'req_4', + 'title' => '유사 레퍼런스 제시', + 'detail' => '비슷한 고민을 했던 다른 고객사의 해결 사례를 보여주며 제안하는 방향의 신뢰를 얻으세요.', + 'pro_tip' => '\'A사도 이렇게 푸셨습니다\'라는 한마디가 백 마디 설명보다 강력합니다.', + ], + ], + ], + [ + 'id' => 3, + 'title' => '개발자 협의', + 'subtitle' => 'Dev Consult', + 'icon' => 'code', + 'color' => 'purple', + 'bg_class' => 'bg-purple-100', + 'text_class' => 'text-purple-600', + 'description' => '파악된 요구사항을 개발팀에 전달하고 기술적 실현 가능성과 공수를 산정합니다.', + 'tips' => '개발자는 \'기능\'을 만들지만, 매니저는 \'가치\'를 만듭니다. 통역사가 되어주세요.', + 'checkpoints' => [ + [ + 'id' => 'dev_1', + 'title' => '요구사항 기술 검토', + 'detail' => '개발 리드와 함께 고객의 요구사항이 기술적으로 구현 가능한지 검토하세요.', + 'pro_tip' => '개발자가 \'안 돼요\'라고 하면 \'왜 안 되는지\', \'대안은 무엇인지\'를 반드시 물어보세요.', + ], + [ + 'id' => 'dev_2', + 'title' => '공수 산정 (Estimation)', + 'detail' => '기능별 개발 예상 시간(M/M)을 산출하고 필요한 리소스를 파악하세요.', + 'pro_tip' => '개발 공수는 항상 버퍼(Buffer)를 20% 정도 두세요. 버그나 스펙 변경은 반드시 일어납니다.', + ], + [ + 'id' => 'dev_3', + 'title' => '아키텍처/스택 선정', + 'detail' => '프로젝트에 적합한 기술 스택과 시스템 아키텍처를 확정하세요.', + 'pro_tip' => '최신 기술보다 유지보수 용이성과 개발팀의 숙련도를 최우선으로 고려하세요.', + ], + [ + 'id' => 'dev_4', + 'title' => '리스크 식별 및 대안 수립', + 'detail' => '기술적 난이도가 높은 기능 등 리스크를 식별하고 대안(Plan B)을 마련하세요.', + 'pro_tip' => '리스크는 감추지 말고 공유해야 합니다. 미리 말하면 관리입니다.', + ], + ], + ], + [ + 'id' => 4, + 'title' => '제안 및 견적', + 'subtitle' => 'Proposal', + 'icon' => 'file-text', + 'color' => 'pink', + 'bg_class' => 'bg-pink-100', + 'text_class' => 'text-pink-600', + 'description' => '개발팀 검토 내용을 바탕으로 수행 계획서(SOW)와 견적서를 작성하여 제안합니다.', + 'tips' => '견적서는 숫자가 아니라 \'신뢰\'를 담아야 합니다.', + 'checkpoints' => [ + [ + 'id' => 'prop_1', + 'title' => 'WBS 및 일정 계획 수립', + 'detail' => '분석/설계/개발/테스트/오픈 등 단계별 상세 일정을 수립하세요.', + 'pro_tip' => '고객의 검수(UAT) 기간을 충분히 잡으세요. 고객은 생각보다 바빠서 피드백이 늦어집니다.', + ], + [ + 'id' => 'prop_2', + 'title' => '견적서(Quotation) 작성', + 'detail' => '개발 공수, 솔루션 비용, 인프라 비용 등을 포함한 상세 견적서를 작성하세요.', + 'pro_tip' => '\'기능별 상세 견적\'을 제공하면 신뢰도가 높아지고 네고 방어에도 유리합니다.', + ], + [ + 'id' => 'prop_3', + 'title' => '제안서(SOW) 작성', + 'detail' => '범위(Scope), 수행 방법론, 산출물 목록 등을 명시한 제안서를 작성하세요.', + 'pro_tip' => '\'제외 범위(Out of Scope)\'를 명확히 적으세요. 나중에 딴소리 듣지 않으려면요.', + ], + [ + 'id' => 'prop_4', + 'title' => '제안 발표 (PT)', + 'detail' => '고객에게 제안 내용을 설명하고 우리가 가장 적임자임을 설득하세요.', + 'pro_tip' => '발표 자료는 \'고객의 언어\'로 작성하세요. 기술 용어 남발은 금물입니다.', + ], + ], + ], + [ + 'id' => 5, + 'title' => '조율 및 협상', + 'subtitle' => 'Negotiation', + 'icon' => 'scale', + 'color' => 'orange', + 'bg_class' => 'bg-orange-100', + 'text_class' => 'text-orange-600', + 'description' => '제안 내용을 바탕으로 고객과 범위, 일정, 비용을 최종 조율하는 단계입니다.', + 'tips' => '협상은 이기는 게 아니라, 같이 갈 수 있는 길을 찾는 것입니다.', + 'checkpoints' => [ + [ + 'id' => 'nego_m_1', + 'title' => '범위 및 일정 조정', + 'detail' => '예산이나 일정에 맞춰 기능을 가감하거나 단계별 오픈 전략을 협의하세요.', + 'pro_tip' => '무리한 일정 단축은 단호하게 거절하되, \'선오픈\'과 같은 대안을 제시하세요.', + ], + [ + 'id' => 'nego_m_2', + 'title' => '추가 요구사항 대응', + 'detail' => '제안 과정에서 나온 추가 요구사항에 대해 비용 청구 여부를 결정하세요.', + 'pro_tip' => '서비스로 해주더라도 \'원래 얼마짜리인데 이번만 하는 것\'이라고 인지시키세요.', + ], + [ + 'id' => 'nego_m_3', + 'title' => 'R&R 명확화', + 'detail' => '우리 회사와 고객사가 각각 해야 할 역할을 명문화하세요.', + 'pro_tip' => '프로젝트 지연의 절반은 고객의 자료 전달 지연입니다. 숙제를 명확히 알려주세요.', + ], + [ + 'id' => 'nego_m_4', + 'title' => '최종 합의 도출', + 'detail' => '모든 쟁점 사항을 정리하고 최종 합의된 내용을 문서로 남기세요.', + 'pro_tip' => '구두 합의는 힘이 없습니다. 반드시 이메일이나 회의록으로 남기세요.', + ], + ], + ], + [ + 'id' => 6, + 'title' => '착수 및 계약', + 'subtitle' => 'Kickoff', + 'icon' => 'flag', + 'color' => 'green', + 'bg_class' => 'bg-green-100', + 'text_class' => 'text-green-600', + 'description' => '계약을 체결하고 프로젝트를 공식적으로 시작하는 단계입니다.', + 'tips' => '시작이 좋아야 끝도 좋습니다. 룰을 명확히 세우세요.', + 'checkpoints' => [ + [ + 'id' => 'kick_1', + 'title' => '계약서 검토 및 날인', + 'detail' => '과업지시서, 기술협약서 등 계약 부속 서류를 꼼꼼히 챙기세요.', + 'pro_tip' => '계약서에 \'검수 조건\'을 명확히 넣으세요. 실현 가능한 조건이어야 합니다.', + ], + [ + 'id' => 'kick_2', + 'title' => '프로젝트 팀 구성', + 'detail' => '수행 인력을 확정하고 내부 킥오프를 진행하세요.', + 'pro_tip' => '팀원들에게 프로젝트 배경뿐만 아니라 \'고객의 성향\'도 공유해 주세요.', + ], + [ + 'id' => 'kick_3', + 'title' => '착수 보고회 (Kick-off)', + 'detail' => '전원이 모여 프로젝트의 목표, 일정, 커뮤니케이션 룰을 공유하세요.', + 'pro_tip' => '첫인상이 전문적이어야 프로젝트가 순탄합니다. 깔끔하게 준비하세요.', + ], + [ + 'id' => 'kick_4', + 'title' => '협업 도구 세팅', + 'detail' => 'Jira, Slack 등 협업 도구를 세팅하고 고객을 초대하세요.', + 'pro_tip' => '소통 채널 단일화가 성공의 열쇠입니다. 간단 가이드를 제공하세요.', + ], + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | 아이콘 매핑 (Heroicons SVG) + |-------------------------------------------------------------------------- + */ + 'icons' => [ + 'search' => '', + 'phone' => '', + 'clipboard-check' => '', + 'presentation' => '', + 'scale' => '', + 'file-signature' => '', + 'arrow-right-left' => '', + 'code' => '', + 'file-text' => '', + 'flag' => '', + ], +]; diff --git a/resources/views/sales/dashboard/index.blade.php b/resources/views/sales/dashboard/index.blade.php index b6b7cb4f..266f22d1 100644 --- a/resources/views/sales/dashboard/index.blade.php +++ b/resources/views/sales/dashboard/index.blade.php @@ -43,4 +43,30 @@ @include('sales.dashboard.partials.data-container') + +{{-- 시나리오 모달용 포털 --}} + @endsection + +@push('scripts') + +@endpush diff --git a/resources/views/sales/dashboard/partials/manager-dropdown.blade.php b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php new file mode 100644 index 00000000..d2435751 --- /dev/null +++ b/resources/views/sales/dashboard/partials/manager-dropdown.blade.php @@ -0,0 +1,170 @@ +{{-- 매니저 드롭다운 컴포넌트 --}} +@php + $cacheKey = "tenant_manager:{$tenant->id}"; + $assignedManager = cache()->get($cacheKey); + $isSelf = !$assignedManager || ($assignedManager['is_self'] ?? true); + $managerName = $assignedManager['name'] ?? '본인'; +@endphp + +
+ {{-- 드롭다운 트리거 --}} + + + {{-- 드롭다운 메뉴 --}} +
+ {{-- 로딩 상태 --}} +
+ + + + +
+ + {{-- 매니저 목록 --}} +
+ {{-- 본인 옵션 --}} + + + {{-- 구분선 --}} +
+ + {{-- 다른 매니저 목록 --}} + + + {{-- 매니저가 없을 때 --}} +
+ 등록된 매니저가 없습니다. +
+
+
+
+ + diff --git a/resources/views/sales/dashboard/partials/tenant-list.blade.php b/resources/views/sales/dashboard/partials/tenant-list.blade.php index 9d7f583f..000d1997 100644 --- a/resources/views/sales/dashboard/partials/tenant-list.blade.php +++ b/resources/views/sales/dashboard/partials/tenant-list.blade.php @@ -1,5 +1,5 @@ {{-- 테넌트 및 계약 관리 --}} -
+
@@ -53,15 +53,8 @@ 영업: {{ auth()->user()->name }} -
- - - - - - 관리: 본인 - -
+ {{-- 매니저 드롭다운 --}} + @include('sales.dashboard.partials.manager-dropdown', ['tenant' => $tenant])
@@ -71,13 +64,16 @@
- + {{-- 영업 진행 버튼 (시나리오 모달 열기) --}} + + @@ -86,13 +82,16 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medi 상세계약 설정 - + + {{-- 매니저 진행 버튼 (시나리오 모달 열기) --}} +
@@ -103,7 +102,7 @@ class="p-1.5 text-gray-400 hover:text-blue-600 transition-colors" title="수정"
@endif + + {{-- 시나리오 모달 컨테이너 --}} +
diff --git a/resources/views/sales/modals/consultation-log.blade.php b/resources/views/sales/modals/consultation-log.blade.php new file mode 100644 index 00000000..0e8bdb7b --- /dev/null +++ b/resources/views/sales/modals/consultation-log.blade.php @@ -0,0 +1,201 @@ +{{-- 상담 기록 컴포넌트 --}} +
+ {{-- 상담 기록 입력 --}} +
+

상담 기록 추가

+
+ +
+ +
+
+
+ + {{-- 상담 기록 목록 --}} +
+

상담 기록 ({{ count($consultations) }}건)

+ + @if(empty($consultations)) +
+ + + +

아직 상담 기록이 없습니다.

+
+ @else +
+ @foreach($consultations as $consultation) +
+ {{-- 삭제 버튼 --}} + + + {{-- 콘텐츠 --}} +
+ {{-- 타입 아이콘 --}} +
+ @if($consultation['type'] === 'text') + + + + @elseif($consultation['type'] === 'audio') + + + + @else + + + + @endif +
+ +
+ @if($consultation['type'] === 'text') +

{{ $consultation['content'] }}

+ @elseif($consultation['type'] === 'audio') +
+
+ 음성 녹음 + @if(isset($consultation['duration'])) + + {{ floor($consultation['duration'] / 60) }}:{{ str_pad($consultation['duration'] % 60, 2, '0', STR_PAD_LEFT) }} + + @endif +
+ @if(isset($consultation['transcript']) && $consultation['transcript']) +

"{{ $consultation['transcript'] }}"

+ @endif +
+ @else +
+ {{ $consultation['file_name'] }} + + {{ number_format(($consultation['file_size'] ?? 0) / 1024, 1) }} KB + +
+ @endif + + {{-- 메타 정보 --}} +
+ {{ $consultation['created_by_name'] }} + | + {{ \Carbon\Carbon::parse($consultation['created_at'])->format('Y-m-d H:i') }} +
+
+
+
+ @endforeach +
+ @endif +
+
+ + diff --git a/resources/views/sales/modals/file-uploader.blade.php b/resources/views/sales/modals/file-uploader.blade.php new file mode 100644 index 00000000..60fd4175 --- /dev/null +++ b/resources/views/sales/modals/file-uploader.blade.php @@ -0,0 +1,260 @@ +{{-- 첨부파일 업로드 컴포넌트 --}} +
+

+ + + + 첨부파일 +

+ + {{-- Drag & Drop 영역 --}} +
+ + + + + + +

+ 파일을 여기에 드래그하거나 클릭하여 선택 +

+

+ 최대 20MB / PDF, 문서, 이미지, 압축파일 지원 +

+
+ + {{-- 업로드 대기 파일 목록 --}} +
+
업로드 대기
+ + + {{-- 업로드 버튼 --}} +
+ +
+
+
+ + diff --git a/resources/views/sales/modals/scenario-modal.blade.php b/resources/views/sales/modals/scenario-modal.blade.php new file mode 100644 index 00000000..76ca4df1 --- /dev/null +++ b/resources/views/sales/modals/scenario-modal.blade.php @@ -0,0 +1,185 @@ +{{-- 영업/매니저 시나리오 모달 --}} +
+ + {{-- 배경 오버레이 --}} +
+ + {{-- 모달 컨테이너 --}} +
+
+ + {{-- 모달 헤더 --}} +
+
+
+ @if($scenarioType === 'sales') + + + + @else + + + + @endif +
+
+

+ {{ $scenarioType === 'sales' ? '영업 전략 시나리오' : '매니저 상담 프로세스' }} +

+

{{ $tenant->company_name }}

+
+
+
+ {{-- 전체 진행률 --}} +
+ 진행률 + {{ $progress['percentage'] }}% +
+ {{-- 닫기 버튼 --}} + +
+
+ + {{-- 모달 바디 --}} +
+ {{-- 좌측 사이드바: 단계 네비게이션 --}} +
+
+

단계별 진행

+ +
+
+ + {{-- 우측 메인 영역: 단계별 콘텐츠 --}} +
+
+ @include('sales.modals.scenario-step', [ + 'step' => collect($steps)->firstWhere('id', $currentStep), + 'tenant' => $tenant, + 'scenarioType' => $scenarioType, + 'progress' => $progress, + 'icons' => $icons, + ]) +
+
+
+
+
+
+ +@push('scripts') + +@endpush diff --git a/resources/views/sales/modals/scenario-step.blade.php b/resources/views/sales/modals/scenario-step.blade.php new file mode 100644 index 00000000..3cefe0b5 --- /dev/null +++ b/resources/views/sales/modals/scenario-step.blade.php @@ -0,0 +1,184 @@ +{{-- 시나리오 단계별 체크리스트 --}} +@php + $step = $step ?? collect($steps)->firstWhere('id', $currentStep ?? 1); + $checkedItems = $progress[$step['id']] ?? []; +@endphp + +
+ {{-- 단계 헤더 --}} +
+
+ + {!! $icons[$step['icon']] ?? '' !!} + +
+
+
+ STEP {{ $step['id'] }} + {{ $step['subtitle'] }} +
+

{{ $step['title'] }}

+

{{ $step['description'] }}

+
+
+ + {{-- 매니저용 팁 (있는 경우) --}} + @if(isset($step['tips'])) +
+
+ + + +
+

매니저 TIP

+

{{ $step['tips'] }}

+
+
+
+ @endif + + {{-- 체크포인트 목록 --}} +
+ @foreach($step['checkpoints'] as $checkpoint) + @php + $checkKey = "{$step['id']}_{$checkpoint['id']}"; + $isChecked = isset($progress[$checkKey]); + @endphp +
+ + {{-- 체크포인트 헤더 --}} +
+ {{-- 체크박스 --}} + + + {{-- 제목 및 설명 --}} +
+

+ {{ $checkpoint['title'] }} +

+

{{ $checkpoint['detail'] }}

+
+ + {{-- 확장 아이콘 --}} + + + +
+ + {{-- 확장 콘텐츠 --}} +
+
+ {{-- 상세 설명 --}} +
+
상세 설명
+

{{ $checkpoint['detail'] }}

+
+ + {{-- PRO TIP --}} +
+
+
+ + + +
+
+

PRO TIP

+

{{ $checkpoint['pro_tip'] }}

+
+
+
+
+
+
+ @endforeach +
+ + {{-- 하단: 상담 기록 및 파일 영역 (마지막 단계에서만) --}} + @if($step['id'] === 6) +
+

상담 기록 및 첨부파일

+ + {{-- 상담 기록 --}} +
+
+
+
+
+
+
+
+
+
+
+ + {{-- 음성 녹음 --}} +
+ @include('sales.modals.voice-recorder', [ + 'tenant' => $tenant, + 'scenarioType' => $scenarioType, + 'stepId' => $step['id'], + ]) +
+ + {{-- 첨부파일 업로드 --}} +
+ @include('sales.modals.file-uploader', [ + 'tenant' => $tenant, + 'scenarioType' => $scenarioType, + 'stepId' => $step['id'], + ]) +
+
+ @endif + + {{-- 단계 이동 버튼 --}} +
+ @if($step['id'] > 1) + + @else +
+ @endif + + @if($step['id'] < count($steps)) + + @else + + @endif +
+
diff --git a/resources/views/sales/modals/voice-recorder.blade.php b/resources/views/sales/modals/voice-recorder.blade.php new file mode 100644 index 00000000..f6c1041a --- /dev/null +++ b/resources/views/sales/modals/voice-recorder.blade.php @@ -0,0 +1,363 @@ +{{-- 음성 녹음 컴포넌트 --}} +
+

+ + + + 음성 녹음 +

+ + {{-- 녹음 컨트롤 --}} +
+ {{-- 파형 시각화 --}} +
+ + {{-- 타이머 오버레이 --}} +
+ + 00:00 +
+
+ + {{-- 실시간 텍스트 변환 표시 --}} +
+

음성 인식 결과

+

+ + +

+
+ + {{-- 컨트롤 버튼 --}} +
+ {{-- 녹음 시작/중지 버튼 --}} + + + {{-- 저장 버튼 (녹음 완료 후) --}} + + + {{-- 취소 버튼 (녹음 완료 후) --}} + +
+ + {{-- 상태 메시지 --}} +

+ + {{-- 저장된 녹음 목록 안내 --}} +

+ 녹음 파일은 상담 기록에 자동으로 저장됩니다. +

+
+
+ + diff --git a/routes/web.php b/routes/web.php index a26ea93d..b1dc8bd2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -791,4 +791,25 @@ // 영업 실적 관리 Route::resource('records', \App\Http\Controllers\Sales\SalesRecordController::class); + + // 영업 시나리오 관리 + Route::prefix('scenarios')->name('scenarios.')->group(function () { + Route::get('/{tenant}/sales', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'salesScenario'])->name('sales'); + Route::get('/{tenant}/manager', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'managerScenario'])->name('manager'); + Route::post('/checklist/toggle', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'toggleChecklist'])->name('checklist.toggle'); + Route::get('/{tenant}/{type}/progress', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'getProgress'])->name('progress'); + }); + + // 상담 기록 관리 + Route::prefix('consultations')->name('consultations.')->group(function () { + Route::get('/{tenant}', [\App\Http\Controllers\Sales\ConsultationController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Sales\ConsultationController::class, 'store'])->name('store'); + Route::delete('/{consultation}', [\App\Http\Controllers\Sales\ConsultationController::class, 'destroy'])->name('destroy'); + Route::post('/upload-audio', [\App\Http\Controllers\Sales\ConsultationController::class, 'uploadAudio'])->name('upload-audio'); + Route::post('/upload-file', [\App\Http\Controllers\Sales\ConsultationController::class, 'uploadFile'])->name('upload-file'); + Route::delete('/file/{file}', [\App\Http\Controllers\Sales\ConsultationController::class, 'deleteFile'])->name('delete-file'); + }); + + // 매니저 지정 변경 + Route::post('/tenants/{tenant}/assign-manager', [\App\Http\Controllers\Sales\SalesDashboardController::class, 'assignManager'])->name('tenants.assign-manager'); });