refactor/rate-domain-redesign 브랜치, 41 commits)
이후 변경된 GraphQL surface 를 기준으로 프론트엔드 설정화면을 재작업하기 위한 가이드다.
1. 개념 변화 요약
| 영역 | 이전 (legacy ari-*) | 신규 (rate-plan / inclusion) |
|---|---|---|
| 도메인 컨테이너 | AriPackage 가 AriRatePlan[] 을 묶음 | RatePlan 자체가 1차 단위. Package 개념은 inclusion bundle 속성으로 흡수 |
| 일별 가격 | AriRate + AriRateAdjustment 폴리모픽 계산 | DailyRate snapshot row + RatePlan.pricingRules 룰 |
| 시즌 / 기간 | AriPeriod JSON daysOfWeek/Month/Year | RateSeason 명명 엔티티 + pricingRules.seasons 인라인 |
| 부가 서비스 | AriInclusion + AriRatePlanInclusion | Inclusion + RatePlanInclusion (override 지원) |
| 판매 통제 | (부재) | RatePlanRestriction 신규: CTA/CTD/StopSell/LOS by date |
| 가격 ID | AriRate.id: Int | DailyRate.id: BigInt → String (resolver 직렬화), RatePlan.id: ULID String |
| 변경 감사 | 자체 history 테이블 | audit-svc 통합 발행 (@vpms-cluster/audit) |
| 규칙 변경 적용 | 즉시 자동 cascade | 명시적 RematerializationProposal → 사용자 승인 |
2. UI 흐름 (재확인)
ADR-0002 §2.5/2.6 에서 확정된 UX:- 요금제 생성: 5단계 Wizard
- 일상 운영: Hybrid Grid + Side Panel
- 변경 영향 관리: 규칙 변경 시 Proposal 알림 → 사용자가 [재물질화 진행 / 부분 적용 / 나중에] 선택
3. RatePlan 생성 — Wizard 5단계
Step 1 — 기본 정보
| UI 필드 | input 필드 | 비고 |
|---|---|---|
| 요금제 이름 | name | required |
| 코드 | code | required, 시설 단위 unique |
| 설명 | description | optional textarea |
| 예약 타입 | type (rent / lodge) | required enum |
| 공개 여부 | isPublic | default true |
| 활성화 | isActive | default true |
| 통화 | currency | ISO 4217 enum |
| 기준 점유 / 최대 점유 | baseOccupancy / maxOccupancy | default 2 / 2 |
| 체크인 / 체크아웃 시간 | checkInTime / checkOutTime | minutes from midnight (Int) — 예: 14:00 → 840 |
| 적용 객실타입 | roomTypeIds: [ID!] | 다중 선택 ULID |
createRatePlan(accommodationId, input: CreateRatePlanInput)
Step 2 — 기본 가격
| UI 필드 | input 필드 | 비고 |
|---|---|---|
| 평일 기본 가격 | defaultAmount: Decimal! | required |
| 요일 가산치 | pricingRules.dayOfWeek | optional. {monday: 1.0, friday: 1.5, saturday: 1.875, sunday: 1.5} 형태. 1.0 = 가산 없음 |
| 시즌 가산치 | pricingRules.seasons[] | optional. {name, startMonth, startDay, endMonth, endDay, multiplier, priority?} |
| 결합 방식 | pricingRules.combination | multiplicative (기본) / additive_to_base |
resolveDailyRate 를 호출 (RatePlan 저장 전이라면 클라이언트 측에서 동일 공식 계산):
pricingRules는 JSON scalar 이며 다음 shape:
Step 3 — 점유 가격
점유 인원별 가격을 별도 row 로 둔다. baseOccupancy 와 다른 인원 가격을 명시. UI: 1인 / 2인 / 3인 / 4인 등 row 입력. 호출:setRatePlanOccupancyPrices(ratePlanId, prices) — 전량 교체.
pricingMode:
absolute: 해당 가격 자체 (예: 1인 ₩60,000)delta_from_base: baseOccupancy 가격 대비 +/- (예: 3인 +₩30,000)
Step 4 — 제약
RatePlan 기본 LOS 와 사전 예약 제한.| UI 필드 | input 필드 |
|---|---|
| 기본 최소 박수 | defaultMinLOS |
| 기본 최대 박수 | defaultMaxLOS |
setRatePlanRestriction / setRatePlanRestrictionBulk).
Step 5 — 포함 서비스 (Inclusion)
RatePlan 에 포함할 inclusion 선택 + 수량/charge override. 호출:setRatePlanInclusions(ratePlanId, inclusions) — 전량 교체.
Wizard 완료
모든 Step 의 input 을 모아createRatePlan 1회 호출 후, Step 3/5 의 bulk mutation 들을
순차 호출한다. 트랜잭션 보장이 필요하다면 백엔드에 묶음 endpoint 가 별도 필요 (현 시점 미제공).
4. 일상 운영 — Hybrid Grid + Side Panel
좌측 Calendar Grid
호출:getDailyRateCalendar
hasSnapshot=true인 셀 → ◆ 표시 (확정 snapshot)hasSnapshot=false→ 일반 (규칙 계산값)- 같은 날
getRatePlanRestrictions응답 cross-reference 해서 🔒 CTA / 🚫 CTD / ⛔ StopSell 아이콘
Restriction overlay
우측 Side Panel — 단일 셀 편집
- 가격 수정:
setDailyRate(input)또는clearDailyRate(...)(snapshot 삭제 → 규칙 계산값 복귀) - 제약 수정:
setRatePlanRestriction(input)/clearRatePlanRestriction(...)
범위 일괄 수정
SetDailyRateBulkInput:
amount/amountDelta/amountPercentDelta중 1개 택일overrideExisting: false→ 기존 snapshot 있는 cell 은 건너뜀 (확정 가격 보호)- 응답은
Int(touched cell 수)
5. RateSeason 관리 화면
별도 시즌 카탈로그 화면.rrule 은 RFC 5545 iCalendar RRULE 문자열 (예: FREQ=WEEKLY;BYDAY=FR,SA).
명시 startDate / endDate 와 함께 사용 가능.
6. Inclusion 관리 화면
postingType:
per_stay: 체크인 시 1회 부과per_night: 박당 부과per_person: 1인당 1회per_person_per_night: 1인당 박당
createInclusion / updateInclusion / deleteInclusion.
7. Rematerialization Proposal 흐름 (중요)
RatePlan 의defaultAmount / pricingRules / derivationRule 을 수정하면 백엔드는
자동으로 미래 DailyRate snapshot 을 덮어쓰지 않는다 (ADR-0001 §2.2.2).
대신 RematerializationProposal 을 생성하고 사용자 승인을 기다린다.
UI 패턴
- 사용자가
updateRatePlan으로 규칙 변경 → 응답은 정상 (RatePlan row 만 갱신됨) - 프론트는 사용자에게 banner 표시:
⚠️ 규칙 변경이 저장되었습니다. 영향 받는 미래 snapshot 47건 / 빈 날짜 683건. [재물질화 진행] [나중에]
- 백엔드가 비동기 분석을 끝내면
RematerializationProposal이 조회 가능해진다. - 사용자가 [재물질화 진행] 클릭 →
applyRematerializationProposal(id)호출 - 또는 [나중에] / [폐기] →
discardRematerializationProposal(id)
Proposal 조회
Proposal id 는 현재updateRatePlan응답에 포함되지 않는다. 다음 두 가지 중 하나:
- 백엔드가 응답에 proposal id 를 포함하도록 surface 확장을 별도 합의
- 프론트가 별도
getRematerializationProposals(ratePlanId, status: pending)형태 list query 를 요청 (현 시점 미제공 — 필요 시 백엔드 PR)
8. Audit 표기 (감사)
엔티티에lastModifiedBy (User ULID), updatedAt, lastModifiedReason 보조 필드가 있다.
Side Panel “이력” 섹션에 표시 권장:
- “정다운, 2026-05-10 14:32 · 가격 조정 (사유: 성수기 인상)”
9. Error Codes (UI 처리 가이드)
| Error code | 의미 | UI 처리 권장 |
|---|---|---|
RATE_PLAN_NOT_FOUND | 요금제 미존재 / soft-delete | 페이지 새로고침 안내 |
INVALID_PRICING_RULES | pricingRules 형식 / multiplier 오류 | Step 2 인라인 에러 표시 |
INVALID_DATE_RANGE | startDate > endDate | 폼 인라인 에러 |
BULK_AMOUNT_REQUIRED | bulk 입력에 amount / delta / percent 누락 | Bulk 다이얼로그 인라인 에러 |
RESTRICTION_VIOLATION | 예약 시 CTA/CTD/StopSell/LOS 위반 | 예약 화면 모달, 위반 일자 강조 |
PROPOSAL_NOT_APPLICABLE | proposal status 가 pending/partial 이 아님 | 재조회 후 banner 갱신 |
PROPOSAL_EXPIRED | 1h TTL 초과 | 사용자 안내, RatePlan 재저장 유도 |
DERIVATION_DEPTH_EXCEEDED | 파생 chain 깊이 5 초과 | 부모 ratePlan 단순화 요청 |
DERIVATION_CYCLE_DETECTED | parent 체인 순환 | 부모 ratePlan 재선택 |
10. GraphQL Operation 카탈로그
Query (9)
getRatePlanList(accommodationId, isActive)getRatePlan(id)getDailyRateCalendar(ratePlanId, roomTypeId, startDate, endDate, occupancy)resolveDailyRate(ratePlanId, roomTypeId, stayDate, occupancy)getRatePlanRestrictions(ratePlanId, startDate, endDate)getInclusionList(accommodationId)getRateSeasonList(accommodationId)/getRateSeason(id)getRematerializationProposal(id)
Mutation (22)
RatePlan 메타:createRatePlan / updateRatePlan / deleteRatePlan
DailyRate: setDailyRate / setDailyRateBulk / clearDailyRate / clearDailyRateBulk
Restriction: setRatePlanRestriction / setRatePlanRestrictionBulk / clearRatePlanRestriction / clearRatePlanRestrictionBulk
RatePlan 하위 전량 교체: setRatePlanOccupancyPrices / setRatePlanInclusions / setRatePlanChannelMappings
Inclusion: createInclusion / updateInclusion / deleteInclusion
RateSeason: createRateSeason / updateRateSeason / deleteRateSeason
Rematerialization: applyRematerializationProposal / discardRematerializationProposal
11. legacy 제거 체크리스트
기존 UI 에서 다음을 모두 제거:- ☐ AriPackage 관련 화면 / 컨테이너 개념
- ☐ AriRate / AriRatePlan / AriPeriod / AriInclusion CRUD 화면
- ☐ Adjustment(multiplier, adjustment, period) 입력 UI
- ☐
appliedRatePlanId / appliedRateId / appliedAdjustmentFlat / appliedAdjustmentMultiplier등 legacy 필드 참조 - ☐
ratePlanVersion / rateIdVersion표시 (BlockComposition archive 는 deprecated) - ☐
currencySymbol표시 →currency코드만 받아Intl.NumberFormat(locale, { style: 'currency', currency })로 클라이언트 변환
- ☑ ratePlanId / roomTypeId 가 ULID 문자열임을 인지 (UUID 와 다름, 26자 Crockford base32)
- ☑ DailyRate / RatePlanRestriction 의
id는 GraphQLID!(string) 이지만 백엔드에서 BigInt — 그대로 string 비교/저장 가능 - ☑ 가격 입력은
Decimal스칼라 (string 으로 전송, 부동소수점 손실 방지) - ☑
pricingRules는 JSON scalar (객체 직렬화)