Files
sam-manage/resources/views/lab/management/sales-scenario.blade.php
hskwon 25ef6659ba feat: 영업 시나리오 체크리스트 기능 구현
- 6단계 영업 프로세스 체크리스트 UI 구현
- 사용자별 체크포인트 저장/조회 API 추가
- 레거시 스타일 가로 아코디언 UI 적용
- 단계별 진행률 표시 및 꿀팁 모달 추가
2025-12-16 23:36:00 +09:00

610 lines
24 KiB
PHP

@extends('layouts.app')
@section('title', 'SAM 영업 시나리오')
@push('styles')
<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; }
/* Color Palette */
.step-blue { --step-bg: #dbeafe; --step-text: #2563eb; --step-accent: #3b82f6; }
.step-indigo { --step-bg: #e0e7ff; --step-text: #4f46e5; --step-accent: #6366f1; }
.step-purple { --step-bg: #f3e8ff; --step-text: #9333ea; --step-accent: #a855f7; }
.step-pink { --step-bg: #fce7f3; --step-text: #db2777; --step-accent: #ec4899; }
.step-orange { --step-bg: #ffedd5; --step-text: #ea580c; --step-accent: #f97316; }
.step-green { --step-bg: #dcfce7; --step-text: #16a34a; --step-accent: #22c55e; }
/* Step Cards Container */
.steps-container {
display: flex;
align-items: flex-start;
gap: 1rem;
overflow-x: auto;
padding: 1rem 2rem 2rem;
scroll-behavior: smooth;
margin: 0 -1rem;
}
.steps-container::before {
content: '';
flex-shrink: 0;
width: 1rem;
}
.steps-container::after {
content: '';
flex-shrink: 0;
width: 1rem;
}
/* Step Card Wrapper */
.step-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
/* Step Cards - Accordion Style */
.step-card {
position: relative;
padding: 1.5rem;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: white;
cursor: pointer;
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
width: 8rem;
opacity: 0.7;
}
.step-card:hover {
border-color: #cbd5e1;
box-shadow: 0 4px 12px -2px rgba(0,0,0,0.1);
opacity: 1;
}
.step-card.active {
width: 20rem;
opacity: 1;
border-color: var(--step-accent);
box-shadow: 0 20px 40px -10px rgba(59, 130, 246, 0.25);
z-index: 10;
transform: scale(1.05);
}
/* Step Number Badge */
.step-number {
position: absolute;
top: 1rem;
right: 1rem;
width: 2rem;
height: 2rem;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 700;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.3s;
background: #f1f5f9;
color: #94a3b8;
}
.step-card.active .step-number {
background: var(--step-accent);
color: white;
}
/* Step Icon */
.step-icon {
width: 3rem;
height: 3rem;
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
color: #94a3b8;
margin: 0 auto 1rem;
transition: all 0.3s;
}
.step-card.active .step-icon {
width: 4rem;
height: 4rem;
background: var(--step-bg);
color: var(--step-text);
}
/* Step Title */
.step-title {
font-weight: 700;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.3s;
font-size: 0.75rem;
color: #64748b;
}
.step-card.active .step-title {
font-size: 1.125rem;
color: #1e293b;
}
/* Step Subtitle & Description - Hidden when inactive */
.step-subtitle,
.step-description {
display: none;
}
.step-card.active .step-subtitle {
display: block;
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
text-align: center;
margin-top: 0.25rem;
animation: fadeIn 0.3s ease-out;
}
.step-card.active .step-description {
display: block;
font-size: 0.875rem;
color: #64748b;
text-align: left;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #f1f5f9;
line-height: 1.6;
animation: fadeIn 0.3s ease-out;
}
/* Progress Bar below card */
.step-progress-wrapper {
width: 6rem;
transition: width 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.step-wrapper.active .step-progress-wrapper {
width: 16rem;
}
.step-progress-label {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #64748b;
margin-bottom: 0.25rem;
}
.step-progress-bar {
height: 0.5rem;
background: #e2e8f0;
border-radius: 9999px;
overflow: hidden;
}
.step-progress-fill {
height: 100%;
border-radius: 9999px;
transition: width 1s ease-out;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.animate-slide-up {
animation: slideUp 0.4s ease-out;
}
/* Checkbox Styling */
.checkpoint-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
position: relative;
flex-shrink: 0;
}
.checkpoint-checkbox:checked {
background: #3b82f6;
border-color: #3b82f6;
}
.checkpoint-checkbox:checked::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkpoint-item.checked .checkpoint-title { text-decoration: line-through; color: #9ca3af; }
.checkpoint-item.checked .checkpoint-detail { color: #9ca3af; }
/* Modal */
.modal-backdrop {
background: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
.modal-content {
animation: slideUp 0.3s ease-out;
}
/* Progress Bar Animation */
.progress-bar {
background: linear-gradient(90deg, #3b82f6, #2563eb);
transition: width 0.8s ease-out;
position: relative;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Responsive */
@media (max-width: 1024px) {
.steps-container {
padding-left: 1rem;
padding-right: 1rem;
}
.step-card.active {
width: 16rem;
}
.step-wrapper.active .step-progress-wrapper {
width: 12rem;
}
}
</style>
@endpush
@section('content')
<div class="max-w-7xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-800">SAM 영업 시나리오</h1>
<p class="text-gray-600 mt-1">성공적인 영업을 위한 6단계 프로세스 가이드</p>
</div>
<span class="text-xs font-medium px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full">v1.0 Standard</span>
</div>
<!-- 전체 진행 현황 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex-1">
<div class="flex justify-between items-end mb-2">
<div>
<h2 class="text-lg font-bold text-gray-900">전체 진행 현황</h2>
<p class="text-sm text-gray-500">모든 단계의 체크포인트를 완료하여 영업 성공률을 높이세요.</p>
</div>
<div class="text-right">
<span id="overall-percent" class="text-3xl font-extrabold text-blue-600">{{ $progress['percent'] }}%</span>
<span class="text-sm text-gray-400 ml-1">완료</span>
</div>
</div>
<div class="w-full bg-gray-100 rounded-full h-4 overflow-hidden relative">
<div id="overall-progress-bar" class="progress-bar h-full rounded-full relative" style="width: {{ $progress['percent'] }}%"></div>
</div>
<div class="flex justify-between mt-2 text-xs text-gray-400 font-medium">
<span>시작</span>
<span id="progress-text">{{ $progress['checked'] }} / {{ $progress['total'] }} 항목 완료</span>
<span>완료</span>
</div>
</div>
<div class="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">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
</div>
</div>
<!-- 단계 카드 (가로 아코디언 스타일) -->
<div class="steps-container">
@foreach($steps as $step)
@php
$stepChecked = isset($checklist[$step['id']]) ? count($checklist[$step['id']]) : 0;
$stepTotal = count($step['checkpoints']);
$stepPercent = $stepTotal > 0 ? round(($stepChecked / $stepTotal) * 100) : 0;
@endphp
<div class="step-wrapper {{ $loop->first ? 'active' : '' }}" data-step-id="{{ $step['id'] }}">
<div class="step-card step-{{ $step['color'] }} {{ $loop->first ? 'active' : '' }}"
data-step-id="{{ $step['id'] }}"
onclick="selectStep({{ $step['id'] }})">
<!-- 단계 번호 -->
<div class="step-number">{{ $step['id'] }}</div>
<!-- 아이콘 -->
<div class="step-icon">
@include('lab.management.partials.sales-scenario-icon', ['icon' => $step['icon']])
</div>
<!-- 제목 -->
<h3 class="step-title">{{ $step['title'] }}</h3>
<p class="step-subtitle">{{ $step['subtitle'] }}</p>
<p class="step-description">{{ $step['description'] }}</p>
</div>
<!-- 진행률 -->
<div class="step-progress-wrapper">
<div class="step-progress-label">
<span>진행률</span>
<span id="progress-percent-{{ $step['id'] }}">{{ $stepPercent }}%</span>
</div>
<div class="step-progress-bar">
<div class="step-progress-fill" id="progress-fill-{{ $step['id'] }}"
style="width: {{ $stepPercent }}%; background: var(--step-accent);"></div>
</div>
</div>
</div>
@endforeach
</div>
<!-- 상세 패널 -->
<div id="detail-panel" class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden animate-slide-up">
@foreach($steps as $step)
<div class="step-detail {{ $loop->first ? '' : 'hidden' }}" id="detail-{{ $step['id'] }}">
<div class="p-6 md:p-8 flex flex-col lg:flex-row gap-8">
<!-- 좌측: 헤더 & 설명 -->
<div class="lg:w-1/3 space-y-6">
<div>
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold mb-4 step-{{ $step['color'] }}" style="background: var(--step-bg); color: var(--step-text);">
STEP {{ $step['id'] }}
</div>
<h2 class="text-3xl font-bold text-gray-900 mb-2">{{ $step['title'] }}</h2>
<p class="text-lg text-gray-500 font-light">{{ $step['subtitle'] }}</p>
</div>
<p class="text-gray-600 leading-relaxed">{{ $step['description'] }}</p>
<div class="p-4 bg-gray-50 rounded-xl border border-gray-100">
<h4 class="text-sm font-bold text-gray-800 mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Sales Tip
</h4>
<p class="text-sm text-gray-600 italic">"{{ $step['tips'] }}"</p>
</div>
</div>
<!-- 우측: 체크포인트 -->
<div class="lg:w-2/3 bg-gray-50 rounded-xl p-6 border border-gray-100">
<h3 class="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
핵심 체크포인트
</h3>
<div class="space-y-3">
@foreach($step['checkpoints'] as $idx => $checkpoint)
<div class="checkpoint-item relative group {{ isset($checklist[$step['id']]) && in_array($idx, $checklist[$step['id']]) ? 'checked' : '' }}"
data-step="{{ $step['id'] }}" data-index="{{ $idx }}">
<label class="flex items-start gap-4 p-4 bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all cursor-pointer pr-12">
<input type="checkbox" class="checkpoint-checkbox mt-1"
data-step="{{ $step['id'] }}"
data-index="{{ $idx }}"
{{ isset($checklist[$step['id']]) && in_array($idx, $checklist[$step['id']]) ? 'checked' : '' }}
onchange="toggleCheckpoint(this)">
<div class="flex-grow">
<span class="checkpoint-title block text-sm font-bold text-gray-800 mb-1">{{ $checkpoint['title'] }}</span>
<span class="checkpoint-detail block text-sm text-gray-600 leading-relaxed">{{ $checkpoint['detail'] }}</span>
</div>
</label>
<!-- 꿀팁 버튼 -->
<button onclick="showTip({{ $step['id'] }}, {{ $idx }})"
class="absolute right-4 top-4 p-2 text-gray-400 hover:text-yellow-500 hover:bg-yellow-50 rounded-full transition-all opacity-0 group-hover:opacity-100"
title="꿀팁 보기">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
@endforeach
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
<!-- 꿀팁 모달 -->
<div id="tip-modal" class="fixed inset-0 z-50 hidden">
<div class="modal-backdrop absolute inset-0" onclick="closeTipModal()"></div>
<div class="flex items-center justify-center min-h-screen p-4">
<div class="modal-content bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden relative z-10">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="text-lg font-bold text-gray-900 flex items-center gap-2">
<svg class="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Sales Pro Tip
</h3>
<button onclick="closeTipModal()" class="p-2 hover:bg-gray-200 rounded-full transition-colors">
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-6">
<h4 id="tip-title" class="text-xl font-bold text-gray-900 mb-3"></h4>
<p id="tip-detail" class="text-gray-600 mb-6 leading-relaxed"></p>
<div class="bg-blue-50 border border-blue-100 rounded-xl p-5">
<div class="flex items-start gap-3">
<div class="p-2 bg-blue-100 rounded-lg text-blue-600 shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5" />
</svg>
</div>
<div>
<h5 class="font-bold text-blue-800 mb-1">실전 꿀팁</h5>
<p id="tip-pro" class="text-blue-700 text-sm leading-relaxed"></p>
</div>
</div>
</div>
</div>
<div class="p-4 border-t border-gray-100 bg-gray-50 flex justify-end">
<button onclick="closeTipModal()" class="px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 font-medium transition-colors">
확인
</button>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = '{{ csrf_token() }}';
const stepsData = @json($steps);
let checklistData = @json($checklist);
let progressData = @json($progress);
// 단계 선택
window.selectStep = function(stepId) {
// 모든 wrapper와 card에서 active 제거
document.querySelectorAll('.step-wrapper').forEach(w => w.classList.remove('active'));
document.querySelectorAll('.step-card').forEach(card => card.classList.remove('active'));
// 선택된 항목에 active 추가
const activeWrapper = document.querySelector(`.step-wrapper[data-step-id="${stepId}"]`);
const activeCard = document.querySelector(`.step-card[data-step-id="${stepId}"]`);
if (activeWrapper) activeWrapper.classList.add('active');
if (activeCard) activeCard.classList.add('active');
// 상세 패널 전환
document.querySelectorAll('.step-detail').forEach(detail => detail.classList.add('hidden'));
const detailPanel = document.getElementById(`detail-${stepId}`);
if (detailPanel) {
detailPanel.classList.remove('hidden');
detailPanel.classList.add('animate-slide-up');
}
// 스크롤하여 카드를 가운데로
if (activeWrapper) {
activeWrapper.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
};
// 체크포인트 토글
window.toggleCheckpoint = function(checkbox) {
const stepId = parseInt(checkbox.dataset.step);
const index = parseInt(checkbox.dataset.index);
const isChecked = checkbox.checked;
// UI 즉시 업데이트
const item = checkbox.closest('.checkpoint-item');
if (isChecked) {
item.classList.add('checked');
} else {
item.classList.remove('checked');
}
// API 호출
fetch('{{ route("lab.management.sales-scenario.toggle") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
step_id: stepId,
checkpoint_index: index,
is_checked: isChecked
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
checklistData[stepId] = data.data;
progressData = data.progress;
updateProgressUI();
updateStepProgress(stepId);
} else {
// 실패시 롤백
checkbox.checked = !isChecked;
item.classList.toggle('checked');
showToast(data.message || '저장에 실패했습니다.', 'error');
}
})
.catch(err => {
console.error('API Error:', err);
checkbox.checked = !isChecked;
item.classList.toggle('checked');
showToast('네트워크 오류가 발생했습니다.', 'error');
});
};
// 진행률 UI 업데이트
function updateProgressUI() {
document.getElementById('overall-percent').textContent = progressData.percent + '%';
document.getElementById('overall-progress-bar').style.width = progressData.percent + '%';
document.getElementById('progress-text').textContent = `${progressData.checked} / ${progressData.total} 항목 완료`;
}
// 단계별 진행률 업데이트
function updateStepProgress(stepId) {
const step = stepsData.find(s => s.id === stepId);
if (!step) return;
const checked = checklistData[stepId] ? checklistData[stepId].length : 0;
const total = step.checkpoints.length;
const percent = Math.round((checked / total) * 100);
const percentEl = document.getElementById(`progress-percent-${stepId}`);
const fillEl = document.getElementById(`progress-fill-${stepId}`);
if (percentEl) percentEl.textContent = percent + '%';
if (fillEl) fillEl.style.width = percent + '%';
}
// 꿀팁 모달 표시
window.showTip = function(stepId, index) {
const step = stepsData.find(s => s.id === stepId);
if (!step) return;
const checkpoint = step.checkpoints[index];
if (!checkpoint) return;
document.getElementById('tip-title').textContent = checkpoint.title;
document.getElementById('tip-detail').textContent = checkpoint.detail;
document.getElementById('tip-pro').textContent = checkpoint.pro_tip;
document.getElementById('tip-modal').classList.remove('hidden');
};
// 꿀팁 모달 닫기
window.closeTipModal = function() {
document.getElementById('tip-modal').classList.add('hidden');
};
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeTipModal();
}
});
// 토스트 메시지 (간단한 구현)
window.showToast = function(message, type = 'info') {
console.log(`[${type.toUpperCase()}] ${message}`);
};
// 첫 번째 카드 활성화
selectStep(1);
});
</script>
@endpush