/** * 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 = `
🤖
GUARDiA 가이드
초기 설정 안내
1 / 8 단계
`; // 스타일 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, '$1') .replace(/`(.+?)`/g, '$1') .replace(/^```[\s\S]*?```$/gm, m => `${m.replace(/```\w*\n?/g,'').trim()}`) .replace(/\n/g, '
'); 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 = `
🤖
`; 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()); } }; })();