(function() { 'use strict'; // ========== 버전 및 설정 ========== var VERSION = '1.0.0'; var BUILD_DATE = '2026-01-28'; // ========== DOM 요소 캐싱 ========== var elements = {}; function initElements() { elements.fab = document.getElementById('vpn-fab'); elements.overlay = document.getElementById('vpn-overlay'); elements.panel = document.getElementById('vpn-panel'); elements.closeBtn = document.getElementById('vpn-close-btn'); elements.chat = document.getElementById('vpn-chat'); elements.input = document.getElementById('vpn-input'); elements.sendBtn = document.getElementById('vpn-send-btn'); elements.wizard = document.getElementById('vpn-wizard'); elements.header = document.getElementById('vpn-header'); elements.menuBar = document.getElementById('vpn-menu-bar'); elements.fabTooltip = document.getElementById('fab-tooltip'); } // 기존 변수 호환성 유지 var fab, overlay, panel, closeBtn, chat, input, sendBtn, wizard, header, menuBar, fabTooltip; function assignLegacyVariables() { fab = elements.fab; overlay = elements.overlay; panel = elements.panel; closeBtn = elements.closeBtn; chat = elements.chat; input = elements.input; sendBtn = elements.sendBtn; wizard = elements.wizard; header = elements.header; menuBar = elements.menuBar; fabTooltip = elements.fabTooltip; } // 초기화 initElements(); assignLegacyVariables(); // Edge Client(내장 브라우저) 여부 — 챗봇 제한 시 안내용 var isEdgeClient = false; try { if (typeof externalWebHost !== 'undefined' && externalWebHost.isAvailable()) { isEdgeClient = true; } } catch (e) {} // ========== 세션 만료 감지 ========== var isSessionExpired = false; function checkSessionExpired() { // 세션 만료 페이지 감지 var pageText = document.body ? document.body.innerText : ''; var sessionExpiredKeywords = ['세션 만료', '세션이 만료', '시간 초과', 'session expired', 'Session Expired']; for (var i = 0; i < sessionExpiredKeywords.length; i++) { if (pageText.indexOf(sessionExpiredKeywords[i]) !== -1) { isSessionExpired = true; return true; } } // "세션 새로 시작" 버튼이 있는지 확인 var sessionBtn = document.querySelector('a[href*="session"], button[onclick*="session"]'); if (sessionBtn && pageText.indexOf('새로 시작') !== -1) { isSessionExpired = true; return true; } return false; } function showSessionExpiredNotice() { if (!isSessionExpired) return; // 챗봇이 열릴 때 세션 만료 안내 메시지 표시 setTimeout(function() { if (elements.chat) { var noticeHtml = '
' + '
' + '' + '
' + '
' + '⏰ 세션이 만료되었습니다

' + '화면의 [ 세션 새로 시작 ] 버튼을 클릭하여
다시 로그인해주세요!' + '
' + '
'; elements.chat.innerHTML = noticeHtml; } }, 300); } // 페이지 로드 시 세션 만료 체크 checkSessionExpired(); // ========== 에러 핸들링 유틸리티 ========== function safeExecute(fn, context) { return function() { try { return fn.apply(context || this, arguments); } catch (e) { console.error('[오비서(Openbase By Service) Error]', e); // 사용자에게 친화적인 에러 표시 if (chat && typeof addMsg === 'function') { addMsg('bot', '⚠️ 일시적인 오류가 발생했어요. 잠시 후 다시 시도해주세요.'); } } }; } // ========== 유틸리티 함수 ========== function debounce(func, wait) { var timeout; return function() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; } function throttle(func, limit) { var inThrottle; return function() { var args = arguments, context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(function() { inThrottle = false; }, limit); } }; } // ========== 로그 서버 설정 ========== var LOG_CONFIG = { // 서버 API 엔드포인트 (설정 필요) serverUrl: '', // 예: 'https://your-server.com/api/chatbot-logs' // 서버 전송 활성화 여부 enableServerLog: false, // 실시간 전송 (각 액션마다) vs 세션 종료시 전송 realTimeLog: false, // 로컬 저장 활성화 enableLocalLog: true, // 콘솔 출력 활성화 enableConsoleLog: true }; // 설정 함수 (외부에서 호출 가능) - ES5 호환 (Object.assign 미지원 환경) window.configureOBChatbotLog = function(config) { var key; if (config && typeof config === 'object') { for (key in config) { if (config.hasOwnProperty(key)) { LOG_CONFIG[key] = config[key]; } } } console.log('[오비서(Openbase By Service)] 로그 설정 업데이트:', LOG_CONFIG); }; // ========== 사용자 행동 로그 시스템 ========== var userLog = { sessionId: 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9), userAgent: navigator.userAgent, platform: navigator.platform, language: navigator.language, screenSize: window.screen.width + 'x' + window.screen.height, startTime: new Date().toISOString(), pageUrl: window.location.href, referrer: document.referrer || 'direct', userId: null, // 사용자가 입력한 계정 정보로 업데이트 userContact: null, actions: [], ticketData: null // 티켓 접수 시 데이터 }; // 사용자 식별 정보 업데이트 function updateUserIdentity(userId, contact) { userLog.userId = userId || userLog.userId; userLog.userContact = contact || userLog.userContact; } function logAction(action, detail) { var logEntry = { timestamp: new Date().toISOString(), action: action, detail: detail || '', timeFromStart: Date.now() - new Date(userLog.startTime).getTime() }; userLog.actions.push(logEntry); if (LOG_CONFIG.enableConsoleLog) { console.log('[오비서(Openbase By Service) Log]', logEntry); } // 실시간 서버 전송 if (LOG_CONFIG.enableServerLog && LOG_CONFIG.realTimeLog && LOG_CONFIG.serverUrl) { sendLogToServer({ type: 'action', sessionId: userLog.sessionId, userId: userLog.userId, action: logEntry }); } } function saveLog(result) { userLog.endTime = new Date().toISOString(); userLog.result = result; userLog.duration = Date.now() - new Date(userLog.startTime).getTime(); // 로그 요약 생성 var summaryParts = []; for (var ai = 0; ai < userLog.actions.length; ai++) { var a = userLog.actions[ai]; summaryParts.push((ai + 1) + '. ' + a.action + (a.detail ? ' - ' + a.detail : '')); } var summary = summaryParts.join('\n'); var fullLog = { // 세션 정보 sessionId: userLog.sessionId, startTime: userLog.startTime, endTime: userLog.endTime, duration: userLog.duration, durationText: formatDuration(userLog.duration), // 사용자 식별 userId: userLog.userId, userContact: userLog.userContact, // 환경 정보 userAgent: userLog.userAgent, platform: userLog.platform, language: userLog.language, screenSize: userLog.screenSize, pageUrl: userLog.pageUrl, referrer: userLog.referrer, // 결과 result: result, actionPath: summary, actionCount: userLog.actions.length, actions: userLog.actions, // 티켓 데이터 (있는 경우) ticketData: userLog.ticketData }; if (LOG_CONFIG.enableConsoleLog) { console.log('========== 오비서(Openbase By Service) Session Log =========='); console.log(JSON.stringify(fullLog, null, 2)); console.log('============================================='); } // localStorage에 저장 if (LOG_CONFIG.enableLocalLog) { try { var logs = JSON.parse(localStorage.getItem('obchatbot_logs') || '[]'); logs.push(fullLog); if (logs.length > 50) logs = logs.slice(-50); localStorage.setItem('obchatbot_logs', JSON.stringify(logs)); } catch(e) { console.log('로그 저장 실패:', e); } } // 서버로 전송 if (LOG_CONFIG.enableServerLog && LOG_CONFIG.serverUrl) { sendLogToServer({ type: 'session', log: fullLog }); } return fullLog; } // 서버로 로그 전송 function sendLogToServer(data) { if (!LOG_CONFIG.serverUrl) { console.warn('[오비서(Openbase By Service)] 로그 서버 URL이 설정되지 않았습니다.'); return; } try { // Beacon API 사용 (페이지 종료 시에도 전송 보장) if (navigator.sendBeacon && data.type === 'session') { navigator.sendBeacon(LOG_CONFIG.serverUrl, JSON.stringify(data)); } else { // Fetch API 사용 fetch(LOG_CONFIG.serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), keepalive: true }).then(function(response) { if (LOG_CONFIG.enableConsoleLog) { console.log('[오비서(Openbase By Service)] 로그 서버 전송 성공'); } }).catch(function(error) { console.error('[오비서(Openbase By Service)] 로그 서버 전송 실패:', error); }); } } catch(e) { console.error('[오비서(Openbase By Service)] 로그 전송 에러:', e); } } // 시간 포맷팅 function formatDuration(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + '분 ' + seconds + '초'; } // 로그 조회 함수 (콘솔에서 사용 가능) window.getOBChatbotLogs = function() { try { return JSON.parse(localStorage.getItem('obchatbot_logs') || '[]'); } catch(e) { return []; } }; // 로그 내보내기 (CSV) window.exportOBChatbotLogs = function() { var logs = window.getOBChatbotLogs(); var csv = 'sessionId,userId,userContact,startTime,endTime,duration,result,actionCount,platform\n'; for (var i = 0; i < logs.length; i++) { var log = logs[i]; csv += [ log.sessionId, log.userId || '', log.userContact || '', log.startTime, log.endTime, log.durationText || '', log.result, log.actionCount, log.platform || '' ].join(',') + '\n'; } var blob = new Blob([csv], { type: 'text/csv' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'chatbot_logs_' + new Date().toISOString().split('T')[0] + '.csv'; a.click(); URL.revokeObjectURL(url); console.log('[오비서(Openbase By Service)] CSV 내보내기 완료'); }; // 로그 통계 조회 window.getOBChatbotStats = function() { var logs = window.getOBChatbotLogs(); var stats = { totalSessions: logs.length, results: {}, avgDuration: 0, avgActions: 0, platforms: {}, userIds: [] }; var totalDuration = 0; var totalActions = 0; for (var i = 0; i < logs.length; i++) { var log = logs[i]; // 결과별 집계 stats.results[log.result] = (stats.results[log.result] || 0) + 1; // 평균 계산 totalDuration += log.duration || 0; totalActions += log.actionCount || 0; // 플랫폼별 집계 var platform = log.platform || 'unknown'; stats.platforms[platform] = (stats.platforms[platform] || 0) + 1; // 사용자 ID 수집 if (log.userId && stats.userIds.indexOf(log.userId) === -1) { stats.userIds.push(log.userId); } } stats.avgDuration = logs.length > 0 ? formatDuration(totalDuration / logs.length) : '0분 0초'; stats.avgActions = logs.length > 0 ? (totalActions / logs.length).toFixed(1) : 0; stats.uniqueUsers = stats.userIds.length; console.log('========== 오비서(Openbase By Service) 통계 =========='); console.log('총 세션:', stats.totalSessions); console.log('고유 사용자:', stats.uniqueUsers); console.log('평균 사용 시간:', stats.avgDuration); console.log('평균 액션 수:', stats.avgActions); console.log('결과별 집계:', stats.results); console.log('======================================'); return stats; }; // ========== FAB 버튼 애니메이션 ========== if (fab) { fab.classList.add('first-load'); setTimeout(function() { fab.classList.remove('first-load'); }, 1500); } // 말풍선 - 상단에 계속 표시, 클릭하면 사라짐 setTimeout(function() { if (fabTooltip && !panel.classList.contains('active')) { fabTooltip.classList.add('show'); // 말풍선 뜨면 애니메이션 즉시 멈춤 if (fab) fab.classList.add('no-animation'); // 클릭하면 사라짐 fabTooltip.onclick = function() { this.classList.remove('show'); this.style.display = 'none'; }; } }, 1500); // ========== 시간 인식 시스템 ========== function getTimeGreeting() { var hour = new Date().getHours(); if (hour >= 6 && hour < 12) { return { emoji: '☀️', greeting: '좋은 아침이에요!', period: 'morning' }; } else if (hour >= 12 && hour < 14) { return { emoji: '🍱', greeting: '점심 식사는 하셨나요?', period: 'lunch' }; } else if (hour >= 14 && hour < 18) { return { emoji: '🌤️', greeting: '오후도 화이팅이에요!', period: 'afternoon' }; } else if (hour >= 18 && hour < 22) { return { emoji: '🌆', greeting: '오늘 하루도 수고하셨어요!', period: 'evening' }; } else { return { emoji: '🌙', greeting: '밤늦게까지 고생이시네요!', period: 'night' }; } } // 다크모드 자동 전환 function applyAutoTheme() { var hour = new Date().getHours(); var isDark = hour >= 22 || hour < 6; if (isDark) { document.body.classList.add('vpn-dark-mode'); } else { document.body.classList.remove('vpn-dark-mode'); } return isDark; } // 초기 테마 적용 applyAutoTheme(); var state = { greeted: false, currentFlow: null, // 'guide', 'error', 'ticket' guideStep: 0, errorStep: 0, ticketStep: 0, selectedError: null, privacyAgreed: false, fromTroubleshoot: false, // 문제해결 플로우에서 왔는지 // 티켓 데이터 calType: 'access', calYear: new Date().getFullYear(), calMonth: new Date().getMonth(), calDate: '', calTime: '', calAmpm: '오전', calHour: 12, calMinute: '00', access_time: '', remote_time: '', issue: '', account: '', contact: '', device: '', history: '', env_change: '', env_change_other: '', tried: '', extra_note: '', attachments: [] }; function pad(n) { if (n < 10) return '0' + n; return '' + n; } // ========== 토스트 알림 ========== function showToast(message, type) { type = type || 'info'; var existing = document.querySelector('.vpn-toast'); if (existing) existing.remove(); var toast = document.createElement('div'); toast.className = 'vpn-toast ' + type; var icon = type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : 'ℹ️'; toast.innerHTML = '' + icon + '' + message + ''; document.body.appendChild(toast); setTimeout(function() { toast.classList.add('show'); }, 10); setTimeout(function() { toast.classList.remove('show'); setTimeout(function() { toast.remove(); }, 300); }, 3000); } // ========== 진행률 맵 (Journey Map) ========== function createJourneyMap(steps, currentStep) { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; var map = document.createElement('div'); map.className = 'vpn-journey-map'; map.id = 'journey-map'; // 연결선 var line = document.createElement('div'); line.className = 'journey-line'; var lineFill = document.createElement('div'); lineFill.className = 'journey-line-fill'; lineFill.style.width = ((currentStep / (steps.length - 1)) * 100) + '%'; line.appendChild(lineFill); map.appendChild(line); // 각 단계 for (var i = 0; i < steps.length; i++) { var step = document.createElement('div'); step.className = 'journey-step'; if (i < currentStep) { step.classList.add('completed'); } else if (i === currentStep) { step.classList.add('current'); } else { step.classList.add('pending'); } var icon = document.createElement('div'); icon.className = 'journey-icon'; icon.innerHTML = i < currentStep ? '✓' : steps[i].icon; step.appendChild(icon); var label = document.createElement('div'); label.className = 'journey-label'; label.textContent = steps[i].label; step.appendChild(label); map.appendChild(step); } bubble.appendChild(map); row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; return row; } // 진행률 맵 업데이트 function updateJourneyMap(currentStep) { var map = document.getElementById('journey-map'); if (!map) return; var steps = map.querySelectorAll('.journey-step'); var lineFill = map.querySelector('.journey-line-fill'); for (var i = 0; i < steps.length; i++) { steps[i].classList.remove('completed', 'current', 'pending'); var icon = steps[i].querySelector('.journey-icon'); if (i < currentStep) { steps[i].classList.add('completed'); icon.innerHTML = '✓'; } else if (i === currentStep) { steps[i].classList.add('current'); } else { steps[i].classList.add('pending'); } } if (lineFill) { lineFill.style.width = ((currentStep / (steps.length - 1)) * 100) + '%'; } } // ========== 네트워크 연결 시각화 ========== function createNetworkVisual(status) { // status: 'idle', 'connecting', 'success', 'error' status = status || 'idle'; var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; var visual = document.createElement('div'); visual.className = 'vpn-network-visual'; visual.id = 'network-visual'; var container = document.createElement('div'); container.className = 'network-container'; // 내 PC var pcNode = createNetworkNode('💻', '내 PC', status === 'idle' ? 'pending' : 'success'); container.appendChild(pcNode); // 첫 번째 케이블 var cable1 = document.createElement('div'); cable1.className = 'network-cable'; var flow1 = document.createElement('div'); flow1.className = 'cable-flow'; if (status === 'connecting') { flow1.classList.add('connecting'); var packet1 = document.createElement('div'); packet1.className = 'data-packet'; cable1.appendChild(packet1); } else if (status === 'success') { flow1.classList.add('active'); } cable1.appendChild(flow1); container.appendChild(cable1); // VPN 서버 var vpnStatus = status === 'error' ? 'error' : (status === 'connecting' ? 'connecting' : (status === 'success' ? 'success' : 'pending')); var vpnNode = createNetworkNode('🔐', 'VPN 서버', vpnStatus); container.appendChild(vpnNode); // 두 번째 케이블 var cable2 = document.createElement('div'); cable2.className = 'network-cable'; var flow2 = document.createElement('div'); flow2.className = 'cable-flow'; if (status === 'success') { flow2.classList.add('active'); var packet2 = document.createElement('div'); packet2.className = 'data-packet'; packet2.style.animationDelay = '0.5s'; cable2.appendChild(packet2); } cable2.appendChild(flow2); container.appendChild(cable2); // 회사망 var companyNode = createNetworkNode('🏢', '회사망', status === 'success' ? 'success' : 'pending'); container.appendChild(companyNode); visual.appendChild(container); // 상태 텍스트 var statusText = document.createElement('div'); statusText.className = 'network-status-text'; if (status === 'idle') { statusText.textContent = '🔌 연결 대기 중...'; } else if (status === 'connecting') { statusText.textContent = '⚡ VPN 연결 중...'; } else if (status === 'success') { statusText.textContent = '✅ 연결 성공!'; statusText.classList.add('success'); } else if (status === 'error') { statusText.textContent = '❌ 연결 실패 - 아래 해결방법을 확인하세요'; statusText.classList.add('error'); } visual.appendChild(statusText); bubble.appendChild(visual); row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; return row; } function createNetworkNode(emoji, label, status) { var node = document.createElement('div'); node.className = 'network-node'; var icon = document.createElement('div'); icon.className = 'node-icon'; if (status === 'success') icon.classList.add('active'); if (status === 'connecting') icon.classList.add('connecting'); if (status === 'error') icon.classList.add('error'); icon.textContent = emoji; node.appendChild(icon); var labelEl = document.createElement('div'); labelEl.className = 'node-label'; labelEl.textContent = label; node.appendChild(labelEl); var statusEl = document.createElement('div'); statusEl.className = 'node-status ' + status; if (status === 'success') statusEl.textContent = '연결됨'; else if (status === 'connecting') statusEl.textContent = '연결중'; else if (status === 'error') statusEl.textContent = '오류'; else statusEl.textContent = '대기'; node.appendChild(statusEl); return node; } // 네트워크 시각화 상태 업데이트 function updateNetworkVisual(status) { var visual = document.getElementById('network-visual'); if (!visual) return; // 기존 시각화 제거하고 새로 생성 var parent = visual.closest('.vpn-msg'); if (parent) parent.remove(); createNetworkVisual(status); } // ========== 타이핑 애니메이션 ========== function showTypingIndicator() { var row = document.createElement('div'); row.className = 'vpn-msg bot typing-row'; row.id = 'typing-indicator'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble typing-bubble'; bubble.innerHTML = '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; return row; } function hideTypingIndicator() { var indicator = document.getElementById('typing-indicator'); if (indicator) { indicator.remove(); } } // 타이핑 효과와 함께 메시지 추가 function addMsgWithTyping(text, actions, callback, delay) { delay = delay || 800; showTypingIndicator(); setTimeout(function() { hideTypingIndicator(); addMsg('bot', text, actions); if (callback) callback(); }, delay); } // ========== 보안: HTML 이스케이프 함수 ========== function escapeHtml(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ========== 메시지 추가 함수 ========== function addMsg(who, text, actions) { var row = document.createElement('div'); row.className = 'vpn-msg ' + who; if (who === 'bot') { var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); } var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; // 사용자 입력은 항상 이스케이프, 봇 메시지만 HTML 허용 if (who === 'user') { bubble.textContent = text; } else if (text.indexOf('<') >= 0) { bubble.innerHTML = text; } else { bubble.textContent = text; } row.appendChild(bubble); if (who === 'bot' && actions && actions.length > 0) { var qa = document.createElement('div'); qa.className = 'vpn-quick-actions'; for (var i = 0; i < actions.length; i++) { var a = actions[i]; var btn = document.createElement('button'); btn.className = a.primary ? 'vpn-quick-btn primary' : 'vpn-quick-btn'; // 아이콘 지원 if (a.icon) { btn.innerHTML = '' + a.icon + '' + a.label; } else { btn.textContent = a.label; } btn.setAttribute('data-action', a.action); if (a.data) { btn.setAttribute('data-extra', JSON.stringify(a.data)); } btn.onclick = function() { var act = this.getAttribute('data-action'); var extraData = this.getAttribute('data-extra'); // 버튼들 비활성화 var btns = qa.querySelectorAll('button'); for (var j = 0; j < btns.length; j++) { btns[j].disabled = true; btns[j].style.opacity = '0.5'; } this.style.opacity = '1'; this.classList.add('selected'); addMsg('user', this.textContent.trim()); handleAction(act, extraData ? JSON.parse(extraData) : null); }; qa.appendChild(btn); } bubble.appendChild(qa); } chat.appendChild(row); chat.scrollTop = chat.scrollHeight; return row; } // 인라인 폼 추가 (채팅 내 입력 폼) function addInlineForm(config) { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble inline-form-bubble'; var form = document.createElement('div'); form.className = 'vpn-inline-form'; if (config.title) { var title = document.createElement('div'); title.className = 'inline-form-title'; title.textContent = config.title; form.appendChild(title); } if (config.type === 'textarea') { var textarea = document.createElement('textarea'); textarea.className = 'vpn-inline-textarea'; textarea.placeholder = config.placeholder || ''; textarea.value = config.value || ''; form.appendChild(textarea); var submitBtn = document.createElement('button'); submitBtn.className = 'vpn-inline-submit'; submitBtn.innerHTML = '' + (config.submitText || '확인') + ''; submitBtn.onclick = function() { var val = textarea.value.trim(); if (!val && config.required) { textarea.style.borderColor = '#ef4444'; return; } submitBtn.disabled = true; textarea.disabled = true; if (config.onSubmit) config.onSubmit(val); }; form.appendChild(submitBtn); } else if (config.type === 'input') { var inputEl = document.createElement('input'); inputEl.type = config.inputType || 'text'; inputEl.className = 'vpn-inline-input'; inputEl.placeholder = config.placeholder || ''; inputEl.value = config.value || ''; form.appendChild(inputEl); var submitBtn = document.createElement('button'); submitBtn.className = 'vpn-inline-submit'; submitBtn.innerHTML = '' + (config.submitText || '확인') + ''; submitBtn.onclick = function() { var val = inputEl.value.trim(); if (!val && config.required) { inputEl.style.borderColor = '#ef4444'; return; } submitBtn.disabled = true; inputEl.disabled = true; if (config.onSubmit) config.onSubmit(val); }; form.appendChild(submitBtn); } else if (config.type === 'datetime') { var dtCard = document.createElement('div'); dtCard.className = 'vpn-inline-datetime'; dtCard.innerHTML = '
📅
' + (config.value || '클릭하여 선택') + '
'; dtCard.onclick = function() { config.onClick(); }; form.appendChild(dtCard); } else if (config.type === 'options') { var optWrap = document.createElement('div'); optWrap.className = 'vpn-inline-options'; for (var i = 0; i < config.options.length; i++) { var opt = config.options[i]; var optBtn = document.createElement('button'); optBtn.className = 'vpn-inline-opt-btn'; optBtn.textContent = opt; optBtn.setAttribute('data-value', opt); optBtn.onclick = function() { var val = this.getAttribute('data-value'); var allBtns = optWrap.querySelectorAll('button'); for (var j = 0; j < allBtns.length; j++) { allBtns[j].classList.remove('selected'); allBtns[j].disabled = true; } this.classList.add('selected'); if (config.onSelect) config.onSelect(val); }; optWrap.appendChild(optBtn); } form.appendChild(optWrap); } else if (config.type === 'dropdown') { var dropWrap = document.createElement('div'); dropWrap.className = 'vpn-inline-dropdown'; var dropBtn = document.createElement('div'); dropBtn.className = 'dropdown-trigger'; dropBtn.innerHTML = '' + (config.value || config.placeholder || '선택해주세요') + ''; dropWrap.appendChild(dropBtn); var dropList = document.createElement('div'); dropList.className = 'dropdown-list'; dropList.style.display = 'none'; for (var i = 0; i < config.options.length; i++) { var opt = config.options[i]; var item = document.createElement('div'); item.className = 'dropdown-item'; item.innerHTML = (opt.icon ? '' + opt.icon + '' : '') + '' + opt.label + ''; item.setAttribute('data-value', opt.value); item.onclick = function() { var val = this.getAttribute('data-value'); dropBtn.innerHTML = '' + this.textContent + ''; dropList.style.display = 'none'; dropWrap.classList.remove('open'); if (config.onSelect) config.onSelect(val); }; dropList.appendChild(item); } dropWrap.appendChild(dropList); dropBtn.onclick = function() { var isOpen = dropList.style.display === 'block'; dropList.style.display = isOpen ? 'none' : 'block'; dropWrap.classList.toggle('open', !isOpen); }; form.appendChild(dropWrap); } else if (config.type === 'upload') { var uploadWrap = document.createElement('div'); uploadWrap.className = 'vpn-inline-upload'; uploadWrap.innerHTML = '
📎
클릭하여 파일 첨부
'; var fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = config.accept || '*/*'; fileInput.multiple = config.multiple || false; fileInput.style.display = 'none'; uploadWrap.onclick = function() { fileInput.click(); }; fileInput.onchange = function() { if (config.onUpload) config.onUpload(fileInput.files); }; form.appendChild(uploadWrap); form.appendChild(fileInput); } else if (config.type === 'multi-input') { // 심플한 입력 필드 for (var i = 0; i < config.fields.length; i++) { var field = config.fields[i]; var fieldWrap = document.createElement('div'); fieldWrap.className = 'multi-input-field'; var label = document.createElement('label'); label.textContent = field.label; fieldWrap.appendChild(label); var inp = document.createElement('input'); inp.type = field.type || 'text'; inp.placeholder = field.placeholder || ''; inp.className = 'vpn-inline-input'; inp.setAttribute('data-key', field.key); // 연락처 필드는 숫자만 입력 if (field.type === 'tel' || field.key === 'contact') { inp.setAttribute('inputmode', 'numeric'); inp.setAttribute('pattern', '[0-9-]*'); inp.oninput = function() { this.value = this.value.replace(/[^0-9-]/g, ''); }; } fieldWrap.appendChild(inp); form.appendChild(fieldWrap); } var submitBtn = document.createElement('button'); submitBtn.className = 'vpn-inline-submit'; submitBtn.innerHTML = '' + (config.submitText || '확인') + ''; submitBtn.onclick = function() { var inputs = form.querySelectorAll('input'); var values = {}; var valid = true; for (var j = 0; j < inputs.length; j++) { var key = inputs[j].getAttribute('data-key'); var val = inputs[j].value.trim(); if (!val) { inputs[j].style.borderColor = '#ef4444'; valid = false; } values[key] = val; } if (!valid) return; submitBtn.disabled = true; for (var j = 0; j < inputs.length; j++) { inputs[j].disabled = true; } if (config.onSubmit) config.onSubmit(values); }; form.appendChild(submitBtn); } bubble.appendChild(form); row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; return row; } // 다운로드 버튼 추가 function addDownloadButton(url, text, extraInfo, onClickCallback) { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble download-bubble'; var downloadCard = document.createElement('div'); downloadCard.className = 'vpn-download-card'; if (extraInfo) { var info = document.createElement('div'); info.className = 'download-info'; info.innerHTML = extraInfo; downloadCard.appendChild(info); } var link = document.createElement('a'); link.href = url; link.download = url.split('/').pop(); link.className = 'vpn-download-btn'; link.innerHTML = '📥' + text; // 클릭 시 콜백 실행 if (onClickCallback) { link.addEventListener('click', function() { setTimeout(function() { onClickCallback(); }, 500); }); } downloadCard.appendChild(link); bubble.appendChild(downloadCard); row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; } // ========== 메뉴 활성화 상태 관리 ========== function setActiveMenu(action) { var menuItems = document.querySelectorAll('.vpn-menu-item'); for (var i = 0; i < menuItems.length; i++) { var item = menuItems[i]; item.classList.remove('active'); if (item.getAttribute('data-action') === action) { item.classList.add('active'); } } } // ========== 입력바 표시/숨김 ========== function showInputBar() { if (state.currentFlow === 'ticket') return; /* 원격접수 중에는 채팅 입력 숨김 */ var inputBar = document.getElementById('vpn-input-bar'); if (inputBar) inputBar.style.display = 'flex'; } function hideInputBar() { var inputBar = document.getElementById('vpn-input-bar'); if (inputBar) inputBar.style.display = 'none'; } // ========== 액션 핸들러 ========== function handleAction(action, data) { // 로그 기록 logAction(action, data); // 메뉴 활성화 상태 업데이트 if (['troubleshoot', 'download', 'faq', 'ticket'].indexOf(action) !== -1) { setActiveMenu(action); } // 채팅 문의(free_chat) 외 다른 메뉴 진입 시 추천키워드 숨기기 if (action !== 'free_chat') { hideSuggestKeywords(); } if (action === 'troubleshoot') { hideInputBar(); startTroubleshootFlow(); } else if (action === 'guide') { hideInputBar(); startGuideFlow(); } else if (action === 'onetouch') { hideInputBar(); startOnetouchFix(); } else if (action === 'vpn_issue') { hideInputBar(); startTroubleshootFlow(); } else if (action === 'error_msg') { hideInputBar(); startErrorFlow(); } else if (action === 'download') { hideInputBar(); startDownloadFlow(); } else if (action === 'faq') { hideInputBar(); startFaqFlow(); } else if (action === 'ticket') { hideInputBar(); startTicketFlow(); } else if (action === 'ticket_direct') { // 이미 시도했다고 선택한 경우 바로 접수 hideInputBar(); startTicketFlow(true); } else if (action === 'ticket_from_flow') { // 문제해결 플로우에서 온 경우 state.fromTroubleshoot = true; hideInputBar(); startTicketFlow(); } else if (action === 'free_chat') { showInputBar(); startFreeChatFlow(); } else if (action === 'slow_issue') { handleSlowIssue(); } else if (action === 'no_match_issue') { handleNoMatchIssue(); } else if (action === 'restart') { restartBot(); } else if (action === 'faq_url') { addMsgWithTyping('VPN 접속 주소 🌐\n\n' + 'https://vpn.cef.or.kr\n\n' + '권장 브라우저:\n• Windows: Chrome, Edge\n• Mac: Safari, Chrome\n\n' + '💡 시크릿 모드(비공개 창)로 접속하면 캐시 문제를 줄일 수 있어요.', getFaqEndButtons(), null, 600); } else if (action === 'faq_login') { addMsgWithTyping('로그인/비밀번호 안내 🔑\n\n' + '• 아이디: 사번(직원) / 이름(협력사)\n' + '• 비밀번호: 그룹웨어 비밀번호와 동일\n\n' + '비밀번호 오류 시:\n' + '1. 그룹웨어에서 비밀번호 초기화\n' + '2. 새 비밀번호로 VPN 접속 시도\n' + '3. 여전히 불가 시 원격 지원 접수\n\n' + '💡 대소문자·숫자 입력 확인해주세요!', getFaqEndButtons(), null, 600); } else if (action === 'faq_install') { addMsgWithTyping('설치/삭제 방법 💻\n\n' + '[설치 절차]\n' + '1. VPN 사이트 접속 후 로그인\n' + '2. 페이지에서 설치 파일 자동 다운로드\n' + '3. 다운로드된 파일을 관리자 권한으로 실행\n' + '4. 설치 완료 후 브라우저 새로고침\n\n' + '[삭제 절차]\n' + '1. Windows: 제어판 → 프로그램 제거\n' + '2. "BIG-IP Edge Client" 검색 후 삭제\n' + '3. PC 재부팅 후 재설치 권장', getFaqEndButtons(), null, 600); } else if (action === 'faq_mobile') { addMsgWithTyping('맥/모바일 사용 안내 📱\n\n' + '[Mac]\n' + '• Safari 또는 Chrome 브라우저로 VPN 사이트 접속\n' + '• 로그인 시 Mac용 클라이언트 자동 설치 안내\n' + '• 시크릿 모드: ⌘ + Shift + N\n\n' + '[모바일 - iOS/Android]\n' + '• 앱스토어에서 "F5 Access" 앱 설치\n' + '• 서버 주소: vpn.cef.or.kr 입력\n' + '• 사내 계정으로 로그인', getFaqEndButtons(), null, 600); } else if (action === 'faq_contact') { addMsgWithTyping('운영시간/담당자 안내 ⏰\n\n' + '해당 기능은 준비중입니다.\n곧 안내해드릴 예정이에요!', getFaqEndButtons(), null, 600); } else if (action === 'faq_direct') { // 스마트 폴백에서 선택한 FAQ 바로 표시 if (data && typeof data.index === 'number' && faqList[data.index]) { var faq = faqList[data.index]; addMsgWithTyping(faq.answer, faq.actions || getFaqEndButtons(), null, 600); } } else if (action === 'no_match_fallback') { // 유사 FAQ도 해당 안 함 addMsgWithTyping('그렇군요! 😊\n\n아래 메뉴에서 선택하시거나,\n채팅 문의로 자세히 알려주세요!', [ { label: '🔧 문제 해결', action: 'troubleshoot', primary: true }, { label: '📋 원격 접수', action: 'ticket' }, { label: '💬 채팅 문의', action: 'free_chat' } ], null, 600); } else if (action === 'guide_next') { nextGuideStep(); } else if (action === 'guide_prev') { prevGuideStep(); } else if (action === 'guide_success') { finishGuideSuccess(); } else if (action === 'guide_fail') { finishGuideFail(); } else if (action === 'error_select') { selectError(data); } else if (action === 'error_resolved') { errorResolved(); } else if (action === 'error_not_resolved') { errorNotResolved(); } else if (action === 'ticket_privacy_agree') { ticketPrivacyAgreed(); } else if (action === 'ticket_privacy_decline') { ticketPrivacyDeclined(); } else if (action === 'ticket_next') { nextTicketStep(); } else if (action === 'onetouch_fix') { startOnetouchFix(); } else if (action === 'step_guide') { startStepGuide(); } else if (action === 'onetouch_confirm') { showStep4Install(); } else if (action === 'onetouch_retry') { retryOnetouch(); } else if (action === 'onetouch_help') { addMsgWithTyping('CMD 창에서 메시지가 안 보이셨나요? 🤔\n\n다음을 확인해주세요:\n• BAT 파일을 마우스 우클릭 → 관리자 권한으로 실행\n• CMD 창이 바로 닫히면 정상입니다\n• 에러 메시지(빨간색)가 보이면 단계별 가이드를 이용해주세요', [ { label: '🔄 다시 시도', action: 'onetouch_fix' }, { label: '📋 단계별 가이드', action: 'step_guide' } ], null, 600); } else if (action === 'fix_success') { showToast('🎉 문제가 해결되었습니다!', 'success'); saveLog('해결완료'); // 네트워크 시각화 성공 상태로 업데이트 updateNetworkVisual('success'); addMsgWithTyping('다행이에요! 🎉\n\n도움이 필요하시면 언제든 말씀해주세요.', null, function() { showSatisfactionSurvey(); }, 600); } else if (action === 'fix_fail') { addMsgWithTyping('아직 해결되지 않으셨군요... 😢\n\n원격 지원을 통해 직접 도와드릴게요!', [ { label: '📋 원격 지원 접수', action: 'ticket_from_flow' }, { label: '🔄 다시 시도', action: 'troubleshoot' } ], null, 600); } else if (action === 'restart') { restartConversation(); } else if (action === 'open_mail') { openMail(); } else if (action === 'ticket_confirm') { state.logCollected = true; updateTicketSidePanel(); nextTicketStep(); } else if (action === 'env_complete') { // 환경 정보 입력 완료 후 다음 빈 단계로 updateTicketSidePanel(); nextTicketStep(); } } // ========== 패널 열기/닫기 ========== function openPanel() { overlay.className = 'active'; // 로그 기록 - 패널 열림 logAction('패널열기', '챗봇 시작'); // 말풍선 숨기기 + 애니메이션 멈춤 if (fabTooltip) { fabTooltip.classList.remove('show'); fabTooltip.style.display = 'none'; } if (fab) fab.classList.add('no-animation'); // 플로팅 효과: 적당한 크기로 시작해서 커지는 애니메이션 panel.style.transform = 'scale(0.8) translateY(30px)'; panel.style.opacity = '0'; panel.classList.add('active'); // 강제 reflow 후 애니메이션 시작 void panel.offsetWidth; panel.style.transition = 'all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)'; panel.style.transform = 'scale(1) translateY(0)'; panel.style.opacity = '1'; hideInputBar(); // 처음에는 입력바 숨김 // 원격접수 진행 중이면 사이드 패널 복원 if (state.currentFlow === 'ticket') { var existingPanel = document.getElementById('ticket-side-panel'); if (!existingPanel) { createTicketSidePanel(); // 기존 데이터로 패널 업데이트 updateTicketSidePanel(); } } // 세션 만료 상태면 안내 메시지 표시 if (isSessionExpired) { showSessionExpiredNotice(); return; // 일반 인사말 표시 안함 } if (!state.greeted) { state.greeted = true; var timeInfo = getTimeGreeting(); var isDark = applyAutoTheme(); setTimeout(function() { // 시간대별 인사말 addMsgWithTyping(timeInfo.emoji + ' ' + timeInfo.greeting, null, function() { var extraMsg = ''; if (timeInfo.period === 'night') { extraMsg = '\n💤 야간엔 답변이 늦을 수 있어요'; } addMsgWithTyping('저는 오비서에요! 👋\nVPN 접속을 도와드릴게요. 무엇이 필요하세요?' + extraMsg, [ { label: '🔧 문제 해결', action: 'troubleshoot' }, { label: '📥 다운로드', action: 'download' }, { label: '❓ FAQ', action: 'faq' }, { label: '📋 원격 접수', action: 'ticket' }, { label: '💬 채팅 문의하기', action: 'free_chat' } ], null, 600); }, 500); }, 300); } else if (state.currentFlow === 'free_chat') { // 채팅 문의 중이었으면 추천 키워드 다시 표시 showInputBar(); setTimeout(function() { showSuggestKeywords(); }, 100); } } function closePanel() { // 빠른 닫기 애니메이션 (쇽!) panel.style.transition = 'all 0.15s ease-in'; panel.style.transform = 'scale(0.9) translateY(20px)'; panel.style.opacity = '0'; // 사이드 패널도 함께 닫기 (즉시 제거) hideReviewPanel(); setTimeout(function() { overlay.className = ''; panel.classList.remove('active'); panel.style.transform = ''; panel.style.opacity = ''; panel.style.transition = ''; }, 150); } // 사이드 패널 숨기기/제거 함수 function hideReviewPanel() { // 모든 사이드 패널 강제 제거 var panels = document.querySelectorAll('#ticket-side-panel'); for (var i = 0; i < panels.length; i++) { var p = panels[i]; p.style.display = 'none'; if (p.parentNode) { p.parentNode.removeChild(p); } } } // ========== 유틸리티 함수 ========== function getFaqEndButtons() { return [ { label: '⬅️ FAQ 목록', action: 'faq' }, { label: '🏠 처음으로', action: 'restart' } ]; } function restartBot() { // 메뉴 활성화 상태 초기화 var menuItems = document.querySelectorAll('.vpn-menu-item'); for (var i = 0; i < menuItems.length; i++) { menuItems[i].classList.remove('active'); } hideInputBar(); addMsgWithTyping('또 도움이 필요하시면 말씀해주세요! 😊', [ { label: '🔧 문제 해결', action: 'troubleshoot' }, { label: '📥 다운로드', action: 'download' }, { label: '❓ FAQ', action: 'faq' }, { label: '📋 원격 접수', action: 'ticket' }, { label: '💬 채팅 문의하기', action: 'free_chat' } ], null, 500); } // ========== 챗봇 대화 모드 ========== function startFreeChatFlow() { state.currentFlow = 'free_chat'; addMsgWithTyping('자유롭게 질문해주세요! 💬\n\nVPN 관련 궁금한 점이나 문제를 입력해주시면\n최대한 도움을 드릴게요.', null, function() { // 추천 키워드 표시 showSuggestKeywords(); }, 600); } // ========== 문제 해결 (통합 메뉴) ========== function startTroubleshootFlow() { state.currentFlow = 'troubleshoot'; addMsgWithTyping('어떤 상황인지 알려주세요! 🔍', [ { label: '🚫 VPN 연결이 안 돼요', action: 'guide' }, { label: '⚠️ 에러 메시지가 떠요', action: 'error_msg' }, { label: '🐢 연결은 되는데 느려요/끊겨요', action: 'slow_issue' }, { label: '❓ 해당하는 문제가 없어요', action: 'no_match_issue' } ], null, 600); } // ========== 다운로드 메뉴 ========== function startDownloadFlow() { state.currentFlow = 'download'; addMsgWithTyping('필요한 파일을 선택해주세요! 📥', null, function() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble download-list-bubble'; bubble.innerHTML = '
' + '' + '💿' + '
VPN 설치파일
f5vpn_setup.exe
' + '
' + '' + '⚙️' + '
서비스 실행파일
Services.exe
' + '
' + '' + '🔄' + '
어댑터 초기화파일
MiniVpn_del.exe
' + '
' + '' + '🗑️' + '
삭제 파일
remove.exe
' + '
' + '' + '📋' + '
로그수집 파일
log_save.exe
' + '
' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; setTimeout(function() { addMsg('bot', '💡 다운로드 후 마우스 우클릭 → "관리자 권한으로 실행"', [ { label: '🏠 처음으로', action: 'restart' } ]); }, 500); }, 600); } // ========== FAQ 메뉴 ========== function startFaqFlow() { state.currentFlow = 'faq'; addMsgWithTyping('어떤 내용이 궁금하세요? ❓', [ { label: '🌐 VPN 주소', action: 'faq_url' }, { label: '🔑 로그인/비밀번호', action: 'faq_login' }, { label: '💻 설치/삭제 방법', action: 'faq_install' }, { label: '📱 맥/모바일 사용', action: 'faq_mobile' }, { label: '⏰ 운영시간/담당자', action: 'faq_contact' }, { label: '🏠 처음으로', action: 'restart' } ], null, 600); } // ========== 느림/끊김 문제 ========== function handleSlowIssue() { addMsgWithTyping('VPN 속도/끊김 문제 해결 팁 🐢\n\n' + '✅ 확인해보세요:\n' + '• 인터넷 연결 상태 확인\n' + '• 다른 프로그램에서 대용량 다운로드 중인지\n' + '• 유선 인터넷 사용 권장\n' + '• 절전 모드/화면 꺼짐 해제\n\n' + '✅ 시도해보세요:\n' + '• VPN 재접속\n' + '• 시크릿 모드로 접속\n' + '• 타사 VPN 프로그램 삭제\n' + '• VPN 재설치', [ { label: '🔧 VPN 재설치 가이드', action: 'guide' }, { label: '📋 원격 접수', action: 'ticket_from_flow' }, { label: '🏠 처음으로', action: 'restart' } ], null, 700); } // ========== 해당하는 문제가 없을 때 ========== function handleNoMatchIssue() { addMsgWithTyping('해당하는 문제를 찾지 못하셨군요 😥\n\n원격 지원을 통해 직접 도움을 드릴게요!\n담당자가 확인 후 연락드립니다.', [ { label: '📋 원격 지원 접수하기', action: 'ticket_from_flow' }, { label: '💬 채팅 문의하기', action: 'free_chat' }, { label: '🏠 처음으로', action: 'restart' } ], null, 600); } // ========== VPN 접속 불가 조치 (가이드) ========== var guideJourneySteps = [ { icon: '⚙️', label: '서비스' }, { icon: '🔄', label: '어댑터' }, { icon: '🗑️', label: '삭제' }, { icon: '📦', label: '설치' }, { icon: '🔗', label: '접속' }, { icon: '✅', label: '완료' } ]; var guideSteps = [ { title: '1단계: 서비스 실행', desc: 'VPN에 연결하기 위한 필수 서비스 실행을 위해\n아래 파일을 다운로드 받아 관리자 권한으로 실행해주세요.', file: { url: '/public/share/download/Services.exe', name: '서비스 실행파일 다운로드' }, tip: '💡 마우스 우클릭 → "관리자 권한으로 실행"', execTip: '⬆️ 위 버튼을 눌러 다운로드 완료 후,\n🖱️ 마우스 우클릭 → "관리자 권한으로 실행"해주세요!' }, { title: '2단계: 네트워크 어댑터 초기화', desc: 'VPN 연결에 사용되는 네트워크 어댑터 초기화를 위해\n아래 파일을 다운로드 받아 관리자 권한으로 실행해주세요.', file: { url: '/public/share/download/MiniVpn_del.exe', name: '어댑터 초기화파일 다운로드' }, tip: '💡 실행 시간이 다소 걸릴 수 있습니다.', execTip: '⬆️ 위 버튼을 눌러 다운로드 완료 후,\n🖱️ 마우스 우클릭 → "관리자 권한으로 실행"해주세요!\n\n⏳ 실행 시간이 다소 걸릴 수 있습니다.' }, { title: '3단계: VPN 프로그램 삭제', desc: 'VPN 실행파일 재설치를 위해\n아래 삭제파일을 다운로드 받아 관리자 권한으로 실행해주세요.', file: { url: '/public/share/download/remove.exe', name: '삭제 파일 다운로드' }, tip: '💡 완전한 제거를 위해 재부팅이 필요할 수 있습니다.', execTip: '⬆️ 위 버튼을 눌러 다운로드 완료 후,\n🖱️ 마우스 우클릭 → "관리자 권한으로 실행"해주세요!\n\n🔄 완전한 제거를 위해 재부팅이 필요할 수 있습니다.' }, { title: '4단계: VPN 설치', desc: 'VPN 프로그램 설치 단계입니다.\n아래 버튼을 클릭하여 VPN 설치파일을 다운받아 실행하시기 바랍니다.', file: { url: '/public/download/f5vpn_setup.exe', name: '설치파일 다운로드' }, tip: '💡 다운로드된 파일을 실행하여 VPN 프로그램을 설치해주세요.', execTip: '⬆️ 위 버튼을 눌러 다운로드 완료 후,\n다운로드된 파일을 더블클릭하여 설치를 진행해주세요! 📦' }, { title: '5단계: VPN 접속 확인', desc: 'VPN 도메인에 접속하여 연결을 확인해주세요.\n\n⚠️ 시크릿 모드(비공개 창)로 접속을 권장합니다!', link: 'https://vpn.cef.or.kr', tip: '🔒 Chrome: Ctrl + Shift + N (Mac: ⌘ + Shift + N)' } ]; function startGuideFlow() { state.currentFlow = 'guide'; state.guideStep = 0; // 네트워크 연결 시각화 (에러 상태) createNetworkVisual('error'); setTimeout(function() { addMsgWithTyping('VPN 접속 불가 조치를 선택해주세요! 💪', [ { label: '⚡ 원터치 자동 해결', action: 'onetouch_fix' }, { label: '📋 단계별 가이드', action: 'step_guide' } ], null, 600); }, 500); } // ========== 원터치 해결 ========== var onetouchSteps = [ { name: '윈도우 서비스 시작', icon: '⚙️' }, { name: '네트워크 어댑터 초기화', icon: '🔄' }, { name: 'VPN 프로그램 삭제', icon: '🗑️' } ]; var currentAnimStep = 0; function startOnetouchFix() { showToast('📥 파일을 다운로드 받으세요!', 'info'); // 네트워크 시각화 연결 중 상태로 업데이트 updateNetworkVisual('connecting'); // 예상 해결 시간 표시 showEstimatedTime(3, '원터치 VPN 해결'); var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var card = document.createElement('div'); card.className = 'onetouch-card'; var dlBtn = document.createElement('a'); dlBtn.href = '/public/share/download/vpn_onetouch_fix.exe'; dlBtn.download = ''; dlBtn.className = 'onetouch-download-btn'; dlBtn.innerHTML = '📥 원터치 해결파일 다운로드'; card.innerHTML = '
' + '🚀' + '
원터치 VPN 해결
' + '
' + '
아래 파일 하나에 3가지 조치가 모두 포함되어 있습니다.
관리자 권한으로 실행해주세요.
'; card.appendChild(dlBtn); row.appendChild(card); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 직접 이벤트 연결 (매번 새로운 버튼에) dlBtn.addEventListener('click', function(e) { showToast('파일 다운로드 시작!', 'success'); hideHelperPin(); setTimeout(function() { showProgressCard(); }, 600); }); // 도우미 핀 표시 - 다운로드 버튼 가리키기 setTimeout(function() { showHelperPin('.onetouch-download-btn', '여기를 클릭하세요!', 'top'); }, 800); } function showProgressCard() { // 기존 progress-card 제거 (재시도 시) var existingCard = document.getElementById('progress-card'); if (existingCard) { existingCard.closest('.vpn-msg').remove(); } var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var card = document.createElement('div'); card.className = 'timeline-card'; card.id = 'progress-card'; card.innerHTML = '
원터치 해결파일은 아래 프로세스로 진행됩니다
' + '
' + '
' + '
' + '1' + '
' + '
서비스
' + '
' + '
' + '
' + '
' + '2' + '
' + '
어댑터
' + '
' + '
' + '
' + '
' + '3' + '
' + '
정리
' + '
' + '
' + '
' + '
' + '
' + '준비 중...' + '0%' + '
' + '
' + ''; row.appendChild(card); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 애니메이션 시작 currentAnimStep = 0; setTimeout(function() { animateProgressStep(); }, 600); } function animateProgressStep() { if (currentAnimStep >= onetouchSteps.length) { finishProgressAnimation(); return; } var step = onetouchSteps[currentAnimStep]; var stepEl = document.getElementById('prog-step-' + currentAnimStep); var fill = document.getElementById('prog-fill'); var percent = document.getElementById('prog-percent'); var status = document.getElementById('prog-status'); var numEl = stepEl.querySelector('.tl-num'); var connector = document.getElementById('conn-' + currentAnimStep); // 현재 단계 활성화 stepEl.classList.add('active'); status.textContent = step.icon + ' ' + step.name + ' 실행 중...'; // 1.3초 후 완료 setTimeout(function() { stepEl.classList.remove('active'); stepEl.classList.add('done'); numEl.innerHTML = ''; // 커넥터 채우기 if (connector) { connector.style.width = '100%'; } var progress = ((currentAnimStep + 1) / 3 * 100); fill.style.width = progress + '%'; percent.textContent = Math.round(progress) + '%'; currentAnimStep++; // 0.4초 후 다음 단계 setTimeout(function() { animateProgressStep(); }, 400); }, 1300); } function finishProgressAnimation() { var status = document.getElementById('prog-status'); var buttons = document.getElementById('prog-buttons'); var card = document.getElementById('progress-card'); status.innerHTML = '✅ 모든 단계 완료!'; card.classList.add('completed'); showToast('📥 다운받은 파일을 실행하세요!', 'info'); setTimeout(function() { buttons.innerHTML = '
' + '🤔' + '파일을 관리자 권한으로 실행하셨나요?' + '
' + '
' + '' + '' + '
'; buttons.style.display = 'block'; document.getElementById('prog-yes').onclick = function() { handleAction('onetouch_confirm'); }; document.getElementById('prog-no').onclick = function() { handleAction('onetouch_retry'); }; chat.scrollTop = chat.scrollHeight; }, 600); } function showStep4Install() { addMsg('user', '✅ 실행완료!'); showToast('이제 마지막 단계입니다!', 'success'); addMsgWithTyping('4단계: VPN 재설치 📥\n\n마지막으로 VPN 프로그램을 재설치해주세요!', null, function() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var card = document.createElement('div'); card.className = 'install-card'; card.innerHTML = '
' + '📥' + 'VPN 실행파일 다운로드' + '
' + '
다운로드 후 실행하여 설치를 완료하세요.
' + '' + '⬇️ VPN 설치파일 다운로드' + ''; row.appendChild(card); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; setTimeout(function() { addMsgWithTyping('설치 완료 후 VPN에 접속해보세요! 🚀', [ { label: '✅ 해결됐어요!', action: 'fix_success', primary: true }, { label: '❌ 아직 안돼요', action: 'fix_fail' } ], null, 800); }, 500); }, 600); } function retryOnetouch() { addMsg('user', '❌ 아직이요'); addMsgWithTyping('괜찮아요! 다운로드 받은 BAT 파일관리자 권한으로 실행해주세요!\n\n실행하면 다음 3가지가 자동으로 진행됩니다:\n1. 윈도우 서비스 시작\n2. 네트워크 어댑터 초기화\n3. VPN 프로그램 삭제\n\n실행하셨으면 알려주세요.', [ { label: '✅ 실행완료!', action: 'onetouch_confirm', primary: true }, { label: '🔄 다시 다운로드', action: 'onetouch_fix' } ], null, 600); } function startStepGuide() { state.guideStep = 0; // 가이드 사이드 패널 생성 createGuideSidePanel(); addMsgWithTyping('VPN 접속 불가 조치를 시작합니다! 💪\n\n총 5단계로 진행됩니다. 차근차근 따라와 주세요.', null, function() { showGuideStep(0); }, 600); } // ========== 가이드 사이드 패널 ========== function createGuideSidePanel() { // 기존 패널 제거 var existing = document.getElementById('guide-side-panel'); if (existing) existing.remove(); var sidePanel = document.createElement('div'); sidePanel.id = 'guide-side-panel'; sidePanel.className = 'guide-side-panel'; // 진행률 맵 HTML 생성 var journeyHtml = '
'; for (var i = 0; i < guideJourneySteps.length; i++) { var stepClass = i === 0 ? 'current' : 'pending'; journeyHtml += '
' + '
' + guideJourneySteps[i].icon + '
' + '
' + guideJourneySteps[i].label + '
' + '
'; } journeyHtml += '
'; sidePanel.innerHTML = '
' + '🔧 VPN 조치 진행' + '' + '
' + journeyHtml + '
' + '
' + '
1
' + '
' + '
서비스 실행
' + '
VPN 필수 서비스를 시작합니다
' + '
' + '
' + '
' + '
' + '
' + '
' + '
💡 관리자 권한으로 실행하세요
' + '
' + '
'; document.body.appendChild(sidePanel); // 토글 버튼 sidePanel.querySelector('.gsp-toggle').onclick = function() { sidePanel.classList.toggle('collapsed'); this.textContent = sidePanel.classList.contains('collapsed') ? '+' : '−'; }; } // 가이드 사이드 패널 업데이트 function updateGuideSidePanel(step) { var journey = document.getElementById('gsp-journey'); if (!journey) return; var steps = journey.querySelectorAll('.gsp-step'); var fill = document.getElementById('gsp-journey-fill'); // 진행률 맵 업데이트 for (var i = 0; i < steps.length; i++) { steps[i].classList.remove('completed', 'current', 'pending'); var icon = steps[i].querySelector('.gsp-step-icon'); if (i < step) { steps[i].classList.add('completed'); icon.textContent = '✓'; } else if (i === step) { steps[i].classList.add('current'); icon.textContent = guideJourneySteps[i].icon; } else { steps[i].classList.add('pending'); icon.textContent = guideJourneySteps[i].icon; } } if (fill) { fill.style.width = ((step / (guideJourneySteps.length - 1)) * 100) + '%'; } // 진행 바 업데이트 var progressBar = document.getElementById('gsp-progress-bar'); if (progressBar) { progressBar.style.width = ((step / (guideSteps.length)) * 100) + '%'; } // 현재 단계 정보 업데이트 var stepNum = document.getElementById('gsp-step-num'); var stepTitle = document.getElementById('gsp-step-title'); var stepDesc = document.getElementById('gsp-step-desc'); var tips = document.getElementById('gsp-tips'); if (step < guideSteps.length) { var data = guideSteps[step]; if (stepNum) stepNum.textContent = step + 1; if (stepTitle) stepTitle.textContent = data.title.replace(/^\d단계: /, ''); if (stepDesc) stepDesc.textContent = data.tip ? data.tip.replace('💡 ', '') : ''; if (tips && data.tip) { tips.innerHTML = '
' + data.tip + '
'; } } else { // 완료 상태 if (stepNum) stepNum.textContent = '✓'; if (stepTitle) stepTitle.textContent = '모든 단계 완료!'; if (stepDesc) stepDesc.textContent = 'VPN 접속을 확인해주세요'; } } // 가이드 사이드 패널 제거 function removeGuideSidePanel() { var panel = document.getElementById('guide-side-panel'); if (panel) { panel.classList.add('hiding'); setTimeout(function() { if (panel.parentNode) panel.remove(); }, 300); } } function showGuideStep(step) { var data = guideSteps[step]; // 사이드 패널 업데이트 updateGuideSidePanel(step); addMsgWithTyping('' + data.title + '\n\n' + data.desc, null, function() { if (data.file) { // 다운로드 버튼 표시 (클릭 후 안내 메시지 표시) setTimeout(function() { addDownloadButton(data.file.url, data.file.name, data.tip, function() { // 버튼 클릭 후 안내 메시지 var execMessage = data.execTip || '⬆️ 위 버튼을 눌러 다운로드 완료 후 실행해주세요!'; addMsgWithTyping(execMessage, null, function() { // 완료 확인 질문 setTimeout(function() { showGuideActions(step); }, 1000); }, 1200); }); }, 600); } else if (data.link) { // VPN 접속 확인 단계 setTimeout(function() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; bubble.innerHTML = ''; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; setTimeout(function() { showGuideActions(step); }, 1200); }, 600); } else { setTimeout(function() { showGuideActions(step); }, 1000); } }, 1200); } function showGuideActions(step) { if (step < 4) { addMsg('bot', '완료되셨나요?', [ { label: '✅ 완료! 다음 단계로', action: 'guide_next', primary: true }, { label: '⬅️ 이전 단계로', action: 'guide_prev' } ]); } else { addMsg('bot', '접속 결과를 알려주세요!', [ { label: '✅ 접속 성공!', action: 'guide_success', primary: true }, { label: '❌ 접속 실패', action: 'guide_fail' } ]); } } function nextGuideStep() { state.guideStep++; if (state.guideStep < guideSteps.length) { showGuideStep(state.guideStep); } } function prevGuideStep() { if (state.guideStep > 0) { state.guideStep--; showGuideStep(state.guideStep); } else { addMsgWithTyping('첫 번째 단계입니다. 계속 진행해주세요!', null, function() { showGuideStep(0); }); } } function finishGuideSuccess() { state.currentFlow = null; // 사이드 패널 완료 상태로 업데이트 후 제거 updateGuideSidePanel(guideJourneySteps.length - 1); setTimeout(removeGuideSidePanel, 2000); // 네트워크 시각화 성공 상태로 updateNetworkVisual('success'); showToast('🎉 VPN 접속 성공!', 'success'); saveLog('가이드해결완료'); addMsgWithTyping('🎉 축하합니다! VPN 접속에 성공하셨군요!\n\n다른 도움이 필요하시면 언제든 말씀해주세요.', [ { label: '🏠 처음으로', action: 'restart' } ]); } function finishGuideFail() { // 사이드 패널 제거 removeGuideSidePanel(); addMsgWithTyping('😔 접속에 실패하셨군요.\n\n원격 지원 접수를 통해 전문가의 도움을 받아보세요!', [ { label: '📋 원격 지원 접수하기', action: 'ticket_from_flow' }, { label: '🔄 가이드 다시 시작', action: 'guide' } ]); } // ========== 에러 메시지 해결 ========== var errorList = [ { id: 1, title: '네트워크 접속 연결 장치를 찾을 수 없습니다', type: 'guide' }, { id: 2, title: '연결이 해제되었습니다', subtitle: '원격 컴퓨터에 연결하지 못했습니다.', type: 'guide' }, { id: 3, title: '인증서 체인을 신뢰한 최상위 인증 기관에 만들 수 없습니다', type: 'cert' }, { id: 4, title: '네트워크 접속 연결 구성 중 오류 발생', type: 'reinstall' }, { id: 5, title: '전화번호부 파일을 업데이트 할 수 없습니다', type: 'ie' }, { id: 6, title: '원격 컴퓨터에 연결하지 못했습니다', type: 'conflict' }, { id: 7, title: '틀린 구조 크기를 검색했습니다', type: 'update' }, { id: 8, title: '연결을 완료하기 전에 원격 컴퓨터가 연결을 끊었습니다', type: 'pending' }, { id: 9, title: '원격 컴퓨터에 연결하지 못했습니다 (네트워크 설정)', type: 'pending' }, { id: 10, title: 'VPN접속 후 PC Local DNS로 Query를 보냄', type: 'dns' } ]; function startErrorFlow() { state.currentFlow = 'error'; state.selectedError = null; addMsgWithTyping('어떤 에러 메시지가 발생했나요?\n\n해당하는 에러를 선택해주세요.', null, function() { showErrorList(); }, 700); } function showErrorList() { var actions = []; for (var i = 0; i < errorList.length; i++) { var err = errorList[i]; actions.push({ label: err.id + '. ' + (err.title.length > 25 ? err.title.substring(0, 25) + '...' : err.title), action: 'error_select', data: { id: err.id } }); } // 하단에 네비게이션 버튼 추가 actions.push({ label: '🏠 처음으로', action: 'restart' }); addMsg('bot', '에러 목록:', actions); } function selectError(data) { var err = null; for (var i = 0; i < errorList.length; i++) { if (errorList[i].id === data.id) { err = errorList[i]; break; } } if (!err) return; state.selectedError = err; if (err.type === 'guide') { addMsgWithTyping('💡 "' + err.title + '" 에러가 선택되었습니다.\n\n이 에러는 VPN 접속 불가 조치 가이드로 해결할 수 있어요!', [ { label: '🔧 VPN 접속 불가 조치 시작', action: 'guide', primary: true } ], null, 700); } else { addMsgWithTyping('💡 "' + err.title + '" 에러가 선택되었습니다.\n\n해결 방법을 안내해드릴게요.', null, function() { showErrorSolution(err); }, 700); } } function showErrorSolution(err) { var solution = getErrorSolution(err.id); addMsgWithTyping(solution.text, null, function() { if (solution.file) { addDownloadButton(solution.file.url, solution.file.name, solution.file.tip); } if (solution.link) { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; bubble.innerHTML = '' + solution.link.text + ''; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; } setTimeout(function() { addMsg('bot', '해결되셨나요?', [ { label: '✅ 해결되었습니다!', action: 'error_resolved', primary: true }, { label: '❌ 아직 안됐어요', action: 'error_not_resolved' } ]); }, 500); }, 700); } function getErrorSolution(id) { var solutions = { 3: { text: '📌 인증서 체인 문제 해결\n\n원인: F5 APM Plug-IN 설치 시 Entrust 인증서 확인 실패\n\n✅ 아래 인증서 설치 파일을 다운로드하여 실행해주세요.', file: { url: '/public/share/f5_ssl_cert_Installer_v1.0.zip', name: '인증서 설치 파일', tip: '다운로드 후 압축 해제 → 설치 파일 실행' } }, 4: { text: '📌 네트워크 접속 연결 구성 중 오류 해결\n\n✅ 확인 및 조치방법:\n1. 제어판 → 프로그램 추가/제거 열기\n2. BIG-IP Component Installer 삭제\n3. PC 재부팅\n4. VPN 도메인 재접속하여 Component 재설치\n\n⚠️ DNS 값을 제대로 할당받지 못하는 경우에도 발생합니다.\n다른 네트워크(Wi-Fi 등)로 변경 후 시도해보세요.' }, 5: { text: '📌 전화번호부 파일 업데이트 불가 해결\n\n✅ IE 보안 설정 확인:\n1. Internet Explorer → 인터넷 옵션 열기\n2. 보안 탭 클릭\n3. 신뢰할 수 있는 사이트 선택\n4. VPN URL을 신뢰할 수 있는 사이트에 추가\n5. "보호 모드 사용" 체크\n6. VPN 재접속' }, 6: { text: '📌 원격 컴퓨터 연결 실패 해결\n\n원인: 다른 프로그램과의 충돌\n\n✅ 제거 대상 프로그램:\n• 타사 VPN Agent\n• Antivirus (백신 프로그램)\n• DRM 프로그램\n• Escort 프로그램\n\n위 프로그램 삭제 후 PC 재부팅' }, 7: { text: '📌 틀린 구조 크기 해결\n\n원인: Windows 10 version 1803 이슈\n\n✅ 해결: Windows 업데이트 필요\nMicrosoft KB4103721 (2018년 5월 8일 출시)에서 수정됨\n\n→ Windows 업데이트를 진행해주세요.', link: { url: 'https://www.microsoft.com/ko-kr/software-download/windows10ISO', text: '🔗 Windows 업데이트 바로가기' } }, 8: { text: '📌 연결 완료 전 끊김 해결\n\n⏳ 현재 확인중인 이슈입니다.\n\n임시 조치:\n1. PC 재부팅 후 재시도\n2. 네트워크 변경(유선↔무선) 후 재시도\n3. VPN 프로그램 재설치 후 재시도\n\n해결되지 않으면 원격 지원 접수를 이용해주세요.' }, 9: { text: '📌 네트워크 설정 변경 필요 해결\n\n⏳ 현재 확인중인 이슈입니다.\n\n임시 조치:\n1. PC 재부팅 후 재시도\n2. 네트워크 어댑터 비활성화 후 재활성화\n3. VPN 프로그램 재설치 후 재시도\n\n해결되지 않으면 원격 지원 접수를 이용해주세요.' }, 10: { text: '📌 Local DNS Query 문제 해결\n\n원인: Windows 10 DNS query 동작 변경\n\n✅ 확인 및 조치방법:\n1. 네트워크 어댑터 설정 열기\n2. 인터넷 프로토콜 4 (TCP/IPv4) → 속성\n3. 고급 클릭\n4. 인터페이스 메트릭을 10으로 변경\n5. 확인 후 VPN 재접속' } }; return solutions[id] || { text: '해당 에러의 해결 방법을 찾을 수 없습니다.' }; } function errorResolved() { state.currentFlow = null; saveLog('에러해결완료'); addMsgWithTyping('🎉 문제가 해결되었군요! 다행입니다.\n\n다른 도움이 필요하시면 언제든 말씀해주세요.', [ { label: '🏠 처음으로', action: 'restart' } ]); } function errorNotResolved() { addMsgWithTyping('😔 아직 해결이 안되셨군요.\n\n원격 지원 접수를 통해 전문가의 도움을 받아보세요!', [ { label: '📋 원격 지원 접수하기', action: 'ticket_from_flow' }, { label: '🔄 다른 에러 선택', action: 'error_msg' } ]); } // ========== 티켓 진행률 맵 정의 ========== var ticketJourneySteps = [ { icon: '🔒', label: '동의' }, { icon: '📅', label: '시간' }, { icon: '📝', label: '증상' }, { icon: '👤', label: '정보' }, { icon: '💻', label: '환경' }, { icon: '🔧', label: '조치' }, { icon: '📋', label: '로그' }, { icon: '✅', label: '완료' } ]; // ========== 원격 지원 접수 (티켓) ========== function startTicketFlow(skipPreCheck) { // 문제해결 통해 온 경우가 아니면 먼저 확인 if (!skipPreCheck && !state.fromTroubleshoot) { showTicketPreCheck(); return; } state.currentFlow = 'ticket'; state.ticketStep = 0; state.privacyAgreed = false; state.fromTroubleshoot = false; // 리셋 resetTicketData(); // 실시간 사이드 패널 생성 createTicketSidePanel(); addMsgWithTyping('원격 지원 접수를 도와드릴게요! 📝\n\n먼저 개인정보 수집 동의가 필요합니다.', null, function() { showPrivacyConsent(); }, 700); } // ========== 실시간 사이드 패널 ========== function createTicketSidePanel() { // 기존 패널 제거 var existing = document.getElementById('ticket-side-panel'); if (existing) existing.remove(); var sidePanel = document.createElement('div'); sidePanel.id = 'ticket-side-panel'; sidePanel.className = 'ticket-side-panel'; // 진행률 맵 HTML 생성 var journeyHtml = '
'; for (var i = 0; i < ticketJourneySteps.length; i++) { var stepClass = i === 0 ? 'current' : 'pending'; journeyHtml += '
' + '
' + (i === 0 ? ticketJourneySteps[i].icon : ticketJourneySteps[i].icon) + '
' + '
' + ticketJourneySteps[i].label + '
' + '
'; } journeyHtml += '
'; sidePanel.innerHTML = '
' + '📋 접수 내용' + '' + '
' + journeyHtml + '
' + '
' + '
📅
' + '
' + '
문제 발생 / 원격 가능
' + '
-
' + '
' + '
' + '
' + '
📝
' + '
' + '
이슈 내용
' + '
-
' + '
' + '
' + '
' + '
👤
' + '
' + '
계정 / 연락처
' + '
-
' + '
' + '
' + '
' + '
💻
' + '
' + '
환경 정보
' + '
-
' + '
' + '
' + '
' + '
🔧
' + '
' + '
시도한 조치
' + '
-
' + '
' + '
' + '
'; document.body.appendChild(sidePanel); // 토글 버튼 sidePanel.querySelector('.tsp-toggle').onclick = function() { sidePanel.classList.toggle('collapsed'); this.textContent = sidePanel.classList.contains('collapsed') ? '+' : '−'; }; // 항목 클릭 시 수정 var items = sidePanel.querySelectorAll('.tsp-item'); for (var idx = 0; idx < items.length; idx++) { (function(index) { var item = items[index]; item.onclick = function() { if (this.classList.contains('filled')) { showTicketStep(index); } }; })(idx); } } // 사이드 패널 진행률 업데이트 function updateSidePanelJourney(currentStep) { var journey = document.getElementById('tsp-journey'); if (!journey) return; var steps = journey.querySelectorAll('.tsp-step'); var fill = document.getElementById('tsp-journey-fill'); for (var i = 0; i < steps.length; i++) { steps[i].classList.remove('completed', 'current', 'pending'); var icon = steps[i].querySelector('.tsp-step-icon'); if (i < currentStep) { steps[i].classList.add('completed'); icon.textContent = '✓'; } else if (i === currentStep) { steps[i].classList.add('current'); icon.textContent = ticketJourneySteps[i].icon; } else { steps[i].classList.add('pending'); icon.textContent = ticketJourneySteps[i].icon; } } if (fill) { fill.style.width = ((currentStep / (steps.length - 1)) * 100) + '%'; } } function updateTicketSidePanel() { var sidePanel = document.getElementById('ticket-side-panel'); if (!sidePanel) return; // 날짜/시간 if (state.access_time || state.remote_time) { var dtValue = sidePanel.querySelector('[data-field="datetime"]'); dtValue.innerHTML = (state.access_time ? '발생: ' + state.access_time : '-') + '
' + (state.remote_time ? '원격: ' + state.remote_time : '-'); dtValue.closest('.tsp-item').classList.add('filled'); } // 이슈 if (state.issue) { var issueValue = sidePanel.querySelector('[data-field="issue"]'); issueValue.textContent = state.issue.length > 20 ? state.issue.substring(0, 20) + '...' : state.issue; issueValue.closest('.tsp-item').classList.add('filled'); } // 연락처 if (state.account || state.contact) { var contactValue = sidePanel.querySelector('[data-field="contact"]'); contactValue.textContent = (state.account || '-') + ' / ' + (state.contact || '-'); contactValue.closest('.tsp-item').classList.add('filled'); } // 환경 if (state.device || state.history) { var envValue = sidePanel.querySelector('[data-field="env"]'); envValue.textContent = (state.device || '-') + ' / ' + (state.history || '-'); envValue.closest('.tsp-item').classList.add('filled'); } // 시도한 조치 if (state.tried) { var triedValue = sidePanel.querySelector('[data-field="tried"]'); triedValue.textContent = state.tried.length > 20 ? state.tried.substring(0, 20) + '...' : state.tried; triedValue.closest('.tsp-item').classList.add('filled'); } } function removeTicketSidePanel() { var sidePanel = document.getElementById('ticket-side-panel'); if (sidePanel) { sidePanel.classList.add('hiding'); setTimeout(function() { sidePanel.remove(); }, 300); } } function showTicketPreCheck() { addMsgWithTyping('원격 지원 접수 전에 확인드릴게요! 🤔\n\n혹시 문제해결 메뉴를 통해 자가 조치를 시도해보셨나요?\n\n간단한 조치로 해결되는 경우가 많아요!', [ { label: '⚡ 원터치 해결 시도하기', action: 'onetouch' }, { label: '🔧 VPN 연결이 안돼요', action: 'vpn_issue' }, { label: '⚠️ 에러 메시지가 떠요', action: 'error_msg' }, { label: '✅ 이미 시도했어요, 접수할게요', action: 'ticket_direct' } ]); } function resetTicketData() { state.access_time = ''; state.remote_time = ''; state.issue = ''; state.account = ''; state.contact = ''; state.device = ''; state.history = ''; state.env_change = ''; state.env_change_other = ''; state.tried = ''; state.extra_note = ''; state.attachments = []; } function showPrivacyConsent() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble privacy-bubble'; bubble.innerHTML = '
' + '
📋 개인정보 수집·이용 동의
' + '
' + '1. 수집 항목: 계정, 연락처, 접속 시간, 이슈 내용, 시스템 로그
' + '2. 수집 목적: VPN 장애 원인 분석 및 원격 기술 지원
' + '3. 보유 기간: 문제 해결 후 3개월 이내 파기
' + '4. 동의 거부 시: 원격 지원 서비스 이용 불가' + '
' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; setTimeout(function() { addMsg('bot', '개인정보 수집에 동의하시겠습니까?', [ { label: '✅ 동의합니다', action: 'ticket_privacy_agree', primary: true }, { label: '❌ 동의하지 않습니다', action: 'ticket_privacy_decline' } ]); }, 400); } function ticketPrivacyAgreed() { state.privacyAgreed = true; addMsgWithTyping('동의해주셔서 감사합니다! 🙏\n\n이제 몇 가지 정보를 입력해주세요.', null, function() { showTicketStep(0); }, 700); } function ticketPrivacyDeclined() { state.currentFlow = null; addMsgWithTyping('⚠️ 개인정보 수집에 동의하지 않으시면 원격 지원 서비스를 이용하실 수 없습니다.\n\n자가 조치 가이드를 이용해보세요.', [ { label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '⚠️ 에러 메시지 해결', action: 'error_msg' } ]); } var ticketSteps = [ { key: 'datetime', title: '📅 시간 선택', desc: '문제 발생 시간과 원격 가능 시간을 선택해주세요.' }, { key: 'issue', title: '📝 이슈 내용', desc: '어떤 문제가 발생했는지 상세히 알려주세요.' }, { key: 'contact', title: '📞 연락처 정보', desc: '계정과 연락처를 입력해주세요.' }, { key: 'env', title: '💻 환경 정보', desc: '사용 환경에 대해 알려주세요.' }, { key: 'tried', title: '🔧 시도한 조치', desc: '이미 시도해보신 조치가 있으면 알려주세요.' }, { key: 'log', title: '📁 로그 수집', desc: '마지막으로 로그 파일을 수집해주세요.' }, { key: 'confirm', title: '✅ 접수 확인', desc: '입력하신 내용을 확인해주세요.' } ]; // 특정 단계에 데이터가 입력되었는지 확인 function isStepCompleted(stepKey) { switch(stepKey) { case 'datetime': return state.access_time && state.remote_time; case 'issue': return state.issue && state.issue.trim() !== ''; case 'contact': return state.account && state.contact; case 'env': return state.device && state.history; case 'tried': return state.tried && state.tried.trim() !== ''; case 'log': return state.logCollected; // 로그 수집 완료 여부 case 'confirm': return false; // 확인은 항상 표시 default: return false; } } // 다음 빈 단계로 이동 function nextTicketStep() { var currentStep = state.ticketStep; var nextStep = currentStep + 1; // 다음 빈 단계 찾기 while (nextStep < ticketSteps.length - 1) { // confirm 전까지 if (!isStepCompleted(ticketSteps[nextStep].key)) { break; } nextStep++; } // 모든 단계가 완료되었거나 마지막 단계면 확인 화면으로 if (nextStep >= ticketSteps.length - 1) { nextStep = ticketSteps.length - 1; // confirm 단계 } showTicketStep(nextStep); } function showTicketStep(step) { state.ticketStep = step; var data = ticketSteps[step]; // 사이드 패널 업데이트 updateTicketSidePanel(); // 사이드 패널 진행률 맵 업데이트 (0: 동의, 1: 시간, 2: 증상, 3: 정보, 4: 환경, 5: 완료) var journeyStep = Math.min(step + 1, ticketJourneySteps.length - 1); updateSidePanelJourney(journeyStep); // confirm 단계는 별도 처리 if (data.key === 'confirm') { showConfirmStep(); return; } addMsgWithTyping('' + data.title + '\n\n' + data.desc, null, function() { renderTicketInput(step); }, 600); } function renderTicketInput(step) { var stepKey = ticketSteps[step].key; if (stepKey === 'datetime') { // 통합 일정 선택 카드 var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble datetime-unified-bubble'; // 통합 카드 HTML bubble.innerHTML = '
' + '
' + '📅' + '일정 선택' + '
' + '
' + '
' + '
' + '
' + '
' + (state.access_time ? '✓' : '1') + '
' + '
' + '
' + '
' + '
문제 발생 일시
' + '
' + (state.access_time || '날짜/시간 선택') + '
' + '
' + '
' + '
' + '
' + '
' + (state.remote_time ? '✓' : '2') + '
' + '
' + '
' + '
원격 가능 시간
' + '
' + (state.remote_time || '날짜/시간 선택') + '
' + '
' + '
' + '
' + '
' + '' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 요소 참조 var step1 = bubble.querySelector('#dt-step-1'); var step2 = bubble.querySelector('#dt-step-2'); var accessValue = bubble.querySelector('#dt-access-value'); var remoteValue = bubble.querySelector('#dt-remote-value'); var submitBtn = bubble.querySelector('#dt-unified-submit'); // 버튼 상태 업데이트 function updateSubmitBtn() { submitBtn.disabled = !(state.access_time && state.remote_time); } // Step 1 클릭 - 문제 발생 일시 step1.onclick = function() { openCalendar('access', function(val) { state.access_time = val; accessValue.textContent = val; step1.classList.remove('active'); step1.classList.add('completed'); step1.querySelector('.dt-step-dot').textContent = '✓'; // 자동으로 Step 2 활성화 및 열기 step2.classList.add('active'); updateSubmitBtn(); // 자동으로 원격 가능 시간 달력 열기 setTimeout(function() { openCalendar('remote', function(val2) { state.remote_time = val2; remoteValue.textContent = val2; step2.classList.remove('active'); step2.classList.add('completed'); step2.querySelector('.dt-step-dot').textContent = '✓'; updateSubmitBtn(); }); }, 300); }); }; // Step 2 클릭 - 원격 가능 시간 step2.onclick = function() { openCalendar('remote', function(val) { state.remote_time = val; remoteValue.textContent = val; step2.classList.remove('active'); step2.classList.add('completed'); step2.querySelector('.dt-step-dot').textContent = '✓'; updateSubmitBtn(); }); }; // 다음 버튼 submitBtn.onclick = function() { if (state.access_time && state.remote_time) { this.disabled = true; addMsg('user', '📅 문제발생: ' + state.access_time + '\n📅 원격가능: ' + state.remote_time); updateTicketSidePanel(); nextTicketStep(); } }; updateSubmitBtn(); } else if (stepKey === 'issue') { addInlineForm({ type: 'textarea', title: '📝 이슈 내용', placeholder: '발생한 문제를 상세히 입력해주세요...', required: true, submitText: '다음으로', onSubmit: function(val) { state.issue = val; addMsg('user', val); updateTicketSidePanel(); nextTicketStep(); } }); } else if (stepKey === 'contact') { addInlineForm({ type: 'multi-input', title: '👤 계정 및 연락처', fields: [ { key: 'account', label: '계정 (ID)', placeholder: 'VPN 접속 계정' }, { key: 'contact', label: '연락처', type: 'tel', placeholder: '010-0000-0000' } ], submitText: '다음으로', onSubmit: function(values) { state.account = values.account; state.contact = values.contact; // 로그에 사용자 식별 정보 저장 updateUserIdentity(values.account, values.contact); addMsg('user', '계정: ' + values.account + '\n연락처: ' + values.contact); updateTicketSidePanel(); nextTicketStep(); } }); } else if (stepKey === 'env') { showEnvStep(); } else if (stepKey === 'tried') { showTriedStep(); } else if (stepKey === 'log') { showLogStep(); } else if (stepKey === 'confirm') { showConfirmStep(); } } function checkDatetimeSubmit() { var btn = document.getElementById('dt-submit'); if (btn) { btn.disabled = !(state.access_time && state.remote_time); } } function showEnvStep() { // 환경 정보를 한 화면에서 모두 선택 var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble env-form-bubble'; bubble.innerHTML = '
' + '
' + '
' + '💻' + '사용 단말' + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '
' + '👤' + '사용자 유형' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
' + '🔄' + '최근 환경 변화' + '
' + '
' + '' + '' + '
' + '' + '
' + '' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 버튼 클릭 이벤트 var envData = { device: '', history: '', env_change: '' }; var chips = bubble.querySelectorAll('.env-chip'); var select = bubble.querySelector('.env-select-premium'); var submitBtn = bubble.querySelector('.env-submit-premium'); var otherWrap = bubble.querySelector('.env-other-wrap'); var otherInput = bubble.querySelector('.env-other-input'); for (var c = 0; c < chips.length; c++) { var chip = chips[c]; chip.onclick = function() { var field = this.parentElement.dataset.field; var siblings = this.parentElement.querySelectorAll('.env-chip'); for (var s = 0; s < siblings.length; s++) { siblings[s].classList.remove('selected'); } this.classList.add('selected'); envData[field] = this.dataset.value; checkEnvComplete(); }; } select.onchange = function() { envData.env_change = this.value; if (this.value) { this.parentElement.classList.add('has-value'); } else { this.parentElement.classList.remove('has-value'); } // 기타 선택 시 텍스트 입력 박스 표시 if (this.value === '기타') { otherWrap.style.display = 'block'; setTimeout(function() { otherInput.focus(); }, 100); } else { otherWrap.style.display = 'none'; otherInput.value = ''; } checkEnvComplete(); }; // 기타 입력 시 검증 otherInput.oninput = function() { checkEnvComplete(); }; function checkEnvComplete() { var isOther = select.value === '기타'; var otherValid = !isOther || (isOther && otherInput.value.trim()); var isComplete = envData.device && envData.history && envData.env_change && otherValid; submitBtn.disabled = !isComplete; if (isComplete) { submitBtn.classList.add('ready'); } else { submitBtn.classList.remove('ready'); } } submitBtn.onclick = function() { var isOther = select.value === '기타'; var finalEnvChange = isOther && otherInput.value.trim() ? '기타: ' + otherInput.value.trim() : envData.env_change; if (envData.device && envData.history && finalEnvChange) { state.device = envData.device; state.history = envData.history; state.env_change = finalEnvChange; addMsg('user', '단말: ' + state.device + ' / 첫접속: ' + state.history + ' / 환경변화: ' + state.env_change); updateTicketSidePanel(); nextTicketStep(); } }; } function showEnvChangeDropdown() { var envOptions = [ { value: '없음', icon: '✅' }, { value: '윈도우 업데이트', icon: '🪟' }, { value: '보안프로그램 설치', icon: '🛡️' }, { value: '타사 VPN 프로그램', icon: '🌐' }, { value: '인터넷 옵션 변경', icon: '⚙️' }, { value: '기타', icon: '✏️' } ]; addInlineForm({ type: 'dropdown', title: '최근 환경 변화가 있었나요?', placeholder: '선택해주세요', options: (function() { var arr = []; for (var oi = 0; oi < envOptions.length; oi++) { var o = envOptions[oi]; arr.push({ value: o.value, label: o.value, icon: o.icon }); } return arr; })(), onSelect: function(val) { if (val === '기타') { // 기타 입력 폼 표시 setTimeout(function() { addInlineForm({ type: 'textarea', title: '기타 환경 변화 내용', placeholder: '어떤 변화가 있었는지 입력해주세요...', required: true, submitText: '다음으로', onSubmit: function(otherVal) { state.env_change = '기타: ' + otherVal; handleAction('env_complete'); } }); }, 300); } else { state.env_change = val; setTimeout(function() { handleAction('env_complete'); }, 300); } } }); } // 시도해본 조치 - 객관식 다중선택 function showTriedStep() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var triedOptions = [ { id: 'reboot', icon: '🔄', text: 'PC 재부팅' }, { id: 'reinstall', icon: '📦', text: 'VPN 재설치' }, { id: 'adapter', icon: '🔌', text: '어댑터 재시작' }, { id: 'internet', icon: '🌐', text: '인터넷 확인' }, { id: 'network', icon: '📶', text: '다른 네트워크' }, { id: 'firewall', icon: '🛡️', text: '방화벽 해제' }, { id: 'dns', icon: '🔧', text: 'DNS 초기화' }, { id: 'cache', icon: '🧹', text: '캐시 삭제' }, { id: 'onetouch', icon: '⚡', text: '원터치 해결' }, { id: 'none', icon: '❌', text: '안 해봄' }, { id: 'other', icon: '✏️', text: '기타' } ]; var bubble = document.createElement('div'); bubble.className = 'vpn-bubble tried-form-bubble'; var html = '
' + '
' + '🔧' + '
' + '시도해본 조치' + '해당하는 항목을 모두 선택해주세요' + '
' + '
' + '
'; for (var o = 0; o < triedOptions.length; o++) { var opt = triedOptions[o]; html += ''; } html += '
' + '' + '' + '
'; bubble.innerHTML = html; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 이벤트 var selectedItems = []; var checkboxes = bubble.querySelectorAll('.tried-item input'); var otherWrap = bubble.querySelector('.tried-other-wrap'); var otherInput = bubble.querySelector('.tried-other-input'); var submitBtn = bubble.querySelector('.tried-submit-btn'); for (var b = 0; b < checkboxes.length; b++) { var cb = checkboxes[b]; cb.onchange = function() { var item = this.closest('.tried-item'); var itemId = item.dataset.id; if (this.checked) { item.classList.add('checked'); if (selectedItems.indexOf(this.value) === -1) { selectedItems.push(this.value); } } else { item.classList.remove('checked'); var idx = selectedItems.indexOf(this.value); if (idx > -1) selectedItems.splice(idx, 1); } // 기타 선택 시 텍스트 입력 표시 if (itemId === 'other') { otherWrap.style.display = this.checked ? 'block' : 'none'; if (this.checked) { setTimeout(function() { otherInput.focus(); }, 100); } } checkTriedComplete(); }; } function checkTriedComplete() { var otherChecked = bubble.querySelector('[data-id="other"] input').checked; var hasSelection = selectedItems.length > 0; var otherValid = !otherChecked || (otherChecked && otherInput.value.trim()); if (hasSelection && otherValid) { submitBtn.disabled = false; submitBtn.classList.add('ready'); } else { submitBtn.disabled = true; submitBtn.classList.remove('ready'); } } otherInput.oninput = function() { checkTriedComplete(); }; submitBtn.onclick = function() { if (this.disabled) return; var result = selectedItems.slice(); var otherChecked = bubble.querySelector('[data-id="other"] input').checked; if (otherChecked && otherInput.value.trim()) { // 기타를 실제 입력값으로 대체 var idx = result.indexOf('기타'); if (idx > -1) { result[idx] = '기타: ' + otherInput.value.trim(); } } state.tried = result.join(', '); addMsg('user', '시도한 조치: ' + state.tried); updateTicketSidePanel(); nextTicketStep(); }; } function showLogStep() { // 로그 수집 안내 메시지 var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble log-step-bubble'; bubble.innerHTML = '
' + '
' + '📁' + '로그 수집' + '
' + '
' + '

원격 지원을 위해 로그 파일이 필요합니다.

' + '' + '📥' + '로그수집파일 다운로드' + '' + '' + '' + '
' + '' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 다운로드 클릭 시 안내 표시 var downloadBtn = bubble.querySelector('#log-download-btn'); var submitBtn = bubble.querySelector('#log-submit-btn'); var logNotice = bubble.querySelector('#log-notice'); var completeNotice = bubble.querySelector('#log-complete-notice'); var downloaded = false; downloadBtn.onclick = function() { downloaded = true; this.classList.add('downloaded'); this.innerHTML = '다운로드 완료!'; // 안내 메시지 표시 setTimeout(function() { logNotice.style.display = 'block'; logNotice.style.animation = 'fadeInUp 0.4s ease'; chat.scrollTop = chat.scrollHeight; }, 300); // 완료 안내 및 버튼 활성화 setTimeout(function() { completeNotice.style.display = 'flex'; completeNotice.style.animation = 'fadeInUp 0.4s ease'; submitBtn.disabled = false; submitBtn.classList.add('ready'); chat.scrollTop = chat.scrollHeight; }, 800); }; submitBtn.onclick = function() { if (downloaded) { this.disabled = true; state.logCollected = true; addMsg('user', '📁 로그 파일 준비 완료'); updateTicketSidePanel(); nextTicketStep(); } else { showToast('⬇️ 먼저 파일을 다운로드 해주세요', 'warning'); } }; } // ========== 확인 단계 + 팝업 모달 ========== function showConfirmStep() { // 사이드 패널 업데이트 updateTicketSidePanel(); addMsgWithTyping('모든 정보가 입력되었습니다! ✅\n\n왼쪽 패널에서 입력 내용을 확인하고,\n항목을 클릭하면 수정할 수 있어요.', null, function() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble confirm-bubble-simple'; bubble.innerHTML = '
' + '
' + '💡 아웃룩 등 메일 앱이 설치되어 있다면 메일 전송 버튼을 클릭하세요.
' + '기본 메일 앱이 없으면 아래 웹메일로 보내 주세요.
' + '📧 tsc@openbase.co.kr 로 메일을 보내 주세요.' + '
' + '
' + '' + '' + '
' + '
' + '웹메일로 보내기 (기본 앱 없을 때)' + '
' + '' + '' + '' + '
' + '열린 뒤: ① Ctrl+V ② 로그 첨부 ③ 전송' + '
' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; bubble.querySelector('.confirm-simple-btn.preview').onclick = function() { showEmailPreviewModal(); }; bubble.querySelector('.confirm-simple-btn.submit').onclick = function() { openMail(); }; var webmailBtns = bubble.querySelectorAll('.confirm-webmail-btn'); for (var w = 0; w < webmailBtns.length; w++) { webmailBtns[w].onclick = function() { var p = this.getAttribute('data-provider'); if (p) openWebMail(p); }; } }, 600); } function showEmailPreviewModal() { var emailBody = getEmailBody(); // 모달 생성 var modal = document.createElement('div'); modal.className = 'email-preview-modal'; modal.innerHTML = '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '' + '
' + '' + '
'; document.body.appendChild(modal); setTimeout(function() { modal.classList.add('active'); }, 10); // 닫기 modal.querySelector('.email-preview-close').onclick = function() { modal.classList.remove('active'); setTimeout(function() { modal.remove(); }, 300); }; modal.querySelector('.email-preview-backdrop').onclick = function() { modal.classList.remove('active'); setTimeout(function() { modal.remove(); }, 300); }; // 복사 modal.querySelector('.email-preview-btn.copy').onclick = function() { navigator.clipboard.writeText(emailBody).then(function() { showToast('📋 클립보드에 복사되었습니다!', 'success'); }); }; modal.querySelector('.email-preview-btn.send').onclick = function() { modal.classList.remove('active'); setTimeout(function() { modal.remove(); }, 300); openMail(); }; var webmailBtns = modal.querySelectorAll('.email-preview-webmail-btn'); for (var w = 0; w < webmailBtns.length; w++) { webmailBtns[w].onclick = function() { var p = this.getAttribute('data-provider'); if (p) { modal.classList.remove('active'); setTimeout(function() { modal.remove(); }, 300); openWebMail(p); } }; } } function getEmailBody() { var body = '【 원격 지원 접수 】\n'; body += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; body += '▶ 문제 발생 일시: ' + state.access_time + '\n\n'; body += '▶ 원격 가능 시간: ' + state.remote_time + '\n\n'; body += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; body += '▶ 이슈 내용:\n'; body += ' ' + state.issue + '\n\n'; body += '▶ 계정: ' + state.account + '\n'; body += '▶ 연락처: ' + state.contact + '\n\n'; body += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; body += '▶ 사용 단말: ' + state.device + '\n'; body += '▶ 처음 접속 여부: ' + state.history + '\n'; body += '▶ 최근 환경 변화: ' + state.env_change + '\n'; body += '▶ 시도한 조치: ' + state.tried + '\n\n'; body += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; body += '⚠️ 바탕화면의 로그 파일(_vpnlog.txt)을 반드시 첨부해주세요!\n\n'; body += '─────────────────────────────\n'; body += '오비서(Openbase By Service) (VPN Support Bot) 자동 생성'; return body; } function finishTicketSubmit() { userLog.ticketData = { accessTime: state.access_time, remoteTime: state.remote_time, issue: state.issue, account: state.account, contact: state.contact, device: state.device, history: state.history, envChange: state.env_change, tried: state.tried, extraNote: state.extra_note }; saveLog('원격지원접수완료'); removeTicketSidePanel(); showToast('✅ 원격 지원 접수 완료!', 'success'); addMsgWithTyping('🎉 접수가 완료되었습니다!\n\n메일 앱에서 로그 파일을 첨부하신 후 전송해주세요.\n빠른 시일 내에 담당자가 연락드리겠습니다.', null, function() { showSatisfactionSurvey(); }, 1000); } function openMail() { var body = getEmailBody(); var url = 'mailto:tsc@openbase.co.kr?subject=' + encodeURIComponent('[VPN] 원격 지원 접수') + '&body=' + encodeURIComponent(body); window.location.href = url; finishTicketSubmit(); } /** 기본 메일 앱이 없을 때: 본문 복사 후 웹메일 작성창 열기 (붙여넣기만 하면 됨) */ function openWebMail(provider) { var body = getEmailBody(); var subject = '[VPN] 원격 지원 접수'; var to = 'tsc@openbase.co.kr'; var url = ''; if (provider === 'gmail') { url = 'https://mail.google.com/mail/?view=cm&fs=1&to=' + encodeURIComponent(to) + '&su=' + encodeURIComponent(subject); } else if (provider === 'naver') { url = 'https://mail.naver.com/write/popup?receiver=' + encodeURIComponent(to) + '&subject=' + encodeURIComponent(subject); } else if (provider === 'outlook') { url = 'https://outlook.live.com/mail/0/deeplink/compose?to=' + encodeURIComponent(to) + '&subject=' + encodeURIComponent(subject); } var stepGuide = '① Ctrl+V 붙여넣기 ② 로그 파일 첨부 ③ 전송'; if (url && typeof navigator.clipboard !== 'undefined' && navigator.clipboard.writeText) { navigator.clipboard.writeText(body).then(function() { showToast('본문 복사됨! 웹메일에서 ' + stepGuide, 'success'); window.open(url, '_blank', 'noopener,noreferrer'); }); } else { window.open(url, '_blank', 'noopener,noreferrer'); showToast('웹메일이 열렸어요. 미리보기에서 복사 후 ' + stepGuide, 'info'); } finishTicketSubmit(); } // ========== 만족도 조사 ========== function showSatisfactionSurvey() { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble satisfaction-bubble'; bubble.innerHTML = '
' + '
📊 서비스 만족도 조사
' + '
이번 상담은 어떠셨나요?
' + '
' + '' + '' + '' + '' + '' + '
' + '
' + '불만족' + '매우만족' + '
' + '
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 별점 클릭 이벤트 var stars = bubble.querySelectorAll('.star-btn'); for (var st = 0; st < stars.length; st++) { (function(index) { var star = stars[index]; star.onmouseenter = function() { highlightStars(stars, index + 1); }; star.onmouseleave = function() { highlightStars(stars, 0); }; star.onclick = function() { var rating = parseInt(this.getAttribute('data-rating'), 10); submitSatisfaction(rating, bubble); }; })(st); } } function highlightStars(stars, count) { for (var i = 0; i < stars.length; i++) { if (i < count) { stars[i].classList.add('active'); } else { stars[i].classList.remove('active'); } } } function submitSatisfaction(rating, bubble) { // 로그 저장 logAction('만족도조사', rating + '점'); var messages = { 1: '소중한 의견 감사합니다. 더 나은 서비스를 위해 노력하겠습니다.', 2: '소중한 의견 감사합니다. 개선할 점을 반영하겠습니다.', 3: '피드백 감사합니다! 더 좋은 서비스로 보답하겠습니다.', 4: '좋은 평가 감사합니다! 😊', 5: '최고 평가 감사합니다! 🎉 앞으로도 최선을 다하겠습니다!' }; bubble.innerHTML = '
' + '
' + '
' + '⭐'.repeat(rating) + '
' + '
' + messages[rating] + '
' + '
' + '
'; setTimeout(function() { addMsg('bot', '다른 도움이 필요하시면 언제든 말씀해주세요! 😊', [ { label: '🏠 처음으로', action: 'restart' } ]); }, 800); } // ========== 대화 재시작 ========== function restartConversation() { state.currentFlow = null; // 사이드 패널 제거 removeTicketSidePanel(); addMsgWithTyping('다시 도움이 필요하시면 말씀해주세요! 😊', [ { label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '⚠️ 에러 메시지 해결', action: 'error_msg' }, { label: '📋 원격 지원 접수', action: 'ticket' } ]); } // ========== 캘린더 모달 ========== var calModal = document.getElementById('vpn-cal-modal'); var calMonth = document.getElementById('vpn-cal-month'); var calDays = document.getElementById('vpn-cal-days'); var timeGrid = document.getElementById('vpn-time-grid'); var calPrev = document.getElementById('vpn-cal-prev'); var calNext = document.getElementById('vpn-cal-next'); var calCancel = document.getElementById('vpn-cal-cancel'); var calApply = document.getElementById('vpn-cal-apply'); var calTitle = document.getElementById('vpn-cal-title'); var calCallback = null; function openCalendar(type, callback) { state.calType = type; calCallback = callback; var now = new Date(); state.calYear = now.getFullYear(); state.calMonth = now.getMonth(); state.calDate = ''; state.calTime = ''; state.calAmpm = ''; state.calHour = ''; state.calMinute = ''; // 타이틀만 표시, 탭 숨김 if (type === 'access') { calTitle.textContent = '📅 문제 발생 일시 선택'; } else { calTitle.textContent = '📅 원격 가능 시간 선택'; } // 탭 숨기기 var tabsContainer = document.querySelector('.vpn-cal-tabs'); if (tabsContainer) tabsContainer.style.display = 'none'; renderCalendar(); hideTimeSection(); calApply.disabled = true; calModal.className = 'active'; } function hideTimeSection() { var timeSection = document.querySelector('.vpn-time-section'); if (timeSection) { timeSection.classList.remove('visible'); } } function showTimeSection() { var timeSection = document.querySelector('.vpn-time-section'); if (timeSection) { timeSection.classList.add('visible'); renderTimeGrid(); } } function renderCalendar() { var year = state.calYear; var month = state.calMonth; calMonth.textContent = year + '년 ' + (month + 1) + '월'; var firstDay = new Date(year, month, 1).getDay(); var daysInMonth = new Date(year, month + 1, 0).getDate(); var today = new Date(); var todayStr = today.getFullYear() + '-' + pad(today.getMonth() + 1) + '-' + pad(today.getDate()); today.setHours(0, 0, 0, 0); calDays.innerHTML = ''; for (var i = 0; i < firstDay; i++) { var empty = document.createElement('div'); empty.className = 'vpn-cal-day disabled'; calDays.appendChild(empty); } for (var d = 1; d <= daysInMonth; d++) { var dateStr = year + '-' + pad(month + 1) + '-' + pad(d); var currentDate = new Date(year, month, d); currentDate.setHours(0, 0, 0, 0); var dayOfWeek = currentDate.getDay(); var div = document.createElement('div'); div.className = 'vpn-cal-day'; var isDisabled = false; if (state.calType === 'access') { if (currentDate > today) isDisabled = true; } else if (state.calType === 'remote') { if (currentDate < today) isDisabled = true; } if (isDisabled) div.className += ' disabled'; if (dayOfWeek === 0) div.className += ' sunday'; if (dayOfWeek === 6) div.className += ' saturday'; if (dateStr === todayStr) div.className += ' today'; if (dateStr === state.calDate) div.className += ' selected'; div.textContent = d; div.setAttribute('data-date', dateStr); if (!isDisabled) { div.onclick = function() { state.calDate = this.getAttribute('data-date'); renderCalendar(); showTimeSection(); }; } calDays.appendChild(div); } } function renderTimeGrid() { timeGrid.innerHTML = ''; var picker = document.createElement('div'); picker.className = 'vpn-time-picker'; // 오전/오후 휠 var ampmContainer = document.createElement('div'); ampmContainer.className = 'vpn-wheel-container vpn-ampm-wheel'; var ampmHighlight = document.createElement('div'); ampmHighlight.className = 'vpn-wheel-highlight'; ampmContainer.appendChild(ampmHighlight); var ampmWheel = document.createElement('div'); ampmWheel.className = 'vpn-wheel'; ampmWheel.id = 'vpn-ampm-wheel'; var ampms = ['오전', '오후']; for (var a = 0; a < ampms.length; a++) { var ampmItem = document.createElement('div'); ampmItem.className = 'vpn-wheel-item'; ampmItem.textContent = ampms[a]; ampmItem.setAttribute('data-value', ampms[a]); ampmWheel.appendChild(ampmItem); } ampmContainer.appendChild(ampmWheel); picker.appendChild(ampmContainer); // 시간 휠 var hourContainer = document.createElement('div'); hourContainer.className = 'vpn-wheel-container vpn-hour-wheel'; var hourHighlight = document.createElement('div'); hourHighlight.className = 'vpn-wheel-highlight'; hourContainer.appendChild(hourHighlight); var hourWheel = document.createElement('div'); hourWheel.className = 'vpn-wheel'; hourWheel.id = 'vpn-hour-wheel'; for (var h = 1; h <= 12; h++) { var hourItem = document.createElement('div'); hourItem.className = 'vpn-wheel-item'; hourItem.textContent = h; hourItem.setAttribute('data-value', h); hourWheel.appendChild(hourItem); } hourContainer.appendChild(hourWheel); picker.appendChild(hourContainer); var hourLabel = document.createElement('span'); hourLabel.className = 'vpn-wheel-label'; hourLabel.textContent = '시'; picker.appendChild(hourLabel); // 분 휠 var minContainer = document.createElement('div'); minContainer.className = 'vpn-wheel-container vpn-minute-wheel'; var minHighlight = document.createElement('div'); minHighlight.className = 'vpn-wheel-highlight'; minContainer.appendChild(minHighlight); var minWheel = document.createElement('div'); minWheel.className = 'vpn-wheel'; minWheel.id = 'vpn-min-wheel'; for (var m = 0; m < 60; m++) { var minItem = document.createElement('div'); minItem.className = 'vpn-wheel-item'; minItem.textContent = pad(m); minItem.setAttribute('data-value', pad(m)); minWheel.appendChild(minItem); } minContainer.appendChild(minWheel); picker.appendChild(minContainer); var minLabel = document.createElement('span'); minLabel.className = 'vpn-wheel-label'; minLabel.textContent = '분'; picker.appendChild(minLabel); timeGrid.appendChild(picker); setTimeout(function() { setupWheelScroll('vpn-ampm-wheel', function(val) { state.calAmpm = val; updateTimeFromWheels(); }); setupWheelScroll('vpn-hour-wheel', function(val) { state.calHour = val; updateTimeFromWheels(); }); setupWheelScroll('vpn-min-wheel', function(val) { state.calMinute = val; updateTimeFromWheels(); }); var now = new Date(); var currentHour = now.getHours(); var currentMin = now.getMinutes(); state.calAmpm = currentHour >= 12 ? '오후' : '오전'; state.calHour = currentHour > 12 ? currentHour - 12 : (currentHour === 0 ? 12 : currentHour); state.calMinute = pad(currentMin); scrollToValue('vpn-ampm-wheel', state.calAmpm); scrollToValue('vpn-hour-wheel', state.calHour); scrollToValue('vpn-min-wheel', state.calMinute); updateTimeFromWheels(); }, 50); } function setupWheelScroll(wheelId, callback) { var wheel = document.getElementById(wheelId); if (!wheel) return; var scrollTimeout; wheel.onscroll = function() { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(function() { var items = wheel.querySelectorAll('.vpn-wheel-item'); var wheelRect = wheel.getBoundingClientRect(); var centerY = wheelRect.top + wheelRect.height / 2; var closestItem = null; var closestDist = Infinity; for (var i = 0; i < items.length; i++) { var itemRect = items[i].getBoundingClientRect(); var itemCenterY = itemRect.top + itemRect.height / 2; var dist = Math.abs(centerY - itemCenterY); if (dist < closestDist) { closestDist = dist; closestItem = items[i]; } } if (closestItem) { for (var j = 0; j < items.length; j++) { items[j].classList.remove('selected'); } closestItem.classList.add('selected'); callback(closestItem.getAttribute('data-value')); var itemTop = closestItem.offsetTop; var scrollTarget = itemTop - (wheel.clientHeight / 2) + (closestItem.clientHeight / 2); wheel.scrollTo({ top: scrollTarget, behavior: 'smooth' }); } }, 80); }; var clickItems = wheel.querySelectorAll('.vpn-wheel-item'); for (var i = 0; i < clickItems.length; i++) { clickItems[i].onclick = function() { var itemTop = this.offsetTop; var scrollTarget = itemTop - (wheel.clientHeight / 2) + (this.clientHeight / 2); wheel.scrollTo({ top: scrollTarget, behavior: 'smooth' }); }; } } function scrollToValue(wheelId, value) { var wheel = document.getElementById(wheelId); if (!wheel) return; var items = wheel.querySelectorAll('.vpn-wheel-item'); for (var i = 0; i < items.length; i++) { items[i].classList.remove('selected'); } for (var i = 0; i < items.length; i++) { if (items[i].getAttribute('data-value') == value) { items[i].classList.add('selected'); var itemTop = items[i].offsetTop; var scrollTarget = itemTop - (wheel.clientHeight / 2) + (items[i].clientHeight / 2); wheel.scrollTop = scrollTarget; break; } } } function updateTimeFromWheels() { if (!state.calAmpm || !state.calHour || state.calMinute === undefined || state.calMinute === '') return; var hour = parseInt(state.calHour); if (state.calAmpm === '오후' && hour !== 12) { hour += 12; } else if (state.calAmpm === '오전' && hour === 12) { hour = 0; } var min = typeof state.calMinute === 'number' ? pad(state.calMinute) : state.calMinute; state.calTime = pad(hour) + ':' + min; if (state.calDate && state.calTime) { calApply.disabled = false; } } function applyCalendar() { if (!state.calDate || !state.calTime) return; var now = new Date(); var selectedParts = state.calDate.split('-'); var selectedDateTime = new Date( parseInt(selectedParts[0]), parseInt(selectedParts[1]) - 1, parseInt(selectedParts[2]), parseInt(state.calTime.split(':')[0]), parseInt(state.calTime.split(':')[1]) ); var today = new Date(); var isToday = (parseInt(selectedParts[0]) === today.getFullYear() && parseInt(selectedParts[1]) - 1 === today.getMonth() && parseInt(selectedParts[2]) === today.getDate()); if (isToday) { if (state.calType === 'access' && selectedDateTime > now) { alert('문제 발생 일시는 현재 시간 이후로 선택할 수 없습니다.'); return; } if (state.calType === 'remote' && selectedDateTime < now) { alert('원격 가능 시간은 현재 시간 이전으로 선택할 수 없습니다.'); return; } } var timeDisplay = state.calAmpm + ' ' + state.calHour + '시 ' + state.calMinute + '분'; var val = state.calDate + ' ' + timeDisplay; calModal.className = ''; if (calCallback) { calCallback(val); } } // ========== FAQ 데이터 ========== var faqList = [ // 접속 관련 { keywords: ['주소', 'url', '어디', '사이트', '링크', '접속 주소'], answer: 'VPN 접속 주소예요! 🌐\n\nhttps://vpn.cef.or.kr\n\n권장 브라우저: Chrome, Edge, Safari\n권장: 시크릿(비공개) 모드로 접속하면 캐시 문제를 줄일 수 있어요.' }, { keywords: ['시크릿', '비공개', '시크릿 모드', '프라이빗', '캐시'], answer: '시크릿 모드 단축키예요! 🔒\n\n• Chrome/Edge (Windows): Ctrl + Shift + N\n• Chrome (Mac): ⌘ + Shift + N\n• Safari (Mac): ⌘ + Shift + N\n\n캐시/쿠키 문제 해결에 효과적이에요!' }, { keywords: ['로그인', '아이디', 'id', '계정', '비밀번호', '비번'], answer: 'VPN 로그인 정보예요! 🔑\n\n• 아이디: 사번(직원) / 이름(협력사)\n• 비밀번호: 그룹웨어 비밀번호와 동일\n\n비밀번호 오류 시 그룹웨어에서 초기화 후 재시도해주세요. 계속 문제면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket', primary: true }] }, // 설치/삭제 { keywords: ['설치', '다운로드', '다운', '받기'], answer: 'VPN 설치 방법이에요! 📥\n\n1. vpn.cef.or.kr 접속\n2. 로그인 후 설치 파일 자동 다운로드\n3. 다운로드된 파일을 관리자 권한으로 실행\n4. 설치 완료 후 브라우저 새로고침\n\n접속이 안 되시면 "VPN 접속 불가 조치" 가이드를 이용해주세요!', actions: [{ label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }] }, { keywords: ['삭제', '제거', '언인스톨', '지우기', '재설치'], answer: 'VPN 삭제/재설치 방법이에요! 🗑️\n\n[삭제] 제어판 → 프로그램 제거 → "BIG-IP Edge Client" 삭제\n[재설치] PC 재부팅 후 VPN 사이트 다시 접속\n\n상세 절차는 "VPN 접속 불가 조치" 가이드에서 확인해주세요!', actions: [{ label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }] }, // 플랫폼별 { keywords: ['맥', 'mac', '맥북', 'macos'], answer: 'Mac VPN 사용법이에요! 🍎\n\n• Safari 또는 Chrome으로 vpn.cef.or.kr 접속\n• 로그인 시 Mac용 클라이언트 자동 설치 안내\n• 시크릿 모드: ⌘ + Shift + N\n\n문제가 있으면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket' }] }, { keywords: ['모바일', '핸드폰', '아이폰', '안드로이드', '폰'], answer: '모바일 VPN 사용법이에요! 📱\n\n• 앱: 앱스토어/플레이스토어에서 "F5 Access" 검색 설치\n• 서버 주소: vpn.cef.or.kr 입력\n• 로그인: 사내 계정(아이디/비밀번호) 사용' }, // 자주 발생하는 문제 { keywords: ['느려', '느림', '속도', '버벅', '렉'], answer: 'VPN이 느리시군요! 🐢\n\n먼저 확인해볼 것:\n• 인터넷 연결 상태(유선 권장)\n• 다른 대용량 다운로드 중인지\n• 시크릿 모드로 재접속 시도\n• VPN 끊었다가 다시 연결\n\n계속 느리면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket' }] }, { keywords: ['끊겨', '끊김', '끊어', '자꾸'], answer: 'VPN이 자주 끊기시나요? 😥\n\n해결 방법:\n• 유선 인터넷 사용 권장(와이파이보다 안정적)\n• PC 절전 모드 해제\n• 타사 VPN 프로그램이 있으면 삭제\n• 방화벽/백신에서 VPN 예외 처리\n\n계속되면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket' }] }, { keywords: ['인증서', 'certificate', '신뢰'], answer: '인증서 관련 오류가 발생하셨군요! 📜\n\n"에러 메시지 해결" 메뉴에서 해당 에러를 선택하시면 단계별 해결 방법을 안내받으실 수 있어요.', actions: [{ label: '⚠️ 에러 메시지 해결', action: 'error_msg', primary: true }] }, { keywords: ['방화벽', 'firewall', '차단'], answer: '방화벽/보안 프로그램 관련이에요! 🛡️\n\n확인할 것:\n• Windows 방화벽: VPN 관련 앱 허용\n• 백신 프로그램: VPN 예외 처리\n• 회사 보안 프로그램: IT팀에 문의\n\n해결이 어려우시면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket' }] }, { keywords: ['타임아웃', 'timeout', '응답없음', '대기'], answer: '연결 타임아웃 문제예요! ⏱️\n\n확인할 것:\n• 인터넷 연결 상태\n• 방화벽/백신에서 VPN 차단 여부\n• 시크릿 모드로 재접속 시도\n\n"VPN 접속 불가 조치" 가이드도 확인해보세요!', actions: [{ label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }] }, { keywords: ['dns', 'ip', '주소 충돌', '네트워크'], answer: 'DNS/네트워크 문제로 보여요! 🌐\n\n"VPN 접속 불가 조치" 2단계에서 네트워크 어댑터 초기화를 진행해보세요. 문제가 계속되면 원격 지원을 접수해주세요!', actions: [{ label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '📋 원격 지원 접수', action: 'ticket' }] }, // 업무 관련 { keywords: ['재택', '재택근무', '집에서', '원격근무'], answer: '재택근무 VPN 사용법이에요! 🏠\n\n1. vpn.cef.or.kr 접속\n2. 사내 계정(아이디/비밀번호) 로그인\n3. VPN 연결 완료 후 사내 시스템 접근\n\n접속이 안 되시면 "VPN 접속 불가 조치" 가이드를 확인해주세요!', actions: [{ label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }] }, { keywords: ['사내', '회사', '내부', '그룹웨어', '접근'], answer: 'VPN 연결 후 사내망 접근 안내예요! 🏢\n\n먼저 확인:\n• VPN이 연결된 상태인지 확인\n• 브라우저 캐시 삭제 후 재시도\n• 시크릿 모드로 접속 시도\n\nVPN은 되는데 사내 시스템 접근이 안 되시면 원격 지원 접수해주세요!', actions: [{ label: '📋 원격 지원 접수', action: 'ticket' }] }, // 연락처 (준비중) { keywords: ['담당자', '연락처', '전화', '상담', '사람'], answer: '운영시간/담당자 안내는 현재 준비중이에요! 📞\n\n원격 지원 접수를 남겨주시면 담당자가 확인 후 연락드릴 예정입니다.', actions: [{ label: '📋 원격 지원 접수', action: 'ticket', primary: true }] }, { keywords: ['운영시간', '업무시간', '몇시'], answer: '운영시간/담당자 안내는 현재 준비중이에요! ⏰\n\n원격 지원 접수를 남겨주시면 담당자가 확인 후 연락드릴 예정입니다.', actions: [{ label: '📋 원격 지원 접수', action: 'ticket', primary: true }] } ]; // ========== 텍스트 입력 처리 ========== function sendText() { var msg = input.value.trim(); if (!msg) return; addMsg('user', msg); input.value = ''; var lowerMsg = msg.toLowerCase(); // 인사말 처리 if (lowerMsg.match(/^(안녕|하이|hi|hello|반가워|헬로|ㅎㅇ|하잉)/)) { addMsgWithTyping('안녕하세요! 😊\n무엇이 궁금하신가요?', [ { label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '⚠️ 에러 메시지 해결', action: 'error_msg' }, { label: '📋 원격 지원 접수', action: 'ticket' } ]); return; } // 감사 인사 if (lowerMsg.match(/(고마워|감사|땡큐|thank|ㄱㅅ|굿|좋아)/)) { addMsgWithTyping('도움이 되셨다니 기뻐요! 😄\n다른 문의사항이 있으시면 언제든 말씀해주세요.', [ { label: '🏠 처음으로', action: 'restart' } ]); return; } // 작별 인사 if (lowerMsg.match(/(잘가|바이|bye|안녕히|ㅂㅂ|끝|종료)/)) { addMsgWithTyping('감사합니다! 좋은 하루 되세요! 👋'); return; } // 도움 요청 if (lowerMsg.match(/(도움|help|도와|뭐해|뭘 할 수|기능|메뉴)/)) { addMsgWithTyping('제가 도와드릴 수 있는 것들이에요! 🤖\n\n• VPN 접속 불가 시 단계별 조치 가이드\n• 에러 메시지별 해결 방법 안내\n• 원격 지원 접수 (메일 전송)\n• VPN 관련 자주 묻는 질문 답변', [ { label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '⚠️ 에러 메시지 해결', action: 'error_msg' }, { label: '📋 원격 지원 접수', action: 'ticket' } ]); return; } // 에러 메시지 매칭 (여러 개 매칭되면 선택지 제공) var errorMatches = findErrorMatches(msg); if (errorMatches.length > 0) { if (errorMatches.length === 1) { // 1개만 매칭되면 바로 해당 에러로 진행 selectError({ id: errorMatches[0].id }); return; } else { // 여러 개 매칭되면 선택지 제공 var actions = []; for (var i = 0; i < errorMatches.length; i++) { actions.push({ label: errorMatches[i].title.length > 30 ? errorMatches[i].title.substring(0, 30) + '...' : errorMatches[i].title, action: 'error_select', data: { id: errorMatches[i].id } }); } addMsgWithTyping('입력하신 내용과 관련된 에러가 여러 개 있어요. 🔍\n\n어떤 에러에 해당하시나요?', actions); return; } } // FAQ 검색 var faqMatch = findFaqMatch(msg); if (faqMatch) { addMsgWithTyping(faqMatch.answer, faqMatch.actions || null); return; } // VPN 액션 키워드 (에러/장애 관련은 에러 목록으로) if (msg.match(/(에러|오류|error)/i)) { handleAction('error_msg'); return; } if (msg.match(/(접수|장애|지원|신청|요청)/)) { handleAction('ticket'); return; } if (msg.match(/(조치|가이드|불가|안돼|안됨|안되|접속이)/)) { handleAction('guide'); return; } if (msg.match(/(에러|오류|메시지|에러창|팝업|경고)/)) { handleAction('error_msg'); return; } // 스마트 폴백: 유사한 FAQ 2~3개 제안 var similarFaqs = findSimilarFaqs(msg, 3); if (similarFaqs.length > 0) { var suggestionText = '정확한 답변을 찾지 못했어요 🤔\n\n혹시 이런 내용인가요?'; var actions = []; for (var si = 0; si < similarFaqs.length; si++) { var faq = similarFaqs[si]; actions.push({ label: (si + 1) + '. ' + getSummaryFromAnswer(faq.answer), action: 'faq_direct', data: { index: faqList.indexOf(faq) } }); } actions.push({ label: '❌ 해당 없음', action: 'no_match_fallback' }); addMsgWithTyping(suggestionText, actions); } else { // 완전히 매칭 안 됨 addMsgWithTyping('죄송해요, 해당 질문에 대한 답변을 찾지 못했어요. 🤔\n\n이런 질문은 대답할 수 있어요:\n• VPN 주소가 뭐야?\n• 설치/삭제 방법\n• 비밀번호 관련\n• 맥/모바일 사용법\n• 속도 느림/끊김 문제\n\n아래 메뉴를 선택해주셔도 좋아요!', [ { label: '🔧 VPN 접속 불가 조치', action: 'guide', primary: true }, { label: '⚠️ 에러 메시지 해결', action: 'error_msg' }, { label: '📋 원격 지원 접수', action: 'ticket' } ]); } } // 유사 FAQ 찾기 (스마트 폴백용) function findSimilarFaqs(msg, limit) { var lowerMsg = msg.toLowerCase(); var scored = []; for (var i = 0; i < faqList.length; i++) { var faq = faqList[i]; var score = 0; for (var j = 0; j < faq.keywords.length; j++) { var keyword = faq.keywords[j].toLowerCase(); if (lowerMsg.indexOf(keyword) >= 0) { score += keyword.length; } else if (keyword.indexOf(lowerMsg) >= 0 && lowerMsg.length >= 2) { // 부분 매칭도 점수 부여 score += 1; } } if (score > 0) { scored.push({ faq: faq, score: score }); } } // 점수 내림차순 정렬 scored.sort(function(a, b) { return b.score - a.score; }); var out = []; var lim = limit < scored.length ? limit : scored.length; for (var sl = 0; sl < lim; sl++) { out.push(scored[sl].faq); } return out; } // FAQ 답변에서 요약 추출 function getSummaryFromAnswer(answer) { // 첫 줄 또는 첫 20자 추출 var firstLine = answer.split('\n')[0].replace(/<[^>]*>/g, ''); if (firstLine.length > 25) { return firstLine.substring(0, 22) + '...'; } return firstLine; } // FAQ 매칭 함수 function findFaqMatch(msg) { var lowerMsg = msg.toLowerCase(); var bestMatch = null; var bestScore = 0; for (var i = 0; i < faqList.length; i++) { var faq = faqList[i]; var score = 0; for (var j = 0; j < faq.keywords.length; j++) { var keyword = faq.keywords[j].toLowerCase(); if (lowerMsg.indexOf(keyword) >= 0) { // 키워드 길이가 길수록 더 정확한 매칭 score += keyword.length; } } if (score > bestScore) { bestScore = score; bestMatch = faq; } } // 최소 점수 이상이어야 매칭으로 인정 if (bestScore >= 2) { return bestMatch; } return null; } // 에러 메시지 매칭 함수 (채팅에서 에러 키워드 감지) function findErrorMatches(msg) { var matches = []; var lowerMsg = msg.toLowerCase(); for (var i = 0; i < errorList.length; i++) { var err = errorList[i]; var errTitle = err.title.toLowerCase(); var errWords = errTitle.split(/\s+/); var matchCount = 0; for (var j = 0; j < errWords.length; j++) { if (errWords[j].length > 1 && lowerMsg.indexOf(errWords[j]) >= 0) { matchCount++; } } // 2개 이상 단어가 일치하면 매칭으로 간주 if (matchCount >= 2) { matches.push(err); } } return matches; } // ========== 이벤트 바인딩 ========== if (fab) { fab.onclick = function() { // 사이드 패널 강제 제거 (패널 닫을 때) var sidePanel = document.getElementById('ticket-side-panel'); if (sidePanel) { sidePanel.style.display = 'none'; sidePanel.remove(); } if (panel && panel.className.indexOf('active') >= 0) { closePanel(); } else { openPanel(); } }; } if (overlay) { overlay.onclick = function() { var sidePanel = document.getElementById('ticket-side-panel'); if (sidePanel) sidePanel.remove(); closePanel(); }; } if (closeBtn) { closeBtn.onclick = function() { var sidePanel = document.getElementById('ticket-side-panel'); if (sidePanel) sidePanel.remove(); closePanel(); }; } if (sendBtn) { sendBtn.onclick = sendText; } if (input) { input.onkeydown = function(e) { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); sendText(); hideAutocomplete(); } else if (e.key === 'Escape') { hideAutocomplete(); } }; // 스마트 자동완성 (한글 입력 지원) input.addEventListener('keyup', function(e) { // 한글 조합 중이면 무시 if (e.isComposing) return; var val = this.value.trim(); if (val.length >= 2) { showAutocomplete(val); showTypingPreview(val); // 실시간 타이핑 프리뷰 } else { hideAutocomplete(); hideTypingPreview(); } }); // 한글 입력 완료 시 input.addEventListener('compositionend', function() { var val = this.value.trim(); if (val.length >= 2) { showAutocomplete(val); showTypingPreview(val); // 실시간 타이핑 프리뷰 } else { hideAutocomplete(); hideTypingPreview(); } }); // 포커스 아웃 시 프리뷰 숨기기 input.addEventListener('blur', function() { setTimeout(hideTypingPreview, 200); }); } // ========== 실시간 타이핑 프리뷰 ========== var typingPreviewTimeout = null; function showTypingPreview(text) { if (!text || text.length < 2) { hideTypingPreview(); return; } // 디바운스 clearTimeout(typingPreviewTimeout); typingPreviewTimeout = setTimeout(function() { var preview = getPreviewAnswer(text); if (preview) { displayTypingPreview(preview); } else { hideTypingPreview(); } }, 300); } function getPreviewAnswer(text) { var lower = text.toLowerCase(); // 프리뷰 데이터 (짧은 미리보기 답변) var previews = [ { keywords: ['연결', '접속', '안됨', '안돼', '불가'], preview: '💡 VPN 연결 안됨? 원터치 해결 또는 단계별 가이드를 시도해보세요!' }, { keywords: ['느려', '느림', '속도'], preview: '🐢 속도 문제라면 네트워크 상태 확인 후 VPN 재연결을 권장드려요' }, { keywords: ['에러', '오류', 'error'], preview: '⚠️ 에러 발생 시 스크린샷을 드래그하시면 분석해드릴게요!' }, { keywords: ['설치', '다운', '받'], preview: '📥 VPN 설치파일은 다운로드 메뉴에서 받으실 수 있어요' }, { keywords: ['삭제', '제거', '언인스톨'], preview: '🗑️ VPN 삭제는 제어판 > 프로그램 제거에서 가능합니다' }, { keywords: ['주소', 'url', 'vpn.'], preview: '🌐 VPN 접속 주소: vpn.cef.or.kr' }, { keywords: ['비번', '비밀번호', '로그인', '아이디'], preview: '🔑 VPN 계정은 그룹웨어 ID와 동일합니다' }, { keywords: ['원격', '지원', '접수'], preview: '📋 원격 지원이 필요하시면 접수 메뉴를 이용해주세요' }, { keywords: ['담당', '연락처', '전화'], preview: '📞 인프라팀 VPN 담당으로 연락주세요' } ]; for (var i = 0; i < previews.length; i++) { for (var j = 0; j < previews[i].keywords.length; j++) { if (lower.indexOf(previews[i].keywords[j]) !== -1) { return previews[i].preview; } } } return null; } function displayTypingPreview(previewText) { var existing = document.getElementById('typing-preview'); if (!existing) { existing = document.createElement('div'); existing.id = 'typing-preview'; existing.className = 'typing-preview'; chat.appendChild(existing); } existing.innerHTML = '
💭 예상 답변
' + previewText + '
'; existing.classList.add('show'); chat.scrollTop = chat.scrollHeight; } function hideTypingPreview() { var existing = document.getElementById('typing-preview'); if (existing) { existing.classList.remove('show'); setTimeout(function() { if (existing && !existing.classList.contains('show')) { existing.remove(); } }, 300); } } // ========== 플로팅 도우미 핀 ========== function showHelperPin(targetSelector, message, position) { hideHelperPin(); // 기존 핀 제거 var target = document.querySelector(targetSelector); if (!target) return; var pin = document.createElement('div'); pin.id = 'helper-pin'; pin.className = 'helper-pin ' + (position || 'top'); pin.innerHTML = '
👆' + message + '
'; document.body.appendChild(pin); // 위치 계산 var rect = target.getBoundingClientRect(); var pinRect = pin.getBoundingClientRect(); if (position === 'bottom') { pin.style.top = (rect.bottom + 10) + 'px'; pin.style.left = (rect.left + rect.width / 2 - pinRect.width / 2) + 'px'; } else if (position === 'left') { pin.style.top = (rect.top + rect.height / 2 - pinRect.height / 2) + 'px'; pin.style.left = (rect.left - pinRect.width - 10) + 'px'; } else if (position === 'right') { pin.style.top = (rect.top + rect.height / 2 - pinRect.height / 2) + 'px'; pin.style.left = (rect.right + 10) + 'px'; } else { // top (default) pin.style.top = (rect.top - pinRect.height - 10) + 'px'; pin.style.left = (rect.left + rect.width / 2 - pinRect.width / 2) + 'px'; } setTimeout(function() { pin.classList.add('show'); }, 100); // 클릭하면 사라짐 pin.onclick = function() { hideHelperPin(); }; // 5초 후 자동 사라짐 setTimeout(function() { hideHelperPin(); }, 5000); return pin; } function hideHelperPin() { var existing = document.getElementById('helper-pin'); if (existing) { existing.classList.remove('show'); setTimeout(function() { if (existing.parentNode) existing.remove(); }, 300); } } // ========== 예상 해결 시간 표시 ========== function showEstimatedTime(minutes, taskName) { var row = document.createElement('div'); row.className = 'vpn-msg bot'; var avatar = document.createElement('div'); avatar.className = 'vpn-bot-avatar'; avatar.innerHTML = ''; row.appendChild(avatar); var bubble = document.createElement('div'); bubble.className = 'vpn-bubble'; var timeCard = document.createElement('div'); timeCard.className = 'estimated-time-card'; timeCard.innerHTML = '
' + '⏱️' + '예상 소요 시간' + '
' + '
' + '
' + (taskName || '이 작업') + '
' + '
' + '' + minutes + '' + '' + '
' + '
실제 시간은 환경에 따라 다를 수 있어요
' + '
' + '
' + '
' + '
'; bubble.appendChild(timeCard); row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 타이머 시작 startEstimatedTimer(minutes); return row; } var estimatedTimerInterval = null; function startEstimatedTimer(minutes) { var totalSeconds = minutes * 60; var elapsed = 0; clearInterval(estimatedTimerInterval); estimatedTimerInterval = setInterval(function() { elapsed++; var progress = (elapsed / totalSeconds) * 100; var bar = document.getElementById('etc-progress-bar'); if (bar) { bar.style.width = Math.min(progress, 100) + '%'; if (progress >= 100) { bar.style.background = 'linear-gradient(90deg, #10b981, #059669)'; clearInterval(estimatedTimerInterval); } } }, 1000); } // ========== 스마트 자동완성 시스템 ========== // 자동완성 데이터 var autocompleteData = [ // FAQ { type: 'faq', text: 'VPN 접속 주소가 뭔가요?', keywords: ['주소', 'url', '접속', '사이트'] }, { type: 'faq', text: '로그인/비밀번호를 모르겠어요', keywords: ['로그인', '비번', '비밀번호', '아이디', 'id'] }, { type: 'faq', text: '설치/삭제 방법이 궁금해요', keywords: ['설치', '삭제', '제거', '다운'] }, { type: 'faq', text: '맥/모바일에서 사용 가능한가요?', keywords: ['맥', 'mac', '모바일', '아이폰', '안드로이드'] }, { type: 'faq', text: '운영시간/담당자 연락처', keywords: ['운영', '시간', '담당', '연락처', '전화'] }, // 에러 메시지 TOP 5 { type: 'error', text: '🔥 연결 시간 초과 (Connection timed out)', keywords: ['시간', '초과', 'timeout', '연결'], hot: true }, { type: 'error', text: '🔥 인증 실패 (Authentication failed)', keywords: ['인증', '실패', 'auth', '비밀번호'], hot: true }, { type: 'error', text: '🔥 서버에 연결할 수 없음', keywords: ['서버', '연결', '불가'], hot: true }, { type: 'error', text: '🔥 네트워크 어댑터 오류', keywords: ['네트워크', '어댑터', '오류', 'adapter'], hot: true }, { type: 'error', text: '🔥 VPN 클라이언트가 응답하지 않음', keywords: ['응답', '클라이언트', '없음'], hot: true }, // 기타 에러 { type: 'error', text: 'DNS 확인 실패', keywords: ['dns', '확인', '실패'] }, { type: 'error', text: 'SSL 인증서 오류', keywords: ['ssl', '인증서', '오류'] }, { type: 'error', text: '원격 컴퓨터 연결 끊김', keywords: ['원격', '컴퓨터', '끊김', '끊어'] }, { type: 'error', text: '접속 프로그램이 실행되지 않음', keywords: ['프로그램', '실행', '안됨'] }, { type: 'error', text: '연결이 느리거나 끊겨요', keywords: ['느림', '끊김', '속도', '느려'] }, // 키워드 { type: 'action', text: '🔧 원터치 자동 해결', keywords: ['원터치', '자동', '해결', '복구'] }, { type: 'action', text: '📋 단계별 가이드', keywords: ['가이드', '단계', '순서'] }, { type: 'action', text: '📋 원격 지원 접수', keywords: ['원격', '지원', '접수', '신청'] } ]; function showAutocomplete(query) { var autocompleteEl = document.getElementById('vpn-autocomplete'); if (!autocompleteEl) return; var q = query.toLowerCase(); var results = []; for (var ri = 0; ri < autocompleteData.length; ri++) { var item = autocompleteData[ri]; var match = item.text.toLowerCase().indexOf(q) !== -1; if (!match && item.keywords) { for (var ki = 0; ki < item.keywords.length; ki++) { if (item.keywords[ki].indexOf(q) !== -1) { match = true; break; } } } if (match) results.push(item); if (results.length >= 6) break; } if (results.length === 0) { hideAutocomplete(); return; } var html = ''; for (var hi = 0; hi < results.length; hi++) { var item = results[hi]; var icon = item.type === 'faq' ? '❓' : item.type === 'error' ? '⚠️' : '⚡'; var typeLabel = item.type === 'faq' ? 'FAQ' : item.type === 'error' ? '에러' : '빠른실행'; var hotBadge = item.hot ? '🔥 TOP' : ''; var typeBadge = '' + typeLabel + ''; html += '
' + '' + icon + '' + '' + item.text + '' + (item.hot ? hotBadge : typeBadge) + '
'; } autocompleteEl.innerHTML = html; autocompleteEl.classList.remove('hidden'); // 클릭 이벤트 var items = autocompleteEl.querySelectorAll('.ac-item'); for (var i = 0; i < items.length; i++) { items[i].onclick = function() { var text = this.getAttribute('data-text'); var type = this.getAttribute('data-type'); input.value = ''; hideAutocomplete(); if (type === 'error') { addMsg('user', text); handleAction('error_msg'); } else if (type === 'action') { if (text.indexOf('원터치') !== -1) handleAction('onetouch_fix'); else if (text.indexOf('가이드') !== -1) handleAction('step_guide'); else if (text.indexOf('원격') !== -1) handleAction('ticket'); } else { addMsg('user', text); handleAction('faq'); } }; } } function hideAutocomplete() { var autocompleteEl = document.getElementById('vpn-autocomplete'); if (autocompleteEl) { autocompleteEl.classList.add('hidden'); autocompleteEl.innerHTML = ''; } } // ========== 스크린샷 드래그 앤 드롭 ========== function setupDragAndDrop() { var dropZone = chat; var dropOverlay = document.createElement('div'); dropOverlay.className = 'vpn-drop-overlay'; dropOverlay.innerHTML = '
📸
스크린샷을 놓아주세요
에러 화면을 분석해드릴게요
'; panel.appendChild(dropOverlay); // 드래그 이벤트 dropZone.addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); dropOverlay.classList.add('active'); }); dropZone.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); }); dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); if (e.target === dropOverlay || !dropZone.contains(e.relatedTarget)) { dropOverlay.classList.remove('active'); } }); dropOverlay.addEventListener('dragleave', function(e) { e.preventDefault(); dropOverlay.classList.remove('active'); }); dropZone.addEventListener('drop', handleDrop); dropOverlay.addEventListener('drop', handleDrop); function handleDrop(e) { e.preventDefault(); e.stopPropagation(); dropOverlay.classList.remove('active'); var files = e.dataTransfer.files; if (files.length > 0) { var file = files[0]; if (file.type.startsWith('image/')) { processDroppedImage(file); } else { showToast('이미지 파일만 가능합니다', 'warning'); } } } } function processDroppedImage(file) { var reader = new FileReader(); reader.onload = function(e) { // 사용자 메시지로 이미지 표시 var row = document.createElement('div'); row.className = 'vpn-msg user'; var bubble = document.createElement('div'); bubble.className = 'vpn-bubble image-bubble'; bubble.innerHTML = '스크린샷
📸 스크린샷 첨부
'; row.appendChild(bubble); chat.appendChild(row); chat.scrollTop = chat.scrollHeight; // 분석 중 표시 showTypingIndicator(); setTimeout(function() { hideTypingIndicator(); analyzeScreenshot(e.target.result); }, 1500); }; reader.readAsDataURL(file); } function analyzeScreenshot(imageData) { // 간단한 에러 패턴 분석 (실제로는 OCR/AI 사용) var errorPatterns = [ { pattern: 'BIG-IP', solution: 'VPN 서버 인증 문제입니다.\n\n로그인 정보 확인 또는 VPN 재설치를 권장드려요.', action: 'guide' }, { pattern: 'timeout', solution: '연결 시간 초과 문제입니다.\n\n네트워크 상태를 확인하고 원터치 해결을 시도해보세요.', action: 'onetouch_fix' }, { pattern: 'error', solution: '에러가 발생한 것 같네요.\n\n아래에서 해결 방법을 선택해주세요.', action: 'troubleshoot' } ]; addMsg('bot', '📸 스크린샷을 확인했어요!\n\n에러 화면으로 보이네요. 아래 방법을 시도해보세요:', [ { label: '⚡ 원터치 해결', action: 'onetouch_fix' }, { label: '📋 단계별 가이드', action: 'step_guide' }, { label: '🎫 원격 지원 접수', action: 'ticket' } ]); } // 드래그 앤 드롭 초기화 if (chat && panel) { setupDragAndDrop(); } // 추천 키워드 숨기기 function hideSuggestKeywords() { var suggestEl = document.getElementById('vpn-suggest-keywords'); if (suggestEl) { suggestEl.innerHTML = ''; suggestEl.style.display = 'none'; } } // 추천 키워드 표시 function showSuggestKeywords() { var suggestEl = document.getElementById('vpn-suggest-keywords'); if (!suggestEl) return; suggestEl.style.display = 'block'; var keywords = [ { text: '연결 안됨', icon: '🔌' }, { text: '느려요', icon: '🐢' }, { text: '에러 발생', icon: '⚠️' }, { text: '설치 방법', icon: '💿' }, { text: '원격 지원', icon: '🖥️' } ]; var html = '
추천 키워드
'; for (var wi = 0; wi < keywords.length; wi++) { var k = keywords[wi]; html += ''; } html += '
'; suggestEl.innerHTML = html; // 이벤트 위임 방식으로 변경 var listEl = document.getElementById('suggest-list'); if (listEl) { listEl.onclick = function(e) { var chip = e.target.closest('.suggest-chip'); if (chip) { e.preventDefault(); e.stopPropagation(); var keyword = chip.getAttribute('data-keyword'); input.value = keyword; showAutocomplete(keyword); // 포커스는 약간 지연 setTimeout(function() { input.focus(); }, 10); } }; } // 마지막 봇 메시지가 키워드 바로 위에 오도록 스크롤 setTimeout(function() { var lastBotMsg = chat.querySelectorAll('.vpn-msg.bot'); if (lastBotMsg.length > 0) { var lastMsg = lastBotMsg[lastBotMsg.length - 1]; var msgBottom = lastMsg.offsetTop + lastMsg.offsetHeight; var chatHeight = chat.clientHeight; var suggestHeight = suggestEl.offsetHeight || 80; // 메시지 하단이 키워드 바로 위에 오도록 chat.scrollTop = msgBottom - chatHeight + suggestHeight + 20; } }, 150); } // 채팅문의 시작 시 추천 키워드 표시 var originalStartFreeChatFlow = typeof startFreeChatFlow === 'function' ? startFreeChatFlow : null; // 메뉴바 이벤트 var menuItems = document.querySelectorAll('.vpn-menu-item'); for (var i = 0; i < menuItems.length; i++) { menuItems[i].onclick = function() { var action = this.getAttribute('data-action'); for (var j = 0; j < menuItems.length; j++) { menuItems[j].classList.remove('active'); } this.classList.add('active'); addMsg('user', this.querySelector('.menu-text').textContent); handleAction(action); }; } // 캘린더 이벤트 var calTabBtns = document.querySelectorAll('.vpn-cal-tab'); for (var i = 0; i < calTabBtns.length; i++) { calTabBtns[i].onclick = function() { state.calType = this.getAttribute('data-type'); for (var j = 0; j < calTabBtns.length; j++) { calTabBtns[j].className = 'vpn-cal-tab'; } this.className = 'vpn-cal-tab active'; calTitle.textContent = state.calType === 'access' ? '문제 발생 일시 선택' : '원격가능시간 선택'; state.calDate = ''; state.calTime = ''; renderCalendar(); hideTimeSection(); calApply.disabled = true; }; } calPrev.onclick = function() { state.calMonth--; if (state.calMonth < 0) { state.calMonth = 11; state.calYear--; } renderCalendar(); }; calNext.onclick = function() { state.calMonth++; if (state.calMonth > 11) { state.calMonth = 0; state.calYear++; } renderCalendar(); }; calCancel.onclick = function() { calModal.className = ''; }; calApply.onclick = applyCalendar; calModal.onclick = function(e) { if (e.target === calModal) { calModal.className = ''; } }; window.onkeydown = function(e) { if (e.key === 'Escape') { var sidePanel = document.getElementById('ticket-side-panel'); if (sidePanel) sidePanel.remove(); closePanel(); } }; // ========== 6. 접근성 개선 ========== function initAccessibility() { // 스크린리더 공지 영역 생성 var srAnnouncer = document.createElement('div'); srAnnouncer.id = 'vpn-sr-announcer'; srAnnouncer.setAttribute('aria-live', 'polite'); srAnnouncer.setAttribute('aria-atomic', 'true'); srAnnouncer.className = 'sr-only'; srAnnouncer.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;'; document.body.appendChild(srAnnouncer); // 스크린리더 공지 함수 window.announceToScreenReader = function(message) { var announcer = document.getElementById('vpn-sr-announcer'); if (announcer) { announcer.textContent = ''; setTimeout(function() { announcer.textContent = message; }, 100); } }; // FAB 버튼 접근성 if (fab) { fab.setAttribute('role', 'button'); fab.setAttribute('aria-label', 'VPN 도움말 챗봇 열기'); fab.setAttribute('aria-expanded', 'false'); fab.setAttribute('tabindex', '0'); fab.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPanel(); } }; } // 패널 접근성 if (panel) { panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', '오비서(Openbase By Service) 대화창'); panel.setAttribute('aria-modal', 'true'); panel.setAttribute('aria-describedby', 'vpn-chat'); } // 채팅 영역 접근성 if (chat) { chat.setAttribute('role', 'log'); chat.setAttribute('aria-label', '대화 내역'); chat.setAttribute('aria-live', 'polite'); } // 입력창 접근성 if (input) { input.setAttribute('aria-label', '메시지 입력'); input.setAttribute('role', 'textbox'); input.setAttribute('aria-autocomplete', 'list'); } // 전송 버튼 접근성 if (sendBtn) { sendBtn.setAttribute('aria-label', '메시지 전송'); sendBtn.setAttribute('type', 'button'); } // 닫기 버튼 접근성 if (closeBtn) { closeBtn.setAttribute('aria-label', '챗봇 닫기'); closeBtn.setAttribute('tabindex', '0'); closeBtn.setAttribute('type', 'button'); } // 메뉴 아이템 키보드 탐색 var menuItems = document.querySelectorAll('.vpn-menu-item'); for (var m = 0; m < menuItems.length; m++) { (function(index) { var item = menuItems[index]; item.setAttribute('tabindex', '0'); item.setAttribute('role', 'button'); item.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); } // 방향키로 메뉴 이동 if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); var next = menuItems[index + 1] || menuItems[0]; next.focus(); } if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); var prev = menuItems[index - 1] || menuItems[menuItems.length - 1]; prev.focus(); } }; })(m); } } // ========== 7. 봇 프로필 클릭 (도움말 팝업) ========== function initBotProfile() { var headerIcon = document.querySelector('.vpn-header-icon'); if (headerIcon) { headerIcon.style.cursor = 'pointer'; headerIcon.setAttribute('title', '클릭하면 도움말!'); headerIcon.setAttribute('tabindex', '0'); headerIcon.setAttribute('role', 'button'); headerIcon.setAttribute('aria-label', '도움말 보기'); // 봇 아이콘에 느낌표 뱃지 추가 var noticeBadge = document.createElement('div'); noticeBadge.className = 'bot-notice-badge'; noticeBadge.innerHTML = '!'; headerIcon.style.position = 'relative'; headerIcon.appendChild(noticeBadge); // 툴팁 생성 (body에 추가) var noticeTooltip = document.createElement('div'); noticeTooltip.id = 'notice-tooltip'; noticeTooltip.innerHTML = '✨ 새 소식이 있어요'; noticeTooltip.style.cssText = 'position:fixed;z-index:100001;opacity:0;pointer-events:none;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:white;font-size:12px;font-weight:600;padding:8px 14px;border-radius:16px;white-space:nowrap;box-shadow:0 4px 15px rgba(99,102,241,0.4);cursor:pointer;'; // 화살표 추가 (아래 가운데) var arrow = document.createElement('div'); arrow.style.cssText = 'position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #8b5cf6;'; noticeTooltip.appendChild(arrow); document.body.appendChild(noticeTooltip); // 툴팁 위치 업데이트 함수 function updateTooltipPosition() { var iconRect = headerIcon.getBoundingClientRect(); var tooltipRect = noticeTooltip.getBoundingClientRect(); var tooltipWidth = tooltipRect.width || 120; var tooltipHeight = tooltipRect.height || 32; // 봇 아이콘 바로 위, 중앙 정렬 (봇을 가리지 않도록) var top = iconRect.top - tooltipHeight - 10; // 봇 위 10px 간격 var left = iconRect.left + (iconRect.width / 2) - (tooltipWidth / 2); noticeTooltip.style.top = top + 'px'; noticeTooltip.style.left = left + 'px'; } // 패널이 열려있을 때 툴팁 표시 function showTooltipIfPanelOpen() { var panel = document.getElementById('vpn-panel'); if (!panel || !panel.classList.contains('active')) { setTimeout(showTooltipIfPanelOpen, 100); return; } // 위치 계산 후 표시 updateTooltipPosition(); noticeTooltip.style.opacity = '1'; noticeTooltip.style.pointerEvents = 'auto'; // 위치 지속 업데이트 var positionInterval = setInterval(function() { var p = document.getElementById('vpn-panel'); if (!p || !p.classList.contains('active') || noticeTooltip.style.opacity === '0') { noticeTooltip.style.opacity = '0'; noticeTooltip.style.pointerEvents = 'none'; clearInterval(positionInterval); return; } updateTooltipPosition(); }, 100); } // 3초 후 툴팁 표시 setTimeout(showTooltipIfPanelOpen, 3000); // 툴팁 클릭 시 공지사항 표시 noticeTooltip.onclick = function() { noticeTooltip.style.opacity = '0'; noticeTooltip.style.pointerEvents = 'none'; noticeBadge.style.display = 'none'; showNotice(); }; // 아이콘에 클릭 유도 애니메이션 추가 headerIcon.classList.add('clickable-hint'); // 클릭 시 효과 제거 및 공지사항 표시 function handleClick() { headerIcon.classList.remove('clickable-hint'); noticeBadge.style.display = 'none'; if (noticeTooltip) { noticeTooltip.style.opacity = '0'; noticeTooltip.style.pointerEvents = 'none'; } showNotice(); } headerIcon.onclick = handleClick; headerIcon.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }; } } function showNotice() { // 기존 팝업이 있으면 제거 var existing = document.getElementById('bot-help-popup'); if (existing) { existing.remove(); return; } var popup = document.createElement('div'); popup.id = 'bot-help-popup'; popup.className = 'bot-help-popup'; popup.innerHTML = '
' + '
' + '🎉' + '공지사항' + '' + '
' + '
' + '
' + '
안녕하세요! 👋
' + '
오비서(Openbase By Service) 서비스가
새롭게 오픈했습니다!
' + '
VPN 접속 문제, 이제 저에게 맡겨주세요 🤖
' + '
' + '
' + '
✨ 이런 것들을 도와드려요
' + '
    ' + '
  • 원터치 자동 해결 - 클릭 한번으로 VPN 문제 해결
  • ' + '
  • 에러 메시지 진단 - 에러별 맞춤 해결 가이드
  • ' + '
  • 원격 지원 접수 - 채팅으로 간편하게 접수
  • ' + '
' + '
' + '
' + '
'; panel.appendChild(popup); // 닫기 버튼 popup.querySelector('.bot-help-close').onclick = function() { popup.classList.add('hiding'); setTimeout(function() { popup.remove(); }, 200); }; // 바깥 클릭 시 닫기 popup.onclick = function(e) { if (e.target === popup) { popup.classList.add('hiding'); setTimeout(function() { popup.remove(); }, 200); } }; // 애니메이션 setTimeout(function() { popup.classList.add('show'); }, 10); } // Edge Client일 때: 기본 브라우저에서 열면 챗봇 사용 가능하다고 안내 function showEdgeClientNotice() { var fabEl = document.getElementById('vpn-fab'); if (!fabEl || document.getElementById('vpn-edge-client-notice')) return; var url = (typeof location !== 'undefined' && location.href) ? location.href : ''; var notice = document.createElement('div'); notice.id = 'vpn-edge-client-notice'; notice.setAttribute('role', 'status'); notice.style.cssText = 'position:fixed;bottom:90px;right:24px;z-index:9998;max-width:280px;padding:12px 14px;background:#1e293b;color:#e2e8f0;font-size:12px;line-height:1.5;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.25);border:1px solid rgba(255,255,255,0.08);'; notice.innerHTML = '
💡 챗봇 사용 안내
' + '
Edge Client에서는 챗봇이 제한될 수 있어요. Chrome·Edge 등 기본 브라우저에서 아래 주소를 열면 챗봇을 사용할 수 있습니다.
' + '
' + (url || '(현재 주소)') + '
' + ''; document.body.appendChild(notice); var closeBtn = document.getElementById('vpn-edge-notice-close'); if (closeBtn) { closeBtn.onclick = function() { if (notice.parentNode) notice.parentNode.removeChild(notice); }; } } // 초기화 setTimeout(function() { try { initAccessibility(); initBotProfile(); } catch (e) {} }, 100); })();