feat: KIMS 약물 상호작용 체크 기능 추가 (조제 탭 버튼 + 모달)
This commit is contained in:
parent
6a786ff042
commit
68dcb919e4
166
backend/app.py
166
backend/app.py
@ -4116,6 +4116,172 @@ def kill_process_on_port(port: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# KIMS 약물 상호작용 API
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@app.route('/api/kims/interaction-check', methods=['POST'])
|
||||
def api_kims_interaction_check():
|
||||
"""
|
||||
KIMS 약물 상호작용 체크 API
|
||||
|
||||
Request:
|
||||
{
|
||||
"drug_codes": ["055101150", "622801610"], // DrugCode 배열
|
||||
"pre_serial": "P20250630001" // 처방번호 (로깅용, optional)
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"interactions": [...],
|
||||
"safe_count": 2,
|
||||
"drugs_checked": [{"code": "...", "name": "...", "kd_code": "..."}]
|
||||
}
|
||||
"""
|
||||
import requests as http_requests
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
drug_codes = data.get('drug_codes', [])
|
||||
pre_serial = data.get('pre_serial', '')
|
||||
|
||||
if len(drug_codes) < 2:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '상호작용 체크를 위해 최소 2개 이상의 약품이 필요합니다.'
|
||||
}), 400
|
||||
|
||||
# 1. DrugCode → BASECODE(KIMS 9자리) 변환
|
||||
drug_session = db_manager.get_session('PM_DRUG')
|
||||
placeholders = ','.join([f"'{c}'" for c in drug_codes])
|
||||
|
||||
code_query = text(f"""
|
||||
SELECT DISTINCT G.DrugCode, G.GoodsName, B.BASECODE
|
||||
FROM CD_GOODS G
|
||||
LEFT JOIN CD_BARCODE B ON G.DrugCode = B.DrugCode
|
||||
WHERE G.DrugCode IN ({placeholders})
|
||||
""")
|
||||
code_result = drug_session.execute(code_query).fetchall()
|
||||
|
||||
# KIMS 코드 매핑
|
||||
kd_codes = []
|
||||
drugs_info = []
|
||||
for row in code_result:
|
||||
kd_code = row.BASECODE
|
||||
if kd_code:
|
||||
kd_codes.append(str(kd_code))
|
||||
drugs_info.append({
|
||||
'drug_code': row.DrugCode,
|
||||
'name': row.GoodsName[:50] if row.GoodsName else '알 수 없음',
|
||||
'kd_code': str(kd_code)
|
||||
})
|
||||
|
||||
if len(kd_codes) < 2:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'KIMS 코드로 변환 가능한 약품이 2개 미만입니다.',
|
||||
'drugs_checked': drugs_info
|
||||
}), 400
|
||||
|
||||
# 2. KIMS API 호출
|
||||
kims_url = "https://api2.kims.co.kr/api/interaction/info"
|
||||
kims_headers = {
|
||||
'Authorization': 'Basic VFNQTUtSOg==',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json; charset=utf-8'
|
||||
}
|
||||
kims_payload = {'KDCodes': kd_codes}
|
||||
|
||||
try:
|
||||
kims_response = http_requests.get(
|
||||
kims_url,
|
||||
headers=kims_headers,
|
||||
data=json.dumps(kims_payload),
|
||||
timeout=10,
|
||||
verify=False # SSL 검증 비활성화 (프로덕션에서는 주의)
|
||||
)
|
||||
|
||||
if kims_response.status_code != 200:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'KIMS API 응답 오류: HTTP {kims_response.status_code}',
|
||||
'drugs_checked': drugs_info
|
||||
}), 502
|
||||
|
||||
kims_data = kims_response.json()
|
||||
|
||||
if kims_data.get('Message') != 'SUCCESS':
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'KIMS API 처리 실패: {kims_data.get("Message", "알 수 없는 오류")}',
|
||||
'drugs_checked': drugs_info
|
||||
}), 502
|
||||
|
||||
except http_requests.Timeout:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'KIMS API 타임아웃 (10초 초과)',
|
||||
'drugs_checked': drugs_info
|
||||
}), 504
|
||||
except Exception as kims_err:
|
||||
logging.error(f"KIMS API 호출 실패: {kims_err}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'KIMS API 연결 실패: {str(kims_err)}',
|
||||
'drugs_checked': drugs_info
|
||||
}), 502
|
||||
|
||||
# 3. 상호작용 결과 파싱
|
||||
interactions = []
|
||||
severity_text = {1: '심각', 2: '중등도', 3: '경미', 4: '참고', 5: '일반'}
|
||||
severity_color = {1: '#dc2626', 2: '#f59e0b', 3: '#3b82f6', 4: '#6b7280', 5: '#9ca3af'}
|
||||
|
||||
for alert in kims_data.get('AlertList', []):
|
||||
for item in alert.get('AlertInfo', []):
|
||||
severity = item.get('SeverityLevel', 5)
|
||||
interactions.append({
|
||||
'drug1_code': item.get('DrugCode1'),
|
||||
'drug1_name': item.get('ProductName1'),
|
||||
'drug2_code': item.get('DrugCode2'),
|
||||
'drug2_name': item.get('ProductName2'),
|
||||
'generic1': item.get('GenericName1'),
|
||||
'generic2': item.get('GenericName2'),
|
||||
'severity': severity,
|
||||
'severity_text': severity_text.get(severity, '알 수 없음'),
|
||||
'severity_color': severity_color.get(severity, '#9ca3af'),
|
||||
'description': item.get('Observation', ''),
|
||||
'management': item.get('ClinicalMng', ''),
|
||||
'action': item.get('ActionToTake', ''),
|
||||
'likelihood': item.get('LikelihoodDesc', '')
|
||||
})
|
||||
|
||||
# 심각도 순 정렬 (1=심각이 먼저)
|
||||
interactions.sort(key=lambda x: x['severity'])
|
||||
|
||||
# 총 약품 쌍 수 계산
|
||||
total_pairs = len(kd_codes) * (len(kd_codes) - 1) // 2
|
||||
safe_count = total_pairs - len(interactions)
|
||||
|
||||
logging.info(f"KIMS 상호작용 체크 완료: {len(kd_codes)}개 약품, {len(interactions)}건 발견 (처방: {pre_serial})")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'interactions': interactions,
|
||||
'interaction_count': len(interactions),
|
||||
'safe_count': max(0, safe_count),
|
||||
'total_pairs': total_pairs,
|
||||
'drugs_checked': drugs_info
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"KIMS 상호작용 체크 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os
|
||||
|
||||
|
||||
@ -1038,6 +1038,10 @@
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 약품 코드 배열 (상호작용 체크용)
|
||||
const drugCodes = (rx.items || []).map(item => item.drug_code).filter(c => c);
|
||||
const drugCodesJson = JSON.stringify(drugCodes);
|
||||
|
||||
return `
|
||||
<div class="purchase-card" style="border-left: 3px solid #6366f1;">
|
||||
<div class="purchase-header">
|
||||
@ -1050,6 +1054,14 @@
|
||||
${rx.items && rx.items.length > 0 ? `
|
||||
<div class="purchase-items">${itemsHtml}</div>
|
||||
` : ''}
|
||||
${drugCodes.length >= 2 ? `
|
||||
<div style="margin-top:10px;text-align:right;">
|
||||
<button onclick='checkDrugInteraction(${drugCodesJson}, "${rx.pre_serial || ""}")'
|
||||
style="background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff;border:none;padding:8px 14px;border-radius:8px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;">
|
||||
🔬 AI 상호작용 체크
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@ -1111,6 +1123,153 @@
|
||||
|
||||
// 페이지 로드 시 검색창 포커스
|
||||
document.getElementById('searchInput').focus();
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// KIMS 약물 상호작용 체크
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
async function checkDrugInteraction(drugCodes, preSerial) {
|
||||
// 로딩 모달 표시
|
||||
showInteractionModal('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/kims/interaction-check', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
drug_codes: drugCodes,
|
||||
pre_serial: preSerial
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showInteractionModal('result', data);
|
||||
} else {
|
||||
showInteractionModal('error', data.error || '알 수 없는 오류');
|
||||
}
|
||||
} catch (err) {
|
||||
showInteractionModal('error', '서버 연결 실패: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showInteractionModal(type, data) {
|
||||
let modal = document.getElementById('interactionModal');
|
||||
if (!modal) {
|
||||
// 모달 생성
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'interactionModal';
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
||||
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
if (type === 'loading') {
|
||||
content = `
|
||||
<div style="background:#fff;border-radius:16px;padding:40px;text-align:center;max-width:400px;">
|
||||
<div style="font-size:48px;margin-bottom:16px;">🔬</div>
|
||||
<div style="font-size:18px;font-weight:600;color:#334155;">상호작용 분석 중...</div>
|
||||
<div style="font-size:14px;color:#64748b;margin-top:8px;">KIMS 데이터베이스 조회 중</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'error') {
|
||||
content = `
|
||||
<div style="background:#fff;border-radius:16px;padding:30px;max-width:400px;">
|
||||
<div style="font-size:40px;text-align:center;margin-bottom:16px;">⚠️</div>
|
||||
<div style="font-size:16px;font-weight:600;color:#dc2626;text-align:center;">분석 실패</div>
|
||||
<div style="font-size:14px;color:#64748b;margin-top:12px;text-align:center;">${escapeHtml(data)}</div>
|
||||
<div style="text-align:center;margin-top:20px;">
|
||||
<button onclick="document.getElementById('interactionModal').remove()"
|
||||
style="background:#6366f1;color:#fff;border:none;padding:10px 24px;border-radius:8px;cursor:pointer;">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'result') {
|
||||
const interactions = data.interactions || [];
|
||||
const drugsChecked = data.drugs_checked || [];
|
||||
|
||||
// 약품 목록
|
||||
const drugsHtml = drugsChecked.map(d =>
|
||||
`<span style="display:inline-block;background:#f1f5f9;padding:4px 8px;border-radius:4px;margin:2px;font-size:12px;">${escapeHtml(d.name.slice(0,20))}</span>`
|
||||
).join('');
|
||||
|
||||
// 상호작용 목록
|
||||
let interactionsHtml = '';
|
||||
if (interactions.length === 0) {
|
||||
interactionsHtml = `
|
||||
<div style="text-align:center;padding:30px;">
|
||||
<div style="font-size:48px;margin-bottom:12px;">✅</div>
|
||||
<div style="font-size:16px;font-weight:600;color:#10b981;">상호작용 없음</div>
|
||||
<div style="font-size:13px;color:#64748b;margin-top:8px;">
|
||||
${data.total_pairs}개 약품 조합을 검사했습니다.<br>
|
||||
주의가 필요한 상호작용이 발견되지 않았습니다.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
interactionsHtml = interactions.map(item => `
|
||||
<div style="background:#fff;border:1px solid ${item.severity_color};border-radius:12px;padding:16px;margin-bottom:12px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
||||
<span style="font-weight:600;color:#334155;">
|
||||
${escapeHtml(item.drug1_name?.slice(0,20) || '')} ↔ ${escapeHtml(item.drug2_name?.slice(0,20) || '')}
|
||||
</span>
|
||||
<span style="background:${item.severity_color};color:#fff;padding:4px 10px;border-radius:12px;font-size:12px;font-weight:500;">
|
||||
${item.severity_text}
|
||||
</span>
|
||||
</div>
|
||||
${item.description ? `
|
||||
<div style="font-size:13px;color:#475569;margin-bottom:8px;line-height:1.5;">
|
||||
📋 ${escapeHtml(item.description)}
|
||||
</div>
|
||||
` : ''}
|
||||
${item.management ? `
|
||||
<div style="font-size:12px;color:#059669;background:#ecfdf5;padding:8px 12px;border-radius:6px;">
|
||||
💡 ${escapeHtml(item.management)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
content = `
|
||||
<div style="background:#f8fafc;border-radius:20px;max-width:500px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column;">
|
||||
<div style="background:linear-gradient(135deg,#8b5cf6,#6366f1);padding:20px 24px;color:#fff;">
|
||||
<div style="font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;">
|
||||
🔬 약물 상호작용 분석
|
||||
</div>
|
||||
<div style="font-size:13px;opacity:0.9;margin-top:6px;">
|
||||
${drugsChecked.length}개 약품 · ${data.total_pairs}개 조합 검사
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px 20px;border-bottom:1px solid #e2e8f0;">
|
||||
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">분석 약품</div>
|
||||
${drugsHtml}
|
||||
</div>
|
||||
<div style="flex:1;overflow-y:auto;padding:16px 20px;">
|
||||
${interactions.length > 0 ? `
|
||||
<div style="font-size:13px;color:#dc2626;font-weight:600;margin-bottom:12px;">
|
||||
⚠️ ${interactions.length}건의 상호작용 발견
|
||||
</div>
|
||||
` : ''}
|
||||
${interactionsHtml}
|
||||
</div>
|
||||
<div style="padding:16px 20px;border-top:1px solid #e2e8f0;text-align:center;">
|
||||
<button onclick="document.getElementById('interactionModal').remove()"
|
||||
style="background:#6366f1;color:#fff;border:none;padding:12px 32px;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;">
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
modal.innerHTML = content;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user