diff --git a/backend/app.py b/backend/app.py index 8449cb8..005b878 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4140,11 +4140,16 @@ def api_kims_interaction_check(): } """ import requests as http_requests + from db.kims_logger import log_kims_call + import time as time_module + + start_time = time_module.time() try: data = request.get_json() drug_codes = data.get('drug_codes', []) pre_serial = data.get('pre_serial', '') + user_id = data.get('user_id') # 회원 ID (있으면) if len(drug_codes) < 2: return jsonify({ @@ -4202,6 +4207,10 @@ def api_kims_interaction_check(): ) if kims_response.status_code != 200: + log_kims_call(pre_serial=pre_serial, drug_codes=kd_codes, drug_names=[d['name'] for d in drugs_info], + api_status='ERROR', http_status=kims_response.status_code, + response_time_ms=int((time_module.time() - start_time) * 1000), + error_message=f'HTTP {kims_response.status_code}') return jsonify({ 'success': False, 'error': f'KIMS API 응답 오류: HTTP {kims_response.status_code}', @@ -4211,6 +4220,10 @@ def api_kims_interaction_check(): kims_data = kims_response.json() if kims_data.get('Message') != 'SUCCESS': + log_kims_call(pre_serial=pre_serial, drug_codes=kd_codes, drug_names=[d['name'] for d in drugs_info], + api_status='ERROR', http_status=200, + response_time_ms=int((time_module.time() - start_time) * 1000), + error_message=f'KIMS: {kims_data.get("Message")}', response_raw=kims_data) return jsonify({ 'success': False, 'error': f'KIMS API 처리 실패: {kims_data.get("Message", "알 수 없는 오류")}', @@ -4218,12 +4231,17 @@ def api_kims_interaction_check(): }), 502 except http_requests.Timeout: + log_kims_call(pre_serial=pre_serial, drug_codes=kd_codes, drug_names=[d['name'] for d in drugs_info], + api_status='TIMEOUT', response_time_ms=10000, error_message='10초 초과') return jsonify({ 'success': False, 'error': 'KIMS API 타임아웃 (10초 초과)', 'drugs_checked': drugs_info }), 504 except Exception as kims_err: + log_kims_call(pre_serial=pre_serial, drug_codes=kd_codes, drug_names=[d['name'] for d in drugs_info], + api_status='ERROR', response_time_ms=int((time_module.time() - start_time) * 1000), + error_message=str(kims_err)) logging.error(f"KIMS API 호출 실패: {kims_err}") return jsonify({ 'success': False, @@ -4276,6 +4294,27 @@ def api_kims_interaction_check(): for drug in drugs_info: drug['has_interaction'] = drug['kd_code'] in interaction_drug_codes + # 응답 시간 계산 + response_time_ms = int((time_module.time() - start_time) * 1000) + + # SQLite 로깅 + try: + log_id = log_kims_call( + pre_serial=pre_serial, + user_id=user_id, + source='admin', + drug_codes=kd_codes, + drug_names=[d['name'] for d in drugs_info], + api_status='SUCCESS', + http_status=200, + response_time_ms=response_time_ms, + interactions=interactions, + response_raw=kims_data + ) + logging.info(f"KIMS 로그 저장: ID={log_id}, {len(kd_codes)}개 약품, {len(interactions)}건 상호작용") + except Exception as log_err: + logging.warning(f"KIMS 로깅 실패 (무시): {log_err}") + logging.info(f"KIMS 상호작용 체크 완료: {len(kd_codes)}개 약품, {len(interactions)}건 발견 (처방: {pre_serial})") return jsonify({ @@ -4289,6 +4328,20 @@ def api_kims_interaction_check(): }) except Exception as e: + # 에러 로깅 + response_time_ms = int((time_module.time() - start_time) * 1000) + try: + log_kims_call( + pre_serial=pre_serial if 'pre_serial' in dir() else None, + source='admin', + drug_codes=drug_codes if 'drug_codes' in dir() else [], + api_status='ERROR', + response_time_ms=response_time_ms, + error_message=str(e) + ) + except: + pass + logging.error(f"KIMS 상호작용 체크 오류: {e}") return jsonify({ 'success': False, diff --git a/backend/db/kims_logger.py b/backend/db/kims_logger.py new file mode 100644 index 0000000..e71c43f --- /dev/null +++ b/backend/db/kims_logger.py @@ -0,0 +1,220 @@ +""" +KIMS API 로깅 모듈 +- API 호출/응답 SQLite 저장 +- AI 학습용 데이터 수집 +""" +import sqlite3 +import json +import os +from datetime import datetime +from pathlib import Path + +# DB 파일 경로 +DB_PATH = Path(__file__).parent / 'kims_logs.db' + +def init_db(): + """DB 초기화 (테이블 생성)""" + schema_path = Path(__file__).parent / 'kims_logs_schema.sql' + + conn = sqlite3.connect(str(DB_PATH)) + cursor = conn.cursor() + + with open(schema_path, 'r', encoding='utf-8') as f: + schema = f.read() + cursor.executescript(schema) + + conn.commit() + conn.close() + print(f"KIMS 로그 DB 초기화 완료: {DB_PATH}") + +def log_kims_call( + pre_serial: str = None, + user_id: int = None, + source: str = 'admin', + drug_codes: list = None, + drug_names: list = None, + api_status: str = 'SUCCESS', + http_status: int = 200, + response_time_ms: int = 0, + interactions: list = None, + response_raw: dict = None, + error_message: str = None +) -> int: + """ + KIMS API 호출 로그 저장 + + Returns: + log_id: 생성된 로그 ID + """ + # DB 없으면 초기화 + if not DB_PATH.exists(): + init_db() + + conn = sqlite3.connect(str(DB_PATH)) + cursor = conn.cursor() + + interactions = interactions or [] + drug_codes = drug_codes or [] + drug_names = drug_names or [] + + # 심각한 상호작용 여부 (severity 1 또는 2) + has_severe = any( + str(i.get('severity', '5')) in ['1', '2'] + for i in interactions + ) + + # 메인 로그 삽입 + cursor.execute(""" + INSERT INTO kims_api_logs ( + pre_serial, user_id, source, + request_drug_codes, request_drug_names, request_drug_count, + api_status, http_status, response_time_ms, + interaction_count, has_severe_interaction, + interactions_json, response_raw, error_message + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + pre_serial, + user_id, + source, + json.dumps(drug_codes, ensure_ascii=False), + json.dumps(drug_names, ensure_ascii=False), + len(drug_codes), + api_status, + http_status, + response_time_ms, + len(interactions), + 1 if has_severe else 0, + json.dumps(interactions, ensure_ascii=False), + json.dumps(response_raw, ensure_ascii=False) if response_raw else None, + error_message + )) + + log_id = cursor.lastrowid + + # 상호작용 상세 삽입 (정규화) + for inter in interactions: + cursor.execute(""" + INSERT INTO kims_interactions ( + log_id, + drug1_code, drug1_name, drug1_generic, + drug2_code, drug2_name, drug2_generic, + severity_level, severity_desc, + likelihood_level, likelihood_desc, + observation, observation_generic, + clinical_management, action_to_take, reference + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + log_id, + inter.get('drug1_code'), + inter.get('drug1_name'), + inter.get('generic1'), + inter.get('drug2_code'), + inter.get('drug2_name'), + inter.get('generic2'), + int(inter.get('severity', 5)) if str(inter.get('severity', '')).isdigit() else None, + inter.get('severity_text'), + None, # likelihood_level + inter.get('likelihood'), + inter.get('description'), + None, # observation_generic + inter.get('management'), + inter.get('action'), + None # reference + )) + + conn.commit() + conn.close() + + return log_id + +def get_recent_logs(limit: int = 50): + """최근 로그 조회""" + if not DB_PATH.exists(): + return [] + + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT * FROM kims_api_logs + ORDER BY created_at DESC + LIMIT ? + """, (limit,)) + + rows = cursor.fetchall() + conn.close() + + return [dict(row) for row in rows] + +def get_log_detail(log_id: int): + """로그 상세 조회 (상호작용 포함)""" + if not DB_PATH.exists(): + return None + + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 메인 로그 + cursor.execute("SELECT * FROM kims_api_logs WHERE id = ?", (log_id,)) + log = cursor.fetchone() + + if not log: + conn.close() + return None + + # 상호작용 상세 + cursor.execute(""" + SELECT * FROM kims_interactions + WHERE log_id = ? + ORDER BY severity_level ASC + """, (log_id,)) + interactions = cursor.fetchall() + + conn.close() + + result = dict(log) + result['interactions_detail'] = [dict(i) for i in interactions] + + return result + +def get_stats(): + """통계 조회""" + if not DB_PATH.exists(): + return {} + + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 전체 통계 + cursor.execute(""" + SELECT + COUNT(*) as total_calls, + SUM(CASE WHEN api_status = 'SUCCESS' THEN 1 ELSE 0 END) as success_count, + SUM(CASE WHEN interaction_count > 0 THEN 1 ELSE 0 END) as with_interaction, + SUM(CASE WHEN has_severe_interaction = 1 THEN 1 ELSE 0 END) as with_severe, + AVG(response_time_ms) as avg_response_ms + FROM kims_api_logs + """) + stats = dict(cursor.fetchone()) + + # 최근 7일 일별 통계 + cursor.execute(""" + SELECT * FROM kims_stats + ORDER BY date DESC + LIMIT 7 + """) + daily = [dict(row) for row in cursor.fetchall()] + + conn.close() + + stats['daily'] = daily + return stats + + +if __name__ == '__main__': + # DB 초기화 테스트 + init_db() + print("KIMS 로그 DB 초기화 완료!") diff --git a/backend/db/kims_logs_schema.sql b/backend/db/kims_logs_schema.sql new file mode 100644 index 0000000..bc7b81a --- /dev/null +++ b/backend/db/kims_logs_schema.sql @@ -0,0 +1,86 @@ +-- KIMS API 로그 테이블 스키마 +-- AI 학습 데이터로 활용 예정 + +-- 1. API 호출 로그 (메인) +CREATE TABLE IF NOT EXISTS kims_api_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 호출 컨텍스트 + pre_serial TEXT, -- 처방번호 + user_id INTEGER, -- 마일리지 회원 ID (있으면) + source TEXT DEFAULT 'admin', -- 호출 소스 (admin, api, batch 등) + + -- 요청 데이터 + request_drug_codes TEXT NOT NULL, -- JSON: ["055101150", "622801610"] + request_drug_names TEXT, -- JSON: ["오메프투캡슐", "락소펜엠정"] + request_drug_count INTEGER, -- 요청 약품 수 + + -- 응답 데이터 + api_status TEXT NOT NULL, -- SUCCESS, ERROR, TIMEOUT + http_status INTEGER, -- HTTP 상태 코드 + response_time_ms INTEGER, -- 응답 시간 (밀리초) + + -- 상호작용 결과 + interaction_count INTEGER DEFAULT 0, -- 발견된 상호작용 수 + has_severe_interaction INTEGER DEFAULT 0, -- 심각한 상호작용 여부 (1/2 등급) + + -- 상세 데이터 (JSON) + interactions_json TEXT, -- 상호작용 상세 정보 JSON + response_raw TEXT, -- 전체 API 응답 (디버깅/학습용) + + -- 에러 정보 + error_message TEXT +); + +-- 2. 상호작용 상세 (정규화, AI 학습용) +CREATE TABLE IF NOT EXISTS kims_interactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + log_id INTEGER NOT NULL, -- kims_api_logs.id FK + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 약품 1 + drug1_code TEXT NOT NULL, + drug1_name TEXT, + drug1_generic TEXT, -- 성분명 (영문) + + -- 약품 2 + drug2_code TEXT NOT NULL, + drug2_name TEXT, + drug2_generic TEXT, -- 성분명 (영문) + + -- 상호작용 정보 + severity_level INTEGER, -- 1=심각, 2=중등도, 3=경미, 4=참고 + severity_desc TEXT, -- 심각도 설명 (중증, 경미 등) + likelihood_level INTEGER, -- 발생 가능성 + likelihood_desc TEXT, + + -- 상세 설명 (AI 학습 핵심 데이터) + observation TEXT, -- 상호작용 설명 (한글) + observation_generic TEXT, -- 일반적 설명 + clinical_management TEXT, -- 임상적 관리 방법 + action_to_take TEXT, -- 권장 조치 + reference TEXT, -- 참고문헌 + + FOREIGN KEY (log_id) REFERENCES kims_api_logs(id) +); + +-- 인덱스 +CREATE INDEX IF NOT EXISTS idx_kims_logs_created ON kims_api_logs(created_at); +CREATE INDEX IF NOT EXISTS idx_kims_logs_pre_serial ON kims_api_logs(pre_serial); +CREATE INDEX IF NOT EXISTS idx_kims_logs_status ON kims_api_logs(api_status); +CREATE INDEX IF NOT EXISTS idx_kims_interactions_log ON kims_interactions(log_id); +CREATE INDEX IF NOT EXISTS idx_kims_interactions_drugs ON kims_interactions(drug1_code, drug2_code); +CREATE INDEX IF NOT EXISTS idx_kims_interactions_severity ON kims_interactions(severity_level); + +-- 통계 뷰 +CREATE VIEW IF NOT EXISTS kims_stats AS +SELECT + DATE(created_at) as date, + COUNT(*) as total_calls, + SUM(CASE WHEN api_status = 'SUCCESS' THEN 1 ELSE 0 END) as success_count, + SUM(CASE WHEN interaction_count > 0 THEN 1 ELSE 0 END) as with_interaction, + SUM(CASE WHEN has_severe_interaction = 1 THEN 1 ELSE 0 END) as with_severe, + AVG(response_time_ms) as avg_response_ms +FROM kims_api_logs +GROUP BY DATE(created_at);