> ## 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.

# 0002 pricing rules and ui patterns

# ADR-0002: Pricing Rules Storage & UI Patterns

* **Status**: Proposed
* **Date**: 2026-05-15
* **Authors**: 정다운 ([daun@vendit.co.kr](mailto:daun@vendit.co.kr))
* **Supersedes**: —
* **Related**: [ADR-0001](./0001-rate-domain-redesign.md)
* **Affected**: `apps/core-svc/src/domains/rate-plan`, `apps/ari-ui` (또는 후속 UI 앱)

***

## 1. Context

[ADR-0001](./0001-rate-domain-redesign.md)에서 Rate Domain 재설계 방향(RatePlan + DailyRate + Restriction + Inclusion + Audit)을 확정했다. 그러나 다음 두 가지는 명세되지 않았다:

1. **Pricing Rules 저장 방식** — 요일 가산치(`토 +87.5%`), 시즌 가산치(`7-8월 +30%`)를 RatePlan 수준에서 어떻게 표현할 것인가
2. **UI 패턴** — RatePlan 생성·운영 화면을 어떻게 구성할 것인가

UI 측 결정이 데이터 모델(특히 DailyRate.source enum과 resolver 우선순위)에 영향을 주므로 별도 ADR로 분리하여 다룬다.

***

## 2. Decision

### 2.1 RatePlan 가격 결정 구조 — Rule(설정) + Snapshot(확정) 분리

ADR-0001 §2.2.1·§2.2.2의 **Price Resolution Contract**와 **Materialization Rules**를 전제로 한다.

* `RatePlan`은 **설정/레시피**(defaultAmount, pricingRules, occupancyPrice, derivationRule)를 보관한다.
* `DailyRate` row는 특정 `stayDate`의 **최종 판매가 snapshot**이다. 단순 "override"가 아니라 예약 엔진·OTA에 노출되는 영속화된 확정값이다.
* 자식 `RatePlan`은 부모의 snapshot에 derivationRule을 적용한 결과를 자신의 snapshot으로 가질 수 있다.

3개 옵션 비교:

| 옵션                                 | 저장 형태                                                 | 트레이드오프                                                           |
| ---------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------- |
| A. Pre-materialize                 | Wizard 완료 시 24개월 DailyRate row 일괄 생성                  | row 폭증. 시설당 5,500+ rows × roomType. Hybrid 원칙 위반                 |
| B. Rule Only                       | `RatePlan.pricingRules` JSON만 저장, 매번 resolver 계산      | row가 전혀 없음 → "확정 가격" 개념 부재. OTA push·Audit·Yield 연동 모두 문제        |
| **C. Rule + Selective Snapshot** ✅ | 규칙은 RatePlan에 보관. 사용자가 명시적으로 확정한 날짜만 DailyRate row 생성 | resolver는 ADR-0001 §2.2.1 순서대로 row → derivation → rule 평가. 가장 깔끔 |

**채택: 옵션 C**. DailyRate row 생성 조건은 ADR-0001 §2.2.2(Materialization Rules)를 따른다.

#### `RatePlan.pricingRules` JSON 스키마

```typescript theme={null}
interface PricingRules {
  // 요일별 가산치 (1.0 = 기본가, 1.5 = +50%)
  dayOfWeek?: {
    monday?: number;
    tuesday?: number;
    wednesday?: number;
    thursday?: number;
    friday?: number;
    saturday?: number;
    sunday?: number;
  };

  // 시즌별 가산치 (RateSeason 참조 또는 인라인)
  seasons?: Array<{
    name: string;
    startMonth: number;   // 1-12
    startDay: number;     // 1-31
    endMonth: number;
    endDay: number;
    multiplier: number;   // base * multiplier
    priority?: number;    // 충돌 시 우선순위 (높을수록 우선)
  }>;

  // 결합 방식
  combination?: 'multiplicative' | 'additive_to_base';
  // 기본 multiplicative: base × dow × season
  // additive_to_base: base × (1 + (dow-1) + (season-1))
}
```

**기본 결합 방식**: `multiplicative`

* 예: `base 80,000 × Saturday 1.875 × Summer 1.3 = 195,000`

### 2.2 Rate Resolution 우선순위

ADR-0001 §2.2.1을 본 ADR의 pricingRules 계산식으로 보강한다:

```
resolveDailyRate(ratePlanId, roomTypeId, date, occupancy):

  1. DailyRate(ratePlanId, roomTypeId, stayDate) row 존재?
     → 최종 판매가 snapshot 반환 (ADR-0001 §2.2.1 step 1)

  2. parentRatePlanId 존재?
     → parent.resolveDailyRate(date) × derivationRule
        (derivationRule은 fixed_amount | fixed_delta | percentage_delta — ADR-0001 §2.2.3)
     → 반환

  3. pricingRules로 계산 (ADR-0001 §2.2.1 step 3):
     baseAmount = RatePlanRoomType(ratePlanId, roomTypeId).defaultAmount   // RoomType별 override
                  ?? ratePlan.defaultAmount                                // 미설정 시 RatePlan 기본가
     dowMultiplier = pricingRules.dayOfWeek[weekday(date)] ?? 1.0
     seasonMultiplier = matchSeasons(date, pricingRules.seasons) ?? 1.0
     return baseAmount × dowMultiplier × seasonMultiplier

  4. occupancyPrice 적용 (occupancy != roomType.defaultSleeps)
     → RatePlanOccupancyPrice 조회 후 mode에 따라 적용
     → 점유 기준은 RoomType.defaultSleeps/maxSleeps 가 source of truth (ADR-0001 §2.2.1).

  4-1. (옵션) 게스트 유형 가산치 적용 — guestMix 가 지정된 경우만
       → RatePlanGuestTypeAdjustment 의 각 row 에 대해 (성인/아동/유아 카운트 × chargeOp)
          를 누적. appliesAfterBaseOccupancy=true 인 row 는 roomType.defaultSleeps 를
          초과한 인원에 대해서만 카운트한다.

  5. 통화 반올림 규칙 normalize (ADR-0001 §2.2.1 step 5)

  6. (Phase 4) Redis 캐시 set
```

캐시는 Phase 4 도입(ADR-0001 §3 Phase-gated). Phase 2의 resolver는 PostgreSQL 직조회로 동작하고, 캐시 미스는 오류가 아니라 성능 저하로 취급한다(ADR-0001 §2.7).

### 2.3 DailyRate.source enum 세분화

ADR-0001 §2.8의 source 개념을 다음으로 구체화한다. DailyRate는 단순 override가 아니라 **확정된 최종 판매가 snapshot**(ADR-0001 §2.2.1)이므로 source는 "어떤 경로로 확정되었는가"를 기록한다:

```prisma theme={null}
enum DailyRateSource {
  manual_user        // 사용자가 Grid에서 단일 셀 직접 수정
  manual_bulk        // 사용자가 일괄수정 다이얼로그로 범위 확정
  rematerialized     // 규칙 변경 후 사용자 승인을 거쳐 재물질화된 결과
  derived_cascade    // 부모 RatePlan 변경에 따른 자식 파생 재산정 결과 (사용자 승인 후)
  horizon_extension  // horizon extension job이 기본값을 선계산한 결과 (Phase 4)
  yield_engine       // 외부 yield management 엔진 push
  channel_pull       // 채널 매니저(OTA) 동기화 pull
  promotion          // Promotion(ADR 추후 예정) 적용 결과
}
```

**중요한 원칙**:

* `wizard_default` 같은 source는 **존재하지 않는다**. Wizard 완료 시점에 DailyRate row를 만들지 않는다. Wizard는 `RatePlan.defaultAmount` + `pricingRules`만 세팅하고, 실제 row는 사용자가 명시적으로 확정한 시점에만 생성된다(ADR-0001 §2.2.2).
* 모든 source는 "확정 시점이 명시적으로 존재"한다. 즉 row의 존재 자체가 "이 가격은 누군가/무언가가 의도적으로 확정한 값"이라는 의미를 갖는다.
* UI는 row가 있으면 `◆` 표기하여 "확정된 snapshot"임을 즉시 식별 가능하게 한다.

### 2.4 Pricing Rules 변경 시 행동 — Explicit Re-materialization

ADR-0001 §2.2.2의 핵심 규칙: **규칙 변경은 기존 DailyRate row를 자동으로 덮어쓰지 않는다.** 본 ADR은 이를 UI 흐름으로 구체화한다.

#### 2.4.1 변경 발생 시 즉시 동작

`RatePlan.defaultAmount`, `pricingRules`, `derivationRule`, `RatePlanOccupancyPrice` 변경 시:

1. **RatePlan/related row만 트랜잭션으로 업데이트** — DailyRate 테이블은 건드리지 않는다.
2. **AuditLog 기록** — `entityType='RatePlan'`, `beforeState`/`afterState`에 변경 diff.
3. **영향 범위 분석 job enqueue** — BullMQ로 비동기 실행:
   * 미래 24개월 중 변경된 규칙에 의해 결과값이 달라지는 (`stayDate × roomType`) 셀 카운트
   * 셀별로 "현재 snapshot(있다면)"과 "새 규칙 적용 시 계산값"을 비교
   * 결과를 `RematerializationProposal` 임시 레코드로 저장 (1시간 TTL)
4. **사용자에게 알림** — Grid 상단에 banner 표시:
   ```
   ⚠️ 규칙 변경이 저장되었습니다.
      • 영향 받는 미래 날짜 snapshot: 47건
      • 영향 받지만 snapshot 없는 날짜: 683건 (자동으로 새 규칙 적용)
      [ 변경 미리보기 ]  [ 재물질화 진행 ]  [ 나중에 ]
   ```

#### 2.4.2 사용자 선택지

| 선택          | 결과                                                                                                   |
| ----------- | ---------------------------------------------------------------------------------------------------- |
| **나중에**     | snapshot은 그대로. 신규 조회만 새 규칙 사용. proposal은 1시간 후 만료                                                    |
| **변경 미리보기** | 영향 받는 셀의 before/after 가격을 Grid 색상으로 오버레이 표시. 다시 결정 선택 가능                                             |
| **재물질화 진행** | proposal에 따라 DailyRate row를 갱신/생성. source=`rematerialized`. 단일 묶음 AuditLog (`action=bulk_update`) 생성 |
| **부분 재물질화** | 미리보기 단계에서 셀을 선택해 일부만 적용 가능. 선택되지 않은 셀은 기존 snapshot 유지                                                |

#### 2.4.3 핵심 보장

* **확정된 snapshot은 사용자 명시 승인 없이 절대 변경되지 않는다.**
* 사용자가 명시적으로 "초기화" 클릭한 셀의 DailyRate row는 삭제되어 resolver가 규칙 기반 계산값으로 복귀.
* 부모 RatePlan 변경에 따른 자식 cascade도 동일 규칙: 자식의 snapshot은 자동 변경되지 않고 proposal로만 표시.

#### 2.4.4 Drift 방지 운영 가드

* 미확정 상태 proposal이 30일 이상 누적되면 운영 대시보드에 경고
* "이번 달 적용 안된 규칙 변경" 위젯을 Grid 상단에 노출
* 채널 매니저 push 시점에는 항상 resolver 최종값을 사용 (proposal 미적용 상태와 무관)

### 2.5 RatePlan 생성 UI — Wizard 5단계

| Step         | 입력 항목                                                                                                                                             | 저장 대상                                                          |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **1. 기본정보**  | name, code, type, isPublic, checkInTime, checkOutTime, 적용 RoomType\[] (base/max occupancy 는 각 RoomType.defaultSleeps/maxSleeps 가 source of truth) | `RatePlan` + `RatePlanRoomType`                                |
| **2. 기본가격**  | defaultAmount, dayOfWeek 가산치, seasons 가산치                                                                                                         | `RatePlan.defaultAmount` + `RatePlan.pricingRules`             |
| **3. 점유가격**  | 1인/2인/3인/4인 가격 (absolute 또는 delta)                                                                                                                | `RatePlanOccupancyPrice[]`                                     |
| **4. 제약**    | 기본 minLOS/maxLOS, CTA/CTD 요일 기본값, 사전예약 제한                                                                                                         | `RatePlan.defaultMinLOS/MaxLOS` + `RatePlanRestriction` 기본 row |
| **5. 포함서비스** | Inclusion 선택, 수량, override 여부                                                                                                                     | `RatePlanInclusion[]`                                          |

#### Wizard UX 원칙

* **실시간 미리보기**: 각 Step에서 "2026-05-17 토요일 가격 = ?" 즉시 계산 표시
* **이전 Step 수정 가능**: 진행도 인디케이터 클릭 시 이전 Step으로 이동
* **Skip 허용**: Step 3·4·5는 선택사항 (default값 사용 가능)
* **완료 시 행동**:
  1. RatePlan + 관련 row 생성 (트랜잭션)
  2. AuditLog `create` 기록 (afterState=전체 snapshot)
  3. Grid 화면으로 자동 이동

### 2.6 일상 운영 화면 — Hybrid Grid + Side Panel

#### 좌측 Grid

| 열                        | 행                |
| ------------------------ | ---------------- |
| RoomType (default 가격 표시) | 날짜 (해당 월 1일\~말일) |

**셀 내용**:

* 가격(천원 단위 표시, 클릭 시 정확한 값)
* 상태 아이콘: `🔒 CTA` / `🚫 CTD` / `⛔ StopSell` / `◆ override`

**상호작용**:

* 단일 셀 클릭 → 우측 Side Panel에 상세 로드
* 셀 인라인 수정 → 가격만 빠르게 변경 가능
* 셀 드래그 → 범위 선택 → 일괄 수정 다이얼로그
* 더블클릭 → 풀스크린 상세 모달

#### 우측 Side Panel

선택된 (날짜 × RoomType) 셀에 대해:

* **가격 상태 라벨**:
  * snapshot이 있는 경우: `◆ 확정 snapshot` + source 표시 (예: `manual_user`, `rematerialized`, `channel_pull`)
  * snapshot이 없는 경우: `🧮 규칙 계산값` (resolver step 3 결과)
* **가격 값**: 현재값 + "규칙 계산값 대비 ±X" diff 표시
  * 입력 후 \[확정 저장] → DailyRate row 생성/갱신 (source=`manual_user`)
  * \[초기화] → DailyRate row 삭제 → 규칙 계산값으로 복귀
* **점유 가격**: 1/2/3/4인 가격 미리보기 (resolver step 4 결과)
* **제약** (ADR-0001 §2.3 — restriction도 날짜별 snapshot이 source of truth):
  * CTA / CTD / StopSell 토글, MinLOS / MaxLOS 입력
  * 저장 시 RatePlanRestriction row 생성/갱신
  * \[초기화] → row 삭제 → 부모/RatePlan default로 상속 복귀
* **포함서비스**: 이 날짜에 적용되는 inclusion 목록 (변경 불가, 정보성)
* **이력**: AuditLog 최근 3건 ("정다운, 2026-05-10 14:32, ₩120,000 → ₩150,000, source=manual\_user")

#### 상단 툴바

* RatePlan 셀렉터 (`BAR ▾`)
* 월 네비게이션 (`◀ 2026-05 ▶`, `[오늘]`)
* 액션 버튼: `[✎ 편집]`, `[⚡ 일괄수정]`, `[💰 프로모션]`
* 검색 (특정 룸타입·기간)

#### 하단 KPI 바 — **Phase 후속**

* 평균가격 / 점유율 / RevPAR
* **이번 ADR 범위 밖** (Booking 도메인 의존)
* 자리는 비워두고 `<KpiBarPlaceholder />` 컴포넌트로 마크
* 별도 PR(Booking 도메인 연계)에서 채움

### 2.7 일괄 수정 (Bulk Edit) 다이얼로그

Grid에서 범위 선택 후 또는 `[⚡ 일괄]` 버튼 클릭 시:

```
대상: 2026-05-15 ~ 2026-05-31 (17일)
      스탠다드, 디럭스 (2 roomType)
      = 34 cell

작업: ○ 가격 +10%        ○ 가격 절대값 설정
      ○ 가격 -₩5,000     ○ CTA 적용
      ○ Stop Sell 적용   ○ MinLOS = 2

기존 snapshot 처리:
  ◉ 새 값으로 덮어쓰기 (기존 snapshot 갱신)
  ○ snapshot 없는 cell만 적용 (확정 가격 보호)
  ○ 미리보기로 cell별 선택 후 적용
```

**모든 cell은 DailyRate row를 명시적으로 생성/갱신**한다. 일괄 수정은 ADR-0001 §2.2.2의 "특정 날짜 범위 가격을 확정 적용한 경우"에 해당하는 source of materialization.

* source: `manual_bulk`
* 단일 AuditLog 항목으로 기록 (`action=bulk_update`, `diff`에 영향 범위 + 작업 종류 요약)
* restriction 일괄 적용도 동일 흐름 (RatePlanRestriction row 생성/갱신)

### 2.8 시각적 표기 규칙

| 표기       | 의미                                         | 색상          |
| -------- | ------------------------------------------ | ----------- |
| ◆        | 확정 snapshot (DailyRate row 존재)             | 회색 dot      |
| 🧮       | 규칙 계산값 (DailyRate row 없음, resolver step 3) | (표시 없음, 기본) |
| 🔒       | CTA (Closed to Arrival)                    | 노랑          |
| 🚫       | CTD (Closed to Departure)                  | 주황          |
| ⛔        | Stop Sell                                  | 빨강          |
| ⚠️ 셀 외곽선 | 재물질화 proposal 영향 받는 셀 (사용자 결정 대기)          | 주황 외곽선      |
| ▓ 셀 배경   | 현재 선택                                      | 파랑 톤        |
| 음영 셀     | 과거 날짜 (수정 가능하나 경고)                         | 회색 톤        |

***

## 3. Out of Scope

* Booking 데이터 의존 KPI (Phase 후속 PR)
* 채널별 가격 분배 UI (ADR-0005 예정)
* Yield Engine 자동 가격 룰 UI (ADR-0004 예정)
* 모바일 전용 UI (필요 시 후속)

***

## 4. Consequences

### 4.1 Positive

* ✅ Pricing Rules JSON으로 시즌·요일 가격을 RatePlan 단위에 응집 → 운영 단순화
* ✅ DailyRate 테이블은 "예외만" 저장 → storage·성능 효율
* ✅ Wizard로 초보자 진입 장벽 ↓
* ✅ Grid + Panel로 숙련자 효율 ↑
* ✅ 일괄 수정·드래그 선택으로 yield manager 워크플로우 지원
* ✅ override가 자동 삭제되지 않음 → 사용자 작업 보호

### 4.2 Negative

* ❌ Resolver 복잡도 ↑ — pricingRules 계산 로직 + override 우선순위 + parent derivation
* ❌ JSON 필드는 type-safe하지 않음 → Zod 등 runtime 검증 필수
* ❌ Wizard와 Grid 이중 UI → 디자인·구현 비용 증가
* ❌ 시즌 정의가 RatePlan 내부에 들어가면 여러 RatePlan에서 공유 불가 → 추후 RateSeason 분리 필요 시 마이그레이션 부담

### 4.3 Risks & Mitigations

| Risk                          | Mitigation                                             |
| ----------------------------- | ------------------------------------------------------ |
| pricingRules JSON 스키마 진화      | `pricingRulesVersion: number` 필드로 버전 관리, resolver에서 분기 |
| Snapshot이 너무 많아져 Hybrid 의미 상실 | "snapshot 비율" 위젯 표시, 80% 초과 시 경고                       |
| 사용자가 규칙 계산값과 snapshot 차이를 혼동  | UI에서 ◆/🧮 표기 + Side Panel "규칙 대비 ±X" 명시                |
| 일괄 수정 실수로 대량 snapshot 생성      | 확인 다이얼로그 + 미리보기 단계 + AuditLog `bulk_update`            |
| 규칙 변경 후 재물질화 미진행으로 drift      | proposal 30일 누적 시 경고 + 운영 위젯에 "미적용 규칙 변경" 노출 (§2.4.4)  |
| 부분 재물질화 시 일관성 깨짐              | 미리보기 단계에서 영향 범위 시각화. 채널 push는 항상 resolver 최종값 사용       |

***

## 5. Alternatives Considered

### A1. Pre-materialize (Wizard 완료 시 24개월 row 생성)

* ❌ 거부 — Hybrid storage 원칙(ADR-0001 2.2) 위반. row 수 폭증.

### A2. Rule Only (DailyRate row 자체를 안 만듦)

* ❌ 거부 — override가 별도 테이블 필요 → 결국 DailyRate 재발명. 또한 Audit·Yield 연동에서 row 단위 추적 필요.

### A3. Spreadsheet UI를 메인 운영화면으로

* ❌ 거부 — 일반 사용자 학습 장벽 높음. Power user는 일괄수정 다이얼로그 + 키보드 단축키로 커버.

### A4. 명령 팔레트(Cmd+K) 기반 자연어 인터페이스

* 보류 — 흥미롭지만 NLP 정확도·다국어 대응 비용 큼. v2 후속 검토.

### A5. RateSeason을 별도 테이블로 즉시 분리

* ❌ 이번 ADR에서는 거부 — pricingRules.seasons JSON으로 시작, RatePlan 간 공유 needs가 명확해질 때 분리(YAGNI).

***

## 6. Implementation Notes

### 6.1 RatePlan 스키마 추가 필드 (ADR-0001 보강)

```prisma theme={null}
model RatePlan {
  // ... ADR-0001의 필드들 ...

  defaultAmount       Decimal  @database.Decimal(15, 4)
  pricingRules        Json?    /// [PricingRules]
  pricingRulesVersion Int      @default(1)

  // ...
}
```

### 6.2 RematerializationProposal 임시 레코드

규칙 변경 시 영향 범위 분석 결과를 보관하는 임시 엔티티 (Redis 또는 PostgreSQL TTL row, Phase 4 의존성 없음):

```prisma theme={null}
model RematerializationProposal {
  id              Bytes    @id @database.ByteA   // ULID
  ratePlanId      Bytes    @database.ByteA
  triggeredBy     Bytes    @database.ByteA       // actor user
  triggerKind     String   @database.VarChar(32) // 'pricing_rules' | 'default_amount' | 'derivation' | 'occupancy'
  beforeRulesSnapshot Json
  afterRulesSnapshot  Json
  affectedSnapshots   Int  // 영향 받는 기존 DailyRate row 수
  affectedComputed    Int  // 영향 받지만 snapshot 없는 날짜 수
  proposalDetails Json     // cell별 before/after
  status          String   @database.VarChar(16) // 'pending' | 'applied' | 'partial' | 'discarded' | 'expired'
  expiresAt       DateTime
  createdAt       DateTime @default(now())

  @@index([ratePlanId, status])
  @@index([expiresAt])
}
```

### 6.3 Resolver 모듈 위치

```
apps/core-svc/src/domains/rate-plan/
  ├── services/
  │   ├── rate-resolver.service.ts             // 가격 해결 (ADR-0001 §2.2.1)
  │   ├── pricing-rules.service.ts             // pricingRules JSON 검증·계산
  │   ├── rematerialization.service.ts         // proposal 생성·적용 (§2.4)
  │   └── rate-audit.publisher.ts              // @vpms-cluster/audit 래퍼 (ADR-0001 §2.8)
  ├── schemas/
  │   └── pricing-rules.schema.ts              // Zod 스키마
  └── workers/
      └── rematerialization-analyzer.worker.ts // BullMQ: 영향 범위 분석
```

`rate-audit.publisher.ts` 는 `@vpms-cluster/audit` 의 `publishAuditLog` / `publishAuditLogBatch` 를
rate domain 관점에서 호출하는 얇은 래퍼이다. 자체 저장소를 갖지 않고 audit-svc 로 발행만 한다.

### 6.3 Zod 스키마 (필수)

```typescript theme={null}
const PricingRulesSchema = z.object({
  dayOfWeek: z.object({
    monday: z.number().positive().optional(),
    // ... 7개 요일
  }).optional(),
  seasons: z.array(z.object({
    name: z.string().min(1).max(64),
    startMonth: z.number().int().min(1).max(12),
    // ...
    multiplier: z.number().positive(),
  })).optional(),
  combination: z.enum(['multiplicative', 'additive_to_base']).optional(),
});
```

### 6.4 UI 컴포넌트 분리

* `<RatePlanWizard />` — 5 Step 컨테이너
* `<DailyRateGrid />` — Grid (가상화 필수, 31 × N roomType × ratePlan 셀)
* `<DailyRateSidePanel />` — 선택된 셀 상세
* `<BulkEditDialog />` — 일괄 수정
* `<KpiBarPlaceholder />` — Booking 도메인 의존, placeholder

***

## 7. References

* [ADR-0001: Rate Domain 재설계](./0001-rate-domain-redesign.md)
* [Apaleo Rate Plan API](https://api.apaleo.com/swagger/index.html?urls.primaryName=Inventory%20API)
* [Mews Rates Documentation](https://mews-systems.gitbook.io/connector-api/operations/rates)
* [React Virtualized / TanStack Virtual](https://tanstack.com/virtual) — Grid 가상화
* [Zod Schema Validation](https://zod.dev/)
