Documentation Index
Fetch the complete documentation index at: https://api-docs.vpms.io/llms.txt
Use this file to discover all available pages before exploring further.
본 문서는 백엔드 rate domain 재설계 (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)
mutation CreateRatePlan($accommodationId: ID!, $input: CreateRatePlanInput!) {
createRatePlan(accommodationId: $accommodationId, input: $input) {
id
code
name
type
baseOccupancy
defaultAmount
currency
roomTypes { id name }
}
}
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 |
Step 2 미리보기: 사용자에게 “토요일 ₩150,000” 같은 결과를 즉시 보여주려면
resolveDailyRate 를 호출 (RatePlan 저장 전이라면 클라이언트 측에서 동일 공식 계산):
query Preview($ratePlanId: ID!, $roomTypeId: ID!, $stayDate: Date!) {
resolveDailyRate(ratePlanId: $ratePlanId, roomTypeId: $roomTypeId, stayDate: $stayDate) {
amount
currency
resolutionSource
}
}
pricingRules 는 JSON scalar 이며 다음 shape:
{
"dayOfWeek": { "saturday": 1.875, "sunday": 1.5 },
"seasons": [
{ "name": "여름성수기", "startMonth": 7, "startDay": 1,
"endMonth": 8, "endDay": 31, "multiplier": 1.3, "priority": 10 }
],
"combination": "multiplicative"
}
Step 3 — 점유 가격
점유 인원별 가격을 별도 row 로 둔다. baseOccupancy 와 다른 인원 가격을 명시.
UI: 1인 / 2인 / 3인 / 4인 등 row 입력.
호출: setRatePlanOccupancyPrices(ratePlanId, prices) — 전량 교체.
mutation SetOccupancyPrices($ratePlanId: ID!, $prices: [RatePlanOccupancyPriceInput!]!) {
setRatePlanOccupancyPrices(ratePlanId: $ratePlanId, prices: $prices) {
occupancy
amount
pricingMode
}
}
pricingMode:
absolute: 해당 가격 자체 (예: 1인 ₩60,000)
delta_from_base: baseOccupancy 가격 대비 +/- (예: 3인 +₩30,000)
Step 4 — 제약
RatePlan 기본 LOS 와 사전 예약 제한.
| UI 필드 | input 필드 |
|---|
| 기본 최소 박수 | defaultMinLOS |
| 기본 최대 박수 | defaultMaxLOS |
날짜별 CTA / CTD / StopSell 은 Step 4 가 아니라 운영 화면의 Side Panel 에서 설정한다
(setRatePlanRestriction / setRatePlanRestrictionBulk).
Step 5 — 포함 서비스 (Inclusion)
RatePlan 에 포함할 inclusion 선택 + 수량/charge override.
호출: setRatePlanInclusions(ratePlanId, inclusions) — 전량 교체.
mutation SetInclusions($ratePlanId: ID!, $inclusions: [RatePlanInclusionInput!]!) {
setRatePlanInclusions(ratePlanId: $ratePlanId, inclusions: $inclusions) {
inclusion { id name }
quantity
chargeOverride
isMandatory
isComplimentary
}
}
Wizard 완료
모든 Step 의 input 을 모아 createRatePlan 1회 호출 후, Step 3/5 의 bulk mutation 들을
순차 호출한다. 트랜잭션 보장이 필요하다면 백엔드에 묶음 endpoint 가 별도 필요 (현 시점 미제공).
4. 일상 운영 — Hybrid Grid + Side Panel
좌측 Calendar Grid
호출: getDailyRateCalendar
query Calendar(
$ratePlanId: ID!, $roomTypeId: ID!, $start: Date!, $end: Date!
) {
getDailyRateCalendar(
ratePlanId: $ratePlanId, roomTypeId: $roomTypeId
startDate: $start, endDate: $end
) {
stayDate
amount
currency
resolutionSource # daily_rate_snapshot / derived_from_parent / default_with_rules
hasSnapshot # true → ◆ 확정 / false → 🧮 규칙 계산값
ratePlanChain
}
}
셀 표기:
hasSnapshot=true 인 셀 → ◆ 표시 (확정 snapshot)
hasSnapshot=false → 일반 (규칙 계산값)
- 같은 날
getRatePlanRestrictions 응답 cross-reference 해서 🔒 CTA / 🚫 CTD / ⛔ StopSell 아이콘
Restriction overlay
query Restrictions($ratePlanId: ID!, $start: Date!, $end: Date!) {
getRatePlanRestrictions(ratePlanId: $ratePlanId, startDate: $start, endDate: $end) {
id
stayDate
roomTypeId
closedToArrival
closedToDeparture
stopSell
minLengthOfStay
maxLengthOfStay
}
}
우측 Side Panel — 단일 셀 편집
- 가격 수정:
setDailyRate(input) 또는 clearDailyRate(...) (snapshot 삭제 → 규칙 계산값 복귀)
- 제약 수정:
setRatePlanRestriction(input) / clearRatePlanRestriction(...)
mutation SetCell($input: SetDailyRateInput!) {
setDailyRate(input: $input) {
id stayDate amount currency source
}
}
mutation SetRestriction($input: SetRatePlanRestrictionInput!) {
setRatePlanRestriction(input: $input) {
id stayDate stopSell closedToArrival closedToDeparture minLengthOfStay maxLengthOfStay
}
}
범위 일괄 수정
mutation BulkRate($input: SetDailyRateBulkInput!) {
setDailyRateBulk(input: $input) # → Int (touched count)
}
mutation BulkRestriction($input: SetRatePlanRestrictionBulkInput!) {
setRatePlanRestrictionBulk(input: $input)
}
mutation ClearRange(
$ratePlanId: ID!, $roomTypeIds: [ID!]!, $start: Date!, $end: Date!
) {
clearDailyRateBulk(ratePlanId: $ratePlanId, roomTypeIds: $roomTypeIds, startDate: $start, endDate: $end)
clearRatePlanRestrictionBulk(ratePlanId: $ratePlanId, roomTypeIds: $roomTypeIds, startDate: $start, endDate: $end)
}
SetDailyRateBulkInput:
amount / amountDelta / amountPercentDelta 중 1개 택일
overrideExisting: false → 기존 snapshot 있는 cell 은 건너뜀 (확정 가격 보호)
- 응답은
Int (touched cell 수)
5. RateSeason 관리 화면
별도 시즌 카탈로그 화면.
query Seasons($accommodationId: ID!) {
getRateSeasonList(accommodationId: $accommodationId) {
id code name startDate endDate rrule priority
}
}
mutation CreateSeason($accommodationId: ID!, $input: CreateRateSeasonInput!) {
createRateSeason(accommodationId: $accommodationId, input: $input) {
id code name
}
}
rrule 은 RFC 5545 iCalendar RRULE 문자열 (예: FREQ=WEEKLY;BYDAY=FR,SA).
명시 startDate / endDate 와 함께 사용 가능.
6. Inclusion 관리 화면
query Inclusions($accommodationId: ID) {
getInclusionList(accommodationId: $accommodationId) {
id code name description postingType charge currency taxIncluded
}
}
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 조회
query Proposal($id: ID!) {
getRematerializationProposal(id: $id) {
id
ratePlanId
triggerKind # pricing_rules / default_amount / derivation / occupancy
affectedSnapshots # 영향 받는 기존 snapshot 수
affectedComputed # 영향 받지만 snapshot 없는 날짜 수
status # pending / applied / partial / discarded / expired
expiresAt # 1시간 TTL
appliedAt
createdAt
}
}
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 · 가격 조정 (사유: 성수기 인상)”
전체 변경 이력은 audit-svc 의 별도 query 로 조회한다 (core-svc 는 발행만 함).
audit-svc surface 는 audit-svc GraphQL endpoint 별도 호출.
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 는 GraphQL ID! (string) 이지만 백엔드에서 BigInt — 그대로 string 비교/저장 가능
- ☑ 가격 입력은
Decimal 스칼라 (string 으로 전송, 부동소수점 손실 방지)
- ☑
pricingRules 는 JSON scalar (객체 직렬화)
12. 참고 ADR / 문서