2025-12-16 23:36:00 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\SalesScenarioChecklist;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 영업 시나리오 서비스
|
|
|
|
|
*/
|
|
|
|
|
class SalesScenarioService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 영업 시나리오 단계 데이터
|
|
|
|
|
*/
|
|
|
|
|
public static function getScenarioSteps(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
[
|
|
|
|
|
'id' => 1,
|
|
|
|
|
'title' => '사전 준비',
|
|
|
|
|
'subtitle' => 'Preparation',
|
|
|
|
|
'icon' => 'search',
|
|
|
|
|
'color' => 'blue',
|
|
|
|
|
'description' => '고객사를 만나기 전, 철저한 분석을 통해 성공 확률을 높이는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '고객사 심층 분석',
|
|
|
|
|
'detail' => '홈페이지, 뉴스, SNS를 통해 최근 3개월 내의 이슈와 경영진의 신년사/인터뷰를 확인하여 회사의 비전과 당면 과제를 파악하세요.',
|
|
|
|
|
'pro_tip' => '구글 알리미(Google Alerts)에 고객사 키워드를 등록해두세요. 잡플래닛/블라인드 리뷰를 통해 직원들의 불만 사항(야근, 비효율 등)을 미리 파악하면 미팅 시 강력한 무기가 됩니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '재무 건전성 확인',
|
|
|
|
|
'detail' => 'DART 또는 기업정보 사이트에서 최근 3년치 매출액, 영업이익 추이를 확인하고 IT 투자 여력을 가늠해보세요.',
|
|
|
|
|
'pro_tip' => '영업이익이 감소 추세라면 \'비용 절감\'을, 성장 추세라면 \'확장성\'과 \'관리 효율\'을 강조하는 전략을 준비하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '경쟁사 및 시장 동향',
|
|
|
|
|
'detail' => '고객사의 경쟁사가 도입한 솔루션을 파악하고, 우리 솔루션(SAM)이 줄 수 있는 차별화된 가치를 정리하세요.',
|
|
|
|
|
'pro_tip' => '경쟁사를 비방하지 마세요. 대신 \'A사는 기능이 많지만 무겁고, 우리는 핵심 기능에 집중하여 도입 속도가 2배 빠릅니다\'와 같이 구체적인 차별점을 제시하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '가설 수립 (Hypothesis)',
|
|
|
|
|
'detail' => '\'이 회사는 현재 재고 관리의 비효율로 인해 월 000만원의 손실이 발생하고 있을 것이다\'와 같은 구체적인 페인포인트 가설을 세우세요.',
|
|
|
|
|
'pro_tip' => '\'만약 ~하다면\' 화법을 사용하세요. \'현재 엑셀로 관리하신다면, 월말 마감에 3일 이상 소요되실 텐데 맞으신가요?\'라는 질문으로 고객의 \'Yes\'를 유도하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '의사결정 구조 파악',
|
|
|
|
|
'detail' => '조직도를 통해 실무자, 중간 관리자, 최종 의사결정권자(Key Decision Maker)의 라인을 미리 파악하세요.',
|
|
|
|
|
'pro_tip' => '링크드인을 통해 누가 예산 권한을 가지고 있는지 확인하세요. 실무자와의 미팅에서도 \'이 프로젝트의 최종 승인은 누가 하시나요?\'라고 자연스럽게 물어보세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => 'IT 예산 집행 시기 확인',
|
|
|
|
|
'detail' => '고객사의 회계연도와 예산 편성 시기를 파악하여, 제안 타이밍이 적절한지 확인하세요.',
|
|
|
|
|
'pro_tip' => '대부분의 기업은 10~11월에 내년도 예산을 편성합니다. 이 시기를 놓쳤다면 \'잔여 예산\'이나 \'파일럿 프로젝트 예산\'을 공략하세요.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '아는 만큼 보입니다. 고객의 언어로 대화할 준비를 하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 2,
|
|
|
|
|
'title' => '접근 및 탐색',
|
|
|
|
|
'subtitle' => 'Approach',
|
|
|
|
|
'icon' => 'phone-call',
|
|
|
|
|
'color' => 'indigo',
|
|
|
|
|
'description' => '담당자와의 첫 접점을 만들고, 미팅 기회를 확보하는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => 'Key-man 식별 및 컨택',
|
|
|
|
|
'detail' => '링크드인, 로켓펀치 등을 통해 실무 책임자(팀장급)와 의사결정권자(임원급)의 연락처를 확보하세요.',
|
|
|
|
|
'pro_tip' => '대표전화로 전화할 때는 \'영업\'이라고 하지 말고, \'000 이사님께 전달드릴 자료가 있어 연락드렸습니다\'라고 하여 Gatekeeper를 통과하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '맞춤형 콜드메일/콜',
|
|
|
|
|
'detail' => '복사-붙여넣기한 제안서가 아닌, 사전 조사한 내용을 바탕으로 \'귀사의 00 문제를 해결해드릴 수 있습니다\'라고 접근하세요.',
|
|
|
|
|
'pro_tip' => '제목에 고객사 이름을 반드시 넣으세요. \'제안서입니다\' 대신 \'CodeBridgeExy의 재고 비용 30% 절감 방안 (for 고객사명)\'과 같이 구체적인 혜택을 명시하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '미팅 일정 확정',
|
|
|
|
|
'detail' => '단순한 회사 소개가 아닌, \'진단\'과 \'인사이트 공유\'를 목적으로 미팅을 제안하여 거부감을 줄이세요.',
|
|
|
|
|
'pro_tip' => '\'언제 시간 되세요?\'라고 묻지 말고, \'다음 주 화요일 오후 2시나 수요일 오전 10시 중 언제가 편하신가요?\'라고 양자택일 질문을 던지세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '사전 자료 공유',
|
|
|
|
|
'detail' => '미팅 전, 우리 회사의 소개서와 유사 업종의 성공 사례(Reference)를 미리 보내 신뢰도를 높이세요.',
|
|
|
|
|
'pro_tip' => '자료를 보낸 후 \'잘 받으셨나요?\'라고 확인 전화하는 것을 핑계로 한 번 더 접점을 만드세요.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '우리 제품을 파는 것이 아니라, \'만나야 할 이유\'를 파세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 3,
|
|
|
|
|
'title' => '현장 진단',
|
|
|
|
|
'subtitle' => 'Diagnosis',
|
|
|
|
|
'icon' => 'stethoscope',
|
|
|
|
|
'color' => 'purple',
|
|
|
|
|
'description' => '고객의 업무 현장을 직접 확인하고 진짜 문제를 찾아내는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => 'AS-IS 프로세스 맵핑',
|
|
|
|
|
'detail' => '현재 업무가 어떻게 진행되는지(주문->생산->출하) 흐름도를 그리고, 엑셀/수기 작업 구간을 찾아내세요.',
|
|
|
|
|
'pro_tip' => '화이트보드를 활용해 고객과 함께 그리세요. 고객이 직접 그리면서 \'여기가 진짜 문제네\'라고 스스로 깨닫게 하는 것이 가장 효과적입니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '비효율/리스크 식별',
|
|
|
|
|
'detail' => '데이터 누락, 중복 입력, 담당자 부재 시 업무 마비 등 구체적인 문제점과 그로 인한 비용 손실을 수치화하세요.',
|
|
|
|
|
'pro_tip' => '\'불편하시죠?\'가 아니라 \'이로 인해 한 달에 몇 시간이나 더 쓰시나요?\'라고 물어 비용으로 환산해 주세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '실무자 인터뷰 (VoC)',
|
|
|
|
|
'detail' => '현업 담당자가 겪는 가장 큰 고충(야근, 스트레스 등)을 듣고 공감대를 형성하여 우군으로 만드세요.',
|
|
|
|
|
'pro_tip' => '실무자의 고충을 해결해 주는 것이 곧 나의 영업 성공입니다. \'제가 이 야근, 없애 드리겠습니다\'라는 확신을 심어주세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => 'TO-BE 이미지 스케치',
|
|
|
|
|
'detail' => 'SAM 도입 후 업무가 어떻게 간소화되고 편해질지 구체적인 모습(Before & After)을 보여주세요.',
|
|
|
|
|
'pro_tip' => '말로만 설명하지 말고, 간단한 장표나 예시 화면을 보여주며 \'이렇게 바뀝니다\'라고 시각화하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '레거시 시스템 연동 확인',
|
|
|
|
|
'detail' => '기존에 사용 중인 ERP, 그룹웨어, 메신저 등과 SAM 시스템의 연동 가능성 및 기술적 제약 사항을 점검하세요.',
|
|
|
|
|
'pro_tip' => '개발팀을 대동하거나, 기술 지원 가능 여부를 현장에서 바로 확인해 주면 신뢰도가 급상승합니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '숨은 이해관계자 발굴',
|
|
|
|
|
'detail' => '표면적인 담당자 외에 도입에 영향을 미칠 수 있는 숨은 실세나 반대 세력(Detractor)을 파악하세요.',
|
|
|
|
|
'pro_tip' => '\'이 시스템을 도입하면 가장 싫어할 부서가 어디일까요?\'라고 넌지시 물어보세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '변화 저항 요소 파악',
|
|
|
|
|
'detail' => '새로운 시스템 도입 시 예상되는 내부 직원의 저항(익숙함 선호 등)을 미리 파악하고 대응 논리를 준비하세요.',
|
|
|
|
|
'pro_tip' => '\'기존 엑셀과 똑같은 화면 구성도 가능합니다\'와 같이 익숙함을 유지하면서 편리함만 더한다는 점을 강조하세요.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '고객이 말하지 않는 불편함까지 찾아내는 것이 전문가입니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 4,
|
|
|
|
|
'title' => '솔루션 제안',
|
|
|
|
|
'subtitle' => 'Proposal',
|
|
|
|
|
'icon' => 'presentation',
|
|
|
|
|
'color' => 'pink',
|
|
|
|
|
'description' => 'SAM을 통해 고객의 문제를 어떻게 해결할 수 있는지 증명하는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '맞춤형 데모 시연',
|
|
|
|
|
'detail' => '모든 기능을 보여주려 하지 말고, 앞서 파악한 고객의 페인포인트를 해결하는 핵심 기능 위주로 시연하세요.',
|
|
|
|
|
'pro_tip' => '고객사의 로고와 실제 데이터를 데모 시스템에 미리 넣어 가세요. \'이미 우리 시스템인 것 같다\'는 느낌을 주세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => 'ROI 분석 보고서',
|
|
|
|
|
'detail' => '솔루션 도입 비용 대비 절감할 수 있는 인건비, 시간, 기회비용을 수치로 산출하여 투자 가치를 증명하세요.',
|
|
|
|
|
'pro_tip' => 'ROI는 보수적으로 잡으세요. 그래도 충분히 매력적인 숫자가 나와야 진짜 설득력이 있습니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '성공 사례(Case Study)',
|
|
|
|
|
'detail' => '고객사와 유사한 규모/업종의 다른 회사가 우리 솔루션으로 어떤 성과를 냈는지 구체적인 사례를 제시하세요.',
|
|
|
|
|
'pro_tip' => '\'A사도 처음엔 고민하셨는데, 도입 3개월 만에 재고 정확도가 99%가 되었습니다\'와 같이 구체적인 수치와 기간을 언급하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '단계별 도입 로드맵',
|
|
|
|
|
'detail' => '한 번에 모든 것을 바꾸는 부담을 줄이기 위해, 단계적 도입(Pilot -> Roll-out) 방안을 제시하세요.',
|
|
|
|
|
'pro_tip' => '1단계는 \'핵심 문제 해결\', 2단계는 \'전사 확산\'으로 나누어 초기 진입 장벽을 낮추세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '경쟁 우위 분석 (Battle Card)',
|
|
|
|
|
'detail' => '경쟁사 대비 SAM 솔루션만의 강점(기능, 가격, 지원 등)을 명확히 비교하여 제시하세요.',
|
|
|
|
|
'pro_tip' => '기능 비교표를 준비하되, 우리가 이기는 항목(예: 모바일 사용성, CS 응대 속도)을 상단에 배치하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '예상 질문(Objection) 방어',
|
|
|
|
|
'detail' => '\'비싸다\', \'어렵다\', \'필요 없다\' 등 예상되는 거절 사유에 대한 명확한 답변과 논리를 준비하세요.',
|
|
|
|
|
'pro_tip' => '\'비싸다\'는 말은 \'가치를 못 느꼈다\'는 뜻입니다. 가격을 깎아주는 대신 가치를 다시 설명하세요.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '기능 나열이 아닌 \'가치\'와 \'변화\'를 보여주세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 5,
|
|
|
|
|
'title' => '협상 및 조율',
|
|
|
|
|
'subtitle' => 'Negotiation',
|
|
|
|
|
'icon' => 'scale',
|
|
|
|
|
'color' => 'orange',
|
|
|
|
|
'description' => '도입을 가로막는 장애물을 제거하고 조건을 합의하는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '가격/조건 협상',
|
|
|
|
|
'detail' => '단순 할인이 아닌, 장기 계약, 선납 조건, 도입 범위 조정 등 다양한 옵션을 통해 서로 만족할 수 있는 합의점을 찾으세요.',
|
|
|
|
|
'pro_tip' => '\'가격을 10% 깎아드리는 대신, 2년 계약을 하시겠습니까?\'와 같이 Give & Take 원칙을 지키세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '기술적 요구사항 조율',
|
|
|
|
|
'detail' => '커스터마이징 요구사항에 대해 가능한 범위와 추가 비용, 개발 일정을 명확히 협의하여 추후 분쟁을 예방하세요.',
|
|
|
|
|
'pro_tip' => '무조건 \'됩니다\'라고 하지 마세요. \'이 기능은 2주가 더 소요되는데 괜찮으신가요?\'라고 현실적인 제약을 공유해야 신뢰를 얻습니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '의사결정권자 설득',
|
|
|
|
|
'detail' => '실무자가 아닌 최종 결재권자(CEO/CFO)의 관심사(비용 절감, 리스크 관리)에 맞는 논리로 최종 승인을 받으세요.',
|
|
|
|
|
'pro_tip' => '임원 보고용 1장 요약 장표를 따로 만들어 실무자에게 전달해 주세요. 실무자가 내부 보고를 잘해야 계약이 성사됩니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '계약 초안 검토',
|
|
|
|
|
'detail' => '계약서의 주요 조항(서비스 수준, 해지 위약금, 유지보수 범위 등)을 꼼꼼히 검토하고 조율하세요.',
|
|
|
|
|
'pro_tip' => '법무팀 검토는 시간이 오래 걸립니다. 표준 계약서를 먼저 보내고, 수정 사항을 붉은색으로 표시해 달라고 요청하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '유지보수(SLA) 범위 확정',
|
|
|
|
|
'detail' => '장애 발생 시 대응 시간, 정기 점검 주기 등 기술 지원 수준(SLA)을 명확히 하여 고객의 불안을 해소하세요.',
|
|
|
|
|
'pro_tip' => '\'문제 생기면 바로 달려갑니다\'라는 말보다 \'평일 09-18시, 2시간 내 원격 지원\'과 같이 명확한 기준을 제시하세요.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '윈-윈(Win-Win)이 아니면 지속 가능한 계약이 아닙니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 6,
|
|
|
|
|
'title' => '계약 체결',
|
|
|
|
|
'subtitle' => 'Closing',
|
|
|
|
|
'icon' => 'pen-tool',
|
|
|
|
|
'color' => 'green',
|
|
|
|
|
'description' => '공식적인 파트너십을 맺고 법적 효력을 발생시키는 단계입니다.',
|
|
|
|
|
'checkpoints' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '계약서 날인 및 교부',
|
|
|
|
|
'detail' => '전자계약 또는 서면 계약을 통해 법적 효력을 확정하고, 계약서 원본을 안전하게 보관하세요.',
|
|
|
|
|
'pro_tip' => '전자계약(모두싸인 등)을 활용하면 계약 체결 시간을 획기적으로 단축할 수 있습니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '세금계산서 및 입금',
|
2026-02-03 16:20:09 +09:00
|
|
|
'detail' => '개발비(초기 도입비)에 대한 세금계산서를 발행하고, 입금 기한 내 수금을 확인하세요.',
|
2025-12-16 23:36:00 +09:00
|
|
|
'pro_tip' => '세금계산서 발행 시 사업자등록증 사본과 이메일 주소를 다시 한번 정확히 확인하세요.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '보안 서약 및 NDA',
|
|
|
|
|
'detail' => '고객사의 데이터 보호를 위한 보안 서약서 및 비밀유지협약(NDA)을 체결하여 신뢰를 강화하세요.',
|
|
|
|
|
'pro_tip' => '보안은 고객이 요구하기 전에 먼저 제안하세요. \'저희는 귀사의 정보를 소중히 다룹니다\'라는 강력한 메시지가 됩니다.',
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => 'Kick-off 미팅 준비',
|
|
|
|
|
'detail' => '본격적인 프로젝트 시작을 알리는 킥오프 미팅 일정을 잡고, 참여 인원과 안건을 확정하세요.',
|
|
|
|
|
'pro_tip' => '킥오프 미팅 때는 떡이나 다과를 준비해 가는 센스를 발휘하세요. 분위기가 훨씬 부드러워집니다.',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'tips' => '계약은 끝이 아니라 진정한 서비스의 시작입니다.',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자의 체크리스트 상태 조회
|
|
|
|
|
*/
|
|
|
|
|
public function getUserChecklist(User $user): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->getUserTenantId($user);
|
|
|
|
|
|
|
|
|
|
$checklists = SalesScenarioChecklist::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('is_checked', true)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$data = [];
|
|
|
|
|
foreach ($checklists as $item) {
|
|
|
|
|
if (! isset($data[$item->step_id])) {
|
|
|
|
|
$data[$item->step_id] = [];
|
|
|
|
|
}
|
|
|
|
|
$data[$item->step_id][] = $item->checkpoint_index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 체크포인트 토글
|
|
|
|
|
*/
|
|
|
|
|
public function toggleCheckpoint(User $user, int $stepId, int $checkpointIndex, bool $isChecked): array
|
|
|
|
|
{
|
|
|
|
|
$tenantId = $this->getUserTenantId($user);
|
|
|
|
|
|
|
|
|
|
if ($isChecked) {
|
|
|
|
|
// Insert or update
|
|
|
|
|
SalesScenarioChecklist::withoutGlobalScopes()->updateOrCreate(
|
|
|
|
|
[
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
'user_id' => $user->id,
|
|
|
|
|
'step_id' => $stepId,
|
|
|
|
|
'checkpoint_index' => $checkpointIndex,
|
|
|
|
|
],
|
|
|
|
|
['is_checked' => true]
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Delete record
|
|
|
|
|
SalesScenarioChecklist::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('step_id', $stepId)
|
|
|
|
|
->where('checkpoint_index', $checkpointIndex)
|
|
|
|
|
->delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return updated list for this step
|
|
|
|
|
$checkedIndices = SalesScenarioChecklist::withoutGlobalScopes()
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('step_id', $stepId)
|
|
|
|
|
->where('is_checked', true)
|
|
|
|
|
->pluck('checkpoint_index')
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
return array_map('intval', $checkedIndices);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 전체 진행률 계산
|
|
|
|
|
*/
|
|
|
|
|
public function getOverallProgress(User $user): array
|
|
|
|
|
{
|
|
|
|
|
$steps = self::getScenarioSteps();
|
|
|
|
|
$checklist = $this->getUserChecklist($user);
|
|
|
|
|
|
|
|
|
|
$totalCheckpoints = 0;
|
|
|
|
|
$totalChecked = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($steps as $step) {
|
|
|
|
|
$totalCheckpoints += count($step['checkpoints']);
|
|
|
|
|
$totalChecked += count($checklist[$step['id']] ?? []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'total' => $totalCheckpoints,
|
|
|
|
|
'checked' => $totalChecked,
|
|
|
|
|
'percent' => $totalCheckpoints > 0 ? round(($totalChecked / $totalCheckpoints) * 100) : 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사용자의 테넌트 ID 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getUserTenantId(User $user): int
|
|
|
|
|
{
|
|
|
|
|
$tenantId = DB::table('department_user')
|
|
|
|
|
->where('user_id', $user->id)
|
|
|
|
|
->where('is_primary', true)
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->value('tenant_id');
|
|
|
|
|
|
|
|
|
|
return $tenantId ?? 1;
|
|
|
|
|
}
|
|
|
|
|
}
|