' +
'' +
'⬇️ 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 += '
';
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 =
'' +
'
' +
'
' +
'📧 메일 미리보기' +
'' +
'
' +
'
' +
'
받는 사람: tsc@openbase.co.kr
' +
'
제목: [VPN] 원격 지원 접수
' +
'
' + emailBody + '
' +
'
' +
'' +
'
';
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 += '