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' => '
아직 상담 기록이 없습니다.
+{{ $consultation['content'] }}
+ @elseif($consultation['type'] === 'audio') +"{{ $consultation['transcript'] }}"
+ @endif ++ 파일을 여기에 드래그하거나 클릭하여 선택 +
++ 최대 20MB / PDF, 문서, 이미지, 압축파일 지원 +
+{{ $tenant->company_name }}
+{{ $step['description'] }}
+매니저 TIP
+{{ $step['tips'] }}
+{{ $checkpoint['detail'] }}
+{{ $checkpoint['detail'] }}
+PRO TIP
+{{ $checkpoint['pro_tip'] }}
+음성 인식 결과
++ + +
++ 녹음 파일은 상담 기록에 자동으로 저장됩니다. +
+