앱은 서드파티 코드를 실행하지 않고 선언적 블록 JSON 으로 VPMS 안에 자기 설정 화면을
그린다. 서버가 소유한 컴포넌트 레지스트리로만 렌더링되므로 manifest 는 심사 대상이며,
알 수 없는 블록/필드는 거부된다. 이 문서는 manifest.surfaces (Level 1 UI Kit) 의 전체
레퍼런스다.
1. Manifest 최상위 구조
앱 버전을 생성할 때(createAppVersion) 제출하는 manifest 의 형태:
{
"name": "V-Cloud Sync",
"version": "1.0.0",
"requestedScopes": ["reservations:write", "marketplace:datasets:write"],
"callbackUrl": "https://your-app.example.com/webhooks/vpms",
"eventSubscriptions": ["installation.activated", "mapping.updated", "action.invoked"],
"surfaces": { "settings": { "blocks": [ /* … */ ] } }
}
| 필드 | 필수 | 설명 |
|---|
name | ✓ | 앱 이름 |
version | ✓ | 버전 문자열 (semver 권장) |
requestedScopes | ✓ | 요청 권한 배열. 카탈로그 밖 scope 는 거부 (§5) |
callbackUrl | | webhook 수신 엔드포인트 (설치 시 구독 자동 생성) |
eventSubscriptions | | 구독할 이벤트 타입. 비우면 전체 구독 |
configSchema | | 설치 단위 설정 스키마 (configForm 블록이 렌더링) |
surfaces | | UI Kit 표면 선언 (아래) |
UI Kit 표면은 현재 settings 하나만 지원한다 — 앱별 설정 페이지이며, 설치 직후 최초
방문이 온보딩을 겸한다. 블록은 최대 32개.
2. surfaces.settings 블록 종류
surfaces.settings.blocks 는 아래 6종 블록의 배열이다. 렌더러는 선언 순서대로 그린다.
markdown
정적 안내 텍스트 (GFM). 최대 20,000자.
{ "type": "markdown", "body": "## 연동 안내\n외부 채널 예약을 자동 동기화합니다." }
image
{ "type": "image", "url": "https://cdn.example.com/guide.png", "alt": "연동 흐름" }
url 은 https 만 허용 (VPMS 프론트가 https 라 mixed-content 차단).
divider
manifest.configSchema 를 폼으로 렌더링해 설치 단위 설정을 입력받는다. 값은
Installation.config 에 저장되고 앱은 GET /api/v1/installation 의 config 로 읽는다.
저장 시 config.updated webhook 이 발행된다.
{ "type": "configForm", "title": "동기화 설정" }
configSchema (manifest 최상위):
{
"configSchema": { "fields": [
{ "key": "baseRatePlanId", "label": "기준 요금제", "type": "ratePlan",
"required": true, "description": "동기화 예약이 분류될 요금제" }
] }
}
| 필드 | 필수 | 설명 |
|---|
key | ✓ | 설정 key (^[a-zA-Z][a-zA-Z0-9_]{0,63}$, 표면 내 유일) |
label | ✓ | 표시 라벨 |
type | ✓ | text 또는 ratePlan (VPMS 요금제 선택 — 값은 요금제 id) |
required | | true 면 setupStatus 판정 대상 — 값이 없으면 CONFIGURED 가 되지 않는다 |
description | | 보조 설명 |
entityMapping
VPMS 엔티티 ↔ 앱이 푸시한 외부 항목(ExternalDataset)을 잇는 매핑 편집기.
가장 핵심 블록이다 — §3 참조.
{
"type": "entityMapping",
"title": "객실 타입 매핑",
"vpmsEntity": "roomType",
"externalDataset": "vcloud_room_types",
"cardinality": "ONE_TO_ONE",
"required": true
}
action
사용자가 버튼 한 번으로 앱에 작업을 위임하는 트리거 — §4 참조.
{
"type": "action",
"key": "sync_reservations",
"title": "외부 예약 동기화",
"description": "기존 예약을 VPMS 로 가져옵니다.",
"buttonLabel": "동기화",
"completion": "event"
}
3. entityMapping 상세
호텔(테넌트)이 VPMS 엔티티와 앱의 외부 엔티티를 잇는다. 왼쪽은 우리 데이터(예: 객실
타입), 오른쪽은 앱이 Open API 로 푸시한 dataset 항목이다.
| 필드 | 필수 | 설명 |
|---|
vpmsEntity | ✓ | 매핑 대상 VPMS 엔티티 (아래) |
externalDataset | ✓ | 앱이 푸시한 dataset key. ^[a-z0-9][a-z0-9_-]{0,63}$ |
title | | 블록 제목 |
cardinality | | ONE_TO_ONE(기본) 또는 ONE_TO_MANY |
required | | true 면 setupStatus 판정 대상 (§6) |
vpmsEntity 종류
roomType · room · package · ratePlan · accommodation · channel
accommodation — 설치 숙소 자신 1행. 외부 시스템 계정 페어링(account linking) 용.
channel — 판매 채널(거래처 연결 단위). 외부 플랫폼 코드 매핑용.
cardinality 의미
- ONE_TO_ONE — VPMS 엔티티 ↔ 외부 항목이 양방향 1:1. 한 쪽을 재배정하면 반대쪽
기존 매핑을 대체한다(steal).
- ONE_TO_MANY — 한 VPMS 엔티티가 여러 외부 항목을 가질 수 있다. 단, 외부 항목은
항상 하나의 엔티티에만 속한다(외부→엔티티 해석 유일성). 예: 한 채널(거래처)에 여러
플랫폼을 묶되, 한 플랫폼은 한 채널에만 배정.
매핑 가능한 (vpmsEntity, externalDataset) 조합은 manifest surface 에 선언된 것만이다.
선언 밖 조합으로 매핑을 시도하면 setResourceMapping 이 BAD_REQUEST 로 거부한다.
4. action 상세
버튼 클릭 → action.invoked webhook 발행 → (선택) 앱이 완료 보고.
| 필드 | 필수 | 설명 |
|---|
key | ✓ | action 식별자. dataset key 와 동일 규칙. 표면 내 유일 |
title | ✓ | 블록 제목 (≤200자) |
description | | 보조 설명 (≤200자) |
buttonLabel | | 버튼 텍스트 (기본 “실행”) |
completion | | none(기본) 또는 event |
completion: "none" — 버튼을 누르면 webhook 발행 즉시 완료로 간주한다.
completion: "event" — 앱이 작업을 마친 뒤
POST /api/v1/action-runs/{run_id}/complete 로 결과를 보고할 때까지 프론트가 진행 중
상태를 표시한다.
action.invoked payload: { "action": "<key>", "run_id": "<id>", "completion": "none|event" }.
action 버튼은 required entityMapping 이 모두 채워지기 전(PROVISIONING·SETUP_REQUIRED)에는
비활성이다 — 매핑 없이 위임된 작업은 앱 쪽에서 실패/부분 처리되기 때문.
5. requestedScopes 카탈로그
resource:action 표기. 카탈로그 밖 값은 manifest 검증에서 거부된다.
| scope | 용도 |
|---|
reservations:read / reservations:write | 예약 조회 / 생성·수정 |
folios:read / folios:write | folio·결제 조회 / 생성 |
rooms:read | 객실·객실타입 조회 |
rates:read / rates:write | 요금제 조회 / 수정 |
inventory:read / inventory:write | 재고 조회 / 수정 |
profiles:read / profiles:write | 고객 프로필 조회 / 수정 |
marketplace:datasets:write | 매핑 후보 dataset 푸시 |
marketplace:mappings:read | 확정된 매핑 조회 |
marketplace:events:write | 이벤트 제출 |
marketplace:usage:read | 사용량 요약 조회 |
6. setupStatus 상태 머신
설치 후 초기 설정 진행 상태. InstallationStatus(토큰 발급 기준) 와 직교하며, API 를
게이팅하지 않는다 — 앱/프론트가 상태를 보고 스스로 판단한다.
| 상태 | 의미 |
|---|
NOT_REQUIRED | required 매핑/설정 없음 — 설정 단계 스킵 |
PROVISIONING | 앱의 dataset 푸시(및 explicit 시 setup-ready) 대기 |
SETUP_REQUIRED | 데이터 준비됨 — 사용자 매핑/필수 설정 입력 필요 |
CONFIGURED | required 매핑 + required 설정 값 모두 저장됨 — 완전 동작 |
파생 계산이라 다운그레이드도 일어난다(매핑 삭제 → SETUP_REQUIRED 복귀). 변경 시
installation.setup_status_changed webhook 이 발행된다.
surfaces.settings.readiness 로 PROVISIONING → SETUP_REQUIRED 전환 조건을 정한다:
implicit(기본) — required 매핑이 참조하는 dataset 이 하나라도 준비되면 전환.
explicit — 추가로 앱이 POST /api/v1/installation/setup-ready 를 호출해야 전환.
7. 데이터 흐름 요약
[앱] PUT /api/v1/datasets/{key} (매핑 후보 항목 푸시; datasets:write)
↓
[VPMS] surfaces.settings 렌더 → 호텔이 entityMapping 편집
↓ setResourceMapping 뮤테이션 → mapping.updated webhook
[앱] GET /api/v1/mappings (확정 매핑 조회; mappings:read)
↓
[VPMS] action 버튼 → action.invoked webhook (+ run_id)
[앱] POST /api/v1/action-runs/{id}/complete (completion=event)
관련 Open API 엔드포인트:
| 메서드 | 경로 | scope |
|---|
PUT | /api/v1/datasets/{key} | marketplace:datasets:write |
GET | /api/v1/datasets/{key} | marketplace:datasets:write |
GET | /api/v1/mappings | marketplace:mappings:read |
POST | /api/v1/installation/setup-ready | marketplace:datasets:write |
POST | /api/v1/action-runs/{run_id}/complete | bearer (자기 설치 run) |
8. 전체 예시
객실 타입(필수) + 판매 채널(선택) 매핑 + 수동 동기화 action 을 갖춘 표면:
{
"surfaces": {
"settings": {
"blocks": [
{ "type": "markdown", "body": "## 외부 채널 연동" },
{ "type": "entityMapping", "title": "숙소 연결", "vpmsEntity": "accommodation",
"externalDataset": "ext_accounts", "cardinality": "ONE_TO_ONE", "required": true },
{ "type": "entityMapping", "title": "객실 타입 매핑", "vpmsEntity": "roomType",
"externalDataset": "ext_room_types", "cardinality": "ONE_TO_ONE", "required": true },
{ "type": "entityMapping", "title": "판매 채널 매핑", "vpmsEntity": "channel",
"externalDataset": "ext_platforms", "cardinality": "ONE_TO_MANY" },
{ "type": "action", "key": "sync_now", "title": "지금 동기화",
"buttonLabel": "동기화", "completion": "event" }
],
"readiness": "implicit"
}
}
}