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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user