// 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 = `
`;
}
$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(`