1081 lines
50 KiB
JavaScript
1081 lines
50 KiB
JavaScript
// 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}">★</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();
|
||
}); |