Rooroo 앱 개발 가이드 (매니페스트 · 웹훅 · 스크립트 · 관리자 확장 · 블록 마켓)

개발자 포털: https://builder.iodev.kr/developers · REST API 는 docs/API.md 참고.

1. 앱 유형과 수익

kind설명설치 시 동작
appAPI/웹훅/스크립트/관리자 페이지/블록을 조합한 일반 앱설치별 OAuth 클라이언트+토큰 발급, 웹훅 구독 생성, 스크립트 삽입, 블록 등록
block에디터 블록(HTML/CSS 템플릿 + 필드)에디터 레퍼런스 패널 > 마켓 블록
section페이지 섹션(Puck 아이템 묶음)에디터 레퍼런스 패널 > 마켓 섹션
theme테마 프리셋(예약)

가격 모델 pricing: free · one_time(1회) · subscription(월) · usage(월 + 사용량). trial_days 로 무료 체험. 테넌트에게는 플랫폼 결제수단으로 청구되고, 개발자에게는 매출의 85% 가 월 정산됩니다 (개발자 누적 매출 첫 10,000,000원까지는 수수료 0%, 이후 15%). 정산서는 개발자 포털 > 정산.

2. 등록 → 심사 → 공개

  1. /developers/signup 가입 시 샌드박스 테넌트(테스트 쇼핑몰) 가 자동 생성됩니다. 포털 대시보드의 샌드박스 관리자 계정으로 /admin 에 로그인해 실제 설치 상태를 확인할 수 있습니다.
  2. /developers/apps 에서 앱 생성 → 폼 또는 원본 JSON 으로 매니페스트 작성 → 샌드박스 설치 로 검증.
  3. 심사 요청 → 플랫폼 운영자가 승인/반려(사유) → 승인되면 마켓에 공개. 승인 후 매니페스트/가격을 바꾸면 자동으로 다시 심사 상태가 됩니다(기존 설치는 유지).
  4. 정지(suspend) 되면 모든 설치가 비활성화됩니다.

3. 매니페스트 스키마 (apps.manifest)

{
  "name": "주문 알림 → Slack",
  "version": "1.0.0",
  "description": "주문이 들어오면 Slack 채널로 알립니다",
  "scopes": ["orders:read", "products:read"],
  "redirect_uris": ["https://myapp.example.com/oauth/callback"],
  "webhooks": [
    { "event": "order.created", "url": "https://myapp.example.com/hooks/rooroo" },
    { "event": "order.paid",    "url": "https://myapp.example.com/hooks/rooroo" }
  ],
  "script_tags": [
    { "src": "https://cdn.example.com/widget.js", "position": "body_end", "pages": "all" },
    { "inline": "console.log('hello', window.__rooroo)", "position": "head", "pages": "product" }
  ],
  "admin_pages": [
    { "path": "dashboard", "title": "알림 설정", "url": "https://myapp.example.com/embed", "icon": "🔔" }
  ],
  "blocks": [
    { "name": "InstaFeed", "label": "인스타 피드", "group": "SNS",
      "html": "<div class=\"ig\"><h3>{{title}}</h3><div data-user=\"{{username}}\"></div></div>",
      "css": ".ig{padding:24px}.ig h3{font-weight:800}",
      "fields": [ { "key": "title", "label": "제목", "type": "text", "default": "인스타그램" }, { "key": "username", "label": "계정", "type": "text", "required": true } ] }
  ],
  "settings_schema": [
    { "key": "webhook_url", "label": "Slack Webhook URL", "type": "text", "required": true },
    { "key": "events", "label": "알림 이벤트", "type": "select", "options": [ { "label": "결제 완료만", "value": "paid" }, { "label": "모든 주문", "value": "all" } ], "default": "paid" }
  ],
  "pricing": { "model": "subscription", "price": 9900, "trial_days": 14 }
}
필드설명
scopes[]API.md §1.2 의 스코프. 설치 시 테넌트에게 동의 화면에 표시
redirect_uris[]authorization_code 흐름의 콜백(https). 설치별 클라이언트에도 복사됨
webhooks[]event ∈ §4 이벤트 또는 *, url https. 설치 시 설치별 구독으로 생성되며 서명 비밀 = 앱의 app_secret
script_tags[]src(https) 또는 inline(≤20KB), position head/body_end, pages all/home/product/cart/checkout
admin_pages[]관리자 콘솔 메뉴. url 은 iframe 으로 열리며 서명 토큰이 붙음(§6)
blocks[]html 안에서 {{key}}(이스케이프) {{{key}}}(원문) {{#if key}}…{{/if}} 사용. fields[].type ∈ text/textarea/number/color/image/select/boolean
settings_schema[]테넌트가 설치 후 입력하는 설정. 값은 app.installed 웹훅 settings 와 스크립트의 window.__rooroo.apps["<slug>"] 로 전달
pricing포털의 가격 필드와 동일(둘 중 포털 값이 우선)

검증은 zod(src/lib/apps/manifest.ts) 로 수행되며 오류는 한글 메시지로 반환됩니다. 블록 name 과 설정 key 는 중복 불가.

4. 웹훅

4.1 이벤트

이벤트시점data
order.created주문 접수{ order }
order.paid결제 승인{ order }
order.status_changed관리자/API 상태 변경{ order_id, order_no, from, to, reason, by }
order.cancelled취소 요청/취소/환불{ order_id, order_no, status, reason }
member.created회원 가입(일반/SNS){ member_id, name, phone, email, provider }
member.updatedAPI 로 회원 수정{ member }
product.created / product.updated / product.deleted상품 변경{ product_id, name?, changed? }
stock.changed매장별 가격/재고 변경{ product_id, stores[] }
page.published페이지 발행{ page_id, path, title }
app.installed앱 설치{ install_id, app, site, tenant_id, status, settings, credentials:{ client_id, client_secret, access_token, api_base } }
app.uninstalled앱 삭제{ install_id, app, site_id }
ping테스트 전송{ message }

4.2 전송 형식

POST {url}
Content-Type: application/json
User-Agent: Rooroo-Webhooks/1.0
X-Rooroo-Event: order.paid
X-Rooroo-Delivery: 6f1c…(UUID)
X-Rooroo-Timestamp: 1762150000
X-Rooroo-Signature: sha256=<hex HMAC-SHA256(rawBody, secret)>

{ "id": "6f1c…", "event": "order.paid", "site_id": 1, "created_at": "2026-11-03T09:00:00.000Z", "data": { … } }
  • 2xx 가 아니면 실패로 간주, 최대 5회 재시도(1분 → 5분 → 30분 → 2시간 → 12시간). 첫 시도는 이벤트 발생 직후 인라인, 재시도는 scripts/apps-worker.ts.
  • 연속 25회 실패 시 구독이 failed 로 전환됩니다(관리자 콘솔에서 재개).
  • 수신 측은 X-Rooroo-Delivery 로 중복 처리를 방지하고, 8초 안에 응답하세요(이후 비동기 처리).

4.3 서명 검증 (Node)

import crypto from "node:crypto";
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected.length === header.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
  • 마켓 앱의 매니페스트 웹훅: secret = app_secret (개발자 포털 > 앱 > 자격증명).
  • 테넌트가 직접 만든 구독 / API 로 만든 구독: 생성 시 반환된 secret.
  • 로컬 테스트용 에코 수신기: POST /api/webhooks/echo?secret=… (수신 로그는 관리자 로그인 후 GET /api/webhooks/echo).

5. 스크립트 태그

설치된 앱의 script_tags 는 쇼핑몰(/s/{slug}/…) 모든 페이지의 본문 상단(head) 또는 하단(body_end)에 <template data-sb-app> 으로 내려가고, 로더가 현재 페이지 종류(home/product/cart/checkout/other)에 맞는 것만 <script nonce> 로 활성화합니다.

  • 스크립트에서 window.__rooroo.page (페이지 종류), window.__rooroo.apps["<app-slug>"] (테넌트가 입력한 settings) 를 사용할 수 있습니다.
  • src 는 https 만 허용(설치 시점에 재검증). 비활성화/삭제된 설치의 스크립트는 즉시 내려가지 않습니다.
  • Next.js 레이아웃 제약으로 position: head 도 실제 <head> 가 아니라 본문 최상단에 삽입됩니다(실행 순서만 보장).

6. 관리자 확장 페이지 (iframe) 와 토큰 검증

테넌트가 관리자 콘솔 > 앱 마켓 > 설치된 앱 > 앱 열기 를 누르면 admin_pages[].url 이 iframe 으로 열리며 다음 쿼리가 붙습니다:

{url}?token=<JWT>&install_id=<id>&site=<slug>

JWT 는 HS256, 앱의 app_secret 으로 서명, 10분 만료, 클레임:

{ "iss": "rooroo", "sub": "install:123", "install_id": 123, "tenant_id": 1, "site_id": 1, "app_id": 7, "scopes": ["orders:read"], "admin": "didwpdn", "site_slug": "garam", "iat": 1762150000, "exp": 1762150600 }

검증 예 (Node, jose):

import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, new TextEncoder().encode(APP_SECRET), { issuer: "rooroo" });
// payload.install_id 로 설치 시 받은 access_token 을 찾아 API 호출

iframe 은 sandbox="allow-scripts allow-forms allow-same-origin allow-popups" 로 열리며 10분마다 토큰이 갱신된 URL 로 다시 로드됩니다. 페이지는 반응형(모바일 폭)으로 만들어 주세요.

7. 설치 흐름과 자격증명

  1. 테넌트가 동의 → app_installs 생성 (유료면 플랫폼 PG 과금, 체험이면 trial).
  2. 매니페스트에 scopes/webhooks/admin_pages 가 있으면 설치별 OAuth 클라이언트(client_id, client_secret) 와 1년 만료 액세스 토큰 + 리프레시 토큰 발급.
  3. app.installed 웹훅으로 credentials 전달 → 앱 서버는 install_id 키로 저장. 이후 Authorization: Bearer <access_token> 으로 API 호출. 만료 전 refresh_token 또는 client_credentials 로 재발급.
  4. 테넌트가 앱을 비활성화하면 토큰 호출이 403 app_disabled, 삭제하면 토큰 폐기 + 구독 제거 + app.uninstalled 웹훅.

8. 블록 마켓 (테넌트 퍼블리시 포함)

  • 개발자: kind: block 앱에 blocks[] 를 정의. 설치한 쇼핑몰의 에디터 레퍼런스 패널에 나타나며, 드롭하면 AppBlock Puck 아이템(props: { appBlockId:"<slug>:<name>", html, css, fields, v_<key>… })으로 삽입되고 필드 값이 {{key}} 에 치환됩니다.
  • kind: section 앱은 data 에 Puck 아이템 배열을 넣습니다(블록 타입은 빌더 카탈로그 기준).
  • 테넌트도 관리자 콘솔 > 앱 마켓 > 내 블록·섹션 판매 에서 자기 커스텀 블록/페이지 섹션을 올릴 수 있습니다. 테넌트용 개발자 계정이 자동 생성되고 같은 심사/정산 규칙이 적용됩니다.

9. 운영 (플랫폼)

  • 워커: npx tsx scripts/apps-worker.ts --webhooks (재시도, 1분 주기 권장) · --renew (구독 갱신/만료, 일 1회) · --payouts [--period=YYYY-MM] (전월 정산 계산, 월 1회)
  • 시드: npx tsx scripts/seed-apps.ts [--echo=http://127.0.0.1:3500/api/webhooks/echo] — 데모 개발자(dev@example.com / dev1234), 샘플 앱 3종 + 블록 1종, garam 설치.
  • 플랫폼 콘솔 /platform/apps: 심사 큐, 앱/개발자 목록, 정산, 마켓 설정(수수료율·0% 구간·카테고리·레이트리밋·테넌트 블록 자동승인).