Skip to main content

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)
도메인 컨테이너AriPackageAriRatePlan[] 을 묶음RatePlan 자체가 1차 단위. Package 개념은 inclusion bundle 속성으로 흡수
일별 가격AriRate + AriRateAdjustment 폴리모픽 계산DailyRate snapshot row + RatePlan.pricingRules
시즌 / 기간AriPeriod JSON daysOfWeek/Month/YearRateSeason 명명 엔티티 + pricingRules.seasons 인라인
부가 서비스AriInclusion + AriRatePlanInclusionInclusion + RatePlanInclusion (override 지원)
판매 통제(부재)RatePlanRestriction 신규: CTA/CTD/StopSell/LOS by date
가격 IDAriRate.id: IntDailyRate.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 필드비고
요금제 이름namerequired
코드coderequired, 시설 단위 unique
설명descriptionoptional textarea
예약 타입type (rent / lodge)required enum
공개 여부isPublicdefault true
활성화isActivedefault true
통화currencyISO 4217 enum
기준 점유 / 최대 점유baseOccupancy / maxOccupancydefault 2 / 2
체크인 / 체크아웃 시간checkInTime / checkOutTimeminutes 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.dayOfWeekoptional. {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.combinationmultiplicative (기본) / 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 패턴

  1. 사용자가 updateRatePlan 으로 규칙 변경 → 응답은 정상 (RatePlan row 만 갱신됨)
  2. 프론트는 사용자에게 banner 표시:
    ⚠️ 규칙 변경이 저장되었습니다. 영향 받는 미래 snapshot 47건 / 빈 날짜 683건. [재물질화 진행] [나중에]
  3. 백엔드가 비동기 분석을 끝내면 RematerializationProposal 이 조회 가능해진다.
  4. 사용자가 [재물질화 진행] 클릭 → applyRematerializationProposal(id) 호출
  5. 또는 [나중에] / [폐기] → 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_RULESpricingRules 형식 / multiplier 오류Step 2 인라인 에러 표시
INVALID_DATE_RANGEstartDate > endDate폼 인라인 에러
BULK_AMOUNT_REQUIREDbulk 입력에 amount / delta / percent 누락Bulk 다이얼로그 인라인 에러
RESTRICTION_VIOLATION예약 시 CTA/CTD/StopSell/LOS 위반예약 화면 모달, 위반 일자 강조
PROPOSAL_NOT_APPLICABLEproposal status 가 pending/partial 이 아님재조회 후 banner 갱신
PROPOSAL_EXPIRED1h TTL 초과사용자 안내, RatePlan 재저장 유도
DERIVATION_DEPTH_EXCEEDED파생 chain 깊이 5 초과부모 ratePlan 단순화 요청
DERIVATION_CYCLE_DETECTEDparent 체인 순환부모 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 / 문서