// ==================== 전역 변수 ==================== let socket; let isStreaming = false; let isRecording = false; let currentScene = null; let timeline = []; // 타임라인 데이터 let timelinePlaying = false; // 타임라인 재생 중 여부 let ragKnowledgeBase = []; // RAG 지식 베이스 let ragEnabled = false; // RAG 활성화 여부 let ragResponseCount = 0; // RAG 답변 횟수 // ==================== 초기화 ==================== document.addEventListener('DOMContentLoaded', function() { initializeSocket(); loadInitialData(); setupEventListeners(); }); // ==================== SocketIO 초기화 ==================== function initializeSocket() { socket = io(); socket.on('connect', function() { console.log('서버에 연결됨'); updateConnectionStatus(true); socket.emit('request_status'); }); socket.on('disconnect', function() { console.log('서버 연결 해제됨'); updateConnectionStatus(false); }); // 장면 변경 이벤트 socket.on('scene_changed', function(data) { console.log('장면 변경:', data.scene_name); currentScene = data.scene_name; updateCurrentScene(data.scene_name); updateSceneButtons(data.scene_name); document.getElementById('currentSceneName').textContent = data.scene_name; showToast(`장면 전환: ${data.scene_name}`); }); // 타임라인 업데이트 socket.on('timeline_updated', function(data) { console.log('타임라인 업데이트:', data); timeline = data.timeline; timelinePlaying = data.playing; displayTimeline(); updateTimelineControls(); }); // 타임라인 재생 중 socket.on('timeline_playing', function(data) { console.log('타임라인 재생:', data); document.getElementById('timelineStatusText').textContent = `재생 중: ${data.scene_name} (${data.index + 1}/${data.total})`; document.getElementById('timelineProgress').textContent = `${data.index + 1} / ${data.total}`; }); // 타임라인 진행 상황 socket.on('timeline_progress', function(data) { const percentage = (data.elapsed / data.duration) * 100; document.getElementById('timelineProgressBar').style.width = percentage + '%'; }); // 타임라인 완료 socket.on('timeline_finished', function(data) { console.log('타임라인 재생 완료'); timelinePlaying = false; updateTimelineControls(); document.getElementById('timelineStatus').style.display = 'none'; document.getElementById('timelineProgressBar').style.width = '0%'; showToast('타임라인 재생 완료!', 'success'); }); // 타임라인 오류 socket.on('timeline_error', function(data) { console.error('타임라인 오류:', data.error); timelinePlaying = false; updateTimelineControls(); showToast('타임라인 재생 오류: ' + data.error, 'error'); }); // RAG 업데이트 socket.on('rag_updated', function(data) { console.log('RAG 업데이트:', data); ragKnowledgeBase = data.knowledge_base; ragEnabled = data.enabled; displayRAGKnowledge(); updateRAGStatus(); }); // 스트리밍 상태 변경 socket.on('stream_state_changed', function(data) { console.log('스트리밍 상태 변경:', data); isStreaming = data.active; updateStreamingStatus(data.active); showToast(data.active ? '스트리밍 시작됨' : '스트리밍 정지됨'); }); // 녹화 상태 변경 socket.on('record_state_changed', function(data) { console.log('녹화 상태 변경:', data); isRecording = data.active; updateRecordingStatus(data.active); showToast(data.active ? '녹화 시작됨' : '녹화 정지됨'); }); // 음소거 상태 변경 socket.on('mute_state_changed', function(data) { console.log('음소거 상태 변경:', data); updateMuteButton(data.input_name, data.muted); }); } // ==================== 초기 데이터 로드 ==================== async function loadInitialData() { try { // 상태 로드 await loadStatus(); // 장면 로드 await loadScenes(); // 소스 로드 await loadSources(); // 타임라인 로드 await loadTimeline(); // RAG 지식 베이스 로드 await loadRAGKnowledge(); } catch (error) { console.error('초기 데이터 로드 실패:', error); showToast('데이터 로드 실패', 'error'); } } async function loadStatus() { try { const response = await fetch('/api/status'); if (!response.ok) throw new Error('상태 조회 실패'); const data = await response.json(); // 현재 상태 업데이트 currentScene = data.current_scene; isStreaming = data.streaming; isRecording = data.recording; updateCurrentScene(data.current_scene); updateStreamingStatus(data.streaming, data.stream_status); updateRecordingStatus(data.recording, data.record_status); updateVirtualCamStatus(data.virtual_cam); } catch (error) { console.error('상태 로드 실패:', error); } } async function loadScenes() { try { const response = await fetch('/api/scenes'); if (!response.ok) throw new Error('장면 목록 조회 실패'); const data = await response.json(); displayScenes(data.scenes, data.current_scene); } catch (error) { console.error('장면 로드 실패:', error); document.getElementById('sceneGrid').innerHTML = '

장면 로드 실패

'; } } async function loadSources() { try { const response = await fetch('/api/sources'); if (!response.ok) throw new Error('소스 목록 조회 실패'); const data = await response.json(); displaySources(data.sources); } catch (error) { console.error('소스 로드 실패:', error); document.getElementById('sourceList').innerHTML = '

소스 로드 실패

'; } } // ==================== UI 업데이트 ==================== function updateConnectionStatus(connected) { const statusDot = document.getElementById('connectionStatus'); const statusText = document.getElementById('connectionText'); if (connected) { statusDot.className = 'status-dot connected'; statusText.textContent = 'OBS 연결됨'; } else { statusDot.className = 'status-dot disconnected'; statusText.textContent = 'OBS 연결 안됨'; } } function updateCurrentScene(sceneName) { document.getElementById('currentScene').textContent = sceneName || '-'; } function updateStreamingStatus(active, statusData) { const statusEl = document.getElementById('streamStatus'); const infoEl = document.getElementById('streamInfo'); const startBtn = document.getElementById('startStreamBtn'); const stopBtn = document.getElementById('stopStreamBtn'); if (active) { statusEl.textContent = '🔴 진행 중'; statusEl.style.color = 'var(--danger-color)'; startBtn.disabled = true; stopBtn.disabled = false; if (statusData && statusData.timecode) { infoEl.textContent = `시간: ${statusData.timecode}`; } } else { statusEl.textContent = '정지'; statusEl.style.color = 'var(--text-secondary)'; startBtn.disabled = false; stopBtn.disabled = true; infoEl.textContent = ''; } } function updateRecordingStatus(active, statusData) { const statusEl = document.getElementById('recordStatus'); const infoEl = document.getElementById('recordInfo'); const startBtn = document.getElementById('startRecordBtn'); const pauseBtn = document.getElementById('pauseRecordBtn'); const stopBtn = document.getElementById('stopRecordBtn'); if (active) { statusEl.textContent = '🔴 녹화 중'; statusEl.style.color = 'var(--danger-color)'; startBtn.disabled = true; pauseBtn.disabled = false; stopBtn.disabled = false; if (statusData && statusData.timecode) { const pausedText = statusData.paused ? ' (일시정지)' : ''; infoEl.textContent = `시간: ${statusData.timecode}${pausedText}`; } } else { statusEl.textContent = '정지'; statusEl.style.color = 'var(--text-secondary)'; startBtn.disabled = false; pauseBtn.disabled = true; stopBtn.disabled = true; infoEl.textContent = ''; } } function updateVirtualCamStatus(active) { const statusEl = document.getElementById('vcamStatus'); if (active) { statusEl.textContent = '🟢 활성'; statusEl.style.color = 'var(--success-color)'; } else { statusEl.textContent = '비활성'; statusEl.style.color = 'var(--text-secondary)'; } } function displayScenes(scenes, currentScene) { const sceneGrid = document.getElementById('sceneGrid'); if (!scenes || scenes.length === 0) { sceneGrid.innerHTML = '

장면이 없습니다

'; return; } sceneGrid.innerHTML = ''; scenes.forEach(sceneName => { const btn = document.createElement('button'); btn.className = 'scene-btn'; if (sceneName === currentScene) { btn.classList.add('active'); } btn.textContent = sceneName; // 클릭 시 타임라인에 추가 (더블클릭 시 즉시 전환) btn.onclick = () => addToTimeline(sceneName); btn.ondblclick = () => setScene(sceneName); btn.title = '클릭: 타임라인 추가 | 더블클릭: 즉시 전환'; sceneGrid.appendChild(btn); }); } function updateSceneButtons(activeScene) { const sceneBtns = document.querySelectorAll('.scene-btn'); sceneBtns.forEach(btn => { if (btn.textContent === activeScene) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } function displaySources(sources) { const sourceList = document.getElementById('sourceList'); if (!sources || sources.length === 0) { sourceList.innerHTML = '

오디오 소스가 없습니다

'; return; } // 오디오 관련 소스만 필터링 const audioSources = sources.filter(source => source.inputKind && ( source.inputKind.includes('audio') || source.inputKind.includes('wasapi') || source.inputKind.includes('coreaudio') || source.inputKind.includes('pulse') ) ); if (audioSources.length === 0) { sourceList.innerHTML = '

오디오 소스가 없습니다

'; return; } sourceList.innerHTML = ''; audioSources.forEach(source => { const item = document.createElement('div'); item.className = 'source-item'; item.innerHTML = `
${source.inputName}
${source.inputKind}
`; sourceList.appendChild(item); }); } function updateMuteButton(sourceName, muted) { const sourceBtns = document.querySelectorAll('.mute-btn'); sourceBtns.forEach(btn => { if (btn.onclick.toString().includes(sourceName)) { if (muted) { btn.classList.add('muted'); btn.innerHTML = '🔇 음소거됨'; } else { btn.classList.remove('muted'); btn.innerHTML = '🔊 활성'; } } }); } // ==================== 이벤트 리스너 ==================== function setupEventListeners() { // 스트리밍 제어 document.getElementById('startStreamBtn').addEventListener('click', startStreaming); document.getElementById('stopStreamBtn').addEventListener('click', stopStreaming); // 녹화 제어 document.getElementById('startRecordBtn').addEventListener('click', startRecording); document.getElementById('pauseRecordBtn').addEventListener('click', pauseRecording); document.getElementById('stopRecordBtn').addEventListener('click', stopRecording); // 가상 카메라 document.getElementById('toggleVcamBtn').addEventListener('click', toggleVirtualCam); } // ==================== API 호출 ==================== async function setScene(sceneName) { try { const response = await fetch('/api/scene/change', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scene_name: sceneName }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '장면 전환 실패'); } showToast(`장면 전환: ${sceneName}`, 'success'); } catch (error) { console.error('장면 전환 실패:', error); showToast(error.message || '장면 전환 실패', 'error'); } } async function startStreaming() { try { const response = await fetch('/api/stream/start', { method: 'POST' }); if (!response.ok) throw new Error('스트리밍 시작 실패'); } catch (error) { console.error('스트리밍 시작 실패:', error); showToast('스트리밍 시작 실패', 'error'); } } async function stopStreaming() { try { const response = await fetch('/api/stream/stop', { method: 'POST' }); if (!response.ok) throw new Error('스트리밍 정지 실패'); } catch (error) { console.error('스트리밍 정지 실패:', error); showToast('스트리밍 정지 실패', 'error'); } } async function startRecording() { try { const response = await fetch('/api/record/start', { method: 'POST' }); if (!response.ok) throw new Error('녹화 시작 실패'); } catch (error) { console.error('녹화 시작 실패:', error); showToast('녹화 시작 실패', 'error'); } } async function pauseRecording() { try { const response = await fetch('/api/record/pause', { method: 'POST' }); if (!response.ok) throw new Error('녹화 일시정지 실패'); const data = await response.json(); showToast(data.paused ? '녹화 일시정지' : '녹화 재개'); } catch (error) { console.error('녹화 일시정지 실패:', error); showToast('녹화 일시정지 실패', 'error'); } } async function stopRecording() { try { const response = await fetch('/api/record/stop', { method: 'POST' }); if (!response.ok) throw new Error('녹화 정지 실패'); const data = await response.json(); if (data.file) { showToast(`녹화 완료: ${data.file}`); } } catch (error) { console.error('녹화 정지 실패:', error); showToast('녹화 정지 실패', 'error'); } } async function toggleMute(sourceName) { try { const response = await fetch(`/api/sources/${encodeURIComponent(sourceName)}/mute`, { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '음소거 토글 실패'); } const data = await response.json(); showToast(data.muted ? `${sourceName} 음소거` : `${sourceName} 음소거 해제`, 'success'); } catch (error) { console.error('음소거 토글 실패:', error); showToast(error.message || '음소거 토글 실패', 'error'); } } async function toggleVirtualCam() { try { const response = await fetch('/api/virtual-camera/toggle', { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '가상 카메라 토글 실패'); } const data = await response.json(); updateVirtualCamStatus(data.active); showToast(data.active ? '가상 카메라 활성화' : '가상 카메라 비활성화'); } catch (error) { console.error('가상 카메라 토글 실패:', error); showToast('가상 카메라 토글 실패', 'error'); } } // ==================== 토스트 알림 ==================== function showToast(message, type = 'success') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast show ${type}`; setTimeout(() => { toast.className = 'toast'; }, 3000); } // ==================== 타임라인 기능 ==================== async function loadTimeline() { try { const response = await fetch('/api/timeline'); if (!response.ok) throw new Error('타임라인 조회 실패'); const data = await response.json(); timeline = data.timeline; timelinePlaying = data.playing; if (data.current_scene) { document.getElementById('currentSceneName').textContent = data.current_scene; } displayTimeline(); updateTimelineControls(); } catch (error) { console.error('타임라인 로드 실패:', error); } } async function addToTimeline(sceneName) { try { // 사용자에게 시간 입력 받기 const duration = prompt(`"${sceneName}" 장면 재생 시간을 입력하세요 (초):`, '30'); if (!duration || isNaN(duration)) { showToast('유효한 시간을 입력해주세요', 'error'); return; } const response = await fetch('/api/timeline/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scene_name: sceneName, duration: parseInt(duration) }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '타임라인 추가 실패'); } const data = await response.json(); timeline = data.timeline; displayTimeline(); showToast(`타임라인에 추가: ${sceneName} (${duration}초)`, 'success'); } catch (error) { console.error('타임라인 추가 실패:', error); showToast(error.message || '타임라인 추가 실패', 'error'); } } function displayTimeline() { const timelineItems = document.getElementById('timelineItems'); const timelineEmpty = document.getElementById('timelineEmpty'); if (!timeline || timeline.length === 0) { timelineEmpty.style.display = 'block'; timelineItems.innerHTML = ''; return; } timelineEmpty.style.display = 'none'; timelineItems.innerHTML = ''; timeline.forEach((item, index) => { const itemEl = document.createElement('div'); itemEl.className = 'timeline-item'; itemEl.style.cssText = ` background: var(--card-bg); padding: 10px 15px; border-radius: 8px; border: 2px solid var(--border-color); display: flex; flex-direction: column; gap: 5px; min-width: 150px; position: relative; `; itemEl.innerHTML = `
#${index + 1}
🎭 ${item.scene_name}
⏱️ ${item.duration}초
`; timelineItems.appendChild(itemEl); }); } async function removeFromTimeline(itemId) { try { const response = await fetch(`/api/timeline/remove/${itemId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('타임라인 제거 실패'); const data = await response.json(); timeline = data.timeline; displayTimeline(); showToast('타임라인에서 제거됨', 'success'); } catch (error) { console.error('타임라인 제거 실패:', error); showToast('타임라인 제거 실패', 'error'); } } async function clearTimeline() { if (!confirm('타임라인을 전체 삭제하시겠습니까?')) { return; } try { const response = await fetch('/api/timeline/clear', { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '타임라인 삭제 실패'); } timeline = []; displayTimeline(); showToast('타임라인이 삭제되었습니다', 'success'); } catch (error) { console.error('타임라인 삭제 실패:', error); showToast(error.message || '타임라인 삭제 실패', 'error'); } } async function playTimeline() { if (!timeline || timeline.length === 0) { showToast('타임라인이 비어있습니다', 'error'); return; } try { const response = await fetch('/api/timeline/play', { method: 'POST' }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '재생 실패'); } timelinePlaying = true; updateTimelineControls(); document.getElementById('timelineStatus').style.display = 'block'; document.getElementById('timelineProgressBar').style.width = '0%'; showToast('타임라인 재생 시작', 'success'); } catch (error) { console.error('타임라인 재생 실패:', error); showToast(error.message || '타임라인 재생 실패', 'error'); } } async function stopTimeline() { try { const response = await fetch('/api/timeline/stop', { method: 'POST' }); if (!response.ok) throw new Error('재생 중지 실패'); timelinePlaying = false; updateTimelineControls(); document.getElementById('timelineStatus').style.display = 'none'; showToast('타임라인 재생 중지', 'success'); } catch (error) { console.error('타임라인 중지 실패:', error); showToast('타임라인 중지 실패', 'error'); } } function updateTimelineControls() { const playBtn = document.getElementById('playTimelineBtn'); const stopBtn = document.getElementById('stopTimelineBtn'); if (timelinePlaying) { playBtn.style.display = 'none'; stopBtn.style.display = 'block'; } else { playBtn.style.display = 'block'; stopBtn.style.display = 'none'; } } // ==================== RAG 시스템 기능 ==================== async function loadRAGKnowledge() { try { const response = await fetch('/api/rag/knowledge'); if (!response.ok) throw new Error('RAG 지식 조회 실패'); const data = await response.json(); ragKnowledgeBase = data.knowledge_base; ragEnabled = data.enabled; ragResponseCount = data.response_count; displayRAGKnowledge(); updateRAGStatus(); } catch (error) { console.error('RAG 지식 로드 실패:', error); } } async function addRAGKnowledge() { const question = document.getElementById('ragQuestion').value.trim(); const answer = document.getElementById('ragAnswer').value.trim(); const keywordsInput = document.getElementById('ragKeywords').value.trim(); if (!question || !answer) { showToast('질문과 답변을 입력하세요', 'error'); return; } const keywords = keywordsInput ? keywordsInput.split(',').map(k => k.trim()) : []; try { const response = await fetch('/api/rag/knowledge/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, answer, keywords }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'RAG 지식 추가 실패'); } const data = await response.json(); ragKnowledgeBase = data.knowledge_base; displayRAGKnowledge(); clearRAGForm(); showToast('질의응답이 추가되었습니다', 'success'); } catch (error) { console.error('RAG 지식 추가 실패:', error); showToast(error.message || 'RAG 지식 추가 실패', 'error'); } } function clearRAGForm() { document.getElementById('ragQuestion').value = ''; document.getElementById('ragAnswer').value = ''; document.getElementById('ragKeywords').value = ''; } function displayRAGKnowledge() { const list = document.getElementById('ragKnowledgeList'); const count = document.getElementById('ragKnowledgeCount'); if (!ragKnowledgeBase || ragKnowledgeBase.length === 0) { list.innerHTML = '

지식이 없습니다. 질의응답을 추가하세요.

'; count.textContent = '0'; return; } count.textContent = ragKnowledgeBase.length; list.innerHTML = ''; ragKnowledgeBase.forEach((item, index) => { const itemEl = document.createElement('div'); itemEl.style.cssText = ` background: var(--card-bg); padding: 12px; margin-bottom: 10px; border-radius: 8px; border-left: 4px solid var(--primary-color); `; const keywords = item.keywords && item.keywords.length > 0 ? item.keywords.slice(0, 5).join(', ') : '없음'; const useCount = item.use_count || 0; itemEl.innerHTML = `
#${index + 1} ${item.question}

${item.answer}

🏷️ ${keywords} 📊 사용: ${useCount}회
`; list.appendChild(itemEl); }); } async function deleteRAGKnowledge(itemId) { if (!confirm('이 질의응답을 삭제하시겠습니까?')) { return; } try { const response = await fetch(`/api/rag/knowledge/${itemId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('RAG 지식 삭제 실패'); const data = await response.json(); ragKnowledgeBase = data.knowledge_base; displayRAGKnowledge(); showToast('질의응답이 삭제되었습니다', 'success'); } catch (error) { console.error('RAG 지식 삭제 실패:', error); showToast('RAG 지식 삭제 실패', 'error'); } } async function clearRAGKnowledge() { if (!confirm('모든 질의응답을 삭제하시겠습니까?')) { return; } try { const response = await fetch('/api/rag/knowledge/clear', { method: 'POST' }); if (!response.ok) throw new Error('RAG 지식 삭제 실패'); ragKnowledgeBase = []; ragResponseCount = 0; displayRAGKnowledge(); updateRAGStatus(); showToast('모든 질의응답이 삭제되었습니다', 'success'); } catch (error) { console.error('RAG 지식 삭제 실패:', error); showToast('RAG 지식 삭제 실패', 'error'); } } async function toggleRAG(enabled) { try { const response = await fetch('/api/rag/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }) }); if (!response.ok) throw new Error('RAG 토글 실패'); const data = await response.json(); ragEnabled = data.enabled; updateRAGStatus(); showToast(ragEnabled ? 'RAG 자동 답변 활성화' : 'RAG 자동 답변 비활성화', 'success'); } catch (error) { console.error('RAG 토글 실패:', error); showToast('RAG 토글 실패', 'error'); } } function updateRAGStatus() { const toggle = document.getElementById('ragToggle'); const statusText = document.getElementById('ragStatusText'); const responseCount = document.getElementById('ragResponseCount'); toggle.checked = ragEnabled; statusText.textContent = ragEnabled ? '활성화' : '비활성화'; statusText.style.color = ragEnabled ? 'var(--primary-color)' : 'var(--text-secondary)'; responseCount.textContent = ragResponseCount; } async function testRAGAnswer() { const question = document.getElementById('ragTestQuestion').value.trim(); if (!question) { showToast('질문을 입력하세요', 'error'); return; } const resultDiv = document.getElementById('ragTestResult'); const answerText = document.getElementById('ragTestAnswerText'); const knowledgeUsed = document.getElementById('ragTestKnowledgeUsed'); resultDiv.style.display = 'block'; answerText.textContent = '답변 생성 중...'; knowledgeUsed.textContent = ''; try { const response = await fetch('/api/rag/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '답변 생성 실패'); } const data = await response.json(); answerText.textContent = data.answer; if (data.relevant_knowledge && data.relevant_knowledge.length > 0) { const questions = data.relevant_knowledge.map(k => k.question).join(', '); knowledgeUsed.textContent = questions; } else { knowledgeUsed.textContent = '없음'; } showToast('답변 생성 완료', 'success'); } catch (error) { console.error('RAG 답변 테스트 실패:', error); answerText.textContent = '오류: ' + error.message; showToast(error.message || 'RAG 답변 테스트 실패', 'error'); } } async function exportRAGKnowledge() { try { const response = await fetch('/api/rag/export'); if (!response.ok) throw new Error('내보내기 실패'); const data = await response.json(); const jsonStr = JSON.stringify(data, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `rag_knowledge_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('지식 베이스를 내보냈습니다', 'success'); } catch (error) { console.error('내보내기 실패:', error); showToast('내보내기 실패', 'error'); } } function showImportRAGModal() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); const response = await fetch('/api/rag/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '가져오기 실패'); } const result = await response.json(); ragKnowledgeBase = result.knowledge_base; displayRAGKnowledge(); showToast('지식 베이스를 가져왔습니다', 'success'); } catch (error) { console.error('가져오기 실패:', error); showToast(error.message || '가져오기 실패', 'error'); } }; input.click(); } // ==================== RAG 채팅 모달 ==================== let ragChatHistory = []; // 채팅 히스토리 function openRAGChatModal() { const modal = document.getElementById('ragChatModal'); modal.style.display = 'block'; document.getElementById('ragChatInput').focus(); } function closeRAGChatModal() { const modal = document.getElementById('ragChatModal'); modal.style.display = 'none'; } function clearRAGChat() { ragChatHistory = []; const messagesDiv = document.getElementById('ragChatMessages'); messagesDiv.innerHTML = `
🤖

질문을 입력하면 RAG 시스템이 답변합니다

지식 베이스에 등록된 정보를 바탕으로 응답합니다
`; } async function sendRAGChatMessage() { const input = document.getElementById('ragChatInput'); const question = input.value.trim(); if (!question) return; // 입력 초기화 input.value = ''; // 사용자 메시지 표시 addRAGChatMessage('user', question); // "답변 생성 중..." 표시 const botMsgId = addRAGChatMessage('bot', '답변 생성 중...', true); try { const response = await fetch('/api/rag/answer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || '답변 생성 실패'); } const data = await response.json(); // "답변 생성 중..." 메시지 업데이트 updateRAGChatMessage(botMsgId, data.answer, data.relevant_knowledge); } catch (error) { console.error('RAG 답변 실패:', error); updateRAGChatMessage(botMsgId, `❌ 오류: ${error.message}`, []); } } function addRAGChatMessage(sender, message, isLoading = false) { const messagesDiv = document.getElementById('ragChatMessages'); // 첫 메시지인 경우 초기 안내 제거 if (ragChatHistory.length === 0) { messagesDiv.innerHTML = ''; } const msgId = `msg-${Date.now()}`; const isUser = sender === 'user'; const messageEl = document.createElement('div'); messageEl.id = msgId; messageEl.style.cssText = ` margin-bottom: 15px; display: flex; ${isUser ? 'justify-content: flex-end;' : 'justify-content: flex-start;'} `; const bubbleStyle = isUser ? 'background: linear-gradient(135deg, var(--primary-color), #667eea); color: white;' : 'background: var(--card-bg); color: var(--text-primary); border: 2px solid var(--border-color);'; messageEl.innerHTML = `
${isUser ? '👤 나' : '🤖 RAG Bot'}
${message}
${isLoading ? '
' : ''}
`; messagesDiv.appendChild(messageEl); // 스크롤을 맨 아래로 messagesDiv.scrollTop = messagesDiv.scrollHeight; // 히스토리에 추가 ragChatHistory.push({ id: msgId, sender, message }); return msgId; } function updateRAGChatMessage(msgId, newMessage, relevantKnowledge = []) { const messageEl = document.getElementById(msgId); if (!messageEl) return; // 봇 메시지는 항상 왼쪽 정렬 messageEl.style.cssText = ` margin-bottom: 15px; display: flex; justify-content: flex-start; `; // 말풍선 전체를 다시 렌더링 (로딩 아이콘 제거) const bubbleStyle = 'background: var(--card-bg); color: var(--text-primary); border: 2px solid var(--border-color);'; let html = newMessage; // 관련 지식 표시 if (relevantKnowledge && relevantKnowledge.length > 0) { html += '
'; html += '
📚 참고한 지식:
'; relevantKnowledge.forEach((item, index) => { html += `
• ${item.question}
`; }); html += '
'; } // 전체 말풍선 HTML 재구성 messageEl.innerHTML = `
🤖 RAG Bot
${html}
`; // 스크롤을 맨 아래로 const messagesDiv = document.getElementById('ragChatMessages'); messagesDiv.scrollTop = messagesDiv.scrollHeight; // 히스토리 업데이트 const historyItem = ragChatHistory.find(h => h.id === msgId); if (historyItem) { historyItem.message = newMessage; } } // ==================== 주기적 상태 업데이트 ==================== setInterval(() => { if (socket && socket.connected) { socket.emit('request_status'); } }, 2000); // 2초마다 상태 업데이트