문서 / 앱 붙이기
이 문서의 큐
앱 백엔드 붙이기
정적 사이트가 아니라 네이티브 앱(iOS·Android·Flutter·React Native)을 새로온 백엔드에 붙이는 흐름입니다. 회원·게시판·피드를 앱에서 바로 씁니다.
다른 문서들이 다루는 data-saeroon-* 속성은 브라우저 SDK가 DOM에 붙는 방식이라 앱에서는 쓸 수 없습니다. 앱은 아래의 REST를 직접 호출합니다.
키가 두 종류입니다
앱을 붙일 때 가장 많이 헷갈리는 지점입니다. 용도가 다르고, 서로 대체되지 않습니다.
| 키 | 쓰는 곳 | 누가 들고 있나 |
|---|---|---|
sk_live_... | 백엔드를 운영할 때 (CLI·MCP·관리 REST) | 개발자·CI. 앱에 넣지 마세요 |
| 세션 토큰 (7일) | 앱이 런타임에 API를 부를 때 | 앱. 소셜 로그인 응답의 body로 받습니다 |
sk_live_를 앱에 넣으면 그 키로 사이트 전체를 조작할 수 있게 됩니다. 앱에는 세션 토큰만 둡니다.
1. 키 발급
developers.saeroon.com/keys 에서 새 API Key 생성. 생성 직후 모달에서만 전체 값을 볼 수 있습니다.
키는 사용자당 발급되며, 그 사용자가 소유한 사이트에 닿습니다. 사이트 소유자와 키 소유자가 다르면 404가 납니다 — 401이 아니라 404라서 사이트 ID를 틀린 것처럼 보입니다. 이 점을 먼저 의심하세요.
export SAEROON_API_KEY=sk_live_...
2. 게시판 시드 — CLI
앱이 붙을 게시판을 만들고 boardId를 받습니다. 앱은 slug가 아니라 boardId로 배선합니다.
npx @saeroon/cli board create \
--site <siteId> --name "피드" --slug feed \
--type general --write-mode open --json
{ "ok": true, "created": true, "board": { "id": "<boardId>", "slug": "feed", "writeMode": "open" } }
멱등합니다. 같은 slug로 다시 실행하면 "created": false와 함께 기존 board를 돌려줍니다 — 중복 생성이 없으니 스크립트에서 안심하고 반복할 수 있습니다.
--allow-anonymous를 넣지 않으면 익명 게시가 막히고 로그인한 회원만 글을 씁니다(기본값). 읽기는 로그인 없이 열려 있습니다.
3. 게시판 시드 — MCP
에이전트(Claude 등)로 붙일 때는 MCP가 편합니다.
claude mcp add --transport http saeroon https://hosting.saeroon.com/api/mcp/streamable
브라우저가 열리면 사이트를 소유한 계정으로 로그인합니다. 다른 계정으로 로그인하면 연결은 되지만 board 도구가 그 사이트에서 실패합니다.
| 도구 | 하는 일 |
|---|---|
saeroon.platform.board.list | 사이트의 게시판 목록 — site_id |
saeroon.platform.board.create | 게시판 생성(멱등) — site_id, name, slug, type, write_mode |
MCP는 sk_live_가 아니라 OAuth 토큰으로 인증합니다. MCP 설정에 sk_live_를 넣으면 401이 납니다.
4. 소셜 로그인 — 세션 토큰 받기
앱이 provider에서 받은 idToken을 넘기면 7일짜리 세션 토큰을 body로 돌려줍니다. 쿠키가 아닙니다.
POST https://api.saeroon.com/api/v1/hosting/public/sites/{siteId}/auth/social/{provider}/native
provider = apple | google
{ "idToken": "<provider id_token>", "authorizationCode": null, "nonce": null }
응답:
{
"sessionToken": "...",
"expiresAt": "2026-07-24T00:00:00Z",
"member": {
"id": "<uuid>",
"nickname": "",
"tier": "free",
"entitlementActive": false,
"entitlementSource": null
}
}
member 는 /me 와 /me/links 도 같은 shape 로 반환합니다.
entitlementActive— 등급이 있는지(대략tier ≠ free).entitlementSource— 등급 부여 출처:"iap"(결제) ·"coupon"(컴프 쿠폰) ·null(무료/운영자 수동).
결제 후원자와 쿠폰 회원은 둘 다 entitlementActive: true 라 tier·active 만으로는 구분되지 않습니다. 이 필드로 구분하세요. active 인데 entitlementSource 가 없으면(옛 응답) 결제로 간주하는 게 안전합니다 — 그래서 쿠폰 부여는 항상 "coupon" 을 싣습니다(새 기기·재설치 재로그인에서 오표기 방지).
이후 모든 회원 호출에 그대로 붙입니다.
Authorization: Bearer <sessionToken>
member.id는 회원의 내부 UUID입니다. provider의 subject id가 아닙니다. 글의 작성자와 상관되는 키가 이 UUID라, 차단·본인글 판별은 전부 이 값으로 합니다.
이 엔드포인트는 사이트별로 소셜 로그인이 등록돼 있어야 동작합니다. 등록 전에는 404로 닫혀 있습니다 (크래시가 아니라 fail-closed).
소셜 자격증명 등록은 아직 셀프서브가 아닙니다. 보드 시드(§2·§3)는 CLI·MCP로 직접 하지만, Apple·Google 자격증명 등록은 현재 플랫폼 운영자가 대신 넣어 드립니다 — 앱 번들 ID와 provider client ID를 문의로 전달하면 등록해 드립니다. (자격증명 등록 전까지/native는404이므로, 앱은 이 응답을 "로그인 미구성" 상태로 다루면 됩니다.)
5. 피드 읽기
GET .../sites/{siteId}/community/boards/{boardId}/posts?sort=new&page=1&pageSize=20
경로에 /community/ 세그먼트가 들어갑니다. 빠뜨리면 404입니다.
| 파라미터 | 값 |
|---|---|
sort | new (최신순) · many (많이 담긴 순) · today (오늘, KST 기준) |
category | 정확히 일치하는 문자열만 필터 |
page / pageSize | 1부터. size도 별칭으로 받습니다 |
로그인 없이 읽힙니다. 응답은 { items, total, page, pageSize }입니다.
sort에 없는 값을 주면 400이 아니라 기본 정렬로 조용히 폴백합니다. 오타가 에러로 드러나지 않으니 클라이언트에서 값을 고정하세요.
6. 피드 쓰기
POST .../sites/{siteId}/community/boards/{boardId}/posts
Authorization: Bearer <sessionToken>
{ "title": "...", "content": "...", "category": "인사" }
title과 content는 필수입니다. 빠뜨리면 400이 납니다.
{ "errors": { "Title": ["The Title field is required."] } }
같은 내용은 한 장으로 — 중복 방지
payload 필드를 함께 보내면 그 값의 해시로 게시판 안에서 중복을 걸러냅니다. 이미 같은 내용이 있으면 새로 만들지 않고 기존 글을 그대로 돌려줍니다.
새로 만들었는지 기존 것을 받았는지는 HTTP 상태 코드로만 구분합니다. 응답 body에는 표시가 없습니다.
| 상태 | 뜻 |
|---|---|
201 | 새로 만들어짐 |
200 | 이미 있던 글을 돌려줌 |
중복 방지가 필요 없는 게시판(예: 짧은 응원처럼 같은 말이 여러 번 올라와도 되는 곳)에서는 payload를 보내지 마세요. 보내면 같은 문구가 한 장으로 합쳐집니다.
7. 담기(좋아요) 토글
POST .../sites/{siteId}/community/posts/{postId}/like
로그인 없이 됩니다. 클라이언트가 만든 익명 ID를 키로 중복 없이 셉니다. 응답은 { "isLiked": true, "likeCount": 12 }입니다.
8. 신고
부적절한 글·댓글을 신고합니다. 로그인 없이도 됩니다(reporterEmail은 선택).
POST .../sites/{siteId}/community/reports
{ "targetType": "Post", "targetId": "<글 또는 댓글 UUID>", "reason": "Inappropriate", "description": null, "reporterEmail": null }
| 필드 | 값 |
|---|---|
targetType | Post · Comment |
reason | Spam · Harassment · Inappropriate · Copyright · Other |
description | 선택. 자유 서술 |
신고 누계가 임계(기본 3)에 도달하면 대상이 자동으로 숨겨집니다(App Store UGC 요건). 임계값은 사이트별 설정입니다.
9. 차단
한 회원이 다른 회원을 차단하면, 차단한 사람의 피드·댓글에서 차단당한 사람의 글이 사라집니다(단방향, 뷰어 기준). 로그인 필수입니다.
POST .../sites/{siteId}/community/blocks Authorization: Bearer <sessionToken>
DELETE .../sites/{siteId}/community/blocks/{blockedMemberId}
GET .../sites/{siteId}/community/blocks
{ "blockedMemberId": "<차단 대상 회원 UUID>" }
blockedMemberId는 §4에서 설명한 회원 내부 UUID(authorId로 오는 값)입니다. 자기 자신은 차단할 수 없습니다. POST는 멱등입니다(이미 차단이면 그대로). 차단 해제는 DELETE.
10. 계정 삭제
회원 본인이 자기 계정을 삭제합니다. 로그인 필수. Apple·Google 스토어의 계정삭제 요건입니다.
DELETE .../sites/{siteId}/me Authorization: Bearer <sessionToken>
호출 즉시 계정이 하드 삭제되고, 그 회원이 쓴 글·댓글의 작성자명은 "[탈퇴한 회원]"으로 익명화됩니다 (글 자체는 남고, 작성자 연결만 끊깁니다). 세션은 무효화되어 기존 토큰은 이후 401이 납니다. 프로필·비밀번호·소셜 연결·차단 기록은 함께 삭제됩니다. 되돌릴 수 없습니다.
응답: { "message": "탈퇴 처리되었습니다." }
응답 필드 이름
읽기 응답의 이름이 직관과 다를 수 있어 미리 적어 둡니다.
| 받는 값 | 의미 |
|---|---|
title | 목록 응답에는 content가 없습니다. 본문은 title로 옵니다 |
likeCount | 담긴 수 |
authorName | 작성자 표시 이름 |
authorId | 작성자 회원 UUID (차단·본인글 판별 키) |
metadata | 글마다 넣어 둔 JSON. 클라이언트가 넣은 값은 그대로 보관·반환됩니다 |
11. 쿠폰 redeem — 회원 등급을 코드로 영구 부여
로그인한 회원이 쿠폰 코드를 입력하면 회원 등급을 영구 부여받습니다(만료 없음). 세션 토큰 Bearer 필수.
POST https://api.saeroon.com/api/v1/hosting/public/sites/{siteId}/coupons/redeem
Authorization: Bearer <sessionToken>
Content-Type: application/json
{ "code": "MC-XXXX-XXXX" }
성공(200):
{ "tier": "gold", "active": true, "grantedBy": "coupon" }
- 성공하면 회원의
tier가 오르고, 이후/native·/me의entitlementSource가"coupon"이 됩니다. - 단일사용·회수는 서버가 강제합니다(온라인 관리형 — 오프라인 해시 쿠폰의 재사용 한계 해소).
- 잘못된/만료된/이미 쓰인 코드 = 비-auth 4xx(사용 불가 쿠폰으로 처리).
401/403·5xx= 진짜 실패(재시도/문의). - 이미 결제(
iap) 후원자면409— 컴프 쿠폰이 결제 정체성을 덮지 않습니다.
쿠폰 발급/회수/사용내역은 개발자센터 도구(CLIcoupon *· MCPsaeroon.platform.coupon.*)로 하는 운영자 작업입니다 — 앱은 redeem 만 호출합니다.memberscapability 를 붙인 사이트면 어디나 쓸 수 있는 범용 기능입니다.
지금 되는 것 / 아직인 것
정직하게 적습니다. 아래 표에 없는 기능은 문서에 없는 게 아니라 아직 없습니다.
| 기능 | 상태 |
|---|---|
| 게시판 시드 (CLI·MCP) | 됩니다 (셀프서브) |
| 소셜 로그인 → 세션 토큰 | 사이트별 소셜 등록 후 됩니다 (등록은 현재 운영자 대행 — §4) |
| 피드 읽기 · 쓰기 · 중복 방지 | 됩니다 |
| 담기 토글 | 됩니다 |
| 신고 · 차단 | 됩니다 (§8·§9) |
계정 삭제 (DELETE /me) | 됩니다 (§10) |
회원 등급 쿠폰 redeem (POST …/coupons/redeem) | 됩니다 (§11) |
회원 shape 의 entitlementSource (iap/coupon) | 됩니다 (§2 응답) |
| 인앱결제 영수증 검증 | 준비 중 (redeem 과 같은 등급 부여 경로) |
다음
요약
정적 사이트가 아니라 네이티브 앱(iOS·Android·Flutter·React Native)을 새로온 백엔드에 붙이는 흐름입니다. 회원·게시판·피드를 앱에서 바로 씁니다.