예약 · 계약 (C-05) — feature/reservation/console

콘솔 /reservation(?customer=<산모 id> 새 견적 · ?reservation=<예약 id> 예약 하나). 메뉴는 ConsoleMenu.booking — 대표 · 원장 · 상담실장만 보이고(셸 · ConsoleMenuAccess), 쓰기는 원장 · 상담실장(대표는 읽기만, ia.md §5).

구조

층 어디 무엇
도메인 lib/src/domain/ 값 객체 · 순수 로직 — 세계 · 화면과 무관하게 시험한다
화면 lib/src/widget/ ReservationScreen(data:, actions:) — 값(ReservationScreenData)과 동작(ReservationActions)만 받는다
세계 연결 lib/src/data/ ReservationWorldReader(읽기 — 신호 0) · ReservationWorldWriter(동작 하나 = 트랜잭션 하나) · ReservationWorldFields(더한 필드의 JSON 모양)
공용 혼합 결제 package/postpartum_world/lib/src/payment/ · package/postpartum_ui/lib/src/payment/ 도메인(PaymentSplit · PaymentSplitLine · PaymentLineJson · MockApproval)과 시트(PaymentSplitSheet · PaymentLinkDialog) — 이 WP 가 만들고 정산(C-10 잔금) · 에스테틱(C-09 코스)이 같이 쓴다. 사용법은 package/postpartum_ui/README.md 「결제 시트」
라우트 lib/src/route/ · lib/src/page/ ReservationRoute(customer:, reservation:) → ReservationPage(세계 · 세션을 서비스 로케이터로 받아 화면 값 · 동작을 만든다)

ReservationPage 는 ServiceLocator.instance.get<PostpartumWorld>() · get<PostpartumSession>() 로 세계를 받는다 — postpartum_binding 은 의존하지 않는다(계약 §9). mergeSignals([world.changes, session.changes]) 로 다시 읽고, 대표는 session.branchScope(null = 전체)로 거른다. 세계가 없으면 S-05, 지점 역할이 다른 지점의 예약 · 산모 주소로 오면 S-04, 상태 미리보기(?fixture=)는 S-02 · S-01 · S-03.

세계 — 읽고 쓰는 것

동작 한 트랜잭션에서 쓰는 것
견적 저장 reservations(상담 — 새 견적이면 res_<꼬리> 행을 만든다, 지난 예약이 있으면 _2 …)
가계약 · 취소 reservations.status hold(+ depositDueOn) · canceled + 이력
결제 링크(모의) payments pay_<꼬리>_deposit(pending · 줄의 link{channel, requestedAt, paidAt?} — 줄 JSON 은 공용 PaymentLineJson) + 이력 linkRequested
계약금 확정 reservations(booked · contractAt = 세계 시각 · 이력 hold → booked) + payments(approved · 줄마다 모의 승인 번호 · approvedAt) + consultations.stage booked(+ 이력) + sessionBuckets bkt_<꼬리>_prenatal(입실일 소멸) · _postnatal + documents kind reservationConfirmation
출산 확인 통화 reservations.birthStatus · bornOn · birthReport · 입실/퇴실 예정일 + 이력 birthCall(통화 결과 · 메모(C)) + 산전 버킷 소멸일
기간 변경 reservations.plan · extras(위약금 periodChangePenalty) · total · dischargeOn + 이력 + 회차 총 회수(쓴 회수 아래로는 줄이지 않는다)
입실 제한 해지 요청 reservations.terminationRequestedAt/By · terminationReasonKey admissionRestriction + 이력 terminationRequested(상태는 예약완료 그대로) + refunds ref_<꼬리>(계약금 전액 · calculated — 입실 제한 전용 규정 행, 아래 「해지 사유 코드」) — 원장 확인 · 지급 · 해지 확정은 정산 · 환불(C-11)

시드(package/postpartum_world/lib/src/seed/seed_reservation.dart): messageTemplates tpl_reservation_confirmed (trigger reservationConfirmed · 문자 · bodyKey · 사용). 집계는 PostpartumFacts 와 같다 — 투어 4 뒤 물결점 당월예약 12 → 13 · 상담 전환 +1 · 매출 +600,000(시험이 단언).

FR ↔ 구현 ↔ 시험

FR 구현 시험
FR-20 요금 계산 QuoteInput(지인 · 제휴병원 · 재이용은 한 칸 — 함께 켤 수 없다) → 규칙 Quote.compute 그대로의 QuoteBreakdown · QuoteCard · QuoteBreakdownView quote_input_test · reservation_editor_test · BDD 1
FR-21 계약 상태 ReservationFlow(ia.md §7 전수) · HoldRequest(기본 기한 3일) · HoldUrgency(지남 = 취소 후보) · 목록 「가계약 기한」 탭 reservation_flow_test · reservation_board_test
FR-22 혼합 결제 · 결제 링크(모의) 공용 PaymentSplit(합 = 계약금 · 0원 줄 · 링크 「요청됨 → 결제됨」 · 모의 승인 번호 MockApproval) · DepositPaymentCard(공용 PaymentSplitSheet 에 계약금 제목 · 「결제 확정 · 예약완료」를 얹는다) · DepositConfirmation(상담 → 가계약 → 예약완료 · 서비스 회차) postpartum_world payment_split_test · postpartum_ui payment_split_sheet_test · deposit_confirmation_test · reservation_editor_test · BDD 2
FR-23 예약확인서 ReservationConfirmationDialog(출력 미리보기) · 문서 행(documents kind reservationConfirmation — 계약금 확정 트랜잭션이 만든다) reservation_booked_test · reservation_world_test
FR-24 입실 제한 경고 AdmissionRestriction(기준은 규칙 AdmissionReview) · 통화 다이얼로그의 즉시 경고 · AdmissionRestrictionAlert(해지 요청 — 사유 코드 + 환불 행(계약금 전액), 아래 「해지 사유 코드」) birth_call_test · reservation_booked_test · reservation_world_test
FR-25 산전 관리 · 해피콜 목록 「산전 일정」(남은 산전 서비스 · 입실일 소멸 · 「산전 마사지 예약」 → C-09 /aesthetic?customer=<산모 id> — 에스테틱 WP 가 받는다) · 「출산 확인 해피콜」(예정일 14일 전부터) · BirthCallDialog → BirthCallResult(실제 분만일 · 분만 형태로 입실/퇴실 재계산) reservation_board_test · birth_call_test · reservation_booked_test · reservation_page_test
FR-26 기간 변경 PeriodChange(박수로 다시 계산 · 줄인 금액의 10% 위약금 줄) · PeriodChangeDialog period_change_test · reservation_booked_test

「예약 확정」 메시지 미리보기는 ReservationMessageCard — 카탈로그 기본 본문, 사용자가 고친 본문(C)이 있으면 그 본문의 변수를 MessageTemplateFill 로 채운다(아래 「메시지 변수」). 보내지 않는다.

메시지 변수 — 「예약 확정」 템플릿 (메시지 WP 표준)

이 WP 가 처음 정한 템플릿 표기다. 메시지 화면(C-12)의 변수 칩 · 미리보기는 같은 이름을 쓰고, 새 변수도 같은 표기로 더한다.

칸 값
템플릿 행 messageTemplates tpl_reservation_confirmed(시드 seed_reservation.dart — 메시지 WP 는 다시 만들지 않는다)
trigger 코드 reservationConfirmed — 계약금 확정(예약완료) 때
channel sms(Figma C-05 「장문」 — 발송은 모의)
bodyKey reservationConfirmed — 기본 본문(B)은 카탈로그가 고른다(이 번들은 message.body)
고친 본문 editedBody(C — 사용자가 C-12 에서 고친 본문). 있으면 기본 본문 대신 쓴다
사용 enabled true

변수는 본문에 {이름}(영문 camelCase — MessageTemplateFill 의 \{[A-Za-z][A-Za-z0-9]*\})으로 쓴다. 값이 없는 변수는 그대로 남아 미리보기에서 빈 곳이 보인다.

변수 뜻 값 · 형식(셸 로케일)
{center} 센터 이름 AppIdentity.displayName
{branch} 지점 이름 지점 표시 이름(B 투영)
{mother} 산모 이름 projector.text(customerName(id)) — 사람이 친 이름(C)도 같은 길
{admissionDate} 입실 예정일 월일 · 요일(MMMEd — ko 「3월 20일 (금)」)
{room} 룸타입 「VIP」 · 「디럭스」
{plan} 기간 「13박14일」 등
{total} 확정금액 금액(AmountFormat.won — ko 「6,250,000원」)
{deposit} 계약금 받은 계약금(확정 전이면 요금표의 계약금) — 금액 형식

코드: 이름은 ReservationMessageToken(enum name), 값은 ReservationMessageCard.valuesOf.

해지 사유 코드 — 정산 · 환불(C-11)과의 경계

칸 값
필드 reservations.terminationReasonKey(+ terminationRequestedAt · terminationRequestedBy)
코드 admissionRestriction — FR-24 입실 제한(재태 · 출생 체중 기준 AdmissionReview)으로 해지 요청
이력 reservations.history[] kind terminationRequested
환불 행(지금) 같은 트랜잭션에서 refunds ref_<꼬리> 를 만든다 — reasonKey admissionRestriction · resultAmount = 받은 계약금 전액 · status calculated · branchId. 규정 행은 resultRow admissionRestriction(계약금 100% 환급) · 귀책 사유와 별개로 입실 제한 전용 행을 우선 적용한다
C-11 이 잇는 것 원장 확인 · 환불신청서 · 지급(모의) · 해지 확정(예약 상태 terminated)

코드 상수: ReservationWorldFields.terminationReasonField · ReservationWorldFields.admissionRestrictionReason.

모의 경계

해석한 규칙(데모)

무엇 해석 근거
기간 변경 위약금 기간을 줄일 때 줄어든 금액의 10%. 늘릴 때는 차액만 prd FR-26 「위약금 10% 를 내역 줄로」 — 늘리는 변경에 위약금은 업무상 맞지 않는다
출산 → 입실 자연분만 2일 · 제왕절개 5일 뒤 입실(StaySchedule.hospitalDaysFor) 이하늘 예정 3/18 → 3/20 입실(world-schema)
해피콜 대상 예약완료 · 출산 전 · 예정일 14일 이내(지난 것 포함) ia.md C-01 「출산 임박 해피콜 대상」
가계약 기한 기본 3일 뒤, 다이얼로그에서 하루씩 —
상담 → 예약완료 계약금 확정 한 번으로 가계약을 거쳐 예약완료(이력 두 줄) ia.md §7 은 상담 → 가계약 → 예약완료만 칸이 있다

Figma 와 다르게 둔 점

자리 Figma 구현 이유
고르기 칩 · 토글 높이 32 48dp × scaling 이상 R-4 터치 하한
날짜 표기 「3/20(금)」 · 「2026-03-18」 로케일 형식(ko 「3월 20일 (금)」 · 「2026. 3. 18.」) DemoFormat — 한국어 패턴을 코드에 두지 않는다
메시지 금액 「625만 원」 「6,250,000원」 금액은 AmountFormat(로케일 형식)
메시지 머리말 「장문 · 모의 발송」 「미리보기 · 실제로 보내지 않아요」 이 화면은 보내지 않는다(발송은 C-12)
포함 서비스 「산전 40분 2회 · 산후 40분 2회」 「산전 2회 · 산후 2회」 요금표는 회수만 정한다(분은 C-09 메뉴)
목록 · 탭 · KPI 없음(편집기만) 탭 넷 + KPI 지시서의 목록(이번 달 예약 · 가계약 기한 · 산전 일정 · 해피콜 대상)
머리 줄 단계 칩 「상담 2차」 「상담 1차」 세계 시드는 Figma C-03 열(1차 이하늘)을 따른다(docs/demo-world.md §8)
병원 이름 「한빛여성병원」 co_faker 로 만든 가상 이름 실제 병원 이름을 만들지 않는다(세계 투영)
아바타 고정 크기 원 글자가 크기를 정하는 원 큰 글자 배율에서 머리글자가 잘렸다(R-1)
금액 입력 정적 금액 숫자 입력칸 수단별 금액을 나누려면 입력이 필요하다