/** * ai-assistant.js — AI 어시스턴트 페이지 스크립트 */ let currentUser = null; let projects = []; let chatHistory = []; // 애니메이션 함수들 function animateHeaderAppearance() { const header = document.getElementById('commonHeader'); if (header) { header.classList.add('header-fade-in'); } } // 페이지 초기화 async function initializeAiAssistant() { try { currentUser = await window.authManager.checkAuth(); if (!currentUser) { document.getElementById('loadingScreen').style.display = 'none'; window.location.href = '/'; return; } window.pagePermissionManager.setUser(currentUser); await window.pagePermissionManager.loadPagePermissions(); if (!window.pagePermissionManager.canAccessPage('ai_assistant')) { alert('AI 어시스턴트 접근 권한이 없습니다.'); window.location.href = '/'; return; } if (window.commonHeader) { await window.commonHeader.init(currentUser, 'ai_assistant'); setTimeout(() => animateHeaderAppearance(), 100); } await loadProjects(); checkAiHealth(); document.getElementById('loadingScreen').style.display = 'none'; } catch (error) { console.error('AI 어시스턴트 초기화 실패:', error); alert('페이지를 불러오는데 실패했습니다.'); document.getElementById('loadingScreen').style.display = 'none'; } } // AuthManager 대기 후 초기화 document.addEventListener('DOMContentLoaded', () => { const checkAuthManager = () => { if (window.authManager) { initializeAiAssistant(); } else { setTimeout(checkAuthManager, 100); } }; checkAuthManager(); }); // 프로젝트 로드 async function loadProjects() { try { const apiUrl = window.API_BASE_URL || '/api'; const response = await fetch(`${apiUrl}/projects/`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' } }); if (response.ok) { projects = await response.json(); updateProjectFilters(); } } catch (error) { console.error('프로젝트 로드 실패:', error); } } function updateProjectFilters() { const selects = ['qaProjectFilter', 'searchProjectFilter']; selects.forEach(id => { const select = document.getElementById(id); if (!select) return; // 기존 옵션 유지 (첫 번째 "전체 프로젝트") while (select.options.length > 1) select.remove(1); projects.forEach(p => { const opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.project_name; select.appendChild(opt); }); }); } // ─── AI 상태 체크 ─────────────────────────────────────── async function checkAiHealth() { try { const health = await AiAPI.checkHealth(); const statusText = document.getElementById('aiStatusText'); const statusIcon = document.getElementById('aiStatusIcon'); const embeddingCount = document.getElementById('aiEmbeddingCount'); const modelName = document.getElementById('aiModelName'); if (health.status === 'healthy' || health.status === 'ok') { statusText.textContent = '연결됨'; statusText.classList.add('text-green-600'); statusIcon.innerHTML = ''; statusIcon.className = 'w-10 h-10 rounded-full bg-green-50 flex items-center justify-center'; } else { statusText.textContent = '연결 안됨'; statusText.classList.add('text-red-500'); statusIcon.innerHTML = ''; statusIcon.className = 'w-10 h-10 rounded-full bg-red-50 flex items-center justify-center'; } const embCount = health.embedding_count ?? health.total_embeddings ?? health.embeddings?.total_documents; if (embCount !== undefined) { embeddingCount.textContent = embCount.toLocaleString() + '건'; } const model = health.model || health.llm_model || (health.ollama?.models?.[0]); if (model) { modelName.textContent = model; } } catch (error) { console.error('AI 상태 체크 실패:', error); document.getElementById('aiStatusText').textContent = '오류'; } } // ─── Q&A 채팅 ─────────────────────────────────────────── function setQuickQuestion(text) { document.getElementById('qaQuestion').value = text; document.getElementById('qaQuestion').focus(); } async function submitQuestion() { const input = document.getElementById('qaQuestion'); const question = input.value.trim(); if (!question) return; const projectId = document.getElementById('qaProjectFilter').value || null; // 플레이스홀더 제거 const placeholder = document.getElementById('chatPlaceholder'); if (placeholder) placeholder.remove(); // 사용자 메시지 추가 appendChatMessage('user', question); input.value = ''; chatHistory.push({ role: 'user', content: question }); // 로딩 표시 appendChatLoading(); try { const result = await AiAPI.askQuestion(question, projectId); removeChatLoading(); if (result.available === false) { appendChatMessage('ai', 'AI 서비스에 연결할 수 없습니다. 잠시 후 다시 시도해주세요.'); return; } const answer = result.answer || result.response || '답변을 생성할 수 없습니다.'; const sources = result.sources || result.related_issues || []; appendChatMessage('ai', answer, sources); chatHistory.push({ role: 'ai', content: answer }); } catch (error) { removeChatLoading(); appendChatMessage('ai', '오류가 발생했습니다: ' + error.message); } } function appendChatMessage(role, content, sources) { const container = document.getElementById('chatContainer'); const wrapper = document.createElement('div'); wrapper.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} mb-3`; const bubble = document.createElement('div'); bubble.className = `chat-bubble ${role === 'user' ? 'chat-bubble-user' : 'chat-bubble-ai'}`; // 내용 렌더링 const contentDiv = document.createElement('div'); contentDiv.className = 'text-sm whitespace-pre-line'; contentDiv.textContent = content; bubble.appendChild(contentDiv); // AI 답변 참고 사례 if (role === 'ai' && sources && sources.length > 0) { const sourcesDiv = document.createElement('div'); sourcesDiv.className = 'mt-2 pt-2 border-t border-gray-200'; const sourcesTitle = document.createElement('p'); sourcesTitle.className = 'text-xs text-gray-500 mb-1'; sourcesTitle.textContent = '참고 사례:'; sourcesDiv.appendChild(sourcesTitle); sources.forEach(source => { const issueId = source.issue_id || source.id; const desc = source.description || source.title || `이슈 #${issueId}`; const similarity = source.similarity ? ` (${(source.similarity * 100).toFixed(0)}%)` : ''; const link = document.createElement('span'); link.className = 'source-link text-xs block'; link.textContent = `#${issueId} ${desc}${similarity}`; link.onclick = () => showAiIssueDetail(issueId); sourcesDiv.appendChild(link); }); bubble.appendChild(sourcesDiv); } wrapper.appendChild(bubble); container.appendChild(wrapper); container.scrollTop = container.scrollHeight; } function appendChatLoading() { const container = document.getElementById('chatContainer'); const wrapper = document.createElement('div'); wrapper.className = 'flex justify-start mb-3'; wrapper.id = 'chatLoadingBubble'; wrapper.innerHTML = `
부적합 관련 질문을 입력하세요.
과거 사례를 분석하여 답변합니다.
검색 결과가 없습니다.
'; return; } result.results.forEach((item, idx) => { const issueId = item.issue_id || item.id; const desc = item.description || ''; const similarity = item.similarity ? (item.similarity * 100).toFixed(1) : '-'; const category = item.category || ''; const status = item.status || item.review_status || ''; const card = document.createElement('div'); card.className = 'result-item border border-gray-200 rounded-lg p-3 flex items-start gap-3'; card.onclick = () => showAiIssueDetail(issueId); card.innerHTML = `${desc}
유사도
검색 중 오류가 발생했습니다.
`; } } // ─── 패턴 분석 ────────────────────────────────────────── async function executePatternAnalysis() { const input = document.getElementById('patternInput').value.trim(); if (!input) return; const loading = document.getElementById('patternLoading'); const resultsDiv = document.getElementById('patternResults'); loading.classList.remove('hidden'); resultsDiv.classList.add('hidden'); try { const result = await AiAPI.analyzePattern(input); loading.classList.add('hidden'); resultsDiv.classList.remove('hidden'); if (result.available === false) { resultsDiv.innerHTML = 'AI 서비스에 연결할 수 없습니다.
'; return; } let html = ''; // 분석 결과 const analysis = result.analysis || result.pattern || result.answer || ''; if (analysis) { html += `분석 결과가 없습니다.
'; } catch (error) { loading.classList.add('hidden'); resultsDiv.classList.remove('hidden'); resultsDiv.innerHTML = `분석 중 오류가 발생했습니다.
`; } } // ─── AI 분류 ──────────────────────────────────────────── async function executeClassification(useRAG) { const description = document.getElementById('classifyDescription').value.trim(); if (!description) { alert('부적합 설명을 입력해주세요.'); return; } const detailNotes = document.getElementById('classifyDetail').value.trim(); const loading = document.getElementById('classifyLoading'); const resultsDiv = document.getElementById('classifyResults'); loading.classList.remove('hidden'); resultsDiv.classList.add('hidden'); try { const result = useRAG ? await AiAPI.classifyWithRAG(description, detailNotes) : await AiAPI.classifyIssue(description, detailNotes); loading.classList.add('hidden'); resultsDiv.classList.remove('hidden'); if (result.available === false) { resultsDiv.innerHTML = 'AI 서비스에 연결할 수 없습니다.
'; return; } const methodLabel = useRAG ? 'RAG 분류 (과거 사례 참고)' : '기본 분류'; const methodColor = useRAG ? 'purple' : 'amber'; let html = `${val}
분류 중 오류가 발생했습니다.
`; } } // ─── 이슈 상세 모달 ───────────────────────────────────── async function showAiIssueDetail(issueId) { const modal = document.getElementById('aiIssueModal'); const title = document.getElementById('aiIssueModalTitle'); const body = document.getElementById('aiIssueModalBody'); title.textContent = `이슈 #${issueId}`; body.innerHTML = '불러오는 중...
${issue.project_name || '-'}
${issue.description || '-'}
${issue.detail_notes}
${issue.category || '-'}
${statusMap[issue.review_status] || issue.review_status || '-'}
${issue.location || '-'}
${issue.assigned_to_name || issue.assignee_name || '-'}
${issue.resolution}
${issue.created_at ? new Date(issue.created_at).toLocaleDateString('ko-KR') : '-'}
이슈를 불러오는데 실패했습니다.
`; } } // 엔트리 포인트 function initializeAiAssistantApp() { console.log('AI 어시스턴트 스크립트 로드 완료'); } initializeAiAssistantApp();