Rooroo 앱 개발 가이드 (매니페스트 · 웹훅 · 스크립트 · 관리자 확장 · 블록 마켓)
개발자 포털: https://builder.iodev.kr/developers · REST API 는 docs/API.md 참고.
1. 앱 유형과 수익
| kind | 설명 | 설치 시 동작 |
|---|---|---|
app | API/웹훅/스크립트/관리자 페이지/블록을 조합한 일반 앱 | 설치별 OAuth 클라이언트+토큰 발급, 웹훅 구독 생성, 스크립트 삽입, 블록 등록 |
block | 에디터 블록(HTML/CSS 템플릿 + 필드) | 에디터 레퍼런스 패널 > 마켓 블록 |
section | 페이지 섹션(Puck 아이템 묶음) | 에디터 레퍼런스 패널 > 마켓 섹션 |
theme | 테마 프리셋 | (예약) |
가격 모델 pricing: free · one_time(1회) · subscription(월) · usage(월 + 사용량). trial_days 로 무료 체험. 테넌트에게는 플랫폼 결제수단으로 청구되고, 개발자에게는 매출의 85% 가 월 정산됩니다 (개발자 누적 매출 첫 10,000,000원까지는 수수료 0%, 이후 15%). 정산서는 개발자 포털 > 정산.
2. 등록 → 심사 → 공개
/developers/signup가입 시 샌드박스 테넌트(테스트 쇼핑몰) 가 자동 생성됩니다. 포털 대시보드의 샌드박스 관리자 계정으로/admin에 로그인해 실제 설치 상태를 확인할 수 있습니다./developers/apps에서 앱 생성 → 폼 또는 원본 JSON 으로 매니페스트 작성 → 샌드박스 설치 로 검증.- 심사 요청 → 플랫폼 운영자가 승인/반려(사유) → 승인되면 마켓에 공개. 승인 후 매니페스트/가격을 바꾸면 자동으로 다시 심사 상태가 됩니다(기존 설치는 유지).
- 정지(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.updated | API 로 회원 수정 | { 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. 설치 흐름과 자격증명
- 테넌트가 동의 →
app_installs생성 (유료면 플랫폼 PG 과금, 체험이면trial). - 매니페스트에 scopes/webhooks/admin_pages 가 있으면 설치별 OAuth 클라이언트(
client_id,client_secret) 와 1년 만료 액세스 토큰 + 리프레시 토큰 발급. app.installed웹훅으로credentials전달 → 앱 서버는install_id키로 저장. 이후Authorization: Bearer <access_token>으로 API 호출. 만료 전refresh_token또는client_credentials로 재발급.- 테넌트가 앱을 비활성화하면 토큰 호출이
403 app_disabled, 삭제하면 토큰 폐기 + 구독 제거 +app.uninstalled웹훅.
8. 블록 마켓 (테넌트 퍼블리시 포함)
- 개발자:
kind: block앱에blocks[]를 정의. 설치한 쇼핑몰의 에디터 레퍼런스 패널에 나타나며, 드롭하면AppBlockPuck 아이템(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% 구간·카테고리·레이트리밋·테넌트 블록 자동승인).