refactor: TKQC AI 기능 재배치

- 현황판: AI 시맨틱 검색/Q&A 제거 (AI 어시스턴트로 이관)
- 관리함 진행중 카드: AI 해결방안 제안 버튼 추가
- aiSuggestSolutionInline() 인라인 카드용 함수 추가
- applyAiSuggestion() AI 제안 → textarea 적용 기능

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-07 12:52:41 +09:00
parent e6cc466a0e
commit 9647ae0d56
3 changed files with 54 additions and 189 deletions

View File

@@ -1786,130 +1786,5 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// 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();

View File

@@ -456,6 +456,18 @@ function createInProgressRow(issue, project) {
<i class="fas fa-lightbulb text-yellow-500 mr-1"></i>해결방안 (확정)
</label>
<textarea id="management_comment_${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" placeholder="확정된 해결 방안을 입력하세요..." ${isPendingCompletion ? 'readonly' : ''}>${cleanManagementComment(issue.management_comment)}</textarea>
${!isPendingCompletion ? `
<button onclick="aiSuggestSolutionInline(${issue.id})" class="mt-2 w-full px-3 py-2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white text-sm rounded-lg hover:from-purple-600 hover:to-indigo-600 transition-all">
<i class="fas fa-lightbulb mr-2"></i>AI 해결방안 제안 (과거 사례 기반)
</button>
<div id="aiSuggestResult_${issue.id}" class="hidden mt-2 p-3 bg-purple-50 border border-purple-200 rounded-lg">
<p id="aiSuggestContent_${issue.id}" class="text-sm text-gray-800 whitespace-pre-wrap"></p>
<p id="aiSuggestSources_${issue.id}" class="text-xs text-purple-600 mt-2"></p>
<button onclick="applyAiSuggestion(${issue.id})" class="mt-2 text-xs px-2 py-1 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors">
<i class="fas fa-paste mr-1"></i>해결방안에 적용
</button>
</div>
` : ''}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -984,6 +996,48 @@ async function aiSuggestSolution() {
if (result) result.classList.remove('hidden');
}
// RAG: AI 해결방안 제안 (인라인 카드용)
async function aiSuggestSolutionInline(issueId) {
if (typeof AiAPI === 'undefined') return;
const btn = event.target.closest('button');
const result = document.getElementById(`aiSuggestResult_${issueId}`);
const content = document.getElementById(`aiSuggestContent_${issueId}`);
const sources = document.getElementById(`aiSuggestSources_${issueId}`);
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>AI 분석 중...'; }
if (result) result.classList.add('hidden');
const data = await AiAPI.suggestSolution(issueId);
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-lightbulb mr-2"></i>AI 해결방안 제안 (과거 사례 기반)'; }
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 제안 → 해결방안 textarea에 적용
function applyAiSuggestion(issueId) {
const content = document.getElementById(`aiSuggestContent_${issueId}`);
const textarea = document.getElementById(`management_comment_${issueId}`);
if (content && textarea) {
textarea.value = content.textContent;
textarea.focus();
}
}
// AI 유사 부적합 검색
async function loadSimilarIssues() {
if (!currentModalIssueId || typeof AiAPI === 'undefined') return;