콘텐츠로 이동

Procedure Hub 목표 임상 데이터 ERD

상태: 논리 모델·물리 baseline 구현 완료 · 2026-09-03 범위: Procedure Hub가 관리·공급하는 임상 콘텐츠와 판단 규칙 제외: 환자 차트, 실제 시술 이력, 전자서명 동의서, 결제·예약 구현 경계: 도메인 메서드 계약 행위 계약: 도메인 메서드 계약

1. 결론

현재 JSON과 ORM은 화면·파일 단위로 나뉘어 있다. 목표 모델은 피부과 의사의 판단 흐름을 기준으로 다음 다섯 영역으로 재구성한다.

환자 고민·적응증
      ↓
시술·제품 후보 ── 임상 지식·근거
      ↓
위험·금기·순서·간격 검증
      ↓
추천 패키지·조합·치료 계획
      ↓
상담 설명·동의·사후관리

핵심 결정은 다음과 같다.

  • Concern은 환자가 표현하는 고민이고 Indication은 의학적 적응증이다. 둘을 같은 엔티티로 취급하지 않는다.
  • ProcedureConcept는 시술군·세부 분류·실제 시술·기법을 하나의 계층으로 관리하고 concept_type으로 의미를 구분한다. 따라서 규칙은 M1 같은 시술군과 IPL 같은 구체 시술을 모두 정확히 가리킬 수 있다.
  • Product는 단일 원장으로 관리한다. taxonomy.json의 제품과 products.json의 제품 목록을 별도 테이블로 유지하지 않는다.
  • 추천·비교·안전 화면은 각각 데이터를 다시 소유하지 않는다. 동일한 시술·제품·규칙을 조합한 read model로 만든다.
  • 안전 규칙과 추천 관계를 분리한다. 충돌 시 ProcedureConstraint가 항상 우선한다.
  • JSONB는 원본 스냅샷과 import 이력에만 사용한다. 조회에 필요한 임상 필드는 정규화한다.

2. 현재 구조에서 확인된 문제

  • src/data에는 JSON 47개가 있고, 이 중 23개가 루트 도메인·운영 파일이다.
  • 20개 도메인은 이미 SQLAlchemy 모델로 미러링되어 있지만 파일 경계가 그대로 테이블 경계가 되었다.
  • Next.js 데이터 route 22개는 JSON을 직접 읽고, FastAPI는 일부만 DB에서 제공한다.
  • 제품은 taxonomy 내부와 독립 제품 사전에 중복되고 실제 명칭 드리프트도 존재한다.
  • 추천은 problem-combos와 problem-packages, 안전·순서는 contraindications, treatment-sequence, synergy와 프론트 알고리즘으로 분산돼 있다.
  • 프론트는 자유 문장에서 순서와 간격을 다시 해석한다. 임상 규칙이 구조화된 원장으로 존재하지 않는다는 신호다.

따라서 기존 테이블을 그대로 유지한 채 저장소만 DB로 바꾸는 것은 목표가 아니다.

3. 전체 컨텍스트

flowchart LR
    Catalog["진료 기준정보\n시술·제품·고민·부위"]
    Knowledge["임상 지식·근거\n기전·효과·문헌"]
    Safety["안전 규칙\n금기·순서·간격·위험"]
    Recommendation["추천·계획\n패키지·조합·회차"]
    Guidance["환자 안내\n동의·사후관리"]
    Operations["콘텐츠 운영\n가져오기·검수·배포·복원"]

    Catalog --> Knowledge
    Catalog --> Safety
    Catalog --> Recommendation
    Knowledge --> Safety
    Knowledge --> Recommendation
    Safety --> Recommendation
    Knowledge --> Guidance
    Safety --> Guidance
    Operations --> Catalog
    Operations --> Knowledge
    Operations --> Safety
    Operations --> Recommendation
    Operations --> Guidance

4. ERD A — 진료 기준정보

erDiagram
    PROCEDURE_CONCEPT o|--o{ PROCEDURE_CONCEPT : parent_of
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_ALIAS : resolves
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_PRODUCT : uses
    PRODUCT ||--o{ PROCEDURE_PRODUCT : assigned_to
    MANUFACTURER o|--o{ PRODUCT : produces
    PRODUCT ||--o{ PRODUCT_REGULATORY_STATUS : regulated_as
    EVIDENCE_SOURCE o|--o{ PRODUCT_REGULATORY_STATUS : verifies

    CONCERN ||--o{ COMPLAINT : details
    CONCERN ||--o{ CONCERN_INDICATION : maps_to
    INDICATION ||--o{ CONCERN_INDICATION : explains
    INDICATION ||--o{ PROCEDURE_INDICATION : treated_by
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_INDICATION : treats

    ANATOMICAL_SITE ||--o{ PROCEDURE_SITE : applicable_to
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_SITE : performed_at
    ANATOMICAL_SITE o|--o{ ANATOMICAL_SITE : parent_of

    PROCEDURE_CONCEPT {
        uuid id PK
        string public_code UK
        string legacy_code UK
        uuid parent_id FK "nullable"
        string concept_type "modality|category|procedure|technique"
        string name_ko
        int sort_order
        boolean active
    }
    PROCEDURE_ALIAS {
        uuid id PK
        uuid procedure_concept_id FK
        string alias UK
        string language
        string source
    }
    PRODUCT {
        uuid id PK
        string public_code UK
        string name_ko
        uuid manufacturer_id FK "nullable"
        string ingredient "nullable"
        string material "nullable"
        boolean active
    }
    MANUFACTURER {
        uuid id PK
        string name_ko
        string country_code
    }
    PRODUCT_REGULATORY_STATUS {
        uuid id PK
        uuid product_id FK
        string country_code
        string authority
        string status
        string approval_number "nullable"
        date verified_at "nullable"
        uuid evidence_source_id FK "nullable"
    }
    PROCEDURE_PRODUCT {
        uuid procedure_concept_id FK
        uuid product_id FK
        string role
        int sort_order
    }
    CONCERN {
        uuid id PK
        string public_code UK "P1-P14"
        string name_ko
        int sort_order
    }
    COMPLAINT {
        uuid id PK
        uuid concern_id FK
        string group_name
        string name_ko
        string detail
        int sort_order
    }
    INDICATION {
        uuid id PK
        string public_code UK
        string name_ko
    }
    CONCERN_INDICATION {
        uuid concern_id FK
        uuid indication_id FK
    }
    PROCEDURE_INDICATION {
        uuid procedure_concept_id FK
        uuid indication_id FK
        string strength
    }
    ANATOMICAL_SITE {
        uuid id PK
        uuid parent_id FK "nullable"
        string public_code UK
        string name_ko
    }
    PROCEDURE_SITE {
        uuid procedure_concept_id FK
        uuid site_id FK
        string applicability
    }

설계 메모:

  • 현재 M-code는 concept_type=modality인 최상위 concept의 외부 API 계약으로 유지한다.
  • M8-IPL, M9-HIFU처럼 파일마다 달라지는 표기는 하위 concept의 ProcedureAlias로 해석한다.
  • 신규 내부 PK는 UUID, 외부 소비자는 안정적인 public_code를 사용한다. 기존 auto-*와 한글 혼합 ID는 legacy_code에 보존하고 compatibility 응답에서만 기존 ID로 사용한다.
  • 분류 깊이는 고정하지 않는다. 시술군 아래 category 없이 실제 시술이 바로 올 수도 있다.

5. ERD B — 임상 지식과 근거

erDiagram
    PROCEDURE_CONCEPT ||--o| PROCEDURE_PROFILE : has
    PROCEDURE_CONCEPT ||--o{ KNOWLEDGE_DOCUMENT : described_by
    PROCEDURE_CONCEPT o|--o{ CLINICAL_CLAIM : concerns
    KNOWLEDGE_DOCUMENT ||--o{ DOCUMENT_CLAIM : contains
    CLINICAL_CLAIM ||--o{ DOCUMENT_CLAIM : included_in
    CLINICAL_CLAIM ||--o{ CLAIM_CITATION : supported_by
    EVIDENCE_SOURCE ||--o{ CLAIM_CITATION : cites
    CLINICAL_CLAIM ||--o{ CLAIM_VERIFICATION : verified_as
    SOURCE_ARTIFACT ||--o{ KNOWLEDGE_DOCUMENT : imported_from
    SOURCE_ARTIFACT o|--o{ EVIDENCE_SOURCE : imported_from

    PROCEDURE_PROFILE {
        uuid procedure_concept_id PK, FK
        string mechanism_group
        int duration_min
        int duration_max
        string onset
        string duration
        string anesthesia
        boolean creates_wound
        boolean uses_gel
        int thermal_load
        int bleeding_risk
        int swelling_risk
    }
    KNOWLEDGE_DOCUMENT {
        uuid id PK
        uuid procedure_concept_id FK
        uuid source_artifact_id FK
        string document_type
        string audience "clinical|counseling|patient"
        string language
        string title
        text body
        string status
    }
    CLINICAL_CLAIM {
        uuid id PK
        uuid procedure_concept_id FK "nullable"
        string topic
        string language
        text statement
        string status
    }
    DOCUMENT_CLAIM {
        uuid document_id FK
        uuid claim_id FK
        int sort_order
    }
    EVIDENCE_SOURCE {
        uuid id PK
        uuid source_artifact_id FK "nullable"
        string source_type
        string pmid UK "nullable"
        string doi UK "nullable"
        string title
        string journal
        int published_year
    }
    CLAIM_CITATION {
        uuid claim_id FK
        uuid evidence_source_id FK
        string support_level
        text note
    }
    CLAIM_VERIFICATION {
        uuid id PK
        uuid claim_id FK
        string status
        datetime checked_at
        string checker
        text reason
    }
    SOURCE_ARTIFACT {
        uuid id PK
        string artifact_type "json|xlsx|paper|textbook"
        string source_name
        string checksum
        string parser_version
        datetime imported_at
    }

상담 관점 / 의료 관점은 독립된 임상 사실 두 벌이 아니다. 하나의 ClinicalClaim에 연결된 audience별 KnowledgeDocument로 표현한다. 화면에서는 해당 문서가 존재할 때만 관점을 노출한다.

6. ERD C — 안전·진료 준비·회복

erDiagram
    SAFETY_SUBJECT ||--o{ SAFETY_SUBJECT_PROCEDURE : resolves_to
    PROCEDURE_CONCEPT ||--o{ SAFETY_SUBJECT_PROCEDURE : participates_in
    SAFETY_SUBJECT ||--o{ PROCEDURE_CONSTRAINT : source
    SAFETY_SUBJECT o|--o{ PROCEDURE_CONSTRAINT : target
    EVIDENCE_SOURCE ||--o{ CONSTRAINT_EVIDENCE : supports
    PROCEDURE_CONSTRAINT ||--o{ CONSTRAINT_EVIDENCE : justified_by

    ANATOMICAL_SITE ||--o{ ANATOMY_HAZARD : has
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_COMPLICATION : may_cause
    COMPLICATION ||--o{ PROCEDURE_COMPLICATION : applies_to
    COMPLICATION ||--o{ COMPLICATION_RISK_FACTOR : worsened_by
    COMPLICATION ||--o{ COMPLICATION_PROTOCOL : handled_by

    PROCEDURE_CONCEPT ||--o{ SCREENING_APPLICABILITY : requires
    SCREENING_QUESTION ||--o{ SCREENING_APPLICABILITY : asked_for
    PROCEDURE_CONCEPT ||--o{ CONSENT_DISCLOSURE : discloses
    PROCEDURE_CONCEPT ||--o{ RECOVERY_TIMELINE : recovers_by
    RECOVERY_TIMELINE ||--o{ RECOVERY_PHASE : consists_of
    RECOVERY_PHASE ||--o{ RECOVERY_REACTION : expects
    PROCEDURE_CONCEPT ||--o{ PIH_RISK_RULE : assessed_by

    SAFETY_SUBJECT {
        uuid id PK
        string public_code UK
        string label
        string subject_type "procedure|procedure_set|medication|material|preparation|state|scope|other"
        string mapping_status "technical_verified|clinical_reviewed|pending"
        json qualifiers
    }
    SAFETY_SUBJECT_PROCEDURE {
        uuid subject_id FK
        uuid procedure_concept_id FK
        int sort_order
    }
    PROCEDURE_CONSTRAINT {
        uuid id PK
        uuid from_subject_id FK
        uuid to_subject_id FK "nullable for state constraint"
        string constraint_type "combination|minimum_interval|same_day_prohibited|state_constraint"
        string safety_level "safe|caution|warning"
        string enforcement "advisory|required"
        int min_interval_value "nullable"
        int max_interval_value "nullable"
        string interval_unit "day|week|month, nullable"
        text raw_interval "nullable"
        boolean directional
        text rationale
        int priority
    }
    CONSTRAINT_EVIDENCE {
        uuid constraint_id FK
        uuid evidence_source_id FK
    }
    ANATOMY_HAZARD {
        uuid id PK
        uuid site_id FK
        string hazard_type "vessel|nerve|structure"
        string name
        string risk_level
        text guidance
    }
    COMPLICATION {
        uuid id PK
        string public_code UK
        string name_ko
        string severity
        boolean emergency
    }
    PROCEDURE_COMPLICATION {
        uuid procedure_concept_id FK
        uuid complication_id FK
        string incidence
        string onset
        string duration
    }
    COMPLICATION_RISK_FACTOR {
        uuid id PK
        uuid complication_id FK
        string factor
        string risk_level
    }
    COMPLICATION_PROTOCOL {
        uuid id PK
        uuid complication_id FK
        string protocol_type "prevention|immediate|referral"
        text instruction
        int sort_order
    }
    SCREENING_QUESTION {
        uuid id PK
        string public_code UK
        text question
        string answer_type
        string risk_level
    }
    SCREENING_APPLICABILITY {
        uuid procedure_concept_id FK
        uuid question_id FK
        string requirement
    }
    CONSENT_DISCLOSURE {
        uuid id PK
        uuid procedure_concept_id FK
        string disclosure_type
        text content
        boolean mandatory
    }
    RECOVERY_TIMELINE {
        uuid id PK
        uuid procedure_concept_id FK
        string title
    }
    RECOVERY_PHASE {
        uuid id PK
        uuid timeline_id FK
        int starts_at_day
        int ends_at_day
        int sort_order
    }
    RECOVERY_REACTION {
        uuid id PK
        uuid phase_id FK
        string name
        string level
        text guidance
    }
    PIH_RISK_RULE {
        uuid id PK
        uuid procedure_concept_id FK
        int fitzpatrick_min
        int fitzpatrick_max
        string risk_level
        text prevention
    }

ProcedureConstraint는 현재 contraindication·sequence·프론트 하드코딩에 흩어진 판단의 단일 원장이다. 다만 원문 endpoint에는 시술 외에도 약물·재료·상태·범위가 있으므로 SafetySubject가 그 의미를 먼저 보존한다. 양쪽 Subject가 시술로 해석될 때만 시술 pair용 ProcedureConstraint projection을 만든다. 더 구체적인 concept의 규칙이 상위 규칙보다 우선하며, 추천 엔진은 이 projection을 통과하지 못한 후보를 추천 결과로 확정할 수 없다. 세부 결정은 ADR 0003을 따른다.

7. ERD D — 추천·조합·가격

erDiagram
    CONCERN ||--o{ RECOMMENDATION_PACKAGE : has
    COMPLAINT o|--o{ RECOMMENDATION_PACKAGE : narrows
    RECOMMENDATION_PACKAGE ||--o{ PACKAGE_ITEM : contains
    RECOMMENDATION_PACKAGE ||--o{ PACKAGE_NARRATIVE : described_by
    PROCEDURE_CONCEPT ||--o{ PACKAGE_ITEM : selected_as
    PRODUCT o|--o{ PACKAGE_ITEM : optionally_uses

    COMBINATION_RECIPE ||--o{ RECIPE_ITEM : contains
    PROCEDURE_CONCEPT ||--o{ RECIPE_ITEM : ordered_as
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_SYNERGY : source
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_SYNERGY : target

    CONCERN ||--o{ CARE_PLAN_TEMPLATE : targets
    CARE_PLAN_TEMPLATE ||--o{ CARE_PLAN_STEP : schedules
    PROCEDURE_CONCEPT ||--o{ CARE_PLAN_STEP : performs
    PROCEDURE_CONCEPT o|--o{ PRICING_OBSERVATION : priced_as
    PRODUCT o|--o{ PRICING_OBSERVATION : priced_as

    RECOMMENDATION_PACKAGE {
        uuid id PK
        uuid concern_id FK
        uuid complaint_id FK "nullable"
        string package_type "tiered|complaint|optimization"
        string tier "nullable"
        int age_min "nullable"
        int age_max "nullable"
        string severity_scope "nullable"
        int priority
        boolean active
    }
    PACKAGE_NARRATIVE {
        uuid id PK
        uuid package_id FK
        string language
        string audience "clinical|counseling"
        string name
        text expected_result
        text explanation
        text rationale
    }
    PACKAGE_ITEM {
        uuid id PK
        uuid package_id FK
        uuid procedure_concept_id FK
        uuid product_id FK "nullable"
        string role "primary|adjunct|alternative"
        int sort_order
    }
    COMBINATION_RECIPE {
        uuid id PK
        string public_code UK
        string name
        string status
        text rationale
    }
    RECIPE_ITEM {
        uuid id PK
        uuid recipe_id FK
        uuid procedure_concept_id FK
        int sort_order
        int wait_after_days "nullable"
    }
    PROCEDURE_SYNERGY {
        uuid id PK
        uuid from_concept_id FK
        uuid to_concept_id FK
        string synergy_type
        int score "nullable"
        string timing
        boolean applies_to_descendants
        text rationale
    }
    CARE_PLAN_TEMPLATE {
        uuid id PK
        uuid concern_id FK
        string name
        int duration_months
        text rationale
    }
    CARE_PLAN_STEP {
        uuid id PK
        uuid template_id FK
        uuid procedure_concept_id FK
        int offset_days
        int sort_order
    }
    PRICING_OBSERVATION {
        uuid id PK
        uuid procedure_concept_id FK "nullable"
        uuid product_id FK "nullable"
        decimal min_amount
        decimal max_amount
        string currency
        string region
        date observed_at
        string source
    }

procedure-comparison은 별도 기준정보로 유지하지 않는다. ProcedureProfile, PricingObservation, 위험 규칙과 검증된 claim을 조합해 만드는 projection이 기본이다. 사람이 직접 작성한 비교 결론만 ClinicalClaim으로 보존한다.

PricingObservation은 procedure concept 또는 product 중 최소 하나를 반드시 참조한다. 금액은 5~8만 같은 문자열이 아니라 통화·지역·관측일과 함께 숫자 범위로 저장한다.

8. ERD E — 콘텐츠 가져오기·배포·복원

erDiagram
    SOURCE_ARTIFACT ||--o{ IMPORT_JOB : submitted_as
    IMPORT_JOB o|--o{ CONTENT_REVISION : produces
    CONTENT_REVISION o|--o{ CONTENT_REVISION : follows
    CONTENT_RELEASE ||--o{ RELEASE_REVISION : contains
    CONTENT_REVISION ||--o{ RELEASE_REVISION : published_in
    CONTENT_REVISION o|--o{ AUDIT_EVENT : audited_by
    CONTENT_RELEASE o|--o{ AUDIT_EVENT : audited_by
    CONTENT_RELEASE o|--o{ CONTENT_RELEASE : rollback_of

    CONTENT_REVISION {
        uuid id PK
        uuid import_job_id FK "nullable"
        uuid previous_revision_id FK "nullable"
        string domain_key
        int revision_no
        string status "draft|in_review|approved|rejected|archived"
        string content_hash
        jsonb snapshot
        jsonb diff_summary
        string created_by
        datetime created_at
    }
    CONTENT_RELEASE {
        uuid id PK
        string version UK
        string status "draft|published|superseded|cancelled"
        uuid rollback_of_release_id FK "nullable"
        string published_by
        datetime published_at
    }
    RELEASE_REVISION {
        uuid release_id FK
        uuid revision_id FK
    }
    IMPORT_JOB {
        uuid id PK
        uuid source_artifact_id FK
        string domain_key
        string status
        jsonb validation_result
        jsonb diff_summary
        string requested_by
        datetime created_at
    }
    AUDIT_EVENT {
        uuid id PK
        uuid revision_id FK "nullable"
        uuid release_id FK "nullable"
        string action
        string actor
        jsonb metadata
        datetime occurred_at
    }

운영 흐름은 이미 glossary에서 검증된 패턴을 일반화한다.

canonical mapping registry도 import 동작을 바꾸는 입력이므로 source artifact와 revision으로 함께 보존한다. 미확정 reference는 IMPORT_MAPPING_ISSUE로 격리하며 source snapshot 저장은 허용하되 release publish는 차단한다.

JSON/XLSX/관리 화면
  → parse
  → schema·FK·임상 규칙 검증
  → diff preview
  → draft revision
  → review
  → release publish transaction
  → 정규화된 live table 반영
  → cache invalidation

복원은 과거 행을 다시 활성화하지 않고 과거 snapshot을 내용으로 하는 새 revision을 발행한다. 감사 이력은 불변으로 남는다.

9. ERD F — 다국어 명칭과 통역 사전

erDiagram
    PROCEDURE_CONCEPT ||--o{ PROCEDURE_CONCEPT_TRANSLATION : translated_as
    PRODUCT ||--o{ PRODUCT_TRANSLATION : translated_as
    CONCERN ||--o{ CONCERN_TRANSLATION : translated_as
    COMPLAINT ||--o{ COMPLAINT_TRANSLATION : translated_as
    INDICATION ||--o{ INDICATION_TRANSLATION : translated_as
    ANATOMICAL_SITE ||--o{ ANATOMICAL_SITE_TRANSLATION : translated_as
    COMPLICATION ||--o{ COMPLICATION_TRANSLATION : translated_as
    GLOSSARY_TERM ||--o{ GLOSSARY_TRANSLATION : translated_as

    PROCEDURE_CONCEPT_TRANSLATION {
        uuid procedure_concept_id FK
        string language
        string name
        string description "nullable"
    }
    PRODUCT_TRANSLATION {
        uuid product_id FK
        string language
        string name
    }
    CONCERN_TRANSLATION {
        uuid concern_id FK
        string language
        string name
    }
    COMPLAINT_TRANSLATION {
        uuid complaint_id FK
        string language
        string name
        string detail "nullable"
    }
    INDICATION_TRANSLATION {
        uuid indication_id FK
        string language
        string name
    }
    ANATOMICAL_SITE_TRANSLATION {
        uuid site_id FK
        string language
        string name
    }
    COMPLICATION_TRANSLATION {
        uuid complication_id FK
        string language
        string name
        string description "nullable"
    }
    GLOSSARY_TERM {
        uuid id PK
        string category
        string ko UK
        string source
        string note "nullable"
    }
    GLOSSARY_TRANSLATION {
        uuid term_id FK
        string language
        string translated_text
        string review_status
    }

KnowledgeDocument, ClinicalClaim, PackageNarrative는 본문 자체에 language와 audience가 있으므로 별도 번역 테이블을 만들지 않는다. 사용자 노출 기준정보는 명시적 translation 테이블을 사용한다. 통역 사전은 entity label과 목적이 다르므로 별도 aggregate로 유지한다.

10. Master·Enum·Const·설정의 경계

DB로 옮긴다는 이유만으로 모든 값을 master table로 만들지 않는다. 값의 생명주기와 변경 주체를 기준으로 다음처럼 구분한다.

구분 판단 기준 변경 방식
Master 다른 데이터가 동일 식별자를 FK로 참조하고, 배포 없이 추가·수정될 수 있음 관리 기능 + revision/release
Versioned domain data 임상 설명·근거·추천·안전 규칙처럼 검수와 이력이 필요함 draft → review → release
Enum 유한하고 닫힌 상태 집합이며, 값 추가 시 코드 분기와 API 계약이 바뀜 코드·계약·DB CHECK를 함께 변경
Const UI 표현이나 고정된 제품 정책이며 임상 데이터와 독립적임 코드 변경
Operational config 환경별 임계값·활성 기능·제한값이며 임상 원장이 아님 환경 변수 또는 배포 설정
Derived read model master와 규칙에서 재현할 수 있는 결과임 저장하지 않고 조회 시 계산

10.1 Master로 관리할 기준정보

Master aggregate 포함 엔티티 Master인 이유
시술 분류 ProcedureConcept, ProcedureAlias 모든 규칙·추천·제품 연결의 기준 식별자이며 계층·별칭이 변경됨
제품 Product, Manufacturer, ProductRegulatoryStatus 제조사·허가 상태·출처를 포함하고 여러 시술에서 재사용됨
고민·적응증 Concern, Complaint, Indication 환자 표현과 의학적 적응증을 분리해 추천과 설명이 공통 참조함
해부학적 부위 AnatomicalSite 시술 적용 부위와 위험 부위가 동일한 canonical site를 참조함
합병증 분류 Complication 시술별 발생 정보·위험 인자·대응 프로토콜이 공통 참조함
문헌 식별자 EvidenceSource claim·안전 규칙·제품 허가 검증이 같은 논문·가이드라인을 참조함
통역 사전 원어 GlossaryTerm 여러 언어 번역과 검수 상태의 기준 식별자임

ProcedureProfile, ClinicalClaim, KnowledgeDocument, ProcedureConstraint, ProcedureSynergy, RecommendationPackage, ScreeningQuestion, ConsentDisclosure, RecoveryTimeline은 DB에 저장하지만 공통 lookup master는 아니다. 이들은 근거와 검수 이력이 필요한 versioned domain data로 관리한다.

10.2 코드 Enum으로 고정할 값

Enum 값 또는 초기 후보 현재 근거
LanguageCode ko, en, ja, zh, yue, th, vi backend Lang, frontend Lang
ProcedureConceptType modality, category, procedure, technique ProcedureConcept 계층 분기
Audience clinical, counseling, patient 문서·설명 관점 분기
IndicationType primary, secondary, extended backend 기존 Enum
ContraindicationType absolute, relative backend 기존 Enum
ConstraintType same_day, order, minimum_interval, prohibited 안전 엔진의 규칙 종류
PairSafetyLevel safe, caution, warning 현재 조합 매트릭스의 임상 주의 단계
ConstraintEnforcement advisory, required 권고와 강제 안전 gate 분리
SynergyType complementary, sequential, alternative 현재 함께, 다음에, 대신을 canonicalize
RecommendationPackageType tiered, complaint, optimization 추천 패키지 생성 방식
RecommendationTier entry, recommended, premium, vip 현재 추천 데이터의 닫힌 tier
PackageItemRole primary, adjunct, alternative 패키지 조립 방식
PihRiskLevel very_low, low, moderate, high, very_high PIH 위험도 정렬·표현
ComplicationSeverity mild, moderate, severe, critical 합병증 대응 분기
RecoveryReactionLevel normal, abnormal, caution, emergency 회복 반응 행동 분기
ContentStatus draft, in_review, approved, rejected, archived 콘텐츠 검수 수명주기
ReleaseStatus draft, published, superseded, cancelled release 수명주기
ImportMode preview, apply glossary에서 검증된 import 방식
RevisionEventType import, edit, publish, rollback 감사 이력 action
SourceArtifactType json, xlsx, paper, textbook 원본 parser 선택
ClaimVerificationStatus pending, verified, rejected, not_found 문헌 검증 처리 분기

Enum은 Python StrEnum을 canonical contract로 두고 TypeScript union을 동일 계약에서 생성한다. DB에는 PostgreSQL native ENUM 대신 String + CHECK를 사용한다. 값 변경 시 baseline과 API 계약을 함께 검증하면서도 migration을 불필요하게 경직시키지 않기 위해서다.

다음 값은 이름이 비슷해도 하나의 공통 Enum으로 합치지 않는다.

  • PihRiskLevel, ComplicationSeverity, RecoveryReactionLevel은 각각 위험 예측, 합병증 중증도, 환자 행동 지침으로 의미와 정렬 기준이 다르다.
  • PairSafetyLevel은 화면의 임상 주의 단계이고 ConstraintEnforcement는 엔진의 강제 수준이다. warning과 required를 같은 필드에 넣지 않는다.
  • 현재 JSON의 범용 type, level, status 필드는 import 단계에서 문맥별 이름으로 바꾼다. GlobalType, GlobalLevel, GlobalStatus는 만들지 않는다.
  • RecommendationTier는 현재 단일 서비스의 고정 동작이므로 Enum으로 시작한다. 병원별 명칭·순서·단계 커스터마이징이 실제 요구가 될 때만 master로 승격한다.

10.3 Const와 운영 설정으로 남길 값

현재 값 목표 위치 판단
MAX_SELECTION = 3 frontend product-policy const 비교 UI의 고정 선택 제한이며 임상 원장이 아님
Enum별 색상·아이콘·CSS class frontend presentation const 상태의 의미가 아니라 표현만 담당
메뉴 route·focus style·empty-state 문구 key frontend presentation const 데이터 정합성과 무관
ISO 국가 코드에 대응하는 flag frontend presentation const 국가명 원장을 별도 구축하지 않고 표준 코드를 사용
SUPPORTED_LANGS environment config LanguageCode 중 배포 환경에서 활성화할 부분집합
glossary 삭제 비율·건수 임계값, history limit backend operational config import 안전장치이며 임상 콘텐츠가 아님

반대로 다음 하드코딩은 Const로 남기지 않는다.

현재 하드코딩 이동 대상 이유
CATEGORY_NAMES ProcedureConcept + translation 화면마다 중복되고 canonical 시술명이 바뀔 수 있음
ORDER_RULES ProcedureConstraint 또는 CombinationRecipe 시술 순서를 결정하는 임상 규칙임
MOA_SYNERGY ProcedureSynergy + ClinicalClaim + citation 추천 점수와 근거를 바꾸는 임상 판단임
의료 단계의 순서·간격 ProcedureConstraint, RecipeItem 단순 표시 순서가 아니라 안전·치료 결과에 영향을 줌
제품 국가·허가 라벨 ProductRegulatoryStatus + translation/read model 검증일·출처·국가별 상태가 필요함

10.4 저장하지 않고 계산할 값

  • 병행 가능, 간격 필요, 주의 필요 라벨은 ProcedureConstraint에서 계산한다.
  • 추천 후보의 allowed_after_confirmation 같은 eligibility는 안전 gate 결과다.
  • 비교 테이블 행, 추천 개수, 누락 개수, 선택 개수는 조회 시 계산한다.
  • 위험 badge의 색상은 Enum→presentation const 매핑으로 결정한다.
  • 시술 A → 시술 B 표시 순서는 구조화된 constraint와 recipe에서 계산한다.

새 필드를 분류할 때는 다음 순서로 결정한다.

다른 데이터가 identity를 참조하고 운영자가 배포 없이 수정하는가? → Master
임상 설명·근거·규칙이며 검수와 복원이 필요한가?             → Versioned domain data
값 추가가 코드 분기와 API 계약을 바꾸는가?                   → Enum
환경마다 달라지는 임계값·활성 범위인가?                      → Operational config
화면 표현만 바꾸는가?                                        → Const
다른 원장과 규칙으로 재현 가능한가?                           → Derived read model

11. 현재 파일의 목표 엔티티 매핑

현재 원본 목표 소유 영역 주요 목표 엔티티
taxonomy 기준정보 + 임상 지식 ProcedureConcept, ProcedureAlias, Concern, Indication, KnowledgeDocument
products + taxonomy 제품 기준정보 Product, ProcedureProduct
extracted/* 임상 지식 SourceArtifact, KnowledgeDocument, ClinicalClaim
procedure-profiles 임상 지식 ProcedureProfile
literature-evidence + integrity 근거 ClinicalClaim, EvidenceSource, ClaimCitation, ClaimVerification
synergy 추천 관계 ProcedureSynergy
contraindications + treatment-sequence 안전 규칙 ProcedureConstraint, ConstraintEvidence
combo-recipes 추천 CombinationRecipe, RecipeItem
problem-combos + problem-packages 추천 Complaint, RecommendationPackage, PackageNarrative, PackageItem
treatment-optimization 추천·계획 RecommendationPackage, CarePlanTemplate, CarePlanStep
procedure-comparison 조회 projection ProcedureProfile + PricingObservation + ClinicalClaim에서 조립
anatomy-danger-zones 안전 AnatomicalSite, AnatomyHazard
complications + subcategories 안전 Complication, ProcedureComplication, RiskFactor, Protocol
pih-risk 안전 PihRiskRule
pre-procedure-checklist 진료 준비 ScreeningQuestion, ScreeningApplicability
consent-checklist 환자 안내 ConsentDisclosure
patient-education 환자 안내 KnowledgeDocument(audience=patient)
post-procedure-timeline 회복 RecoveryTimeline, RecoveryPhase, RecoveryReaction
market-pricing 가격 PricingObservation
i18n/*, glossary 번역 엔티티별 translation + GlossaryTerm/GlossaryTranslation
extraction-guardrails ingestion 설정 DB 임상 원장 제외; parser 설정으로 버전 관리

12. 저장하지 않을 것과 계산할 것

항목 처리
비교 화면의 완성된 행 저장하지 않고 profile·price·risk에서 계산
병행 가능/간격 라벨 constraint에서 계산
추천 후보의 안전 여부 추천 후 safety gate에서 계산
시술 순서 constraint와 recipe를 이용해 backend에서 계산
상담/의료 관점 노출 해당 audience 문서가 존재할 때만 노출
현재 pre-check 선택값 기본적으로 저장하지 않음
동의 체크 상태 전자서명 요구가 생기기 전까지 저장하지 않음
화면 색상·배지·정렬 프론트 표현 규칙으로 유지

판단 결과에는 최소한 content_release_id, engine_version, rule_refs, evidence_refs를 포함한다. LLM은 검증된 결과를 설명할 수 있지만 안전 등급·간격·순서를 결정하지 않는다.

13. 구현 결과

Task 1. 식별자와 단일 원장 확정 — 기술 mapping 118건과 문헌 identity·taxonomy validation 완료, claim 임상 승인 대기

목표: ProcedureConcept, Product, Concern, Indication의 canonical ID와 기존 alias 매핑을 확정한다.

예상 결과:

  • taxonomy/product 중복 제품의 병합표가 존재함
  • 기존 M/P-code와 외부 API public code가 보존됨
  • 해석되지 않는 시술·제품 텍스트 참조가 0건이거나 명시적 보류 목록에 있음

Task 2. 기준정보·revision baseline 구현 — 완료

목표: Catalog와 콘텐츠 revision/release/import 테이블을 먼저 구현한다.

예상 결과:

  • JSON/XLSX를 draft revision으로 가져올 수 있음
  • 검증·diff 후 하나의 release로 발행할 수 있음
  • 기존 live 데이터를 덮지 않는 preview가 존재함

Task 3. 안전 규칙 구조화 — 완료, legacy pair 27/27 parity

목표: contraindication·sequence·프론트 하드코딩을 ProcedureConstraint로 통합한다.

예상 결과:

  • 방향, 순서, 최소·최대 간격이 자유 문장 파싱 없이 조회됨
  • 동일 pair의 충돌 규칙이 import 단계에서 차단됨
  • safety 우선순위가 backend 테스트로 고정됨

Task 4. 추천·근거·안내 데이터 이관 — 완료, 미검증 근거 publication 차단

목표: 추천 패키지와 임상 claim·문헌·환자 안내를 canonical entity에 연결한다.

예상 결과:

  • 추천 항목의 시술·제품이 모두 FK로 연결됨
  • 추천 결과가 적용 규칙과 근거를 추적 가능함
  • 상담/의료/환자 설명이 동일 임상 사실을 중복 소유하지 않음

Task 5. API 전환과 migration squash — additive API와 단일 baseline 완료, legacy cutover 보류

목표: Next.js 파일 읽기를 FastAPI 조회로 전환하고 STG 직전 하나의 baseline migration을 만든다.

예상 결과:

  • 기존 CHARTY·Linky 응답 계약 parity 테스트가 통과함
  • 런타임 JSON 직접 읽기가 제거됨
  • STG용 단일 baseline migration과 import manifest가 존재함

14. 다음 운영 게이트

Mapping report, product drift report, pair conflict report를 importer와 proof script로 실측했고 본 ERD를 SQLAlchemy 모델과 단일 Alembic 0045 baseline으로 내렸다. 다음 단계는 설계 확장이 아니라 아래 운영 승인이다.

  1. 완료 — 시술 reference 3건의 canonical 위치를 결정적으로 확정했다.
  2. 완료 — 문헌 taxonomy mismatch와 PMID/identifier validation 78건을 NCBI 원본과 대조해 교정 또는 제외했다.
  3. 피부과 의사가 교정 source의 claim과 revision을 검수·승인한다.
  4. 최초 release를 발행한 뒤 natural-language retrieval 평가를 수행한다.