feat: PAAI (Pharmacist Assistant AI) 기능 구현

- PAAI 로그 테이블 스키마 (paai_logs_schema.sql)
- PAAI 로거 모듈 (db/paai_logger.py)
- /pmr/api/paai/analyze API 엔드포인트
- KIMS API 연동 (KD코드 기반 상호작용 조회)
- Clawdbot AI 연동 (HTTP API)
- PMR 화면 PAAI 버튼 및 모달
- Admin 페이지 (/admin/paai)
- 피드백 수집 기능
This commit is contained in:
thug0bin
2026-03-05 00:36:51 +09:00
parent 141b211f07
commit 1b33f82fd4
6 changed files with 1755 additions and 0 deletions

View File

@@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PAAI 분석 로그 - 관리자</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
background: #f3f4f6;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 1.5rem; }
.header a { color: #fff; text-decoration: none; opacity: 0.8; }
.header a:hover { opacity: 1; }
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 통계 카드 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: #fff;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.stat-card .num { font-size: 2rem; font-weight: 700; color: #10b981; }
.stat-card .label { font-size: 0.85rem; color: #6b7280; margin-top: 5px; }
.stat-card.severe .num { color: #ef4444; }
/* 필터 */
.filters {
background: #fff;
padding: 15px 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.filters input, .filters select {
padding: 8px 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 0.9rem;
}
.filters input:focus, .filters select:focus {
outline: none;
border-color: #10b981;
}
.filters button {
padding: 8px 20px;
background: #10b981;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.filters button:hover { background: #059669; }
/* 로그 테이블 */
.log-table {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.log-table table {
width: 100%;
border-collapse: collapse;
}
.log-table th {
background: #f9fafb;
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
}
.log-table td {
padding: 12px 15px;
border-bottom: 1px solid #f3f4f6;
font-size: 0.9rem;
}
.log-table tr:hover { background: #f9fafb; cursor: pointer; }
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-success { background: #d1fae5; color: #065f46; }
.badge-error { background: #fee2e2; color: #991b1b; }
.badge-pending { background: #fef3c7; color: #92400e; }
.badge-severe { background: #fee2e2; color: #dc2626; }
.badge-caution { background: #fef3c7; color: #d97706; }
.feedback-icon { font-size: 1.1rem; }
/* 상세 모달 */
.modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1000;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal.show { display: flex; }
.modal-content {
background: #fff;
border-radius: 16px;
width: 100%;
max-width: 900px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
padding: 20px 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-close {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
}
.modal-body {
padding: 25px;
overflow-y: auto;
flex: 1;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h4 {
font-size: 0.95rem;
color: #374151;
margin-bottom: 10px;
border-bottom: 2px solid #10b981;
padding-bottom: 5px;
}
.detail-section pre {
background: #f9fafb;
padding: 15px;
border-radius: 8px;
font-size: 0.85rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.detail-item {
background: #f9fafb;
padding: 10px 15px;
border-radius: 8px;
}
.detail-item .label { font-size: 0.8rem; color: #6b7280; }
.detail-item .value { font-weight: 600; color: #111827; }
.loading {
text-align: center;
padding: 60px;
color: #9ca3af;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #10b981;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="header">
<h1>🤖 PAAI 분석 로그</h1>
<a href="/admin">← 관리자 홈</a>
</div>
<div class="container">
<!-- 통계 -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="num" id="statTotal">-</div>
<div class="label">전체</div>
</div>
<div class="stat-card">
<div class="num" id="statToday">-</div>
<div class="label">오늘</div>
</div>
<div class="stat-card">
<div class="num" id="statSuccessRate">-</div>
<div class="label">성공률</div>
</div>
<div class="stat-card">
<div class="num" id="statAvgTime">-</div>
<div class="label">평균 응답(ms)</div>
</div>
<div class="stat-card severe">
<div class="num" id="statSevere">-</div>
<div class="label">심각 상호작용</div>
</div>
<div class="stat-card">
<div class="num" id="statFeedback">-</div>
<div class="label">유용 피드백</div>
</div>
</div>
<!-- 필터 -->
<div class="filters">
<input type="date" id="filterDate" placeholder="날짜">
<select id="filterStatus">
<option value="">상태: 전체</option>
<option value="success">성공</option>
<option value="error">오류</option>
<option value="pending">대기중</option>
</select>
<select id="filterSevere">
<option value="">상호작용: 전체</option>
<option value="true">심각 있음</option>
<option value="false">심각 없음</option>
</select>
<button onclick="loadLogs()">🔍 조회</button>
<button onclick="loadLogs()" style="background:#6b7280;">🔄 새로고침</button>
</div>
<!-- 로그 테이블 -->
<div class="log-table">
<table>
<thead>
<tr>
<th>시간</th>
<th>환자</th>
<th>처방번호</th>
<th>약품수</th>
<th>KIMS</th>
<th>상태</th>
<th>응답시간</th>
<th>피드백</th>
</tr>
</thead>
<tbody id="logTableBody">
<tr><td colspan="8" class="loading"><div class="spinner"></div>로딩 중...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- 상세 모달 -->
<div class="modal" id="detailModal">
<div class="modal-content">
<div class="modal-header">
<h3>📋 분석 상세</h3>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body" id="modalBody">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
<script>
// 페이지 로드
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadLogs();
});
// 통계 로드
async function loadStats() {
try {
const res = await fetch('/api/paai/logs/stats');
const data = await res.json();
if (data.success) {
const s = data.stats;
document.getElementById('statTotal').textContent = s.total;
document.getElementById('statToday').textContent = s.today;
document.getElementById('statSuccessRate').textContent = s.success_rate + '%';
document.getElementById('statAvgTime').textContent = s.avg_response_time;
document.getElementById('statSevere').textContent = s.severe_count;
document.getElementById('statFeedback').textContent =
s.feedback ? `${s.feedback.useful}/${s.feedback.total}` : '0/0';
}
} catch (err) {
console.error('Stats error:', err);
}
}
// 로그 로드
async function loadLogs() {
const tbody = document.getElementById('logTableBody');
tbody.innerHTML = '<tr><td colspan="8" class="loading"><div class="spinner"></div>로딩 중...</td></tr>';
try {
const date = document.getElementById('filterDate').value;
const status = document.getElementById('filterStatus').value;
const severe = document.getElementById('filterSevere').value;
const params = new URLSearchParams();
if (date) params.append('date', date);
if (status) params.append('status', status);
if (severe) params.append('has_severe', severe);
params.append('limit', '100');
const res = await fetch(`/api/paai/logs?${params}`);
const data = await res.json();
if (data.success) {
if (data.logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#9ca3af;padding:40px;">로그가 없습니다.</td></tr>';
return;
}
tbody.innerHTML = data.logs.map(log => {
const time = new Date(log.created_at).toLocaleString('ko-KR', {
month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const statusBadge = {
'success': '<span class="badge badge-success">성공</span>',
'error': '<span class="badge badge-error">오류</span>',
'pending': '<span class="badge badge-pending">대기</span>',
'kims_done': '<span class="badge badge-pending">AI대기</span>'
}[log.status] || log.status;
const kimsBadge = log.kims_has_severe
? `<span class="badge badge-severe">🔴 ${log.kims_interaction_count}건</span>`
: log.kims_interaction_count > 0
? `<span class="badge badge-caution">⚠️ ${log.kims_interaction_count}건</span>`
: '<span style="color:#9ca3af;">-</span>';
const feedback = log.feedback_useful === 1 ? '👍'
: log.feedback_useful === 0 ? '👎' : '-';
return `
<tr onclick="showDetail(${log.id})">
<td>${time}</td>
<td>${log.patient_name || '-'}</td>
<td>${log.pre_serial || '-'}</td>
<td>${log.current_med_count || 0}종</td>
<td>${kimsBadge}</td>
<td>${statusBadge}</td>
<td>${log.ai_response_time_ms || '-'}ms</td>
<td class="feedback-icon">${feedback}</td>
</tr>
`;
}).join('');
}
} catch (err) {
console.error('Logs error:', err);
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#ef4444;">로드 실패</td></tr>';
}
}
// 상세 보기
async function showDetail(logId) {
const modal = document.getElementById('detailModal');
const body = document.getElementById('modalBody');
body.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
modal.classList.add('show');
try {
const res = await fetch(`/api/paai/logs/${logId}`);
const data = await res.json();
if (data.success) {
const log = data.log;
body.innerHTML = `
<div class="detail-section">
<h4>📌 기본 정보</h4>
<div class="detail-grid">
<div class="detail-item">
<div class="label">환자</div>
<div class="value">${log.patient_name || '-'} (${log.patient_code || '-'})</div>
</div>
<div class="detail-item">
<div class="label">처방번호</div>
<div class="value">${log.pre_serial || '-'}</div>
</div>
<div class="detail-item">
<div class="label">질병1</div>
<div class="value">[${log.disease_code_1 || '-'}] ${log.disease_name_1 || '-'}</div>
</div>
<div class="detail-item">
<div class="label">질병2</div>
<div class="value">[${log.disease_code_2 || '-'}] ${log.disease_name_2 || '-'}</div>
</div>
</div>
</div>
<div class="detail-section">
<h4>💊 현재 처방 (${log.current_med_count || 0}종)</h4>
<pre>${JSON.stringify(log.current_medications, null, 2)}</pre>
</div>
<div class="detail-section">
<h4>⚠️ KIMS 상호작용 (${log.kims_interaction_count || 0}건)</h4>
<pre>${JSON.stringify(log.kims_interactions, null, 2)}</pre>
</div>
<div class="detail-section">
<h4>🤖 AI 분석 결과</h4>
<pre>${JSON.stringify(log.ai_response, null, 2)}</pre>
</div>
<div class="detail-section">
<h4>📊 성능</h4>
<div class="detail-grid">
<div class="detail-item">
<div class="label">KIMS 응답</div>
<div class="value">${log.kims_response_time_ms || '-'}ms</div>
</div>
<div class="detail-item">
<div class="label">AI 응답</div>
<div class="value">${log.ai_response_time_ms || '-'}ms</div>
</div>
<div class="detail-item">
<div class="label">상태</div>
<div class="value">${log.status}</div>
</div>
<div class="detail-item">
<div class="label">피드백</div>
<div class="value">${log.feedback_useful === 1 ? '👍 유용' : log.feedback_useful === 0 ? '👎 아님' : '미응답'}</div>
</div>
</div>
</div>
`;
}
} catch (err) {
console.error('Detail error:', err);
body.innerHTML = '<div style="text-align:center;color:#ef4444;">로드 실패</div>';
}
}
function closeModal() {
document.getElementById('detailModal').classList.remove('show');
}
// ESC로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
</script>
</body>
</html>

View File

@@ -161,6 +161,167 @@
transform: scale(1.05);
}
/* PAAI 버튼 */
.detail-header .rx-info .paai-badge {
background: linear-gradient(135deg, #10b981, #059669) !important;
color: #fff !important;
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
}
.detail-header .rx-info .paai-badge:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
/* PAAI 모달 */
.paai-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1100;
justify-content: center;
align-items: center;
padding: 20px;
}
.paai-modal.show { display: flex; }
.paai-modal-content {
background: #fff;
border-radius: 16px;
width: 100%;
max-width: 700px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
}
.paai-modal-header {
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
padding: 20px 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.paai-modal-header h3 { font-size: 1.3rem; }
.paai-modal-close {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
transition: background 0.2s;
}
.paai-modal-close:hover { background: rgba(255,255,255,0.3); }
.paai-modal-body {
padding: 25px;
overflow-y: auto;
flex: 1;
}
.paai-loading {
text-align: center;
padding: 60px 20px;
}
.paai-loading .spinner {
width: 50px;
height: 50px;
border: 4px solid #e5e7eb;
border-top-color: #10b981;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.paai-section {
margin-bottom: 24px;
}
.paai-section-title {
font-size: 1rem;
font-weight: 700;
color: #374151;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.paai-section-content {
background: #f9fafb;
padding: 15px;
border-radius: 10px;
font-size: 0.95rem;
line-height: 1.6;
color: #4b5563;
}
.paai-list {
list-style: none;
padding: 0;
margin: 0;
}
.paai-list li {
padding: 8px 0;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: flex-start;
gap: 10px;
}
.paai-list li:last-child { border-bottom: none; }
.paai-list li::before {
content: '•';
color: #10b981;
font-weight: bold;
}
.paai-caution {
background: #fef3c7 !important;
border-left: 4px solid #f59e0b;
}
.paai-otc-rec {
background: #dbeafe !important;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 10px;
}
.paai-otc-rec .product { font-weight: 600; color: #1e40af; }
.paai-otc-rec .reason { font-size: 0.9rem; color: #64748b; margin-top: 4px; }
.paai-kims-severe {
background: #fee2e2 !important;
border-left: 4px solid #ef4444;
}
.paai-modal-footer {
padding: 15px 25px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.paai-feedback {
display: flex;
gap: 10px;
align-items: center;
}
.paai-feedback span { font-size: 0.9rem; color: #6b7280; }
.paai-feedback button {
padding: 8px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.paai-feedback button:hover { border-color: #10b981; }
.paai-feedback button.selected { background: #d1fae5; border-color: #10b981; }
.paai-timing {
font-size: 0.8rem;
color: #9ca3af;
}
/* OTC 모달 */
.otc-modal {
display: none;
@@ -592,6 +753,31 @@
<button class="btn btn-primary" onclick="printLabels()">🖨️ 라벨 인쇄</button>
</div>
<!-- PAAI 분석 모달 -->
<div class="paai-modal" id="paaiModal">
<div class="paai-modal-content">
<div class="paai-modal-header">
<h3>🤖 PAAI 분석 결과</h3>
<button class="paai-modal-close" onclick="closePaaiModal()">×</button>
</div>
<div class="paai-modal-body" id="paaiBody">
<div class="paai-loading">
<div class="spinner"></div>
<div>AI 분석 중...</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">KIMS 상호작용 확인 + AI 분석</div>
</div>
</div>
<div class="paai-modal-footer" id="paaiFooter" style="display:none;">
<div class="paai-feedback">
<span>도움이 되셨나요?</span>
<button onclick="sendPaaiFeedback(true)" id="paaiUseful">👍 유용해요</button>
<button onclick="sendPaaiFeedback(false)" id="paaiNotUseful">👎 아니요</button>
</div>
<div class="paai-timing" id="paaiTiming"></div>
</div>
</div>
</div>
<!-- OTC 구매 이력 모달 -->
<div class="otc-modal" id="otcModal">
<div class="otc-modal-content">
@@ -1096,9 +1282,14 @@
} else {
otcData = null;
}
// PAAI 버튼 추가 (항상 표시)
addPaaiButton();
} catch (err) {
console.error('OTC check error:', err);
otcData = null;
// OTC 오류여도 PAAI 버튼은 추가
addPaaiButton();
}
}
@@ -1171,6 +1362,230 @@
document.getElementById('otcModal').style.display = 'none';
}
// ─────────────────────────────────────────────────────────────
// PAAI (Pharmacist Assistant AI) 함수들
// ─────────────────────────────────────────────────────────────
let currentPaaiLogId = null;
function addPaaiButton() {
const rxInfo = document.getElementById('rxInfo');
if (!rxInfo || rxInfo.querySelector('.paai-badge')) return;
const paaiBtn = document.createElement('span');
paaiBtn.className = 'paai-badge';
paaiBtn.textContent = '🤖 PAAI 분석';
paaiBtn.onclick = showPaaiModal;
rxInfo.appendChild(paaiBtn);
}
async function showPaaiModal() {
if (!currentPrescription) return;
const modal = document.getElementById('paaiModal');
const body = document.getElementById('paaiBody');
const footer = document.getElementById('paaiFooter');
// 초기화
body.innerHTML = `
<div class="paai-loading">
<div class="spinner"></div>
<div>AI 분석 중...</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">KIMS 상호작용 확인 + AI 분석</div>
</div>
`;
footer.style.display = 'none';
modal.classList.add('show');
try {
// 요청 데이터 구성
const requestData = {
pre_serial: currentPrescription.pre_serial,
cus_code: currentPrescription.cus_code,
patient_name: currentPrescription.name,
disease_info: {
code_1: currentPrescription.st1 || '',
name_1: currentPrescription.st1_name || '',
code_2: currentPrescription.st2 || '',
name_2: currentPrescription.st2_name || ''
},
current_medications: (currentPrescription.medications || []).map(med => ({
code: med.medication_code,
name: med.med_name,
dosage: med.dosage,
frequency: med.frequency,
days: med.days
})),
previous_serial: currentPrescription.previous_serial || '',
previous_medications: (currentPrescription.previous_medications || []).map(med => ({
code: med.medication_code,
name: med.med_name,
dosage: med.dosage,
frequency: med.frequency,
days: med.days
})),
otc_history: otcData ? {
visit_count: otcData.summary?.total_visits || 0,
frequent_items: otcData.summary?.frequent_items || [],
purchases: otcData.purchases || []
} : {}
};
const response = await fetch('/pmr/api/paai/analyze', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(requestData)
});
const result = await response.json();
if (result.success) {
currentPaaiLogId = result.log_id;
displayPaaiResult(result);
} else {
throw new Error(result.error || '분석 실패');
}
} catch (err) {
console.error('PAAI error:', err);
body.innerHTML = `
<div style="text-align:center;padding:40px;color:#ef4444;">
<div style="font-size:2rem;margin-bottom:15px;">⚠️</div>
<div>분석 중 오류가 발생했습니다</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">${err.message}</div>
</div>
`;
}
}
function displayPaaiResult(result) {
const body = document.getElementById('paaiBody');
const footer = document.getElementById('paaiFooter');
const timing = document.getElementById('paaiTiming');
const analysis = result.analysis || {};
const kims = result.kims_summary || {};
let html = '';
// KIMS 상호작용 요약
if (kims.interaction_count > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">⚠️ KIMS 상호작용 (${kims.interaction_count}건)</div>
<div class="paai-section-content ${kims.has_severe ? 'paai-kims-severe' : 'paai-caution'}">
${analysis.kims_analysis || 'KIMS 상호작용이 감지되었습니다. 상세 내용을 확인하세요.'}
</div>
</div>
`;
}
// 처방 분석
if (analysis.prescription_insight) {
html += `
<div class="paai-section">
<div class="paai-section-title">📋 처방 분석</div>
<div class="paai-section-content">${analysis.prescription_insight}</div>
</div>
`;
}
// 복용 주의사항
if (analysis.cautions && analysis.cautions.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">⚡ 복용 주의사항</div>
<div class="paai-section-content paai-caution">
<ul class="paai-list">
${analysis.cautions.map(c => `<li>${c}</li>`).join('')}
</ul>
</div>
</div>
`;
}
// OTC 추천
if (analysis.otc_recommendations && analysis.otc_recommendations.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">💊 OTC 추천</div>
<div>
${analysis.otc_recommendations.map(rec => `
<div class="paai-otc-rec">
<div class="product">${rec.product}</div>
<div class="reason">${rec.reason}</div>
</div>
`).join('')}
</div>
</div>
`;
}
// 상담 포인트
if (analysis.counseling_points && analysis.counseling_points.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">💬 상담 포인트</div>
<div class="paai-section-content">
<ul class="paai-list">
${analysis.counseling_points.map(p => `<li>${p}</li>`).join('')}
</ul>
</div>
</div>
`;
}
// fallback 메시지
if (analysis._fallback) {
html += `
<div style="text-align:center;padding:20px;color:#9ca3af;font-size:0.9rem;">
⚠️ AI 서비스 연결 불가 - KIMS 데이터만 표시됨
</div>
`;
}
body.innerHTML = html || '<div style="text-align:center;padding:40px;color:#9ca3af;">분석 결과가 없습니다.</div>';
// 타이밍 정보
if (result.timing) {
timing.textContent = `KIMS: ${result.timing.kims_ms}ms / AI: ${result.timing.ai_ms}ms / 총: ${result.timing.total_ms}ms`;
}
// 피드백 버튼 초기화
document.getElementById('paaiUseful').classList.remove('selected');
document.getElementById('paaiNotUseful').classList.remove('selected');
footer.style.display = 'flex';
}
function closePaaiModal() {
document.getElementById('paaiModal').classList.remove('show');
}
async function sendPaaiFeedback(useful) {
if (!currentPaaiLogId) return;
try {
await fetch('/pmr/api/paai/feedback', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
log_id: currentPaaiLogId,
useful: useful
})
});
// 버튼 표시 업데이트
document.getElementById('paaiUseful').classList.toggle('selected', useful);
document.getElementById('paaiNotUseful').classList.toggle('selected', !useful);
} catch (err) {
console.error('Feedback error:', err);
}
}
// ─────────────────────────────────────────────────────────────
// 상세 초기화
function clearDetail() {
document.getElementById('detailHeader').style.display = 'none';