feat: [RD] 온보딩 도움말기능 구현 (Driver.js 기반)
- Driver.js 라이브러리 설치 및 Vite 번들 등록 - SamOnboarding JS 모듈 작성 (가이드 정의/실행/상태저장) - 온보딩 도움말기능 관리 페이지 (데모 체험, 개발자 가이드) - RD 대시보드에 온보딩 가이드 시범 적용 - 메뉴 라우트 추가 (/rd/onboarding-guide)
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
7
public/build/assets/app-BiJrT4B1.js
Normal file
7
public/build/assets/app-BiJrT4B1.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/assets/app-C-Kb46Vq.css
Normal file
1
public/build/assets/app-C-Kb46Vq.css
Normal file
File diff suppressed because one or more lines are too long
1
public/build/assets/app-DB0Q8XAf.css
Normal file
1
public/build/assets/app-DB0Q8XAf.css
Normal 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}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
140
resources/js/onboarding-guide.js
Normal file
140
resources/js/onboarding-guide.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
395
resources/views/rd/onboarding-guide/index.blade.php
Normal file
395
resources/views/rd/onboarding-guide/index.blade.php
Normal 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
|
||||
@@ -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 화면만)
|
||||
|
||||
Reference in New Issue
Block a user