// main.js – index.html 전체 SPA 로직(jQuery 기반) // 핵심 기능: 도구 목록 로드, 채팅 페이지 렌더, 파일 사이드바, 세션/메시지 관리 등. // 코드가 600+ 라인으로 길기 때문에 함수별 세부 주석은 추후 리팩터링 시 모듈 분할과 함께 추가 권장. // 여기서는 상단 설명만 삽입한다. // ----------------------------------------------------------------------------- $(function() { // 도구 데이터 (서버에서 동적으로 가져옴) let tools = []; let favorites = JSON.parse(localStorage.getItem('favorites') || '[]'); // 현재 선택된 도구와 세션 let currentTool = null; let currentSessionId = null; // 도구별 채팅 메시지 저장소 { sessionId: messages[] } let toolChatMessages = {}; // 도구별 마지막 세션 ID를 기억해 도구를 다시 선택했을 때 복원 let lastSessionPerTool = {}; // 업로드 progress interval 저장소 {placeholderId: intervalId} let uploadIntervals = {}; // IME 조합 상태 플래그 let isComposing = false; let enterPressedWhileComposing = false; // IME 조합 시작/종료 이벤트 등록 $('.chat-input').on('compositionstart', function() { isComposing = true; }); $('.chat-input').on('compositionend', function(e) { isComposing = false; // 조합 중 Enter가 눌렸다면, 조합 끝난 후 전송 if (enterPressedWhileComposing) { enterPressedWhileComposing = false; sendChat(); } }); // 도구 목록 가져오기 async function loadTools() { try { const response = await fetch('http://localhost:8010/tools'); if (response.ok) { const toolsData = await response.json(); tools = toolsData.map(tool => { // 도구 유형(오픈AI / 내부AI)에 따라 카테고리 지정 const category = ['전체']; if (tool.id === 'dev_chatbot' || tool.id === 'chatgpt' || tool.id === 'doc_translation') { category.push('오픈AI'); } else if ([ 'chatbot_gxp', 'lims_text2sql', 'research_qa' ].includes(tool.id)) { category.push('내부AI'); } return { ...tool, category, id: tool.id }; }); renderTools('all'); // 도구 자동 선택 (최초 진입 시) if (tools.length > 0 && !currentTool) { selectTool(tools[0].id); } } else { console.error('도구 목록을 가져오는데 실패했습니다.'); } } catch (error) { console.error('도구 목록 로드 오류:', error); } } async function loadFileList() { try { const res = await fetch('http://localhost:8010/files'); const data = await res.json(); const $list = $('.file-list'); $list.empty(); data.files.forEach(f => { let display = f; const parts = f.split("_"); if(parts.length>1 && /^\d{17,}$/.test(parts[0])){ display = parts.slice(1).join("_"); } $list.append(`
  • ${display}
  • `); }); } catch(e) { console.error('파일 목록 로드 오류', e); } } // 문서번역 파일 리스트 로드 async function loadDocTranslationFileList() { try { const res = await fetch('http://localhost:8010/doc_translation/files'); const data = await res.json(); const $list = $('.file-list'); $list.empty(); // 파일 추가 버튼의 기능을 Word 파일 업로드로 변경 $('.file-add-btn').off('click').on('click', function() { const input = $(''); $('body').append(input); input.on('change', function() { uploadDocTranslationFiles(this.files); $(this).remove(); }); input.click(); }); data.forEach(file => { // 실제 파일명 추출 함수 function extractRealFilename(filename) { // 패턴: [doc_translation]_admin_YYYYMMDDHHMMSS[fff]_실제파일명 const regex = /^\[doc_translation\]_admin_\d{14,17}_(.+)$/; const match = filename.match(regex); return match ? match[1] : filename; } let displayName = extractRealFilename(file.filename); let listItem = ''; if (file.has_result) { // 결과 파일명도 정리 let resultDisplayName = extractRealFilename(file.result_filename); // 번역 완료된 파일 - 원본 파일명과 결과 파일명을 세로로 표시 listItem = `
  • ${displayName}
    → ${resultDisplayName}
  • `; } else { // 번역 중인 파일 - 파일명 숨김 listItem = `
  • 번역중...
  • `; } $list.append(listItem); }); } catch(e) { console.error('문서번역 파일 목록 로드 오류', e); } } // Word 파일 업로드 함수 async function uploadDocTranslationFiles(files) { if (!files || files.length === 0) return; // MS Word 파일 검증 for (let file of files) { if (!file.name.toLowerCase().endsWith('.doc') && !file.name.toLowerCase().endsWith('.docx')) { alert('MS Word 파일(.doc, .docx)만 업로드 가능합니다.'); return; } } const formData = new FormData(); for (let file of files) { formData.append('files', file); } try { // 업로드 시작 시 로딩 상태 표시 const $fileList = $('.file-list'); const loadingItem = $('
  • 업로드 및 번역 중...
  • '); $fileList.prepend(loadingItem); const response = await fetch('http://localhost:8010/doc_translation/upload_doc', { method: 'POST', body: formData }); const result = await response.json(); // 로딩 아이템 제거 loadingItem.remove(); if (result.status === 'success') { // 업로드와 번역이 동기적으로 완료됨 - 즉시 완료 상태로 표시 loadDocTranslationFileList(); } else { alert('파일 업로드에 실패했습니다: ' + (result.detail || '알 수 없는 오류')); } } catch (error) { // 로딩 아이템 제거 $('.uploading-item').remove(); console.error('파일 업로드 오류:', error); alert('파일 업로드 중 오류가 발생했습니다.'); } } // --- PDF 뷰어 로딩 오버레이 표시/숨김 유틸 --- function showPdfLoading() { const $slide = $('.file-slide'); let $overlay = $slide.find('.file-loading-overlay'); if ($overlay.length === 0) { $overlay = $('
    로딩중…
    '); $slide.append($overlay); } $overlay.show(); $('.file-viewer').hide(); } function hidePdfLoading() { $('.file-loading-overlay').hide(); $('.file-viewer').show(); } // 삭제 버튼 클릭 (동적으로 처리 - 도구에 따라 다른 엔드포인트 사용) $('.file-list').on('click', '.file-del-btn', async function(e) { e.stopPropagation(); const fname = $(this).data('fname'); // 문서번역의 경우 실제 파일명만 표시 let displayName = fname; if (currentTool && currentTool.id === 'doc_translation') { // 실제 파일명 추출 함수 function extractRealFilename(filename) { const regex = /^\[doc_translation\]_admin_\d{14,17}_(.+)$/; const match = filename.match(regex); return match ? match[1] : filename; } displayName = extractRealFilename(fname); } if(!confirm(displayName + ' 파일을 삭제하시겠습니까?')) return; try { let deleteUrl; if (currentTool && currentTool.id === 'doc_translation') { deleteUrl = `http://localhost:8010/doc_translation/files/${encodeURIComponent(fname)}`; } else { deleteUrl = `http://localhost:8010/file?filename=${encodeURIComponent(fname)}`; } const res = await fetch(deleteUrl, { method: 'DELETE' }); const data = await res.json(); if(data.status==='success') { if (currentTool && currentTool.id === 'doc_translation') { loadDocTranslationFileList(); } else { loadFileList(); } // 슬라이드가 해당 파일이라면 닫기 if($('.file-slide.show').length && $('.file-slide-content').text().includes(fname)) { $('.file-slide').removeClass('show'); } } else { alert('삭제 실패: '+data.message); } } catch(err){ alert('삭제 오류: '+err); } }); // 문서번역 원본 파일 다운로드 링크 클릭 $('.file-list').on('click', '.download-original-link', async function(e) { e.preventDefault(); e.stopPropagation(); const originalFilename = $(this).data('original-fname'); // 실제 파일명 추출 함수 function extractRealFilename(filename) { const regex = /^\[doc_translation\]_admin_\d{14,17}_(.+)$/; const match = filename.match(regex); return match ? match[1] : filename; } try { const response = await fetch(`http://localhost:8010/doc_translation/download/${encodeURIComponent(originalFilename)}`); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = extractRealFilename(originalFilename); // 실제 파일명으로 다운로드 document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } else { alert('원본 파일 다운로드에 실패했습니다.'); } } catch (error) { console.error('원본 파일 다운로드 오류:', error); alert('원본 파일 다운로드 중 오류가 발생했습니다.'); } }); // 문서번역 결과 다운로드 링크 클릭 $('.file-list').on('click', '.download-result-link', async function(e) { e.preventDefault(); e.stopPropagation(); const resultFilename = $(this).data('result-fname'); // 실제 파일명 추출 함수 function extractRealFilename(filename) { const regex = /^\[doc_translation\]_admin_\d{14,17}_(.+)$/; const match = filename.match(regex); return match ? match[1] : filename; } try { const response = await fetch(`http://localhost:8010/doc_translation/download/${encodeURIComponent(resultFilename)}`); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = extractRealFilename(resultFilename); // 실제 파일명으로 다운로드 document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } else { alert('결과 파일 다운로드에 실패했습니다.'); } } catch (error) { console.error('결과 파일 다운로드 오류:', error); alert('결과 파일 다운로드 중 오류가 발생했습니다.'); } }); // 파일 클릭 -> 슬라이드 열기 (파일 내용 확인 요청은 실패해도 뷰어 열기) $('.file-list').off('click', 'li').on('click', 'li', async function() { const fname = $(this).data('fname'); const url = `http://localhost:8010/pdf?filename=${encodeURIComponent(fname)}`; const $viewer = $('.file-viewer'); // 로딩 오버레이 표시 showPdfLoading(); // 로드 완료 시 오버레이 숨김 $viewer.off('load').on('load', function() { hidePdfLoading(); }); // PDF 로드 시작 $viewer.attr('src', url); $('.file-slide').css('display','block').addClass('show'); // 백그라운드로 파일 존재 여부를 확인하여 오류시 알림 try { const res = await fetch(`http://localhost:8010/file_content?filename=${encodeURIComponent(fname)}`); const data = await res.json(); if (data.status !== 'success') { console.warn('파일 내용 조회 실패:', data.message); } } catch(err) { console.error('파일 내용 조회 오류:', err); } }); // 닫기 버튼 $('.file-slide').on('click', '.file-slide-close', function() { $('.file-slide').removeClass('show').css('display','none'); }); // 슬라이드 종료(배경 클릭) $('.file-slide').click(function(e) { if (e.target === this) { $(this).removeClass('show').css('display','none'); } }); // 출처 링크 클릭 시: 기존 뷰어가 열려 있으면 닫았다가 새로 열어 원하는 페이지로 이동 $('.chat-center').off('click', '.ref-link').on('click', '.ref-link', function(e){ e.preventDefault(); const fname = $(this).data('fname'); const page = $(this).data('page'); const url = `http://localhost:8010/pdf?filename=${encodeURIComponent(fname)}#page=${page}`; const $slide = $('.file-slide'); const $viewer = $('.file-viewer'); // 슬라이드가 이미 열려 있으면 우선 닫기 (애니메이션 제거 목적) if ($slide.hasClass('show')) { $slide.removeClass('show').css('display', 'none'); } // 로딩 오버레이 표시 showPdfLoading(); // 먼저 blank 로드로 강제 새로고침 $viewer.off('load'); $viewer.attr('src', 'about:blank'); // PDF 로드 시작 (약간의 딜레이 후 실행) setTimeout(() => { $viewer.off('load').on('load', function() { hidePdfLoading(); }); $viewer.attr('src', url); $slide.css('display', 'block').addClass('show'); }, 50); }); // 채팅 메시지 렌더링 함수 (loading 타입 처리) function renderChatMessages(autoScroll=false) { const $chatCenter = $('.chat-center'); $chatCenter.empty(); if (!currentTool) { $chatCenter.append(`
    엔큐톡
    도구를 선택하여 채팅을 시작하세요
    `); return; } // iframe을 사용하는 도구 매핑 const iframeTools = { 'research_qa': 'http://yongin-qa-chatbot.daewoongai.com/', 'lims_text2sql': 'http://3.38.184.255:8080/' }; if (iframeTools[currentTool.id]) { $('.chat-input-wrapper').hide(); $chatCenter.addClass('iframe-mode'); $chatCenter.append(``); return; } else { $('.chat-input-wrapper').show(); $chatCenter.removeClass('iframe-mode'); } $chatCenter.append(`
    ${currentTool.name}
    ${currentTool.description}
    `); // 현재 세션 메시지 출력 const sessionKeyToShow = getMsgKey(currentTool.id, currentSessionId); if (toolChatMessages[currentTool.id] && toolChatMessages[currentTool.id][sessionKeyToShow]) { let messages = toolChatMessages[currentTool.id][sessionKeyToShow]; messages.forEach(msg => { let messageClass = msg.type === 'user' ? 'user-message' : (msg.type === 'bot' ? 'bot-message' : (msg.type === 'progress' ? 'bot-message progress' : 'bot-message loading')); let contentHtml = msg.content // 구백엔드 형식: 파일.pdf (p1) .replace(/([^\s]+\.pdf) \(p(\d+)\)/g, '$1 (p$2)') // 신백엔드 형식: [p1](/pdf?filename=파일.pdf#page=1) .replace(/\[p(\d+)\]\(\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pageNum, fnameEnc, pageNum2){ const fname = decodeURIComponent(fnameEnc); return `p${pageNum}`; }) // gxp-chat 경로 링크 .replace(/\[p(\d+)\]\(\/gxp-chat\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pg, fnameEnc){ const fname = decodeURIComponent(fnameEnc); return `p${pg}`; }) contentHtml = contentHtml.replace(/\n/g, "
    "); if (msg.type === 'progress') { const label = msg.content || '생각중'; contentHtml = `${label}
    `; } else if (msg.type === 'loading') { contentHtml = `${msg.content}`; } let timeDisplay = msg.time; if (msg.type === 'loading') { const elapsed = Math.floor((Date.now() - (msg.start||Date.now()))/1000); timeDisplay = elapsed + 's'; } $chatCenter.append(`
    ${contentHtml}
    ${timeDisplay}
    `); }); } else if (toolChatMessages[currentTool.id] && toolChatMessages[currentTool.id][getMsgKey(currentTool.id,null)]) { let messages = toolChatMessages[currentTool.id][getMsgKey(currentTool.id,null)]; messages.forEach(msg => { let messageClass = msg.type === 'user' ? 'user-message' : (msg.type === 'bot' ? 'bot-message' : (msg.type === 'progress' ? 'bot-message progress' : 'bot-message loading')); let contentHtml = msg.content // 구백엔드 형식: 파일.pdf (p1) .replace(/([^\s]+\.pdf) \(p(\d+)\)/g, '$1 (p$2)') // 신백엔드 형식: [p1](/pdf?filename=파일.pdf#page=1) .replace(/\[p(\d+)\]\(\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pageNum, fnameEnc, pageNum2){ const fname = decodeURIComponent(fnameEnc); return `p${pageNum}`; }) // gxp-chat 경로 링크 .replace(/\[p(\d+)\]\(\/gxp-chat\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pg, fnameEnc){ const fname = decodeURIComponent(fnameEnc); return `p${pg}`; }) contentHtml = contentHtml.replace(/\n/g, "
    "); if (msg.type === 'progress') { const label = msg.content || '생각중'; contentHtml = `${label}
    `; } else if (msg.type === 'loading') { contentHtml = `${msg.content}`; } let timeDisplay = msg.time; if (msg.type === 'loading') { const elapsed = Math.floor((Date.now() - (msg.start||Date.now()))/1000); timeDisplay = elapsed + 's'; } $chatCenter.append(`
    ${contentHtml}
    ${timeDisplay}
    `); }); } // after messages appended if(autoScroll){ $chatCenter.scrollTop($chatCenter[0].scrollHeight); } } // Helper: send message to backend and get response async function sendMessageToAI(toolId, sessionId, message, model, knowledgeMode){ const url = toolId === 'chatgpt' ? 'http://localhost:8010/chatgpt/chat' : 'http://localhost:8010/chat'; const formData = new FormData(); formData.append('message', message); formData.append('tool_id', toolId); if(sessionId) formData.append('session_id', sessionId); formData.append('model', model); if(toolId === 'dev_chatbot'){ formData.append('knowledge_mode', knowledgeMode); } const res = await fetch(url,{method:'POST',body:formData}); if(!res.ok) throw new Error('HTTP '+res.status); return await res.json(); } // Modify sendChat implementation function sendChat(){ const message = $('.chat-input').text().trim(); if(!message) return; const now = new Date().toISOString(); const sessionId = currentSessionId || 'temp-session'; const toolId = currentTool.id; const msgKey = getMsgKey(toolId, sessionId); if(!toolChatMessages[toolId]) toolChatMessages[toolId] = {}; if(!toolChatMessages[toolId][msgKey]) toolChatMessages[toolId][msgKey]=[]; toolChatMessages[toolId][msgKey].push({type:'user',content:message,time:now}); const loadingMsg = {type:'loading',content:'생각중',start:Date.now()}; toolChatMessages[toolId][msgKey].push(loadingMsg); // 주기적으로 경과 시간 업데이트 const loadingInterval = setInterval(()=>{ // 종료 조건: 로딩 메시지가 더 이상 마지막이 아닐 때 const msgs = toolChatMessages[toolId][msgKey]; if(!msgs || msgs[msgs.length-1] !== loadingMsg){ clearInterval(loadingInterval); return; } renderChatMessages(true); },1000); $('.chat-input').text(''); renderChatMessages(true); // Determine model & knowledgeMode let model='gpt-oss:latest'; if(currentTool.id==='chatgpt'){ model = $('.model-select').val()||'auto'; } else if(currentTool.id==='dev_chatbot'){ model = $('.gc-model-select').val()||'woong'; } else if(currentTool.id==='doc_translation'){ model = $('.dt-model-select').val()||'external'; } const kMode = $('.knowledge-mode').val()||'hybrid'; sendMessageToAI(currentTool.id, currentSessionId, message, model, kMode) .then(resp=>{ // replace last loading message const msgs = toolChatMessages[toolId][msgKey]; msgs[msgs.length-1] = {type:'bot',content:resp.response,time:new Date().toISOString()}; // update session id if(!currentSessionId && resp.session_id){ currentSessionId = resp.session_id; lastSessionPerTool[currentTool.id]=resp.session_id; const newKey = getMsgKey(toolId, resp.session_id); toolChatMessages[toolId][newKey]=msgs; delete toolChatMessages['temp']; // Remove temp session from toolChatMessages delete toolChatMessages['temp-session']; // Remove temp session from toolChatMessages } renderChatMessages(true); }) .catch(err=>{ const msgs=toolChatMessages[toolId][msgKey]; msgs[msgs.length-1]={type:'bot',content:'서버 오류: '+err.message,time:new Date().toISOString()}; renderChatMessages(true); }); } // 채팅 입력 핸들러 (Enter 전송, Shift+Enter 줄바꿈) $('.chat-input').on('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey && !isComposing) { e.preventDefault(); sendChat(); } }); // 채팅 전송 버튼 클릭 핸들러 $('.send-chat').on('click', function() { sendChat(); }); // 세션 변경 핸들러 $('.session-selector').on('change', function() { const selectedSessionId = $(this).val(); if (selectedSessionId !== currentSessionId) { currentSessionId = selectedSessionId; renderChatMessages(); } }); // 도구 선택 핸들러 function selectTool(toolId) { currentTool = tools.find(tool => tool.id === toolId); if (!currentTool) return; // 페이지 전환: 채팅 페이지 활성화 $('.menu-item').removeClass('active'); $('.menu-item[data-page="chat"]').addClass('active'); $('.page').removeClass('active'); $('.page-chat').addClass('active'); $('.page-title').text('채팅'); // 세션 리셋 또는 기존 세션 복원 currentSessionId = lastSessionPerTool[toolId] || null; // 모델/지식모드 드롭다운 표시 제어 if (toolId === 'chatgpt') { $('.model-select').show(); $('.knowledge-mode').hide(); $('.gc-model-select').hide(); $('.dt-model-select').hide(); // 파일 사이드바 숨김 (개발챗봇 전용) $('.file-sidebar').hide(); $('.page-chat').removeClass('has-file-sidebar'); } else if (toolId === 'dev_chatbot') { $('.model-select').hide(); $('.knowledge-mode').show(); $('.gc-model-select').show(); $('.dt-model-select').hide(); // 개발챗봇에서만 PDF 파일 사이드바 표시 $('.file-sidebar').show(); $('.page-chat').addClass('has-file-sidebar'); loadFileList(); } else if (toolId === 'doc_translation') { $('.model-select').hide(); $('.knowledge-mode').hide(); $('.gc-model-select').hide(); $('.dt-model-select').show(); // 문서번역에서도 파일 사이드바 표시 (Word 파일용) $('.file-sidebar').show(); $('.page-chat').addClass('has-file-sidebar'); loadDocTranslationFileList(); } else { $('.model-select').hide(); $('.knowledge-mode').hide(); $('.gc-model-select').hide(); $('.dt-model-select').hide(); // 다른 도구 선택 시 파일 사이드바 숨김 $('.file-sidebar').hide(); $('.page-chat').removeClass('has-file-sidebar'); } renderChatMessages(); } // ---------- 도구 목록 렌더링 및 이벤트 ---------- function renderTools(filter) { let list = tools; if (filter && filter !== 'all') { if (filter === 'favorite') { list = tools.filter(t => favorites.includes(t.id)); } else { list = tools.filter(t => t.category && t.category.includes(filter)); } } const $list = $('.tools-list'); $list.empty(); if (list.length === 0) { $list.append('
    도구가 없습니다.
    '); return; } list.forEach(tool => { const fav = favorites.includes(tool.id) ? 'active' : ''; $list.append(`
    ${tool.name}
    ${tool.description}
    `); }); } // 필터 버튼 클릭 -> 목록 갱신 $(document).on('click', '.filter-btn', function() { $('.filter-btn').removeClass('active'); $(this).addClass('active'); const filter = $(this).data('filter'); renderTools(filter); }); // 도구 카드 클릭 이벤트 (즐겨찾기 별 클릭 제외) $(document).on('click', '.tool-card', function(e) { if (!$(e.target).hasClass('tool-fav')) { const toolId = $(this).data('tool-id'); selectTool(toolId); } }); // 즐겨찾기 별 토글 $(document).on('click', '.tool-fav', function(e) { e.stopPropagation(); const id = $(this).data('id'); if (favorites.includes(id)) { favorites = favorites.filter(f => f !== id); } else { favorites.push(id); } localStorage.setItem('favorites', JSON.stringify(favorites)); const filter = $('.filter-btn.active').data('filter') || 'all'; renderTools(filter); }); // ---------- 도구 목록 End ---------- // ---------- 파일 업로드 UI ---------- // 파일 리스트 헤더 '+' 버튼 클릭 → 파일 선택창 오픈 $('.file-add-btn').on('click', function(){ $('.chat-file-input').click(); }); // 파일 선택 후 업로드 요청 $('.chat-file-input').on('change', async function(){ const files = this.files; if(!files || files.length===0) return; const formData = new FormData(); Array.from(files).forEach(f => formData.append('files', f)); // ---- dev_chatbot: progress 메시지 표시 ---- let placeholderIds = []; if(currentTool && currentTool.id === 'dev_chatbot'){ const $list = $('.file-list'); Array.from(files).forEach(f=>{ const pid = 'upload-'+Date.now()+Math.random().toString(36).slice(2,8); placeholderIds.push(pid); $list.append(`
  • 공부중...
  • `); let dotCnt = 3; // start with visible dots uploadIntervals[pid] = setInterval(()=>{ dotCnt = (dotCnt + 1) % 4; const dots = '.'.repeat(dotCnt); $(`li.uploading[data-temp="${pid}"] .uploading-dots`).text(dots); }, 400); }); } try { const res = await fetch('http://localhost:8010/upload_pdf', { method: 'POST', body: formData }); const data = await res.json(); // 업로드 결과 처리 if(currentTool && currentTool.id === 'dev_chatbot'){ if(data.status==='success'){ const uploaded = data.files || []; // 성공한 파일 대체 uploaded.forEach((fname, idx)=>{ const pid = placeholderIds[idx]; const $li = $(`li.uploading[data-temp="${pid}"]`); if($li.length){ clearInterval(uploadIntervals[pid]); delete uploadIntervals[pid]; $li.removeClass('uploading'); $li.removeAttr('data-temp'); $li.html(`${fname}`); } }); // 업로드되지 않은 placeholder 정리 if(uploaded.length < placeholderIds.length){ placeholderIds.slice(uploaded.length).forEach(pid=>{ clearInterval(uploadIntervals[pid]); delete uploadIntervals[pid]; $(`li.uploading[data-temp="${pid}"]`).remove(); }); if(uploaded.length===0){ alert('업로드할 PDF가 없습니다.'); } } // 최종 파일 리스트 새로고침 loadFileList(); } else { // 실패 시 placeholder 제거 placeholderIds.forEach(pid=>{ clearInterval(uploadIntervals[pid]); delete uploadIntervals[pid]; $(`li.uploading[data-temp="${pid}"]`).remove(); }); alert('업로드 실패: '+ (data.message||'알 수 없는 오류')); } } else { // dev_chatbot 아닐 때는 알림 if(data.status==='success'){ alert('파일 업로드가 완료되었습니다.'); } else { alert('업로드 실패: '+ (data.message||'알 수 없는 오류')); } } if(data.status==='success'){ loadFileList(); } } catch(err){ if(currentTool && currentTool.id === 'dev_chatbot'){ // 오류 시 placeholder 제거 및 경고 placeholderIds.forEach(pid=>{ clearInterval(uploadIntervals[pid]); delete uploadIntervals[pid]; $(`li.uploading[data-temp="${pid}"]`).remove(); }); alert('업로드 오류: ' + err.message); } else { alert('업로드 오류: ' + err.message); } } finally { // 같은 파일을 다시 업로드할 수 있도록 값 초기화 $(this).val(''); } }); // ---------- 파일 업로드 End ---------- // ----- 강의 페이지 렌더링 ----- function renderLecturePage(){ const $page = $('.page-lecture'); if($page.length===0) return; $page.empty(); const videos=[ "https://www.youtube.com/watch?v=GlvOHXJT_gI&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=14&t=26s", "https://www.youtube.com/watch?v=nl34M5bKkVM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=12", "https://www.youtube.com/watch?v=0sRlrW_UyLk&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=11", "https://www.youtube.com/watch?v=MtTAprzHOBg&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=10", "https://www.youtube.com/watch?v=CyWcZWVwCjQ&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=9", "https://www.youtube.com/watch?v=X7ycln4JREM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=8", "https://www.youtube.com/watch?v=rrCmsOFt2UU&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=7", "https://www.youtube.com/watch?v=7cyDMjzcdxM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=6", "https://www.youtube.com/watch?v=Sr4MEivnt4M&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=5", "https://www.youtube.com/watch?v=F4ExQ3P_A5w&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=4", "https://www.youtube.com/watch?v=tiC5k9P93cE&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=3", "https://www.youtube.com/watch?v=nfPXfsVz6jM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=2", "https://www.youtube.com/watch?v=9NH3FhBX2ng&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=1" ]; const videoHtml1=videos.map(url=>{ const id=url.match(/v=([\w-]{11})/); const vid=id? id[1]:''; const thumb=vid?`https://img.youtube.com/vi/${vid}/hqdefault.jpg`:''; return `video thumbnail`; }).join(''); // second list const videos2=[ "https://www.youtube.com/watch?v=menXWx89QFg&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=14", "https://www.youtube.com/watch?v=6IsYJy3ussQ&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=13", "https://www.youtube.com/watch?v=Mpk4LNZ_P4c&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=12", "https://www.youtube.com/watch?v=r2UMvkwJTRc&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=11", "https://www.youtube.com/watch?v=4uc58O4poJE&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=10", "https://www.youtube.com/watch?v=njAu-W-JA6c&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=9", "https://www.youtube.com/watch?v=SaYxoKVYgsk&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=8", "https://www.youtube.com/watch?v=sDDW5w2jD2Q&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=7", "https://www.youtube.com/watch?v=Ext47QeBCbY&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=6", "https://www.youtube.com/watch?v=og0mGD29Hw8&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=5", "https://www.youtube.com/watch?v=GkZgTth3guQ&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=4", "https://www.youtube.com/watch?v=oSZBGZmaTTw&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=3", "https://www.youtube.com/watch?v=t6Xc1Ey5PMY&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=2", "https://www.youtube.com/watch?v=aFw4Z9F4Ens&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=1" ]; const videoHtml2=videos2.map(url=>{ const id=url.match(/v=([\w-]{11})/); const vid=id? id[1]:''; const thumb=vid?`https://img.youtube.com/vi/${vid}/hqdefault.jpg`:''; return `video thumbnail`; }).join(''); // third list const videos3=[ "https://www.youtube.com/watch?v=kEAV-PqWD_4", "https://www.youtube.com/watch?v=ha9kn0qe4Mc", "https://www.youtube.com/watch?v=ISrYHGg2C2c", "https://www.youtube.com/watch?v=cxOoV2guNQQ", "https://www.youtube.com/watch?v=oAxunD8k0C8", "https://www.youtube.com/watch?v=oZ1O6Z9HwW0", "https://www.youtube.com/watch?v=nyZnrKVaIXU", "https://www.youtube.com/watch?v=BzwhskWZ-CQ" ]; const videoHtml3=videos3.map(url=>{ const id=url.match(/v=([\w-]{11})/); const vid=id? id[1]:''; const thumb=vid?`https://img.youtube.com/vi/${vid}/hqdefault.jpg`:''; return `video thumbnail`; }).join(''); $page.append(`

    강수진박사의 프롬프트 엔지니어링의 세계

    ${videoHtml1}

    AI, 안해보면 모른다

    ${videoHtml2}

    MCP를 배워보자

    ${videoHtml3}
    `); } // ---------- 사이드바 메뉴 전환 ---------- $(document).on('click', '.menu-item', function(){ $('.menu-item').removeClass('active'); $(this).addClass('active'); const page = $(this).data('page'); $('.page').removeClass('active'); $('.page-' + page).addClass('active'); $('.page-title').text($(this).text()); // 채팅 페이지로 돌아올 때 현재 도구 상태 유지해서 메시지 다시 렌더링 if(page === 'chat') { renderChatMessages(); } if(page === 'community') { $('.community-menu-item').removeClass('active'); $('.community-menu-item[data-content="newsletter"]').addClass('active'); showCommunityContent('newsletter'); } if(page === 'lecture') { renderLecturePage(); } }); // ---------- 메뉴 전환 End ---------- // ---------- 커뮤니티 서브 메뉴 ---------- function showCommunityContent(type){ const $content = $('.community-content'); if($content.length===0) return; $content.empty(); if(type === 'newsletter'){ $content.append(`

    뉴스레터

    `); } else if(type === 'lecture'){ // 재사용을 위해 내부 함수 function toThumb(url){ const id=url.match(/v=([\w-]{11})/); const vid=id? id[1]:''; return vid?`https://img.youtube.com/vi/${vid}/hqdefault.jpg`:''; } const sec1=[ "https://www.youtube.com/watch?v=GlvOHXJT_gI&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=14&t=26s", "https://www.youtube.com/watch?v=nl34M5bKkVM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=12", "https://www.youtube.com/watch?v=0sRlrW_UyLk&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=11", "https://www.youtube.com/watch?v=MtTAprzHOBg&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=10", "https://www.youtube.com/watch?v=CyWcZWVwCjQ&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=9", "https://www.youtube.com/watch?v=X7ycln4JREM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=8", "https://www.youtube.com/watch?v=rrCmsOFt2UU&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=7", "https://www.youtube.com/watch?v=7cyDMjzcdxM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=6", "https://www.youtube.com/watch?v=Sr4MEivnt4M&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=5", "https://www.youtube.com/watch?v=F4ExQ3P_A5w&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=4", "https://www.youtube.com/watch?v=tiC5k9P93cE&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=3", "https://www.youtube.com/watch?v=nfPXfsVz6jM&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=2", "https://www.youtube.com/watch?v=9NH3FhBX2ng&list=PL7d4-rFjtYdK4-RGBJTXbgLI5a-H7lz7g&index=1" ]; const sec2=[ "https://www.youtube.com/watch?v=menXWx89QFg&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=14", "https://www.youtube.com/watch?v=6IsYJy3ussQ&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=13", "https://www.youtube.com/watch?v=Mpk4LNZ_P4c&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=12", "https://www.youtube.com/watch?v=r2UMvkwJTRc&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=11", "https://www.youtube.com/watch?v=4uc58O4poJE&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=10", "https://www.youtube.com/watch?v=njAu-W-JA6c&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=9", "https://www.youtube.com/watch?v=SaYxoKVYgsk&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=8", "https://www.youtube.com/watch?v=sDDW5w2jD2Q&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=7", "https://www.youtube.com/watch?v=Ext47QeBCbY&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=6", "https://www.youtube.com/watch?v=og0mGD29Hw8&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=5", "https://www.youtube.com/watch?v=GkZgTth3guQ&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=4", "https://www.youtube.com/watch?v=oSZBGZmaTTw&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=3", "https://www.youtube.com/watch?v=t6Xc1Ey5PMY&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=2", "https://www.youtube.com/watch?v=aFw4Z9F4Ens&list=PL7d4-rFjtYdK5d2KOXyyaBymJYtEDRmy3&index=1" ]; const sec3=[ "https://www.youtube.com/watch?v=kEAV-PqWD_4", "https://www.youtube.com/watch?v=ha9kn0qe4Mc", "https://www.youtube.com/watch?v=ISrYHGg2C2c", "https://www.youtube.com/watch?v=cxOoV2guNQQ", "https://www.youtube.com/watch?v=oAxunD8k0C8", "https://www.youtube.com/watch?v=oZ1O6Z9HwW0", "https://www.youtube.com/watch?v=nyZnrKVaIXU", "https://www.youtube.com/watch?v=BzwhskWZ-CQ" ]; const makeHtml=list=>list.map(url=>{ const thumb=toThumb(url); return `video thumbnail`; }).join(''); $content.append(`

    강수진박사의 프롬프트 엔지니어링의 세계

    ${makeHtml(sec1)}

    AI, 안해보면 모른다

    ${makeHtml(sec2)}

    MCP를 배워보자

    ${makeHtml(sec3)}
    `); } else if(type === 'qna'){ const rows=[ {no:1,title:'AI 모델 선택은 어떻게?',date:'2025-07-21',views:8}, {no:2,title:'PDF 뷰어 오류 해결',date:'2025-07-20',views:21}, {no:3,title:'질의 응답 게시판이 생성되었습니다.',date:'2025-07-19',views:12} ]; const rowHtml=rows.map(r=>`${r.no}${r.title}${r.date}${r.views}`).join(''); $content.append(`

    질의응답

    ${rowHtml}
    번호제목등록일조회수
    `); } } // PDF 뷰 닫기 (X 버튼) $(document).on('click', '.newsletter-close', function(){ // 복원 리스트 영역 const $area = $('.newsletter-content-area'); if($area.length){ $area.html(` `); } }); // 뉴스레터 항목 클릭 → PDF 보기 $(document).on('click', '.newsletter-link', function(e){ e.preventDefault(); const fname = $(this).data('file'); const encoded = encodeURIComponent(fname); const url = `community/${encoded}`; const $area = $('.newsletter-content-area'); if(!$area.length) return; $area.html(`
    `); }); // 서브 메뉴 클릭 (뉴스레터/질의응답 등) $(document).on('click', '.community-menu-item', function(){ $('.community-menu-item').removeClass('active'); $(this).addClass('active'); const type = $(this).data('content'); showCommunityContent(type); }); // ---------- 커뮤니티 서브 메뉴 End ---------- // ===== Helper ===== function getMsgKey(toolId, sessionId){ return sessionId && sessionId!=='temp-session' ? `${toolId}__${sessionId}` : `temp__${toolId}`; } // ------------------ // 초기 로딩 loadTools(); loadFileList(); renderChatMessages(); });