예약 · 계약 (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.
모의 경계
- 카드 · 간편결제 · 계좌이체 · 현금 승인 번호는
MockApproval(키에서 결정적 8자리) — 청구 · 출금 없음. - 지역화폐 · 정부 바우처 · 지자체 지원도 같은 모의 승인 — 확정 다이얼로그에 S-07 「바우처 · 지역화폐 승인」을 함께 보인다.
- 결제 링크(알림톡 · 문자)는 기록만 남는다 — 「결제됨으로 표시」로 승인한다. 외부 요청 0.
- 실행 직전 화면(확정 · 링크 다이얼로그)에만 S-07 을 둔다 — 확정 다이얼로그는
PaymentSplitSheet.mockNotices(split)(결제 + 바우처 · 지역화폐 줄이 있으면 바우처). 결제 카드 안의 모의 결제 문구는 Figma 그대로 늘 보인다.
해석한 규칙(데모)
| 무엇 | 해석 | 근거 |
|---|---|---|
| 기간 변경 위약금 | 기간을 줄일 때 줄어든 금액의 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) |
| 금액 입력 | 정적 금액 | 숫자 입력칸 | 수단별 금액을 나누려면 입력이 필요하다 |