LearningnRunning commited on
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 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