Skip to main content
앱은 서드파티 코드를 실행하지 않고 선언적 블록 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)
callbackUrlwebhook 수신 엔드포인트 (설치 시 구독 자동 생성)
eventSubscriptions구독할 이벤트 타입. 비우면 전체 구독
configSchema설치 단위 설정 스키마 (configForm 블록이 렌더링)
surfacesUI 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": "연동 흐름" }
urlhttps 만 허용 (VPMS 프론트가 https 라 mixed-content 차단).

divider

{ "type": "divider" }

configForm

manifest.configSchema 를 폼으로 렌더링해 설치 단위 설정을 입력받는다. 값은 Installation.config 에 저장되고 앱은 GET /api/v1/installationconfig 로 읽는다. 저장 시 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표시 라벨
typetext 또는 ratePlan (VPMS 요금제 선택 — 값은 요금제 id)
requiredtrue 면 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블록 제목
cardinalityONE_TO_ONE(기본) 또는 ONE_TO_MANY
requiredtrue 면 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 에 선언된 것만이다. 선언 밖 조합으로 매핑을 시도하면 setResourceMappingBAD_REQUEST 로 거부한다.

4. action 상세

버튼 클릭 → action.invoked webhook 발행 → (선택) 앱이 완료 보고.
필드필수설명
keyaction 식별자. dataset key 와 동일 규칙. 표면 내 유일
title블록 제목 (≤200자)
description보조 설명 (≤200자)
buttonLabel버튼 텍스트 (기본 “실행”)
completionnone(기본) 또는 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:writefolio·결제 조회 / 생성
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_REQUIREDrequired 매핑/설정 없음 — 설정 단계 스킵
PROVISIONING앱의 dataset 푸시(및 explicit 시 setup-ready) 대기
SETUP_REQUIRED데이터 준비됨 — 사용자 매핑/필수 설정 입력 필요
CONFIGUREDrequired 매핑 + 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/mappingsmarketplace:mappings:read
POST/api/v1/installation/setup-readymarketplace:datasets:write
POST/api/v1/action-runs/{run_id}/completebearer (자기 설치 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"
    }
  }
}