[구현 9] 패키지 상품의 실행 순서와 선택 조건을 방문 계획에 담는 과정

선택형 패키지는 구성 항목 목록만으로 설명할 수 없다
섹션 제목: “선택형 패키지는 구성 항목 목록만으로 설명할 수 없다”환자 A가 맞춤 피부 관리 패키지를 구매한다고 해보겠습니다. 피부 진단은 필수이고, 진단 뒤에는 레이저 토닝, 수분 집중 관리, 진정 마스크 관리 중 두 가지를 고릅니다. 환자 A는 레이저 토닝과 진정 마스크 관리를 선택했습니다.
이 구매에서 확정되는 정보는 세 종류입니다.
| 구분 | 환자 A의 구매에서 확정된 내용 | 예약에서 필요한 이유 |
|---|---|---|
| 상품 구성 | 피부 진단은 필수, 관리 후보 3개 중 2개 선택 | 선택 결과가 상품 규칙을 충족하는지 확인 |
| 구매 결과 | 레이저 토닝과 진정 마스크 관리 선택 | 선택하지 않은 수분 집중 관리를 계획에 넣지 않음 |
| 실행 조건 | 진단이 먼저이며, 일부 관리는 같은 방문에 배치할 수 있음 | 실행 순서와 방문 구성을 각각 계산 |
예약 서비스가 상품 구성 항목만 다시 읽어 구매를 해석하면 이 세 사실이 섞입니다. 상품 카탈로그에는 후보 목록이 있지만 환자가 무엇을 골랐는지는 없습니다. 현재 상품 정의에는 최신 버전이 있지만 구매 당시 어떤 버전을 적용했는지는 없습니다. 항목 이름만으로는 어떤 진료가 먼저 끝나야 하는지, 어떤 항목을 같은 방문에 배치해도 되는지도 알 수 없습니다.
더 큰 문제는 시간이 지난 뒤에 드러납니다. 구매가 끝난 다음 레이저 토닝의 준비 시간이나 필요한 장비가 바뀌었다고 해보겠습니다. 예약 서비스가 매번 최신 카탈로그를 조회해 구매를 다시 해석하면, 이미 판매한 패키지의 실행 조건까지 새 정의로 바뀝니다. 환자가 고른 항목은 그대로인데 예약 서비스가 다른 버전의 시간과 자원 조건을 적용하는 상황이 생길 수 있습니다.
그래서 상품·구매 서비스는 선택과 반복을 이미 전개한 PackageExecutionSnapshot을 보냅니다. 이 실행 기준 데이터는
예약 서비스가 상품 의미를 다시 계산하기 위한 재료가 아니라, 구매 시점의 실행 계약입니다.
| 실행 계약에 들어가는 정보 | 이 정보가 필요한 이유 |
|---|---|
| 패키지 상품 ID·버전·해시 | 어떤 상품 정의로 구매를 해석했는지 고정 |
| 선택한 구성 상품 ID·정확한 버전·수량 | 나중에 최신 버전으로 바뀌지 않게 보존 |
| 선택군과 필수 선택 수 | 3개 중 2개처럼 선택 조건을 검증 |
| 전개된 진료 항목과 근거 이력 | 각 항목이 어느 구성 상품 버전에서 왔는지 확인 |
| 실행 의존성과 방문 묶음 조건 | 실행 순서와 같은 방문 가능 여부를 서로 다른 규칙으로 보존 |
여기서 snapshotHash는 화면에 표시할 부가 정보가 아니라 발행자가 계산한 실행 계약의 식별 해시입니다. 이벤트
처리 단계에서는 이 값과 모든 페이로드 필드를 포함해 계산한 표준 해시를 원본 버전과 함께 비교합니다. 같은
원본 버전인데 표준 해시가 다르면 단순 재전송으로 처리하지 않고 계약 충돌로 격리해야 합니다.
본문의 피부 관리 이름은 승인된 업무 설계 사례입니다. 현재 테스트는 미백 진료 5회와 필링 진료 1회를 선택한 패키지를 사용합니다. 사례의 이름과 수량은 다르지만, 후보 3개 중 2개 선택, 정확한 구성 상품 버전, 진료 항목의 근거 이력, 실행 의존성과 방문 분리 조건을 같은 모델로 검증합니다.
예약 서비스는 검증한 실행 계약을 AppointmentPlan(이하 Plan)의 새 불변 리비전으로 보존합니다. 이 글에서
Plan은 방문 가능 시간을 계산한 결과가 아니라, 구매에 따라 생긴 진료 의무와 관계를 누적하는 방문 계획입니다.
실행 기준 데이터는 선택 결과, 실행 항목, 관계를 나누어 보존한다
섹션 제목: “실행 기준 데이터는 선택 결과, 실행 항목, 관계를 나누어 보존한다”PackageExecutionSnapshot을 하나의 큰 JSON으로만 보면 각 필드가 왜 필요한지 알기 어렵습니다. 예약
서비스의 관점에서는 세 범주로 나누어 읽는 편이 이해하기 쉽습니다.
1. 어떤 구성 상품을 구매했는가
섹션 제목: “1. 어떤 구성 상품을 구매했는가”selectedComponentVersions에는 실제 선택한 구성 상품만 들어갑니다. 각 항목은 안정적인 상품 ID, 구매 당시의
정확한 버전, 반복 수량, 선택군 ID를 가집니다. 후보였지만 선택하지 않은 상품은 포함하지 않습니다.
예를 들어 환자 A가 레이저 토닝과 진정 마스크 관리를 골랐다면 두 구성 상품의 버전만 들어갑니다. 수분
집중 관리는 선택 후보였다는 사실만 componentSelections의 후보 수로 남고, 실행 항목의 출처가 될 수는
없습니다.
2. 실제로 몇 번의 진료 의무가 생겼는가
섹션 제목: “2. 실제로 몇 번의 진료 의무가 생겼는가”expandedTreatmentItems는 선택과 반복 횟수를 모두 펼친 결과입니다. 구성 상품의 수량이 5라면 수량 5를
카운터 하나로만 두지 않고 sequence가 1부터 5인 실행 항목 다섯 개로 전달합니다. 각 실행 항목에는 다음
정보가 개별적으로 남습니다.
- 실행 계약 안에서 유일한
treatmentKey - 출처가 된 구성 상품 ID와 정확한 버전
- 원본 BOM 항목 ID와 반복 회차
- 표시할 진료명과 세부 진료 코드
- 준비·진료·회복 시간
- 담당자 역량, 장비 유형, 공간 조건
이렇게 전개해야 같은 패키지 안에서도 항목별 시간과 자원 조건을 유지할 수 있습니다. 패키지 전체 시간을 한 숫자로 합치면 어떤 장비가 어느 진료에 필요한지, 어느 회차가 완료됐는지 추적할 수 없습니다.
3. 항목 사이에 어떤 관계가 있는가
섹션 제목: “3. 항목 사이에 어떤 관계가 있는가”executionDependencies는 선행 항목과 후행 항목을 방향이 있는 관계로 연결합니다. 반면
visitGroupingConstraints는 두 항목을 같은 방문에 배치할 수 있는지 나타내며 방향이 없습니다. 두 목록을
분리해 두어야 “먼저 끝나야 한다”와 “같은 날 진행할 수 있다”를 독립적으로 판단할 수 있습니다.
예약 서비스는 실행 계약을 다섯 단계로 검증한다
섹션 제목: “예약 서비스는 실행 계약을 다섯 단계로 검증한다”PackageExecutionPlanner는 상품 카탈로그를 조회하지 않습니다. 외부에서 이미 전개한 계약이 구조적으로
완전한지 다음 순서로 확인합니다. 각 검사는 서로 다른 오류를 막습니다.
| 단계 | 확인하는 내용 | 거부해야 하는 예 |
|---|---|---|
| 처리 상한 | 구성 상품 반복 수량, 전개된 진료 항목 수, 관계 수 | 수량 101, 진료 항목 501개, 관계 4,001개 |
| 구성 상품 | (상품 ID, 버전 ID) 조합이 중복되지 않는지 | 같은 구성 상품 버전이 두 번 포함됨 |
| 선택 결과 | 선택군 ID가 유일하고 실제 선택 수가 필수 선택 수와 정확히 같은지 | 3개 중 2개를 골라야 하는데 1개만 전달됨 |
| 근거 이력과 참조 | 모든 진료가 선택한 정확한 구성 상품 버전에서 왔고 관계가 실제 진료를 가리키는지 | 선택하지 않은 필링 버전을 진료 출처로 사용 |
| 그래프 구조 | 실행 의존성에 순환이 없는지 | care-1 → care-2 → care-1 |
기본 상한은 구성 상품 하나의 반복 수량 100, 전체 진료 항목 500개, 실행 의존성과 방문 묶음 관계를 합해 4,000개입니다. 이 값은 “병원 상품은 반드시 100회 이하여야 한다”는 상품 정책이 아닙니다. 동기식 플래너가 한 번의 요청에서 안전하게 검증할 계산량을 제한하는 운영 상한입니다. 더 큰 계약을 처리해야 한다면 일부를 잘라 성공으로 반환할 것이 아니라, 발행 범위를 나누거나 별도의 비동기 계획 경로를 마련해야 합니다.
검증 순서도 중요합니다. 선택 수가 맞더라도 진료 항목이 선택하지 않은 구성 상품 버전을 참조하면 거부합니다. 모든 참조가 존재하더라도 실행 의존성이 순환하면 거부합니다. 한 단계의 성공으로 다른 단계의 정합성을 대신하지 않습니다.
검증을 모두 통과해야 Plan 리비전 초안을 만듭니다. 코드의 핵심 경계도 짧습니다.
fun plan(snapshot: PackageExecutionSnapshot): AppointmentPlanRevisionDraft { validateLimits(snapshot) validateComponents(snapshot) validateTreatments(snapshot) validateSelections(snapshot) validateRelations(snapshot) validateAcyclic(snapshot.executionDependencies)
return AppointmentPlanRevisionDraft( packageProductId = snapshot.packageProductId, packageProductVersionId = snapshot.packageProductVersionId, sourceSnapshotHash = snapshot.snapshotHash, treatments = snapshot.expandedTreatmentItems.toList(), dependencies = snapshot.executionDependencies.toList(), visitGroupingConstraints = snapshot.visitGroupingConstraints.toList(), )}이 코드는 방문 시간을 계산하지 않습니다. 환자의 희망 날짜를 읽지 않고 의료진·장비·공간의 실제 수용량도
조회하지 않습니다. 검증한 패키지 버전, 실행 기준 데이터 해시, 실행 항목과 관계를 AppointmentPlanRevisionDraft로
복사할 뿐입니다. 이후 핸들러가 이 초안을 기존 Plan의 새 리비전과 하위 행으로 저장합니다.
따라서 플래너가 성공했다는 말은 “이 실행 계약을 방문 계획에 기록해도 구조적으로 안전하다”는 뜻입니다. “이 날짜에 방문할 수 있다”거나 “예약이 확정됐다”는 뜻은 아닙니다.

실제 제품 화면을 캡처한 것이 아니라 현재 구현의 데이터를 바탕으로 구성한 운영 화면 시안입니다. 운영자는 구매 당시 선택한 구성과 정확한 상품 버전, 실행 관계, 검증 결과와 저장된 Plan 리비전을 한 화면에서 대조할 수 있습니다. 상품 구성 단계는 패키지 상품 구성 시각 자료에서, 실행 계약이 방문 계획으로 이어지는 과정은 상품 BOM에서 방문 계획까지의 흐름에서 확인할 수 있습니다.

실행 순서와 방문 묶음은 다른 판단이다
섹션 제목: “실행 순서와 방문 묶음은 다른 판단이다”리프팅 집중 관리 패키지를 비교하면 두 규칙의 차이가 더 분명해집니다.
| 항목 | 실행 의존성 | 방문 묶음 조건 |
|---|---|---|
| 사전 진단 v3 | 선행 항목 없음 | 리프팅 시술과 별도 방문 |
| 리프팅 시술 v5 | 진단 완료 후 3~14일 | 진정 관리와 같은 방문 가능 |
| 진정 관리 v4 | 시술 뒤 진행하는 항목 | MAY_SAME_VISIT |
| 사후 점검 v2 | 시술 완료 후 7일 | MUST_SEPARATE_VISIT |
실행 의존성은 무엇이 먼저 끝나야 하는가를 말합니다. 방문 묶음 조건은 서로 다른 진료 항목을 같은 방문에
배치해도 되는가를 말합니다. MAY_SAME_VISIT는 함께 배치할 수 있다는 뜻이지, 플래너가 같은 방문으로
확정했다는 뜻이 아닙니다. 실제 자원과 환자 동의를 반영한 방문 후보 계산은 후속 단계가 담당합니다.
두 규칙은 데이터 모양도 다릅니다.
| 구분 | 실행 의존성 | 방문 묶음 조건 |
|---|---|---|
| 방향 | 선행 → 후행 | 방향 없는 두 항목의 쌍 |
| 대표 값 | BLOCKING, NON_BLOCKING | MUST_SAME_VISIT, MAY_SAME_VISIT, MUST_SEPARATE_VISIT |
| 시간 정보 | 최소·권장·최대 간격 | 간격 없음 |
| 후속 판단 | 선행 완료 시각을 기준으로 후행 가능 구간 계산 | 한 방문에 함께 넣을 후보 조합 결정 |
BLOCKING은 선행 완료 시각이 후행 예약 가능 범위를 결정하고, 선행 상태가 바뀌면 후속 계산 범위까지
전파해야 하는 관계입니다. NON_BLOCKING은 관계를 기록하되 선행 변경만으로 후행 항목을 자동 보류하거나
재계산하지 않습니다.
방문 묶음 조건도 이름 그대로 적용해야 합니다. MUST_SAME_VISIT라도 필요한 장비와 공간을 동시에 확보하지
못하면 유효한 방문 후보가 생기지 않습니다. MAY_SAME_VISIT는 최적화 후보일 뿐이므로 별도 방문도 유효할 수
있습니다. MUST_SEPARATE_VISIT는 자원이 남아 있어도 두 항목을 같은 방문에 넣지 못하게 합니다.
이 차이를 무시하고 하나의 dependsOn 필드로 합치면, 선행 진료를 마친 뒤 다른 날 와야 하는 조건과 같은 날
연속으로 진행할 수 있는 조건을 구분할 수 없습니다.
구매 완료와 패키지 실행 계획은 별도 이벤트로 들어온다
섹션 제목: “구매 완료와 패키지 실행 계획은 별도 이벤트로 들어온다”구매 완료 이벤트와 패키지 실행 이벤트는 같은 일이 아닙니다. PurchaseCompletedHandler는 구매 사실을 받아
기존 AppointmentPlan을 만듭니다. 선택과 실행 조건은 별도의 PackageExecutionEvent가 도착했을 때
VisitPlanningEventHandler가 그 Plan의 새 리비전으로 추가합니다.
이벤트를 나눈 이유는 단순히 클래스를 분리하기 위해서가 아닙니다. 구매 완료는 “이 구매에 대응하는 계획이 존재한다”는 기준을 만듭니다. 패키지 실행 이벤트는 “그 계획에 어떤 실행 계약을 추가할 것인가”를 전달합니다. 두 이벤트의 도착 시점과 재전송 횟수가 달라도 각각의 결과가 하나의 Plan으로 수렴해야 합니다.
VisitPlanningEventHandler는 다음 순서로 처리합니다.
- 이벤트 메타데이터와 페이로드 크기·식별자 형식을 검증한다.
- 테넌트와 병원의 소속 관계를 확인한다.
- 구매 기준 데이터 원본과 구매 ID로 기존 Plan을 조회하고 루트 행을 잠근다.
- 실행 계약의 패키지 상품 ID가 Plan의 상품 ID와 같은지 확인한다.
- 원본 버전과 표준화한 페이로드 해시로 중복·버전 누락·충돌을 판정한다.
- 순서가 유효할 때만
PackageExecutionPlanner를 호출한다. - 검증된 초안을 새 불변 리비전으로 추가하고 결과를 기록한다.
따라서 PurchaseCompletedHandler가 PackageExecutionPlanner를 직접 호출한다고 그리거나 설명하면 현재
구현과 다릅니다. Plan 생성과 실행 계약 반영 사이에는 별도의 이벤트, 버전 판정, 재처리 경계가 있습니다.

최종 상태는 중복, 대기, 격리, 리비전 추가로 나뉜다
섹션 제목: “최종 상태는 중복, 대기, 격리, 리비전 추가로 나뉜다”이벤트 처리는 성공과 실패 두 가지로만 끝나지 않습니다. 운영자가 다음 작업을 판단하려면 왜 쓰지 않았는지까지 구분해야 합니다.
| 최종 상태 | 대표 조건 | 저장 결과 | 운영상 다음 작업 |
|---|---|---|---|
DUPLICATE | 같은 이벤트 ID 또는 같은 원본 버전·같은 해시 | 리비전과 아웃박스를 다시 만들지 않음 | 정상 재전송으로 종료 |
WAITING_GAP | 로컬 버전이 1인데 버전 4가 먼저 도착 | 재시도 시각과 횟수를 기록 | 앞선 이벤트 도착 뒤 다시 판정 |
| 격리 | Plan 누락, 상품 불일치, 같은 버전의 다른 해시, 오래된 버전, 잘못된 실행 계약 | 암호화한 원문 참조와 안정적인 사유 코드 보존 | 사유 코드에 따라 데이터·순서·계약 조사 |
| 리비전 추가 | 순서와 계약 검증을 모두 통과 | 새 불변 리비전과 하위 그래프 저장 | 후속 방문 후보 계산 가능 |
버전 누락은 즉시 잘못된 이벤트로 단정하지 않습니다. 네트워크나 브로커 전달 순서 때문에 앞선 버전이 늦게
올 수 있기 때문입니다. 핸들러는 설정된 상한 안에서 재시도하고, 상한에 도달했을 때만
SOURCE_VERSION_GAP_EXHAUSTED로 격리합니다. 반면 같은 원본 버전의 해시가 다르면 기다려도 해결되지 않는
계약 충돌이므로 SOURCE_VERSION_HASH_CONFLICT로 격리합니다.
동시에 같은 원본 버전의 이벤트 두 개가 들어오는 경우도 있습니다. 핸들러는 Plan 루트 행을 잠근 뒤 버전을
다시 판정합니다. 테스트에서는 한 요청만 CREATED가 되고 다른 요청은 DUPLICATE로 수렴하며, 리비전과
아웃박스는 각각 하나만 남습니다.
쓰기 모드에서는 리비전 헤더와 진료 항목·관계, 인박스 처리 결과, 아웃박스(outbox)를 같은 트랜잭션에 저장합니다. 중간에 실패하면 일부만 남지 않습니다. 기존 활성 리비전도 덮어쓰지 않고 새 리비전을 활성화해 구매 당시 실행 계약이 어떻게 바뀌었는지 추적할 수 있습니다.
원자적 저장이 필요한 이유는 하위 그래프가 여러 테이블로 나뉘기 때문입니다. 리비전 헤더만 저장되고 진료 항목이 빠지거나, 리비전은 저장됐는데 아웃박스가 빠지면 같은 이벤트를 안전하게 재처리할 수 없습니다. 실제 테스트는 리비전 저장 직후 강제로 예외를 발생시키고 인박스 처리 결과, 리비전과 하위 행, 아웃박스가 모두 롤백되는지 확인합니다.
관측 정보에도 경계가 있습니다. 메트릭에는 처리 결과와 안정적인 사유 코드만 기록합니다. 아웃박스에는
sourceSnapshotHash와 새 리비전 식별 정보를 넣지만 진료명이나 원본 실행 계약 전체를 싣지 않습니다. 격리
대상 원문도 일반 로그에 남기지 않고 암호화한 참조로 보존합니다. 운영자가 결과를 구분할 수 있어야 하지만,
그 과정에서 환자나 진료 정보를 노출해서는 안 됩니다.
OFF, SHADOW, WRITE는 같은 기능의 단계별 운영 모드다
섹션 제목: “OFF, SHADOW, WRITE는 같은 기능의 단계별 운영 모드다”새 이벤트 처리 경로를 바로 쓰기 모드로 전환하지 않도록 핸들러는 세 가지 모드를 둡니다.
| 모드 | 수행 범위 | 데이터 변경 |
|---|---|---|
OFF | 이벤트 형식을 검증한 뒤 소비 기능을 사용하지 않음 | 없음 |
SHADOW | 기존 Plan 조회와 플래너 검증 결과를 평가 | 새 인박스·리비전·아웃박스·격리 행 없음 |
WRITE | 순서 판정, 검증, 리비전 저장과 최종 상태 기록 | 하나의 트랜잭션으로 반영 |
SHADOW는 실제 페이로드가 현재 Plan 및 실행 계약 규칙에 맞는지 먼저 관찰하는 단계입니다. 쓰기를 하지 않으므로 성공
건수만 보고 전환하면 안 됩니다. 처리 결과와 격리 사유 코드, 버전 누락과 상한 초과 빈도를 확인한 뒤
WRITE로 전환해야 합니다. 이 모드 구분은 플래너의 업무 규칙을 바꾸지 않고 운영 위험만 단계적으로 줄입니다.
현재 구현이 보장하는 범위
섹션 제목: “현재 구현이 보장하는 범위”현재 구현은 다음을 보장합니다.
- 외부에서 확정한 선택 결과와 정확한 구성 상품 버전을 구조적으로 검증한다.
- 선택군의 필수 선택 수, 진료 항목의 근거 이력, 관계 참조, 실행 의존성 순환과 계산 상한을 확인한다.
- 검증한 항목과 관계를 기존
AppointmentPlan의 새 불변 리비전으로 추가한다. - 같은 이벤트와 같은 원본 버전·해시는 중복으로 수렴시키고, 버전 누락과 계약 충돌을 서로 다른 최종 상태로 처리한다.
- 리비전, 하위 그래프, 인박스, 아웃박스(outbox)를 원자적으로 저장한다.
- 원본 실행 계약을 일반 로그와 아웃박스에 그대로 노출하지 않고 안정적인 사유 코드로 관측한다.
반면, 플래너는 방문 후보를 계산하지 않고 예약을 확정하지도 않습니다. 실행 의존성에 최소·권장·최대 간격이
있다고 해서 플래너가 다음 방문 날짜를 정하는 것도 아닙니다. MAY_SAME_VISIT가 있다고 두 진료를 자동으로 한 방문에
넣지도 않습니다. 피부 관리와 리프팅 사례는 업무 규칙을 설명하는 승인된 설계 사례이며, 실제 운영에서 어떤
조합을 허용할지는 병원 정책, 환자 동의, 의료진·장비·공간 수용량을 더 검증해야 합니다.
이 경계를 표로 정리하면 다음과 같습니다.
| 질문 | 현재 단계의 답 |
|---|---|
| 환자가 어떤 구성 상품을 구매했는가? | 실행 계약에서 고정하고 검증함 |
| 각 진료 항목이 어느 버전에서 왔는가? | 근거 이력으로 검증하고 리비전에 보존함 |
| 어떤 항목이 먼저 끝나야 하는가? | 실행 의존성으로 보존함 |
| 같은 방문에 넣을 수 있는가? | 허용·필수·분리 조건만 보존함 |
| 실제로 같은 방문에 넣을 수 있는가? | 아직 결정하지 않음. 자원과 병원 정책 검증 필요 |
| 언제 방문할 것인가? | 아직 결정하지 않음. 방문 후보 계산과 환자 동의 필요 |
| 예약이 확정됐는가? | 아님. Plan 리비전은 확정 예약이 아님 |
구현된 저장 경계와 후속 운영 판단을 나누어야 독자가 현재 시스템의 책임을 정확히 이해할 수 있습니다.
패키지 상품은 구성 항목 목록이 아니라 구매 시점에 확정한 실행 계약으로 예약 서비스에 들어옵니다.
PackageExecutionSnapshot은 선택 결과, 구성 상품 버전, 전개 항목과 관계를 고정한다.PackageExecutionPlanner는 카탈로그를 다시 읽지 않고 처리 상한, 선택 수, 근거 이력, 참조와 순환을 검증한다.- 실행 의존성과 방문 묶음 조건은 다른 규칙이며,
MAY_SAME_VISIT는 방문 확정이 아니다. - 구매 완료는 Plan을 만들고, 별도 패키지 실행 이벤트는 새 Plan 리비전을 추가한다.
- 이벤트는 중복, 버전 대기, 격리, 리비전 추가라는 최종 상태로 수렴한다.
- 동시 재전송과 저장 중 실패가 발생해도 리비전과 아웃박스가 중복되거나 일부만 남지 않는다.
- Plan 리비전이 생겨도 방문 후보나 확정 예약이 생긴 것은 아니다.
실행 계약을 정확히 보존하면 다음 단계에서 환자 동의, 병원 자원, 임상 간격을 반영해 방문 후보를 계산할 수 있습니다. 무엇을 이행하기로 했는지 먼저 고정해야 언제 방문할지를 안전하게 정할 수 있습니다.
근거 자료
섹션 제목: “근거 자료”- clinic-appointment 저장소
PackageExecutionSnapshotPackageExecutionPlannerPackageExecutionEventVisitPlanningEventHandlerPackageExecutionPlannerTestVisitPlanningEventHandlerTest- 패키지 상품 실행 그래프
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.