Commit
Β·
3a338e5
1
Parent(s):
0136146
feat: Integrate Supabase for data management, replacing local PostgreSQL. Implement Supabase service for CRUD operations on patients and scenarios, and update application logic to utilize Supabase API. Add Dockerfile for containerization and .dockerignore for build optimization.
Browse files- .dockerignore +66 -0
- Dockerfile +41 -0
- README.md +172 -1
- app.py +189 -206
- config/database.py +63 -34
- docker-compose.yml +0 -23
- init_db.py +16 -11
- models/__init__.py +0 -0
- models/chat_history.py +0 -44
- models/handoff_record.py +0 -42
- models/patient_data.py +0 -65
- pyproject.toml +3 -2
- services/db_service.py +0 -296
- services/gemini_service.py +172 -0
- services/supabase_service.py +647 -0
- start.sh +22 -8
- supabase_init.sql +331 -0
.dockerignore
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
.gitattributes
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
*.egg-info/
|
| 13 |
+
dist/
|
| 14 |
+
build/
|
| 15 |
+
|
| 16 |
+
# Virtual environments
|
| 17 |
+
.venv/
|
| 18 |
+
venv/
|
| 19 |
+
ENV/
|
| 20 |
+
env/
|
| 21 |
+
|
| 22 |
+
# IDE
|
| 23 |
+
.vscode/
|
| 24 |
+
.idea/
|
| 25 |
+
*.swp
|
| 26 |
+
*.swo
|
| 27 |
+
.cursor/
|
| 28 |
+
|
| 29 |
+
# Documentation
|
| 30 |
+
*.md
|
| 31 |
+
!README.md
|
| 32 |
+
|
| 33 |
+
# Test files
|
| 34 |
+
test_*.py
|
| 35 |
+
*_test.py
|
| 36 |
+
tests/
|
| 37 |
+
examples/
|
| 38 |
+
|
| 39 |
+
# Build files
|
| 40 |
+
*.lock
|
| 41 |
+
.pytest_cache/
|
| 42 |
+
.coverage
|
| 43 |
+
htmlcov/
|
| 44 |
+
|
| 45 |
+
# OS
|
| 46 |
+
.DS_Store
|
| 47 |
+
Thumbs.db
|
| 48 |
+
|
| 49 |
+
# Database
|
| 50 |
+
*.db
|
| 51 |
+
*.sqlite3
|
| 52 |
+
|
| 53 |
+
# Logs
|
| 54 |
+
*.log
|
| 55 |
+
|
| 56 |
+
# Docker
|
| 57 |
+
Dockerfile.*
|
| 58 |
+
docker-compose*.yml
|
| 59 |
+
.dockerignore
|
| 60 |
+
|
| 61 |
+
# Misc
|
| 62 |
+
.svn
|
| 63 |
+
CVS
|
| 64 |
+
*.orig
|
| 65 |
+
*.rej
|
| 66 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python 3.12 κΈ°λ° μ΄λ―Έμ§
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# μμ
λλ ν 리 μ€μ
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# μμ€ν
μμ‘΄μ± μ€μΉ
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
gcc \
|
| 10 |
+
postgresql-client \
|
| 11 |
+
curl \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# νκ²½ λ³μ μ€μ
|
| 15 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 16 |
+
PYTHONDONTWRITEBYTECODE=1
|
| 17 |
+
|
| 18 |
+
# uv μ€μΉ (λΉ λ₯Έ Python ν¨ν€μ§ κ΄λ¦¬μ)
|
| 19 |
+
RUN pip install --no-cache-dir uv
|
| 20 |
+
|
| 21 |
+
# μμ‘΄μ± νμΌ λ³΅μ¬ λ° μ€μΉ
|
| 22 |
+
COPY pyproject.toml ./
|
| 23 |
+
RUN uv pip install --system -e .
|
| 24 |
+
|
| 25 |
+
# μ ν리μΌμ΄μ
μ½λ 볡μ¬
|
| 26 |
+
COPY config/ ./config/
|
| 27 |
+
COPY models/ ./models/
|
| 28 |
+
COPY services/ ./services/
|
| 29 |
+
COPY data/ ./data/
|
| 30 |
+
COPY app.py init_db.py ./
|
| 31 |
+
|
| 32 |
+
# ν¬νΈ λ
ΈμΆ (Gradio κΈ°λ³Έ ν¬νΈ)
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
|
| 35 |
+
# ν¬μ€μ²΄ν¬ μΆκ°
|
| 36 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 37 |
+
CMD curl -f http://localhost:7860/ || exit 1
|
| 38 |
+
|
| 39 |
+
# μμ μ€ν¬λ¦½νΈ
|
| 40 |
+
CMD ["sh", "-c", "uv run python init_db.py && uv run python app.py"]
|
| 41 |
+
|
README.md
CHANGED
|
@@ -1,2 +1,173 @@
|
|
| 1 |
-
#
|
|
|
|
| 2 |
Clinical Handover Training Chatbot utilizing SBAR/ISBAR, EMR data, and immersive technology (Metaverse/VR) for standardized, high-fidelity nursing handover practice.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π₯ κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ
|
| 2 |
+
|
| 3 |
Clinical Handover Training Chatbot utilizing SBAR/ISBAR, EMR data, and immersive technology (Metaverse/VR) for standardized, high-fidelity nursing handover practice.
|
| 4 |
+
|
| 5 |
+
## π κ°μ
|
| 6 |
+
|
| 7 |
+
κ°νΈμ¬ SBAR νμ μΈμμΈκ³ νλ ¨μ μν AI κΈ°λ° κ΅μ‘ νλ«νΌμ
λλ€. Gradio κΈ°λ° μΉ μΈν°νμ΄μ€λ₯Ό ν΅ν΄ μ€μ EMR λ°μ΄ν°λ₯Ό νμ©ν μΈμμΈκ³ μ°μ΅μ μ 곡ν©λλ€.
|
| 8 |
+
|
| 9 |
+
## π μμνκΈ°
|
| 10 |
+
|
| 11 |
+
### 1. μ μ₯μ ν΄λ‘
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
git clone https://github.com/your-username/nurse-handover-simulator.git
|
| 15 |
+
cd nurse-handover-simulator
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### 2. νκ²½ λ³μ μ€μ
|
| 19 |
+
|
| 20 |
+
#### Supabase μ¬μ© (κΆμ₯)
|
| 21 |
+
|
| 22 |
+
1. `ENV_EXAMPLE` νμΌμ `.env`λ‘ λ³΅μ¬ν©λλ€:
|
| 23 |
+
```bash
|
| 24 |
+
cp ENV_EXAMPLE .env
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
2. Supabase νλ‘μ νΈ μ€μ μμ νμν μ 보λ₯Ό 볡μ¬ν©λλ€:
|
| 28 |
+
- Project URL: `https://project_id.supabase.co`
|
| 29 |
+
- API Key (anon key)
|
| 30 |
+
- Database URL (Connection String)
|
| 31 |
+
|
| 32 |
+
3. `.env` νμΌμ Supabase μ 보λ₯Ό μ
λ ₯ν©λλ€:
|
| 33 |
+
```env
|
| 34 |
+
SUPABASE_URL=https://[project_id].supabase.co
|
| 35 |
+
SUPABASE_ANON_KEY=your_anon_key_here
|
| 36 |
+
SUPABASE_DB_URL=postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
|
| 37 |
+
GOOGLE_API_KEY=your_google_api_key_here
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
#### λ‘컬 PostgreSQL μ¬μ© (κ°λ° νκ²½)
|
| 41 |
+
|
| 42 |
+
νκ²½ λ³μμ `SUPABASE_DB_URL`μ μ€μ νμ§ μμΌλ©΄ μλμΌλ‘ λ‘컬 PostgreSQLμ μ¬μ©ν©λλ€.
|
| 43 |
+
|
| 44 |
+
### 3. μμ‘΄μ± μ€μΉ
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
# uvλ₯Ό μ¬μ©νλ κ²½μ°
|
| 48 |
+
uv sync
|
| 49 |
+
|
| 50 |
+
# λλ μΌλ° pip μ¬μ©
|
| 51 |
+
pip install -e .
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### 4. Google Gemini API ν€ λ°κΈ
|
| 55 |
+
|
| 56 |
+
1. [Google AI Studio](https://makersuite.google.com/app/apikey)μμ API ν€ λ°κΈ
|
| 57 |
+
2. `.env` νμΌμ `GOOGLE_API_KEY` μΆκ°
|
| 58 |
+
|
| 59 |
+
### 5. μ ν리μΌμ΄μ
μ€ν
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
# start.sh μ¬μ© (κΆμ₯)
|
| 63 |
+
bash start.sh
|
| 64 |
+
|
| 65 |
+
# λλ μ§μ μ€ν
|
| 66 |
+
uv run app.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## π³ Docker λ°°ν¬
|
| 70 |
+
|
| 71 |
+
### λ‘컬 Docker μ€ν
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# Docker μ΄λ―Έμ§ λΉλ
|
| 75 |
+
docker build -t nurse-handover-simulator .
|
| 76 |
+
|
| 77 |
+
# Docker 컨ν
μ΄λ μ€ν (νκ²½ λ³μ ν¬ν¨)
|
| 78 |
+
docker run -p 7860:7860 \
|
| 79 |
+
-e SUPABASE_URL="https://your-project.supabase.co" \
|
| 80 |
+
-e SUPABASE_ANON_KEY="your_key" \
|
| 81 |
+
-e SUPABASE_DB_URL="postgresql://..." \
|
| 82 |
+
-e GOOGLE_API_KEY="your_key" \
|
| 83 |
+
nurse-handover-simulator
|
| 84 |
+
|
| 85 |
+
# λλ .env νμΌ μ¬μ©
|
| 86 |
+
docker run -p 7860:7860 --env-file .env nurse-handover-simulator
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Hugging Face Spaces λ°°ν¬
|
| 90 |
+
|
| 91 |
+
1. Hugging Face Spacesμ μ Space μμ± (Docker νμ
μ ν)
|
| 92 |
+
2. Repository Secretsμ λ€μ νκ²½ λ³μ λ±λ‘:
|
| 93 |
+
- `SUPABASE_URL`
|
| 94 |
+
- `SUPABASE_ANON_KEY`
|
| 95 |
+
- `SUPABASE_DB_URL`
|
| 96 |
+
- `GOOGLE_API_KEY`
|
| 97 |
+
3. Dockerfileκ³Ό μ½λ νΈμ
|
| 98 |
+
4. μλ λΉλ λ° λ°°ν¬ μλ£
|
| 99 |
+
|
| 100 |
+
## π νλ‘μ νΈ κ΅¬μ‘°
|
| 101 |
+
|
| 102 |
+
```
|
| 103 |
+
nurse-handover-simulator/
|
| 104 |
+
βββ app.py # Gradio λ©μΈ μ ν리μΌμ΄μ
|
| 105 |
+
βββ config/
|
| 106 |
+
β βββ database.py # λ°μ΄ν°λ² μ΄μ€ μ°κ²° μ€μ
|
| 107 |
+
βββ models/
|
| 108 |
+
β βββ patient_data.py # νμ λ° μλλ¦¬μ€ λͺ¨λΈ
|
| 109 |
+
β βββ handoff_record.py # μΈμμΈκ³ κΈ°λ‘ λͺ¨λΈ
|
| 110 |
+
β βββ chat_history.py # μ±ν
νμ€ν 리 λͺ¨λΈ
|
| 111 |
+
βββ services/
|
| 112 |
+
β βββ db_service.py # λ°μ΄ν°λ² μ΄μ€ CRUD μλΉμ€
|
| 113 |
+
β βββ gemini_service.py # Google Gemini AI μλΉμ€
|
| 114 |
+
βββ data/
|
| 115 |
+
β βββ scenarios.json # μλλ¦¬μ€ λ°μ΄ν°
|
| 116 |
+
βββ Dockerfile # Docker λ°°ν¬ μ€μ
|
| 117 |
+
βββ docker-compose.yml # λ‘컬 PostgreSQL μ€μ
|
| 118 |
+
βββ ENV_EXAMPLE # νκ²½ λ³μ ν
νλ¦Ώ
|
| 119 |
+
βββ start.sh # μμ μ€ν¬λ¦½νΈ
|
| 120 |
+
βββ init_db.py # λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€ν¬λ¦½νΈ
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## π νκ²½ λ³μ μ€λͺ
|
| 124 |
+
|
| 125 |
+
| λ³μ | μ€λͺ
| νμ μ¬λΆ |
|
| 126 |
+
|------|------|---------|
|
| 127 |
+
| `SUPABASE_URL` | Supabase νλ‘μ νΈ URL | Supabase μ¬μ© μ |
|
| 128 |
+
| `SUPABASE_ANON_KEY` | Supabase Anon API Key | Supabase μ¬μ© μ |
|
| 129 |
+
| `SUPABASE_DB_URL` | Supabase λ°μ΄ν°λ² μ΄μ€ μ°κ²° URL | Supabase μ¬μ© μ |
|
| 130 |
+
| `GOOGLE_API_KEY` | Google Gemini API ν€ | νμ |
|
| 131 |
+
| `DB_HOST` | λ‘컬 PostgreSQL νΈμ€νΈ | λ‘컬 PostgreSQL μ¬μ© μ |
|
| 132 |
+
| `DB_PORT` | λ‘컬 PostgreSQL ν¬νΈ | λ‘컬 PostgreSQL μ¬μ© μ |
|
| 133 |
+
|
| 134 |
+
## π 보μ κ°μ΄λ
|
| 135 |
+
|
| 136 |
+
### νκ²½ λ³μ κ΄λ¦¬
|
| 137 |
+
|
| 138 |
+
1. **μ λ `.env` νμΌμ Gitμ 컀λ°νμ§ λ§μΈμ**
|
| 139 |
+
- `.gitignore`μ `.env`κ° μ΄λ―Έ ν¬ν¨λμ΄ μμ΅λλ€
|
| 140 |
+
|
| 141 |
+
2. **Supabase Keys**
|
| 142 |
+
- `SUPABASE_ANON_KEY`: ν΄λΌμ΄μΈνΈ λ
ΈμΆ κ°λ₯ (RLSλ‘ λ³΄νΈλ¨)
|
| 143 |
+
- `SUPABASE_DB_URL`: μλ²μμλ§ μ¬μ©, λ
ΈμΆ κΈμ§
|
| 144 |
+
|
| 145 |
+
3. **Google API Key**
|
| 146 |
+
- μλ²μμλ§ μ¬μ©, λ
ΈμΆ κΈμ§
|
| 147 |
+
|
| 148 |
+
4. **Hugging Face Spaces**
|
| 149 |
+
- Repository Secretsλ₯Ό μ¬μ©νμ¬ νκ²½ λ³μλ₯Ό μμ νκ² κ΄λ¦¬
|
| 150 |
+
|
| 151 |
+
## π μ£Όμ κΈ°λ₯
|
| 152 |
+
|
| 153 |
+
- π **EMR μ 보 μ‘°ν**: μ€μ λ³μ EMRκ³Ό μ μ¬ν μΈν°νμ΄μ€
|
| 154 |
+
- π¬ **AI νΌλλ°±**: Google Gemini AIλ₯Ό νμ©ν μ€μκ° νΌλλ°±
|
| 155 |
+
- π **SBAR νμ μΈμμΈκ³**: νμ€ μΈμμΈκ³ νμ μ°μ΅
|
| 156 |
+
- π **νκ° μμ€ν
**: μμ μ±, μ νμ±, λͺ
λ£μ±, μ°μ μμ νκ°
|
| 157 |
+
- π **νμ μ΄λ ₯ μ‘°ν**: νμλ³ νμ΅ μ΄λ ₯ λ° ν΅κ³
|
| 158 |
+
|
| 159 |
+
## π οΈ κΈ°μ μ€ν
|
| 160 |
+
|
| 161 |
+
- **Backend**: Python 3.12
|
| 162 |
+
- **UI**: Gradio
|
| 163 |
+
- **Database**: Supabase (PostgreSQL) / λ‘컬 PostgreSQL
|
| 164 |
+
- **AI**: Google Gemini 2.0 Flash
|
| 165 |
+
- **ORM**: SQLAlchemy
|
| 166 |
+
|
| 167 |
+
## π λΌμ΄μ μ€
|
| 168 |
+
|
| 169 |
+
MIT License
|
| 170 |
+
|
| 171 |
+
## π€ κΈ°μ¬νκΈ°
|
| 172 |
+
|
| 173 |
+
Pull Requestλ μΈμ λ νμν©λλ€!
|
app.py
CHANGED
|
@@ -2,24 +2,41 @@
|
|
| 2 |
κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ - Gradio λ©ν°νμ΄μ§ μ ν리μΌμ΄μ
(μ±ν
κΈ°λ°)
|
| 3 |
"""
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
-
from config.database import SessionLocal, init_db
|
| 7 |
-
from services.db_service import DatabaseService, load_scenarios_to_db
|
| 8 |
from services.gemini_service import GeminiEvaluator
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν
|
| 11 |
try:
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
except Exception as e:
|
| 21 |
print(f"β οΈ λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€ μ€λ₯: {e}")
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# Gemini νκ°κΈ° μ΄κΈ°ν
|
| 25 |
evaluator = GeminiEvaluator()
|
|
@@ -321,19 +338,45 @@ def load_scenario_data(scenario_id: str):
|
|
| 321 |
if not scenario_id:
|
| 322 |
return None, None, ""
|
| 323 |
|
| 324 |
-
|
| 325 |
-
db_service = DatabaseService(db)
|
| 326 |
-
|
| 327 |
-
scenario = db_service.get_scenario(scenario_id)
|
| 328 |
-
patient = db_service.get_patient(scenario.patient_id) if scenario else None
|
| 329 |
|
| 330 |
-
|
|
|
|
| 331 |
|
| 332 |
if not scenario or not patient:
|
| 333 |
return None, None, "μλ리μ€λ₯Ό μ°Ύμ μ μμ΅λλ€."
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
|
| 338 |
|
| 339 |
def handle_chat_message(message, chat_history, session_id, student_id, scenario_id):
|
|
@@ -354,16 +397,14 @@ def handle_chat_message(message, chat_history, session_id, student_id, scenario_
|
|
| 354 |
"""
|
| 355 |
return chat_history + [[message, warning_msg]], ""
|
| 356 |
|
| 357 |
-
#
|
| 358 |
-
|
| 359 |
-
db_service = DatabaseService(db)
|
| 360 |
|
| 361 |
# μλλ¦¬μ€ λ° νμ μ 보 μ‘°ν
|
| 362 |
-
scenario =
|
| 363 |
-
patient =
|
| 364 |
|
| 365 |
if not scenario or not patient:
|
| 366 |
-
db.close()
|
| 367 |
error_msg = """
|
| 368 |
β **μλλ¦¬μ€ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€.**
|
| 369 |
|
|
@@ -372,26 +413,26 @@ EMR μ 보 μ‘°ν νμμ μλ리μ€λ₯Ό λ€μ μ νν΄μ£ΌμΈμ.
|
|
| 372 |
return chat_history + [[message, error_msg]], ""
|
| 373 |
|
| 374 |
# μ¬μ©μ λ©μμ§ μ μ₯
|
| 375 |
-
|
| 376 |
|
| 377 |
# μλλ¦¬μ€ λ° νμ λ°μ΄ν° μ€λΉ
|
| 378 |
scenario_data = {
|
| 379 |
-
"title": scenario
|
| 380 |
-
"handoff_situation": scenario.handoff_situation,
|
| 381 |
-
"hospital_day": scenario.hospital_day,
|
| 382 |
-
"vitals": scenario.vitals,
|
| 383 |
-
"labs": scenario.labs,
|
| 384 |
-
"orders": scenario.orders,
|
| 385 |
-
"nursing_notes": scenario.nursing_notes,
|
| 386 |
}
|
| 387 |
|
| 388 |
patient_data = {
|
| 389 |
-
"name": patient
|
| 390 |
-
"age": patient
|
| 391 |
-
"gender": patient
|
| 392 |
-
"diagnosis": patient
|
| 393 |
-
"admission_date": str(patient
|
| 394 |
-
"allergies": patient
|
| 395 |
}
|
| 396 |
|
| 397 |
# μ΄μ μ±ν
κΈ°λ‘ ν¬λ§·ν
(Geminiμ©)
|
|
@@ -406,9 +447,7 @@ EMR μ 보 μ‘°ν νμμ μλ리μ€λ₯Ό λ€μ μ νν΄μ£ΌμΈμ.
|
|
| 406 |
ai_response = evaluator.chat_feedback(message, history_for_gemini, scenario_data, patient_data)
|
| 407 |
|
| 408 |
# AI μλ΅ μ μ₯
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
db.close()
|
| 412 |
|
| 413 |
# μ±ν
νμ€ν 리 μ
λ°μ΄νΈ
|
| 414 |
updated_history = chat_history + [[message, ai_response]]
|
|
@@ -416,160 +455,113 @@ EMR μ 보 μ‘°ν νμμ μλ리μ€λ₯Ό λ€μ μ νν΄μ£ΌμΈμ.
|
|
| 416 |
return updated_history, ""
|
| 417 |
|
| 418 |
|
| 419 |
-
def
|
| 420 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
if not chat_history:
|
| 422 |
-
return chat_history + [[None, "
|
| 423 |
-
|
| 424 |
-
# μ±ν
κΈ°λ‘μμ SBAR μΆμΆ (νμμ λ§μ§λ§ λ©μμ§λ€ λΆμ)
|
| 425 |
-
# κ°λ¨ν νκΈ° μν΄ νμμκ² λͺ
μμ μΌλ‘ SBAR νμμΌλ‘ μμ±νλλ‘ μμ²
|
| 426 |
-
final_message = """
|
| 427 |
-
μ΅μ’
μ μΆμ μμ²νμ
¨μ΅λλ€.
|
| 428 |
-
|
| 429 |
-
μ§κΈκΉμ§ μμ±νμ λ΄μ©μ λ°νμΌλ‘ μ΅μ’
νκ°λ₯Ό μ§ννκ² μ΅λλ€.
|
| 430 |
-
SBAR νμμΌλ‘ μ 리λ λ΄μ©μ λ€μ ν λ² μμ±ν΄μ£ΌμΈμ:
|
| 431 |
-
|
| 432 |
-
**S - Situation (μν©):**
|
| 433 |
-
[μ¬κΈ°μ μμ±]
|
| 434 |
-
|
| 435 |
-
**B - Background (λ°°κ²½):**
|
| 436 |
-
[μ¬κΈ°μ μμ±]
|
| 437 |
-
|
| 438 |
-
**A - Assessment (νκ°):**
|
| 439 |
-
[μ¬κΈ°μ μμ±]
|
| 440 |
-
|
| 441 |
-
**R - Recommendation (κΆκ³ μ¬ν):**
|
| 442 |
-
[μ¬κΈ°μ μμ±]
|
| 443 |
-
|
| 444 |
-
μ νμμΌλ‘ μμ± ν λ€μ "μ΅μ’
μ μΆ" λ²νΌμ λλ¬μ£ΌμΈμ.
|
| 445 |
-
"""
|
| 446 |
-
|
| 447 |
-
# μ€μ ꡬνμμλ μ±ν
κΈ°λ‘μ νμ±νμ¬ SBAR μΆμΆ
|
| 448 |
-
# μ¬κΈ°μλ κ°λ¨ν μλ΄ λ©μμ§λ§ λ°ν
|
| 449 |
-
return chat_history + [[None, final_message]], ""
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
def extract_sbar_from_message(message):
|
| 453 |
-
"""λ©μμ§μμ SBAR μΆμΆ"""
|
| 454 |
-
import re
|
| 455 |
-
|
| 456 |
-
sbar = {
|
| 457 |
-
"situation": "",
|
| 458 |
-
"background": "",
|
| 459 |
-
"assessment": "",
|
| 460 |
-
"recommendation": ""
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
# S, B, A, R μΉμ
μΆμΆ
|
| 464 |
-
s_match = re.search(r'\*\*S.*?Situation.*?\*\*:?\s*(.*?)(?=\*\*B|\*\*Background|$)', message, re.DOTALL | re.IGNORECASE)
|
| 465 |
-
b_match = re.search(r'\*\*B.*?Background.*?\*\*:?\s*(.*?)(?=\*\*A|\*\*Assessment|$)', message, re.DOTALL | re.IGNORECASE)
|
| 466 |
-
a_match = re.search(r'\*\*A.*?Assessment.*?\*\*:?\s*(.*?)(?=\*\*R|\*\*Recommendation|$)', message, re.DOTALL | re.IGNORECASE)
|
| 467 |
-
r_match = re.search(r'\*\*R.*?Recommendation.*?\*\*:?\s*(.*?)$', message, re.DOTALL | re.IGNORECASE)
|
| 468 |
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
if b_match:
|
| 472 |
-
sbar["background"] = b_match.group(1).strip()
|
| 473 |
-
if a_match:
|
| 474 |
-
sbar["assessment"] = a_match.group(1).strip()
|
| 475 |
-
if r_match:
|
| 476 |
-
sbar["recommendation"] = r_match.group(1).strip()
|
| 477 |
-
|
| 478 |
-
return sbar
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
def process_final_submission(message, chat_history, session_id, student_id, scenario_id):
|
| 482 |
-
"""SBAR νμ λ©μμ§λ₯Ό λ°μ μ΅μ’
νκ° μν"""
|
| 483 |
-
# SBAR μΆμΆ
|
| 484 |
-
sbar_data = extract_sbar_from_message(message)
|
| 485 |
-
|
| 486 |
-
# λͺ¨λ μΉμ
μ΄ μμ±λμλμ§ νμΈ
|
| 487 |
-
if not all([sbar_data["situation"], sbar_data["background"],
|
| 488 |
-
sbar_data["assessment"], sbar_data["recommendation"]]):
|
| 489 |
-
return chat_history + [[message, "SBAR νμμ΄ μμ νμ§ μμ΅λλ€. S, B, A, R λͺ¨λ μΉμ
μ μμ±ν΄μ£ΌμΈμ."]], ""
|
| 490 |
-
|
| 491 |
-
# λ°μ΄ν°λ² μ΄μ€ μ°κ²°
|
| 492 |
-
db = SessionLocal()
|
| 493 |
-
db_service = DatabaseService(db)
|
| 494 |
|
| 495 |
# μλλ¦¬μ€ λ° νμ μ 보 μ‘°ν
|
| 496 |
-
scenario =
|
| 497 |
-
patient =
|
| 498 |
|
| 499 |
if not scenario or not patient:
|
| 500 |
-
|
| 501 |
-
return chat_history + [[message, "μλλ¦¬μ€ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€."]], ""
|
| 502 |
|
| 503 |
# μλλ¦¬μ€ λ° νμ λ°μ΄ν° μ€λΉ
|
| 504 |
scenario_data = {
|
| 505 |
-
"title": scenario
|
| 506 |
-
"handoff_situation": scenario.handoff_situation,
|
| 507 |
-
"hospital_day": scenario.hospital_day,
|
| 508 |
-
"vitals": scenario.vitals,
|
| 509 |
-
"labs": scenario.labs,
|
| 510 |
-
"orders": scenario.orders,
|
| 511 |
-
"nursing_notes": scenario.nursing_notes,
|
| 512 |
}
|
| 513 |
|
| 514 |
patient_data = {
|
| 515 |
-
"name": patient
|
| 516 |
-
"age": patient
|
| 517 |
-
"gender": patient
|
| 518 |
-
"diagnosis": patient
|
| 519 |
-
"admission_date": str(patient
|
| 520 |
-
"allergies": patient
|
| 521 |
}
|
| 522 |
|
| 523 |
-
# Gemini
|
| 524 |
-
evaluation = evaluator.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
|
| 526 |
-
# λ°μ΄ν°λ² μ΄μ€μ μ μ₯
|
| 527 |
try:
|
| 528 |
-
record =
|
| 529 |
student_id=student_id,
|
| 530 |
scenario_id=scenario_id,
|
| 531 |
-
sbar_data=
|
| 532 |
evaluation=evaluation,
|
| 533 |
session_id=session_id
|
| 534 |
)
|
| 535 |
-
print(f"β
μΈμμΈκ³ κΈ°λ‘ μ μ₯ μλ£ (ID: {record.id})")
|
|
|
|
|
|
|
|
|
|
| 536 |
except Exception as e:
|
| 537 |
print(f"β οΈ λ°μ΄ν°λ² μ΄μ€ μ μ₯ μ€λ₯: {e}")
|
| 538 |
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
improvements = evaluation.get("improvements", [])
|
| 546 |
-
detailed_feedback = evaluation.get("detailed_feedback", "")
|
| 547 |
|
| 548 |
result_message = f"""
|
| 549 |
-
|
| 550 |
|
| 551 |
-
|
| 552 |
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
- μ νμ± (Accuracy): {category_scores.get('accuracy', 0)} / 25
|
| 556 |
-
- λͺ
λ£μ± (Clarity): {category_scores.get('clarity', 0)} / 25
|
| 557 |
-
- μ°μ μμ (Priority): {category_scores.get('priority', 0)} / 25
|
| 558 |
|
| 559 |
-
|
| 560 |
-
{chr(10).join([f"- {
|
| 561 |
|
| 562 |
-
|
| 563 |
-
{
|
| 564 |
|
| 565 |
-
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
|
| 568 |
-
---
|
| 569 |
-
μκ³ νμ
¨μ΅λλ€! λ€λ₯Έ μλ리μ€λ‘ μ°μ΅μ κ³μνμλ €λ©΄ EMR νμ΄μ§λ‘ λμκ° μλ‘μ΄ μλ리μ€λ₯Ό μ ννμΈμ.
|
| 570 |
"""
|
| 571 |
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
|
| 575 |
def get_student_history(student_id: str):
|
|
@@ -577,13 +569,10 @@ def get_student_history(student_id: str):
|
|
| 577 |
if not student_id:
|
| 578 |
return "νμ IDλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ."
|
| 579 |
|
| 580 |
-
|
| 581 |
-
db_service = DatabaseService(db)
|
| 582 |
|
| 583 |
-
stats =
|
| 584 |
-
records =
|
| 585 |
-
|
| 586 |
-
db.close()
|
| 587 |
|
| 588 |
if stats["total_submissions"] == 0:
|
| 589 |
return f"νμ ID '{student_id}'μ μ μΆ κΈ°λ‘μ΄ μμ΅λλ€."
|
|
@@ -603,12 +592,20 @@ def get_student_history(student_id: str):
|
|
| 603 |
"""
|
| 604 |
|
| 605 |
for i, record in enumerate(records[:10], 1):
|
| 606 |
-
submitted_time = record.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
history += f"""
|
| 608 |
-
### {i}. {record.scenario_id}
|
| 609 |
- **μ μΆ μκ°**: {submitted_time}
|
| 610 |
-
- **μ μ**: {record.total_score:.1f}μ
|
| 611 |
-
- **κ°μ **: {', '.join(record.strengths[:2]) if record.strengths else 'N/A'}
|
| 612 |
---
|
| 613 |
"""
|
| 614 |
|
|
@@ -681,12 +678,12 @@ with gr.Blocks(title="κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ", theme=gr.themes.Sof
|
|
| 681 |
|
| 682 |
# νμ΄μ§ 2: μ±ν
κΈ°λ° λ΅μ μμ±
|
| 683 |
with gr.Tab("π¬ λ΅μ μμ± (μ±ν
)", id=1) as tab_chat:
|
| 684 |
-
gr.Markdown("##
|
| 685 |
gr.Markdown("""
|
| 686 |
**π μμνκΈ°:**
|
| 687 |
1. κΈ°λ³Έ μλ리μ€λ **"Day 0 - μκΈμ€βλ³λ μΈκ³"**μ
λλ€ (μλ¨μμ νμΈ κ°λ₯)
|
| 688 |
2. EMR μ 보λ₯Ό νμΈνλ €λ©΄ **"EMR μ 보 μ‘°ν"** νμ ν΄λ¦νμΈμ
|
| 689 |
-
3. μ€λΉκ° λλ©΄
|
| 690 |
""")
|
| 691 |
|
| 692 |
with gr.Row():
|
|
@@ -702,37 +699,27 @@ with gr.Blocks(title="κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ", theme=gr.themes.Sof
|
|
| 702 |
with gr.Row():
|
| 703 |
msg_input = gr.Textbox(
|
| 704 |
label="λ©μμ§ μ
λ ₯",
|
| 705 |
-
placeholder="
|
| 706 |
lines=3,
|
| 707 |
scale=4
|
| 708 |
)
|
| 709 |
|
| 710 |
with gr.Row():
|
| 711 |
send_btn = gr.Button("μ μ‘", variant="primary")
|
| 712 |
-
|
| 713 |
clear_btn = gr.Button("λν μ΄κΈ°ν")
|
| 714 |
|
| 715 |
gr.Markdown("""
|
| 716 |
**μ¬μ© λ°©λ²:**
|
| 717 |
-
1.
|
| 718 |
-
2. AI κ΅μκ° νΌλλ°±μ μ 곡ν©λλ€
|
| 719 |
-
3. νΌλλ°±μ λ°νμΌλ‘ μμ νκ³
|
| 720 |
-
4. μ€λΉκ° λλ©΄
|
| 721 |
-
|
| 722 |
-
**μ΅μ’
μ μΆ μ νμ:**
|
| 723 |
-
```
|
| 724 |
-
**S - Situation (μν©):**
|
| 725 |
-
[λ΄μ©]
|
| 726 |
-
|
| 727 |
-
**B - Background (λ°°κ²½):**
|
| 728 |
-
[λ΄μ©]
|
| 729 |
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
[λ΄μ©]
|
| 735 |
-
```
|
| 736 |
""")
|
| 737 |
|
| 738 |
# νμ΄μ§ 3: κ΄λ¦¬μ μ΄λ ₯ μ‘°ν
|
|
@@ -769,10 +756,8 @@ with gr.Blocks(title="κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ", theme=gr.themes.Sof
|
|
| 769 |
|
| 770 |
if scenario:
|
| 771 |
# μ μ±ν
μΈμ
μμ±
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
new_session_id = db_service.create_chat_session(student_id, scenario_id)
|
| 775 |
-
db.close()
|
| 776 |
|
| 777 |
scenario_info = f"**νμ¬ μλ리μ€**: {scenario.title}"
|
| 778 |
|
|
@@ -801,20 +786,18 @@ with gr.Blocks(title="κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ", theme=gr.themes.Sof
|
|
| 801 |
show_progress="minimal"
|
| 802 |
)
|
| 803 |
|
| 804 |
-
#
|
| 805 |
-
|
| 806 |
-
fn=
|
| 807 |
-
inputs=[
|
| 808 |
outputs=[chatbot, msg_input]
|
| 809 |
)
|
| 810 |
|
| 811 |
# λν μ΄κΈ°ν
|
| 812 |
def clear_chat(student_id, scenario_id):
|
| 813 |
"""λν μ΄κΈ°ν λ° μ μΈμ
μμ±"""
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
new_session_id = db_service.create_chat_session(student_id, scenario_id)
|
| 817 |
-
db.close()
|
| 818 |
|
| 819 |
return [], new_session_id
|
| 820 |
|
|
|
|
| 2 |
κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ - Gradio λ©ν°νμ΄μ§ μ ν리μΌμ΄μ
(μ±ν
κΈ°λ°)
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
import gradio as gr
|
|
|
|
|
|
|
| 8 |
from services.gemini_service import GeminiEvaluator
|
| 9 |
+
from services.supabase_service import SupabaseService
|
| 10 |
+
|
| 11 |
+
from config.database import supabase_client
|
| 12 |
|
| 13 |
# λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν
|
| 14 |
try:
|
| 15 |
+
print("π Supabase λͺ¨λλ‘ μ€νν©λλ€...")
|
| 16 |
+
|
| 17 |
+
# Supabase μλΉμ€ μ΄κΈ°ν
|
| 18 |
+
if supabase_client:
|
| 19 |
+
supabase_service = SupabaseService()
|
| 20 |
+
print("β
Supabase ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μλ£")
|
| 21 |
+
|
| 22 |
+
# μλλ¦¬μ€ λ°μ΄ν° λ‘λ (ν
μ΄λΈμ΄ μλ κ²½μ° λ¬΄μ)
|
| 23 |
+
try:
|
| 24 |
+
print("π₯ μλλ¦¬μ€ λ°μ΄ν° λ‘λ μ€...")
|
| 25 |
+
supabase_service.load_scenarios_from_json("data/scenarios.json")
|
| 26 |
+
print("β
μλλ¦¬μ€ λ°μ΄ν° λ‘λ μλ£")
|
| 27 |
+
except Exception as load_error:
|
| 28 |
+
print(f"β οΈ μλλ¦¬μ€ λ°μ΄ν° λ‘λ μ€ν¨: {load_error}")
|
| 29 |
+
print(" Supabase Dashboardμμ ν
μ΄λΈμ λ¨Όμ μμ±ν΄μ£ΌμΈμ:")
|
| 30 |
+
print(" 1. supabase_schema.sql μ€ν")
|
| 31 |
+
print(" 2. supabase_rls_fix_complete.sql μ€ν")
|
| 32 |
+
else:
|
| 33 |
+
print("β οΈ Supabase ν΄λΌμ΄μΈνΈκ° μ΄κΈ°νλμ§ μμμ΅λλ€.")
|
| 34 |
+
print(" νκ²½ λ³μ SUPABASE_URLκ³Ό SUPABASE_ANON_KEYλ₯Ό νμΈνμΈμ.")
|
| 35 |
except Exception as e:
|
| 36 |
print(f"β οΈ λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€ μ€λ₯: {e}")
|
| 37 |
+
import traceback
|
| 38 |
+
traceback.print_exc()
|
| 39 |
+
print("βΉοΈ Supabase μ¬μ© μ: νκ²½ λ³μλ₯Ό νμΈν΄μ£ΌμΈμ")
|
| 40 |
|
| 41 |
# Gemini νκ°κΈ° μ΄κΈ°ν
|
| 42 |
evaluator = GeminiEvaluator()
|
|
|
|
| 338 |
if not scenario_id:
|
| 339 |
return None, None, ""
|
| 340 |
|
| 341 |
+
supabase_service = SupabaseService()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
+
scenario = supabase_service.get_scenario(scenario_id)
|
| 344 |
+
patient = supabase_service.get_patient(scenario['patient_id']) if scenario else None
|
| 345 |
|
| 346 |
if not scenario or not patient:
|
| 347 |
return None, None, "μλ리μ€λ₯Ό μ°Ύμ μ μμ΅λλ€."
|
| 348 |
|
| 349 |
+
# Supabase λ°μ΄ν°λ₯Ό κ°μ²΄μ²λΌ μ¬μ©ν μ μλλ‘ λν
|
| 350 |
+
class Scenario:
|
| 351 |
+
def __init__(self, data):
|
| 352 |
+
self.id = data['id']
|
| 353 |
+
self.patient_id = data['patient_id']
|
| 354 |
+
self.title = data['title']
|
| 355 |
+
self.hospital_day = data.get('hospital_day')
|
| 356 |
+
self.vitals = data.get('vitals')
|
| 357 |
+
self.labs = data.get('labs')
|
| 358 |
+
self.orders = data.get('orders')
|
| 359 |
+
self.nursing_notes = data.get('nursing_notes')
|
| 360 |
+
self.surgery_info = data.get('surgery_info')
|
| 361 |
+
self.discharge_education = data.get('discharge_education')
|
| 362 |
+
|
| 363 |
+
class Patient:
|
| 364 |
+
def __init__(self, data):
|
| 365 |
+
self.id = data['id']
|
| 366 |
+
self.name = data['name']
|
| 367 |
+
self.age = data['age']
|
| 368 |
+
self.gender = data['gender']
|
| 369 |
+
self.diagnosis = data['diagnosis']
|
| 370 |
+
self.admission_date = data['admission_date']
|
| 371 |
+
self.allergies = data['allergies']
|
| 372 |
+
self.comorbidities = data.get('comorbidities', '')
|
| 373 |
+
self.attending_physician = data.get('attending_physician', '')
|
| 374 |
+
self.room_number = data.get('room_number', '')
|
| 375 |
+
|
| 376 |
+
scenario_obj = Scenario(scenario)
|
| 377 |
+
patient_obj = Patient(patient)
|
| 378 |
+
emr_display = create_emr_html(patient_obj, scenario_obj)
|
| 379 |
+
return scenario_obj, patient_obj, emr_display
|
| 380 |
|
| 381 |
|
| 382 |
def handle_chat_message(message, chat_history, session_id, student_id, scenario_id):
|
|
|
|
| 397 |
"""
|
| 398 |
return chat_history + [[message, warning_msg]], ""
|
| 399 |
|
| 400 |
+
# Supabase μλΉμ€ μ°κ²°
|
| 401 |
+
supabase_service = SupabaseService()
|
|
|
|
| 402 |
|
| 403 |
# μλλ¦¬μ€ λ° νμ μ 보 μ‘°ν
|
| 404 |
+
scenario = supabase_service.get_scenario(scenario_id)
|
| 405 |
+
patient = supabase_service.get_patient(scenario['patient_id']) if scenario else None
|
| 406 |
|
| 407 |
if not scenario or not patient:
|
|
|
|
| 408 |
error_msg = """
|
| 409 |
β **μλλ¦¬μ€ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€.**
|
| 410 |
|
|
|
|
| 413 |
return chat_history + [[message, error_msg]], ""
|
| 414 |
|
| 415 |
# μ¬μ©μ λ©μμ§ μ μ₯
|
| 416 |
+
supabase_service.save_chat_message(session_id, student_id, scenario_id, "user", message)
|
| 417 |
|
| 418 |
# μλλ¦¬μ€ λ° νμ λ°μ΄ν° μ€λΉ
|
| 419 |
scenario_data = {
|
| 420 |
+
"title": scenario['title'],
|
| 421 |
+
"handoff_situation": scenario.get('handoff_situation'),
|
| 422 |
+
"hospital_day": scenario.get('hospital_day'),
|
| 423 |
+
"vitals": scenario.get('vitals'),
|
| 424 |
+
"labs": scenario.get('labs'),
|
| 425 |
+
"orders": scenario.get('orders'),
|
| 426 |
+
"nursing_notes": scenario.get('nursing_notes'),
|
| 427 |
}
|
| 428 |
|
| 429 |
patient_data = {
|
| 430 |
+
"name": patient['name'],
|
| 431 |
+
"age": patient['age'],
|
| 432 |
+
"gender": patient['gender'],
|
| 433 |
+
"diagnosis": patient['diagnosis'],
|
| 434 |
+
"admission_date": str(patient['admission_date']),
|
| 435 |
+
"allergies": patient['allergies'],
|
| 436 |
}
|
| 437 |
|
| 438 |
# μ΄μ μ±ν
κΈ°λ‘ ν¬λ§·ν
(Geminiμ©)
|
|
|
|
| 447 |
ai_response = evaluator.chat_feedback(message, history_for_gemini, scenario_data, patient_data)
|
| 448 |
|
| 449 |
# AI μλ΅ μ μ₯
|
| 450 |
+
supabase_service.save_chat_message(session_id, student_id, scenario_id, "assistant", ai_response)
|
|
|
|
|
|
|
| 451 |
|
| 452 |
# μ±ν
νμ€ν 리 μ
λ°μ΄νΈ
|
| 453 |
updated_history = chat_history + [[message, ai_response]]
|
|
|
|
| 455 |
return updated_history, ""
|
| 456 |
|
| 457 |
|
| 458 |
+
def finalize_chat_session(chat_history, session_id, student_id, scenario_id):
|
| 459 |
+
"""
|
| 460 |
+
μ±ν
μ’
λ£ μ μ 체 λνλ₯Ό λΆμνμ¬ νΌλλ°± μ 곡
|
| 461 |
+
- μ μ μμ΄ μ§μ νΌλλ°±λ§ μ 곡
|
| 462 |
+
- μ 체 λν λ΄μ©μ Geminiμκ² μ λ¬
|
| 463 |
+
"""
|
| 464 |
if not chat_history:
|
| 465 |
+
return chat_history + [[None, "λνκ° μμ΅λλ€. λ¨Όμ λνλ₯Ό μμν΄μ£ΌμΈμ."]], ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
+
# Supabase μλΉμ€ μ°κ²°
|
| 468 |
+
supabase_service = SupabaseService()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
# μλλ¦¬μ€ λ° νμ μ 보 μ‘°ν
|
| 471 |
+
scenario = supabase_service.get_scenario(scenario_id)
|
| 472 |
+
patient = supabase_service.get_patient(scenario['patient_id']) if scenario else None
|
| 473 |
|
| 474 |
if not scenario or not patient:
|
| 475 |
+
return chat_history + [[None, "μλλ¦¬μ€ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€."]], ""
|
|
|
|
| 476 |
|
| 477 |
# μλλ¦¬μ€ λ° νμ λ°μ΄ν° μ€λΉ
|
| 478 |
scenario_data = {
|
| 479 |
+
"title": scenario['title'],
|
| 480 |
+
"handoff_situation": scenario.get('handoff_situation'),
|
| 481 |
+
"hospital_day": scenario.get('hospital_day'),
|
| 482 |
+
"vitals": scenario.get('vitals'),
|
| 483 |
+
"labs": scenario.get('labs'),
|
| 484 |
+
"orders": scenario.get('orders'),
|
| 485 |
+
"nursing_notes": scenario.get('nursing_notes'),
|
| 486 |
}
|
| 487 |
|
| 488 |
patient_data = {
|
| 489 |
+
"name": patient['name'],
|
| 490 |
+
"age": patient['age'],
|
| 491 |
+
"gender": patient['gender'],
|
| 492 |
+
"diagnosis": patient['diagnosis'],
|
| 493 |
+
"admission_date": str(patient['admission_date']),
|
| 494 |
+
"allergies": patient['allergies'],
|
| 495 |
}
|
| 496 |
|
| 497 |
+
# μ 체 λνλ₯Ό Geminiμκ² μ λ¬νμ¬ νκ°
|
| 498 |
+
evaluation = evaluator.evaluate_conversation(
|
| 499 |
+
chat_history=chat_history,
|
| 500 |
+
scenario_data=scenario_data,
|
| 501 |
+
patient_data=patient_data
|
| 502 |
+
)
|
| 503 |
|
| 504 |
+
# λ°μ΄ν°λ² μ΄μ€μ μ μ₯ (μ μ μμ΄)
|
| 505 |
try:
|
| 506 |
+
record = supabase_service.create_handoff_record(
|
| 507 |
student_id=student_id,
|
| 508 |
scenario_id=scenario_id,
|
| 509 |
+
sbar_data={}, # λν κΈ°λ° νκ°μ΄λ―λ‘ SBAR λ°μ΄ν°λ λΉμ
|
| 510 |
evaluation=evaluation,
|
| 511 |
session_id=session_id
|
| 512 |
)
|
| 513 |
+
print(f"β
μΈμμΈκ³ κΈ°λ‘ μ μ₯ μλ£ (ID: {record.get('id')})")
|
| 514 |
+
|
| 515 |
+
# μΈμ
μνλ₯Ό 'completed'λ‘ λ³κ²½
|
| 516 |
+
supabase_service.update_session_status(session_id, 'completed')
|
| 517 |
except Exception as e:
|
| 518 |
print(f"β οΈ λ°μ΄ν°λ² μ΄μ€ μ μ₯ μ€λ₯: {e}")
|
| 519 |
|
| 520 |
+
# νΌλλ°± λ©μμ§ ν¬λ§·ν
(μ μ μμ΄)
|
| 521 |
+
strengths = evaluation.get('strengths', [])
|
| 522 |
+
improvements = evaluation.get('improvements', [])
|
| 523 |
+
detailed_feedback = evaluation.get('detailed_feedback', '')
|
| 524 |
+
missing_info = evaluation.get('missing_critical_info', [])
|
| 525 |
+
safety_concerns = evaluation.get('safety_concerns', [])
|
|
|
|
|
|
|
| 526 |
|
| 527 |
result_message = f"""
|
| 528 |
+
π **νμ΅ μ’
ν© νΌλλ°±**
|
| 529 |
|
| 530 |
+
{'='*50}
|
| 531 |
|
| 532 |
+
### πͺ μν μ
|
| 533 |
+
{chr(10).join([f"- {s}" for s in strengths]) if strengths else "- μμ"}
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
+
### π κ°μ ν μ
|
| 536 |
+
{chr(10).join([f"- {i}" for i in improvements]) if improvements else "- μμ"}
|
| 537 |
|
| 538 |
+
### π μ’
ν© μ견
|
| 539 |
+
{detailed_feedback if detailed_feedback else "μ’
ν© νΌλλ°±μ΄ μμ΅λλ€."}
|
| 540 |
|
| 541 |
+
"""
|
| 542 |
+
|
| 543 |
+
if missing_info:
|
| 544 |
+
result_message += f"""
|
| 545 |
+
### β οΈ λλ½λ μ€μ μ 보
|
| 546 |
+
{chr(10).join([f"- {m}" for m in missing_info])}
|
| 547 |
|
|
|
|
|
|
|
| 548 |
"""
|
| 549 |
|
| 550 |
+
if safety_concerns:
|
| 551 |
+
result_message += f"""
|
| 552 |
+
### π₯ νμ μμ κ΄λ ¨ μ£Όμμ¬ν
|
| 553 |
+
{chr(10).join([f"- {s}" for s in safety_concerns])}
|
| 554 |
+
|
| 555 |
+
"""
|
| 556 |
+
|
| 557 |
+
result_message += f"""
|
| 558 |
+
{'='*50}
|
| 559 |
+
|
| 560 |
+
μκ³ νμ
¨μ΅λλ€! λ€λ₯Έ μλ리μ€λ‘ μ°μ΅μ κ³μνμλ €λ©΄
|
| 561 |
+
EMR νμ΄μ§λ‘ λμκ° μλ‘μ΄ μλ리μ€λ₯Ό μ ννμΈμ.
|
| 562 |
+
"""
|
| 563 |
+
|
| 564 |
+
return chat_history + [[None, result_message]], ""
|
| 565 |
|
| 566 |
|
| 567 |
def get_student_history(student_id: str):
|
|
|
|
| 569 |
if not student_id:
|
| 570 |
return "νμ IDλ₯Ό μ
λ ₯ν΄μ£ΌμΈμ."
|
| 571 |
|
| 572 |
+
supabase_service = SupabaseService()
|
|
|
|
| 573 |
|
| 574 |
+
stats = supabase_service.get_student_statistics(student_id)
|
| 575 |
+
records = supabase_service.get_records_by_student(student_id)
|
|
|
|
|
|
|
| 576 |
|
| 577 |
if stats["total_submissions"] == 0:
|
| 578 |
return f"νμ ID '{student_id}'μ μ μΆ κΈ°λ‘μ΄ μμ΅λλ€."
|
|
|
|
| 592 |
"""
|
| 593 |
|
| 594 |
for i, record in enumerate(records[:10], 1):
|
| 595 |
+
submitted_time = record.get('submitted_at', 'N/A')
|
| 596 |
+
if submitted_time and isinstance(submitted_time, str):
|
| 597 |
+
# ISO νμμμ μκ° μΆμΆ
|
| 598 |
+
try:
|
| 599 |
+
from datetime import datetime
|
| 600 |
+
dt = datetime.fromisoformat(submitted_time.replace('Z', '+00:00'))
|
| 601 |
+
submitted_time = dt.strftime("%Y-%m-%d %H:%M")
|
| 602 |
+
except:
|
| 603 |
+
pass
|
| 604 |
history += f"""
|
| 605 |
+
### {i}. {record.get('scenario_id', 'N/A')}
|
| 606 |
- **μ μΆ μκ°**: {submitted_time}
|
| 607 |
+
- **μ μ**: {record.get('total_score', 0):.1f}μ
|
| 608 |
+
- **κ°μ **: {', '.join(record.get('strengths', [])[:2]) if record.get('strengths') else 'N/A'}
|
| 609 |
---
|
| 610 |
"""
|
| 611 |
|
|
|
|
| 678 |
|
| 679 |
# νμ΄μ§ 2: μ±ν
κΈ°λ° λ΅μ μμ±
|
| 680 |
with gr.Tab("π¬ λ΅μ μμ± (μ±ν
)", id=1) as tab_chat:
|
| 681 |
+
gr.Markdown("## AI κ΅μμ ν¨κ»νλ μΈμμΈκ³ μ°μ΅")
|
| 682 |
gr.Markdown("""
|
| 683 |
**π μμνκΈ°:**
|
| 684 |
1. κΈ°λ³Έ μλ리μ€λ **"Day 0 - μκΈμ€βλ³λ μΈκ³"**μ
λλ€ (μλ¨μμ νμΈ κ°λ₯)
|
| 685 |
2. EMR μ 보λ₯Ό νμΈνλ €λ©΄ **"EMR μ 보 μ‘°ν"** νμ ν΄λ¦νμΈμ
|
| 686 |
+
3. μ€λΉκ° λλ©΄ AI κ΅μμ μμ λ‘κ² λννλ©° μΈμμΈκ³ λ΄μ©μ μμ±νμΈμ
|
| 687 |
""")
|
| 688 |
|
| 689 |
with gr.Row():
|
|
|
|
| 699 |
with gr.Row():
|
| 700 |
msg_input = gr.Textbox(
|
| 701 |
label="λ©μμ§ μ
λ ₯",
|
| 702 |
+
placeholder="μμ λ‘κ² λννλ©° μΈμμΈκ³ λ΄μ©μ μμ±νκ±°λ, AI κ΅μμκ² μ§λ¬ΈνμΈμ...",
|
| 703 |
lines=3,
|
| 704 |
scale=4
|
| 705 |
)
|
| 706 |
|
| 707 |
with gr.Row():
|
| 708 |
send_btn = gr.Button("μ μ‘", variant="primary")
|
| 709 |
+
finalize_btn = gr.Button("μ±ν
μ’
λ£ λ° νκ°λ°κΈ°", variant="stop")
|
| 710 |
clear_btn = gr.Button("λν μ΄κΈ°ν")
|
| 711 |
|
| 712 |
gr.Markdown("""
|
| 713 |
**μ¬μ© λ°©λ²:**
|
| 714 |
+
1. μμ λ‘κ² λννλ©° μΈμμΈκ³ λ΄μ©μ μμ±νμΈμ
|
| 715 |
+
2. AI κ΅μκ° μ¦μ νΌλλ°±μ μ 곡ν©λλ€
|
| 716 |
+
3. νΌλλ°±μ λ°νμΌλ‘ μμ νκ³ λ λμ λ΄μ©μ μμ±ν μ μμ΅λλ€
|
| 717 |
+
4. μ€λΉκ° λλ©΄ **μ±ν
μ’
λ£ λ° νκ°λ°κΈ°** λ²νΌμ λλ¬ μ’
ν© νΌλλ°±μ λ°μΌμΈμ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
|
| 719 |
+
**π‘ ν:**
|
| 720 |
+
- μμ°μ€λ¬μ΄ λνλ‘ λ°°μ°λ κ²μ΄ ν΅μ¬μ
λλ€
|
| 721 |
+
- SBAR νμμ κ°μ νμ§ μμ΅λλ€. λνλ₯Ό ν΅ν΄ μλμΌλ‘ νκ°λ©λλ€
|
| 722 |
+
- μΈμ λ μ§ AI κ΅μμκ² μ§λ¬ΈνμΈμ
|
|
|
|
|
|
|
| 723 |
""")
|
| 724 |
|
| 725 |
# νμ΄μ§ 3: κ΄λ¦¬μ μ΄λ ₯ μ‘°ν
|
|
|
|
| 756 |
|
| 757 |
if scenario:
|
| 758 |
# μ μ±ν
μΈμ
μμ±
|
| 759 |
+
supabase_service = SupabaseService()
|
| 760 |
+
new_session_id = supabase_service.create_chat_session(student_id, scenario_id)
|
|
|
|
|
|
|
| 761 |
|
| 762 |
scenario_info = f"**νμ¬ μλ리μ€**: {scenario.title}"
|
| 763 |
|
|
|
|
| 786 |
show_progress="minimal"
|
| 787 |
)
|
| 788 |
|
| 789 |
+
# μ±ν
μ’
λ£ λ° νκ°
|
| 790 |
+
finalize_btn.click(
|
| 791 |
+
fn=finalize_chat_session,
|
| 792 |
+
inputs=[chatbot, session_id_state, student_id_state, scenario_id_state],
|
| 793 |
outputs=[chatbot, msg_input]
|
| 794 |
)
|
| 795 |
|
| 796 |
# λν μ΄κΈ°ν
|
| 797 |
def clear_chat(student_id, scenario_id):
|
| 798 |
"""λν μ΄κΈ°ν λ° μ μΈμ
μμ±"""
|
| 799 |
+
supabase_service = SupabaseService()
|
| 800 |
+
new_session_id = supabase_service.create_chat_session(student_id, scenario_id)
|
|
|
|
|
|
|
| 801 |
|
| 802 |
return [], new_session_id
|
| 803 |
|
config/database.py
CHANGED
|
@@ -1,53 +1,82 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
import os
|
| 6 |
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
from
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# νκ²½ λ³μ λ‘λ
|
| 13 |
load_dotenv()
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 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 |
-
#
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
# Base ν΄λμ€
|
| 29 |
-
Base = declarative_base()
|
| 30 |
|
| 31 |
-
|
| 32 |
-
def get_db():
|
| 33 |
"""
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
try:
|
| 38 |
-
|
| 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 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Supabase REST API μ€μ
|
| 3 |
"""
|
| 4 |
|
| 5 |
import os
|
| 6 |
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from supabase import Client, create_client
|
| 11 |
+
except ImportError:
|
| 12 |
+
create_client = None
|
| 13 |
+
Client = None
|
| 14 |
|
| 15 |
# νκ²½ λ³μ λ‘λ
|
| 16 |
load_dotenv()
|
| 17 |
|
| 18 |
+
# Supabase μ°κ²° μ€μ
|
| 19 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 20 |
+
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
# SUPABASE_KEYλ μ§μ (νΈνμ±)
|
| 23 |
+
if not SUPABASE_ANON_KEY:
|
| 24 |
+
SUPABASE_ANON_KEY = os.getenv("SUPABASE_KEY")
|
| 25 |
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
def check_supabase_health() -> bool:
|
|
|
|
| 28 |
"""
|
| 29 |
+
Supabase μ°κ²° μν νμΈ
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
bool: Supabase μ¬μ© κ°λ₯ μ¬λΆ
|
| 33 |
"""
|
| 34 |
+
if not SUPABASE_URL:
|
| 35 |
+
return False
|
| 36 |
+
|
| 37 |
try:
|
| 38 |
+
import requests
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
# μ¬λ¬ μλν¬μΈνΈ μλ
|
| 41 |
+
health_endpoints = [
|
| 42 |
+
"/rest/v1/",
|
| 43 |
+
"/auth/v1/health",
|
| 44 |
+
"/health"
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
for endpoint in health_endpoints:
|
| 48 |
+
try:
|
| 49 |
+
health_url = f"{SUPABASE_URL}{endpoint}"
|
| 50 |
+
response = requests.get(health_url, timeout=3)
|
| 51 |
+
if response.status_code in [200, 301, 302, 404]:
|
| 52 |
+
print(f"β
Supabase μ°κ²° νμΈ: {endpoint}")
|
| 53 |
+
return True
|
| 54 |
+
except Exception:
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
return False
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"β οΈ Supabase ν¬μ€μ²΄ν¬ μ€ν¨: {e}")
|
| 60 |
+
return False
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
# Supabase ν΄λΌμ΄μΈνΈ μ΄κΈ°ν (REST API μ¬μ©)
|
| 64 |
+
supabase_client: Client | None = None
|
| 65 |
+
|
| 66 |
+
if SUPABASE_URL and SUPABASE_ANON_KEY and create_client:
|
| 67 |
+
try:
|
| 68 |
+
supabase_client = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
|
| 69 |
+
print("β
Supabase REST API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μλ£")
|
| 70 |
+
|
| 71 |
+
# Supabase μ°κ²° μν νμΈ
|
| 72 |
+
is_supabase = check_supabase_health()
|
| 73 |
+
if is_supabase:
|
| 74 |
+
print("β
Supabase μ°κ²° νμΈ μλ£")
|
| 75 |
+
else:
|
| 76 |
+
print("β οΈ Supabase ν¬μ€μ²΄ν¬ μ€ν¨, REST APIλ μ¬μ© κ°λ₯ν©λλ€")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"β οΈ Supabase REST API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μ€ν¨: {e}")
|
| 79 |
+
supabase_client = None
|
| 80 |
+
else:
|
| 81 |
+
print("β οΈ Supabase νκ²½ λ³μκ° μ€μ λμ§ μμμ΅λλ€.")
|
| 82 |
+
print(" SUPABASE_URLκ³Ό SUPABASE_ANON_KEYλ₯Ό μ€μ νμΈμ.")
|
docker-compose.yml
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
version: '3.8'
|
| 2 |
-
|
| 3 |
-
services:
|
| 4 |
-
postgres:
|
| 5 |
-
image: postgres:15-alpine
|
| 6 |
-
container_name: nursing_handoff_db
|
| 7 |
-
environment:
|
| 8 |
-
POSTGRES_DB: nursing_handoff
|
| 9 |
-
POSTGRES_USER: postgres
|
| 10 |
-
POSTGRES_PASSWORD: postgres123
|
| 11 |
-
ports:
|
| 12 |
-
- "5433:5432"
|
| 13 |
-
volumes:
|
| 14 |
-
- postgres_data:/var/lib/postgresql/data
|
| 15 |
-
healthcheck:
|
| 16 |
-
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
| 17 |
-
interval: 10s
|
| 18 |
-
timeout: 5s
|
| 19 |
-
retries: 5
|
| 20 |
-
|
| 21 |
-
volumes:
|
| 22 |
-
postgres_data:
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init_db.py
CHANGED
|
@@ -1,23 +1,28 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
-
from
|
| 6 |
-
from services.db_service import load_scenarios_to_db
|
| 7 |
|
| 8 |
-
|
| 9 |
-
print("π λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μμ...")
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
# μλλ¦¬μ€ λ°μ΄ν° λ‘λ
|
| 15 |
print("\nπ₯ μλλ¦¬μ€ λ°μ΄ν° λ‘λ μ€...")
|
| 16 |
-
db = SessionLocal()
|
| 17 |
try:
|
| 18 |
-
|
| 19 |
print("\nβ
λͺ¨λ μ΄κΈ°ν μμ
μ΄ μλ£λμμ΅λλ€!")
|
| 20 |
except Exception as e:
|
| 21 |
print(f"\nβ μ€λ₯ λ°μ: {e}")
|
| 22 |
-
|
| 23 |
-
|
|
|
|
| 1 |
"""
|
| 2 |
+
Supabase μλλ¦¬μ€ λ°μ΄ν° λ‘λ μ€ν¬λ¦½νΈ
|
| 3 |
"""
|
| 4 |
|
| 5 |
+
from services.supabase_service import SupabaseService
|
|
|
|
| 6 |
|
| 7 |
+
from config.database import supabase_client
|
|
|
|
| 8 |
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
print("π Supabase μλλ¦¬μ€ λ°μ΄ν° λ‘λ μμ...")
|
| 11 |
+
|
| 12 |
+
if not supabase_client:
|
| 13 |
+
print("β Supabase ν΄λΌμ΄μΈνΈκ° μ΄κΈ°νλμ§ μμμ΅λλ€.")
|
| 14 |
+
print(" νκ²½ λ³μ SUPABASE_URLκ³Ό SUPABASE_ANON_KEYλ₯Ό νμΈνμΈμ.")
|
| 15 |
+
exit(1)
|
| 16 |
|
| 17 |
+
# Supabase μλΉμ€ μ΄κΈ°ν
|
| 18 |
+
supabase_service = SupabaseService()
|
| 19 |
+
|
| 20 |
# μλλ¦¬μ€ λ°μ΄ν° λ‘λ
|
| 21 |
print("\nπ₯ μλλ¦¬μ€ λ°μ΄ν° λ‘λ μ€...")
|
|
|
|
| 22 |
try:
|
| 23 |
+
supabase_service.load_scenarios_from_json("data/scenarios.json")
|
| 24 |
print("\nβ
λͺ¨λ μ΄κΈ°ν μμ
μ΄ μλ£λμμ΅λλ€!")
|
| 25 |
except Exception as e:
|
| 26 |
print(f"\nβ μ€λ₯ λ°μ: {e}")
|
| 27 |
+
import traceback
|
| 28 |
+
traceback.print_exc()
|
models/__init__.py
DELETED
|
File without changes
|
models/chat_history.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 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})>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
CHANGED
|
@@ -6,7 +6,8 @@ requires-python = ">=3.12"
|
|
| 6 |
dependencies = [
|
| 7 |
"google-generativeai>=0.8.5",
|
| 8 |
"gradio>=5.49.1",
|
| 9 |
-
"psycopg2-binary>=2.9.11",
|
| 10 |
"python-dotenv>=1.1.1",
|
| 11 |
-
"
|
|
|
|
|
|
|
| 12 |
]
|
|
|
|
| 6 |
dependencies = [
|
| 7 |
"google-generativeai>=0.8.5",
|
| 8 |
"gradio>=5.49.1",
|
|
|
|
| 9 |
"python-dotenv>=1.1.1",
|
| 10 |
+
"requests>=2.31.0",
|
| 11 |
+
"supabase>=2.0.0",
|
| 12 |
+
"tsidpy>=1.1.5",
|
| 13 |
]
|
services/db_service.py
DELETED
|
@@ -1,296 +0,0 @@
|
|
| 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
CHANGED
|
@@ -380,3 +380,175 @@ class GeminiEvaluator:
|
|
| 380 |
μλ΅μ μμ±ν΄μ£ΌμΈμ:
|
| 381 |
"""
|
| 382 |
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
μλ΅μ μμ±ν΄μ£ΌμΈμ:
|
| 381 |
"""
|
| 382 |
return prompt
|
| 383 |
+
|
| 384 |
+
def evaluate_conversation(
|
| 385 |
+
self,
|
| 386 |
+
chat_history: list,
|
| 387 |
+
scenario_data: dict,
|
| 388 |
+
patient_data: dict
|
| 389 |
+
) -> dict:
|
| 390 |
+
"""
|
| 391 |
+
μ 체 λνλ₯Ό λΆμνμ¬ μ§μ νΌλλ°± μ 곡 (μ μ μμ)
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
chat_history: μ 체 λν κΈ°λ‘ [[user_msg, assistant_msg], ...]
|
| 395 |
+
scenario_data: μλλ¦¬μ€ λ°μ΄ν° (EMR μ 보)
|
| 396 |
+
patient_data: νμ κΈ°λ³Έ μ 보
|
| 397 |
+
|
| 398 |
+
Returns:
|
| 399 |
+
νκ° κ²°κ³Ό λμ
λ리 (μ μ μμ΄ μ§μ νΌλλ°±λ§)
|
| 400 |
+
"""
|
| 401 |
+
prompt = self._create_conversation_evaluation_prompt(
|
| 402 |
+
chat_history, scenario_data, patient_data
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
try:
|
| 406 |
+
response = self.model.generate_content(
|
| 407 |
+
prompt,
|
| 408 |
+
generation_config={
|
| 409 |
+
"temperature": 0.7,
|
| 410 |
+
"top_p": 0.95,
|
| 411 |
+
"max_output_tokens": 2048,
|
| 412 |
+
},
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
# JSON μλ΅ νμ±
|
| 416 |
+
result_text = response.text.strip()
|
| 417 |
+
|
| 418 |
+
# Markdown μ½λ λΈλ‘ μ κ±°
|
| 419 |
+
if result_text.startswith("```"):
|
| 420 |
+
result_text = result_text.split("```")[1]
|
| 421 |
+
if result_text.startswith("json"):
|
| 422 |
+
result_text = result_text[4:]
|
| 423 |
+
result_text = result_text.strip()
|
| 424 |
+
|
| 425 |
+
evaluation = json.loads(result_text)
|
| 426 |
+
return evaluation
|
| 427 |
+
|
| 428 |
+
except json.JSONDecodeError as e:
|
| 429 |
+
print(f"JSON νμ± μ€λ₯: {e}")
|
| 430 |
+
print(f"μλ΅ ν
μ€νΈ: {response.text}")
|
| 431 |
+
return self._create_default_evaluation_conversation()
|
| 432 |
+
except Exception as e:
|
| 433 |
+
print(f"Gemini API μ€λ₯: {e}")
|
| 434 |
+
return self._create_default_evaluation_conversation()
|
| 435 |
+
|
| 436 |
+
def _create_conversation_evaluation_prompt(
|
| 437 |
+
self, chat_history: list, scenario_data: dict, patient_data: dict
|
| 438 |
+
) -> str:
|
| 439 |
+
"""μ 체 λν νκ° ν둬ννΈ μμ±"""
|
| 440 |
+
|
| 441 |
+
# μ 체 λν λ΄μ© ν¬λ§·ν
|
| 442 |
+
conversation_text = ""
|
| 443 |
+
for user_msg, bot_msg in chat_history:
|
| 444 |
+
if user_msg:
|
| 445 |
+
conversation_text += f"\n**νμ**: {user_msg}\n"
|
| 446 |
+
if bot_msg:
|
| 447 |
+
conversation_text += f"\n**AI κ΅μ**: {bot_msg}\n"
|
| 448 |
+
|
| 449 |
+
prompt = f"""λΉμ μ 20λ
κ²½λ ₯μ νκ΅ κ°νΈ κ΅μ‘ μ λ¬Έκ°μ΄μ κ°νΈν κ΅μμ
λλ€.
|
| 450 |
+
κ°νΈ νμμ΄ AI κ΅μμ λλ λνλ₯Ό λΆμνμ¬ κ±΄μ€μ μΈ μ’
ν© νΌλλ°±μ μ 곡ν΄μ£ΌμΈμ.
|
| 451 |
+
|
| 452 |
+
**μ€μ**:
|
| 453 |
+
- λνμμ "νμ"μ΄ μ ν λΆλΆμ΄ μ€μ νμ΅νλ κ°νΈ νμμ
λλ€.
|
| 454 |
+
- "νμ"λ EMR μ 보μ λμ¨ νμμ
λλ€.
|
| 455 |
+
- νμμ΄ νμμ λν μΈμμΈκ³ λ΄μ©μ μμ±νλ κ²μ νκ°ν΄μ£ΌμΈμ.
|
| 456 |
+
|
| 457 |
+
## π νμ μ 보 (EMR λ°μ΄ν° - νμμ΄ μΈμμΈκ³ν΄μΌ ν νμ)
|
| 458 |
+
- **νμλͺ
**: {patient_data.get('name')} ({patient_data.get('age')}μΈ {patient_data.get('gender')})
|
| 459 |
+
- **μ§λ¨λͺ
**: {patient_data.get('diagnosis')}
|
| 460 |
+
- **μ
μμΌ**: {patient_data.get('admission_date')}
|
| 461 |
+
- **μλ λ₯΄κΈ°**: {patient_data.get('allergies')}
|
| 462 |
+
|
| 463 |
+
## π₯ μΈμμΈκ³ μν©
|
| 464 |
+
- **μλ리μ€**: {scenario_data.get('title')}
|
| 465 |
+
- **μΈκ³ μν©**: {scenario_data.get('handoff_situation')}
|
| 466 |
+
|
| 467 |
+
### π νλ ₯μ§ν (Vital Signs)
|
| 468 |
+
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
|
| 469 |
+
|
| 470 |
+
### π§ͺ κ²μ¬ κ²°κ³Ό (Laboratory Results)
|
| 471 |
+
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
|
| 472 |
+
|
| 473 |
+
### π μμ¬ μ²λ°© (Physician Orders)
|
| 474 |
+
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
|
| 475 |
+
|
| 476 |
+
### π κ°νΈ κΈ°λ‘ (Nursing Notes)
|
| 477 |
+
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## π¬ νμκ³Ό AI κ΅μ κ° λν λ΄μ©
|
| 482 |
+
**νμμ΄ μμ±ν μΈμμΈκ³ λ΄μ©κ³Ό AI κ΅μμ νΌλλ°±μ΄ ν¬ν¨λ μ 체 λν:**
|
| 483 |
+
|
| 484 |
+
{conversation_text if conversation_text else "(λν μμ)"}
|
| 485 |
+
|
| 486 |
+
**μ λνμμ νμμ΄ νμ({patient_data.get('name')})μ λν μΈμμΈκ³λ₯Ό μ΄λ»κ² μμ±νλμ§ λΆμνμΈμ.**
|
| 487 |
+
|
| 488 |
+
---
|
| 489 |
+
|
| 490 |
+
## π νκ° κ°μ΄λ
|
| 491 |
+
|
| 492 |
+
**μ€μ:**
|
| 493 |
+
- β μ μλ₯Ό λ§€κΈ°μ§ λ§μΈμ (total_score, category_scores μ¬μ© κΈμ§)
|
| 494 |
+
- β
μ§μ νΌλλ°±μ μ§μ€νμΈμ
|
| 495 |
+
- β
λνμμ λνλ κ°μ μ ꡬ체μ μΌλ‘ μΈκΈνμΈμ
|
| 496 |
+
- β
κ°μ ν μ μ 건μ€μ μΌλ‘ μ μνμΈμ
|
| 497 |
+
- β
λλ½λ μ€μ μ 보λ₯Ό νμ
νμΈμ
|
| 498 |
+
- β
νμ μμ κ³Ό κ΄λ ¨λ μ£Όμμ¬νμ κ°μ‘°νμΈμ
|
| 499 |
+
- β
νμ΅μ κ²©λ €νλ ν€μ μ μ§νμΈμ
|
| 500 |
+
|
| 501 |
+
**νκ° κΈ°μ€:**
|
| 502 |
+
1. **μμ μ±**: νμ μ 보(νμ μ μ, μ§λ¨, νλ ₯μ§ν, μ²λ°© λ±) ν¬ν¨ μ¬λΆ
|
| 503 |
+
2. **μ νμ±**: μλ£ μ 보μ μ νμ±
|
| 504 |
+
3. **λͺ
λ£μ±**: μμ¬μν΅μ λͺ
νμ±
|
| 505 |
+
4. **μ°μ μμ**: μ€μν μ 보μ μ°μ μ λ¬
|
| 506 |
+
5. **νμ μμ **: νμ μμ οΏ½οΏ½ κ΄λ ¨λ μ 보 κ°μ‘°
|
| 507 |
+
|
| 508 |
+
---
|
| 509 |
+
|
| 510 |
+
## π€ μΆλ ₯ νμ (JSON)
|
| 511 |
+
|
| 512 |
+
λ°λμ μλ JSON νμμΌλ‘λ§ μλ΅νμΈμ:
|
| 513 |
+
|
| 514 |
+
```json
|
| 515 |
+
{{
|
| 516 |
+
"strengths": [
|
| 517 |
+
"ꡬ체μ μΌλ‘ μν μ 1",
|
| 518 |
+
"ꡬ체μ μΌλ‘ μν μ 2",
|
| 519 |
+
"ꡬ체μ μΌλ‘ μν μ 3"
|
| 520 |
+
],
|
| 521 |
+
"improvements": [
|
| 522 |
+
"ꡬ체μ μΌλ‘ κ°μ ν μ 1",
|
| 523 |
+
"ꡬ체μ μΌλ‘ κ°μ ν μ 2",
|
| 524 |
+
"ꡬ체μ μΌλ‘ κ°μ ν μ 3"
|
| 525 |
+
],
|
| 526 |
+
"detailed_feedback": "νμμ μ λ°μ μΈ νμ΅ μνμ λν μ’
ν©μ μΈ μ견. κΈΈκ² μμ±νλ ꡬ체μ μΌλ‘.",
|
| 527 |
+
"missing_critical_info": [
|
| 528 |
+
"λλ½λ μ€μ μ 보 1",
|
| 529 |
+
"λλ½λ μ€μ μ 보 2"
|
| 530 |
+
],
|
| 531 |
+
"safety_concerns": [
|
| 532 |
+
"νμ μμ κ΄λ ¨ μ£Όμμ¬ν 1",
|
| 533 |
+
"νμ μμ κ΄λ ¨ μ£Όμμ¬ν 2"
|
| 534 |
+
]
|
| 535 |
+
}}
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
**μ°Έκ³ :**
|
| 539 |
+
- μ μ νλ(total_score, category_scores)λ ν¬ν¨νμ§ λ§μΈμ
|
| 540 |
+
- λ°°μ΄ νλͺ©μ μ΅μ 2-3κ° μ΄μ μμ±νμΈμ
|
| 541 |
+
- detailed_feedbackμ 200μ μ΄μ μμ±νμΈμ
|
| 542 |
+
- νκΈλ‘ μμ±νμΈμ
|
| 543 |
+
"""
|
| 544 |
+
return prompt
|
| 545 |
+
|
| 546 |
+
def _create_default_evaluation_conversation(self) -> dict:
|
| 547 |
+
"""νκ° μ€ν¨ μ κΈ°λ³Έ κ²°κ³Ό λ°ν (μ μ μμ΄)"""
|
| 548 |
+
return {
|
| 549 |
+
"strengths": ["λν λ΄μ©μ λΆμνμ΅λλ€."],
|
| 550 |
+
"improvements": ["νκ° μμ€ν
μ€λ₯λ‘ κ΅¬μ²΄μ μΈ νΌλλ°±μ μ 곡ν μ μμ΅λλ€."],
|
| 551 |
+
"detailed_feedback": "νκ° μμ€ν
μ λ¬Έμ κ° λ°μνμ΅λλ€. λμ€μ λ€μ μλν΄μ£ΌμΈμ.",
|
| 552 |
+
"missing_critical_info": [],
|
| 553 |
+
"safety_concerns": []
|
| 554 |
+
}
|
services/supabase_service.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Supabase REST API μλΉμ€
|
| 3 |
+
|
| 4 |
+
Supabase Python Clientλ₯Ό νμ©ν CRUD μμ
|
| 5 |
+
- Row Level Security (RLS) μλ μ μ©
|
| 6 |
+
- μ€μκ° κ΅¬λ
μ§μ
|
| 7 |
+
- μλ νμ
λ³ν
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from datetime import date, datetime
|
| 11 |
+
from typing import Any, Dict, List, Optional
|
| 12 |
+
|
| 13 |
+
from config.database import supabase_client
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SupabaseService:
|
| 17 |
+
"""Supabase REST APIλ₯Ό νμ©ν λ°μ΄ν°λ² μ΄μ€ μλΉμ€"""
|
| 18 |
+
|
| 19 |
+
def __init__(self):
|
| 20 |
+
if not supabase_client:
|
| 21 |
+
raise ValueError("Supabase ν΄λΌμ΄μΈνΈκ° μ΄κΈ°νλμ§ μμμ΅λλ€. νκ²½ λ³μλ₯Ό νμΈνμΈμ.")
|
| 22 |
+
self.client = supabase_client
|
| 23 |
+
|
| 24 |
+
# ===== Patient κ΄λ ¨ =====
|
| 25 |
+
|
| 26 |
+
def create_patient(self, patient_data: dict) -> Dict[str, Any]:
|
| 27 |
+
"""
|
| 28 |
+
νμ μμ±
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
patient_data: νμ μ 보 λμ
λ리
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
μμ±λ νμ λ°μ΄ν°
|
| 35 |
+
"""
|
| 36 |
+
# admission_dateλ₯Ό λ¬Έμμ΄λ‘ λ³ν
|
| 37 |
+
if isinstance(patient_data.get('admission_date'), date):
|
| 38 |
+
patient_data['admission_date'] = patient_data['admission_date'].isoformat()
|
| 39 |
+
|
| 40 |
+
response = self.client.table('patients').insert(patient_data).execute()
|
| 41 |
+
return response.data[0] if response.data else None
|
| 42 |
+
|
| 43 |
+
def get_patient(self, patient_id: str) -> Optional[Dict[str, Any]]:
|
| 44 |
+
"""
|
| 45 |
+
νμ μ‘°ν
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
patient_id: νμ ID
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
νμ λ°μ΄ν° λλ None
|
| 52 |
+
"""
|
| 53 |
+
response = self.client.table('patients')\
|
| 54 |
+
.select('*')\
|
| 55 |
+
.eq('id', patient_id)\
|
| 56 |
+
.execute()
|
| 57 |
+
|
| 58 |
+
return response.data[0] if response.data else None
|
| 59 |
+
|
| 60 |
+
def get_all_patients(self) -> List[Dict[str, Any]]:
|
| 61 |
+
"""
|
| 62 |
+
λͺ¨λ νμ μ‘°ν
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
νμ λ°μ΄ν° 리μ€νΈ
|
| 66 |
+
"""
|
| 67 |
+
response = self.client.table('patients')\
|
| 68 |
+
.select('*')\
|
| 69 |
+
.order('created_at', desc=True)\
|
| 70 |
+
.execute()
|
| 71 |
+
|
| 72 |
+
return response.data or []
|
| 73 |
+
|
| 74 |
+
def update_patient(self, patient_id: str, update_data: dict) -> Optional[Dict[str, Any]]:
|
| 75 |
+
"""
|
| 76 |
+
νμ μ 보 μ
λ°μ΄νΈ
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
patient_id: νμ ID
|
| 80 |
+
update_data: μ
λ°μ΄νΈν λ°μ΄ν°
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
μ
λ°μ΄νΈλ νμ λ°μ΄ν°
|
| 84 |
+
"""
|
| 85 |
+
response = self.client.table('patients')\
|
| 86 |
+
.update(update_data)\
|
| 87 |
+
.eq('id', patient_id)\
|
| 88 |
+
.execute()
|
| 89 |
+
|
| 90 |
+
return response.data[0] if response.data else None
|
| 91 |
+
|
| 92 |
+
# ===== Scenario κ΄λ ¨ =====
|
| 93 |
+
|
| 94 |
+
def create_scenario(self, scenario_data: dict) -> Dict[str, Any]:
|
| 95 |
+
"""
|
| 96 |
+
μλλ¦¬μ€ μμ±
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
scenario_data: μλλ¦¬μ€ μ 보 λμ
λ리
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
μμ±λ μλλ¦¬μ€ λ°μ΄ν°
|
| 103 |
+
"""
|
| 104 |
+
response = self.client.table('scenarios').insert(scenario_data).execute()
|
| 105 |
+
return response.data[0] if response.data else None
|
| 106 |
+
|
| 107 |
+
def get_scenario(self, scenario_id: str) -> Optional[Dict[str, Any]]:
|
| 108 |
+
"""
|
| 109 |
+
μλλ¦¬μ€ μ‘°ν
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
μλλ¦¬μ€ λ°μ΄ν° λλ None
|
| 116 |
+
"""
|
| 117 |
+
response = self.client.table('scenarios')\
|
| 118 |
+
.select('*')\
|
| 119 |
+
.eq('id', scenario_id)\
|
| 120 |
+
.execute()
|
| 121 |
+
|
| 122 |
+
return response.data[0] if response.data else None
|
| 123 |
+
|
| 124 |
+
def get_scenarios_by_patient(self, patient_id: str) -> List[Dict[str, Any]]:
|
| 125 |
+
"""
|
| 126 |
+
νΉμ νμμ λͺ¨λ μλλ¦¬μ€ μ‘°ν
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
patient_id: νμ ID
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
μλλ¦¬μ€ λ°μ΄ν° 리μ€νΈ
|
| 133 |
+
"""
|
| 134 |
+
response = self.client.table('scenarios')\
|
| 135 |
+
.select('*')\
|
| 136 |
+
.eq('patient_id', patient_id)\
|
| 137 |
+
.order('day')\
|
| 138 |
+
.execute()
|
| 139 |
+
|
| 140 |
+
return response.data or []
|
| 141 |
+
|
| 142 |
+
def get_all_scenarios(self) -> List[Dict[str, Any]]:
|
| 143 |
+
"""
|
| 144 |
+
λͺ¨λ μλλ¦¬μ€ μ‘°ν
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
μλλ¦¬μ€ λ°μ΄ν° 리μ€νΈ
|
| 148 |
+
"""
|
| 149 |
+
response = self.client.table('scenarios')\
|
| 150 |
+
.select('*')\
|
| 151 |
+
.order('created_at', desc=True)\
|
| 152 |
+
.execute()
|
| 153 |
+
|
| 154 |
+
return response.data or []
|
| 155 |
+
|
| 156 |
+
# ===== HandoffRecord κ΄λ ¨ =====
|
| 157 |
+
|
| 158 |
+
def create_handoff_record(
|
| 159 |
+
self,
|
| 160 |
+
student_id: str,
|
| 161 |
+
scenario_id: str,
|
| 162 |
+
sbar_data: dict,
|
| 163 |
+
evaluation: dict,
|
| 164 |
+
session_id: Optional[str] = None
|
| 165 |
+
) -> Dict[str, Any]:
|
| 166 |
+
"""
|
| 167 |
+
μΈμμΈκ³ κΈ°λ‘ μμ±
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
student_id: νμ ID
|
| 171 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 172 |
+
sbar_data: SBAR λ°μ΄ν°
|
| 173 |
+
evaluation: νκ° κ²°κ³Ό
|
| 174 |
+
session_id: μΈμ
ID (μ ν)
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
μμ±λ μΈμμΈκ³ κΈ°λ‘
|
| 178 |
+
"""
|
| 179 |
+
# μ μλ μ νμ (λν κΈ°λ° νκ°μμλ NULL)
|
| 180 |
+
record_data = {
|
| 181 |
+
'session_id': session_id,
|
| 182 |
+
'student_id': student_id,
|
| 183 |
+
'scenario_id': scenario_id,
|
| 184 |
+
'situation': sbar_data.get('situation', '') if sbar_data else '',
|
| 185 |
+
'background': sbar_data.get('background', '') if sbar_data else '',
|
| 186 |
+
'assessment': sbar_data.get('assessment', '') if sbar_data else '',
|
| 187 |
+
'recommendation': sbar_data.get('recommendation', '') if sbar_data else '',
|
| 188 |
+
'total_score': evaluation.get('total_score'), # NULL νμ©
|
| 189 |
+
'category_scores': evaluation.get('category_scores'), # NULL νμ©
|
| 190 |
+
'strengths': evaluation.get('strengths', []),
|
| 191 |
+
'improvements': evaluation.get('improvements', []),
|
| 192 |
+
'detailed_feedback': evaluation.get('detailed_feedback', ''),
|
| 193 |
+
'missing_critical_info': evaluation.get('missing_critical_info', []), # μ νλ
|
| 194 |
+
'safety_concerns': evaluation.get('safety_concerns', []), # μ νλ
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
response = self.client.table('handoff_records').insert(record_data).execute()
|
| 198 |
+
return response.data[0] if response.data else None
|
| 199 |
+
|
| 200 |
+
def get_handoff_record(self, record_id: int) -> Optional[Dict[str, Any]]:
|
| 201 |
+
"""
|
| 202 |
+
μΈμμΈκ³ κΈ°λ‘ μ‘°ν
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
record_id: κΈ°λ‘ IDγ
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
μΈμμΈκ³ κΈ°λ‘ λλ None
|
| 209 |
+
"""
|
| 210 |
+
response = self.client.table('handoff_records')\
|
| 211 |
+
.select('*')\
|
| 212 |
+
.eq('id', record_id)\
|
| 213 |
+
.execute()
|
| 214 |
+
|
| 215 |
+
return response.data[0] if response.data else None
|
| 216 |
+
|
| 217 |
+
def get_records_by_student(self, student_id: str) -> List[Dict[str, Any]]:
|
| 218 |
+
"""
|
| 219 |
+
νΉμ νμμ λͺ¨λ μΈμμΈκ³ κΈ°λ‘ μ‘°ν
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
student_id: νμ ID
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
μΈμμΈκ³ κΈ°λ‘ λ¦¬μ€νΈ
|
| 226 |
+
"""
|
| 227 |
+
response = self.client.table('handoff_records')\
|
| 228 |
+
.select('*')\
|
| 229 |
+
.eq('student_id', student_id)\
|
| 230 |
+
.order('submitted_at', desc=True)\
|
| 231 |
+
.execute()
|
| 232 |
+
|
| 233 |
+
return response.data or []
|
| 234 |
+
|
| 235 |
+
def get_records_by_scenario(self, scenario_id: str) -> List[Dict[str, Any]]:
|
| 236 |
+
"""
|
| 237 |
+
νΉμ μλ리μ€μ λͺ¨λ μΈμμΈκ³ κΈ°λ‘ μ‘°ν
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
μΈμμΈκ³ κΈ°λ‘ λ¦¬μ€νΈ
|
| 244 |
+
"""
|
| 245 |
+
response = self.client.table('handoff_records')\
|
| 246 |
+
.select('*')\
|
| 247 |
+
.eq('scenario_id', scenario_id)\
|
| 248 |
+
.order('submitted_at', desc=True)\
|
| 249 |
+
.execute()
|
| 250 |
+
|
| 251 |
+
return response.data or []
|
| 252 |
+
|
| 253 |
+
# ===== ChatHistory κ΄λ ¨ =====
|
| 254 |
+
|
| 255 |
+
def save_chat_message(
|
| 256 |
+
self,
|
| 257 |
+
session_id: str,
|
| 258 |
+
student_id: str,
|
| 259 |
+
scenario_id: str,
|
| 260 |
+
role: str,
|
| 261 |
+
message: str
|
| 262 |
+
) -> Dict[str, Any]:
|
| 263 |
+
"""
|
| 264 |
+
μ±ν
λ©μμ§ μ μ₯
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
session_id: μΈμ
ID
|
| 268 |
+
student_id: νμ ID
|
| 269 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 270 |
+
role: μν ('user' λλ 'assistant')
|
| 271 |
+
message: λ©μμ§ λ΄μ©
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
μ μ₯λ μ±ν
λ©μμ§
|
| 275 |
+
"""
|
| 276 |
+
chat_data = {
|
| 277 |
+
'session_id': session_id,
|
| 278 |
+
'student_id': student_id,
|
| 279 |
+
'scenario_id': scenario_id,
|
| 280 |
+
'role': role,
|
| 281 |
+
'message': message,
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
response = self.client.table('chat_history').insert(chat_data).execute()
|
| 285 |
+
return response.data[0] if response.data else None
|
| 286 |
+
|
| 287 |
+
def get_chat_history(self, session_id: str) -> List[Dict[str, Any]]:
|
| 288 |
+
"""
|
| 289 |
+
νΉμ μΈμ
μ μ±ν
κΈ°λ‘ μ‘°ν
|
| 290 |
+
|
| 291 |
+
Args:
|
| 292 |
+
session_id: μΈμ
ID
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
μ±ν
λ©μμ§ λ¦¬μ€νΈ
|
| 296 |
+
"""
|
| 297 |
+
response = self.client.table('chat_history')\
|
| 298 |
+
.select('*')\
|
| 299 |
+
.eq('session_id', session_id)\
|
| 300 |
+
.order('timestamp')\
|
| 301 |
+
.execute()
|
| 302 |
+
|
| 303 |
+
return response.data or []
|
| 304 |
+
|
| 305 |
+
def get_chat_sessions_by_student(self, student_id: str) -> List[Dict[str, Any]]:
|
| 306 |
+
"""
|
| 307 |
+
νΉμ νμμ λͺ¨λ μ±ν
μΈμ
μ‘°ν
|
| 308 |
+
|
| 309 |
+
Args:
|
| 310 |
+
student_id: νμ ID
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
μ±ν
μΈμ
리μ€νΈ
|
| 314 |
+
"""
|
| 315 |
+
response = self.client.table('chat_history')\
|
| 316 |
+
.select('session_id, scenario_id, timestamp')\
|
| 317 |
+
.eq('student_id', student_id)\
|
| 318 |
+
.order('timestamp', desc=True)\
|
| 319 |
+
.execute()
|
| 320 |
+
|
| 321 |
+
# μ€λ³΅ μ κ±° (μΈμ
λ³λ‘ κ·Έλ£Ήν)
|
| 322 |
+
sessions = {}
|
| 323 |
+
for chat in response.data or []:
|
| 324 |
+
session_id = chat['session_id']
|
| 325 |
+
if session_id not in sessions:
|
| 326 |
+
sessions[session_id] = chat
|
| 327 |
+
|
| 328 |
+
return list(sessions.values())
|
| 329 |
+
|
| 330 |
+
# ===== SbarDraft κ΄λ ¨ =====
|
| 331 |
+
|
| 332 |
+
def save_draft_sbar(
|
| 333 |
+
self,
|
| 334 |
+
session_id: str,
|
| 335 |
+
student_id: str,
|
| 336 |
+
scenario_id: str,
|
| 337 |
+
sbar_data: dict
|
| 338 |
+
) -> Dict[str, Any]:
|
| 339 |
+
"""
|
| 340 |
+
μμ± μ€μΈ SBAR μμ μ μ₯ (Upsert)
|
| 341 |
+
|
| 342 |
+
Args:
|
| 343 |
+
session_id: μΈμ
ID
|
| 344 |
+
student_id: νμ ID
|
| 345 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 346 |
+
sbar_data: SBAR λ°μ΄ν°
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
μ μ₯λ SBAR μ΄μ
|
| 350 |
+
"""
|
| 351 |
+
draft_data = {
|
| 352 |
+
'session_id': session_id,
|
| 353 |
+
'student_id': student_id,
|
| 354 |
+
'scenario_id': scenario_id,
|
| 355 |
+
'situation': sbar_data.get('situation', ''),
|
| 356 |
+
'background': sbar_data.get('background', ''),
|
| 357 |
+
'assessment': sbar_data.get('assessment', ''),
|
| 358 |
+
'recommendation': sbar_data.get('recommendation', ''),
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
# Upsert: session_idκ° μμΌλ©΄ μ
λ°μ΄νΈ, μμΌλ©΄ μ½μ
|
| 362 |
+
response = self.client.table('sbar_drafts')\
|
| 363 |
+
.upsert(draft_data, on_conflict='session_id')\
|
| 364 |
+
.execute()
|
| 365 |
+
|
| 366 |
+
return response.data[0] if response.data else None
|
| 367 |
+
|
| 368 |
+
def get_draft_sbar(self, session_id: str) -> Dict[str, str]:
|
| 369 |
+
"""
|
| 370 |
+
μμ μ μ₯λ SBAR λΆλ¬μ€κΈ°
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
session_id: μΈμ
ID
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
SBAR λ°μ΄ν° λμ
λ리
|
| 377 |
+
"""
|
| 378 |
+
response = self.client.table('sbar_drafts')\
|
| 379 |
+
.select('*')\
|
| 380 |
+
.eq('session_id', session_id)\
|
| 381 |
+
.execute()
|
| 382 |
+
|
| 383 |
+
if response.data:
|
| 384 |
+
draft = response.data[0]
|
| 385 |
+
return {
|
| 386 |
+
'situation': draft.get('situation', ''),
|
| 387 |
+
'background': draft.get('background', ''),
|
| 388 |
+
'assessment': draft.get('assessment', ''),
|
| 389 |
+
'recommendation': draft.get('recommendation', ''),
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
return {
|
| 393 |
+
'situation': '',
|
| 394 |
+
'background': '',
|
| 395 |
+
'assessment': '',
|
| 396 |
+
'recommendation': '',
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
# ===== ν΅κ³ κ΄λ ¨ =====
|
| 400 |
+
|
| 401 |
+
def get_student_statistics(self, student_id: str) -> dict:
|
| 402 |
+
"""
|
| 403 |
+
νμλ³ ν΅κ³
|
| 404 |
+
|
| 405 |
+
Args:
|
| 406 |
+
student_id: νμ ID
|
| 407 |
+
|
| 408 |
+
Returns:
|
| 409 |
+
ν΅κ³ λ°μ΄ν°
|
| 410 |
+
"""
|
| 411 |
+
records = self.get_records_by_student(student_id)
|
| 412 |
+
|
| 413 |
+
if not records:
|
| 414 |
+
return {
|
| 415 |
+
'total_submissions': 0,
|
| 416 |
+
'average_score': 0,
|
| 417 |
+
'highest_score': 0,
|
| 418 |
+
'lowest_score': 0,
|
| 419 |
+
'recent_submissions': [],
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
scores = [r['total_score'] for r in records if r.get('total_score') is not None]
|
| 423 |
+
|
| 424 |
+
return {
|
| 425 |
+
'total_submissions': len(records),
|
| 426 |
+
'average_score': sum(scores) / len(scores) if scores else 0,
|
| 427 |
+
'highest_score': max(scores) if scores else 0,
|
| 428 |
+
'lowest_score': min(scores) if scores else 0,
|
| 429 |
+
'recent_submissions': [
|
| 430 |
+
{
|
| 431 |
+
'id': r['id'],
|
| 432 |
+
'scenario_id': r['scenario_id'],
|
| 433 |
+
'score': r['total_score'],
|
| 434 |
+
'submitted_at': r.get('submitted_at'),
|
| 435 |
+
}
|
| 436 |
+
for r in records[:5] # μ΅κ·Ό 5κ°
|
| 437 |
+
],
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
def get_scenario_statistics(self, scenario_id: str) -> dict:
|
| 441 |
+
"""
|
| 442 |
+
μλ리μ€λ³ ν΅κ³
|
| 443 |
+
|
| 444 |
+
Args:
|
| 445 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
ν΅κ³ λ°μ΄ν°
|
| 449 |
+
"""
|
| 450 |
+
records = self.get_records_by_scenario(scenario_id)
|
| 451 |
+
|
| 452 |
+
if not records:
|
| 453 |
+
return {
|
| 454 |
+
'total_attempts': 0,
|
| 455 |
+
'average_score': 0,
|
| 456 |
+
'score_distribution': {},
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
scores = [r['total_score'] for r in records if r.get('total_score') is not None]
|
| 460 |
+
|
| 461 |
+
# μ μ ꡬκ°λ³ λΆν¬
|
| 462 |
+
distribution = {
|
| 463 |
+
'excellent': len([s for s in scores if s >= 81]),
|
| 464 |
+
'good': len([s for s in scores if 61 <= s < 81]),
|
| 465 |
+
'needs_improvement': len([s for s in scores if s < 61]),
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
return {
|
| 469 |
+
'total_attempts': len(records),
|
| 470 |
+
'average_score': sum(scores) / len(scores) if scores else 0,
|
| 471 |
+
'score_distribution': distribution,
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
# ===== μ€μκ° κ΅¬λ
(μ νμ ) =====
|
| 475 |
+
|
| 476 |
+
def subscribe_to_chat(self, session_id: str, callback):
|
| 477 |
+
"""
|
| 478 |
+
μ±ν
λ©μμ§ μ€μκ° κ΅¬λ
|
| 479 |
+
|
| 480 |
+
Args:
|
| 481 |
+
session_id: μΈμ
ID
|
| 482 |
+
callback: μ λ©μμ§ μμ μ νΈμΆν μ½λ°± ν¨μ
|
| 483 |
+
"""
|
| 484 |
+
channel = self.client.channel(f'chat_{session_id}')
|
| 485 |
+
|
| 486 |
+
channel.on_postgres_changes(
|
| 487 |
+
event='INSERT',
|
| 488 |
+
schema='public',
|
| 489 |
+
table='chat_history',
|
| 490 |
+
filter=f'session_id=eq.{session_id}',
|
| 491 |
+
callback=callback
|
| 492 |
+
).subscribe()
|
| 493 |
+
|
| 494 |
+
return channel
|
| 495 |
+
|
| 496 |
+
# ===== μλλ¦¬μ€ λ‘λ λ° μΈμ
κ΄λ¦¬ =====
|
| 497 |
+
|
| 498 |
+
def create_chat_session(self, student_id: str, scenario_id: str) -> str:
|
| 499 |
+
"""
|
| 500 |
+
μ μ±ν
μΈμ
μμ± λ° μΈμ
ID λ°ν (TSID μ¬μ©)
|
| 501 |
+
|
| 502 |
+
Args:
|
| 503 |
+
student_id: νμ ID
|
| 504 |
+
scenario_id: μλλ¦¬μ€ ID
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
session_id: TSID κΈ°λ° μΈμ
ID
|
| 508 |
+
"""
|
| 509 |
+
from tsidpy import TSID
|
| 510 |
+
|
| 511 |
+
# TSIDλ₯Ό μ¬μ©νμ¬ μκ° μ λ ¬ κ°λ₯ν κ³ μ ID μμ±
|
| 512 |
+
tsid = TSID.create()
|
| 513 |
+
session_id = f"ses_{tsid.to_string()}"
|
| 514 |
+
|
| 515 |
+
# μΈμ
μ λ°μ΄ν°λ² μ΄μ€μ μ μ₯
|
| 516 |
+
try:
|
| 517 |
+
session_data = {
|
| 518 |
+
'session_id': session_id,
|
| 519 |
+
'student_id': student_id,
|
| 520 |
+
'scenario_id': scenario_id,
|
| 521 |
+
'status': 'active',
|
| 522 |
+
'started_at': 'now()'
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
# μΈμ
ν
μ΄λΈμ΄ μλ€λ©΄ μ μ₯ (μμΌλ©΄ κ·Έλ₯ IDλ§ λ°ν)
|
| 526 |
+
try:
|
| 527 |
+
self.client.table('sessions').insert(session_data).execute()
|
| 528 |
+
except Exception:
|
| 529 |
+
# sessions ν
μ΄λΈμ΄ μμ κ²½μ° λ¬΄μ
|
| 530 |
+
pass
|
| 531 |
+
|
| 532 |
+
except Exception as e:
|
| 533 |
+
print(f"β οΈ μΈμ
μ μ₯ μ€ν¨ (IDλ§ μ¬μ©): {e}")
|
| 534 |
+
|
| 535 |
+
return session_id
|
| 536 |
+
|
| 537 |
+
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 538 |
+
"""
|
| 539 |
+
μΈμ
μ‘°ν
|
| 540 |
+
|
| 541 |
+
Args:
|
| 542 |
+
session_id: μΈμ
ID
|
| 543 |
+
|
| 544 |
+
Returns:
|
| 545 |
+
μΈμ
λ°μ΄ν° λλ None
|
| 546 |
+
"""
|
| 547 |
+
try:
|
| 548 |
+
response = self.client.table('sessions')\
|
| 549 |
+
.select('*')\
|
| 550 |
+
.eq('session_id', session_id)\
|
| 551 |
+
.execute()
|
| 552 |
+
|
| 553 |
+
return response.data[0] if response.data else None
|
| 554 |
+
except Exception:
|
| 555 |
+
return None
|
| 556 |
+
|
| 557 |
+
def update_session_status(self, session_id: str, status: str):
|
| 558 |
+
"""
|
| 559 |
+
μΈμ
μν μ
λ°μ΄νΈ
|
| 560 |
+
|
| 561 |
+
Args:
|
| 562 |
+
session_id: μΈμ
ID
|
| 563 |
+
status: μν ('active', 'completed', 'archived')
|
| 564 |
+
"""
|
| 565 |
+
try:
|
| 566 |
+
# sessions ν
μ΄λΈμ΄ μμ λλ§ μ
λ°μ΄νΈ μλ
|
| 567 |
+
self.client.table('sessions')\
|
| 568 |
+
.update({'status': status})\
|
| 569 |
+
.eq('session_id', session_id)\
|
| 570 |
+
.execute()
|
| 571 |
+
except Exception as e:
|
| 572 |
+
# sessions ν
μ΄λΈμ΄ μλ κ²½μ° λ¬΄μ (μ νμ κΈ°λ₯)
|
| 573 |
+
pass
|
| 574 |
+
|
| 575 |
+
def get_active_sessions_by_student(self, student_id: str) -> List[Dict[str, Any]]:
|
| 576 |
+
"""
|
| 577 |
+
νμμ νμ± μΈμ
λͺ©λ‘ μ‘°ν
|
| 578 |
+
|
| 579 |
+
Args:
|
| 580 |
+
student_id: νμ ID
|
| 581 |
+
|
| 582 |
+
Returns:
|
| 583 |
+
μΈμ
λ°μ΄ν° 리μ€νΈ
|
| 584 |
+
"""
|
| 585 |
+
try:
|
| 586 |
+
response = self.client.table('sessions')\
|
| 587 |
+
.select('*')\
|
| 588 |
+
.eq('student_id', student_id)\
|
| 589 |
+
.eq('status', 'active')\
|
| 590 |
+
.order('started_at', desc=True)\
|
| 591 |
+
.execute()
|
| 592 |
+
|
| 593 |
+
return response.data or []
|
| 594 |
+
except Exception:
|
| 595 |
+
return []
|
| 596 |
+
|
| 597 |
+
def load_scenarios_from_json(self, json_file_path: str):
|
| 598 |
+
"""
|
| 599 |
+
JSON νμΌμμ νμ λ° μλλ¦¬μ€ λ°μ΄ν°λ₯Ό μ½μ΄ Supabaseμ μ μ₯
|
| 600 |
+
|
| 601 |
+
Args:
|
| 602 |
+
json_file_path: JSON νμΌ κ²½λ‘
|
| 603 |
+
"""
|
| 604 |
+
import json
|
| 605 |
+
from datetime import datetime
|
| 606 |
+
|
| 607 |
+
with open(json_file_path, "r", encoding="utf-8") as f:
|
| 608 |
+
data = json.load(f)
|
| 609 |
+
|
| 610 |
+
# νμ λ°μ΄ν° μ μ₯
|
| 611 |
+
patient_data = data["patient"]
|
| 612 |
+
|
| 613 |
+
# admission_dateλ₯Ό λ¬Έμμ΄μμ date κ°μ²΄λ‘ λ³ν
|
| 614 |
+
if isinstance(patient_data.get("admission_date"), str):
|
| 615 |
+
patient_data["admission_date"] = datetime.strptime(
|
| 616 |
+
patient_data["admission_date"], "%Y-%m-%d"
|
| 617 |
+
).date()
|
| 618 |
+
|
| 619 |
+
# κΈ°μ‘΄ νμκ° μμΌλ©΄ μμ±
|
| 620 |
+
existing_patient = self.get_patient(patient_data["id"])
|
| 621 |
+
if not existing_patient:
|
| 622 |
+
self.create_patient(patient_data)
|
| 623 |
+
print(f"β
νμ μμ±: {patient_data['name']}")
|
| 624 |
+
else:
|
| 625 |
+
print(f"βΉοΈ νμ μ΄λ―Έ μ‘΄μ¬: {patient_data['name']}")
|
| 626 |
+
|
| 627 |
+
# μλλ¦¬μ€ λ°μ΄ν° μ μ₯
|
| 628 |
+
for scenario_data in data["scenarios"]:
|
| 629 |
+
existing_scenario = self.get_scenario(scenario_data["id"])
|
| 630 |
+
if not existing_scenario:
|
| 631 |
+
self.create_scenario(scenario_data)
|
| 632 |
+
print(f"β
μλλ¦¬μ€ μμ±: {scenario_data['title']}")
|
| 633 |
+
else:
|
| 634 |
+
print(f"βΉοΈ μλλ¦¬μ€ μ΄λ―Έ μ‘΄μ¬: {scenario_data['title']}")
|
| 635 |
+
|
| 636 |
+
def get_statistics_for_handoff(self, student_id: str) -> dict:
|
| 637 |
+
"""
|
| 638 |
+
νμλ³ ν΅κ³ (νμ νΈνμ± μ μ§)
|
| 639 |
+
|
| 640 |
+
Args:
|
| 641 |
+
student_id: νμ ID
|
| 642 |
+
|
| 643 |
+
Returns:
|
| 644 |
+
ν΅κ³ λ°μ΄ν°
|
| 645 |
+
"""
|
| 646 |
+
return self.get_student_statistics(student_id)
|
| 647 |
+
|
start.sh
CHANGED
|
@@ -3,19 +3,33 @@
|
|
| 3 |
echo "π₯ κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ μμ..."
|
| 4 |
echo ""
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
#
|
| 15 |
echo "ποΈ λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€..."
|
| 16 |
uv run init_db.py
|
| 17 |
|
| 18 |
-
#
|
| 19 |
echo ""
|
| 20 |
echo "π Gradio μ ν리μΌμ΄μ
μμ..."
|
| 21 |
echo "λΈλΌμ°μ μμ http://localhost:7860 μΌλ‘ μ μνμΈμ."
|
|
|
|
| 3 |
echo "π₯ κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ μμ..."
|
| 4 |
echo ""
|
| 5 |
|
| 6 |
+
# νκ²½ λ³μ νμΈ
|
| 7 |
+
if [ -f .env ]; then
|
| 8 |
+
export $(cat .env | grep -v '^#' | xargs)
|
| 9 |
+
fi
|
| 10 |
|
| 11 |
+
# 1. λ°μ΄ν°λ² μ΄μ€ μ€μ νμΈ
|
| 12 |
+
if [ -n "$SUPABASE_DB_URL" ]; then
|
| 13 |
+
echo "π Supabase λͺ¨λλ‘ μ€νν©λλ€..."
|
| 14 |
+
USE_LOCAL_POSTGRES=false
|
| 15 |
+
else
|
| 16 |
+
echo "π λ‘컬 PostgreSQL λͺ¨λλ‘ μ€νν©λλ€..."
|
| 17 |
+
USE_LOCAL_POSTGRES=true
|
| 18 |
+
|
| 19 |
+
# PostgreSQL μμ
|
| 20 |
+
echo "π¦ PostgreSQL 컨ν
μ΄λ μμ μ€..."
|
| 21 |
+
docker-compose up -d
|
| 22 |
+
|
| 23 |
+
# PostgreSQL μ€λΉ λκΈ°
|
| 24 |
+
echo "β³ PostgreSQL μ€λΉ λκΈ° μ€..."
|
| 25 |
+
sleep 5
|
| 26 |
+
fi
|
| 27 |
|
| 28 |
+
# 2. λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν
|
| 29 |
echo "ποΈ λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€..."
|
| 30 |
uv run init_db.py
|
| 31 |
|
| 32 |
+
# 3. Gradio μ± μ€ν
|
| 33 |
echo ""
|
| 34 |
echo "π Gradio μ ν리μΌμ΄μ
μμ..."
|
| 35 |
echo "λΈλΌμ°μ μμ http://localhost:7860 μΌλ‘ μ μνμΈμ."
|
supabase_init.sql
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- =====================================================
|
| 2 |
+
-- Supabase μ΄κΈ° μ€μ ν΅ν© μ€ν¬λ¦½νΈ
|
| 3 |
+
-- κ°νΈ μΈμμΈκ³ κ΅μ‘ νλ«νΌ
|
| 4 |
+
-- μ€ν μμ: μ΄ νμΌμ Supabase SQL Editorμμ ν λ²λ§ μ€ννμΈμ
|
| 5 |
+
-- =====================================================
|
| 6 |
+
|
| 7 |
+
-- =====================================================
|
| 8 |
+
-- 1. κΈ°λ³Έ ν
μ΄λΈ μμ±
|
| 9 |
+
-- =====================================================
|
| 10 |
+
|
| 11 |
+
-- 1.1 patients ν
μ΄λΈ (νμ κΈ°λ³Έ μ 보)
|
| 12 |
+
CREATE TABLE IF NOT EXISTS patients (
|
| 13 |
+
id VARCHAR(50) PRIMARY KEY,
|
| 14 |
+
name VARCHAR(100) NOT NULL,
|
| 15 |
+
age INTEGER NOT NULL,
|
| 16 |
+
gender VARCHAR(10) NOT NULL,
|
| 17 |
+
weight INTEGER,
|
| 18 |
+
height INTEGER,
|
| 19 |
+
diagnosis VARCHAR(200) NOT NULL,
|
| 20 |
+
admission_date DATE NOT NULL,
|
| 21 |
+
attending_physician VARCHAR(100),
|
| 22 |
+
allergies VARCHAR(200),
|
| 23 |
+
comorbidities TEXT,
|
| 24 |
+
room_number VARCHAR(20),
|
| 25 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 26 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
COMMENT ON TABLE patients IS 'νμ κΈ°λ³Έ μ 보';
|
| 30 |
+
COMMENT ON COLUMN patients.id IS 'νμ ID (μ: P001)';
|
| 31 |
+
COMMENT ON COLUMN patients.name IS 'νμ μ΄λ¦';
|
| 32 |
+
COMMENT ON COLUMN patients.age IS 'λμ΄';
|
| 33 |
+
COMMENT ON COLUMN patients.gender IS 'μ±λ³ (λ¨/μ¬)';
|
| 34 |
+
COMMENT ON COLUMN patients.diagnosis IS 'μ§λ¨λͺ
';
|
| 35 |
+
COMMENT ON COLUMN patients.admission_date IS 'μ
μμΌ';
|
| 36 |
+
COMMENT ON COLUMN patients.allergies IS 'μλ λ₯΄κΈ°';
|
| 37 |
+
|
| 38 |
+
-- 1.2 scenarios ν
μ΄λΈ (μΈμμΈκ³ μλ리μ€)
|
| 39 |
+
CREATE TABLE IF NOT EXISTS scenarios (
|
| 40 |
+
id VARCHAR(50) PRIMARY KEY,
|
| 41 |
+
patient_id VARCHAR(50) NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
|
| 42 |
+
title VARCHAR(200) NOT NULL,
|
| 43 |
+
day INTEGER NOT NULL,
|
| 44 |
+
handoff_situation VARCHAR(200),
|
| 45 |
+
vitals JSONB,
|
| 46 |
+
labs JSONB,
|
| 47 |
+
orders JSONB,
|
| 48 |
+
nursing_notes JSONB,
|
| 49 |
+
physical_exam JSONB,
|
| 50 |
+
imaging JSONB,
|
| 51 |
+
surgery_info JSONB,
|
| 52 |
+
discharge_education JSONB,
|
| 53 |
+
hospital_day INTEGER,
|
| 54 |
+
status_badge VARCHAR(20),
|
| 55 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 56 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 57 |
+
);
|
| 58 |
+
|
| 59 |
+
COMMENT ON TABLE scenarios IS 'μΈμμΈκ³ μλ리μ€';
|
| 60 |
+
COMMENT ON COLUMN scenarios.id IS 'μλλ¦¬μ€ ID (μ: S001_D0_ER_WARD)';
|
| 61 |
+
COMMENT ON COLUMN scenarios.patient_id IS 'νμ ID (μΈλν€)';
|
| 62 |
+
COMMENT ON COLUMN scenarios.title IS 'μλλ¦¬μ€ μ λͺ©';
|
| 63 |
+
COMMENT ON COLUMN scenarios.day IS 'μ¬μ μΌμ (0, 1, 2...)';
|
| 64 |
+
COMMENT ON COLUMN scenarios.handoff_situation IS 'μΈμμΈκ³ μν© (μ: μκΈμ€βλ³λ)';
|
| 65 |
+
|
| 66 |
+
-- 1.3 handoff_records ν
μ΄λΈ (μΈμμΈκ³ κΈ°λ‘ λ° νκ°)
|
| 67 |
+
-- λν κΈ°λ° νκ°λ₯Ό κ³ λ €νμ¬ NULL νμ©
|
| 68 |
+
CREATE TABLE IF NOT EXISTS handoff_records (
|
| 69 |
+
id SERIAL PRIMARY KEY,
|
| 70 |
+
session_id VARCHAR(100),
|
| 71 |
+
student_id VARCHAR(50) NOT NULL,
|
| 72 |
+
scenario_id VARCHAR(50) NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
| 73 |
+
submitted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 74 |
+
situation TEXT,
|
| 75 |
+
background TEXT,
|
| 76 |
+
assessment TEXT,
|
| 77 |
+
recommendation TEXT,
|
| 78 |
+
total_score FLOAT,
|
| 79 |
+
category_scores JSONB,
|
| 80 |
+
strengths JSONB,
|
| 81 |
+
improvements JSONB,
|
| 82 |
+
detailed_feedback TEXT,
|
| 83 |
+
-- λν κΈ°λ° νκ°μ© 컬λΌ
|
| 84 |
+
missing_critical_info JSONB,
|
| 85 |
+
safety_concerns JSONB
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
COMMENT ON TABLE handoff_records IS 'νμμ μΈμμΈκ³ κΈ°λ‘ λ° AI νκ° κ²°κ³Ό';
|
| 89 |
+
COMMENT ON COLUMN handoff_records.session_id IS 'μ±ν
μΈμ
ID';
|
| 90 |
+
COMMENT ON COLUMN handoff_records.student_id IS 'νμ μλ³μ';
|
| 91 |
+
COMMENT ON COLUMN handoff_records.scenario_id IS 'μλλ¦¬μ€ ID (μΈλν€)';
|
| 92 |
+
COMMENT ON COLUMN handoff_records.missing_critical_info IS 'λλ½λ μ€μ μ 보 λͺ©λ‘';
|
| 93 |
+
COMMENT ON COLUMN handoff_records.safety_concerns IS 'νμ μμ κ΄λ ¨ μ£Όμμ¬ν λͺ©λ‘';
|
| 94 |
+
|
| 95 |
+
-- 1.4 chat_history ν
μ΄λΈ (νμκ³Ό AI κ°μ μ±ν
κΈ°λ‘)
|
| 96 |
+
CREATE TABLE IF NOT EXISTS chat_history (
|
| 97 |
+
id SERIAL PRIMARY KEY,
|
| 98 |
+
session_id VARCHAR(100) NOT NULL,
|
| 99 |
+
student_id VARCHAR(50) NOT NULL,
|
| 100 |
+
scenario_id VARCHAR(50) NOT NULL,
|
| 101 |
+
role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant')),
|
| 102 |
+
message TEXT NOT NULL,
|
| 103 |
+
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
COMMENT ON TABLE chat_history IS 'νμκ³Ό AI κ°μ μ±ν
κΈ°λ‘';
|
| 107 |
+
|
| 108 |
+
-- 1.5 sbar_drafts ν
μ΄λΈ (μμ± μ€μΈ SBAR μμ μ μ₯)
|
| 109 |
+
CREATE TABLE IF NOT EXISTS sbar_drafts (
|
| 110 |
+
id SERIAL PRIMARY KEY,
|
| 111 |
+
session_id VARCHAR(100) NOT NULL UNIQUE,
|
| 112 |
+
student_id VARCHAR(50) NOT NULL,
|
| 113 |
+
scenario_id VARCHAR(50) NOT NULL,
|
| 114 |
+
situation TEXT DEFAULT '',
|
| 115 |
+
background TEXT DEFAULT '',
|
| 116 |
+
assessment TEXT DEFAULT '',
|
| 117 |
+
recommendation TEXT DEFAULT '',
|
| 118 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 119 |
+
);
|
| 120 |
+
|
| 121 |
+
COMMENT ON TABLE sbar_drafts IS 'μμ± μ€μΈ SBAR μμ μ μ₯';
|
| 122 |
+
|
| 123 |
+
-- 1.6 sessions ν
μ΄λΈ (μΈμ
κ΄λ¦¬)
|
| 124 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 125 |
+
id SERIAL PRIMARY KEY,
|
| 126 |
+
session_id VARCHAR(50) UNIQUE NOT NULL,
|
| 127 |
+
student_id VARCHAR(50) NOT NULL,
|
| 128 |
+
scenario_id VARCHAR(50) NOT NULL,
|
| 129 |
+
status VARCHAR(20) DEFAULT 'active',
|
| 130 |
+
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 131 |
+
ended_at TIMESTAMP WITH TIME ZONE,
|
| 132 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 133 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 134 |
+
);
|
| 135 |
+
|
| 136 |
+
COMMENT ON TABLE sessions IS 'TSID κΈ°λ° μ±ν
μΈμ
κ΄λ¦¬ ν
μ΄λΈ';
|
| 137 |
+
COMMENT ON COLUMN sessions.session_id IS 'TSID κΈ°λ° μΈμ
μλ³μ (ses_XXXXX νμ)';
|
| 138 |
+
COMMENT ON COLUMN sessions.status IS 'μΈμ
μν: active, completed, archived';
|
| 139 |
+
|
| 140 |
+
-- =====================================================
|
| 141 |
+
-- 2. μΈλ±μ€ μμ± (μ±λ₯ μ΅μ ν)
|
| 142 |
+
-- =====================================================
|
| 143 |
+
|
| 144 |
+
-- handoff_records μΈλ±μ€
|
| 145 |
+
CREATE INDEX IF NOT EXISTS idx_handoff_records_student ON handoff_records(student_id);
|
| 146 |
+
CREATE INDEX IF NOT EXISTS idx_handoff_records_scenario ON handoff_records(scenario_id);
|
| 147 |
+
CREATE INDEX IF NOT EXISTS idx_handoff_records_session ON handoff_records(session_id);
|
| 148 |
+
CREATE INDEX IF NOT EXISTS idx_handoff_records_submitted ON handoff_records(submitted_at DESC);
|
| 149 |
+
|
| 150 |
+
-- chat_history μΈλ±μ€
|
| 151 |
+
CREATE INDEX IF NOT EXISTS idx_chat_history_session ON chat_history(session_id);
|
| 152 |
+
CREATE INDEX IF NOT EXISTS idx_chat_history_student ON chat_history(student_id);
|
| 153 |
+
CREATE INDEX IF NOT EXISTS idx_chat_history_timestamp ON chat_history(timestamp);
|
| 154 |
+
|
| 155 |
+
-- sbar_drafts μΈλ±μ€
|
| 156 |
+
CREATE INDEX IF NOT EXISTS idx_sbar_drafts_session ON sbar_drafts(session_id);
|
| 157 |
+
CREATE INDEX IF NOT EXISTS idx_sbar_drafts_student ON sbar_drafts(student_id);
|
| 158 |
+
|
| 159 |
+
-- scenarios μΈλ±μ€
|
| 160 |
+
CREATE INDEX IF NOT EXISTS idx_scenarios_patient ON scenarios(patient_id);
|
| 161 |
+
CREATE INDEX IF NOT EXISTS idx_scenarios_day ON scenarios(day);
|
| 162 |
+
|
| 163 |
+
-- sessions μΈλ±μ€
|
| 164 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_student_id ON sessions(student_id);
|
| 165 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_scenario_id ON sessions(scenario_id);
|
| 166 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
| 167 |
+
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
|
| 168 |
+
|
| 169 |
+
-- =====================================================
|
| 170 |
+
-- 3. νΈλ¦¬κ±° ν¨μ λ° νΈλ¦¬κ±° μμ±
|
| 171 |
+
-- =====================================================
|
| 172 |
+
|
| 173 |
+
-- updated_at μλ μ
λ°μ΄νΈ ν¨μ
|
| 174 |
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
| 175 |
+
RETURNS TRIGGER AS $$
|
| 176 |
+
BEGIN
|
| 177 |
+
NEW.updated_at = NOW();
|
| 178 |
+
RETURN NEW;
|
| 179 |
+
END;
|
| 180 |
+
$$ LANGUAGE plpgsql;
|
| 181 |
+
|
| 182 |
+
-- μΈμ
μ’
λ£ μ ended_at μλ μ
λ°μ΄νΈ ν¨μ
|
| 183 |
+
CREATE OR REPLACE FUNCTION update_session_ended_at()
|
| 184 |
+
RETURNS TRIGGER AS $$
|
| 185 |
+
BEGIN
|
| 186 |
+
IF NEW.status = 'completed' OR NEW.status = 'archived' THEN
|
| 187 |
+
IF NEW.ended_at IS NULL THEN
|
| 188 |
+
NEW.ended_at = NOW();
|
| 189 |
+
END IF;
|
| 190 |
+
END IF;
|
| 191 |
+
NEW.updated_at = NOW();
|
| 192 |
+
RETURN NEW;
|
| 193 |
+
END;
|
| 194 |
+
$$ LANGUAGE plpgsql;
|
| 195 |
+
|
| 196 |
+
-- νΈλ¦¬κ±° μμ±
|
| 197 |
+
CREATE TRIGGER update_patients_updated_at
|
| 198 |
+
BEFORE UPDATE ON patients
|
| 199 |
+
FOR EACH ROW
|
| 200 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 201 |
+
|
| 202 |
+
CREATE TRIGGER update_scenarios_updated_at
|
| 203 |
+
BEFORE UPDATE ON scenarios
|
| 204 |
+
FOR EACH ROW
|
| 205 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 206 |
+
|
| 207 |
+
CREATE TRIGGER update_sbar_drafts_updated_at
|
| 208 |
+
BEFORE UPDATE ON sbar_drafts
|
| 209 |
+
FOR EACH ROW
|
| 210 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 211 |
+
|
| 212 |
+
CREATE TRIGGER update_sessions_updated_at
|
| 213 |
+
BEFORE UPDATE ON sessions
|
| 214 |
+
FOR EACH ROW
|
| 215 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 216 |
+
|
| 217 |
+
CREATE TRIGGER update_session_on_status_change
|
| 218 |
+
BEFORE UPDATE ON sessions
|
| 219 |
+
FOR EACH ROW
|
| 220 |
+
WHEN (OLD.status IS DISTINCT FROM NEW.status)
|
| 221 |
+
EXECUTE FUNCTION update_session_ended_at();
|
| 222 |
+
|
| 223 |
+
-- =====================================================
|
| 224 |
+
-- 4. Row Level Security (RLS) μ€μ
|
| 225 |
+
-- =====================================================
|
| 226 |
+
|
| 227 |
+
-- RLS νμ±ν
|
| 228 |
+
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
|
| 229 |
+
ALTER TABLE scenarios ENABLE ROW LEVEL SECURITY;
|
| 230 |
+
ALTER TABLE handoff_records ENABLE ROW LEVEL SECURITY;
|
| 231 |
+
ALTER TABLE chat_history ENABLE ROW LEVEL SECURITY;
|
| 232 |
+
ALTER TABLE sbar_drafts ENABLE ROW LEVEL SECURITY;
|
| 233 |
+
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
| 234 |
+
|
| 235 |
+
-- κΈ°μ‘΄ μ μ±
μμ (μ€λ³΅ λ°©μ§)
|
| 236 |
+
DO $$
|
| 237 |
+
BEGIN
|
| 238 |
+
-- Chat History μ μ±
μμ
|
| 239 |
+
DROP POLICY IF EXISTS "Allow all for chat_history" ON chat_history;
|
| 240 |
+
DROP POLICY IF EXISTS "Allow all operations for anon users on chat_history" ON chat_history;
|
| 241 |
+
DROP POLICY IF EXISTS "Allow all operations for authenticated users on chat_history" ON chat_history;
|
| 242 |
+
DROP POLICY IF EXISTS "Enable all access for chat_history" ON chat_history;
|
| 243 |
+
DROP POLICY IF EXISTS "chat_history_all_access" ON chat_history;
|
| 244 |
+
|
| 245 |
+
-- Handoff Records μ μ±
μμ
|
| 246 |
+
DROP POLICY IF EXISTS "Allow all for handoff_records" ON handoff_records;
|
| 247 |
+
DROP POLICY IF EXISTS "Allow all operations for anon users on handoff_records" ON handoff_records;
|
| 248 |
+
DROP POLICY IF EXISTS "Allow all operations for authenticated users on handoff_records" ON handoff_records;
|
| 249 |
+
DROP POLICY IF EXISTS "Enable all access for handoff_records" ON handoff_records;
|
| 250 |
+
DROP POLICY IF EXISTS "handoff_records_all_access" ON handoff_records;
|
| 251 |
+
|
| 252 |
+
-- SBAR Drafts μ μ±
μμ
|
| 253 |
+
DROP POLICY IF EXISTS "Allow all for sbar_drafts" ON sbar_drafts;
|
| 254 |
+
DROP POLICY IF EXISTS "Allow all operations for anon users on sbar_drafts" ON sbar_drafts;
|
| 255 |
+
DROP POLICY IF EXISTS "Allow all operations for authenticated users on sbar_drafts" ON sbar_drafts;
|
| 256 |
+
DROP POLICY IF EXISTS "Enable all access for sbar_drafts" ON sbar_drafts;
|
| 257 |
+
DROP POLICY IF EXISTS "sbar_drafts_all_access" ON sbar_drafts;
|
| 258 |
+
|
| 259 |
+
-- Patients μ μ±
μμ
|
| 260 |
+
DROP POLICY IF EXISTS "Allow read access for all users" ON patients;
|
| 261 |
+
DROP POLICY IF EXISTS "Allow all for patients" ON patients;
|
| 262 |
+
DROP POLICY IF EXISTS "Allow all operations for anon users on patients" ON patients;
|
| 263 |
+
DROP POLICY IF EXISTS "Allow all operations for authenticated users on patients" ON patients;
|
| 264 |
+
DROP POLICY IF EXISTS "Enable all operations for anon users" ON patients;
|
| 265 |
+
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON patients;
|
| 266 |
+
DROP POLICY IF EXISTS "patients_all_access" ON patients;
|
| 267 |
+
|
| 268 |
+
-- Scenarios μ μ±
μμ
|
| 269 |
+
DROP POLICY IF EXISTS "Allow read access for all users" ON scenarios;
|
| 270 |
+
DROP POLICY IF EXISTS "Allow all for scenarios" ON scenarios;
|
| 271 |
+
DROP POLICY IF EXISTS "Allow all operations for anon users on scenarios" ON scenarios;
|
| 272 |
+
DROP POLICY IF EXISTS "Allow all operations for authenticated users on scenarios" ON scenarios;
|
| 273 |
+
DROP POLICY IF EXISTS "Enable all operations for anon users" ON scenarios;
|
| 274 |
+
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON scenarios;
|
| 275 |
+
DROP POLICY IF EXISTS "scenarios_all_access" ON scenarios;
|
| 276 |
+
|
| 277 |
+
-- Sessions μ μ±
μμ
|
| 278 |
+
DROP POLICY IF EXISTS "Enable all operations for anon users" ON sessions;
|
| 279 |
+
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON sessions;
|
| 280 |
+
DROP POLICY IF EXISTS "sessions_all_access" ON sessions;
|
| 281 |
+
END $$;
|
| 282 |
+
|
| 283 |
+
-- μ μ μ±
μμ± (λͺ¨λ μ¬μ©μ μ κ·Ό νμ©)
|
| 284 |
+
CREATE POLICY "patients_all_access" ON patients
|
| 285 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 286 |
+
|
| 287 |
+
CREATE POLICY "scenarios_all_access" ON scenarios
|
| 288 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 289 |
+
|
| 290 |
+
CREATE POLICY "handoff_records_all_access" ON handoff_records
|
| 291 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 292 |
+
|
| 293 |
+
CREATE POLICY "chat_history_all_access" ON chat_history
|
| 294 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 295 |
+
|
| 296 |
+
CREATE POLICY "sbar_drafts_all_access" ON sbar_drafts
|
| 297 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 298 |
+
|
| 299 |
+
CREATE POLICY "sessions_all_access" ON sessions
|
| 300 |
+
FOR ALL USING (true) WITH CHECK (true);
|
| 301 |
+
|
| 302 |
+
-- =====================================================
|
| 303 |
+
-- μλ£ λ©μμ§
|
| 304 |
+
-- =====================================================
|
| 305 |
+
|
| 306 |
+
DO $$
|
| 307 |
+
BEGIN
|
| 308 |
+
RAISE NOTICE '';
|
| 309 |
+
RAISE NOTICE 'ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ';
|
| 310 |
+
RAISE NOTICE 'β β
Supabase μ΄κΈ° μ€μ μλ£! β';
|
| 311 |
+
RAISE NOTICE 'ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ';
|
| 312 |
+
RAISE NOTICE '';
|
| 313 |
+
RAISE NOTICE 'π μμ±λ ν
μ΄λΈ:';
|
| 314 |
+
RAISE NOTICE ' - patients (νμ μ 보)';
|
| 315 |
+
RAISE NOTICE ' - scenarios (μλ리μ€)';
|
| 316 |
+
RAISE NOTICE ' - handoff_records (μΈμμΈκ³ κΈ°λ‘)';
|
| 317 |
+
RAISE NOTICE ' - chat_history (μ±ν
κΈ°λ‘)';
|
| 318 |
+
RAISE NOTICE ' - sbar_drafts (SBAR μ΄μ)';
|
| 319 |
+
RAISE NOTICE ' - sessions (μΈμ
κ΄λ¦¬)';
|
| 320 |
+
RAISE NOTICE '';
|
| 321 |
+
RAISE NOTICE 'π RLS μ μ±
: λͺ¨λ ν
μ΄λΈμ μ 체 μ κ·Ό νμ©';
|
| 322 |
+
RAISE NOTICE 'π μΈλ±μ€: μ±λ₯ μ΅μ ν μλ£';
|
| 323 |
+
RAISE NOTICE 'βοΈ νΈλ¦¬κ±°: μλ updated_at μ
λ°μ΄νΈ λ° μΈμ
μ’
λ£ μ²λ¦¬';
|
| 324 |
+
RAISE NOTICE '';
|
| 325 |
+
RAISE NOTICE 'π λ€μ λ¨κ³:';
|
| 326 |
+
RAISE NOTICE ' 1. uv run scripts/init_db.py - μλλ¦¬μ€ λ°μ΄ν° λ‘λ';
|
| 327 |
+
RAISE NOTICE ' 2. uv run test_supabase_connection.py - μ°κ²° ν
μ€νΈ';
|
| 328 |
+
RAISE NOTICE ' 3. uv run app.py - μ± μ€ν';
|
| 329 |
+
RAISE NOTICE '';
|
| 330 |
+
END $$;
|
| 331 |
+
|