[온보딩 API (routers/onboarding.py)] - 8단계 온보딩 플로우: 0. 환영 → 1. 비밀번호변경 → 2. 대시보드 → 3. 프로젝트등록 → 4. 서버등록 → 5. 소스코드등록 → 6. 메신저봇 → 7. 완료 - POST /api/onboarding/message: 현재화면 + 사용자질문 → Ollama 답변 - 화면별 스포트라이트 target 정의 (CSS selector) - 사용자별 단계 상태 영속 관리 [온보딩 챗봇 UI (static/onboarding.js)] - 우측 고정 패널 (360px, 모바일 하단 슬라이드) - 타이핑 애니메이션 효과 + 마크다운 렌더링 - 스포트라이트: 현재 단계 UI 요소를 하이라이트 - 화면 변화 감지 (MutationObserver + click 이벤트) - 최소화/닫기/재시작 제어 - 사용자 질문 입력 → Ollama 실시간 답변 - 온보딩 완료 후 우측하단 ? 도움말 버튼 - 액션버튼: next/navigate/external/complete/skip [설치 자동화 연동] - install_auto.sh: 설치 완료 후 onboarding reset API 호출 - 브라우저 열릴 때 챗봇 자동 표시 사용자 경험: 설치 완료 → 브라우저 자동 오픈 → 챗봇 우측 등장 → "환영합니다!" → 비밀번호변경 화면 이동 안내 → CMDB 서버등록 스포트라이트 → Gitea 소스등록 → 완료 후 ? 버튼으로 재시작 가능 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
631 lines
22 KiB
JavaScript
631 lines
22 KiB
JavaScript
/**
|
|
* GUARDiA ITSM 온보딩 가이드 챗봇
|
|
* 설치 완료 후 자동 실행 — 로그인부터 프로젝트 등록까지 단계별 안내
|
|
*/
|
|
(function GUARDiAOnboarding() {
|
|
'use strict';
|
|
|
|
// ── 상태 ──────────────────────────────────────────────────
|
|
let state = {
|
|
visible: false,
|
|
minimized: false,
|
|
currentStep: null,
|
|
totalSteps: 8,
|
|
messages: [],
|
|
isTyping: false,
|
|
spotlightEl: null,
|
|
};
|
|
|
|
let _token = null;
|
|
let _pollId = null;
|
|
|
|
// ── 초기화 ────────────────────────────────────────────────
|
|
function init() {
|
|
// 토큰 확인 (로그인 상태만)
|
|
_token = localStorage.getItem('access_token');
|
|
if (!_token) return;
|
|
|
|
// 온보딩 상태 조회
|
|
fetchStatus().then(status => {
|
|
if (!status) return;
|
|
if (status.show_bot) {
|
|
buildUI();
|
|
show();
|
|
loadStep(status.current);
|
|
// 화면 변화 감지
|
|
watchNavigation();
|
|
} else {
|
|
// 완료됐어도 우측하단 도움말 버튼만 남김
|
|
buildHelpButton();
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── API ───────────────────────────────────────────────────
|
|
async function api(method, path, body) {
|
|
const opts = {
|
|
method,
|
|
headers: {
|
|
'Authorization': 'Bearer ' + _token,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
if (body) opts.body = JSON.stringify(body);
|
|
try {
|
|
const r = await fetch(path, opts);
|
|
return r.ok ? r.json() : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function fetchStatus() {
|
|
return api('GET', '/api/onboarding/status');
|
|
}
|
|
|
|
async function postStep(stepId, action) {
|
|
return api('POST', '/api/onboarding/step', { step_id: stepId, action });
|
|
}
|
|
|
|
async function postMessage(userMessage) {
|
|
return api('POST', '/api/onboarding/message', {
|
|
current_view: location.pathname,
|
|
current_step: state.currentStep?.id,
|
|
user_message: userMessage,
|
|
});
|
|
}
|
|
|
|
async function completeOnboarding() {
|
|
await api('POST', '/api/onboarding/complete');
|
|
}
|
|
|
|
async function dismissOnboarding() {
|
|
await api('POST', '/api/onboarding/dismiss');
|
|
}
|
|
|
|
// ── UI 빌드 ───────────────────────────────────────────────
|
|
function buildUI() {
|
|
if (document.getElementById('grd-onboarding')) return;
|
|
|
|
const panel = document.createElement('div');
|
|
panel.id = 'grd-onboarding';
|
|
panel.innerHTML = `
|
|
<div id="grd-ob-header">
|
|
<div class="grd-ob-avatar">🤖</div>
|
|
<div class="grd-ob-header-info">
|
|
<div class="grd-ob-title">GUARDiA 가이드</div>
|
|
<div class="grd-ob-subtitle" id="grd-ob-subtitle">초기 설정 안내</div>
|
|
</div>
|
|
<div class="grd-ob-header-actions">
|
|
<button class="grd-ob-btn-icon" id="grd-ob-minimize" title="최소화">─</button>
|
|
<button class="grd-ob-btn-icon" id="grd-ob-close" title="닫기">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 진행 바 -->
|
|
<div id="grd-ob-progress-bar">
|
|
<div id="grd-ob-progress-fill"></div>
|
|
</div>
|
|
<div id="grd-ob-progress-label">1 / 8 단계</div>
|
|
|
|
<!-- 메시지 영역 -->
|
|
<div id="grd-ob-messages"></div>
|
|
|
|
<!-- 입력 영역 -->
|
|
<div id="grd-ob-input-area">
|
|
<input id="grd-ob-input" type="text" placeholder="질문이 있으면 입력하세요..." />
|
|
<button id="grd-ob-send" title="전송">➤</button>
|
|
</div>
|
|
`;
|
|
|
|
// 스타일
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#grd-onboarding {
|
|
position: fixed;
|
|
right: 0; top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 360px;
|
|
max-height: 85vh;
|
|
background: #1e2333;
|
|
border-left: 3px solid #818cf8;
|
|
border-radius: 16px 0 0 16px;
|
|
box-shadow: -8px 0 40px rgba(0,0,0,.4);
|
|
display: flex; flex-direction: column;
|
|
z-index: 9999;
|
|
font-family: 'Noto Sans KR', Arial, sans-serif;
|
|
font-size: 13px; color: #e2e8f0;
|
|
transition: all .3s cubic-bezier(.4,0,.2,1);
|
|
overflow: hidden;
|
|
}
|
|
#grd-onboarding.minimized {
|
|
height: 52px; max-height: 52px;
|
|
border-radius: 16px 0 0 16px;
|
|
}
|
|
#grd-onboarding.minimized #grd-ob-messages,
|
|
#grd-onboarding.minimized #grd-ob-input-area,
|
|
#grd-onboarding.minimized #grd-ob-progress-bar,
|
|
#grd-onboarding.minimized #grd-ob-progress-label { display: none; }
|
|
|
|
#grd-ob-header {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 12px 14px;
|
|
background: #252b3b;
|
|
cursor: pointer; flex-shrink: 0;
|
|
}
|
|
.grd-ob-avatar {
|
|
width: 36px; height: 36px; border-radius: 50%;
|
|
background: linear-gradient(135deg,#818cf8,#6366f1);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 18px; flex-shrink: 0;
|
|
animation: pulse 2s ease infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%,100%{box-shadow:0 0 0 0 rgba(129,140,248,.4)}
|
|
50%{box-shadow:0 0 0 8px rgba(129,140,248,0)}
|
|
}
|
|
.grd-ob-header-info { flex: 1; }
|
|
.grd-ob-title { font-weight: 700; font-size: 14px; color: #fff; }
|
|
.grd-ob-subtitle { font-size: 11px; color: #818cf8; margin-top: 1px; }
|
|
.grd-ob-header-actions { display: flex; gap: 4px; }
|
|
.grd-ob-btn-icon {
|
|
width: 26px; height: 26px; border-radius: 6px;
|
|
background: rgba(255,255,255,.08);
|
|
color: #94a3b8; border: none; cursor: pointer;
|
|
font-size: 13px; display: flex; align-items: center; justify-content: center;
|
|
transition: all .15s;
|
|
}
|
|
.grd-ob-btn-icon:hover { background: rgba(255,255,255,.15); color: #fff; }
|
|
|
|
#grd-ob-progress-bar {
|
|
height: 4px; background: rgba(255,255,255,.08);
|
|
flex-shrink: 0;
|
|
}
|
|
#grd-ob-progress-fill {
|
|
height: 100%; background: linear-gradient(90deg,#818cf8,#6ee7b7);
|
|
transition: width .5s ease;
|
|
}
|
|
#grd-ob-progress-label {
|
|
font-size: 10px; color: #64748b;
|
|
text-align: right; padding: 3px 12px 0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#grd-ob-messages {
|
|
flex: 1; overflow-y: auto; padding: 14px 12px;
|
|
display: flex; flex-direction: column; gap: 10px;
|
|
scrollbar-width: thin;
|
|
}
|
|
.grd-ob-msg {
|
|
display: flex; gap: 8px; align-items: flex-start;
|
|
}
|
|
.grd-ob-msg.user { flex-direction: row-reverse; }
|
|
.grd-ob-msg-avatar {
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|
background: linear-gradient(135deg,#818cf8,#6366f1);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 14px; flex-shrink: 0;
|
|
}
|
|
.grd-ob-msg.user .grd-ob-msg-avatar {
|
|
background: rgba(99,102,241,.25);
|
|
}
|
|
.grd-ob-msg-bubble {
|
|
background: #252b3b;
|
|
border-radius: 4px 14px 14px 14px;
|
|
padding: 10px 13px;
|
|
max-width: 270px;
|
|
line-height: 1.6;
|
|
white-space: pre-line;
|
|
}
|
|
.grd-ob-msg.user .grd-ob-msg-bubble {
|
|
background: #4f46e5;
|
|
border-radius: 14px 4px 14px 14px;
|
|
}
|
|
.grd-ob-msg-bubble strong { color: #a5b4fc; }
|
|
.grd-ob-msg-bubble code {
|
|
background: rgba(255,255,255,.1);
|
|
padding: 2px 5px; border-radius: 4px;
|
|
font-family: monospace; font-size: 12px;
|
|
}
|
|
.grd-ob-actions {
|
|
display: flex; flex-wrap: wrap; gap: 6px;
|
|
margin-top: 8px; padding-left: 36px;
|
|
}
|
|
.grd-ob-action-btn {
|
|
padding: 7px 14px; border-radius: 20px;
|
|
background: rgba(129,140,248,.18);
|
|
color: #818cf8; border: 1px solid rgba(129,140,248,.3);
|
|
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
transition: all .15s; white-space: nowrap;
|
|
}
|
|
.grd-ob-action-btn:hover {
|
|
background: rgba(129,140,248,.35);
|
|
color: #fff;
|
|
}
|
|
.grd-ob-action-btn.primary {
|
|
background: #4f46e5; color: #fff; border-color: transparent;
|
|
}
|
|
.grd-ob-action-btn.primary:hover { background: #4338ca; }
|
|
|
|
.grd-ob-typing {
|
|
display: flex; gap: 4px; padding: 10px 14px;
|
|
background: #252b3b; border-radius: 4px 14px 14px 14px;
|
|
width: fit-content;
|
|
}
|
|
.grd-ob-typing span {
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|
background: #818cf8; animation: typing .8s ease infinite;
|
|
}
|
|
.grd-ob-typing span:nth-child(2) { animation-delay: .2s; }
|
|
.grd-ob-typing span:nth-child(3) { animation-delay: .4s; }
|
|
@keyframes typing { 0%,60%,100%{opacity:.3} 30%{opacity:1} }
|
|
|
|
#grd-ob-input-area {
|
|
display: flex; gap: 8px; padding: 10px 12px;
|
|
border-top: 1px solid rgba(255,255,255,.07);
|
|
flex-shrink: 0;
|
|
}
|
|
#grd-ob-input {
|
|
flex: 1; background: rgba(255,255,255,.06);
|
|
border: 1px solid rgba(255,255,255,.1);
|
|
border-radius: 8px; color: #e2e8f0;
|
|
padding: 8px 12px; font-size: 13px; outline: none;
|
|
font-family: inherit;
|
|
}
|
|
#grd-ob-input:focus { border-color: #818cf8; }
|
|
#grd-ob-send {
|
|
width: 34px; height: 34px; border-radius: 8px;
|
|
background: #4f46e5; color: #fff; border: none;
|
|
cursor: pointer; font-size: 14px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: background .15s;
|
|
}
|
|
#grd-ob-send:hover { background: #4338ca; }
|
|
|
|
/* 스포트라이트 */
|
|
.grd-spotlight {
|
|
position: fixed; z-index: 9998;
|
|
border: 2px solid #818cf8;
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 0 9999px rgba(0,0,0,.5), 0 0 24px rgba(129,140,248,.6);
|
|
pointer-events: none;
|
|
transition: all .3s ease;
|
|
animation: spotlight-pulse 2s ease infinite;
|
|
}
|
|
@keyframes spotlight-pulse {
|
|
0%,100%{box-shadow:0 0 0 9999px rgba(0,0,0,.5),0 0 24px rgba(129,140,248,.4)}
|
|
50%{box-shadow:0 0 0 9999px rgba(0,0,0,.5),0 0 40px rgba(129,140,248,.8)}
|
|
}
|
|
|
|
/* 도움말 버튼 (온보딩 완료 후) */
|
|
#grd-help-btn {
|
|
position: fixed; right: 20px; bottom: 20px;
|
|
width: 48px; height: 48px; border-radius: 50%;
|
|
background: linear-gradient(135deg,#818cf8,#6366f1);
|
|
color: #fff; border: none; cursor: pointer;
|
|
font-size: 22px; z-index: 9000;
|
|
box-shadow: 0 4px 16px rgba(129,140,248,.4);
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: transform .2s;
|
|
}
|
|
#grd-help-btn:hover { transform: scale(1.1); }
|
|
|
|
@media (max-width: 768px) {
|
|
#grd-onboarding {
|
|
width: 100%; right: 0; top: auto; bottom: 0;
|
|
transform: none; border-radius: 16px 16px 0 0;
|
|
border-left: none; border-top: 3px solid #818cf8;
|
|
max-height: 65vh;
|
|
}
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
document.body.appendChild(panel);
|
|
|
|
// 이벤트 연결
|
|
document.getElementById('grd-ob-minimize').onclick = toggleMinimize;
|
|
document.getElementById('grd-ob-close').onclick = closeBotConfirm;
|
|
document.getElementById('grd-ob-header').ondblclick = toggleMinimize;
|
|
document.getElementById('grd-ob-send').onclick = sendUserMessage;
|
|
document.getElementById('grd-ob-input').onkeydown = e => {
|
|
if (e.key === 'Enter') sendUserMessage();
|
|
};
|
|
}
|
|
|
|
function buildHelpButton() {
|
|
if (document.getElementById('grd-help-btn')) return;
|
|
const btn = document.createElement('button');
|
|
btn.id = 'grd-help-btn';
|
|
btn.textContent = '?';
|
|
btn.title = 'GUARDiA 도움말';
|
|
btn.onclick = () => {
|
|
_onboarding_state_dismissed = false;
|
|
api('POST', '/api/onboarding/reset').then(() => location.reload());
|
|
};
|
|
document.body.appendChild(btn);
|
|
}
|
|
|
|
// ── 메시지 렌더링 ─────────────────────────────────────────
|
|
function renderMessage(text, isUser = false, actions = []) {
|
|
const msgArea = document.getElementById('grd-ob-messages');
|
|
if (!msgArea) return;
|
|
|
|
const msg = document.createElement('div');
|
|
msg.className = 'grd-ob-msg' + (isUser ? ' user' : '');
|
|
|
|
const avi = document.createElement('div');
|
|
avi.className = 'grd-ob-msg-avatar';
|
|
avi.textContent = isUser ? '👤' : '🤖';
|
|
|
|
const bubble = document.createElement('div');
|
|
bubble.className = 'grd-ob-msg-bubble';
|
|
// 마크다운 간단 렌더링
|
|
bubble.innerHTML = text
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
.replace(/^```[\s\S]*?```$/gm, m => `<code style="display:block;padding:8px;margin:4px 0;background:rgba(0,0,0,.3);border-radius:6px;font-size:11px;white-space:pre">${m.replace(/```\w*\n?/g,'').trim()}</code>`)
|
|
.replace(/\n/g, '<br>');
|
|
|
|
msg.appendChild(avi);
|
|
msg.appendChild(bubble);
|
|
msgArea.appendChild(msg);
|
|
|
|
// 액션 버튼
|
|
if (actions && actions.length > 0) {
|
|
const actDiv = document.createElement('div');
|
|
actDiv.className = 'grd-ob-actions';
|
|
actions.forEach((act, i) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'grd-ob-action-btn' + (i === 0 ? ' primary' : '');
|
|
btn.textContent = act.label;
|
|
btn.onclick = () => handleAction(act);
|
|
actDiv.appendChild(btn);
|
|
});
|
|
msgArea.appendChild(actDiv);
|
|
}
|
|
|
|
msgArea.scrollTop = msgArea.scrollHeight;
|
|
}
|
|
|
|
function showTyping() {
|
|
const msgArea = document.getElementById('grd-ob-messages');
|
|
if (!msgArea) return;
|
|
const el = document.createElement('div');
|
|
el.className = 'grd-ob-msg';
|
|
el.id = 'grd-ob-typing';
|
|
el.innerHTML = `
|
|
<div class="grd-ob-msg-avatar">🤖</div>
|
|
<div class="grd-ob-typing">
|
|
<span></span><span></span><span></span>
|
|
</div>`;
|
|
msgArea.appendChild(el);
|
|
msgArea.scrollTop = msgArea.scrollHeight;
|
|
}
|
|
|
|
function hideTyping() {
|
|
document.getElementById('grd-ob-typing')?.remove();
|
|
}
|
|
|
|
// ── 단계 로드 ─────────────────────────────────────────────
|
|
function loadStep(step) {
|
|
if (!step) return;
|
|
state.currentStep = step;
|
|
|
|
// 헤더 업데이트
|
|
const sub = document.getElementById('grd-ob-subtitle');
|
|
if (sub) sub.textContent = `${step.icon} ${step.title}`;
|
|
|
|
// 진행 바 업데이트
|
|
const pct = Math.round(step.order / 7 * 100);
|
|
const fill = document.getElementById('grd-ob-progress-fill');
|
|
const label = document.getElementById('grd-ob-progress-label');
|
|
if (fill) fill.style.width = pct + '%';
|
|
if (label) label.textContent = `${step.order + 1} / 8 단계`;
|
|
|
|
// 타이핑 효과 후 메시지 표시
|
|
showTyping();
|
|
setTimeout(() => {
|
|
hideTyping();
|
|
renderMessage(step.message, false, step.actions);
|
|
// 스포트라이트
|
|
if (step.target) spotlightElement(step.target);
|
|
}, 800);
|
|
|
|
// 화면 이동 힌트
|
|
if (step.view && location.pathname !== step.view.split('?')[0]) {
|
|
setTimeout(() => {
|
|
renderMessage(`💡 현재 화면: **${location.pathname}**\n이 단계는 **${step.view}** 화면에서 진행됩니다.`, false, [
|
|
{ label: `${step.view} 이동`, action: 'navigate', path: step.view }
|
|
]);
|
|
}, 1500);
|
|
}
|
|
}
|
|
|
|
// ── 액션 처리 ─────────────────────────────────────────────
|
|
async function handleAction(act) {
|
|
const step = state.currentStep;
|
|
|
|
switch (act.action) {
|
|
case 'next':
|
|
case 'complete_step':
|
|
if (step) {
|
|
showTyping();
|
|
const result = await postStep(step.id, 'complete');
|
|
hideTyping();
|
|
if (result?.step) loadStep(result.step);
|
|
}
|
|
break;
|
|
|
|
case 'navigate':
|
|
if (act.path) {
|
|
if (act.path.startsWith('http')) {
|
|
window.open(act.path, '_blank');
|
|
} else if (act.path.includes('?view=')) {
|
|
const viewName = act.path.split('?view=')[1];
|
|
// GUARDiA SPA 뷰 전환
|
|
const navItem = document.querySelector(`[data-view="${viewName}"]`);
|
|
if (navItem) navItem.click();
|
|
} else {
|
|
location.href = act.path;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'external':
|
|
window.open(act.url || act.path, '_blank');
|
|
break;
|
|
|
|
case 'complete':
|
|
await completeOnboarding();
|
|
renderMessage('🎉 모든 설정이 완료되었습니다!\n이제 GUARDiA의 모든 기능을 사용하세요.\n\n우측 하단 **?** 버튼으로 언제든 가이드를 다시 볼 수 있습니다.', false, []);
|
|
setTimeout(() => {
|
|
hide();
|
|
buildHelpButton();
|
|
}, 3000);
|
|
break;
|
|
|
|
case 'skip':
|
|
if (step) {
|
|
const result = await postStep(step.id, 'skip');
|
|
if (result?.step) loadStep(result.step);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ── 사용자 메시지 전송 ────────────────────────────────────
|
|
async function sendUserMessage() {
|
|
const input = document.getElementById('grd-ob-input');
|
|
if (!input) return;
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
input.value = '';
|
|
renderMessage(text, true);
|
|
|
|
showTyping();
|
|
const resp = await postMessage(text);
|
|
hideTyping();
|
|
|
|
if (resp?.message) {
|
|
renderMessage(resp.message, false);
|
|
}
|
|
}
|
|
|
|
// ── 스포트라이트 ──────────────────────────────────────────
|
|
function spotlightElement(selector) {
|
|
// 기존 스포트라이트 제거
|
|
clearSpotlight();
|
|
|
|
const el = document.querySelector(selector);
|
|
if (!el) return;
|
|
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
setTimeout(() => {
|
|
const rect = el.getBoundingClientRect();
|
|
const pad = 6;
|
|
const spot = document.createElement('div');
|
|
spot.className = 'grd-spotlight';
|
|
spot.id = 'grd-spotlight';
|
|
spot.style.cssText = `
|
|
top: ${rect.top - pad + window.scrollY}px;
|
|
left: ${rect.left - pad}px;
|
|
width: ${rect.width + pad * 2}px;
|
|
height: ${rect.height + pad * 2}px;
|
|
`;
|
|
document.body.appendChild(spot);
|
|
|
|
// 8초 후 자동 제거
|
|
setTimeout(clearSpotlight, 8000);
|
|
}, 400);
|
|
}
|
|
|
|
function clearSpotlight() {
|
|
document.getElementById('grd-spotlight')?.remove();
|
|
}
|
|
|
|
// ── 화면 변화 감지 ────────────────────────────────────────
|
|
function watchNavigation() {
|
|
// SPA 네비게이션 감지 (hash/pushState)
|
|
let lastPath = location.pathname;
|
|
const observer = new MutationObserver(() => {
|
|
if (location.pathname !== lastPath) {
|
|
lastPath = location.pathname;
|
|
onViewChange(location.pathname);
|
|
}
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
// GUARDiA nav-item 클릭 감지
|
|
document.addEventListener('click', e => {
|
|
const navItem = e.target.closest('[data-view]');
|
|
if (navItem) {
|
|
setTimeout(() => onViewChange(location.pathname, navItem.dataset.view), 300);
|
|
}
|
|
});
|
|
}
|
|
|
|
function onViewChange(path, viewId) {
|
|
clearSpotlight();
|
|
const step = state.currentStep;
|
|
if (!step || !step.view) return;
|
|
|
|
const stepView = step.view.split('?')[0];
|
|
if (path === stepView || (viewId && step.target?.includes(viewId))) {
|
|
// 현재 단계의 화면으로 이동했으면 힌트 표시
|
|
setTimeout(() => {
|
|
if (step.target) spotlightElement(step.target);
|
|
renderMessage(`✅ 좋아요! 지금 **${step.title}** 단계를 진행 중입니다.\n\n${step.message.split('\n')[0]}`, false);
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
// ── 패널 제어 ─────────────────────────────────────────────
|
|
function show() {
|
|
const panel = document.getElementById('grd-onboarding');
|
|
if (panel) { panel.style.display = 'flex'; state.visible = true; }
|
|
}
|
|
|
|
function hide() {
|
|
const panel = document.getElementById('grd-onboarding');
|
|
if (panel) { panel.style.display = 'none'; state.visible = false; }
|
|
clearSpotlight();
|
|
}
|
|
|
|
function toggleMinimize() {
|
|
const panel = document.getElementById('grd-onboarding');
|
|
if (!panel) return;
|
|
state.minimized = !state.minimized;
|
|
panel.classList.toggle('minimized', state.minimized);
|
|
document.getElementById('grd-ob-minimize').textContent = state.minimized ? '□' : '─';
|
|
}
|
|
|
|
function closeBotConfirm() {
|
|
if (confirm('온보딩 가이드를 닫을까요?\n언제든 우측하단 ? 버튼으로 다시 열 수 있습니다.')) {
|
|
dismissOnboarding();
|
|
hide();
|
|
buildHelpButton();
|
|
}
|
|
}
|
|
|
|
// ── 진입점 ────────────────────────────────────────────────
|
|
// DOM 준비 후 실행
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
// 로그인 완료 후 토큰이 설정되면 초기화
|
|
setTimeout(init, 1000);
|
|
}
|
|
|
|
// 로그인 이벤트 감지 (localStorage 변화)
|
|
window.addEventListener('storage', e => {
|
|
if (e.key === 'access_token' && e.newValue && !_token) {
|
|
_token = e.newValue;
|
|
setTimeout(init, 500);
|
|
}
|
|
});
|
|
|
|
// 전역 노출 (수동 재시작)
|
|
window.GUARDiAOnboarding = { restart: () => { api('POST','/api/onboarding/reset').then(()=>location.reload()); } };
|
|
|
|
})();
|