콘텐츠로 이동

[설계 5] 상품 BOM은 어떻게 AppointmentPlan과 방문 계획으로 이어지는가

구매 이벤트, 상품 BOM, 예약 계획 리비전, 방문 항목과 자원 점유를 하나의 업무 흐름으로 연결하는 작은 로봇 작업자들
상품 BOM이 생겼다고 달력에 방문이 바로 생기지는 않습니다. 구매 계약을 계획으로 보존하고, 그 계획에서 방문 후보와 항목별 자원을 다시 계산해야 합니다.

앞 글에서 환자 A의 패키지는 상담·검사·시술·사후 점검이 연결된 실행 그래프가 되었습니다. 이번 글에서는 그 그래프가 예약 서비스 안에서 실제 모델로 어떻게 전개되는지 따라가 보겠습니다.

가장 먼저 구분할 것은 네 가지 사실입니다.

구매 완료 이벤트 ≠ 상품 BOM 이벤트 ≠ AppointmentPlan 리비전 ≠ 예약 확정

구매가 일어났다는 사실은 계약의 시작입니다. 상품 BOM은 그 계약을 어떤 진료 항목과 순서로 수행할지 고정한 입력입니다. AppointmentPlan은 구매 한 건의 이행 의무를 보존하고, Appointment는 환자가 병원에 한 번 방문하는 물리적 단위를 표현합니다. 특정 시간에 방문하기로 합의한 사실은 예약 정보로 남습니다.

환자 A를 두 개의 이벤트로 따라가기

섹션 제목: “환자 A를 두 개의 이벤트로 따라가기”

환자 A가 상담 → 검사 → 본 시술 → 사후 점검 패키지를 구매했다고 하겠습니다. 실제 상품명과 병원 정책은 공개하지 않고, 예약 업무에 필요한 구조만 사용합니다.

시점환자 A의 행위 또는 이벤트업무 의미예약 서비스가 보존하는 것
T1패키지를 구매하고 결제를 완료한다구매 계약이 생김sourcePurchaseAuthority, 구매 ID, 병원 범위, 구매 당시 상품 버전
T2구매 완료 이벤트가 예약 서비스에 도착한다구매 계약을 예약 계획의 뿌리로 연결함AppointmentPlan, 카탈로그 버전·payload 해시, 반복 항목과 의존 관계
T3상품·구매 서비스가 선택과 반복을 전개한 상품 BOM을 발행한다“어떤 항목을 수행할 것인가”가 고정됨PackageExecutionSnapshot, 구성 상품 버전, 선택 결과, snapshotHash
T4상품 BOM 이벤트를 수렴한다기존 Plan에 새로운 실행 리비전을 추가함PlannedTreatment, 의존성, 방문 묶음 제약, 리비전 이력
T5가능한 방문 후보를 계산한다여러 항목을 한 번에 묶을지 나눌지 판단함제안 시간, 항목별 자원 후보, 적용 정책 기록
T6환자와 병원이 일정에 동의한다특정 시간·항목·자원으로 예약을 확정함Appointment, AppointmentItem, ResourceAllocation, 예약 상태
T7일부 항목이 완료되거나 일정이 깨진다완료한 사실과 남은 작업을 분리함완료 근거 이력, 새 attempt, 후속 제안 이력

T2와 T4를 하나의 저장 작업으로 뭉치면 구매 계약과 상품팀이 전개한 실행 계약의 책임이 섞입니다. T6까지 한 번에 처리하면 “계획이 생성되었다”는 사실을 예약 확정으로 잘못 읽게 됩니다.

authority는 역할이 아니라 기준 데이터의 소유자다

섹션 제목: “authority는 역할이 아니라 기준 데이터의 소유자다”

이 흐름에서 authority는 로그인 사용자의 역할이나 권한 등급을 뜻하지 않습니다. 특정 정보를 발행하고 책임지는 기준 데이터 원본 시스템을 가리키는 식별자입니다. 같은 문자열의 구매 ID나 상품 ID가 여러 병원과 서비스에 존재할 수 있으므로, ID만으로는 출처를 식별할 수 없습니다.

식별 범위대표 필드답하는 질문
구매 사실tenantGroupId + clinicId + sourcePurchaseAuthority + sourcePurchaseId이 구매 계약은 어느 서비스가 어느 병원 범위에서 소유하는가?
상품 기준 데이터catalogSourceAuthority + productId + catalogVersion어떤 상품 소유자가 어떤 버전을 발행했는가?
실행 계약packageProductVersionId + 선택된 구성 상품 버전 + snapshotHash구매 당시 어떤 BOM을 실행하기로 고정했는가?
예약 확정proposalId + 리비전 + 제안 해시 + 동의 증빙환자와 병원이 정확히 어떤 조건에 합의했는가?

따라서 sourcePurchaseId=42만으로 Plan을 찾지 않습니다. 테넌트·병원·기준 데이터 원본이 먼저 맞아야 하고, 같은 기준 데이터 원본 안에서도 다른 payload가 오면 재생이 아니라 충돌 또는 격리 대상이 됩니다. 이것이 상품 개발부와 구매 서비스가 보낸 기준 데이터를 예약 서비스가 임의로 다시 해석하지 않는 첫 번째 이유입니다.

첫 번째 전개: 구매 이벤트에서 AppointmentPlan으로

섹션 제목: “첫 번째 전개: 구매 이벤트에서 AppointmentPlan으로”

현재 기반 구현에서 PurchaseCompletedHandler는 구매 완료 이벤트를 받아 활성 카탈로그 프로젝션을 확인하고, AppointmentPlanFactoryPlan aggregate(집합 루트) 초안을 만듭니다. 이 factory(생성기)는 I/O를 수행하지 않는 결정적 계산기이며, 예약 시간을 확정하지 않습니다.

Plan 루트에는 다음 사실이 함께 들어갑니다.

보존하는 값의미
sourcePurchaseAuthority, sourcePurchaseIdPlan을 만든 구매 사실의 원본 범위
catalogSourceAuthority, productId, catalogVersionPlan 생성에 사용한 상품 기준 데이터
catalogPayloadHash동일 카탈로그 기준 데이터로 재생할 수 있는지 확인하는 해시
bookingPreference구매 시점에 전달된 고객 희망 정보. 확정 시간이 아님
PlannedTreatmentBOM 항목을 반복 횟수만큼 펼친 이행 의무
TreatmentDependency항목 사이의 선행·후행 관계와 간격

카탈로그의 repeatCount=5는 다섯 개의 PlannedTreatment 회차로 확장됩니다. 각 회차는 sequenceNo, 대표 진료명, 세부 코드, 예상 시간, 담당자 역량, 장비와 공간 요구를 가집니다. 패키지 전체를 “총 180분”으로만 저장하지 않는 이유입니다. 상담실과 시술실, 회복 공간은 서로 다른 시간과 자원을 점유할 수 있기 때문입니다.

이 단계의 결과는 ACTIVE 상태인 Plan과 PLANNED 상태의 이행 의무입니다. 아직 Appointment도, 자원 선점도, 고객 동의도 아닙니다.

두 번째 전개: 상품 BOM에서 Plan 리비전으로

섹션 제목: “두 번째 전개: 상품 BOM에서 Plan 리비전으로”

패키지 상품의 선택과 반복은 상품 관리 서비스 또는 구매 서비스가 구매 시점에 전개합니다. 예약 서비스가 나중에 원본 상품을 재귀 조회해 “현재 최신 상품”으로 다시 조립하지 않습니다. 발행자가 보낸 실행 계약이 PackageExecutionSnapshot입니다.

이 실행 계약은 다음 내용을 한 덩어리의 불변 입력으로 묶습니다.

실행 계약의 구성예약에서 필요한 이유
구매한 패키지와 구성 상품의 정확한 버전이후 카탈로그 변경이 기존 구매를 다시 쓰지 않게 함
선택된 구성 상품과 수량M-of-N 선택 결과와 반복 횟수를 구매 계약에 고정함
완전히 전개된 ExecutionTreatment준비·진료·회복 시간과 자원 근거 이력을 항목별로 보존함
BLOCKING·NON_BLOCKING 의존성실제 완료 사실이 후속 항목에 미치는 범위를 구분함
MUST_SAME_VISIT·MAY_SAME_VISIT·MUST_SEPARATE_VISIT실행 의존성과 다른 방문 묶음 축을 보존함
snapshotHash같은 원본 버전의 payload가 같은 실행 계약인지 검증함
패키지 상품의 상담·검사·본 시술·진정 관리·사후 점검 항목이 상품 BOM 기준 데이터, AppointmentPlan 리비전, PlannedTreatment, AppointmentItem과 실제 방문으로 이어지는 구조
상품 BOM은 구매 시점의 구성 계약이다. 예약 서비스는 그 계약을 다시 해석하지 않고 계획과 항목별 방문 단위로 전개한다.

PackageExecutionPlanner는 이 실행 계약의 선택 수, 반복 수, 항목 수, 간선 수, 근거 이력, 참조 유효성, 순환 여부를 검증합니다. 검증이 끝나면 실행 계약의 항목과 관계를 AppointmentPlanRevisionDraft로 복사합니다. 예약 서비스가 “이 상품은 진정 관리가 포함되어 있으니 이 순서가 맞다”고 상품 의미를 새로 결정하는 코드가 아닙니다.

실행 이벤트를 처리하는 VisitPlanningEventHandlerauthority로 한정한 구매 범위에서 기존 Plan을 찾고, 통과한 초안을 새 리비전으로 추가합니다. 같은 원본 버전과 같은 payload(입력 데이터) 해시를 다시 받으면 DUPLICATE로 수렴하고, 같은 버전의 다른 해시나 순서가 맞지 않는 이벤트는 성공으로 위장하지 않고 격리하거나 WAITING_GAP 대기로 남깁니다. 인박스(inbox)·리비전 행·아웃박스(outbox)·처리 완료 표시는 같은 트랜잭션 경계에서 함께 결정됩니다.

이 구간의 핵심은 “중복 이벤트를 어떻게 막는가”보다 어떤 실행 계약이 같은 구매의 다음 리비전인지 판정하는가입니다. 중복 방지는 그 계약을 안전하게 재처리하기 위한 조건입니다.

Plan과 방문 사이에 AppointmentItem이 필요한 이유

섹션 제목: “Plan과 방문 사이에 AppointmentItem이 필요한 이유”

승인된 모델의 관계는 다음과 같습니다.

Purchase 1 ──> AppointmentPlan 1 ──> PlanRevision 1..N
└─> PlannedTreatment N
Appointment 1 ──> AppointmentItem N ──> PlannedTreatment 1
└──────────> ResourceAllocation N

AppointmentPlan은 구매 한 건의 전체 이행 의무를 보존합니다. PlannedTreatment는 그 의무를 실행 가능한 항목으로 펼친 결과입니다. Appointment는 환자가 한 번 방문하는 단위이고, AppointmentItem은 그 방문에서 특정 PlannedTreatment를 수행하거나 시도한 기록입니다.

따라서 한 방문에 여러 AppointmentItem이 들어갈 수 있습니다. 반대로 하나의 패키지 Plan이 여러 방문으로 나뉠 수도 있습니다. 한 환자의 다른 Plan에서 온 항목도 임상적으로 호환되고 자원이 양립하면 같은 방문에 묶을 수 있도록 모델의 직접 1:N 관계를 피합니다.

방문 묶음은 의존성과 별도의 제약 축이다

섹션 제목: “방문 묶음은 의존성과 별도의 제약 축이다”

환자 A의 실행 항목을 다음처럼 생각해 보겠습니다.

실행 항목실행 의존성방문 묶음 힌트가능한 방문
사전 상담없음검사와 MUST_SAME_VISIT 가능방문 1
사전 검사상담 완료 후상담과 MUST_SAME_VISIT 또는 별도방문 1 또는 2
본 시술검사 완료 후 BLOCKING진정 관리와 MAY_SAME_VISIT방문 2
진정 관리본 시술과 자원 양립 필요본 시술과 MAY_SAME_VISIT방문 2
사후 점검본 시술의 실제 완료 후 간격 필요본 시술과 MUST_SEPARATE_VISIT방문 3

BLOCKING은 선행 항목의 실제 완료가 후행 항목의 가능 범위를 제한한다는 뜻입니다. MUST_SAME_VISIT은 같은 방문 연결요소를 강제하고, MAY_SAME_VISIT은 최적화기가 선택할 수 있는 힌트로 남습니다. MUST_SEPARATE_VISIT이나 자원 비호환 관계가 같은 연결요소 안에서 발견되면 후보를 억지로 만들지 않고 거부합니다.

그래서 환자 A에게 가장 자연스러운 결과는 상담·검사를 방문 1, 본 시술·진정 관리를 방문 2, 사후 점검을 방문 3으로 제안하는 것입니다. 병원의 자원과 안전 규칙이 허용하면 방문 1과 방문 2를 합칠 수 있지만, 각 항목의 준비·진료·회복 시간과 자원 요구는 계속 별도로 남습니다.

패키지 구성과 상품 BOM의 차이는 패키지 상품 구성 보조 자료에서, BOM이 계획과 방문 후보로 전개되는 모양은 상품 BOM의 예약 전개 흐름에서 확인할 수 있습니다. 보조 자료는 승인된 설계 Markdown과 원본 저장소를 대신하지 않는 설명 자료입니다.

AppointmentItemResourceAllocation은 실제 방문을 설명한다

섹션 제목: “AppointmentItem과 ResourceAllocation은 실제 방문을 설명한다”

한 번의 방문에 상담과 검사를 함께 넣었다면 고객 화면에는 “상담·검사 방문”처럼 보일 수 있습니다. 그러나 예약 서비스는 내부적으로 두 AppointmentItem과 각 항목의 ResourceAllocation을 보존해야 합니다.

모델보존하는 업무 사실
Appointment환자가 병원에 한 번 방문하는 시간·상태 단위
AppointmentItem어떤 Plan 리비전의 어떤 진료 의무를 몇 번째 시도로 수행했는가
ResourceAllocation담당자·장비·진료 공간·수용량 버킷을 언제 얼마나 점유하는가
AppointmentCommitment제안이 PROPOSED·HELD·CONFIRMED 중 어디까지 고객과 병원에 합의되었는가

CONFIRMED는 구체적인 제안 버전, 제안 해시, 자원 점유와 해당 제안에 대한 고객 동의가 결합된 예약 확정 상태입니다. 새 후보가 생겼다는 이유만으로 기존 예약의 confirmedProposalId와 자원 점유를 먼저 비우지 않습니다. 새 후보가 실패하거나 고객이 거부하면 기존에 확정한 예약의 일시를 보호해야 하기 때문입니다.

이 구분은 병원 운영에도 중요합니다. 상담은 끝났지만 검사가 장비 문제로 중단될 수 있고, 본 시술은 완료했지만 사후 점검은 아직 남을 수 있습니다. 방문 전체를 하나의 완료 플래그(flag)로 바꾸면 다음 예약과 고객 상담 서비스가 어느 의무가 남았는지 알 수 없습니다.

일부 완료와 상품 변경은 과거를 다시 쓰지 않는다

섹션 제목: “일부 완료와 상품 변경은 과거를 다시 쓰지 않는다”

환자 A의 방문 2에서 본 시술은 완료했지만 진정 관리가 중단되었다고 하겠습니다. 예약 서비스는 다음 순서로 처리합니다.

  1. 완료된 AppointmentItem과 임상 근거 이력을 동결합니다.
  2. 완료되지 않은 PlannedTreatment만 남은 작업으로 계산합니다.
  3. 필요한 경우 새 AppointmentItem 시도(attempt)를 만들고, 선행 완료 시각을 기준으로 후속 허용 기간을 계산합니다.
  4. 새 제안과 자원 점유를 만든 뒤 환자 동의를 받아야 CONFIRMED가 됩니다.

상품 버전이 바뀌는 경우도 같습니다. 신규 구매는 새 버전을 사용할 수 있지만, 이미 구매한 환자 A의 Plan과 완료된 항목을 최신 카탈로그로 다시 전개하지 않습니다. 상품팀이 전환표와 고객 동의를 발행한 예외적인 경우에만 같은 Plan의 새 리비전을 만들고, 미진행 미래 항목만 승계합니다. 과거 리비전과 완료 사실은 그대로 남깁니다.

서비스별 책임은 Plan과 방문의 경계에서 나뉜다

섹션 제목: “서비스별 책임은 Plan과 방문의 경계에서 나뉜다”

상품 BOM이 예약 서비스 안에 들어왔다고 해서 상품이나 임상 결과의 소유권까지 넘어오는 것은 아닙니다.

상품 관리·상품 개발과 커머스·구매가 상품 BOM과 구매 계약을 예약 서비스의 AppointmentPlan·예약·AppointmentItem으로 전달하고, 임상·CRM·알림 서비스가 방문 사실을 소비하는 책임 경계
예약 서비스는 시간·자원·동의·상태를 소유한다. 임상 완료, CRM 정책, 알림·통계는 각각의 서비스가 책임진다.
서비스기준 데이터 또는 원본 사실예약 서비스와의 경계
상품 관리·상품 개발상품 버전, 구성 그래프, BOM, 선택·반복 규칙검증된 프로젝션과 상품 BOM 근거 이력을 발행함
구매·커머스구매 계약, 선택 결과, 환불, 구매 원본 ID구매 완료·환불 이벤트를 발행하고 계약 소유권을 유지함
예약 서비스Plan, 리비전, 제안, 예약, 항목(item), 자원 배정(allocation)시간·수용량·동의·상태·예약 이력을 소유함
임상·시술실제 시작·완료·부분 완료완료 근거를 발행하고 임상 결과를 소유함
고객 상담·CRM고객 프로필(profile), 상담, 민원, 보상, 서비스 등급 판단노쇼·지연·재예약 같은 객관적 사실을 상담 판단에 사용함
알림·통계 소비자연락 동의, 발송 이력, 프로젝션, 지표예약 트랜잭션 밖에서 아웃박스(outbox)와 조회 모델을 소비함

따라서 노쇼가 반복된 환자에게 다음 예약의 조건을 다르게 제안하거나, VIP 고객에게 우선순위를 주는 정책이 필요하더라도 예약 서비스가 임의로 영구 예약 제한 명단을 만들거나 예약을 강제 취소하지 않습니다. CRM과 병원 운영 정책이 기준과 근거를 정의하고, 예약 서비스는 기존에 확정한 예약의 일시와 이미 점유한 의료 자원을 보호하면서 그 판단에 필요한 입력과 결과를 기록합니다. 구체적인 공정성 정책은 후속 글의 범위입니다.

현재 구현과 승인된 설계를 구분해 읽기

섹션 제목: “현재 구현과 승인된 설계를 구분해 읽기”

이 글에서 모델 이름이 등장한다고 해서 모든 경로가 운영 배포를 끝냈다는 뜻은 아닙니다.

사실성 표지이번 글에서 의미하는 범위
현재 구현카탈로그 프로젝션, 구매 완료 이벤트에서 Plan을 만드는 생성기(factory), PackageExecutionEvent 수신·검증·리비전 추가(append), 중복·오래된·간격 누락·격리 처리와 관련 테스트에서 확인한 계약
승인된 설계한 방문의 여러 AppointmentItem, 항목별 ResourceAllocation, 제안·동의, 예약 상태와 부분 완료 후속 흐름
운영 대기브로커 전송 경로(transport), 임상·커머스 실제 이벤트 연동, 아웃박스(outbox) 카나리, 운영 백필·재생·복구 검증
로드맵환자 채널에서 후속 후보를 선택하는 화면, 구체적인 노쇼 페널티와 VIP 우선순위 정책

이 글은 현재 구현과 승인된 설계를 구분해 설명합니다. 이미 저장된 계획과 고객 동의를 거쳐 예약을 확정하는 단계는 서로 다릅니다.

Plan과 방문 모델이 준비되었다고 환자 A가 원하는 내원 날짜에 바로 예약이 확정되는 것은 아닙니다. 다음 글 고객 희망 내원 날짜는 예약 확정이 아니다에서는 고객 희망 내원 날짜, 병원 제안, 자원 HELD, 고객 동의를 거쳐 CONFIRMED에 이르는 순서를 살펴보겠습니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.