Commit
Β·
0136146
1
Parent(s):
41a320c
feat: Add database models and services for patient data, scenarios, chat history, and handoff records. Implement database connection settings and CRUD operations for managing patient and scenario data.
Browse files- config/__init__.py +0 -0
- config/database.py +53 -0
- data/scenarios.json +286 -0
- models/__init__.py +0 -0
- models/chat_history.py +44 -0
- models/handoff_record.py +42 -0
- models/patient_data.py +65 -0
- services/__init__.py +0 -0
- services/db_service.py +296 -0
- services/gemini_service.py +382 -0
config/__init__.py
ADDED
|
File without changes
|
config/database.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
λ°μ΄ν°λ² μ΄μ€ μ°κ²° μ€μ
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from sqlalchemy import create_engine
|
| 9 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 10 |
+
from sqlalchemy.orm import sessionmaker
|
| 11 |
+
|
| 12 |
+
# νκ²½ λ³μ λ‘λ
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# λ°μ΄ν°λ² μ΄μ€ URL ꡬμ±
|
| 16 |
+
DB_HOST = os.getenv("DB_HOST", "localhost")
|
| 17 |
+
DB_PORT = os.getenv("DB_PORT", "5433")
|
| 18 |
+
DB_NAME = os.getenv("DB_NAME", "nursing_handoff")
|
| 19 |
+
DB_USER = os.getenv("DB_USER", "postgres")
|
| 20 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres123")
|
| 21 |
+
|
| 22 |
+
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
| 23 |
+
|
| 24 |
+
# SQLAlchemy μμ§ λ° μΈμ
μμ±
|
| 25 |
+
engine = create_engine(DATABASE_URL, echo=True)
|
| 26 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 27 |
+
|
| 28 |
+
# Base ν΄λμ€
|
| 29 |
+
Base = declarative_base()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_db():
|
| 33 |
+
"""
|
| 34 |
+
λ°μ΄ν°λ² μ΄μ€ μΈμ
μ μμ±νκ³ λ°ννλ μ λλ μ΄ν°
|
| 35 |
+
"""
|
| 36 |
+
db = SessionLocal()
|
| 37 |
+
try:
|
| 38 |
+
yield db
|
| 39 |
+
finally:
|
| 40 |
+
db.close()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def init_db():
|
| 44 |
+
"""
|
| 45 |
+
λ°μ΄ν°λ² μ΄μ€ ν
μ΄λΈ μμ±
|
| 46 |
+
"""
|
| 47 |
+
# Import models to register them with SQLAlchemy
|
| 48 |
+
import models.chat_history # noqa: F401
|
| 49 |
+
import models.handoff_record # noqa: F401
|
| 50 |
+
import models.patient_data # noqa: F401
|
| 51 |
+
|
| 52 |
+
Base.metadata.create_all(bind=engine)
|
| 53 |
+
print("β
λ°μ΄ν°λ² μ΄μ€ ν
μ΄λΈμ΄ μμ±λμμ΅λλ€.")
|
data/scenarios.json
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"patient": {
|
| 3 |
+
"id": "P001",
|
| 4 |
+
"name": "κΉλ―Όμ",
|
| 5 |
+
"age": 24,
|
| 6 |
+
"gender": "λ¨",
|
| 7 |
+
"weight": 68,
|
| 8 |
+
"height": 174,
|
| 9 |
+
"diagnosis": "κΈμ± μΆ©μμΌ (Acute Appendicitis)",
|
| 10 |
+
"admission_date": "2025-10-08",
|
| 11 |
+
"attending_physician": "μΈκ³Ό μ λ¬Έμ",
|
| 12 |
+
"allergies": "μμ",
|
| 13 |
+
"comorbidities": "νΉμ΄μ¬ν μμ",
|
| 14 |
+
"room_number": "501νΈ"
|
| 15 |
+
},
|
| 16 |
+
"scenarios": [
|
| 17 |
+
{
|
| 18 |
+
"id": "S001_D0_ER_WARD",
|
| 19 |
+
"patient_id": "P001",
|
| 20 |
+
"title": "Day 0 - μκΈμ€βλ³λ μΈκ³ (μμ μ )",
|
| 21 |
+
"day": 0,
|
| 22 |
+
"handoff_situation": "μκΈμ€ β μΈκ³Ό λ³λ",
|
| 23 |
+
"hospital_day": 1,
|
| 24 |
+
"status_badge": "critical",
|
| 25 |
+
"vitals": {
|
| 26 |
+
"time": "2025-10-08 15:20",
|
| 27 |
+
"BP": "128/82 mmHg",
|
| 28 |
+
"HR": "96 bpm",
|
| 29 |
+
"RR": "20 /min",
|
| 30 |
+
"BT": "37.9β",
|
| 31 |
+
"SpO2": "98% (RA)",
|
| 32 |
+
"Pain": "NRS 8/10"
|
| 33 |
+
},
|
| 34 |
+
"labs": {
|
| 35 |
+
"time": "2025-10-08 15:40 μ±ν, 16:10 λ³΄κ³ ",
|
| 36 |
+
"CBC": {
|
| 37 |
+
"WBC": "14.8 Γ10Β³/Β΅L (β)",
|
| 38 |
+
"Neutrophil": "83%",
|
| 39 |
+
"Hb": "14.0 g/dL",
|
| 40 |
+
"Plt": "280Γ10Β³/Β΅L"
|
| 41 |
+
},
|
| 42 |
+
"CRP": "6.2 mg/dL (β)",
|
| 43 |
+
"BMP": {
|
| 44 |
+
"Na": 138,
|
| 45 |
+
"K": 3.8,
|
| 46 |
+
"Cl": 103,
|
| 47 |
+
"BUN": 10,
|
| 48 |
+
"Cr": 0.9
|
| 49 |
+
},
|
| 50 |
+
"LFT": {
|
| 51 |
+
"AST": 24,
|
| 52 |
+
"ALT": 28,
|
| 53 |
+
"ALP": 78,
|
| 54 |
+
"T_bil": 0.8
|
| 55 |
+
},
|
| 56 |
+
"UA": "WBC 0-2/HPF, Nitrite(β), Ketone trace, νλ¨(β)",
|
| 57 |
+
"Lactate": "1.6 mmol/L"
|
| 58 |
+
},
|
| 59 |
+
"physical_exam": {
|
| 60 |
+
"abdomen": "McBurney point μν΅(+), λ°λ°ν΅(+), κ·Όμ±κ²½μ§(+), Rovsing(+)"
|
| 61 |
+
},
|
| 62 |
+
"imaging": {
|
| 63 |
+
"CT": "Abd. CT w/ contrast (16:40) β μΆ©μ μ§κ²½ 9 mm, λ²½λΉνΒ·μ£Όλ³ μ§λ°©μΉ¨μ€(+), μ²κ³΅Β·λμ μμ, free air(β)"
|
| 64 |
+
},
|
| 65 |
+
"orders": [
|
| 66 |
+
"NPO μ μ§",
|
| 67 |
+
"NS 1L bolus β μ΄ν D5NS 80 mL/hr μ μ§",
|
| 68 |
+
"Ceftriaxone 2 g IV q12h + Metronidazole 500 mg IV q8h μμ",
|
| 69 |
+
"Tramadol 50 mg IV q6h PRN (NRSβ₯5)",
|
| 70 |
+
"Ondansetron 4 mg IV q8h PRN",
|
| 71 |
+
"DVT μλ°©: κ°νμ 곡기μλ°(SCD), μ‘°κΈ° 보ν κ³ν",
|
| 72 |
+
"μκΈ λ³΅κ°κ²½ μΆ©μμ μ μ μμ½ (18:40 μμ μμ )"
|
| 73 |
+
],
|
| 74 |
+
"nursing_notes": [
|
| 75 |
+
{
|
| 76 |
+
"time": "15:30",
|
| 77 |
+
"note": "μκΈμ€ λμ°©. 24μκ° μ λΆν° λ°°κΌ½ μ£Όμμμ μ°ν볡λΆ(RLQ)λ‘ μ΄λνλ λ³΅ν΅ νΈμ. μ€μ¬Β·μμμ ν. κ±·κΈ° μ΄λ €μν¨."
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"time": "16:20",
|
| 81 |
+
"note": "νμ‘ κ²μ¬ μλ£. WBC, CRP μμΉ νμΈ. CT κ²μ¬ μμ ."
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"time": "17:00",
|
| 85 |
+
"note": "CT κ²°κ³Ό: κΈμ± μΆ©μμΌ μ§λ¨. μκΈ μμ κ²°μ . μμ λμμ λ° λ§μ·¨ νκ° μλ£."
|
| 86 |
+
}
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"id": "S001_D0_WARD_OR",
|
| 91 |
+
"patient_id": "P001",
|
| 92 |
+
"title": "Day 0 - λ³λβμμ μ€ μΈκ³",
|
| 93 |
+
"day": 0,
|
| 94 |
+
"handoff_situation": "μΈκ³Ό λ³λ β μμ μ€",
|
| 95 |
+
"hospital_day": 1,
|
| 96 |
+
"status_badge": "critical",
|
| 97 |
+
"vitals": {
|
| 98 |
+
"time": "2025-10-08 18:20",
|
| 99 |
+
"BP": "124/80 mmHg",
|
| 100 |
+
"HR": "92 bpm",
|
| 101 |
+
"RR": "20 /min",
|
| 102 |
+
"BT": "37.8β",
|
| 103 |
+
"SpO2": "98% (RA)",
|
| 104 |
+
"Pain": "NRS 7/10"
|
| 105 |
+
},
|
| 106 |
+
"labs": {
|
| 107 |
+
"reference": "15:40 μ±ν κ²°κ³Ό μ°Έμ‘°",
|
| 108 |
+
"note": "μΆκ° κ²μ¬ μμ. μμ μ κΈ°λ³Έ κ²μ¬ μλ£."
|
| 109 |
+
},
|
| 110 |
+
"orders": [
|
| 111 |
+
"NPO μ μ§",
|
| 112 |
+
"D5NS 80 mL/hr κ³μ",
|
| 113 |
+
"νμμ κ³μ (Ceftriaxone + Metronidazole)",
|
| 114 |
+
"Tramadol 50 mg IV ν¬μ¬ (18:00)",
|
| 115 |
+
"μμ λμμ, λ§μ·¨ νκ° μλ£",
|
| 116 |
+
"볡κ°κ²½ μΆ©μμ μ μ 18:40 μμ μμ "
|
| 117 |
+
],
|
| 118 |
+
"nursing_notes": [
|
| 119 |
+
{
|
| 120 |
+
"time": "17:30",
|
| 121 |
+
"note": "λ³λ μ μ€. μμ μ€λΉ μ€. NPO μ μ§, IV line νμΈ."
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"time": "18:00",
|
| 125 |
+
"note": "Tramadol 50 mg IV ν¬μ¬. ν΅μ¦ 7/10."
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"time": "18:20",
|
| 129 |
+
"note": "μμ μ€ μ΄μ‘ μ€λΉ μλ£. νμ λ° λ³΄νΈμμοΏ½οΏ½οΏ½ μμ κ³Όμ μ€λͺ
."
|
| 130 |
+
}
|
| 131 |
+
]
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "S001_D0_RECOVERY",
|
| 135 |
+
"patient_id": "P001",
|
| 136 |
+
"title": "Day 0 - ν볡μ€βλ³λ μ λ (μμ ν)",
|
| 137 |
+
"day": 0,
|
| 138 |
+
"handoff_situation": "νλ³΅μ€ β μΈκ³Ό λ³λ",
|
| 139 |
+
"hospital_day": 1,
|
| 140 |
+
"status_badge": "watch",
|
| 141 |
+
"vitals": {
|
| 142 |
+
"time": "2025-10-08 20:30",
|
| 143 |
+
"BP": "122/80 mmHg",
|
| 144 |
+
"HR": "88 bpm",
|
| 145 |
+
"RR": "20 /min",
|
| 146 |
+
"BT": "37.4β",
|
| 147 |
+
"SpO2": "98% (RA)",
|
| 148 |
+
"Pain": "NRS 6/10"
|
| 149 |
+
},
|
| 150 |
+
"surgery_info": {
|
| 151 |
+
"procedure": "볡κ°κ²½ μΆ©μμ μ μ (Laparoscopic Appendectomy)",
|
| 152 |
+
"time": "18:40-19:35",
|
| 153 |
+
"findings": "μΆ©μ μ§κ²½ ~9 mm, λ°μ Β·λΆμ’
Β·μ¬μ μ μΉ¨μ°©, μ²κ³΅Β·λμ μμ. μ₯μ‘μ± λ³΅μ μλ.",
|
| 154 |
+
"technique": "3-port μ κ·Ό, Endoloop 2νλ‘ base κ²°μ°° ν μ μ . λ°°μ‘κ΄ μ½μ
μμ.",
|
| 155 |
+
"EBL": "30 mL",
|
| 156 |
+
"fluids": "Crystalloid 1,200 mL",
|
| 157 |
+
"urine_output": "300 mL",
|
| 158 |
+
"complications": "μμ"
|
| 159 |
+
},
|
| 160 |
+
"orders": [
|
| 161 |
+
"NPO μ μ§ (λ΄μΌ μμΉ¨κΉμ§)",
|
| 162 |
+
"D5NS 80 mL/hr μ μ§",
|
| 163 |
+
"Ceftriaxone 2 g IV q12h + Metronidazole 500 mg IV q8h (24hκΉμ§)",
|
| 164 |
+
"Tramadol 50 mg IV q6h PRN (NRSβ₯5)",
|
| 165 |
+
"Ondansetron 4 mg IV q8h PRN",
|
| 166 |
+
"Foley catheter μ μ§ (λ΄μΌ μμΉ¨ μ κ±° μμ )",
|
| 167 |
+
"V/S q4h, I/O chart",
|
| 168 |
+
"μμ² κ΄μ°° q8h"
|
| 169 |
+
],
|
| 170 |
+
"nursing_notes": [
|
| 171 |
+
{
|
| 172 |
+
"time": "20:30",
|
| 173 |
+
"note": "ν볡μ€μμ λ³λ μ λ. Foley catheter in situ. Trocar site 3κ³³ λλ μ± clean & dry. ν΅μ¦ NRS 6/10."
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"time": "21:00",
|
| 177 |
+
"note": "Tramadol 50 mg IV ν¬μ¬ ν ν΅μ¦ 6β5λ‘ κ°μ. 체μλ³κ²½Β·μ¬νΈν‘ κ΅μ‘. λμμλ°©μκΈ° κ΅μ‘."
|
| 178 |
+
}
|
| 179 |
+
]
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "S001_D1_MORNING",
|
| 183 |
+
"patient_id": "P001",
|
| 184 |
+
"title": "Day 1 - μμΉ¨ μΈκ³ (POD1)",
|
| 185 |
+
"day": 1,
|
| 186 |
+
"handoff_situation": "μΌκ° 근무 β μ£Όκ° κ·Όλ¬΄",
|
| 187 |
+
"hospital_day": 2,
|
| 188 |
+
"status_badge": "stable",
|
| 189 |
+
"vitals": {
|
| 190 |
+
"time": "2025-10-09 07:00",
|
| 191 |
+
"BP": "118/76 mmHg",
|
| 192 |
+
"HR": "80 bpm",
|
| 193 |
+
"RR": "18 /min",
|
| 194 |
+
"BT": "37.1β",
|
| 195 |
+
"SpO2": "98% (RA)",
|
| 196 |
+
"Pain": "NRS 4/10"
|
| 197 |
+
},
|
| 198 |
+
"labs": {
|
| 199 |
+
"time": "06:30 μ±ν, 08:10 λ³΄κ³ ",
|
| 200 |
+
"WBC": "11.2 Γ10Β³/Β΅L (β)",
|
| 201 |
+
"Neutrophil": "78%",
|
| 202 |
+
"CRP": "5.1 mg/dL (β)",
|
| 203 |
+
"electrolytes": "Na 139, K 3.9, Cl 102 (μμ )",
|
| 204 |
+
"Cr": "0.9 mg/dL (μμ )"
|
| 205 |
+
},
|
| 206 |
+
"orders": [
|
| 207 |
+
"Acetaminophen 1 g PO q8h (κΈ°λ³Έ μ§ν΅)",
|
| 208 |
+
"Tramadol 50 mg IV PRN",
|
| 209 |
+
"νμμ 24μκ°κΉμ§ μ μ§ ν μ€λ¨ (μ€λ λ°€ 20:00 μ’
λ£)",
|
| 210 |
+
"Clear liquid β μ μ¬λΆν° Soft diet (μ¦μ μμΌλ©΄)",
|
| 211 |
+
"Foley μ€μ 07:30 μ κ±°, μλ° λ°°λ¨ νμΈ",
|
| 212 |
+
"μ‘°κΈ° 보ν 1μΌ 3ν μ΄μ",
|
| 213 |
+
"IS(μ¬νΈν‘) κ΅μ‘",
|
| 214 |
+
"V/S q4h, μμ² κ΄μ°° q8h"
|
| 215 |
+
],
|
| 216 |
+
"nursing_notes": [
|
| 217 |
+
{
|
| 218 |
+
"time": "07:30",
|
| 219 |
+
"note": "Foley μ κ±°. μλ° λ°°λ¨ μνΈ(ν¬λͺ
). μ€μ¬ κ²½λ―Έ(β/Β±)."
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"time": "10:30",
|
| 223 |
+
"note": "보ν 1ν μν(λΆμΆ). μ΄μ§λΌ κ²½λ―Έ νΈμ, ν΄μ ν νΈμ . Clear diet 무리 μμ΄ μ μ¬λΆν° soft κΆμ₯."
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"time": "14:00",
|
| 227 |
+
"note": "Soft diet μμ. μμ² λ°μ Β·μΌμΆ μμ. C/D/I μν μ μ§."
|
| 228 |
+
}
|
| 229 |
+
]
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"id": "S001_D2_DISCHARGE",
|
| 233 |
+
"patient_id": "P001",
|
| 234 |
+
"title": "Day 2 - ν΄μ μ μΈκ³ (POD2)",
|
| 235 |
+
"day": 2,
|
| 236 |
+
"handoff_situation": "λ³λ κ°νΈμ¬ β ν΄μ κ΅μ‘",
|
| 237 |
+
"hospital_day": 3,
|
| 238 |
+
"status_badge": "stable",
|
| 239 |
+
"vitals": {
|
| 240 |
+
"time": "2025-10-10 07:00",
|
| 241 |
+
"BP": "116/74 mmHg",
|
| 242 |
+
"HR": "76 bpm",
|
| 243 |
+
"RR": "16 /min",
|
| 244 |
+
"BT": "36.9β",
|
| 245 |
+
"SpO2": "98% (RA)",
|
| 246 |
+
"Pain": "NRS 2/10"
|
| 247 |
+
},
|
| 248 |
+
"labs": {
|
| 249 |
+
"time": "06:30 μ±ν, 08:00 λ³΄κ³ ",
|
| 250 |
+
"WBC": "9.0 Γ10Β³/Β΅L (μ μν)",
|
| 251 |
+
"Neutrophil": "70%",
|
| 252 |
+
"CRP": "2.1 mg/dL (νΈμ )",
|
| 253 |
+
"κΈ°ν": "μ ν΄μ§, μ μ₯Β·κ°κΈ°λ₯ μ μ"
|
| 254 |
+
},
|
| 255 |
+
"orders": [
|
| 256 |
+
"νμμ μ€λ¨ μ μ§ (λΉμ²κ³΅Β·ν©λ³μ¦ μμ)",
|
| 257 |
+
"Acetaminophen 500 mg 2μ PO q8h PRN (3μΌλΆ μ²λ°©)",
|
| 258 |
+
"μμ²: 48μκ° μ΄ν μ€μ μ μ μ§ μκ² μ£Όμ β 72μκ° ν κ°λ²Όμ΄ λ¬Ό μ μ΄ κ°λ₯",
|
| 259 |
+
"νλ: λ¬΄κ±°μ΄ λ¬Όκ±΄ λ€κΈ°Β·κ²©ν μ΄λ 1μ£Ό ννΌ, ν΅μ¦ λ²μ λ΄ λ³΄ν κΆμ₯",
|
| 260 |
+
"μΈλ: 7μΌ ν μμ² νμΈ/νμ μ λ΄ν©μ¬ μ κ±°",
|
| 261 |
+
"ν΄μ μμ : 2025-10-10 μ€ν"
|
| 262 |
+
],
|
| 263 |
+
"discharge_education": [
|
| 264 |
+
"μμ² κ΄λ¦¬: 48μκ° ν μ€μ κ°λ₯ (λ¬Ό μ μ΄ μ΅μν), λ°μ Β·κ³ λ¦ μ λ΄μ",
|
| 265 |
+
"μμ΄: λΆλλ¬μ΄ μμ¬λΆν° μμ, μ μ§μ μΌλ‘ μΌλ°μμΌλ‘ μ ν",
|
| 266 |
+
"νλ: κ°λ²Όμ΄ 보ν κΆμ₯, λ¬΄κ±°μ΄ λ¬Όκ±΄(5kg μ΄μ) 1μ£Όκ° κΈμ§",
|
| 267 |
+
"κ²½κ³ μ¦μ: 38.0β μ΄μ λ°μ΄, μμ² λ°μ /κ³ λ¦, μ
νλλ 볡ν΅Β·κ΅¬ν , λ°°λ³Β·λ°°λ¨ μ΄μ μ μ¦μ λ΄μ",
|
| 268 |
+
"μΈλ μμ½: 7μΌ ν μΈκ³Ό μΈλ"
|
| 269 |
+
],
|
| 270 |
+
"nursing_notes": [
|
| 271 |
+
{
|
| 272 |
+
"time": "08:30",
|
| 273 |
+
"note": "Soft diet μ μμ·¨. κ°μ€Β·λ°°λ³ νμΈ(+). 보ν λ
립μ μΌλ‘ κ°λ₯."
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"time": "09:30",
|
| 277 |
+
"note": "ν΄μ μ κ΅μ‘: μμ²κ΄λ¦¬Β·μμ΄Β·νλΒ·κ²½κ³ μ¦μ μλ΄. 보νΈμ λμ, μ΄ν΄λ 'μ’μ'. κ΅μ‘μλ£ μ 곡."
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"time": "14:00",
|
| 281 |
+
"note": "ν΄μ μλ£. μ²λ°©μ λ° ν΄μ μμ½μ μ λ¬. 7μΌ ν μΈλ μμ½ νμΈ."
|
| 282 |
+
}
|
| 283 |
+
]
|
| 284 |
+
}
|
| 285 |
+
]
|
| 286 |
+
}
|
models/__init__.py
ADDED
|
File without changes
|
models/chat_history.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
μ±ν
νμ€ν 리 λͺ¨λΈ
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from config.database import Base
|
| 6 |
+
from sqlalchemy import Column, DateTime, Integer, String, Text
|
| 7 |
+
from sqlalchemy.sql import func
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ChatHistory(Base):
|
| 11 |
+
"""νμκ³Ό AI κ°μ μ±ν
κΈ°λ‘"""
|
| 12 |
+
|
| 13 |
+
__tablename__ = "chat_history"
|
| 14 |
+
|
| 15 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 16 |
+
session_id = Column(String(100), nullable=False, index=True) # μ±ν
μΈμ
ID
|
| 17 |
+
student_id = Column(String(50), nullable=False, index=True) # νμ μλ³μ
|
| 18 |
+
scenario_id = Column(String(50), nullable=False) # μλλ¦¬μ€ ID
|
| 19 |
+
role = Column(String(20), nullable=False) # 'user' λλ 'assistant'
|
| 20 |
+
message = Column(Text, nullable=False) # λ©μμ§ λ΄μ©
|
| 21 |
+
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
| 22 |
+
|
| 23 |
+
def __repr__(self):
|
| 24 |
+
return f"<ChatHistory(id={self.id}, session_id={self.session_id}, role={self.role})>"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class SbarDraft(Base):
|
| 28 |
+
"""μμ± μ€μΈ SBAR μμ μ μ₯"""
|
| 29 |
+
|
| 30 |
+
__tablename__ = "sbar_drafts"
|
| 31 |
+
|
| 32 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 33 |
+
session_id = Column(String(100), nullable=False, unique=True, index=True)
|
| 34 |
+
student_id = Column(String(50), nullable=False)
|
| 35 |
+
scenario_id = Column(String(50), nullable=False)
|
| 36 |
+
situation = Column(Text, default="")
|
| 37 |
+
background = Column(Text, default="")
|
| 38 |
+
assessment = Column(Text, default="")
|
| 39 |
+
recommendation = Column(Text, default="")
|
| 40 |
+
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
| 41 |
+
|
| 42 |
+
def __repr__(self):
|
| 43 |
+
return f"<SbarDraft(session_id={self.session_id}, student_id={self.student_id})>"
|
| 44 |
+
|
models/handoff_record.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
μΈμμΈκ³ κΈ°λ‘ λͺ¨λΈ
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from config.database import Base
|
| 6 |
+
from sqlalchemy import (JSON, Column, DateTime, Float, ForeignKey, Integer,
|
| 7 |
+
String, Text)
|
| 8 |
+
from sqlalchemy.orm import relationship
|
| 9 |
+
from sqlalchemy.sql import func
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class HandoffRecord(Base):
|
| 13 |
+
"""νμμ μΈμμΈκ³ κΈ°λ‘ λ° νκ° κ²°κ³Ό"""
|
| 14 |
+
|
| 15 |
+
__tablename__ = "handoff_records"
|
| 16 |
+
|
| 17 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 18 |
+
session_id = Column(String(100), nullable=True, index=True) # μ±ν
μΈμ
ID (μ±ν
κΈ°λ‘κ³Ό μ°κ²°)
|
| 19 |
+
student_id = Column(String(50), nullable=False) # νμ μλ³μ
|
| 20 |
+
scenario_id = Column(String(50), ForeignKey("scenarios.id"), nullable=False)
|
| 21 |
+
|
| 22 |
+
# μ μΆ μκ°
|
| 23 |
+
submitted_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 24 |
+
|
| 25 |
+
# SBAR μ
λ ₯ λ΄μ©
|
| 26 |
+
situation = Column(Text, nullable=False)
|
| 27 |
+
background = Column(Text, nullable=False)
|
| 28 |
+
assessment = Column(Text, nullable=False)
|
| 29 |
+
recommendation = Column(Text, nullable=False)
|
| 30 |
+
|
| 31 |
+
# Gemini AI νκ° κ²°κ³Ό
|
| 32 |
+
total_score = Column(Float) # μ΄μ (0-100)
|
| 33 |
+
category_scores = Column(JSON) # μΉ΄ν
κ³ λ¦¬λ³ μ μ
|
| 34 |
+
strengths = Column(JSON) # κ°μ λͺ©λ‘
|
| 35 |
+
improvements = Column(JSON) # κ°μ μ¬ν λͺ©λ‘
|
| 36 |
+
detailed_feedback = Column(Text) # μμΈ νΌλλ°±
|
| 37 |
+
|
| 38 |
+
# κ΄κ³
|
| 39 |
+
scenario = relationship("Scenario", back_populates="handoff_records")
|
| 40 |
+
|
| 41 |
+
def __repr__(self):
|
| 42 |
+
return f"<HandoffRecord(id={self.id}, student_id={self.student_id}, score={self.total_score})>"
|
models/patient_data.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
νμ λ° μλλ¦¬μ€ λ°μ΄ν° λͺ¨λΈ
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from config.database import Base
|
| 6 |
+
from sqlalchemy import JSON, Column, Date, ForeignKey, Integer, String, Text
|
| 7 |
+
from sqlalchemy.orm import relationship
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Patient(Base):
|
| 11 |
+
"""νμ κΈ°λ³Έ μ 보"""
|
| 12 |
+
|
| 13 |
+
__tablename__ = "patients"
|
| 14 |
+
|
| 15 |
+
id = Column(String(50), primary_key=True) # μ: P001
|
| 16 |
+
name = Column(String(100), nullable=False)
|
| 17 |
+
age = Column(Integer, nullable=False)
|
| 18 |
+
gender = Column(String(10), nullable=False)
|
| 19 |
+
weight = Column(Integer) # kg
|
| 20 |
+
height = Column(Integer) # cm
|
| 21 |
+
diagnosis = Column(String(200), nullable=False)
|
| 22 |
+
admission_date = Column(Date, nullable=False)
|
| 23 |
+
attending_physician = Column(String(100))
|
| 24 |
+
allergies = Column(String(200))
|
| 25 |
+
comorbidities = Column(Text)
|
| 26 |
+
room_number = Column(String(20))
|
| 27 |
+
|
| 28 |
+
# κ΄κ³
|
| 29 |
+
scenarios = relationship("Scenario", back_populates="patient")
|
| 30 |
+
|
| 31 |
+
def __repr__(self):
|
| 32 |
+
return f"<Patient(id={self.id}, name={self.name}, diagnosis={self.diagnosis})>"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Scenario(Base):
|
| 36 |
+
"""νμ μλλ¦¬μ€ (μΈμμΈκ³ μν©)"""
|
| 37 |
+
|
| 38 |
+
__tablename__ = "scenarios"
|
| 39 |
+
|
| 40 |
+
id = Column(String(50), primary_key=True) # μ: S001_D0_ER_WARD
|
| 41 |
+
patient_id = Column(String(50), ForeignKey("patients.id"), nullable=False)
|
| 42 |
+
title = Column(String(200), nullable=False)
|
| 43 |
+
day = Column(Integer, nullable=False) # 0, 1, 2
|
| 44 |
+
handoff_situation = Column(String(200)) # μ: μκΈμ€βλ³λ
|
| 45 |
+
|
| 46 |
+
# EMR λ°μ΄ν° (JSON νμ)
|
| 47 |
+
vitals = Column(JSON) # νλ ₯μ§ν
|
| 48 |
+
labs = Column(JSON) # κ²μ¬ κ²°κ³Ό
|
| 49 |
+
orders = Column(JSON) # μμ¬ μ²λ°©
|
| 50 |
+
nursing_notes = Column(JSON) # κ°νΈ κΈ°λ‘
|
| 51 |
+
physical_exam = Column(JSON) # μ 체 κ²μ§
|
| 52 |
+
imaging = Column(JSON) # μμ κ²μ¬
|
| 53 |
+
surgery_info = Column(JSON) # μμ μ 보
|
| 54 |
+
discharge_education = Column(JSON) # ν΄μ κ΅μ‘
|
| 55 |
+
|
| 56 |
+
# μΆκ° μ 보
|
| 57 |
+
hospital_day = Column(Integer)
|
| 58 |
+
status_badge = Column(String(20)) # stable, watch, critical
|
| 59 |
+
|
| 60 |
+
# κ΄κ³
|
| 61 |
+
patient = relationship("Patient", back_populates="scenarios")
|
| 62 |
+
handoff_records = relationship("HandoffRecord", back_populates="scenario")
|
| 63 |
+
|
| 64 |
+
def __repr__(self):
|
| 65 |
+
return f"<Scenario(id={self.id}, title={self.title})>"
|
services/__init__.py
ADDED
|
File without changes
|
services/db_service.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
λ°μ΄ν°λ² μ΄μ€ CRUD μλΉμ€
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
from sqlalchemy.orm import Session
|
| 9 |
+
|
| 10 |
+
from models.chat_history import ChatHistory, SbarDraft
|
| 11 |
+
from models.handoff_record import HandoffRecord
|
| 12 |
+
from models.patient_data import Patient, Scenario
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class DatabaseService:
|
| 16 |
+
"""λ°μ΄ν°λ² μ΄μ€ κ΄λ ¨ μλΉμ€"""
|
| 17 |
+
|
| 18 |
+
def __init__(self, db: Session):
|
| 19 |
+
self.db = db
|
| 20 |
+
|
| 21 |
+
# ===== Patient κ΄λ ¨ =====
|
| 22 |
+
|
| 23 |
+
def create_patient(self, patient_data: dict) -> Patient:
|
| 24 |
+
"""νμ μμ±"""
|
| 25 |
+
patient = Patient(**patient_data)
|
| 26 |
+
self.db.add(patient)
|
| 27 |
+
self.db.commit()
|
| 28 |
+
self.db.refresh(patient)
|
| 29 |
+
return patient
|
| 30 |
+
|
| 31 |
+
def get_patient(self, patient_id: str) -> Patient:
|
| 32 |
+
"""νμ μ‘°ν"""
|
| 33 |
+
return self.db.query(Patient).filter(Patient.id == patient_id).first()
|
| 34 |
+
|
| 35 |
+
def get_all_patients(self):
|
| 36 |
+
"""λͺ¨λ νμ μ‘°ν"""
|
| 37 |
+
return self.db.query(Patient).all()
|
| 38 |
+
|
| 39 |
+
# ===== Scenario κ΄λ ¨ =====
|
| 40 |
+
|
| 41 |
+
def create_scenario(self, scenario_data: dict) -> Scenario:
|
| 42 |
+
"""μλλ¦¬μ€ μμ±"""
|
| 43 |
+
scenario = Scenario(**scenario_data)
|
| 44 |
+
self.db.add(scenario)
|
| 45 |
+
self.db.commit()
|
| 46 |
+
self.db.refresh(scenario)
|
| 47 |
+
return scenario
|
| 48 |
+
|
| 49 |
+
def get_scenario(self, scenario_id: str) -> Scenario:
|
| 50 |
+
"""μλλ¦¬μ€ μ‘°ν"""
|
| 51 |
+
return self.db.query(Scenario).filter(Scenario.id == scenario_id).first()
|
| 52 |
+
|
| 53 |
+
def get_scenarios_by_patient(self, patient_id: str):
|
| 54 |
+
"""νΉμ νμμ λͺ¨λ μλλ¦¬μ€ μ‘°ν"""
|
| 55 |
+
return self.db.query(Scenario).filter(Scenario.patient_id == patient_id).all()
|
| 56 |
+
|
| 57 |
+
def get_all_scenarios(self):
|
| 58 |
+
"""λͺ¨λ μλλ¦¬μ€ μ‘°ν"""
|
| 59 |
+
return self.db.query(Scenario).all()
|
| 60 |
+
|
| 61 |
+
# ===== HandoffRecord κ΄λ ¨ =====
|
| 62 |
+
|
| 63 |
+
def create_handoff_record(
|
| 64 |
+
self, student_id: str, scenario_id: str, sbar_data: dict, evaluation: dict, session_id: str = None
|
| 65 |
+
) -> HandoffRecord:
|
| 66 |
+
"""μΈμμΈκ³ κΈ°λ‘ μμ±"""
|
| 67 |
+
record = HandoffRecord(
|
| 68 |
+
session_id=session_id,
|
| 69 |
+
student_id=student_id,
|
| 70 |
+
scenario_id=scenario_id,
|
| 71 |
+
situation=sbar_data.get("situation", ""),
|
| 72 |
+
background=sbar_data.get("background", ""),
|
| 73 |
+
assessment=sbar_data.get("assessment", ""),
|
| 74 |
+
recommendation=sbar_data.get("recommendation", ""),
|
| 75 |
+
total_score=evaluation.get("total_score", 0),
|
| 76 |
+
category_scores=evaluation.get("category_scores", {}),
|
| 77 |
+
strengths=evaluation.get("strengths", []),
|
| 78 |
+
improvements=evaluation.get("improvements", []),
|
| 79 |
+
detailed_feedback=evaluation.get("detailed_feedback", ""),
|
| 80 |
+
)
|
| 81 |
+
self.db.add(record)
|
| 82 |
+
self.db.commit()
|
| 83 |
+
self.db.refresh(record)
|
| 84 |
+
return record
|
| 85 |
+
|
| 86 |
+
def get_handoff_record(self, record_id: int) -> HandoffRecord:
|
| 87 |
+
"""μΈμμΈκ³ κΈ°λ‘ μ‘°ν"""
|
| 88 |
+
return (
|
| 89 |
+
self.db.query(HandoffRecord).filter(HandoffRecord.id == record_id).first()
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
def get_records_by_student(self, student_id: str):
|
| 93 |
+
"""νΉμ νμμ λͺ¨λ μΈμμΈκ³ κΈ°λ‘ μ‘°ν"""
|
| 94 |
+
return (
|
| 95 |
+
self.db.query(HandoffRecord)
|
| 96 |
+
.filter(HandoffRecord.student_id == student_id)
|
| 97 |
+
.order_by(HandoffRecord.submitted_at.desc())
|
| 98 |
+
.all()
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
def get_records_by_scenario(self, scenario_id: str):
|
| 102 |
+
"""νΉμ μλ리μ€μ λͺ¨λ μΈμμΈκ³ κΈ°λ‘ μ‘°ν"""
|
| 103 |
+
return (
|
| 104 |
+
self.db.query(HandoffRecord)
|
| 105 |
+
.filter(HandoffRecord.scenario_id == scenario_id)
|
| 106 |
+
.order_by(HandoffRecord.submitted_at.desc())
|
| 107 |
+
.all()
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def get_all_handoff_records(self):
|
| 111 |
+
"""λͺ¨λ μΈμμΈκ³ κΈ°λ‘ μ‘°ν"""
|
| 112 |
+
return (
|
| 113 |
+
self.db.query(HandoffRecord)
|
| 114 |
+
.order_by(HandoffRecord.submitted_at.desc())
|
| 115 |
+
.all()
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# ===== ν΅κ³ κ΄λ ¨ =====
|
| 119 |
+
|
| 120 |
+
def get_student_statistics(self, student_id: str) -> dict:
|
| 121 |
+
"""νμλ³ ν΅κ³"""
|
| 122 |
+
records = self.get_records_by_student(student_id)
|
| 123 |
+
|
| 124 |
+
if not records:
|
| 125 |
+
return {
|
| 126 |
+
"total_submissions": 0,
|
| 127 |
+
"average_score": 0,
|
| 128 |
+
"highest_score": 0,
|
| 129 |
+
"lowest_score": 0,
|
| 130 |
+
"recent_submissions": [],
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
scores = [r.total_score for r in records if r.total_score is not None]
|
| 134 |
+
|
| 135 |
+
return {
|
| 136 |
+
"total_submissions": len(records),
|
| 137 |
+
"average_score": sum(scores) / len(scores) if scores else 0,
|
| 138 |
+
"highest_score": max(scores) if scores else 0,
|
| 139 |
+
"lowest_score": min(scores) if scores else 0,
|
| 140 |
+
"recent_submissions": [
|
| 141 |
+
{
|
| 142 |
+
"id": r.id,
|
| 143 |
+
"scenario_id": r.scenario_id,
|
| 144 |
+
"score": r.total_score,
|
| 145 |
+
"submitted_at": r.submitted_at.isoformat()
|
| 146 |
+
if r.submitted_at
|
| 147 |
+
else None,
|
| 148 |
+
}
|
| 149 |
+
for r in records[:5] # μ΅κ·Ό 5κ°
|
| 150 |
+
],
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
def get_scenario_statistics(self, scenario_id: str) -> dict:
|
| 154 |
+
"""μλ리μ€λ³ ν΅κ³"""
|
| 155 |
+
records = self.get_records_by_scenario(scenario_id)
|
| 156 |
+
|
| 157 |
+
if not records:
|
| 158 |
+
return {"total_attempts": 0, "average_score": 0, "score_distribution": {}}
|
| 159 |
+
|
| 160 |
+
scores = [r.total_score for r in records if r.total_score is not None]
|
| 161 |
+
|
| 162 |
+
# μ μ ꡬκ°λ³ λΆν¬ (0-60: λ―Έν‘, 61-80: 보ν΅, 81-100: μ°μ)
|
| 163 |
+
distribution = {
|
| 164 |
+
"excellent": len([s for s in scores if s >= 81]),
|
| 165 |
+
"good": len([s for s in scores if 61 <= s < 81]),
|
| 166 |
+
"needs_improvement": len([s for s in scores if s < 61]),
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
return {
|
| 170 |
+
"total_attempts": len(records),
|
| 171 |
+
"average_score": sum(scores) / len(scores) if scores else 0,
|
| 172 |
+
"score_distribution": distribution,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
# ===== ChatHistory κ΄λ ¨ =====
|
| 176 |
+
|
| 177 |
+
def create_chat_session(self, student_id: str, scenario_id: str) -> str:
|
| 178 |
+
"""μ μ±ν
μΈμ
μμ± λ° μΈμ
ID λ°ν"""
|
| 179 |
+
session_id = f"{student_id}_{scenario_id}_{uuid.uuid4().hex[:8]}"
|
| 180 |
+
return session_id
|
| 181 |
+
|
| 182 |
+
def save_chat_message(
|
| 183 |
+
self, session_id: str, student_id: str, scenario_id: str, role: str, message: str
|
| 184 |
+
) -> ChatHistory:
|
| 185 |
+
"""μ±ν
λ©μμ§ μ μ₯"""
|
| 186 |
+
chat = ChatHistory(
|
| 187 |
+
session_id=session_id,
|
| 188 |
+
student_id=student_id,
|
| 189 |
+
scenario_id=scenario_id,
|
| 190 |
+
role=role,
|
| 191 |
+
message=message,
|
| 192 |
+
)
|
| 193 |
+
self.db.add(chat)
|
| 194 |
+
self.db.commit()
|
| 195 |
+
self.db.refresh(chat)
|
| 196 |
+
return chat
|
| 197 |
+
|
| 198 |
+
def get_chat_history(self, session_id: str):
|
| 199 |
+
"""νΉμ μΈμ
μ μ±ν
κΈ°λ‘ μ‘°ν"""
|
| 200 |
+
return (
|
| 201 |
+
self.db.query(ChatHistory)
|
| 202 |
+
.filter(ChatHistory.session_id == session_id)
|
| 203 |
+
.order_by(ChatHistory.timestamp.asc())
|
| 204 |
+
.all()
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
def get_chat_sessions_by_student(self, student_id: str):
|
| 208 |
+
"""νΉμ νμμ λͺ¨λ μ±ν
μΈμ
μ‘°ν"""
|
| 209 |
+
return (
|
| 210 |
+
self.db.query(ChatHistory.session_id, ChatHistory.scenario_id, ChatHistory.timestamp)
|
| 211 |
+
.filter(ChatHistory.student_id == student_id)
|
| 212 |
+
.distinct(ChatHistory.session_id)
|
| 213 |
+
.order_by(ChatHistory.session_id, ChatHistory.timestamp.desc())
|
| 214 |
+
.all()
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
# ===== SbarDraft κ΄λ ¨ =====
|
| 218 |
+
|
| 219 |
+
def save_draft_sbar(
|
| 220 |
+
self, session_id: str, student_id: str, scenario_id: str, sbar_data: dict
|
| 221 |
+
) -> SbarDraft:
|
| 222 |
+
"""μμ± μ€μΈ SBAR μμ μ μ₯"""
|
| 223 |
+
# κΈ°μ‘΄ draftκ° μμΌλ©΄ μ
λ°μ΄νΈ, μμΌλ©΄ μμ±
|
| 224 |
+
draft = self.db.query(SbarDraft).filter(SbarDraft.session_id == session_id).first()
|
| 225 |
+
|
| 226 |
+
if draft:
|
| 227 |
+
draft.situation = sbar_data.get("situation", "")
|
| 228 |
+
draft.background = sbar_data.get("background", "")
|
| 229 |
+
draft.assessment = sbar_data.get("assessment", "")
|
| 230 |
+
draft.recommendation = sbar_data.get("recommendation", "")
|
| 231 |
+
else:
|
| 232 |
+
draft = SbarDraft(
|
| 233 |
+
session_id=session_id,
|
| 234 |
+
student_id=student_id,
|
| 235 |
+
scenario_id=scenario_id,
|
| 236 |
+
situation=sbar_data.get("situation", ""),
|
| 237 |
+
background=sbar_data.get("background", ""),
|
| 238 |
+
assessment=sbar_data.get("assessment", ""),
|
| 239 |
+
recommendation=sbar_data.get("recommendation", ""),
|
| 240 |
+
)
|
| 241 |
+
self.db.add(draft)
|
| 242 |
+
|
| 243 |
+
self.db.commit()
|
| 244 |
+
self.db.refresh(draft)
|
| 245 |
+
return draft
|
| 246 |
+
|
| 247 |
+
def get_draft_sbar(self, session_id: str) -> dict:
|
| 248 |
+
"""μμ μ μ₯λ SBAR λΆλ¬μ€κΈ°"""
|
| 249 |
+
draft = self.db.query(SbarDraft).filter(SbarDraft.session_id == session_id).first()
|
| 250 |
+
|
| 251 |
+
if draft:
|
| 252 |
+
return {
|
| 253 |
+
"situation": draft.situation or "",
|
| 254 |
+
"background": draft.background or "",
|
| 255 |
+
"assessment": draft.assessment or "",
|
| 256 |
+
"recommendation": draft.recommendation or "",
|
| 257 |
+
}
|
| 258 |
+
return {"situation": "", "background": "", "assessment": "", "recommendation": ""}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def load_scenarios_to_db(db: Session, json_file_path: str):
|
| 262 |
+
"""
|
| 263 |
+
JSON νμΌμμ νμ λ° μλλ¦¬μ€ λ°μ΄ν°λ₯Ό μ½μ΄ DBμ μ μ₯
|
| 264 |
+
"""
|
| 265 |
+
import json
|
| 266 |
+
|
| 267 |
+
with open(json_file_path, "r", encoding="utf-8") as f:
|
| 268 |
+
data = json.load(f)
|
| 269 |
+
|
| 270 |
+
db_service = DatabaseService(db)
|
| 271 |
+
|
| 272 |
+
# νμ λ°μ΄ν° μ μ₯
|
| 273 |
+
patient_data = data["patient"]
|
| 274 |
+
|
| 275 |
+
# admission_dateλ₯Ό λ¬Έμμ΄μμ date κ°μ²΄λ‘ λ³ν
|
| 276 |
+
if isinstance(patient_data.get("admission_date"), str):
|
| 277 |
+
patient_data["admission_date"] = datetime.strptime(
|
| 278 |
+
patient_data["admission_date"], "%Y-%m-%d"
|
| 279 |
+
).date()
|
| 280 |
+
|
| 281 |
+
# κΈ°μ‘΄ νμκ° μμΌλ©΄ μμ±
|
| 282 |
+
existing_patient = db_service.get_patient(patient_data["id"])
|
| 283 |
+
if not existing_patient:
|
| 284 |
+
db_service.create_patient(patient_data)
|
| 285 |
+
print(f"β
νμ μμ±: {patient_data['name']}")
|
| 286 |
+
else:
|
| 287 |
+
print(f"βΉοΈ νμ μ΄λ―Έ μ‘΄μ¬: {patient_data['name']}")
|
| 288 |
+
|
| 289 |
+
# μλλ¦¬μ€ λ°μ΄ν° μ μ₯
|
| 290 |
+
for scenario_data in data["scenarios"]:
|
| 291 |
+
existing_scenario = db_service.get_scenario(scenario_data["id"])
|
| 292 |
+
if not existing_scenario:
|
| 293 |
+
db_service.create_scenario(scenario_data)
|
| 294 |
+
print(f"β
μλλ¦¬μ€ μμ±: {scenario_data['title']}")
|
| 295 |
+
else:
|
| 296 |
+
print(f"βΉοΈ μλλ¦¬μ€ μ΄λ―Έ μ‘΄μ¬: {scenario_data['title']}")
|
services/gemini_service.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Google Gemini AI νκ° μλΉμ€
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
# Gemini API μ€μ
|
| 14 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 15 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class GeminiEvaluator:
|
| 19 |
+
"""κ°νΈ μΈμμΈκ³ νκ°λ₯Ό μν Gemini AI μλΉμ€"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, model_name="gemini-2.0-flash-exp"):
|
| 22 |
+
self.model = genai.GenerativeModel(model_name)
|
| 23 |
+
|
| 24 |
+
def evaluate_handoff(
|
| 25 |
+
self, student_sbar: dict, scenario_data: dict, patient_data: dict
|
| 26 |
+
) -> dict:
|
| 27 |
+
"""
|
| 28 |
+
νμμ SBAR μΈμμΈκ³λ₯Ό νκ°ν©λλ€.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
student_sbar: νμμ΄ μμ±ν SBAR (situation, background, assessment, recommendation)
|
| 32 |
+
scenario_data: μλλ¦¬μ€ λ°μ΄ν° (EMR μ 보)
|
| 33 |
+
patient_data: νμ κΈ°λ³Έ μ 보
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
νκ° κ²°κ³Ό λμ
λ리 (total_score, category_scores, strengths, improvements, detailed_feedback)
|
| 37 |
+
"""
|
| 38 |
+
prompt = self._create_evaluation_prompt(
|
| 39 |
+
student_sbar, scenario_data, patient_data
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
response = self.model.generate_content(
|
| 44 |
+
prompt,
|
| 45 |
+
generation_config={
|
| 46 |
+
"temperature": 0.7,
|
| 47 |
+
"top_p": 0.95,
|
| 48 |
+
"max_output_tokens": 2048,
|
| 49 |
+
},
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# JSON μλ΅ νμ±
|
| 53 |
+
result_text = response.text.strip()
|
| 54 |
+
|
| 55 |
+
# Markdown μ½λ λΈλ‘ μ κ±° (```json ... ```)
|
| 56 |
+
if result_text.startswith("```"):
|
| 57 |
+
result_text = result_text.split("```")[1]
|
| 58 |
+
if result_text.startswith("json"):
|
| 59 |
+
result_text = result_text[4:]
|
| 60 |
+
result_text = result_text.strip()
|
| 61 |
+
|
| 62 |
+
evaluation = json.loads(result_text)
|
| 63 |
+
return evaluation
|
| 64 |
+
|
| 65 |
+
except json.JSONDecodeError as e:
|
| 66 |
+
print(f"JSON νμ± μ€λ₯: {e}")
|
| 67 |
+
print(f"μλ΅ ν
μ€νΈ: {response.text}")
|
| 68 |
+
return self._create_default_evaluation()
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"Gemini API μ€λ₯: {e}")
|
| 71 |
+
return self._create_default_evaluation()
|
| 72 |
+
|
| 73 |
+
def _create_evaluation_prompt(
|
| 74 |
+
self, student_sbar: dict, scenario_data: dict, patient_data: dict
|
| 75 |
+
) -> str:
|
| 76 |
+
"""κ³ λνλ νκ° ν둬ννΈ μμ±"""
|
| 77 |
+
|
| 78 |
+
prompt = f"""λΉμ μ 20λ
κ²½λ ₯μ νκ΅ κ°νΈ κ΅μ‘ μ λ¬Έκ°μ΄μ κ°νΈν κ΅μμ
λλ€.
|
| 79 |
+
κ°νΈ νμμ΄ μμ±ν SBAR νμμ μΈμμΈκ³λ₯Ό νκ΅ κ°νΈμ¬ κ΅κ°μν λ° μμ μ€λ¬΄ νμ€μ λ°λΌ νκ°ν΄μ£ΌμΈμ.
|
| 80 |
+
|
| 81 |
+
## π νμ μ 보 (EMR λ°μ΄ν°)
|
| 82 |
+
- **νμλͺ
**: {patient_data.get('name')} ({patient_data.get('age')}μΈ {patient_data.get('gender')})
|
| 83 |
+
- **μ§λ¨λͺ
**: {patient_data.get('diagnosis')}
|
| 84 |
+
- **μ
μμΌ**: {patient_data.get('admission_date')} (μ¬μ {scenario_data.get('hospital_day')}μΌμ°¨)
|
| 85 |
+
- **μλ λ₯΄κΈ°**: {patient_data.get('allergies')}
|
| 86 |
+
|
| 87 |
+
## π₯ μΈμμΈκ³ μν©
|
| 88 |
+
- **μλ리μ€**: {scenario_data.get('title')}
|
| 89 |
+
- **μΈκ³ μν©**: {scenario_data.get('handoff_situation')}
|
| 90 |
+
|
| 91 |
+
### π νλ ₯μ§ν (Vital Signs)
|
| 92 |
+
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
|
| 93 |
+
|
| 94 |
+
### π§ͺ κ²μ¬ κ²°κ³Ό (Laboratory Results)
|
| 95 |
+
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
|
| 96 |
+
|
| 97 |
+
### π μμ¬ μ²λ°© (Physician Orders)
|
| 98 |
+
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
|
| 99 |
+
|
| 100 |
+
### π κ°νΈ κΈ°λ‘ (Nursing Notes)
|
| 101 |
+
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## π νμμ΄ μμ±ν SBAR μΈμμΈκ³
|
| 106 |
+
|
| 107 |
+
**S - Situation (μν©)**
|
| 108 |
+
{student_sbar.get('situation', '')}
|
| 109 |
+
|
| 110 |
+
**B - Background (λ°°κ²½)**
|
| 111 |
+
{student_sbar.get('background', '')}
|
| 112 |
+
|
| 113 |
+
**A - Assessment (νκ°)**
|
| 114 |
+
{student_sbar.get('assessment', '')}
|
| 115 |
+
|
| 116 |
+
**R - Recommendation (κΆκ³ μ¬ν)**
|
| 117 |
+
{student_sbar.get('recommendation', '')}
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## π νκ° κΈ°μ€ (μ΄ 100μ )
|
| 122 |
+
|
| 123 |
+
### 1οΈβ£ μμ μ± (Completeness, 0-25μ )
|
| 124 |
+
**νμ μ 보 ν¬ν¨ μ¬λΆλ₯Ό νκ°ν©λλ€:**
|
| 125 |
+
|
| 126 |
+
**Situation (6μ ):**
|
| 127 |
+
- νμ μ μ (μ΄λ¦, λμ΄, μ±λ³) [2μ ]
|
| 128 |
+
- μ£ΌνΈμ/μ
μ μ¬μ [2μ ]
|
| 129 |
+
- νμ¬ μν μμ½ [2μ ]
|
| 130 |
+
|
| 131 |
+
**Background (6μ ):**
|
| 132 |
+
- μ§λ¨λͺ
[1μ ]
|
| 133 |
+
- κ³Όκ±°λ ₯ [1μ ]
|
| 134 |
+
- μλ λ₯΄κΈ° [1μ ]
|
| 135 |
+
- νμ¬ ν¬μ½ [2μ ]
|
| 136 |
+
- μμ λ ₯ (ν΄λΉ μ) [1μ ]
|
| 137 |
+
|
| 138 |
+
**Assessment (8μ ):**
|
| 139 |
+
- νλ ₯μ§ν (BP, HR, RR, BT, SpO2) [3μ ]
|
| 140 |
+
- μ£Όμ κ²μ¬ κ²°κ³Ό (WBC, CRP λ±) [2μ ]
|
| 141 |
+
- ν΅μ¦ μ μ [1μ ]
|
| 142 |
+
- IV/Foley/Drain λ± λΌμΈ μν [1μ ]
|
| 143 |
+
- μ€μν λ³ν/μ°λ €μ¬ν [1μ ]
|
| 144 |
+
|
| 145 |
+
**Recommendation (5μ ):**
|
| 146 |
+
- λ€μ λ¨κ³ κ³ν [2μ ]
|
| 147 |
+
- μ£Όμμ¬ν/λͺ¨λν°λ§ [2μ ]
|
| 148 |
+
- μμ κ²μ¬/μΉλ£ [1μ ]
|
| 149 |
+
|
| 150 |
+
### 2οΈβ£ μ νμ± (Accuracy, 0-25μ )
|
| 151 |
+
**μλ£ μ 보μ μ νμ±μ νκ°ν©λλ€:**
|
| 152 |
+
|
| 153 |
+
- νλ ₯μ§ν μμΉμ μ νμ± [5μ ]
|
| 154 |
+
- κ²μ¬ κ²°κ³Ό μμΉμ μ νμ± [5μ ]
|
| 155 |
+
- μν μ©μ΄μ μ¬λ°λ₯Έ μ¬μ© [5μ ]
|
| 156 |
+
- μκ° μμμ μ νοΏ½οΏ½οΏ½ [5μ ]
|
| 157 |
+
- μΈκ³Όκ΄κ³μ λ
Όλ¦¬μ± [5μ ]
|
| 158 |
+
|
| 159 |
+
**κ°μ μμ:**
|
| 160 |
+
- μμΉ μ€λ₯ (κ° -2μ )
|
| 161 |
+
- μν μ©μ΄ μ€μ© (κ° -2μ )
|
| 162 |
+
- μκ° μμ νΌλ (-3μ )
|
| 163 |
+
- μ€μ μ 보 λλ½ (κ° -3μ )
|
| 164 |
+
|
| 165 |
+
### 3οΈβ£ λͺ
λ£μ± (Clarity, 0-25μ )
|
| 166 |
+
**μμ¬μν΅μ λͺ
νμ±μ νκ°ν©λλ€:**
|
| 167 |
+
|
| 168 |
+
- κ°κ²°νκ³ λͺ
νν νν [7μ ]
|
| 169 |
+
- λ
Όλ¦¬μ μΈ νλ¦ [6μ ]
|
| 170 |
+
- λΆνμν μ€λ³΅ μμ [6μ ]
|
| 171 |
+
- μ λ¬Έμ μ΄λ©΄μ μ΄ν΄νκΈ° μ¬μ΄ νν [6μ ]
|
| 172 |
+
|
| 173 |
+
**κ°μ μμ:**
|
| 174 |
+
- λͺ¨νΈν νν (κ° -2μ )
|
| 175 |
+
- κ³Όλν μ€λ³΅ (κ° -2μ )
|
| 176 |
+
- λΉλ
Όλ¦¬μ μ κ° (-3μ )
|
| 177 |
+
|
| 178 |
+
### 4οΈβ£ μ°μ μμ (Priority, 0-25μ )
|
| 179 |
+
**μ€μ μ 보μ μ°μ μ λ¬μ νκ°ν©λλ€:**
|
| 180 |
+
|
| 181 |
+
- μκΈ/μ£Όμμ¬ν μ°μ μΈκΈ [8μ ]
|
| 182 |
+
- μ€μλμ λ°λ₯Έ μ 보 λ°°μΉ [7μ ]
|
| 183 |
+
- μ¦κ° μ‘°μΉ νμ μ¬ν κ°μ‘° [5μ ]
|
| 184 |
+
- νμ μμ κ΄λ ¨ μ 보 μ°μ [5μ ]
|
| 185 |
+
|
| 186 |
+
**νΉλ³ κ³ λ €μ¬ν:**
|
| 187 |
+
- λΉμ μ νλ ₯μ§ν μ°μ μΈκΈ (+보λμ€)
|
| 188 |
+
- μλ λ₯΄κΈ° μ‘°κΈ° μΈκΈ (+보λμ€)
|
| 189 |
+
- ν΅μ¦/λΆνΈκ° μ¦μ μΈκΈ (+보λμ€)
|
| 190 |
+
|
| 191 |
+
---
|
| 192 |
+
|
| 193 |
+
## π― νκ° μ§μΉ¨
|
| 194 |
+
|
| 195 |
+
1. **νκ΅ κ°νΈ μ€λ¬΄ νμ€ μ€μ**: λνκ°νΈνν μΈμμΈκ³ κ°μ΄λλΌμΈ κΈ°μ€
|
| 196 |
+
2. **νμ μμ μ΅μ°μ **: νμ μμ μ μν₯μ λ―ΈμΉλ μ 보 νκ° κ°ν
|
| 197 |
+
3. **μ€λ¬΄ μ μ©μ±**: μ€μ μμμμ μ¬μ© κ°λ₯ν μμ€ νκ°
|
| 198 |
+
4. **κ΅μ‘μ νΌλλ°±**: 건μ€μ μ΄κ³ ꡬ체μ μΈ κ°μ μ¬ν μ μ
|
| 199 |
+
|
| 200 |
+
## π€ μΆλ ₯ νμ
|
| 201 |
+
|
| 202 |
+
**λ°λμ μλ JSON νμμΌλ‘λ§ μλ΅νμΈμ. λ€λ₯Έ ν
μ€νΈλ ν¬ν¨νμ§ λ§μΈμ.**
|
| 203 |
+
|
| 204 |
+
```json
|
| 205 |
+
{{
|
| 206 |
+
"total_score": 85,
|
| 207 |
+
"category_scores": {{
|
| 208 |
+
"completeness": 22,
|
| 209 |
+
"accuracy": 23,
|
| 210 |
+
"clarity": 20,
|
| 211 |
+
"priority": 20
|
| 212 |
+
}},
|
| 213 |
+
"strengths": [
|
| 214 |
+
"νμμ μ£ΌνΈμμ νμ¬ μνλ₯Ό λͺ
ννκ² κΈ°μ νμ΅λλ€.",
|
| 215 |
+
"λΉμ μ νλ ₯μ§ν(HR 96, BT 37.9β)λ₯Ό μ νν ν¬ν¨νκ³ μ°μ μμλ₯Ό λμμ΅λλ€.",
|
| 216 |
+
"μ£Όμ κ²μ¬ κ²°κ³Ό(WBC 14.8, CRP 6.2)λ₯Ό ꡬ체μ μΌλ‘ μΈκΈνμ΅λλ€.",
|
| 217 |
+
"μκΈ μμ μμ μκ°κ³Ό μ€λΉ μ¬νμ λͺ
νν μ λ¬νμ΅λλ€.",
|
| 218 |
+
"NPO μ μ§ λ° νμμ ν¬μ¬ λ± νμ¬ μΉλ£ κ³νμ μ νν κΈ°μ νμ΅λλ€."
|
| 219 |
+
],
|
| 220 |
+
"improvements": [
|
| 221 |
+
"ν΅μ¦ μ μ(NRS 8/10)λ₯Ό Assessment μΉμ
μλΆλΆμ λͺ
μνμ¬ νμμ λΆνΈκ°μ κ°μ‘°νμΈμ.",
|
| 222 |
+
"IV λΌμΈ μν(D5NS 80 mL/hr)λ₯Ό Assessmentμ μΆκ°νλ©΄ λ μμ ν©λλ€.",
|
| 223 |
+
"Recommendationμμ μμ ν λͺ¨λν°λ§ κ³ν(V/S q4h, ν©λ³μ¦ κ΄μ°°)μ ꡬ체μ μΌλ‘ λͺ
μνμΈμ.",
|
| 224 |
+
"McBurney point μν΅, λ°λ°ν΅ λ± μ 체 κ²μ§ μ견μ Assessmentμ ν¬ν¨νλ©΄ μ’μ΅λλ€.",
|
| 225 |
+
"μ
μ κ²½λ‘(μκΈμ€ κ²½μ )μ μ
μ μκ°μ Backgroundμ μΆκ°νλ©΄ μμ μ±μ΄ ν₯μλ©λλ€."
|
| 226 |
+
],
|
| 227 |
+
"missing_critical_info": [
|
| 228 |
+
"IV λΌμΈ μ’
λ₯ λ° μ μ",
|
| 229 |
+
"μ 체 κ²μ§ μ견 (McBurney point μν΅, λ°λ°ν΅)",
|
| 230 |
+
"μμ ν λͺ¨λν°λ§ κ³ν"
|
| 231 |
+
],
|
| 232 |
+
"safety_concerns": [
|
| 233 |
+
"ν΅μ¦ μ μκ° λμ(NRS 8/10)μλ λΆκ΅¬νκ³ ν΅μ¦ κ΄λ¦¬ μ°μ μμκ° λͺ
ννμ§ μμ΅λλ€.",
|
| 234 |
+
"λΉμ μ νλ ₯μ§ν(λ°μ΄ 37.9β)μ λν μ§μμ λͺ¨λν°λ§ κ³νμ΄ κ΅¬μ²΄μ μ΄μ§ μμ΅λλ€."
|
| 235 |
+
],
|
| 236 |
+
"clinical_reasoning": {{
|
| 237 |
+
"situation_assessment": "νμμ νμμ μ£ΌνΈμμ νμ¬ μνλ₯Ό λͺ
νν νμ
νκ³ μμΌλ, ν΅μ¦μ μ¬κ°μ±μ μΆ©λΆν κ°μ‘°νμ§ λͺ»νμ΅λλ€.",
|
| 238 |
+
"background_completeness": "μ§λ¨λͺ
κ³Ό κ²μ¬ κ²°κ³Όλ μ ν¬ν¨νμΌλ, μ
μ κ²½λ‘μ μκ°μ λν μ λ³΄κ° λΆμ‘±ν©λλ€.",
|
| 239 |
+
"assessment_quality": "νλ ₯μ§νμ μ£Όμ κ²μ¬ κ²°κ³Όλ₯Ό ν¬ν¨νμΌλ, μ 체 κ²μ§ μ견과 IV λΌμΈ μ λ³΄κ° λλ½λμμ΅λλ€.",
|
| 240 |
+
"recommendation_specificity": "μμ κ³νμ λͺ
ννλ, μμ μ ν ꡬ체μ μΈ κ°νΈ μ€μ¬ λ° λͺ¨λν°λ§ κ³νμ΄ λ―Έν‘ν©λλ€."
|
| 241 |
+
}},
|
| 242 |
+
"grade_level": "B+ (μ°μ)",
|
| 243 |
+
"pass_fail": "Pass",
|
| 244 |
+
"detailed_feedback": "μ λ°μ μΌλ‘ SBAR ꡬ쑰λ₯Ό μ μ΄ν΄νκ³ μμΌλ©°, νμμ ν΅μ¬ λ¬Έμ λ₯Ό νμ
νμ¬ μΈμμΈκ³νμ΅λλ€. νΉν λΉμ μ νλ ₯μ§νμ μ£Όμ κ²μ¬ κ²°κ³Όλ₯Ό μ νν ν¬ν¨ν μ μ΄ μ°μν©λλ€.\\n\\n**κ°μ :** Situationμμ νμμ μ£ΌνΈμ(μ°νλ³΅λΆ ν΅μ¦)μ μ΄λ μμμ λͺ
νν κΈ°μ νκ³ , Backgroundμμ κΈμ± μΆ©μμΌ μ§λ¨κ³Ό CT μ견μ ꡬ체μ μΌλ‘ μΈκΈνμ΅λλ€. Recommendationμμ μκΈ μμ μκ°μ λͺ
μν κ²λ μ’μ΅λλ€.\\n\\n**κ°μ μ¬ν:** Assessmentμμ ν΅μ¦ μ μ(NRS 8/10)λ₯Ό λ μλΆλΆμ κ°μ‘°νκ³ , IV λΌμΈ μνμ μ 체 κ²μ§ μ견(McBurney point μν΅, λ°λ°ν΅)μ μΆκ°νλ©΄ μμ μ±μ΄ ν₯μλ©λλ€. Recommendationμμ μμ ν V/S λͺ¨λν°λ§ μ£ΌκΈ°(q4h)μ ν©λ³μ¦ κ΄μ°° νλͺ©μ ꡬ체μ μΌλ‘ λͺ
μνμΈμ.\\n\\n**νμ μμ :** λμ ν΅μ¦ μ μμ λ°μ΄μ λν μ§μμ κ΄μ°°μ΄ νμνλ©°, μ΄λ₯Ό μΈμμΈκ³ μ λ κ°μ‘°ν΄μΌ ν©λλ€. NPO μ μ§μ νμμ ν¬μ¬λ μ μΈκΈνμ΅λλ€.\\n\\n**μ€λ¬΄ μ μ©:** μμμμ μΆ©λΆν μ¬μ© κ°λ₯ν μμ€μ΄λ, μ 체 κ²μ§ μ견과 λΌμΈ μ 보λ₯Ό μΆκ°νλ©΄ λμ± μμ±λ λμ μΈμμΈκ³κ° λ κ²μ
λλ€."
|
| 245 |
+
}}
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
**νκ° μ μ£Όμμ¬ν:**
|
| 249 |
+
1. κΈμ μ μΈ μΈ‘λ©΄μ λ¨Όμ μΈκΈνκ³ κ±΄μ€μ μΈ νΌλλ°± μ 곡
|
| 250 |
+
2. ꡬ체μ μΈ κ°μ λ°©λ² μ μ (μμ ν¬ν¨)
|
| 251 |
+
3. νμ μμ κ³Ό κ΄λ ¨λ μ€μ μ 보 λλ½ μ λͺ
νν μ§μ
|
| 252 |
+
4. μ€μ μμ μ μ© κ°λ₯μ± κ³ λ €
|
| 253 |
+
5. νμμ μμ€μ λ§λ κΈ°λμΉ μ€μ (κ³Όλν λΉν μ§μ)
|
| 254 |
+
"""
|
| 255 |
+
return prompt
|
| 256 |
+
|
| 257 |
+
def _create_default_evaluation(self) -> dict:
|
| 258 |
+
"""API μ€λ₯ μ κΈ°λ³Έ νκ° κ²°κ³Ό λ°ν"""
|
| 259 |
+
return {
|
| 260 |
+
"total_score": 0,
|
| 261 |
+
"category_scores": {
|
| 262 |
+
"completeness": 0,
|
| 263 |
+
"accuracy": 0,
|
| 264 |
+
"clarity": 0,
|
| 265 |
+
"priority": 0,
|
| 266 |
+
},
|
| 267 |
+
"strengths": ["νκ°λ₯Ό μ§νν μ μμ΅λλ€."],
|
| 268 |
+
"improvements": ["API μ€λ₯κ° λ°μνμ΅λλ€. λμ€μ λ€μ μλν΄μ£ΌμΈμ."],
|
| 269 |
+
"detailed_feedback": "νκ° μμ€ν
μ λ¬Έμ κ° λ°μνμ΅λλ€. κ΄λ¦¬μμκ² λ¬ΈμνμΈμ.",
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
def chat_feedback(
|
| 273 |
+
self,
|
| 274 |
+
user_message: str,
|
| 275 |
+
chat_history: list,
|
| 276 |
+
scenario_data: dict,
|
| 277 |
+
patient_data: dict,
|
| 278 |
+
) -> str:
|
| 279 |
+
"""
|
| 280 |
+
λνν SBAR νΌλλ°± μ 곡
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
user_message: νμμ νμ¬ λ©μμ§
|
| 284 |
+
chat_history: μ΄μ λν κΈ°λ‘ [(role, message), ...]
|
| 285 |
+
scenario_data: μλλ¦¬μ€ λ°μ΄ν° (EMR μ 보)
|
| 286 |
+
patient_data: νμ κΈ°λ³Έ μ 보
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
AI νΌλλ°± λ©μμ§
|
| 290 |
+
"""
|
| 291 |
+
prompt = self._create_chat_feedback_prompt(
|
| 292 |
+
user_message, chat_history, scenario_data, patient_data
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
try:
|
| 296 |
+
response = self.model.generate_content(
|
| 297 |
+
prompt,
|
| 298 |
+
generation_config={
|
| 299 |
+
"temperature": 0.8,
|
| 300 |
+
"top_p": 0.95,
|
| 301 |
+
"max_output_tokens": 1500,
|
| 302 |
+
},
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
return response.text.strip()
|
| 306 |
+
|
| 307 |
+
except Exception as e:
|
| 308 |
+
print(f"Gemini API μ€λ₯: {e}")
|
| 309 |
+
return "μ£μ‘ν©λλ€. μΌμμ μΈ μ€λ₯κ° λ°μνμ΅λλ€. λ€μ μλν΄μ£ΌμΈμ."
|
| 310 |
+
|
| 311 |
+
def _create_chat_feedback_prompt(
|
| 312 |
+
self, user_message: str, chat_history: list, scenario_data: dict, patient_data: dict
|
| 313 |
+
) -> str:
|
| 314 |
+
"""λνν νΌλλ°± ν둬ννΈ μμ±"""
|
| 315 |
+
|
| 316 |
+
# μ΄μ λν κΈ°λ‘ ν¬λ§·ν
|
| 317 |
+
history_text = ""
|
| 318 |
+
if chat_history:
|
| 319 |
+
for role, message in chat_history:
|
| 320 |
+
if role == "user":
|
| 321 |
+
history_text += f"\nνμ: {message}\n"
|
| 322 |
+
else:
|
| 323 |
+
history_text += f"\nAI κ΅μ: {message}\n"
|
| 324 |
+
|
| 325 |
+
prompt = f"""λΉμ μ 20λ
κ²½λ ₯μ νκ΅ κ°νΈ κ΅μ‘ μ λ¬Έκ°μ΄μ κ°νΈν κ΅μμ
λλ€.
|
| 326 |
+
νμμ΄ SBAR νμμ μΈμμΈκ³λ₯Ό μ°μ΅νκ³ μμΌλ©°, λνλ₯Ό ν΅ν΄ νΌλλ°±μ μ 곡νκ³ μμ΅λλ€.
|
| 327 |
+
|
| 328 |
+
## π νμ μ 보 (EMR λ°μ΄ν°)
|
| 329 |
+
- **νμλͺ
**: {patient_data.get('name')} ({patient_data.get('age')}μΈ {patient_data.get('gender')})
|
| 330 |
+
- **μ§λ¨λͺ
**: {patient_data.get('diagnosis')}
|
| 331 |
+
- **μ
μμΌ**: {patient_data.get('admission_date')}
|
| 332 |
+
- **μλ λ₯΄κΈ°**: {patient_data.get('allergies')}
|
| 333 |
+
|
| 334 |
+
## π₯ μΈμμΈκ³ μν©
|
| 335 |
+
- **μλ리μ€**: {scenario_data.get('title')}
|
| 336 |
+
- **μΈκ³ μν©**: {scenario_data.get('handoff_situation')}
|
| 337 |
+
|
| 338 |
+
### π νλ ₯μ§ν (Vital Signs)
|
| 339 |
+
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
|
| 340 |
+
|
| 341 |
+
### π§ͺ κ²μ¬ κ²°κ³Ό (Laboratory Results)
|
| 342 |
+
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
|
| 343 |
+
|
| 344 |
+
### π μμ¬ μ²λ°© (Physician Orders)
|
| 345 |
+
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
|
| 346 |
+
|
| 347 |
+
### π κ°νΈ κΈ°λ‘ (Nursing Notes)
|
| 348 |
+
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
|
| 349 |
+
|
| 350 |
+
---
|
| 351 |
+
|
| 352 |
+
## π¬ μ΄μ λν κΈ°λ‘
|
| 353 |
+
{history_text if history_text else "(첫 λνμ
λλ€)"}
|
| 354 |
+
|
| 355 |
+
---
|
| 356 |
+
|
| 357 |
+
## π νμμ νμ¬ λ©μμ§
|
| 358 |
+
{user_message}
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
## π νΌλλ°± κ°μ΄λλΌμΈ
|
| 363 |
+
|
| 364 |
+
1. **μΉμ νκ³ κ²©λ €μ μΈ ν€**: νμμ΄ νΈμνκ² μ§λ¬Ένκ³ κ°μ ν μ μλλ‘ μ§μ
|
| 365 |
+
2. **SBAR ꡬ쑰 μ€μ νμΈ**: νμμ΄ μμ±ν λ΄μ©μ΄ SBAR νμμ λ°λ₯΄λμ§ νμΈ
|
| 366 |
+
3. **ꡬ체μ μΈ κ°μ μ μ**: λ§μ°ν νΌλλ°±μ΄ μλ ꡬ체μ μΈ μμ μ 곡
|
| 367 |
+
4. **μ€μ μ 보 λλ½ μ§μ **: EMR λ°μ΄ν°μ μλ μ€μν μ λ³΄κ° λΉ μ‘λ€λ©΄ μΈκΈ
|
| 368 |
+
5. **νμ μμ κ°μ‘°**: νμ μμ κ³Ό κ΄λ ¨λ μ 보λ νΉν κ°μ‘°
|
| 369 |
+
6. **λ¨κ³μ κ°μ **: ν λ²μ λͺ¨λ κ²μ κ³ μΉλ € νμ§ λ§κ³ , μ°μ μμκ° λμ κ²λΆν° μ μ
|
| 370 |
+
7. **μ΅μ’
μ μΆ μ **: μ μλ₯Ό λ§€κΈ°μ§ λ§κ³ , κ°μ λ°©ν₯λ§ μ μ
|
| 371 |
+
|
| 372 |
+
## μλ΅ νμ
|
| 373 |
+
|
| 374 |
+
- μμ°μ€λ¬μ΄ λνμ²΄λ‘ μλ΅νμΈμ.
|
| 375 |
+
- νμμ΄ μν λΆλΆμ λ¨Όμ μΉμ°¬νμΈμ.
|
| 376 |
+
- κ°μ μ΄ νμν λΆλΆμ "~νλ©΄ λ μ’μ κ² κ°μμ", "~λ₯Ό μΆκ°ν΄λ³΄λ 건 μ΄λ¨κΉμ?" κ°μ μ μ νμμΌλ‘ μ μνμΈμ.
|
| 377 |
+
- νμμ μμλ₯Ό λ€μ΄ μ€λͺ
νμΈμ.
|
| 378 |
+
- νμμ΄ "μ΅μ’
μ μΆ"μ μΈκΈνμ§ μλ ν, μ μλ λ±κΈμ λ§€κΈ°μ§ λ§μΈμ.
|
| 379 |
+
|
| 380 |
+
μλ΅μ μμ±ν΄μ£ΌμΈμ:
|
| 381 |
+
"""
|
| 382 |
+
return prompt
|