feat: AI 서비스 및 AI 어시스턴트 전용 페이지 추가
- ai-service: Ollama 기반 AI 서비스 (분류, 시맨틱 검색, RAG Q&A, 패턴 분석) - AI 어시스턴트 페이지: 채팅형 Q&A, 시맨틱 검색, 패턴 분석, 분류 테스트 - 권한 시스템에 ai_assistant 페이지 등록 (기본 비활성) - 기존 페이지에 AI 기능 통합 (대시보드, 수신함, 관리함) - docker-compose, gateway, nginx 설정 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1786,8 +1786,130 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// API 스크립트 동적 로드
|
||||
const script = document.createElement('script');
|
||||
script.src = '/static/js/api.js?v=20260213';
|
||||
script.onload = initializeDashboardApp;
|
||||
document.body.appendChild(script);
|
||||
// AI 시맨틱 검색
|
||||
async function aiSemanticSearch() {
|
||||
const query = document.getElementById('aiSearchQuery')?.value?.trim();
|
||||
if (!query || typeof AiAPI === 'undefined') return;
|
||||
|
||||
const loading = document.getElementById('aiSearchLoading');
|
||||
const results = document.getElementById('aiSearchResults');
|
||||
if (loading) loading.classList.remove('hidden');
|
||||
if (results) { results.classList.add('hidden'); results.innerHTML = ''; }
|
||||
|
||||
const data = await AiAPI.searchSimilar(query, 8);
|
||||
if (loading) loading.classList.add('hidden');
|
||||
|
||||
if (!data.available || !data.results || data.results.length === 0) {
|
||||
results.innerHTML = '<p class="text-sm text-gray-400 text-center py-2">검색 결과가 없습니다</p>';
|
||||
results.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
results.innerHTML = data.results.map(r => {
|
||||
const meta = r.metadata || {};
|
||||
const similarity = Math.round((r.similarity || 0) * 100);
|
||||
const issueId = meta.issue_id || r.id.replace('issue_', '');
|
||||
const doc = (r.document || '').substring(0, 100);
|
||||
const cat = meta.category || '';
|
||||
const status = meta.review_status || '';
|
||||
return `
|
||||
<div class="flex items-start space-x-3 bg-gray-50 rounded-lg p-3 hover:bg-purple-50 transition-colors cursor-pointer"
|
||||
onclick="showAiIssueModal(${issueId})"
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
|
||||
<span class="text-xs font-bold text-purple-700">${similarity}%</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center space-x-2 mb-1">
|
||||
<span class="text-sm font-medium text-gray-800">No.${issueId}</span>
|
||||
${cat ? `<span class="text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-700">${cat}</span>` : ''}
|
||||
${status ? `<span class="text-xs text-gray-400">${status}</span>` : ''}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 truncate">${doc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
results.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// RAG Q&A
|
||||
async function aiAskQuestion() {
|
||||
const question = document.getElementById('aiQaQuestion')?.value?.trim();
|
||||
if (!question || typeof AiAPI === 'undefined') return;
|
||||
|
||||
const loading = document.getElementById('aiQaLoading');
|
||||
const result = document.getElementById('aiQaResult');
|
||||
const answer = document.getElementById('aiQaAnswer');
|
||||
const sources = document.getElementById('aiQaSources');
|
||||
|
||||
if (loading) loading.classList.remove('hidden');
|
||||
if (result) result.classList.add('hidden');
|
||||
|
||||
const projectId = document.getElementById('projectFilter')?.value || null;
|
||||
const data = await AiAPI.askQuestion(question, projectId ? parseInt(projectId) : null);
|
||||
|
||||
if (loading) loading.classList.add('hidden');
|
||||
|
||||
if (!data.available) {
|
||||
if (answer) answer.textContent = 'AI 서비스를 사용할 수 없습니다';
|
||||
if (result) result.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer) answer.textContent = data.answer || '';
|
||||
if (sources && data.sources) {
|
||||
const refs = data.sources.slice(0, 5).map(s =>
|
||||
`No.${s.id}(${s.similarity}%)`
|
||||
).join(', ');
|
||||
sources.textContent = refs ? `참고: ${refs}` : '';
|
||||
}
|
||||
if (result) result.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// AI 이슈 상세 모달
|
||||
async function showAiIssueModal(issueId) {
|
||||
const modal = document.getElementById('aiIssueModal');
|
||||
const title = document.getElementById('aiIssueModalTitle');
|
||||
const body = document.getElementById('aiIssueModalBody');
|
||||
if (!modal || !body) return;
|
||||
|
||||
title.textContent = `부적합 No.${issueId}`;
|
||||
body.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin text-purple-500"></i> 로딩 중...</div>';
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const token = typeof TokenManager !== 'undefined' ? TokenManager.getToken() : null;
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch(`/api/issues/${issueId}`, { headers });
|
||||
if (!res.ok) throw new Error('fetch failed');
|
||||
const issue = await res.json();
|
||||
|
||||
const categoryText = typeof getCategoryText === 'function' ? getCategoryText(issue.category || issue.final_category) : (issue.category || issue.final_category || '-');
|
||||
const statusText = typeof getStatusText === 'function' ? getStatusText(issue.review_status) : (issue.review_status || '-');
|
||||
const deptText = typeof getDepartmentText === 'function' ? getDepartmentText(issue.responsible_department) : (issue.responsible_department || '-');
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-700">${categoryText}</span>
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-700">${statusText}</span>
|
||||
<span class="px-2 py-1 text-xs rounded-full bg-orange-100 text-orange-700">${deptText}</span>
|
||||
${issue.report_date ? `<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-600">${issue.report_date}</span>` : ''}
|
||||
</div>
|
||||
${issue.description ? `<div><strong class="text-gray-600">설명:</strong><p class="mt-1 whitespace-pre-line">${issue.description}</p></div>` : ''}
|
||||
${issue.detail_notes ? `<div><strong class="text-gray-600">상세:</strong><p class="mt-1 whitespace-pre-line">${issue.detail_notes}</p></div>` : ''}
|
||||
${issue.final_description ? `<div><strong class="text-gray-600">최종 판정:</strong><p class="mt-1 whitespace-pre-line">${issue.final_description}</p></div>` : ''}
|
||||
${issue.solution ? `<div><strong class="text-gray-600">해결방안:</strong><p class="mt-1 whitespace-pre-line">${issue.solution}</p></div>` : ''}
|
||||
${issue.cause_detail ? `<div><strong class="text-gray-600">원인:</strong><p class="mt-1 whitespace-pre-line">${issue.cause_detail}</p></div>` : ''}
|
||||
${issue.management_comment ? `<div><strong class="text-gray-600">관리 의견:</strong><p class="mt-1 whitespace-pre-line">${issue.management_comment}</p></div>` : ''}
|
||||
<div class="pt-3 border-t text-right">
|
||||
<a href="/issues-management.html#issue-${issueId}" class="text-xs text-purple-500 hover:underline">관리함에서 보기 →</a>
|
||||
</div>
|
||||
`;
|
||||
} catch (e) {
|
||||
body.innerHTML = `<p class="text-red-500">이슈를 불러올 수 없습니다</p>
|
||||
<a href="/issues-management.html#issue-${issueId}" class="text-xs text-purple-500 hover:underline">관리함에서 보기 →</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 초기화
|
||||
initializeDashboardApp();
|
||||
|
||||
Reference in New Issue
Block a user