Files
ncuetalk_backend/main.js
dsyoon 46460b77f8 init
2025-12-27 14:06:26 +09:00

1081 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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(`<li data-fname="${f}" title="${display}"><span>${display}</span><button class="file-del-btn" data-fname="${f}">✕</button></li>`);
});
} 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 = $('<input type="file" accept=".doc,.docx" multiple style="display:none;">');
$('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 = `
<li class="translation-result-item" data-fname="${file.filename}" title="${displayName}">
<div class="file-item-content">
<div class="original-file">${displayName}</div>
<div class="result-file"> → ${resultDisplayName}</div>
<div class="file-actions">
<a href="#" class="download-original-link" data-original-fname="${file.filename}">[원본다운]</a>
<a href="#" class="download-result-link" data-result-fname="${file.result_filename}">[결과다운]</a>
<button class="file-del-btn" data-fname="${file.filename}">✕</button>
</div>
</div>
</li>
`;
} else {
// 번역 중인 파일 - 파일명 숨김
listItem = `
<li data-fname="${file.filename}" title="번역중...">
<span class="translating">번역중...</span>
<button class="file-del-btn" data-fname="${file.filename}">✕</button>
</li>
`;
}
$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 = $('<li class="uploading-item"><span class="translating">업로드 및 번역 중...</span></li>');
$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 = $('<div class="file-loading-overlay">로딩중…</div>');
$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(`
<div class="chat-logo"></div>
<div class="chat-title">엔큐톡</div>
<div class="chat-subtitle">도구를 선택하여 채팅을 시작하세요</div>
`);
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(`<iframe src="${iframeTools[currentTool.id]}" class="external-iframe"></iframe>`);
return;
} else {
$('.chat-input-wrapper').show();
$chatCenter.removeClass('iframe-mode');
}
$chatCenter.append(`
<div class="tool-chat-header">
<div class="tool-chat-title">${currentTool.name}</div>
<div class="tool-chat-desc">${currentTool.description}</div>
</div>
`);
// 현재 세션 메시지 출력
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, '<a href="#" class="ref-link" data-fname="$1" data-page="$2">$1 (p$2)</a>')
// 신백엔드 형식: [p1](/pdf?filename=파일.pdf#page=1)
.replace(/\[p(\d+)\]\(\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pageNum, fnameEnc, pageNum2){
const fname = decodeURIComponent(fnameEnc);
return `<a href="#" class="ref-link" data-fname="${fname}" data-page="${pageNum}">p${pageNum}</a>`;
})
// gxp-chat 경로 링크
.replace(/\[p(\d+)\]\(\/gxp-chat\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pg, fnameEnc){
const fname = decodeURIComponent(fnameEnc);
return `<a href="#" class="ref-link" data-fname="${fname}" data-page="${pg}">p${pg}</a>`;
})
contentHtml = contentHtml.replace(/\n/g, "<br>");
if (msg.type === 'progress') {
const label = msg.content || '생각중';
contentHtml = `<span style="color:#aaa;font-style:italic;">${label}</span><div class="progress-bar"><div class="progress-bar-inner"></div></div>`;
} else if (msg.type === 'loading') {
contentHtml = `<span style="color:#aaa;font-style:italic;">${msg.content}</span>`;
}
let timeDisplay = msg.time;
if (msg.type === 'loading') {
const elapsed = Math.floor((Date.now() - (msg.start||Date.now()))/1000);
timeDisplay = elapsed + 's';
}
$chatCenter.append(`<div class="chat-message ${messageClass}"><div class="message-content">${contentHtml}</div><div class="message-time">${timeDisplay}</div></div>`);
});
} 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, '<a href="#" class="ref-link" data-fname="$1" data-page="$2">$1 (p$2)</a>')
// 신백엔드 형식: [p1](/pdf?filename=파일.pdf#page=1)
.replace(/\[p(\d+)\]\(\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pageNum, fnameEnc, pageNum2){
const fname = decodeURIComponent(fnameEnc);
return `<a href="#" class="ref-link" data-fname="${fname}" data-page="${pageNum}">p${pageNum}</a>`;
})
// gxp-chat 경로 링크
.replace(/\[p(\d+)\]\(\/gxp-chat\/pdf\?filename=([^#]+)#page=(\d+)\)/g, function(_, pg, fnameEnc){
const fname = decodeURIComponent(fnameEnc);
return `<a href="#" class="ref-link" data-fname="${fname}" data-page="${pg}">p${pg}</a>`;
})
contentHtml = contentHtml.replace(/\n/g, "<br>");
if (msg.type === 'progress') {
const label = msg.content || '생각중';
contentHtml = `<span style="color:#aaa;font-style:italic;">${label}</span><div class="progress-bar"><div class="progress-bar-inner"></div></div>`;
} else if (msg.type === 'loading') {
contentHtml = `<span style="color:#aaa;font-style:italic;">${msg.content}</span>`;
}
let timeDisplay = msg.time;
if (msg.type === 'loading') {
const elapsed = Math.floor((Date.now() - (msg.start||Date.now()))/1000);
timeDisplay = elapsed + 's';
}
$chatCenter.append(`<div class="chat-message ${messageClass}"><div class="message-content">${contentHtml}</div><div class="message-time">${timeDisplay}</div></div>`);
});
}
// 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('<div style="padding:40px;color:#bbb;">도구가 없습니다.</div>');
return;
}
list.forEach(tool => {
const fav = favorites.includes(tool.id) ? 'active' : '';
$list.append(`
<div class="tool-card" data-tool-id="${tool.id}">
<div class="tool-title">${tool.name}</div>
<div class="tool-desc">${tool.description}</div>
<div class="tool-fav ${fav}" data-id="${tool.id}">&#9733;</div>
</div>
`);
});
}
// 필터 버튼 클릭 -> 목록 갱신
$(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(`<li class="uploading" data-temp="${pid}" style="font-style:italic; color:#aaa;"><span class="uploading-label">공부중<span class="uploading-dots">...</span></span></li>`);
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(`<span>${fname}</span><button class="file-del-btn" data-fname="${fname}">✕</button>`);
}
});
// 업로드되지 않은 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 `<a href="${url}" target="_blank" class="lecture-video"><img src="${thumb}" alt="video thumbnail"></a>`;
}).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 `<a href="${url}" target="_blank" class="lecture-video"><img src="${thumb}" alt="video thumbnail"></a>`;
}).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 `<a href="${url}" target="_blank" class="lecture-video"><img src="${thumb}" alt="video thumbnail"></a>`;
}).join('');
$page.append(`
<h3 class="lecture-topic">강수진박사의 프롬프트 엔지니어링의 세계</h3>
<div class="lecture-video-list">${videoHtml1}</div>
<h3 class="lecture-topic" style="margin-top:24px;">AI, 안해보면 모른다</h3>
<div class="lecture-video-list">${videoHtml2}</div>
<h3 class="lecture-topic" style="margin-top:24px;">MCP를 배워보자</h3>
<div class="lecture-video-list">${videoHtml3}</div>
`);
}
// ---------- 사이드바 메뉴 전환 ----------
$(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(`
<h2 class="newsletter-title">뉴스레터</h2>
<div class="newsletter-content-area">
<ul class="newsletter-list">
<li><a href="#" class="newsletter-link" data-file="AI뉴스레터25년7월호_IT전략팀.pdf">25년 7월호</a></li>
</ul>
</div>
`);
} 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 `<a href="${url}" target="_blank" class="lecture-video"><img src="${thumb}" alt="video thumbnail"></a>`;
}).join('');
$content.append(`
<h3 class="lecture-topic">강수진박사의 프롬프트 엔지니어링의 세계</h3>
<div class="lecture-video-list">${makeHtml(sec1)}</div>
<h3 class="lecture-topic" style="margin-top:24px;">AI, 안해보면 모른다</h3>
<div class="lecture-video-list">${makeHtml(sec2)}</div>
<h3 class="lecture-topic" style="margin-top:24px;">MCP를 배워보자</h3>
<div class="lecture-video-list">${makeHtml(sec3)}</div>
`);
} 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=>`<tr><td>${r.no}</td><td class="qna-title">${r.title}</td><td>${r.date}</td><td>${r.views}</td></tr>`).join('');
$content.append(`
<h2 class="qna-title-head">질의응답</h2>
<table class="qna-table">
<thead><tr><th style="width:60px;">번호</th><th>제목</th><th style="width:120px;">등록일</th><th style="width:80px;">조회수</th></tr></thead>
<tbody>${rowHtml}</tbody>
</table>
`);
}
}
// PDF 뷰 닫기 (X 버튼)
$(document).on('click', '.newsletter-close', function(){
// 복원 리스트 영역
const $area = $('.newsletter-content-area');
if($area.length){
$area.html(`
<ul class="newsletter-list">
<li><a href="#" class="newsletter-link" data-file="AI뉴스레터25년7월호_IT전략팀.pdf">25년 7월호</a></li>
</ul>`);
}
});
// 뉴스레터 항목 클릭 → 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(`
<div class="newsletter-pdf-wrapper">
<button class="newsletter-close">×</button>
<iframe src="${url}" class="newsletter-iframe"></iframe>
</div>
`);
});
// 서브 메뉴 클릭 (뉴스레터/질의응답 등)
$(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();
});