feat: [RD] 온보딩 도움말기능 구현 (Driver.js 기반)

- Driver.js 라이브러리 설치 및 Vite 번들 등록
- SamOnboarding JS 모듈 작성 (가이드 정의/실행/상태저장)
- 온보딩 도움말기능 관리 페이지 (데모 체험, 개발자 가이드)
- RD 대시보드에 온보딩 가이드 시범 적용
- 메뉴 라우트 추가 (/rd/onboarding-guide)
This commit is contained in:
김보곤
2026-03-22 20:19:26 +09:00
parent cd27547749
commit 347a052c3f
13 changed files with 615 additions and 15 deletions

View File

@@ -769,4 +769,16 @@ public function autoQuotation(Request $request): View|\Illuminate\Http\Response
return view('rd.auto-quotation');
}
/**
* 온보딩 도움말기능
*/
public function onboardingGuide(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.onboarding-guide'));
}
return view('rd.onboarding-guide.index');
}
}

View File

@@ -20,6 +20,7 @@
"dependencies": {
"@capacitor/core": "^8.0.0",
"@capacitor/push-notifications": "^8.0.0",
"driver.js": "^1.4.0",
"htmx.org": "^2.0.8"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.driver-active .driver-overlay,.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;box-sizing:border-box;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,"Apple Color Emoji",Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:hover,.driver-popover-footer button:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-right{right:100%;border-left-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-top{top:100%;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:transparent;border-top-color:transparent;border-right-color:transparent}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-start,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-end,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-top.driver-popover-arrow-align-center,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none}

View File

@@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-B0m1D6Gu.css",
"file": "assets/app-C-Kb46Vq.css",
"src": "resources/css/app.css",
"isEntry": true,
"name": "app",
@@ -9,9 +9,12 @@
]
},
"resources/js/app.js": {
"file": "assets/app-4nAhvfTZ.js",
"file": "assets/app-BiJrT4B1.js",
"name": "app",
"src": "resources/js/app.js",
"isEntry": true
"isEntry": true,
"css": [
"assets/app-DB0Q8XAf.css"
]
}
}

View File

@@ -2,10 +2,17 @@ import './bootstrap';
import htmx from 'htmx.org';
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';
import { SamOnboarding } from './onboarding-guide';
// HTMX를 전역으로 설정
window.htmx = htmx;
// Driver.js + SAM 온보딩 전역 설정
window.driver = driver;
window.SamOnboarding = SamOnboarding;
// Capacitor 앱에서만 실행 (포그라운드 푸시 알림 소리)
if (Capacitor.isNativePlatform()) {
PushNotifications.addListener('pushNotificationReceived', (notification) => {

View File

@@ -0,0 +1,140 @@
import { driver } from 'driver.js';
/**
* SAM 온보딩 가이드 시스템
*
* 사용법:
* const guide = new SamOnboarding('page-key', [
* { element: '#btn-create', title: '새 등록', description: '새 항목을 등록합니다.' },
* { element: '.data-grid', title: '데이터 목록', description: '등록된 데이터를 확인하세요.' },
* ]);
* guide.start(); // 가이드 시작
* guide.startIfFirstVisit(); // 첫 방문 시에만 자동 시작
*/
export class SamOnboarding {
constructor(pageKey, steps = [], options = {}) {
this.pageKey = pageKey;
this.storageKey = `sam_onboarding_${pageKey}`;
this.steps = steps;
this.options = options;
this.driverInstance = null;
}
/**
* 가이드 시작
*/
start() {
if (!this.steps.length) return;
const driverSteps = this.steps.map((step, index) => ({
element: step.element,
popover: {
title: step.title || '',
description: this._buildDescription(step.description, index),
side: step.side || 'bottom',
align: step.align || 'start',
},
}));
this.driverInstance = driver({
showProgress: true,
animate: true,
allowClose: true,
overlayColor: 'rgba(0, 0, 0, 0.6)',
stagePadding: 8,
stageRadius: 8,
popoverClass: 'sam-onboarding-popover',
progressText: '{{current}} / {{total}}',
nextBtnText: '다음',
prevBtnText: '이전',
doneBtnText: '완료',
...this.options,
steps: driverSteps,
onDestroyStarted: () => {
this._markCompleted();
this.driverInstance.destroy();
},
});
this.driverInstance.drive();
}
/**
* 첫 방문 시에만 자동 시작
*/
startIfFirstVisit() {
if (!this._isCompleted()) {
// DOM 렌더링 대기 후 시작
setTimeout(() => this.start(), 500);
}
}
/**
* 완료 상태 초기화 (다시 보기)
*/
reset() {
localStorage.removeItem(this.storageKey);
}
/**
* 완료 여부 확인
*/
isCompleted() {
return this._isCompleted();
}
_isCompleted() {
return localStorage.getItem(this.storageKey) === 'completed';
}
_markCompleted() {
localStorage.setItem(this.storageKey, 'completed');
}
_buildDescription(desc, index) {
return desc || '';
}
}
/**
* 가이드 정의 레지스트리
* 각 페이지의 가이드 정의를 중앙에서 관리
*/
export const GuideRegistry = {
_guides: {},
/**
* 가이드 등록
* @param {string} pageKey - 페이지 식별자
* @param {Array} steps - 가이드 스텝 배열
* @param {Object} options - 추가 옵션
*/
register(pageKey, steps, options = {}) {
this._guides[pageKey] = { steps, options };
},
/**
* 등록된 가이드 조회
*/
get(pageKey) {
return this._guides[pageKey] || null;
},
/**
* 등록된 가이드 인스턴스 생성
*/
create(pageKey) {
const def = this._guides[pageKey];
if (!def) return null;
return new SamOnboarding(pageKey, def.steps, def.options);
},
/**
* 모든 등록된 가이드 키 목록
*/
keys() {
return Object.keys(this._guides);
},
};
window.GuideRegistry = GuideRegistry;

View File

@@ -9,18 +9,21 @@
<i class="ri-flask-line text-purple-600"></i>
연구개발 대시보드
</h1>
<div class="flex gap-2">
<div id="rd-header-actions" class="flex gap-2">
<button onclick="rdGuide.start()" class="bg-white hover:bg-gray-100 text-gray-500 px-3 py-2 rounded-lg border transition" title="도움말 보기">
<i class="ri-question-line"></i>
</button>
<a href="{{ route('rd.ai-quotation.index') }}" class="bg-white hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg border transition">
견적 목록
</a>
<a href="{{ route('rd.ai-quotation.create') }}" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition">
<a id="rd-btn-create" href="{{ route('rd.ai-quotation.create') }}" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition">
+ AI 견적 생성
</a>
</div>
</div>
<!-- 통계 카드 -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div id="rd-stats" class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<!-- 전체 -->
<div class="bg-white rounded-lg shadow-sm p-5">
<div class="flex items-center justify-between">
@@ -75,7 +78,7 @@
</div>
<!-- R&D 메뉴 카드 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div id="rd-menu-cards" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<!-- AI 견적 엔진 -->
<a href="{{ route('rd.ai-quotation.index') }}" class="bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition group">
<div class="flex items-start gap-4">
@@ -147,7 +150,7 @@
</div>
<!-- 최근 견적 요청 -->
<div class="bg-white rounded-lg shadow-sm">
<div id="rd-recent-list" class="bg-white rounded-lg shadow-sm">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">최근 AI 견적 요청</h2>
<a href="{{ route('rd.ai-quotation.index') }}" class="text-sm text-purple-600 hover:text-purple-800">전체 보기 </a>
@@ -179,3 +182,37 @@
</div>
</div>
@endsection
@push('scripts')
<script>
const rdGuide = new SamOnboarding('rd-dashboard', [
{
element: '#rd-stats',
title: '통계 현황',
description: 'AI 견적의 전체/완료/진행중/실패 현황을 한눈에 확인할 수 있습니다.',
side: 'bottom',
},
{
element: '#rd-btn-create',
title: 'AI 견적 생성',
description: '새로운 AI 견적을 생성합니다.<br>인터뷰 내용을 입력하면 AI가 자동으로 견적서를 작성합니다.',
side: 'left',
},
{
element: '#rd-menu-cards',
title: 'R&D 도구 모음',
description: 'AI 견적, 나레이션 제작, 자동도면 생성 등<br>연구개발 관련 도구들을 이용할 수 있습니다.',
side: 'top',
},
{
element: '#rd-recent-list',
title: '최근 견적 요청',
description: '최근 생성된 AI 견적 요청 목록입니다.<br>클릭하면 상세 페이지로 이동합니다.',
side: 'top',
},
]);
// 첫 방문 시 자동 시작
rdGuide.startIfFirstVisit();
</script>
@endpush

View File

@@ -0,0 +1,395 @@
@extends('layouts.app')
@section('title', '온보딩 도움말기능')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
<i class="ri-guide-line text-indigo-600"></i>
온보딩 도움말기능
</h1>
</div>
<!-- 소개 카드 -->
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl p-6 mb-8 text-white">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center shrink-0">
<i class="ri-lightbulb-flash-line text-2xl"></i>
</div>
<div>
<h2 class="text-xl font-bold mb-2">SAM 온보딩 가이드 시스템</h2>
<p class="text-white/90 text-sm leading-relaxed">
화면에 처음 진입하는 사용자에게 주요 기능을 단계별로 안내하는 인터랙티브 투어입니다.
버튼, 입력 필드, 데이터 영역 등을 하이라이트하며 사용법을 설명합니다.
</p>
</div>
</div>
</div>
<!-- 데모 섹션 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-8">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
<i class="ri-play-circle-line text-indigo-500"></i>
가이드 데모 체험
</h3>
<p class="text-sm text-gray-500 mt-1">아래 버튼을 눌러 페이지에서 온보딩 가이드가 어떻게 작동하는지 직접 체험해보세요.</p>
</div>
<!-- 데모 UI 영역 -->
<div class="p-6">
<div class="flex flex-wrap gap-6 mb-6">
<!-- 샘플 카드들 (가이드 대상) -->
<div id="demo-stat-card" class="bg-blue-50 rounded-lg p-5 border border-blue-200" style="flex: 1 1 200px; max-width: 280px;">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-blue-600 mb-1">오늘 등록 건수</p>
<p class="text-3xl font-bold text-blue-800">24</p>
</div>
<div class="w-10 h-10 bg-blue-200 rounded-full flex items-center justify-center">
<i class="ri-file-add-line text-blue-600 text-lg"></i>
</div>
</div>
</div>
<div id="demo-progress-card" class="bg-green-50 rounded-lg p-5 border border-green-200" style="flex: 1 1 200px; max-width: 280px;">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-green-600 mb-1">완료율</p>
<p class="text-3xl font-bold text-green-800">87%</p>
</div>
<div class="w-10 h-10 bg-green-200 rounded-full flex items-center justify-center">
<i class="ri-check-double-line text-green-600 text-lg"></i>
</div>
</div>
</div>
<div id="demo-alert-card" class="bg-amber-50 rounded-lg p-5 border border-amber-200" style="flex: 1 1 200px; max-width: 280px;">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-amber-600 mb-1">주의 항목</p>
<p class="text-3xl font-bold text-amber-800">3</p>
</div>
<div class="w-10 h-10 bg-amber-200 rounded-full flex items-center justify-center">
<i class="ri-alarm-warning-line text-amber-600 text-lg"></i>
</div>
</div>
</div>
</div>
<!-- 데모 액션 -->
<div class="flex flex-wrap gap-3 mb-6">
<button id="demo-btn-create" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
<i class="ri-add-line"></i> 신규 등록
</button>
<button id="demo-btn-filter" class="bg-white hover:bg-gray-50 text-gray-700 px-4 py-2 rounded-lg border transition flex items-center gap-2">
<i class="ri-filter-3-line"></i> 필터
</button>
<button id="demo-btn-export" class="bg-white hover:bg-gray-50 text-gray-700 px-4 py-2 rounded-lg border transition flex items-center gap-2">
<i class="ri-download-2-line"></i> 내보내기
</button>
</div>
<!-- 데모 테이블 -->
<div id="demo-data-table" class="border border-gray-200 rounded-lg overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-gray-600 font-medium">No.</th>
<th class="px-4 py-3 text-left text-gray-600 font-medium">항목명</th>
<th class="px-4 py-3 text-left text-gray-600 font-medium">상태</th>
<th class="px-4 py-3 text-left text-gray-600 font-medium">담당자</th>
<th class="px-4 py-3 text-left text-gray-600 font-medium">등록일</th>
</tr>
</thead>
<tbody>
<tr class="border-t border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 text-gray-800">1</td>
<td class="px-4 py-3 text-gray-800">방화셔터 KS-3000</td>
<td class="px-4 py-3"><span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">완료</span></td>
<td class="px-4 py-3 text-gray-600">김철수</td>
<td class="px-4 py-3 text-gray-500">2026-03-20</td>
</tr>
<tr class="border-t border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 text-gray-800">2</td>
<td class="px-4 py-3 text-gray-800">방화셔터 FD-2500</td>
<td class="px-4 py-3"><span class="bg-amber-100 text-amber-700 px-2 py-1 rounded-full text-xs">진행중</span></td>
<td class="px-4 py-3 text-gray-600">이영희</td>
<td class="px-4 py-3 text-gray-500">2026-03-21</td>
</tr>
<tr class="border-t border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 text-gray-800">3</td>
<td class="px-4 py-3 text-gray-800">일반셔터 NS-1200</td>
<td class="px-4 py-3"><span class="bg-blue-100 text-blue-700 px-2 py-1 rounded-full text-xs">대기</span></td>
<td class="px-4 py-3 text-gray-600">박지민</td>
<td class="px-4 py-3 text-gray-500">2026-03-22</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 데모 시작 버튼 -->
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-xl flex items-center gap-3">
<button id="btn-start-demo" onclick="startDemoGuide()" class="bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2.5 rounded-lg transition flex items-center gap-2 font-medium">
<i class="ri-play-fill"></i> 데모 가이드 시작
</button>
<button onclick="resetDemoGuide()" class="bg-white hover:bg-gray-100 text-gray-600 px-4 py-2.5 rounded-lg border transition flex items-center gap-2 text-sm">
<i class="ri-refresh-line"></i> 완료 상태 초기화
</button>
<span id="demo-status" class="text-sm text-gray-500 ml-2"></span>
</div>
</div>
<!-- 적용 가이드 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- 사용법 카드 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
<i class="ri-code-s-slash-line text-green-500"></i>
개발자 가이드: 페이지에 적용하기
</h3>
</div>
<div class="p-6">
<div class="bg-gray-900 rounded-lg p-4 text-sm font-mono text-green-400 overflow-x-auto">
<pre class="whitespace-pre"><span class="text-gray-500">// Blade 파일의 @push('scripts') 안에 추가</span>
<span class="text-purple-400">const</span> guide = <span class="text-purple-400">new</span> <span class="text-yellow-300">SamOnboarding</span>(<span class="text-amber-300">'my-page'</span>, [
{
element: <span class="text-amber-300">'#btn-create'</span>,
title: <span class="text-amber-300">'신규 등록'</span>,
description: <span class="text-amber-300">'새 항목을 등록합니다.'</span>,
},
{
element: <span class="text-amber-300">'.data-grid'</span>,
title: <span class="text-amber-300">'데이터 목록'</span>,
description: <span class="text-amber-300">'등록된 데이터를 확인하세요.'</span>,
},
]);
<span class="text-gray-500">// 방문 시에만 자동 시작</span>
guide.<span class="text-blue-300">startIfFirstVisit</span>();
<span class="text-gray-500">// 또는 버튼 클릭으로 시작</span>
guide.<span class="text-blue-300">start</span>();</pre>
</div>
</div>
</div>
<!-- 스텝 옵션 설명 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
<i class="ri-settings-3-line text-amber-500"></i>
스텝 옵션
</h3>
</div>
<div class="p-6">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-2 text-gray-600 font-medium">속성</th>
<th class="text-left py-2 text-gray-600 font-medium">필수</th>
<th class="text-left py-2 text-gray-600 font-medium">설명</th>
</tr>
</thead>
<tbody class="text-gray-700">
<tr class="border-b border-gray-100">
<td class="py-2.5"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-indigo-600">element</code></td>
<td class="py-2.5"><span class="text-red-500">*</span></td>
<td class="py-2.5">CSS 셀렉터 (<code>#id</code>, <code>.class</code>)</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-2.5"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-indigo-600">title</code></td>
<td class="py-2.5"><span class="text-red-500">*</span></td>
<td class="py-2.5">툴팁 제목</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-2.5"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-indigo-600">description</code></td>
<td class="py-2.5"><span class="text-red-500">*</span></td>
<td class="py-2.5">설명 텍스트 (HTML 가능)</td>
</tr>
<tr class="border-b border-gray-100">
<td class="py-2.5"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-indigo-600">side</code></td>
<td class="py-2.5"></td>
<td class="py-2.5">top, bottom, left, right</td>
</tr>
<tr>
<td class="py-2.5"><code class="bg-gray-100 px-1.5 py-0.5 rounded text-indigo-600">align</code></td>
<td class="py-2.5"></td>
<td class="py-2.5">start, center, end</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 적용된 페이지 목록 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
<i class="ri-pages-line text-blue-500"></i>
가이드가 적용된 페이지
</h3>
</div>
<div class="p-6">
<div id="applied-pages-list" class="space-y-3">
<!-- JS에서 동적 렌더링 -->
</div>
<div id="no-pages-msg" class="text-center py-8 text-gray-400">
<i class="ri-inbox-line text-4xl mb-2 block"></i>
<p>아직 가이드가 적용된 페이지가 없습니다.</p>
<p class="text-sm mt-1">개발자 가이드를 참고하여 페이지에 온보딩 가이드를 추가해보세요.</p>
</div>
</div>
</div>
@endsection
@push('scripts')
<style>
/* Driver.js SAM 커스텀 테마 */
.sam-onboarding-popover .driver-popover {
border-radius: 12px !important;
box-shadow: 0 20px 60px rgba(0,0,0,0.15) !important;
}
.driver-popover-title {
font-size: 16px !important;
font-weight: 700 !important;
color: #1e293b !important;
}
.driver-popover-description {
font-size: 14px !important;
color: #475569 !important;
line-height: 1.6 !important;
}
.driver-popover-progress-text {
font-size: 12px !important;
color: #94a3b8 !important;
}
.driver-popover-next-btn {
background-color: #4f46e5 !important;
border-radius: 8px !important;
padding: 6px 16px !important;
font-size: 13px !important;
text-shadow: none !important;
}
.driver-popover-next-btn:hover {
background-color: #4338ca !important;
}
.driver-popover-prev-btn {
border-radius: 8px !important;
padding: 6px 16px !important;
font-size: 13px !important;
color: #6366f1 !important;
}
.driver-popover-close-btn {
color: #94a3b8 !important;
}
.driver-popover-close-btn:hover {
color: #475569 !important;
}
</style>
<script>
// 데모 가이드 정의
const demoGuide = new SamOnboarding('onboarding-demo', [
{
element: '#demo-stat-card',
title: '1. 통계 카드',
description: '주요 지표를 한눈에 확인할 수 있는 통계 카드입니다.<br>실시간으로 업데이트되는 현황을 파악하세요.',
side: 'bottom',
},
{
element: '#demo-progress-card',
title: '2. 진행 현황',
description: '전체 작업 대비 완료율을 보여줍니다.<br>목표 달성률을 추적하세요.',
side: 'bottom',
},
{
element: '#demo-alert-card',
title: '3. 주의 항목',
description: '확인이 필요한 항목의 수를 표시합니다.<br>클릭하면 상세 목록으로 이동합니다.',
side: 'bottom',
},
{
element: '#demo-btn-create',
title: '4. 신규 등록',
description: '새로운 항목을 등록할 수 있는 버튼입니다.<br>클릭하면 등록 폼이 열립니다.',
side: 'bottom',
},
{
element: '#demo-btn-filter',
title: '5. 필터',
description: '날짜, 상태, 담당자 등 다양한 조건으로<br>데이터를 필터링할 수 있습니다.',
side: 'bottom',
},
{
element: '#demo-data-table',
title: '6. 데이터 목록',
description: '등록된 데이터를 테이블 형태로 확인합니다.<br>행을 클릭하면 상세 페이지로 이동합니다.',
side: 'top',
},
]);
function startDemoGuide() {
demoGuide.reset();
demoGuide.start();
}
function resetDemoGuide() {
demoGuide.reset();
updateDemoStatus();
}
function updateDemoStatus() {
const status = document.getElementById('demo-status');
if (demoGuide.isCompleted()) {
status.innerHTML = '<span class="text-green-600"><i class="ri-check-line"></i> 데모 가이드 완료됨</span>';
} else {
status.innerHTML = '<span class="text-gray-400">아직 체험하지 않음</span>';
}
}
// 적용된 페이지 목록 렌더링
function renderAppliedPages() {
const keys = GuideRegistry.keys();
const list = document.getElementById('applied-pages-list');
const noMsg = document.getElementById('no-pages-msg');
if (keys.length === 0) {
noMsg.style.display = '';
return;
}
noMsg.style.display = 'none';
list.innerHTML = keys.map(key => {
const completed = localStorage.getItem(`sam_onboarding_${key}`) === 'completed';
const statusBadge = completed
? '<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs">완료</span>'
: '<span class="bg-gray-100 text-gray-500 px-2 py-1 rounded-full text-xs">미완료</span>';
return `
<div class="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:bg-gray-50">
<div class="flex items-center gap-3">
<i class="ri-file-text-line text-gray-400"></i>
<span class="text-sm font-medium text-gray-700">${key}</span>
</div>
<div class="flex items-center gap-2">
${statusBadge}
<button onclick="localStorage.removeItem('sam_onboarding_${key}'); renderAppliedPages();"
class="text-xs text-gray-400 hover:text-gray-600" title="초기화">
<i class="ri-refresh-line"></i>
</button>
</div>
</div>`;
}).join('');
}
document.addEventListener('DOMContentLoaded', function() {
updateDemoStatus();
renderAppliedPages();
});
</script>
@endpush

View File

@@ -479,6 +479,9 @@
// 견적서 자동기획 프로젝트
Route::get('/auto-quotation', [RdController::class, 'autoQuotation'])->name('auto-quotation');
// 온보딩 도움말기능
Route::get('/onboarding-guide', [RdController::class, 'onboardingGuide'])->name('onboarding-guide');
});
// 일일 스크럼 (Blade 화면만)