753 lines
45 KiB
PHP
753 lines
45 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SAM 영업 시나리오 - CodeBridgeExy</title>
|
|
|
|
<!-- Fonts: Pretendard -->
|
|
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/static/pretendard.css" />
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Pretendard', 'sans-serif'],
|
|
},
|
|
colors: {
|
|
background: 'rgb(250, 250, 250)',
|
|
primary: {
|
|
DEFAULT: '#2563eb', // blue-600
|
|
light: '#dbeafe', // blue-100
|
|
dark: '#1e40af', // blue-800
|
|
foreground: '#ffffff',
|
|
},
|
|
slate: {
|
|
850: '#1e293b', // Custom dark slate
|
|
}
|
|
},
|
|
borderRadius: {
|
|
'card': '12px',
|
|
'pill': '9999px',
|
|
},
|
|
animation: {
|
|
'fade-in': 'fadeIn 0.5s ease-out',
|
|
'slide-up': 'slideUp 0.5s ease-out',
|
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
},
|
|
keyframes: {
|
|
fadeIn: {
|
|
'0%': { opacity: '0' },
|
|
'100%': { opacity: '1' },
|
|
},
|
|
slideUp: {
|
|
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- React & ReactDOM -->
|
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
|
|
<!-- Babel for JSX -->
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
|
|
<!-- Icons: Lucide -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<style>
|
|
/* Custom Scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
.step-connector {
|
|
position: absolute;
|
|
top: 2rem;
|
|
left: 50%;
|
|
width: 2px;
|
|
height: 100%;
|
|
background-color: #e2e8f0;
|
|
z-index: 0;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.step-connector {
|
|
top: 50%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 2px;
|
|
transform: translateY(-50%);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-background text-slate-800 antialiased min-h-screen flex flex-col">
|
|
<div id="root" class="flex-grow flex flex-col"></div>
|
|
|
|
<script type="text/babel">
|
|
const { useState, useEffect, useRef } = React;
|
|
|
|
// --- Data: Sales Scenario Steps ---
|
|
const SCENARIO_STEPS = [
|
|
{
|
|
id: 1,
|
|
title: "사전 준비",
|
|
subtitle: "Preparation",
|
|
icon: "search",
|
|
color: "bg-blue-100 text-blue-600",
|
|
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: "bg-indigo-100 text-indigo-600",
|
|
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: "bg-purple-100 text-purple-600",
|
|
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: "bg-pink-100 text-pink-600",
|
|
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: "bg-orange-100 text-orange-600",
|
|
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: "bg-green-100 text-green-600",
|
|
description: "공식적인 파트너십을 맺고 법적 효력을 발생시키는 단계입니다.",
|
|
checkpoints: [
|
|
{
|
|
title: "계약서 날인 및 교부",
|
|
detail: "전자계약 또는 서면 계약을 통해 법적 효력을 확정하고, 계약서 원본을 안전하게 보관하세요.",
|
|
pro_tip: "전자계약(모두싸인 등)을 활용하면 계약 체결 시간을 획기적으로 단축할 수 있습니다."
|
|
},
|
|
{
|
|
title: "세금계산서 및 입금",
|
|
detail: "가입비(초기 도입비)에 대한 세금계산서를 발행하고, 입금 기한 내 수금을 확인하세요.",
|
|
pro_tip: "세금계산서 발행 시 사업자등록증 사본과 이메일 주소를 다시 한번 정확히 확인하세요."
|
|
},
|
|
{
|
|
title: "보안 서약 및 NDA",
|
|
detail: "고객사의 데이터 보호를 위한 보안 서약서 및 비밀유지협약(NDA)을 체결하여 신뢰를 강화하세요.",
|
|
pro_tip: "보안은 고객이 요구하기 전에 먼저 제안하세요. '저희는 귀사의 정보를 소중히 다룹니다'라는 강력한 메시지가 됩니다."
|
|
},
|
|
{
|
|
title: "Kick-off 미팅 준비",
|
|
detail: "본격적인 프로젝트 시작을 알리는 킥오프 미팅 일정을 잡고, 참여 인원과 안건을 확정하세요.",
|
|
pro_tip: "킥오프 미팅 때는 떡이나 다과를 준비해 가는 센스를 발휘하세요. 분위기가 훨씬 부드러워집니다."
|
|
}
|
|
],
|
|
tips: "계약은 끝이 아니라 진정한 서비스의 시작입니다."
|
|
}
|
|
];
|
|
|
|
// --- Components ---
|
|
|
|
const Header = () => (
|
|
<header className="bg-white border-b border-slate-100 sticky top-0 z-50 shadow-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center text-white font-bold shadow-md">
|
|
S
|
|
</div>
|
|
<h1 className="text-lg font-bold text-slate-900 tracking-tight">SAM Sales Scenario</h1>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<a href="../index.php" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1 font-medium transition-colors">
|
|
<i data-lucide="home" className="w-4 h-4"></i>
|
|
홈으로
|
|
</a>
|
|
<div className="h-4 w-px bg-slate-200"></div>
|
|
<span className="text-xs font-medium px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full">v1.0 Standard</span>
|
|
<div className="w-8 h-8 rounded-full bg-slate-200 overflow-hidden border border-slate-300">
|
|
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" alt="User" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
|
|
const StepCard = ({ step, isActive, isCompleted, onClick, progress }) => {
|
|
const Icon = lucide.icons[step.icon] ? lucide.icons[step.icon] : lucide.icons.circle; // Fallback
|
|
|
|
// Calculate progress percentage
|
|
const progressPercent = progress ? Math.round((progress.checked / progress.total) * 100) : 0;
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div
|
|
onClick={() => onClick(step)}
|
|
className={`
|
|
relative flex-shrink-0 p-6 rounded-card border transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)] cursor-pointer group overflow-hidden
|
|
${isActive
|
|
? 'w-80 bg-white border-primary ring-2 ring-primary/20 shadow-2xl z-30 scale-105'
|
|
: 'w-32 bg-white border-slate-100 hover:border-slate-300 hover:shadow-md opacity-80 hover:opacity-100 z-0'
|
|
}
|
|
`}
|
|
>
|
|
{/* Step Number Badge */}
|
|
<div className={`
|
|
absolute top-4 right-4 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold border-2 border-white shadow-sm transition-colors duration-300 z-20
|
|
${isActive ? 'bg-primary text-white' : 'bg-slate-100 text-slate-400'}
|
|
`}>
|
|
{step.id}
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center text-center space-y-4 w-full">
|
|
<div className={`
|
|
w-12 h-12 rounded-2xl flex items-center justify-center text-2xl mb-2 transition-all duration-300
|
|
${isActive ? 'w-16 h-16 text-3xl ' + step.color : 'bg-slate-50 text-slate-400'}
|
|
`}>
|
|
<i data-lucide={step.icon} className={`${isActive ? 'w-8 h-8' : 'w-6 h-6'}`}></i>
|
|
</div>
|
|
|
|
<div className="w-full">
|
|
<h3 className={`font-bold transition-all duration-300 whitespace-nowrap overflow-hidden text-ellipsis ${isActive ? 'text-lg text-slate-900' : 'text-xs text-slate-500'}`}>
|
|
{step.title}
|
|
</h3>
|
|
{isActive && (
|
|
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mt-1 animate-fade-in">
|
|
{step.subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{isActive && (
|
|
<div className="w-full pt-4 border-t border-slate-100 animate-fade-in">
|
|
<p className="text-sm text-slate-600 leading-relaxed text-left line-clamp-3">
|
|
{step.description}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Active Indicator Arrow */}
|
|
{isActive && (
|
|
<div className="absolute -bottom-3 left-1/2 transform -translate-x-1/2 w-6 h-6 bg-white border-b border-r border-primary rotate-45 z-20 md:hidden"></div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Progress Graph (Below Card) */}
|
|
<div className={`transition-all duration-500 ${isActive ? 'w-64 opacity-100' : 'w-24 opacity-60'}`}>
|
|
<div className="flex justify-between text-xs mb-1 font-medium text-slate-500">
|
|
<span>진행률</span>
|
|
<span>{progressPercent}%</span>
|
|
</div>
|
|
<div className="w-full bg-slate-200 rounded-full h-2 overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-1000 ease-out ${step.color.replace('text-', 'bg-')}`}
|
|
style={{ width: `${progressPercent}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TipModal = ({ checkpoint, onClose }) => {
|
|
if (!checkpoint) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden animate-slide-up" onClick={e => e.stopPropagation()}>
|
|
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50">
|
|
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
|
|
<i data-lucide="lightbulb" className="w-5 h-5 text-yellow-500"></i>
|
|
Sales Pro Tip
|
|
</h3>
|
|
<button onClick={onClose} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
|
|
<i data-lucide="x" className="w-5 h-5 text-slate-500"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
<h4 className="text-xl font-bold text-slate-900 mb-3">{checkpoint.title}</h4>
|
|
<p className="text-slate-600 mb-6 leading-relaxed">{checkpoint.detail}</p>
|
|
|
|
<div className="bg-blue-50 border border-blue-100 rounded-xl p-5">
|
|
<div className="flex items-start gap-3">
|
|
<div className="p-2 bg-blue-100 rounded-lg text-blue-600 shrink-0">
|
|
<i data-lucide="thumbs-up" className="w-5 h-5"></i>
|
|
</div>
|
|
<div>
|
|
<h5 className="font-bold text-blue-800 mb-1">실전 꿀팁</h5>
|
|
<p className="text-blue-700 text-sm leading-relaxed">
|
|
{checkpoint.pro_tip}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-slate-100 bg-slate-50 flex justify-end">
|
|
<button onClick={onClose} className="px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium transition-colors">
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const DetailPanel = ({ step, checkedItems, onCheck }) => {
|
|
const [selectedCheckpoint, setSelectedCheckpoint] = useState(null);
|
|
|
|
if (!step) return null;
|
|
|
|
return (
|
|
<>
|
|
<div className="bg-white rounded-card shadow-lg border border-slate-100 overflow-hidden animate-slide-up">
|
|
<div className="p-6 md:p-8 flex flex-col md:flex-row gap-8">
|
|
{/* Left: Header & Description */}
|
|
<div className="md:w-1/3 space-y-6">
|
|
<div>
|
|
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold mb-4 ${step.color.replace('text-', 'bg-opacity-20 text-')}`}>
|
|
STEP {step.id}
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-slate-900 mb-2">{step.title}</h2>
|
|
<p className="text-lg text-slate-500 font-light">{step.subtitle}</p>
|
|
</div>
|
|
|
|
<p className="text-slate-600 leading-relaxed">
|
|
{step.description}
|
|
</p>
|
|
|
|
<div className="p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<h4 className="text-sm font-bold text-slate-800 mb-2 flex items-center gap-2">
|
|
<i data-lucide="lightbulb" className="w-4 h-4 text-yellow-500"></i>
|
|
Sales Tip
|
|
</h4>
|
|
<p className="text-sm text-slate-600 italic">
|
|
"{step.tips}"
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Checkpoints */}
|
|
<div className="md:w-2/3 bg-slate-50 rounded-xl p-6 border border-slate-100">
|
|
<h3 className="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
|
|
<i data-lucide="check-square" className="w-5 h-5 text-primary"></i>
|
|
핵심 체크포인트
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{step.checkpoints.map((point, idx) => (
|
|
<div key={idx} className="relative group">
|
|
<label className="flex items-start gap-4 p-4 bg-white rounded-xl border border-slate-200 hover:border-primary/50 hover:shadow-md transition-all cursor-pointer pr-12">
|
|
<div className="relative flex items-center mt-1 shrink-0">
|
|
<input
|
|
type="checkbox"
|
|
className="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-slate-300 checked:border-primary checked:bg-primary transition-all"
|
|
checked={checkedItems.includes(idx)}
|
|
onChange={(e) => onCheck(step.id, idx, e.target.checked)}
|
|
/>
|
|
<i data-lucide="check" className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100 pointer-events-none"></i>
|
|
</div>
|
|
<div className="flex-grow">
|
|
<span className={`block text-sm font-bold transition-colors mb-1 ${checkedItems.includes(idx) ? 'text-slate-400 line-through' : 'text-slate-800 group-hover:text-primary'}`}>
|
|
{point.title}
|
|
</span>
|
|
<span className={`block text-sm leading-relaxed ${checkedItems.includes(idx) ? 'text-slate-400' : 'text-slate-600'}`}>
|
|
{point.detail}
|
|
</span>
|
|
</div>
|
|
</label>
|
|
|
|
{/* Help Button (Floating) */}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedCheckpoint(point);
|
|
}}
|
|
className="absolute right-4 top-4 p-2 text-slate-400 hover:text-yellow-500 hover:bg-yellow-50 rounded-full transition-all opacity-0 group-hover:opacity-100"
|
|
title="꿀팁 보기"
|
|
>
|
|
<i data-lucide="help-circle" className="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tip Modal */}
|
|
{selectedCheckpoint && (
|
|
<TipModal
|
|
checkpoint={selectedCheckpoint}
|
|
onClose={() => setSelectedCheckpoint(null)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const App = () => {
|
|
const [activeStepId, setActiveStepId] = useState(1);
|
|
const [checklistData, setChecklistData] = useState({}); // { stepId: [checkedIndex1, checkedIndex2] }
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const activeStep = SCENARIO_STEPS.find(s => s.id === activeStepId);
|
|
const scrollContainerRef = useRef(null);
|
|
const tenantId = "tenant_001"; // Hardcoded for demo, normally from auth
|
|
|
|
useEffect(() => {
|
|
// Fetch initial checklist data
|
|
fetch(`api_handler.php?tenant_id=${tenantId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
setChecklistData(data.data);
|
|
}
|
|
setLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error("Failed to fetch checklist:", err);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
lucide.createIcons();
|
|
}, [activeStepId, checklistData]);
|
|
|
|
const handleCheck = (stepId, index, isChecked) => {
|
|
// Optimistic update
|
|
setChecklistData(prev => {
|
|
const currentStepChecks = prev[stepId] || [];
|
|
let newStepChecks;
|
|
if (isChecked) {
|
|
newStepChecks = [...currentStepChecks, index];
|
|
} else {
|
|
newStepChecks = currentStepChecks.filter(i => i !== index);
|
|
}
|
|
return { ...prev, [stepId]: newStepChecks };
|
|
});
|
|
|
|
// API Call
|
|
fetch('api_handler.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tenant_id: tenantId,
|
|
step_id: stepId,
|
|
checkpoint_index: index,
|
|
is_checked: isChecked
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
// Revert on failure (optional, but good practice)
|
|
console.error("Update failed:", data.message);
|
|
}
|
|
})
|
|
.catch(err => console.error("API Error:", err));
|
|
};
|
|
|
|
const getProgress = (step) => {
|
|
const checked = checklistData[step.id]?.length || 0;
|
|
const total = step.checkpoints.length;
|
|
return { checked, total };
|
|
};
|
|
|
|
// Calculate Overall Progress
|
|
const totalCheckpoints = SCENARIO_STEPS.reduce((sum, step) => sum + step.checkpoints.length, 0);
|
|
|
|
// Only count checked items for steps that actually exist in SCENARIO_STEPS
|
|
const validStepIds = SCENARIO_STEPS.map(s => s.id);
|
|
const totalChecked = Object.entries(checklistData).reduce((sum, [stepId, checks]) => {
|
|
if (validStepIds.includes(parseInt(stepId))) {
|
|
return sum + checks.length;
|
|
}
|
|
return sum;
|
|
}, 0);
|
|
|
|
const overallProgress = totalCheckpoints > 0 ? Math.min(100, Math.round((totalChecked / totalCheckpoints) * 100)) : 0;
|
|
|
|
return (
|
|
<div className="min-h-screen pb-20">
|
|
<Header />
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
|
|
|
{/* Overall Progress Section */}
|
|
<div className="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 flex flex-col md:flex-row items-center gap-6 animate-fade-in">
|
|
<div className="flex-1 w-full">
|
|
<div className="flex justify-between items-end mb-2">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-slate-900">전체 진행 현황</h2>
|
|
<p className="text-sm text-slate-500">모든 단계의 체크포인트를 완료하여 영업 성공률을 높이세요.</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="text-3xl font-extrabold text-primary">{overallProgress}%</span>
|
|
<span className="text-sm text-slate-400 ml-1">완료</span>
|
|
</div>
|
|
</div>
|
|
<div className="w-full bg-slate-100 rounded-full h-4 overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-blue-500 to-blue-600 rounded-full transition-all duration-1000 ease-out shadow-[0_0_10px_rgba(37,99,235,0.3)]"
|
|
style={{ width: `${overallProgress}%` }}
|
|
>
|
|
<div className="w-full h-full opacity-30 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9InAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBMMTAgME0yMCAxMEwxMCAyMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI3ApIi8+PC9zdmc+')] animate-[shimmer_2s_linear_infinite]"></div>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between mt-2 text-xs text-slate-400 font-medium">
|
|
<span>시작</span>
|
|
<span>{totalChecked} / {totalCheckpoints} 항목 완료</span>
|
|
<span>완료</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Circular Indicator (Optional, for visual balance) */}
|
|
<div className="hidden md:flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 text-blue-600 border border-blue-100 shrink-0">
|
|
<i data-lucide="trophy" className="w-8 h-8"></i>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Process Map */}
|
|
<div className="relative py-10">
|
|
{/* Connector Line (Desktop) */}
|
|
<div className="hidden lg:block absolute top-[130px] left-0 w-full h-1 bg-slate-100 rounded-full z-0"></div>
|
|
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className="flex items-start justify-start lg:justify-center gap-4 overflow-x-auto pb-8 pt-4 px-4 snap-x snap-mandatory hide-scrollbar min-h-[400px]"
|
|
style={{ scrollBehavior: 'smooth' }}
|
|
>
|
|
{SCENARIO_STEPS.map((step, index) => (
|
|
<div key={step.id} className="snap-center shrink-0 flex justify-center transition-all duration-500">
|
|
<StepCard
|
|
step={step}
|
|
isActive={activeStepId === step.id}
|
|
onClick={(s) => setActiveStepId(s.id)}
|
|
progress={getProgress(step)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Detail Panel */}
|
|
<div className="transition-all duration-500 ease-in-out">
|
|
<DetailPanel
|
|
key={activeStepId}
|
|
step={activeStep}
|
|
checkedItems={checklistData[activeStepId] || []}
|
|
onCheck={handleCheck}
|
|
/>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer className="bg-white border-t border-slate-100 py-8 mt-auto">
|
|
<div className="max-w-7xl mx-auto px-4 text-center text-slate-400 text-sm">
|
|
© 2025 codebridge-x.com. All rights reserved.
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
root.render(<App />);
|
|
</script>
|
|
</body>
|
|
</html>
|