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:
Hyungi Ahn
2026-03-06 09:38:30 +09:00
parent d385ce7ac1
commit b3012b8320
44 changed files with 2914 additions and 53 deletions

View File

@@ -930,13 +930,100 @@ async function openIssueDetailModal(issueId) {
// 모달 표시
document.getElementById('issueDetailModal').classList.remove('hidden');
// AI 유사 부적합 자동 로드
const aiPanel = document.getElementById('aiSimilarPanel');
if (aiPanel) {
aiPanel.classList.remove('hidden');
loadSimilarIssues();
}
}
function closeIssueDetailModal() {
document.getElementById('issueDetailModal').classList.add('hidden');
const aiPanel = document.getElementById('aiSimilarPanel');
if (aiPanel) aiPanel.classList.add('hidden');
// RAG 결과 초기화
const suggestResult = document.getElementById('aiSuggestResult');
if (suggestResult) suggestResult.classList.add('hidden');
currentModalIssueId = null;
}
// RAG: AI 해결방안 제안
async function aiSuggestSolution() {
if (!currentModalIssueId || typeof AiAPI === 'undefined') return;
const btn = document.getElementById('aiSuggestSolutionBtn');
const loading = document.getElementById('aiSuggestLoading');
const result = document.getElementById('aiSuggestResult');
const content = document.getElementById('aiSuggestContent');
const sources = document.getElementById('aiSuggestSources');
if (btn) btn.disabled = true;
if (loading) loading.classList.remove('hidden');
if (result) result.classList.add('hidden');
const data = await AiAPI.suggestSolution(currentModalIssueId);
if (loading) loading.classList.add('hidden');
if (btn) btn.disabled = false;
if (!data.available) {
if (content) content.textContent = 'AI 서비스를 사용할 수 없습니다';
if (result) result.classList.remove('hidden');
return;
}
if (content) content.textContent = data.suggestion || '';
if (sources && data.referenced_issues) {
const refs = data.referenced_issues
.filter(r => r.has_solution)
.map(r => `No.${r.id}(${r.similarity}%)`)
.join(', ');
sources.textContent = refs ? `참고 사례: ${refs}` : '';
}
if (result) result.classList.remove('hidden');
}
// AI 유사 부적합 검색
async function loadSimilarIssues() {
if (!currentModalIssueId || typeof AiAPI === 'undefined') return;
const loading = document.getElementById('aiSimilarLoading');
const results = document.getElementById('aiSimilarResults');
const empty = document.getElementById('aiSimilarEmpty');
if (loading) loading.classList.remove('hidden');
if (results) results.innerHTML = '';
if (empty) empty.classList.add('hidden');
const data = await AiAPI.getSimilarIssues(currentModalIssueId, 5);
if (loading) loading.classList.add('hidden');
if (!data.available || !data.results || data.results.length === 0) {
if (empty) empty.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, 80);
const cat = meta.category || '';
return `
<div class="bg-purple-50 border border-purple-100 rounded-lg p-3 cursor-pointer hover:bg-purple-100 transition-colors"
onclick="openIssueDetailModal(${issueId})"
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-purple-700">No.${issueId}</span>
<span class="text-xs px-2 py-0.5 rounded-full ${similarity >= 70 ? 'bg-purple-200 text-purple-800' : 'bg-gray-200 text-gray-600'}">
${similarity}% 유사
</span>
</div>
<p class="text-xs text-gray-600 line-clamp-2">${doc}...</p>
${cat ? `<span class="text-xs text-purple-500 mt-1 inline-block">${cat}</span>` : ''}
</div>
`;
}).join('');
}
function createModalContent(issue, project) {
return `
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -1186,17 +1273,8 @@ function getPriorityBadge(priority) {
return `<span class="badge ${p.class}">${p.text}</span>`;
}
// API 스크립트 동적 로딩
const script = document.createElement('script');
script.src = '/static/js/api.js?v=20260213';
script.onload = function() {
console.log('✅ API 스크립트 로드 완료 (issues-management.js)');
initializeManagement();
};
script.onerror = function() {
console.error('❌ API 스크립트 로드 실패');
};
document.head.appendChild(script);
// 초기화 (api.js는 HTML에서 로드됨)
initializeManagement();
// 추가 정보 모달 관련 함수들
let selectedIssueId = null;