Skip to content

Dev-MAMOKEY/Mom-Med-Backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

106 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

์—„๋งˆ์•ฝ (Mom's Med) โ€” Backend

2026 ๋ณด๊ฑด์˜๋ฃŒ๋น…๋ฐ์ดํ„ฐยทAI ํ™œ์šฉ ์ฐฝ์—…๊ฒฝ์ง„๋Œ€ํšŒ ์ถœํ’ˆ์ž‘ Java 21 + Spring Boot 4.0.6 + PostgreSQL 14+ + Redis 7+

์ž๋…€๊ฐ€ ๋ถ€๋ชจ๋‹˜์˜ ์•ฝยท๊ธฐ์ €์งˆํ™˜ยท์•Œ๋ ˆ๋ฅด๊ธฐ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด (1) DUR + NB 2๊ฒน ์•ฝ๋ฌผ ์•ˆ์ „ ๊ฒ€์ฆ, (2) ์•Œ์•ฝ ์‚ฌ์ง„ ์‹๋ณ„, (3) 8์งˆ๋ณ‘ ร— 4๋‹จ๊ณ„ = 32 ๋ฃฐ ๊ธฐ๋ฐ˜ ๋‚ ์”จ ์—ฐ๊ณ„ ์ฃผ์˜ ํ‘ธ์‹œ, (4) ์‘๊ธ‰์‹ค์šฉ QR ์นด๋“œ๋ฅผ ํ†ตํ•ฉ ์ œ๊ณตํ•œ๋‹ค.


๐Ÿณ ๋„์ปค๋กœ 5๋ถ„ ์•ˆ์— ๋„์šฐ๊ธฐ (โ˜… ์ฒ˜์Œ ๋ฐ›๋Š” ๋ถ„์€ ์—ฌ๊ธฐ๋ถ€ํ„ฐ)

์ „์ฒด ๋ฐฑ์—”๋“œ(Spring Boot + PostgreSQL + Redis)๋ฅผ ํ•œ ์ค„๋กœ ๋„์šธ ์ˆ˜ ์žˆ๊ฒŒ ์ปจํ…Œ์ด๋„ˆํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์–ด๋–ค OSยทํ™˜๊ฒฝ์ด๋“  ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์ „ ์š”๊ตฌ์‚ฌํ•ญ

1. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ž‘์„ฑ

cp .env.example .env

.env๋ฅผ ์—ด์–ด์„œ ๋ฐ˜๋“œ์‹œ ์ฑ„์›Œ์•ผ ํ•  ๊ฐ’ (ํŒ€์žฅ์—๊ฒŒ ๋ฐ›๊ธฐ):

  • POSTGRES_PASSWORD โ€” ๋„์ปค PG ๋น„๋ฐ€๋ฒˆํ˜ธ (์•„๋ฌด๊ฑฐ๋‚˜ ์ •ํ•ด๋„ OK)
  • APP_ENCRYPTION_KEY โ€” 32๋ฐ”์ดํŠธ base64 (ํŒ€ ๊ณตํ†ต๊ฐ’)
  • MFDS_API_KEY / HIRA_API_KEY / KMA_API_KEY โ€” ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ ์ธ์ฆํ‚ค (ํŒ€ ๊ณตํ†ต)
  • GEMINI_API_KEY โ€” Google AI Studio ํ‚ค (ํŒ€ ๊ณตํ†ต)

๋ณด์•ˆ: .env๋Š” .gitignore๋กœ ์ œ์™ธ๋˜์–ด git์— ์•ˆ ์˜ฌ๋ผ๊ฐ‘๋‹ˆ๋‹ค. ํŒ€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋งค๋‹ˆ์ €์—์„œ ๋ฐ›์œผ์„ธ์š”.

2. ์ปจํ…Œ์ด๋„ˆ ๋„์šฐ๊ธฐ

docker compose up -d --build

์ฒซ ๋นŒ๋“œ๋Š” 5~10๋ถ„ (Java ๋นŒ๋“œ + ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ). ๋‘ ๋ฒˆ์งธ๋ถ€ํ„ฐ 30์ดˆ.

3. ํ™•์ธ

docker compose ps              # 3๊ฐœ ๋ชจ๋‘ (healthy)
docker compose logs -f backend # "Started BackendApplication in N seconds"
  • API ๋ฌธ์„œ: http://localhost:8080/swagger-ui.html
  • ๋ฐ๋ชจ ์‹œ๋“œ(๋ถ€๋ชจ 2๋ช… + ์•ฝ 4์ข… + ์–ด๋จธ๋‹ˆ ๋ฉ”ํŠธํฌ๋ฅด๋ฏผ)๋Š” ์ž๋™์œผ๋กœ Flyway๊ฐ€ ์ ์žฌํ•ฉ๋‹ˆ๋‹ค โ†’ ํ”„๋ก ํŠธ์—”๋“œ BLOCK ์‹œ์—ฐ ์ฆ‰์‹œ ๊ฐ€๋Šฅ.

4. DUR 837Kํ–‰ ์ ์žฌ (์„ ํƒ) โ€” BLOCK ์‹œ์—ฐ ์™„์ „์ฒด๋กœ

๊ธฐ๋ณธ ๋„์ปค ์…‹์—…์—๋Š” DUR ๋ณ‘์šฉ๊ธˆ๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค. BLOCK ๋ชจ๋‹ฌ์ด ์ง„์งœ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด:

  1. ํŒ€์žฅ์—๊ฒŒ DUR CSV 5์ข… ๊ตฌ๊ธ€๋“œ๋ผ์ด๋ธŒ ๋งํฌ ๋ฐ›๊ธฐ
  2. ์••์ถ• ํ’€๊ณ  backend/dur-data/ ํด๋”์— 5๊ฐœ CSV ๊ทธ๋Œ€๋กœ ๋ฐฐ์น˜
  3. .env์— ์ถ”๊ฐ€:
    DUR_DATA_DIR=./dur-data
    APP_ETL_DUR_ENABLED=true
  4. docker compose down && docker compose up -d โ€” ์ฒซ ๋ถ€ํŒ… ์‹œ 1๋ถ„ ETL ์ž๋™ ์ง„ํ–‰ (docker compose logs -f backend)

ETL์€ idempotent(์ค‘๋ณต ๋ฌด์‹œ)๋ผ ์ดํ›„ ๋งค ๋ถ€ํŒ…์— ๊ทธ๋Œ€๋กœ ๋‘ฌ๋„ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

์ž์ฃผ ์“ฐ๋Š” ๋ช…๋ น

docker compose ps                     # ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ
docker compose logs -f backend        # ๋ฐฑ์—”๋“œ ๋กœ๊ทธ ๋ผ์ด๋ธŒ
docker compose stop                   # ์ž„์‹œ ์ค‘์ง€ (๋ฐ์ดํ„ฐ ์œ ์ง€)
docker compose start                  # ๋‹ค์‹œ ์‹œ์ž‘
docker compose down                   # ์ปจํ…Œ์ด๋„ˆ ์ œ๊ฑฐ (๋ณผ๋ฅจ ๋ฐ์ดํ„ฐ๋Š” ๋‚จ์Œ)
docker compose down -v                # ์™„์ „ ์‚ญ์ œ (DB๊นŒ์ง€ ๋‚ ์•„๊ฐ)
docker compose up -d --build          # ์ฝ”๋“œ ๋ณ€๊ฒฝ ํ›„ ์žฌ๋นŒ๋“œ

๋ฌธ์ œ ํ•ด๊ฒฐ

์ฆ์ƒ ํ•ด๊ฒฐ
ํฌํŠธ 8080ยท15432ยท16379 ์ถฉ๋Œ .env์˜ BACKEND_PORT / POSTGRES_PORT / REDIS_PORT ๋ณ€๊ฒฝ
POSTGRES_PASSWORD must be set ์—๋Ÿฌ .env ์•ˆ ๋งŒ๋“ค์—ˆ์Œ โ€” cp .env.example .env ํ›„ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž‘์„ฑ
ํ˜ธ์ŠคํŠธ์—์„œ localhost:8080 ์‘๋‹ต ์—†์Œ (Windows) IPv6 localhost ํ•ด์„ ์ด์Šˆ โ€” ํ”„๋ก ํŠธ์—”๋“œ .env์˜ API_BASE_URL์„ 127.0.0.1:8080๋กœ
๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์‹ถ์Œ docker compose down -v && docker compose up -d --build (DB๊นŒ์ง€ ๋ฆฌ์…‹)

๐Ÿ›  ๋„์ปค ์•ˆ ์“ฐ๊ณ  ํ˜ธ์ŠคํŠธ์—์„œ ์ง์ ‘ ๋„์šฐ๊ธฐ (๋กœ์ปฌ ๊ฐœ๋ฐœ์šฉ)

  • PostgreSQL 16+, Redis 7+ ํ˜ธ์ŠคํŠธ์— ์„ค์น˜
  • src/main/resources/application-local.yml ์ž‘์„ฑ (DB ๋น„๋ฒˆยทAPI ํ‚ค ์ง์ ‘ ์ž…๋ ฅ โ€” .gitignore๋กœ git ์ œ์™ธ๋จ)
  • ./gradlew bootRun --args='--spring.profiles.active=local'

0. ํŒ€ ์ž‘์—… ๋ถ„๋‹ด ์š”์•ฝ (โ˜… ๊ฐ€์žฅ ๋จผ์ € ๋ณด์„ธ์š”)

Track A = ๋™ํ™˜ / Track B = ์ˆ˜์ข… / ๐Ÿค = ๊ณต๋™ ์ž‘์—…

์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ + ๋‹ด๋‹น์ž

[๐Ÿค Slice 00 ์ธํ”„๋ผ]           โ† ๋‘˜์ด ๊ฐ™์ด ์…‹์—…ํ•˜๊ณ  ๊ฐˆ๋ผ์ง
       โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[A] 01 ์•ฝ ๋งˆ์Šคํ„ฐ โ”€โ”€โ”
       โ”‚                            โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[A] 02 DUR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
       โ”‚                            โ–ผ
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[A] 03 NB ์ถ”์ถœ โ”€โ”€ [๐Ÿค SafetyJudge ํ†ตํ•ฉ]
       โ”‚                            โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[B] 04 ๋ถ€๋ชจ/์•ฝ์žฅ โ˜… โ”˜   โ† ๋ฐ๋ชจ ํ•ต์‹ฌ, 04 Controller๋Š” B / SafetyJudge๋Š” A
       โ”‚                            โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[B] 05 ์งˆ๋ณ‘/๊ธˆ๊ธฐ โ”€โ”€โ”ค
       โ”‚                            โ”‚
       โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[A] 06 ๋‚ ์”จ ๋ฃฐ์…‹ โ”€โ”€โ”˜
       โ”‚                            โ”‚
       โ”‚         [๐Ÿค Slice 07 ETL+ํ‘ธ์‹œ]   โ† ๋ฐ์ดํ„ฐ=A / ๋ถ€๋ชจ์ปจํ…์ŠคํŠธยทํ‘ธ์‹œ=B
       โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[B] 08 ์‘๊ธ‰์นด๋“œ

์Šฌ๋ผ์ด์Šค๋ณ„ ๋‹ด๋‹น

# ์Šฌ๋ผ์ด์Šค ๋‹ด๋‹น ๋น„๊ณ 
00 ์ธํ”„๋ผ + ๊ณตํ†ต ์œ ํ‹ธ ๐Ÿค ๊ณต๋™ DDL/Flyway/๊ณตํ†ต ํด๋ž˜์Šค๋Š” ๋‘˜์ด ํ•ฉ์˜ํ•ด์„œ ์„ค๊ณ„
01 ์•ฝ ์‹๋ณ„ + ์•Œ์•ฝ ์‚ฌ์ง„ (์‹์•ฝ์ฒ˜ API) A ๋™ํ™˜ ์™ธ๋ถ€ API ํด๋ผ์ด์–ธํŠธ ๋ผ์ธ
02 DUR ๋ณ‘์šฉ๊ธˆ๊ธฐ (837kํ–‰ CSV ์ ์žฌ + ์ •ํ™•๋งค์นญ) A ๋™ํ™˜ ์ธ๋ฑ์Šค ํŠœ๋‹ยทEXPLAIN ๊ฒ€์ฆ
03 NB AI ์ถ”์ถœ + ํ™˜๊ฐ ๊ฒ€์ฆ (LLM) A ๋™ํ™˜ Gemini ํด๋ผ์ด์–ธํŠธยทํ”„๋กฌํ”„ํŠธ
04 ๋ถ€๋ชจ/์•ฝ์žฅ/์•Œ๋ ˆ๋ฅด๊ธฐ/๋””๋ฐ”์ด์Šคํ† ํฐ (โ˜… ๋ฐ๋ชจ ํ•ต์‹ฌ) B ์ˆ˜์ข… + ๐Ÿค ํ†ตํ•ฉ ControllerยทService๋Š” B / SafetyJudge ํ˜ธ์ถœ ์ง€์ ์€ A์™€ ํ•ฉ์˜
05 ๊ธฐ์ €์งˆํ™˜ + ํ™˜์ž๋ถ„๋ฅ˜ ๊ธˆ๊ธฐ B ์ˆ˜์ข… HIRA 12904ยทKCD ๋งคํ•‘
06 ๋‚ ์”จ ๋ฃฐ์…‹ + ๊ฒฉ์ž์ขŒํ‘œ + ๋ฃฉ์—… A ๋™ํ™˜ seed JSONยทxlsx ์ ์žฌ
07 ๊ธฐ์ƒ์ฒญ ETL + ๋งค์ผ ํ‘ธ์‹œ (Redis ๋ฝ) ๐Ÿค ๊ณต๋™ ๊ธฐ์ƒ์ฒญ ํ˜ธ์ถœยท์ž์ฒด ํŒ์ •=A / ๋ถ€๋ชจ์ปจํ…์ŠคํŠธยทํ‘ธ์‹œ๋ฐœ์†ก=B
08 ์‘๊ธ‰์นด๋“œ + HIRA ๋ณ‘์›/์•ฝ๊ตญ B ์ˆ˜์ข… QRยทRate Limitยท๊ณต๊ฐœ ์—”๋“œํฌ์ธํŠธ

์ฃผ์ฐจ๋ณ„ ๋™์‹œ ์ง„ํ–‰ ๊ณ„ํš (5์ฃผ)

Week A ๋™ํ™˜ B ์ˆ˜์ข… ๐Ÿค ๊ณต๋™
1 Slice 01 (์‹์•ฝ์ฒ˜ ํด๋ผ์ด์–ธํŠธ + drugs_master) Slice 04 ๋„๋ฉ”์ธ ์Šค์บํด๋“œ (PatientProfile/Allergy/DeviceToken ์—”ํ‹ฐํ‹ฐ+CRUD, SafetyJudge๋Š” mock) Slice 00: docker-compose, Flyway V001~V003, ๊ณตํ†ต ์œ ํ‹ธ 7์ข…, ํ™˜๊ฒฝ๋ณ€์ˆ˜
2 Slice 02 (DUR 5 CSV ์ ์žฌ + DurEngineService) Slice 04 Medication CRUD (mock SafetyJudge๋กœ ์šฐ์„  ๋™์ž‘) API ์‘๋‹ต ๊ตฌ์กฐ ํ•ฉ์˜ ์ ๊ฒ€ (HTTP 409 BLOCK ๋“ฑ)
3 Slice 03 (NB ์ถ”์ถœ + ํ™˜๊ฐ ๊ฒ€์ฆ + SafetyJudge ์™„์„ฑ) Slice 05 (HIRA 12904 + patient_conditions ์ •๊ทœํ™”) ๐Ÿค SafetyJudge ํ†ตํ•ฉ โ€” A๊ฐ€ ๋งŒ๋“  SafetyJudgeService.judgeFull()์„ B์˜ MedicationController๊ฐ€ ํ˜ธ์ถœํ•˜๋„๋ก ์™€์ด์–ด๋ง. HTTP 409 ์‘๋‹ต e2e ๊ฒ€์ฆ โ˜…
4 Slice 06 (weather_rules 32๋ฃฐยท๊ฒฉ์ž์ขŒํ‘œ ์ ์žฌ + lookupRules) Slice 08 (HIRA 11999/12101/12100 + EmergencyCard + /em/{token} ๊ณต๊ฐœ ์—”๋“œํฌ์ธํŠธ) โ€”
5 Slice 07 ETL ๋ถ€๋ถ„ (KMA ํ˜ธ์ถœยท์ž์ฒด ํŒ์ •ยทobservations ์ ์žฌ) Slice 07 ํ‘ธ์‹œ ๋ถ€๋ถ„ (parent_conditions ์กฐํšŒยทdevice_tokens ๋ณตํ˜ธํ™”ยทํ‘ธ์‹œ ๋ฐœ์†ก mock) ๐Ÿค Slice 07 ํ†ตํ•ฉ โ€” A์˜ ETL ๊ฒฐ๊ณผ๋ฅผ B์˜ ํ‘ธ์‹œ ์žก์ด ์†Œ๋น„. Redisson ๋ฝ 2๋‹จ ๊ฐ™์ด ๊ฒ€์ฆ. ๋ฐ๋ชจ ์‹œ๋‚˜๋ฆฌ์˜ค e2e ๋ฆฌํ—ˆ์„ค

๊ณต๋™ ์ž‘์—…์ด ํ•„์š”ํ•œ ํ•ฉ์˜ ํฌ์ธํŠธ (๐Ÿค)

  1. Slice 00 ์ธํ”„๋ผ โ€” Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋ช…๋ช… ๊ทœ์น™, 4-schema, ๊ณตํ†ต ์—”ํ‹ฐํ‹ฐ(BaseTimeEntity), ์˜ˆ์™ธ ๊ณ„์ธต(SafetyBlockException, ApiException), ์‘๋‹ต ํฌ๋งท(ApiResponse<T>, BlockErrorResponse)
  2. SafetyJudgeService ์ธํ„ฐํŽ˜์ด์Šค โ€” Week 1์— ์‹œ๊ทธ๋‹ˆ์ฒ˜๋งŒ ๋จผ์ € ๋ฐ•์•„๋‘๊ณ  ์–‘์ชฝ์ด ๋™์‹œ ๊ฐœ๋ฐœ (Verdict judgeFull(PatientContext, List<Drug> current, Drug newDrug))
  3. API ์‘๋‹ต ๊ตฌ์กฐ โ€” HTTP 409 BLOCK / 201 ALLOWยทWARN ๊ตฌ์กฐ (HANDOFF_COMMON.md ground truth)
  4. Slice 07 ํ•ธ๋“œ์˜คํ”„ โ€” WeatherEtlService.runDaily() ๊ฒฐ๊ณผ โ†’ WeatherPushService.dispatch() ์ž…๋ ฅ DTO ํ•ฉ์˜
  5. ๋ฐ๋ชจ ์‹œ๋‚˜๋ฆฌ์˜ค e2e โ€” ์•ฝ ์ถ”๊ฐ€ BLOCK โ†’ ์‘๊ธ‰์นด๋“œ โ†’ ํญ์—ผ ์‹œ๋ฎฌ (Week 5 ๊ฐ™์ด ๋Œ๋ ค๋ด„)

์ถ”๊ฐ€ ์ž‘์—… ๋ถ„๋‹ด ๋””ํ…Œ์ผ์€ ยง11 MVP TODO ์ฐธ๊ณ .


1. ๋น ๋ฅธ ์‹œ์ž‘

# 1) ์ธํ”„๋ผ
docker compose up -d           # PostgreSQL 14 + Redis 7

# 2) DB ์Šคํ‚ค๋งˆ (Flyway)
./gradlew flywayMigrate

# 3) ํ™˜๊ฒฝ๋ณ€์ˆ˜ (application-local.yaml ๋˜๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜)
MFDS_API_KEY=...
HIRA_API_KEY=...
KMA_API_KEY=...
GEMINI_API_KEY=...
DATABASE_URL=postgresql://localhost:5432/mom_med
REDIS_URL=redis://localhost:6379
APP_ENCRYPTION_KEY=...           # pgcrypto ์ปฌ๋Ÿผ ์•”ํ˜ธํ™” ๋งˆ์Šคํ„ฐํ‚ค

# 4) ์‹คํ–‰
./gradlew bootRun

ํ•„์ˆ˜ API ํ‚ค 4๊ฐœ: ์‹์•ฝ์ฒ˜ / HIRA / ๊ธฐ์ƒ์ฒญ (์ „๋ถ€ data.go.kr ํ™œ์šฉ์‹ ์ฒญ) / Google Gemini (Google AI Studio).


2. ๊ธฐ์ˆ  ์Šคํƒ (ํ˜„์žฌ ํ™•์ •)

์˜์—ญ ์„ ํƒ ๋น„๊ณ 
JDK Java 21 (toolchain.languageVersion=21) Virtual Threads, Pattern Matching, Records ์ ๊ทน ํ™œ์šฉ ๊ถŒ์žฅ
ํ”„๋ ˆ์ž„์›Œํฌ Spring Boot 4.0.6 (Spring Framework 7 ๊ธฐ๋ฐ˜) Boot 4์—์„œ spring-boot-starter-web โ†’ **spring-boot-starter-webmvc**๋กœ ๋ถ„๋ฆฌ๋จ (์ด๋ฏธ build.gradle ๋ฐ˜์˜)
๋นŒ๋“œ Gradle build.gradle
DB PostgreSQL 14+ 4-schema ๋ถ„๋ฆฌ, pg_trgmยทpgcryptoยทbtree_ginยทpg_stat_statements ํ™•์žฅ
์บ์‹œยท๋ฝ Redis 7+ LLM ์บ์‹œ / Slice 07 ๋ถ„์‚ฐ ๋ฝ 2๋‹จ
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ Flyway src/main/resources/db/migration/V###__*.sql
DTOยท๋งคํ•‘ Lombok + MapStruct (or Java 21 records) records ๊ถŒ์žฅ (response DTO)
API ๋ฌธ์„œ springdoc-openapi 3.0.2 ์ด๋ฏธ ์ถ”๊ฐ€๋จ, /swagger-ui.html
LLM Gemini 2.5 Flash Lite utils/llm_client Spring WebClient ๋ž˜ํผ
๊ฒ€์ฆ spring-boot-starter-validation ์ด๋ฏธ ์ถ”๊ฐ€๋จ
HTTP ํด๋ผ์ด์–ธํŠธ RestClient (Spring 6.1+) ๋˜๋Š” WebClient Boot 4์—์„œ RestClient๊ฐ€ ๊ถŒ์žฅ

Spring Boot 4.x ํŠน์ด์‚ฌํ•ญ (Boot 3 โ†’ 4 ์ฐจ์ด)

  • โœ… spring-boot-starter-web โ†’ spring-boot-starter-webmvc (build.gradle ์ด๋ฏธ ์ ์šฉ)
  • โœ… ํ…Œ์ŠคํŠธ๋„ spring-boot-starter-webmvc-test ์‚ฌ์šฉ (build.gradle ์ด๋ฏธ ์ ์šฉ)
  • Spring Framework 7 ๊ธฐ๋ฐ˜ โ€” JEP 442 ๋“ฑ ์ตœ์‹  Java ๊ธฐ๋Šฅ ํ˜ธํ™˜
  • AOT ์ปดํŒŒ์ผ ๊ธฐ๋ณธ ๊ฐ•ํ™” (๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฏธ์ง€ ์นœํ™”)
  • Jakarta EE 11 / Servlet 6.1
  • Java 17 ์ด์ƒ ํ•„์ˆ˜ (์šฐ๋ฆฌ๋Š” Java 21)

์ถ”๊ฐ€ ์˜์กด์„ฑ (ํ˜„์žฌ build.gradle์— ์—†๋Š” ๊ฒƒ, Week 1์—์„œ ์ถ”๊ฐ€ ํ•„์š”)

dependencies {
    // ๋ฐ์ดํ„ฐ
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-database-postgresql'

    // ๋ถ„์‚ฐ ๋ฝ (Slice 07)
    implementation 'org.redisson:redisson-spring-boot-starter:3.34.1'

    // Rate Limit (Slice 08 โ€” /em/{token})
    implementation 'com.bucket4j:bucket4j-core:8.10.1'

    // ์™ธ๋ถ€ API ํ˜ธ์ถœ (RestClient๋Š” starter-webmvc ํฌํ•จ, ์ถ”๊ฐ€ ์„ค์ •๋งŒ)
    // ๋˜๋Š” WebFlux ํด๋ผ์ด์–ธํŠธ๋งŒ:
    // implementation 'org.springframework.boot:spring-boot-starter-webflux'

    // ๋งคํ•‘
    implementation 'org.mapstruct:mapstruct:1.6.3'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'

    // ํ…Œ์ŠคํŠธ
    testImplementation 'org.testcontainers:postgresql:1.20.4'
    testImplementation 'org.testcontainers:junit-jupiter:1.20.4'
}

partial UNIQUE ์ธ๋ฑ์Šค๊ฐ€ PostgreSQL ํŠนํ™” ๊ธฐ๋Šฅ์ด๋ผ H2๋กœ ํ…Œ์ŠคํŠธํ•˜๋ฉด ์•ˆ ๋จ โ†’ Testcontainers PostgreSQL ํ•„์ˆ˜.

Java 21 ํ™œ์šฉ ๊ถŒ์žฅ ํŒจํ„ด

  • records๋กœ ์‘๋‹ต/์š”์ฒญ DTO: public record AddMedicationRequest(String itemSeq, String source, String notes) {}
  • pattern matching switch: Verdict.decision์— ๋”ฐ๋ผ ์‘๋‹ต ๋ถ„๊ธฐ
  • virtual threads: Slice 07 ETL์—์„œ ๊ฒฉ์ž๋ณ„ KMA ํ˜ธ์ถœ ๋ณ‘๋ ฌ โ€” Executors.newVirtualThreadPerTaskExecutor()
  • sealed interface: Evidence ๊ณ„์ธต (DurEvidence / NbEvidence / AllergyEvidence)

3. ๋ฌธ์„œ ์ง€๋„ (์–ด๋””์„œ ๋ฌด์—‡์„ ์ฐพ์„์ง€)

๋ณด๊ณ  ์‹ถ์€ ๊ฒƒ ํŒŒ์ผ
์ „์ฒด ๋น„์ „, ์‚ฌ์šฉ์ž ์Šคํ† ๋ฆฌ, 4-schema ์ฒญ์‚ฌ์ง„ PRD_์—„๋งˆ์•ฝ_v6.md
๊ฐœ๋ฐœ ์Šฌ๋ผ์ด์Šค๋ณ„ ์ƒ์„ธ ๋ช…์„ธ (DDL ยท API ยท ์˜์‚ฌ์ฝ”๋“œ) issues_v2/00 ~ 08-*.md
์Šฌ๋ผ์ด์Šค ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ยท ๊ถŒ์žฅ ์ž‘์—… ์ˆœ์„œ issues_v2/README.md
ํ”„๋ก ํŠธ์™€์˜ ์‘๋‹ต๊ตฌ์กฐ ํ•ฉ์˜ (HTTP 409 BLOCK ๋“ฑ) HANDOFF_COMMON.md
ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์‘๋‹ต JSON Zod ์Šคํ‚ค๋งˆ api-types/*.ts
์‹œ๊ฐํ™” ๋‹ค์ด์–ด๊ทธ๋žจ (๋ธŒ๋ผ์šฐ์ €๋กœ ์—ด์–ด๋ณด๊ธฐ) ์—„๋งˆ์•ฝ_API_ํ๋ฆ„๋„_v8.html
DUR/์‹์•ฝ์ฒ˜ OpenAPI ํ˜ธ์ถœ ๊ฐ€์ด๋“œ data/OpenAPIํ™œ์šฉ๊ฐ€์ด๋“œ_*.pdf
๊ธฐ์ƒ์ฒญ ๋‹จ๊ธฐ์˜ˆ๋ณด API ๊ฐ€์ด๋“œ + ๊ฒฉ์ž์ขŒํ‘œ xlsx ๊ธฐ์ƒ์ฒญ41_๋‹จ๊ธฐ์˜ˆ๋ณด_*/
๋‚ ์”จ ๋ฃฐ์…‹ 32๊ฑด + ํ™˜๊ฐ ๊ฒ€์ฆ ground truth seed/
LLM NB ์ถ”์ถœ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ fixture nb_amlodipine.*, nb_warfarin.*

์ถฉ๋Œ ์‹œ ground truth ์šฐ์„ ์ˆœ์œ„: API ์‘๋‹ต ๊ตฌ์กฐ = api-types/*.ts โ†’ DB DDL = issues_v2/*.md โ†’ ๊ธฐ๋Šฅ ๋ฒ”์œ„ = PRD_์—„๋งˆ์•ฝ_v6.md โ†’ ์–‘ํŒ€ ํ•ฉ์˜ = HANDOFF_COMMON.md


4. ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ • (PRD v6์—์„œ ์ž ๊ธˆ)

4.1 DB๋Š” PostgreSQL 14+ โ€” 4-schema ๋ถ„๋ฆฌ

mom_med database
โ”œโ”€โ”€ ref      ์™ธ๋ถ€ ๋งˆ์Šคํ„ฐ (์‹์•ฝ์ฒ˜ยทHIRAยท๊ธฐ์ƒ์ฒญ ์ ์žฌ)         โ† SELECT๋งŒ, ๋ฐฑ์—… ์ ๊ฒŒ
โ”œโ”€โ”€ app      ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ (PII, RLS, PITR ๋ฐฑ์—… ํ•„์ˆ˜)     โ† ์šด์˜์˜ ํ•ต์‹ฌ
โ”œโ”€โ”€ derived  LLM ๊ฐ€๊ณต ์บ์‹œ (์žฌํ˜ธ์ถœ๋กœ ๋ณต๊ตฌ ๊ฐ€๋Šฅ)
โ””โ”€โ”€ logs     ์‹œ๊ณ„์—ด (์›”๋ณ„ RANGE ํŒŒํ‹ฐ์…˜, append-only)

ํ•„์ˆ˜ PostgreSQL ํ™•์žฅ: pg_trgm, pgcrypto, btree_gin, pg_stat_statements

4.2 ๋ณด์•ˆ

  • RLS (Row-Level Security): ๋งค ์š”์ฒญ๋งˆ๋‹ค SET LOCAL app.current_parent_id = '...' ์ฃผ์ž…
  • ์ปฌ๋Ÿผ ์•”ํ˜ธํ™”: device_tokens.token_encrypted = pgp_sym_encrypt(token, :key)
  • ๊ณต๊ฐœ ์—”๋“œํฌ์ธํŠธ GET /em/{token}: ์ธ์ฆ ์—†์Œ โ€” ๋ฐ˜๋“œ์‹œ Rate Limit + 90์ผ ๋งŒ๋ฃŒ + revoke ์ง€์›

4.3 ์‘๋‹ต ๊ตฌ์กฐ ground truth (โ˜… ํ”„๋ก ํŠธ์™€ ํ•ฉ์˜, ๋ณ€๊ฒฝ ๊ธˆ์ง€)

์•ฝ ์ถ”๊ฐ€ ์‹œ BLOCK (HTTP 409):

{
  "error": "block",
  "verdict": {
    "decision": "BLOCK",
    "evidences": [
      { "source":"DUR", "severity":"high", "message":"...", "citation":"...", "citation_source":"..." }
    ]
  }
}

์•ฝ ์ถ”๊ฐ€ ์‹œ ALLOW / WARN (HTTP 201):

{
  "medication_id": "uuid",
  "item_seq": "200610660",
  "safety_check": { "decision":"ALLOW"|"WARN", "evidences": [...] }
}

์‘๊ธ‰์นด๋“œ QR URL: https://app.์—„๋งˆ์•ฝ.com/em/{token} (โ† /c/{token} ์•„๋‹˜)

4.4 enum ์ •๋ฆฌ (ํ˜ผ๋™ ์ฃผ์˜)

๋„๋ฉ”์ธ ๊ฐ’ ์‚ฌ์šฉ์ฒ˜
์•ฝ๋ฌผ ์•ˆ์ „ํŒ์ • high / medium / low (์˜๋ฌธ) Slice 02ยท03ยท04
๋‚ ์”จ ์•Œ๋ฆผ ๊ด€์‹ฌ / ์ฃผ์˜ / ๊ฒฝ๊ณ  / ์œ„ํ—˜ (ํ•œ๊ธ€) Slice 06ยท07
์•Œ๋ ˆ๋ฅด๊ธฐ severe / moderate / mild Slice 04
์•ˆ์ „ ํŒ์ • ๊ฒฐ๊ณผ BLOCK / WARN / INFO / ALLOW Slice 04 ์‘๋‹ต
ํ‘ธ์‹œ ์†ก์‹  ์ƒํƒœ sent / failed / simulated / skipped_fatigue / skipped_review Slice 07

4.5 ํ•ญ์‘๊ณ ์ œ ํŒ๋ณ„ (์–‘ํŒ€ ๋™์ผ ๊ทœ์•ฝ)

public static boolean isAnticoagulant(String atcCode) {
    return atcCode != null && atcCode.startsWith("B01A");
}

5. ์Šฌ๋ผ์ด์Šค ๋นŒ๋“œ ์ˆœ์„œ (โ˜… ์ž‘์—… ํ๋ฆ„์˜ ํ•ต์‹ฌ)

00 ์ธํ”„๋ผ (4-schema, LLM ํด๋ผ์ด์–ธํŠธ, ํ™˜๊ฐ ๊ฒ€์ฆ๊ธฐ)        ๐Ÿค ๊ณต๋™
 โ†“
01 ์•ฝ ๋งˆ์Šคํ„ฐ + ์•Œ์•ฝ ์ด๋ฏธ์ง€ (์‹์•ฝ์ฒ˜ 15095677ยท15057639)      A ๋™ํ™˜
 โ†“
02 DUR ๋ณ‘์šฉ๊ธˆ๊ธฐ (HIRA 11983, 837kํ–‰, ์ •ํ™•๋งค์นญ โ˜…)         A ๋™ํ™˜
 โ†“
03 NB ์ถ”์ถœ (LLM 2๊ฒน ์•ˆ์ „๋ง, ํ™˜๊ฐ ๊ฒ€์ฆ)                    A ๋™ํ™˜
 โ†“
04 ๋ถ€๋ชจ ์•ฝ์žฅ + ์•ˆ์ „ํŒ์ • ํ†ตํ•ฉ (โ˜… ๊ฐ€์žฅ ํฐ ์Šฌ๋ผ์ด์Šค)         B ์ˆ˜์ข… + ๐Ÿค ํ†ตํ•ฉ
 โ†“
05 ์งˆ๋ณ‘ยทํ™˜์ž๋ถ„๋ฅ˜ ๊ธˆ๊ธฐ (KCD ๋งคํ•‘)                          B ์ˆ˜์ข…
 โ†“
06 ๋‚ ์”จ ๋ฃฐ์…‹ 32๊ฐœ + ๊ฒฉ์ž์ขŒํ‘œ + ๋ฃฉ์—…                       A ๋™ํ™˜
 โ†“
07 ๋งค์ผ ETL + ์ž์ฒด ํŒ์ • + ํ‘ธ์‹œ (Redis ๋ฝ 2๋‹จ)            ๐Ÿค ๊ณต๋™
 โ†“
08 ์‘๊ธ‰์นด๋“œ + HIRA 11999/12101/12100                     B ์ˆ˜์ข…

6. ์Šฌ๋ผ์ด์Šค ์š”์•ฝ (๊ฐ issues_v2/*.md ์••์ถ•)

๊ฐ ์Šฌ๋ผ์ด์Šค ๋ฌธ์„œ๊ฐ€ ground truth. ์•„๋ž˜๋Š” ์ธ๋ฑ์Šค + ํ•ต์‹ฌ ๋ฐœ์ทŒ.

Slice 00 โ€” 4-schema ์ธํ”„๋ผ + ๊ณต์œ  ์œ ํ‹ธ ๐Ÿค [โ†’ issues_v2/00-foundation-v2.md]

๋‹ด๋‹น ๋ถ„๋‹ด:

  • ๋™ํ™˜: DrugNameNormalizer (15 ์—ผ ๋ชฉ๋ก), HallucinationVerifier (3๋‹จ๊ณ„ ๋งค์นญ), GeminiClient (RestClient/WebClient)
  • ์ˆ˜์ข…: BaseTimeEntity, GlobalExceptionHandler, ApiResponse<T>/BlockErrorResponse, RedisCacheService, PgCryptoService
  • ๊ณต๋™: Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (V001 extensions / V002 schemas / V003 audit trigger), docker-compose, application.yaml, ํ™˜๊ฒฝ๋ณ€์ˆ˜ 7๊ฐœ, SafetyJudgeService ์ธํ„ฐํŽ˜์ด์Šค ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ฐ•์•„๋‘๊ธฐ

Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 3์ข…:

-- V001__extensions.sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- V002__schemas.sql
CREATE SCHEMA IF NOT EXISTS ref;
CREATE SCHEMA IF NOT EXISTS app;
CREATE SCHEMA IF NOT EXISTS derived;
CREATE SCHEMA IF NOT EXISTS logs;
ALTER DATABASE mom_med SET search_path TO app, ref, derived, logs, public;

-- V003__audit_trigger.sql
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;

์ˆ˜๋ฝ ๊ธฐ์ค€: docker compose up โ†’ PG+Redis ์ •์ƒ, Flyway ํ›„ \dn์— 4 schema, DrugNameNormalizer ๋‹จ์œ„ํ…Œ์ŠคํŠธ 6 ์ผ€์ด์Šค, GeminiClient.call("ping") ์„ฑ๊ณต


Slice 01 โ€” ์•ฝ ์‹๋ณ„ + ์•Œ์•ฝ ์‚ฌ์ง„ [A ๋™ํ™˜] [โ†’ issues_v2/01-drug-identity-pill-visual-v2.md]

์™ธ๋ถ€ API 3๊ฐœ (์‹์•ฝ์ฒ˜):

API sno ์šฉ๋„
์˜์•ฝํ’ˆ ์ œํ’ˆ ํ—ˆ๊ฐ€์ •๋ณด [๋ชฉ๋ก] 15095677 item_name โ†’ ITEM_SEQ ๋งค์นญ
์˜์•ฝํ’ˆ ์ œํ’ˆ ํ—ˆ๊ฐ€์ •๋ณด [์ƒ์„ธ] 15095677 ITEM_SEQ โ†’ ์„ฑ๋ถ„/ATC/NB_DOC_DATA
์˜์•ฝํ’ˆ ๋‚ฑ์•Œ์‹๋ณ„ ์ •๋ณด 15057639 ITEM_NAME โ†’ ์•Œ์•ฝ ์‚ฌ์ง„ยท๊ฐ์ธยท์ƒ‰ยท๋ชจ์–‘

ํ…Œ์ด๋ธ”: ref.drugs_master (PK item_seq), ref.pill_visuals (PK item_seq FK)

ํ•ต์‹ฌ ์ปฌ๋Ÿผ: main_ingr_norm โ† DrugNameNormalizer(main_ingr_en) โ€” Slice 02 DUR ์กฐ์ธ ํ‚ค

API: GET /v1/drugs/identify?name={user_input} โ†’ 200/300(๋™๋ช…์ดํ’ˆ)/404

์ˆ˜๋ฝ ๊ธฐ์ค€: ํƒ€์ด๋ ˆ๋†€500 โ†’ item_seq=202106092, main_ingr_norm="acetaminophen", 2ํšŒ ํ˜ธ์ถœ ์‹œ ์™ธ๋ถ€ API 0ํšŒ


Slice 02 โ€” DUR 1์ฐจ ๋ณ‘์šฉ๊ธˆ๊ธฐ (์ •ํ™•๋งค์นญ) [A ๋™ํ™˜] [โ†’ issues_v2/02-dur-coadministration-check-v2.md]

์™ธ๋ถ€ API 0๊ฑด. CSV 5๊ฐœ ์ ์žฌ:

ํ…Œ์ด๋ธ” ํ–‰์ˆ˜
ref.dur_combo_contraindications 837,837 (231MB, cp949)
ref.dur_elderly_caution 572
ref.dur_elderly_nsaid_caution 1,083
ref.dur_age_contraindication 2,905 (Slice 05์—์„œ ์ ์žฌ)
ref.dur_pregnancy_contraindication 19,015 (Slice 05์—์„œ ์ ์žฌ)

โš  ํ•ต์‹ฌ: LIKE '%X%' ์ ˆ๋Œ€ ๊ธˆ์ง€. ingredient_norm = ? ์ •ํ™• ๋งค์นญ + idx_dur_combo_pair(a_norm, b_norm) ์ธ๋ฑ์Šค

SafetyJudge MVP (๋™ํ™˜์ด ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„):

  • ๋ณ‘์šฉ๊ธˆ๊ธฐ ๋งค์นญ โ†’ BLOCK / 65์„ธโ†‘ + ๋…ธ์ธ์ฃผ์˜ โ†’ WARN / ๊ทธ ์™ธ โ†’ ALLOW
  • Dedupe: (์„ฑ๋ถ„A, ์„ฑ๋ถ„B) ์ง ๊ธฐ์ค€

์ˆ˜๋ฝ ๊ธฐ์ค€: amlodipine+itraconazole โ†’ BLOCK, amlodipine+simvastatin โ†’ ALLOW (Slice 03์—์„œ ํšŒ์ˆ˜), ์‘๋‹ต < 50ms, EXPLAIN ์ธ๋ฑ์Šค hit


Slice 03 โ€” NB AI ์ถ”์ถœ + 2๊ฒน ์•ˆ์ „๋ง [A ๋™ํ™˜] [โ†’ issues_v2/03-nb-extraction-2layer-safety-v2.md]

์™ธ๋ถ€ API: Gemini 2.5 Flash Lite (RestClient/WebClient)

ํ…Œ์ด๋ธ”: derived.nb_extractions, derived.nb_interactions (โ˜… entry_type CHECK ('drug_drug' | 'patient_class') ์ฒ˜์Œ๋ถ€ํ„ฐ ์ •์˜)

risk_level enum ๋งคํ•‘ โ†’ ํ†ตํ•ฉ ํŒ์ •:

๋ณธ๋ฌธ ํ‘œํ˜„ enum ํŒ์ •
"ํˆฌ์—ฌํ•˜์ง€ ๋ง ๊ฒƒ", "๊ธˆ๊ธฐ" ๋™์‹œํˆฌ์—ฌํ”ผํ•ด์•ผํ•จ BLOCK
"๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค" ๊ถŒ์žฅํ•˜์ง€์•Š์Œ WARN
"์ฃผ์˜๊นŠ๊ฒŒ ๊ด€์ฐฐ" ์ฃผ์˜ INFO
"์˜ํ–ฅ์ด ์—†์—ˆ๋‹ค" ์ •๋ณด๋งŒ (UI ๋…ธ์ถœ ์•ˆ ํ•จ)

์•ฝ๋ฌผ๊ตฐ โ†’ ATC prefix ๋งคํ•‘ (NB๊ฐ€ "CYP3A4 ์ €ํ•ด์ œ" ๊ฐ™์€ ๊ตฐ๋ช… ๋ฐ˜ํ™˜ ์‹œ):

Map.of("CYP3A4 ์ €ํ•ด์ œ", List.of("J02AC","J01FA09"),
       "๋น„์Šคํ…Œ๋กœ์ด๋“œ์„ฑ ์†Œ์—ผ์ œ", List.of("M01A"),
       "ํ•ญ์‘๊ณ ์ œ", List.of("B01A"))

HallucinationVerifier: 3๋‹จ๊ณ„ (exact โ†’ normalized โ†’ fuzzy 40์ž). ์‹คํŒจ ์‹œ 1ํšŒ ์žฌ์‹œ๋„, ๊ทธ๋ž˜๋„ ์‹คํŒจํ•˜๋ฉด verified=false, ์‚ฌ์šฉ์ž ๋…ธ์ถœ ๊ธˆ์ง€.

SafetyJudgeService.judgeFull() ์™„์„ฑ (โ˜… ์ˆ˜์ข…์˜ 04์™€ ํ†ตํ•ฉ ์ง€์ ):

  1. DUR (Slice 02) + 2. ์ƒˆ ์•ฝ NB โ†” ๊ธฐ์กด ์•ฝ + 3. ๊ธฐ์กด ์•ฝ NB โ†” ์ƒˆ ์•ฝ

์ˆ˜๋ฝ ๊ธฐ์ค€: ๋…ธ๋ฐ”์Šคํฌ 39 entry / ์›ŒํŒŒ๋ฆฐ 46 entry / ํ™˜๊ฐ 0๊ฑด, ๋…ธ๋ฐ”์Šคํฌ+์‹ฌ๋ฐ”์Šคํƒ€ํ‹ด โ†’ NB๋กœ WARN ํšŒ์ˆ˜


Slice 04 โ€” ๋ถ€๋ชจ + ์•ฝ์žฅ + ์•Œ๋ ˆ๋ฅด๊ธฐ + ๋””๋ฐ”์ด์Šค ํ† ํฐ โ˜… [B ์ˆ˜์ข… + ๐Ÿค ํ†ตํ•ฉ] [โ†’ issues_v2/04-parent-medication-management-v2.md]

๊ฐ€์žฅ ํฐ ์Šฌ๋ผ์ด์Šค. ๊ณต๋ชจ์ „ ๋ฐ๋ชจ์˜ ํ•ต์‹ฌ.

ํ…Œ์ด๋ธ” 4์ข… (app.*):

ํ…Œ์ด๋ธ” PK ํ•ต์‹ฌ
app.patient_profiles parent_id UUID birthdate, sex, nx/ny, is_pregnant
app.patient_medications id BIGSERIAL soft delete, partial UNIQUE(parent_id, item_seq) WHERE deleted_at IS NULL
app.patient_allergies id BIGSERIAL allergen_norm (์•ฝ๋ฌผ๋งŒ ์ •๊ทœํ™”)
app.device_tokens id BIGSERIAL token_encrypted BYTEA (pgp_sym_encrypt)

logs.safety_check_log (์›”๋ณ„ ํŒŒํ‹ฐ์…˜) โ€” ๋ชจ๋“  ์•ฝ ์ถ”๊ฐ€ ์‹œ๋„ ๊ธฐ๋ก (BLOCK๋„ ํฌํ•จ)

๐Ÿค ํ†ตํ•ฉ ์ง€์  (Week 3) โ€” ์ˆ˜์ข… โ†” ๋™ํ™˜:

  • ์ˆ˜์ข…์ด MedicationController.addMedication()์„ ๋งŒ๋“ฆ
  • ๋™ํ™˜์ด ๋งŒ๋“  SafetyJudgeService.judgeFull(parentCtx, currentDrugs, newDrug)์„ ํ˜ธ์ถœ
  • Week 1์—” ์ˆ˜์ข…์ด mock ๊ตฌํ˜„์ฒด๋กœ ์šฐ์„  ๋™์ž‘ (return Verdict.ALLOW;)
  • Week 3์— ์‹ค์ œ ๊ตฌํ˜„์ฒด swap โ†’ e2e BLOCK ์‘๋‹ต ๊ฒ€์ฆ

ํ•ต์‹ฌ API โ˜…:

POST /v1/parents/{parentId}/medications
  ๋‚ด๋ถ€:
    1. ref.drugs_master์—์„œ item_seq ์กฐํšŒ (์—†์œผ๋ฉด Slice 01 ํ˜ธ์ถœ)
    2. SafetyJudgeService.judgeFull(parentCtx, currentDrugs, newDrug)   โ† ๋™ํ™˜ ์ฝ”๋“œ
    3. logs.safety_check_log INSERT (BLOCK๋„ ๊ธฐ๋ก)                      โ† @Transactional(REQUIRES_NEW)
    4. BLOCK โ†’ SafetyBlockException ๋ฐœ์ƒ โ†’ HTTP 409
       WARN/ALLOW โ†’ INSERT + HTTP 201

ํŠธ๋žœ์žญ์…˜ ์ฃผ์˜: log ๊ธฐ๋ก์€ @Transactional(propagation=REQUIRES_NEW) ๋˜๋Š” ApplicationEvent๋กœ ๋ถ„๋ฆฌํ•ด BLOCK rollback ์‹œ์—๋„ log๋Š” ๋‚จ๊ธฐ๊ธฐ.

์ˆ˜๋ฝ ๊ธฐ์ค€: HTTP 409 BLOCK ์‘๋‹ต, partial UNIQUE ์œ„๋ฐ˜, ํ† ํฐ ํ‰๋ฌธ DB ๋ฏธ์ €์žฅ (๋‹จ์œ„ํ…Œ์ŠคํŠธ), consent_data_share=false โ†’ 403


Slice 05 โ€” ๊ธฐ์ €์งˆํ™˜ + ํ™˜์ž๋ถ„๋ฅ˜ ๊ธˆ๊ธฐ [B ์ˆ˜์ข…] [โ†’ issues_v2/05-disease-patient-class-contraindication-v2.md]

์™ธ๋ถ€ API: HIRA 12904 (์งˆ๋ณ‘์ •๋ณด์„œ๋น„์Šค) โ€” XML ์‘๋‹ต. KCD ๊ฒ€์ฆ์šฉ.

ํ…Œ์ด๋ธ”:

  • ref.disease_master (PK sick_cd) โ€” 47,798ํ–‰ (HIRA 11984 CSV)
  • app.patient_conditions โ€” ์ •๊ทœํ™” (JSONB ์˜ต์…˜ ๊ฑฐ๋ถ€), partial UNIQUE(parent_id, disease_code) WHERE deleted_at IS NULL

KCD ๋งคํ•‘ ์‚ฌ์ „ (LLM ํ”„๋กฌํ”„ํŠธ ํ™•์žฅ์œผ๋กœ patient_class entry ์ถ”์ถœ):

NB ๋ณธ๋ฌธ ํ‘œํ˜„ KCD
"๊ฐ„์žฅ์•  ํ™˜์ž" K70-K77
"์‹ ์žฅ(์ฝฉํŒฅ)์žฅ์•  ํ™˜์ž" N17-N19
"์‹ฌ์žฅ๊ธฐ๋Šฅ์ €ํ•˜" I50, I20-I25
"์†Œํ™”์„ฑ๊ถค์–‘" K25-K27
"ํ˜ˆ์•ก ์ด์ƒ" D60-D64, D70-D77
"๋‹น๋‡จ ํ™˜์ž" E10-E14

๐Ÿค ํ•ฉ์˜ ํ•„์š”: ๋™ํ™˜์˜ Slice 03 LLM ํ”„๋กฌํ”„ํŠธ์— entry_type='patient_class' ์ถ”์ถœ ํ•ญ๋ชฉ ์ถ”๊ฐ€ (์ˆ˜์ข…์ด ํ”„๋กฌํ”„ํŠธ PR ํ•จ๊ป˜ ๊ฒ€ํ† )

SafetyJudge ํ™•์žฅ (3์ฐจ ์•ˆ์ „๋ง):

  • checkPatientClassContraindication / checkAgeContraindication / checkPregnancyContraindication / checkAllergy

์ˆ˜๋ฝ ๊ธฐ์ค€: ๋ถ€๋ชจ K25 + NSAID โ†’ WARN, ๋ถ€๋ชจ ํŽ˜๋‹ˆ์‹ค๋ฆฐ ์•Œ๋ ˆ๋ฅด๊ธฐ + amoxicillin โ†’ BLOCK, ๋ถ€๋ชจ ์ž„์‹  + ์ž„๋ถ€๊ธˆ๊ธฐ โ†’ BLOCK


Slice 06 โ€” ๋‚ ์”จ ๋ฃฐ์…‹ + ๊ฒฉ์ž ์ขŒํ‘œ + ๋ฃฉ์—… [A ๋™ํ™˜] [โ†’ issues_v2/06-weather-ruleset-lookup-v2.md]

์™ธ๋ถ€ API 0๊ฑด. JSON/xlsx ์‹œ๋“œ ์ ์žฌ.

ํ…Œ์ด๋ธ”:

  • ref.weather_rules โ€” 32ํ–‰ (seed/weather_rules_v0.2.json), severity CHECK IN ('๊ด€์‹ฌ','์ฃผ์˜','๊ฒฝ๊ณ ','์œ„ํ—˜')
  • ref.region_grid โ€” 26K+ (xlsx)

updateParentGrid(parentId) โ€” 1์ˆœ์œ„ ์‹œยท๊ตฌยท๋™ ์ •ํ™•๋งค์นญ, 2์ˆœ์œ„ ์‹œยท๊ตฌ fallback (๋„๋†๋ณตํ•ฉ์ง€์—ญ ๊ฒฉ์ž ์ •ํ™•๋„)

WeatherAdvisory DTO: rule_id ํ•„์ˆ˜ ํฌํ•จ (Slice 07 dedup์šฉ โ€” v6 ๋ธ”๋กœ์ปค 9๋ฒˆ)

๐Ÿค ํ•ธ๋“œ์˜คํ”„: lookupRules(diseaseCodes, alerts) ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ Week 4์— ๋ฏธ๋ฆฌ ํ•ฉ์˜ โ†’ Week 5์— ์ˆ˜์ข…์ด ํ˜ธ์ถœ

์ˆ˜๋ฝ ๊ธฐ์ค€: ๋Œ€๊ตฌ ์ค‘๊ตฌ โ†’ nx=89,ny=90, lookupRules(["I10","E11"], ["ํญ์—ผ๊ฒฝ๋ณด"]) โ†’ 2 advisory + rule_id


Slice 07 โ€” ๊ธฐ์ƒ์ฒญ ETL + ๋งค์ผ ํ‘ธ์‹œ ๐Ÿค [โ†’ issues_v2/07-weather-etl-daily-push-v2.md]

์ด ์Šฌ๋ผ์ด์Šค๋Š” AยทB ๋‘˜ ๋‹ค ์†๋Œ€๋Š” ํ†ตํ•ฉ ์Šฌ๋ผ์ด์Šค.

A ๋™ํ™˜ ๋‹ด๋‹น (๋ฐ์ดํ„ฐ ๋ผ์ธ):

  • KmaForecastClient โ€” getVilageFcst ํ˜ธ์ถœ
  • TMX/TMN ์ถ”์ถœ + ์ž์ฒด ์ž„๊ณ„๊ฐ’ ํŒ์ • (TMXโ‰ฅ35 โ†’ ํญ์—ผ๊ฒฝ๋ณด ๋“ฑ)
  • logs.weather_observations_daily ์ ์žฌ
  • @Scheduled(cron="0 30 5 * * *", zone="Asia/Seoul") ์žก โ€” Java 21 Virtual Threads๋กœ ๊ฒฉ์ž๋ณ„ ๋ณ‘๋ ฌ ํ˜ธ์ถœ
  • ์žก ๋ฝ: Redisson RLock (weather_etl:job:{date}, TTL 30๋ถ„)

B ์ˆ˜์ข… ๋‹ด๋‹น (ํ‘ธ์‹œ ๋ผ์ธ):

  • ํ™œ์„ฑ ๋ถ€๋ชจ ์กฐํšŒ (consent_data_share=true, nx/ny ์žˆ์Œ)
  • app.patient_conditions JOIN์œผ๋กœ disease_codes
  • lookupRules() ํ˜ธ์ถœ (โ† ๋™ํ™˜์˜ Slice 06)
  • ์šฐ์„ ์ˆœ์œ„ ์ •์ฑ… (severity ์œ„ํ—˜ ์šฐ์„  1ํ‘ธ์‹œ/์ผ)
  • ์•Œ๋žŒ ํ”ผ๋กœ ๋ฐฉ์ง€ + requires_review skip
  • device_tokens ๋ณตํ˜ธํ™” (pgp_sym_decrypt) โ†’ ํ‘ธ์‹œ ๋ฐœ์†ก (mock)
  • logs.advisory_push_log ์ ์žฌ + delivery_status 5์ข… ๋ถ„๊ธฐ
  • ๋ถ€๋ชจ ๋ฝ: Redisson RLock (weather_etl:parent:{pid}:{date}, TTL 5๋ถ„)

๐Ÿค ํ•ฉ์˜ ํฌ์ธํŠธ:

  • WeatherEtlService.runDaily() ๊ฒฐ๊ณผ DTO (๊ด€์ธก + alerts ๋ฆฌ์ŠคํŠธ)
  • WeatherPushService.dispatch(observation, parents) ์‹œ๊ทธ๋‹ˆ์ฒ˜
  • Redisson ์„ค์ •ยทํ‚ค ๋„ค์ž„์ŠคํŽ˜์ด์Šค
  • ํ†ตํ•ฉ e2e: TMX=36 mock โ†’ ํญ์—ผ๊ฒฝ๋ณด โ†’ I10 ๋ถ€๋ชจ์— ํ‘ธ์‹œ 1๊ฑด

์ž์ฒด ์ž„๊ณ„๊ฐ’:

์กฐ๊ฑด derived_alerts
TMX โ‰ฅ 35โ„ƒ ํญ์—ผ๊ฒฝ๋ณด
33 โ‰ค TMX < 35 ํญ์—ผ์ฃผ์˜๋ณด
TMN โ‰ค -15โ„ƒ ํ•œํŒŒ๊ฒฝ๋ณด
-15 < TMN โ‰ค -12 ํ•œํŒŒ์ฃผ์˜๋ณด

์ˆ˜๋ฝ ๊ธฐ์ค€: TMX=36 mock โ†’ ํญ์—ผ๊ฒฝ๋ณด+ํ‘ธ์‹œ 1๊ฑด, ๊ฐ™์€ (parent,rule,date) 2๋ฒˆ์งธ โ†’ skipped_fatigue, I10(์œ„ํ—˜)+E11(์ฃผ์˜) โ†’ I10 1ํ‘ธ์‹œ๋งŒ, ETL 2๋ฒˆ ํŠธ๋ฆฌ๊ฑฐ โ†’ skipped_duplicate_run


Slice 08 โ€” ์‘๊ธ‰์นด๋“œ [B ์ˆ˜์ข…] [โ†’ issues_v2/08-emergency-card-v2.md]

์™ธ๋ถ€ API 3๊ฐœ (HIRA):

API sno ์šฉ๋„
๋ณ‘์›์ •๋ณด์„œ๋น„์Šค 11999 yadmNm/ykiho
์˜๋ฃŒ๊ธฐ๊ด€๋ณ„์ƒ์„ธ์ •๋ณด 12101 ์‘๊ธ‰์‹ค ์šด์˜
์•ฝ๊ตญ์ •๋ณด์„œ๋น„์Šค 12100 ๋‹จ๊ณจ ์•ฝ๊ตญ

ํ…Œ์ด๋ธ”: app.parent_hospitals, app.hospital_emergency_info, app.parent_pharmacies, app.emergency_cards (PK parent_id, UNIQUE qr_token)

snapshot ์šฐ์„ ์ˆœ์œ„:

  1. ์•Œ๋ ˆ๋ฅด๊ธฐ (severity DESC) โ˜… TOP
  2. ํ•ญ์‘๊ณ ์ œ atc_code LIKE 'B01A%' โ˜… 2์ˆœ์œ„
  3. ๊ธฐ์ €์งˆํ™˜ (Slice 05 JOIN)
  4. ์ „์ฒด ์•ฝ์žฅ (ํ•ญ์‘๊ณ ์ œ ์ตœ์ƒ๋‹จ)
  5. ๋‹จ๊ณจ ๋ณ‘์›ยท์•ฝ๊ตญ

QR ํ† ํฐ: Java SecureRandom 48๋ฐ”์ดํŠธ โ†’ Base64URL โ‰ˆ 64์ž

access_count ์›์ž์  ์ฆ๊ฐ€:

UPDATE app.emergency_cards SET access_count = access_count + 1, last_accessed_at = NOW()
WHERE qr_token = :t;

Rate Limit (Bucket4j + Redis):

  • IP๋‹น ๋ถ„๋‹น 10ํšŒ / ํ† ํฐ๋‹น ์‹œ๊ฐ„๋‹น 60ํšŒ / ํ† ํฐ๋‹น ์ผ์ผ 200ํšŒ (์ดˆ๊ณผ ์‹œ ์ž๋™ revoke)

ํ•ต์‹ฌ API:

POST /v1/parents/{parentId}/emergency-card/regenerate    โ†’ 200 { qr_token, qr_url:"/em/{token}", valid_until }
POST /v1/parents/{parentId}/emergency-card/rotate        โ†’ 200 (new token, old revoked)
POST /v1/parents/{parentId}/emergency-card/revoke        โ†’ 204
GET  /em/{token}                                          โ†’ 200 snapshot / 410 Gone / 429 Too Many

์ˆ˜๋ฝ ๊ธฐ์ค€: ํ•ญ์‘๊ณ ์ œ ์ •๋ ฌ, access_count ์›์ž ์ฆ๊ฐ€ (๋™์‹œ์„ฑ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ), ๋งŒ๋ฃŒ ํ›„ 410, revoke ํ›„ ์ฆ‰์‹œ 410, Rate limit โ†’ 429 + ์ž๋™ revoke


7. Spring Boot ํŒจํ‚ค์ง€ ๊ตฌ์กฐ (๋„๋ฉ”์ธ ์ค‘์‹ฌ)

mamokey.mom_med.backend
โ”œโ”€โ”€ BackendApplication.java
โ”‚
โ”œโ”€โ”€ global/                      ๐Ÿค ๊ณต๋™
โ”‚   โ”œโ”€โ”€ config/          SecurityConfig, JpaConfig, RedisConfig, OpenApiConfig, RestClientConfig
โ”‚   โ”œโ”€โ”€ exception/       GlobalExceptionHandler, SafetyBlockException, ApiException
โ”‚   โ”œโ”€โ”€ response/        ApiResponse, ErrorResponse, BlockErrorResponse  (record)
โ”‚   โ”œโ”€โ”€ util/            DrugNameNormalizer[A], HallucinationVerifier[A], KcdRangeExpander[B]
โ”‚   โ”œโ”€โ”€ audit/           BaseTimeEntity
โ”‚   โ””โ”€โ”€ ratelimit/       Bucket4j ์„ค์ • (Slice 08)
โ”‚
โ”œโ”€โ”€ infra/
โ”‚   โ”œโ”€โ”€ cache/           RedisCacheService                              [B ๋˜๋Š” ๊ณต๋™]
โ”‚   โ”œโ”€โ”€ crypto/          PgCryptoService (pgp_sym_encrypt/decrypt)      [B]
โ”‚   โ”œโ”€โ”€ llm/             GeminiClient (RestClient ๋˜๋Š” WebClient)        [A]
โ”‚   โ”œโ”€โ”€ lock/            RedissonLockService (Slice 07)                  [๊ณต๋™]
โ”‚   โ””โ”€โ”€ scheduler/       WeatherEtlScheduler (@Scheduled)                [A ํŠธ๋ฆฌ๊ฑฐ]
โ”‚
โ”œโ”€โ”€ external/
โ”‚   โ”œโ”€โ”€ mfds/            MfdsClient (์‹์•ฝ์ฒ˜ 3 API)                       [A]
โ”‚   โ”œโ”€โ”€ hira/            HiraDurClient ยท HiraHospitalClient ยท HiraPharmacyClient ยท HiraDiseaseClient   [HiraDur=A / HospitalยทPharmacyยทDisease=B]
โ”‚   โ””โ”€โ”€ kma/             KmaForecastClient                               [A]
โ”‚
โ””โ”€โ”€ domain/
    โ”œโ”€โ”€ drug/            Slice 01 โ€” Drug, PillVisual, DrugController     [A]
    โ”œโ”€โ”€ dur/             Slice 02 โ€” DurCombo*, DurEngineService          [A]
    โ”œโ”€โ”€ nb/              Slice 03 โ€” NbExtraction, NbInteraction          [A]
    โ”œโ”€โ”€ safety/          Slice 02+03 โ€” SafetyJudgeService, Verdict, Evidence (sealed)   [A ๊ตฌํ˜„ / B ํ˜ธ์ถœ]
    โ”œโ”€โ”€ parent/          Slice 04 โ€” PatientProfile, ParentController     [B]
    โ”œโ”€โ”€ medication/      Slice 04 โ€” PatientMedication, MedicationController, SafetyBlockException   [B]
    โ”œโ”€โ”€ allergy/         Slice 04 โ€” PatientAllergy                       [B]
    โ”œโ”€โ”€ device/          Slice 04 โ€” DeviceToken (pgcrypto)               [B]
    โ”œโ”€โ”€ condition/       Slice 05 โ€” PatientCondition, DiseaseMaster      [B]
    โ”œโ”€โ”€ weather/         Slice 06+07 โ€” WeatherRule, RegionGrid, WeatherAdvisorService [A] / WeatherEtlService [A] / WeatherPushService [B]
    โ”œโ”€โ”€ emergency/       Slice 08 โ€” EmergencyCard, ParentHospital, ParentPharmacy, PublicEmController   [B]
    โ””โ”€โ”€ log/             SafetyCheckLog, AdvisoryPushLog, ApiCallLog (logs.*)  [๊ฐ์ž ์ž๊ธฐ ์Šฌ๋ผ์ด์Šค ์ฑ…์ž„]

8. ์ „์ฒด API ๋ชฉ๋ก (์ธ์ฆ โœ… = ์ž๋…€ ์„ธ์…˜ ํ•„์š”)

Method URL ์Šฌ๋ผ์ด์Šค ๋‹ด๋‹น ์ธ์ฆ
GET /v1/drugs/identify?name= 01 A โœ…
GET /v1/drugs/{itemSeq}/contraindications 03 A โœ…
POST /v1/parents 04 B โœ…
GET / PATCH / DELETE /v1/parents/{id} 04 B โœ…
GET /v1/parents/{id}/medications 04 B โœ…
POST /v1/parents/{id}/medications โ˜… 04 B (+๐Ÿค SafetyJudge) โœ…
DELETE /v1/parents/{id}/medications/{medId} 04 B โœ…
POST / GET / DELETE /v1/parents/{id}/allergies 04 B โœ…
POST / DELETE /v1/parents/{id}/device-tokens 04 B โœ…
POST / GET / DELETE /v1/parents/{id}/conditions[/{id}] 05 B โœ…
POST /v1/safety/check 02/03/05 A โœ…
GET /v1/parents/{id}/weather-advisory 06/07 B (Controller) โ† A (๋ฃฉ์—…) โœ…
POST /v1/parents/{id}/hospitals 08 B โœ…
POST /v1/parents/{id}/pharmacies 08 B โœ…
POST /v1/parents/{id}/emergency-card/regenerate 08 B โœ…
POST /v1/parents/{id}/emergency-card/rotate 08 B โœ…
POST /v1/parents/{id}/emergency-card/revoke 08 B โœ…
GET /em/{token} 08 B โŒ Rate Limit
POST /v1/admin/weather/etl-run 07 A ๊ด€๋ฆฌ์ž
GET /v1/admin/weather/recent-pushes?days= 07 B ๊ด€๋ฆฌ์ž
GET /v1/admin/weather-rules?status=needs_review 06 A ๊ด€๋ฆฌ์ž
POST /v1/admin/weather-rules/{id}/approve 06 A ๊ด€๋ฆฌ์ž

9. ๋ฐ์ดํ„ฐ ์ ์žฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

์Šฌ๋ผ์ด์Šค ๋ฐ์ดํ„ฐ ์œ„์น˜ ํฌ๊ธฐ ๋‹ด๋‹น
02 DUR 5๊ฐœ CSV data/๊ฑด๊ฐ•๋ณดํ—˜์‹ฌ์‚ฌํ‰๊ฐ€์›_์˜์•ฝํ’ˆ์•ˆ์ „์‚ฌ์šฉ์„œ๋น„์Šค(DUR)*/ 240MB (๋ณ‘์šฉ๊ธˆ๊ธฐ 231MB ๋ณ„๋„ ๋‹ค์šด๋กœ๋“œ) A
01ยท02 ATC ๋งคํ•‘ data/๊ฑด๊ฐ•๋ณดํ—˜์‹ฌ์‚ฌํ‰๊ฐ€์›_ATC์ฝ”๋“œ ๋งคํ•‘ ๋ชฉ๋ก_20250630.csv 2.4MB A
05 ์งˆ๋ณ‘๋งˆ์Šคํ„ฐ HIRA 11984 ๋ณ„๋„ ๋‹ค์šด๋กœ๋“œ โ€” B
06 ๋‚ ์”จ ๋ฃฐ์…‹ 32๊ฐœ seed/weather_rules_v0.2.json 76KB A
06 ๊ฒฉ์ž ์ขŒํ‘œ 26K ๊ธฐ์ƒ์ฒญ41_๋‹จ๊ธฐ์˜ˆ๋ณด_*/๊ฒฉ์ž_์œ„๊ฒฝ๋„(2510).xlsx 680KB A
03 ํšŒ๊ท€ NB ์ถ”์ถœ ๊ณจ๋“  nb_amlodipine.*, nb_warfarin.* 66KB A

231MB CSV ์ถœ์ฒ˜: HIRA ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ sno 11983 (data.go.kr/data/15095677)


10. ์ฃผ์˜์‚ฌํ•ญ

10.1 MVP ๋ฒ”์œ„ ๋ฐ– (mock ์ฒ˜๋ฆฌ, ๋ณ„๋„ PRD)

  • ์นด์นด์˜ค OAuth ยท SMS ๋ณธ์ธ์ธ์ฆ โ†’ "์ธ์ฆยท์„ธ์…˜ PRD"
  • ์นด์นด์˜คํ†ก ์•Œ๋ฆผํ†ก / FCMยทAPNs ์‹ค์ œ ๋ฐœ์†ก โ†’ "ํ‘ธ์‹œ ์ธํ”„๋ผ PRD"
  • ์ฒ˜๋ฐฉ์ „ OCR โ†’ "OCR PRD"
  • ์˜๋ฃŒ์ง„ ๊ฒ€์ˆ˜ ์–ด๋“œ๋ฏผ โ†’ "๊ด€๋ฆฌ์ž PRD"

MVP์—์„œ ์ž๋…€ ์„ธ์…˜์€ X-Parent-Owner ํ—ค๋” ๋˜๋Š” mock JWT๋กœ ๋Œ€์ฒด. (์ˆ˜์ข…์ด ์ธ์ฆ mock ์ธํ„ฐ์…‰ํ„ฐ ์„ค์น˜)

10.2 DBยท์ธํ”„๋ผ ์œ„ํ—˜

  • DUR 837kํ–‰ LIKE ๊ธˆ์ง€ โ†’ ingredient_norm = ? ์ •ํ™•๋งค์นญ (A ๋™ํ™˜)
  • partial UNIQUE๋Š” PostgreSQL ํŠนํ™” โ†’ Testcontainers PostgreSQL ํ•„์ˆ˜ (H2 โŒ)
  • ์›”๋ณ„ RANGE ํŒŒํ‹ฐ์…˜ ์ž๋™ ์ƒ์„ฑ ์žก โ€” pg_partman or Spring @Scheduled๋กœ ๋‹ค์Œ๋‹ฌ ๋ฏธ๋ฆฌ ์ƒ์„ฑ (๊ณต๋™, 04ยท07 ์‚ฌ์šฉ)
  • Spring Boot 4.x: spring-boot-starter-web โ†’ webmvc (์ด๋ฏธ ์ ์šฉ), Java 17 ๋ฏธ๋งŒ ํ˜ธํ™˜ ๋ถˆ๊ฐ€ โ€” Java 21 ์‚ฌ์šฉ

10.3 ๋ณด์•ˆยทํŠธ๋žœ์žญ์…˜

  • ์•ฝ ์ถ”๊ฐ€: safety_check_log INSERT๋Š” @Transactional(REQUIRES_NEW) ๋˜๋Š” ApplicationEvent๋กœ BLOCK rollback๊ณผ ๋ถ„๋ฆฌ (B ์ˆ˜์ข…)
  • device_tokens ํ•ญ์ƒ pgp_sym_encrypt/decrypt ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ. JPA AttributeConverter๋กœ๋„ ๊ตฌํ˜„ ๊ฐ€๋Šฅ (B ์ˆ˜์ข…)
  • /em/{token} Bucket4j + Redis Rate Limit + Cache-Control: no-store (B ์ˆ˜์ข…)
  • RLS๋Š” ์ธ์ฆ ์‹œ์Šคํ…œ ๋ถ™์€ ํ›„ ์ธํ„ฐ์…‰ํ„ฐ์—์„œ SET LOCAL app.current_parent_id ์ž๋™ ์ฃผ์ž… (๊ณต๋™)

10.4 LLM ํ™˜๊ฐ ๋ฐฉ์ง€ (A ๋™ํ™˜)

  • source_quote๊ฐ€ NB ์›๋ฌธ์— byte-for-byte ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์‚ฌ์šฉ์ž ๋…ธ์ถœ ๊ธˆ์ง€
  • verified=false ๋ฐ์ดํ„ฐ๋Š” SELECT์—์„œ ์ œ์™ธ
  • ํ™˜๊ฐ ๊ฒ€์ฆ ๋‹จ์œ„ํ…Œ์ŠคํŠธ ํ•„์ˆ˜ (Slice 00 ์ˆ˜๋ฝ๊ธฐ์ค€)

11. MVP TODO ๋ฆฌ์ŠคํŠธ (๋‹ด๋‹น์ž๋ณ„)

๐Ÿฅ‡ Week 1 โ€” ์ธํ”„๋ผ + ์Šฌ๋ผ์ด์Šค 01ยท04 ์Šค์บํด๋“œ

๐Ÿค ๊ณต๋™ (Day 1~2 ๊ฐ™์ด ํŽ˜์–ด ๊ถŒ์žฅ)

  • build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€ (data-jpa, data-redis, flyway, redisson, bucket4j, mapstruct, testcontainers)
  • docker-compose.yml โ€” PostgreSQL 14 + Redis 7
  • Flyway: V001__extensions.sql, V002__schemas.sql, V003__audit_trigger.sql
  • application.yaml + ํ™˜๊ฒฝ๋ณ€์ˆ˜ 7๊ฐœ + application-local.yaml
  • BaseTimeEntity (@MappedSuperclass, created_at/updated_at)
  • GlobalExceptionHandler + ApiResponse<T> record + ErrorResponse record + BlockErrorResponse record
  • SafetyBlockException, ApiException
  • SafetyJudgeService ์ธํ„ฐํŽ˜์ด์Šค ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ•ฉ์˜ ํ›„ ๋ฐ•์•„๋‘๊ธฐ (Week 3 ํ†ตํ•ฉ ๋Œ€๋น„)
  • CI (GitHub Actions: build, test, Flyway dry-run)

A ๋™ํ™˜

  • DrugNameNormalizer (15 ์—ผ ๋ชฉ๋ก) + 6 ์ผ€์ด์Šค ๋‹จ์œ„ํ…Œ์ŠคํŠธ
  • HallucinationVerifier (3๋‹จ๊ณ„ ๋งค์นญ) + ๋‹จ์œ„ํ…Œ์ŠคํŠธ
  • GeminiClient (RestClient) + ping ํ†ตํ•ฉํ…Œ์ŠคํŠธ
  • Slice 01 ์‹œ์ž‘: MfdsClient ๊ณจ๊ฒฉ + Drug ์—”ํ‹ฐํ‹ฐ

B ์ˆ˜์ข…

  • RedisCacheService (read-through, fallback)
  • PgCryptoService (๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ)
  • Slice 04 ์‹œ์ž‘: PatientProfile, PatientMedication, PatientAllergy, DeviceToken ์—”ํ‹ฐํ‹ฐ + Repository
  • ParentController, MedicationController ์Šค์บํด๋“œ (SafetyJudge mock์œผ๋กœ ์šฐ์„  ๋™์ž‘)

๐Ÿฅ‡ Week 2 โ€” Slice 02 + Slice 04 Medication CRUD

A ๋™ํ™˜ โ€” Slice 02

  • DUR 5 CSV ์ ์žฌ (Spring Batch Job ๋˜๋Š” CommandLineRunner) โ€” cp949 ์ธ์ฝ”๋”ฉ
  • ref.dur_* 5 ํ…Œ์ด๋ธ” + ingredient_norm ์ธ๋ฑ์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • ATC ๋งคํ•‘ CSV ์ ์žฌ
  • DurEngineService ์ •ํ™•๋งค์นญ (ingredient_norm = ?)
  • SafetyJudgeService MVP ๊ตฌํ˜„ (DUR + ๋…ธ์ธ์ฃผ์˜๋งŒ, NB๋Š” ๋‹ค์Œ ์ฃผ)
  • EXPLAIN์œผ๋กœ ์ธ๋ฑ์Šค hit ๊ฒ€์ฆ
  • amlodipine+itraconazole BLOCK ํ†ตํ•ฉํ…Œ์ŠคํŠธ

B ์ˆ˜์ข… โ€” Slice 04 CRUD

  • ParentController CRUD ์™„์„ฑ (POST/GET/PATCH/DELETE)
  • MedicationController.addMedication โ€” mock SafetyJudge๋กœ ๋™์ž‘
  • AllergyController CRUD
  • DeviceTokenController + pgcrypto ์•”ํ˜ธํ™” ๋‹จ์œ„ํ…Œ์ŠคํŠธ
  • logs.safety_check_log ์›”๋ณ„ ํŒŒํ‹ฐ์…˜ + ์ฒซ ํŒŒํ‹ฐ์…˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • partial UNIQUE ์œ„๋ฐ˜ ํ†ตํ•ฉํ…Œ์ŠคํŠธ (Testcontainers)
  • soft delete ํ†ตํ•ฉํ…Œ์ŠคํŠธ

๐Ÿค ๊ณต๋™ ์ ๊ฒ€

  • HTTP 409 BLOCK ์‘๋‹ต ๊ตฌ์กฐ ์ ๊ฒ€ (api-types/safety.ts์™€ ์ •ํ•ฉ)

๐Ÿฅ‡ Week 3 โ€” Slice 03 + Slice 05 + SafetyJudge ํ†ตํ•ฉ โ˜…

A ๋™ํ™˜ โ€” Slice 03

  • NB ์ถ”์ถœ LLM ํŒŒ์ดํ”„๋ผ์ธ (NbExtractionService.extract(itemSeq))
  • derived.nb_extractions, derived.nb_interactions ํ…Œ์ด๋ธ” (entry_type ์ฒ˜์Œ๋ถ€ํ„ฐ ํฌํ•จ)
  • HallucinationVerifier ํ†ตํ•ฉ (์‹คํŒจ ์‹œ 1ํšŒ ์žฌ์‹œ๋„, verified=false ์ œ์™ธ)
  • ์•ฝ๋ฌผ๊ตฐ โ†’ ATC prefix ๋งคํ•‘ ์‚ฌ์ „
  • SafetyJudgeService.judgeFull() ์™„์„ฑ (DUR + NB ์–‘๋ฐฉํ–ฅ)
  • ํšŒ๊ท€: amlodipine 39 entry / warfarin 46 entry / ํ™˜๊ฐ 0

B ์ˆ˜์ข… โ€” Slice 05

  • HiraDiseaseClient (HIRA 12904 XML)
  • ref.disease_master CSV ์ ์žฌ (47,798ํ–‰)
  • ConditionController + app.patient_conditions ์ •๊ทœํ™”
  • KCD ๋งคํ•‘ ์‚ฌ์ „ + KcdRangeExpander ์œ ํ‹ธ (K70-K77 โ†’ set ํ™•์žฅ)

๐Ÿค ํ†ตํ•ฉ (Week 3 ํ›„๋ฐ˜) โ˜… โ€” ๋ฐ๋ชจ ํ•ต์‹ฌ

  • A์˜ SafetyJudgeService ์‹ค๊ตฌํ˜„ โ†’ B์˜ MedicationController๊ฐ€ ํ˜ธ์ถœํ•˜๋„๋ก swap
  • e2e: ๋…ธ๋ฐ”์Šคํฌ+์‹ฌ๋ฐ”์Šคํƒ€ํ‹ด ์ถ”๊ฐ€ โ†’ 409 ์‘๋‹ต ๊ฒ€์ฆ
  • e2e: ๋ถ€๋ชจ K25 + NSAID ์ถ”๊ฐ€ โ†’ WARN ์‘๋‹ต ๊ฒ€์ฆ
  • e2e: ๋ถ€๋ชจ ํŽ˜๋‹ˆ์‹ค๋ฆฐ ์•Œ๋ ˆ๋ฅด๊ธฐ + amoxicillin โ†’ 409 BLOCK ๊ฒ€์ฆ
  • AยทB ํŽ˜์–ด PR review

๐Ÿฅˆ Week 4 โ€” Slice 06 + Slice 08

A ๋™ํ™˜ โ€” Slice 06

  • weather_rules_v0.2.json 32 ๋ฃฐ ์ ์žฌ (ref.weather_rules)
  • ๊ฒฉ์ž์ขŒํ‘œ xlsx 26K ์ ์žฌ (ref.region_grid) โ€” Apache POI
  • WeatherAdvisorService.lookupRules(diseaseCodes, alerts) (rule_id ํฌํ•จ)
  • WeatherAdvisorService.updateParentGrid(parentId) (๋™ ์šฐ์„  ๋งค์นญ)
  • ์˜๋ฃŒ์ง„ ๊ฒ€์ˆ˜ ์–ด๋“œ๋ฏผ API (needs_review ํ, 17๊ฑด)

B ์ˆ˜์ข… โ€” Slice 08

  • HiraHospitalClient / HiraPharmacyClient (XML)
  • parent_hospitals, hospital_emergency_info, parent_pharmacies, emergency_cards ํ…Œ์ด๋ธ”
  • EmergencyCardService.regenerate() โ€” ํ•ญ์‘๊ณ ์ œ ์ •๋ ฌ, snapshot JSON ์ƒ์„ฑ
  • PublicEmController.getByToken() โ€” GET /em/{token} + Bucket4j Rate Limit + Redis ์นด์šดํ„ฐ
  • ํ† ํฐ ํšŒ์ „ยทrevokeยท๋งŒ๋ฃŒ ์‹œ๋‚˜๋ฆฌ์˜ค
  • access_count ์›์ž ์ฆ๊ฐ€ ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ (JMeter or ๊ฐ€๋ฒผ์šด ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ)

๐Ÿค ํ•ฉ์˜

  • lookupRules ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์ • (Week 5 ETL ํ†ตํ•ฉ ๋Œ€๋น„)

๐Ÿฅˆ Week 5 โ€” Slice 07 ํ†ตํ•ฉ ETL + ๋ฐ๋ชจ ๋ฆฌํ—ˆ์„ค

A ๋™ํ™˜ โ€” ETL ๋ฐ์ดํ„ฐ ๋ผ์ธ

  • KmaForecastClient.getVilageFcst(nx, ny, baseDate, baseTime)
  • TMX/TMN ์ถ”์ถœ + ์ž์ฒด ์ž„๊ณ„๊ฐ’ ํŒ์ • ๋กœ์ง
  • logs.weather_observations_daily ํŒŒํ‹ฐ์…˜ ์ ์žฌ
  • @Scheduled(cron="0 30 5 * * *", zone="Asia/Seoul") ์žก โ€” Java 21 Virtual Threads๋กœ ๊ฒฉ์ž ๋ณ‘๋ ฌ ํ˜ธ์ถœ
  • Redisson ์žก ๋ฝ (weather_etl:job:{date})

B ์ˆ˜์ข… โ€” ํ‘ธ์‹œ ๋ผ์ธ

  • ํ™œ์„ฑ ๋ถ€๋ชจ ์กฐํšŒ + ๊ฒฉ์ž ๊ทธ๋ฃนํ•‘
  • app.patient_conditions JOIN โ†’ A์˜ lookupRules() ํ˜ธ์ถœ
  • ์šฐ์„ ์ˆœ์œ„ ์ •์ฑ… (severity ์œ„ํ—˜ ์šฐ์„  1ํ‘ธ์‹œ/์ผ)
  • ์•Œ๋žŒ ํ”ผ๋กœ ๋ฐฉ์ง€ + requires_review skip โ†’ delivery_status 5์ข… ๋ถ„๊ธฐ
  • device_tokens ๋ณตํ˜ธํ™” โ†’ ํ‘ธ์‹œ ๋ฐœ์†ก (mock)
  • logs.advisory_push_log ํŒŒํ‹ฐ์…˜ ์ ์žฌ
  • Redisson ๋ถ€๋ชจ ๋ฝ (weather_etl:parent:{pid}:{date})

๐Ÿค Week 5 ํ›„๋ฐ˜ โ€” ํ†ตํ•ฉ + ๋ฐ๋ชจ ๋ฆฌํ—ˆ์„ค

  • ETL โ†’ ํ‘ธ์‹œ e2e: TMX=36 mock โ†’ ํญ์—ผ๊ฒฝ๋ณด โ†’ I10 ๋ถ€๋ชจ ํ‘ธ์‹œ 1๊ฑด
  • ์•Œ๋žŒ ํ”ผ๋กœ: ๊ฐ™์€ (parent,rule,date) 2๋ฒˆ์งธ ํ˜ธ์ถœ โ†’ skipped_fatigue
  • ์šฐ์„ ์ˆœ์œ„: I10(์œ„ํ—˜)+E11(์ฃผ์˜) โ†’ I10๋งŒ
  • ์ค‘๋ณต์‹คํ–‰: ETL 2ํšŒ ํŠธ๋ฆฌ๊ฑฐ โ†’ 2๋ฒˆ์งธ skipped_duplicate_run
  • ๊ณต๋ชจ์ „ ๋ฐ๋ชจ ์‹œ๋‚˜๋ฆฌ์˜ค e2e ๋ฆฌํ—ˆ์„ค (HANDOFF_COMMON.md ยง13):
    1. ๋ถ€๋ชจ ๋“ฑ๋ก โ†’ ์•ฝ 4๊ฐœ ์ถ”๊ฐ€ (warfarin ํฌํ•จ)
    2. ์•„์Šคํ”ผ๋ฆฐ ์ถ”๊ฐ€ โ†’ 409 BLOCK ๋ชจ๋‹ฌ
    3. ์‘๊ธ‰์นด๋“œ QR โ†’ /em/{token} ์™ธ๋ถ€ ์กฐํšŒ โ†’ critical_drugs์— warfarin
    4. ํญ์—ผ๊ฒฝ๋ณด ์‹œ๋ฎฌ โ†’ I10 ๋งค์นญ advisory
  • ํ”„๋ก ํŠธ์™€ ํ†ตํ•ฉ (EXPO_PUBLIC_USE_MOCK=false)

๐Ÿฅ‰ Week 6+ โ€” ๊ณ ๋„ํ™” (๋‹ด๋‹น ์ถ”ํ›„ ์ •ํ•จ)

  • RLS ํ™œ์„ฑํ™” + ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ
  • ์นด์นด์˜ค OAuth (๋ณ„๋„ PRD)
  • FCM/APNs ์‹ค์ œ ๋ฐœ์†ก
  • ์ฒ˜๋ฐฉ์ „ OCR ํ†ตํ•ฉ
  • ์˜๋ฃŒ์ง„ ๊ฒ€์ˆ˜ ์–ด๋“œ๋ฏผ
  • Prometheus + Grafana ๋Œ€์‹œ๋ณด๋“œ
  • pg_partman ์ž๋™ ํŒŒํ‹ฐ์…˜
  • Phase 2 ๋ฃฐ์…‹ (์น˜๋งค, ๋ฏธ์„ธ๋จผ์ง€, ํ™ฉ์‚ฌ)

12. ์ •์˜ (Done)

  • ๊ฐ ์Šฌ๋ผ์ด์Šค "์ˆ˜๋ฝ ๊ธฐ์ค€" โœ… + ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ๊ทธ๋ฆฐ + PR ๋จธ์ง€
  • ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ทธ๋ฆฐ
  • Smoke Test ํ†ต๊ณผ (๊ฐ ์Šฌ๋ผ์ด์Šค ๋ฌธ์„œ ํ•˜๋‹จ curl ์‹œ๋‚˜๋ฆฌ์˜ค)
  • ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ README๋งŒ ๋ณด๊ณ  docker compose up โ†’ ํ™˜๊ฒฝ ๊ธฐ๋™ ๊ฐ€๋Šฅ

๊ตฌํ˜„ ์ˆœ์„œยทDDLยทAPI ๊ณ„์•ฝ์€ issues_v2/*.md๊ฐ€ ground truth. ๋ณ€๊ฒฝ ํ•„์š”ํ•˜๋ฉด ์Šฌ๋ผ์ด์Šค ๋ฌธ์„œ ๋จผ์ € ์ˆ˜์ •, ์ฝ”๋“œ๋Š” ๊ทธ ๋‹ค์Œ.


13. ํ”„๋ก ํŠธ์—”๋“œ ๋™๊ธฐํ™”

๋ฐฑ์—”๋“œ ์Šฌ๋ผ์ด์Šค ์™„๋ฃŒ ์‹œ ํ”„๋ก ํŠธ๊ฐ€ mock โ†’ ์‹ค API ๊ต์ฒด ์ฑ…์ž„
04 (์•ฝ์žฅ + ์•ˆ์ „ํŒ์ •) F2 (โ˜… ๋ฐ๋ชจ ํ•ต์‹ฌ) B ์ˆ˜์ข…
05 (์งˆ๋ณ‘๊ธˆ๊ธฐ) F3 B ์ˆ˜์ข…
06ยท07 (๋‚ ์”จ) F5 ๊ณต๋™
08 (์‘๊ธ‰์นด๋“œ) F4 B ์ˆ˜์ข…
01 (์•ฝ ๋งˆ์Šคํ„ฐ) F2ยทF4ยทF6 ์•Œ์•ฝ ์‚ฌ์ง„ A ๋™ํ™˜

๊ฐ ์Šฌ๋ผ์ด์Šค PR ๋จธ์ง€ ์‹œ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž์—๊ฒŒ Slack/Teams ์•Œ๋ฆผ โ€” ๋‹ด๋‹น์ž๊ฐ€ ์ง์ ‘.


14. ๋ง‰ํž ๋•Œ

์ƒํ™ฉ ์ฐธ์กฐ
DDL ๋ชจ๋ฅด๊ฒ ์Œ ํ•ด๋‹น ์Šฌ๋ผ์ด์Šค์˜ "DB ์ ์žฌ" ์„น์…˜
API ์‘๋‹ต ๊ตฌ์กฐ ํ•ด๋‹น ์Šฌ๋ผ์ด์Šค์˜ "API ๊ณ„์•ฝ" + api-types/*.ts
๋ธ”๋กœ์ปค 12๊ฐœ ๊ฒฐ์ • PRD_์—„๋งˆ์•ฝ_v6.md ยง๋ธ”๋กœ์ปค 12๊ฐœ ๋งคํ•‘
ํ”„๋ก ํŠธ์™€ ์ถฉ๋Œ HANDOFF_COMMON.md
์ „์ฒด ํ๋ฆ„ ์‹œ๊ฐํ™” ์—„๋งˆ์•ฝ_API_ํ๋ฆ„๋„_v8.html (๋ธŒ๋ผ์šฐ์ €)
Spring Boot 4.x ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes
์ง๊ฟ์ด ๋ง‰ํž˜ ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ / ํŽ˜์–ด PR review

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors