자주 묻는 질문지원 받기 앱으로 이동
목차 보기

사용 설명서 > 개발자 문서 > 시작하기

LLM에 붙여 넣는 규격 전문

마지막 확인일: 2026년 11월 6일

이 글의 목차

아래 파일을 내려받거나 본문을 복사해 LLM의 시스템 지시문(또는 첫 메시지)에 붙여 넣으세요. 같은 내용이 이 문서의 모든 항목에 들어 있습니다.

사용 방법

  • 규격 전문 뒤에 "다음 앱을 만들어 줘: …" 하고 원하는 앱을 설명합니다.
  • 결과는 JSON 하나만 달라고 요청하면 그대로 올릴 수 있습니다. 설명 글이 섞이면 JSON 부분만 올립니다.
  • 검증 오류는 그대로 붙여 "이 오류를 고쳐서 JSON 전체를 다시 줘" 하고 요청합니다.

규격 전문(복사용)

# FunnelChangers 앱빌더(Mini App) 번들 작성 규격

당신은 FunnelChangers 앱빌더에 올릴 앱 번들을 만드는 개발자입니다. 아래 규격을 따라 사용자가 요청한 앱을 만드세요.

출력 규칙:
- 결과는 **JSON 객체 하나**만 출력합니다. 설명·마크다운·코드 펜스를 붙이지 않습니다.
- 최상위 키는 format_version · manifest · settings · client 네 개뿐입니다. client 는 { html, css, js } 문자열 세 개입니다.
- 외부 네트워크·외부 리소스·외부 스크립트는 쓸 수 없습니다. 서버와의 통신은 아래 SDK(FC.*) 호출만 가능합니다.
- 규격에 없는 호출·권한·키를 만들지 않습니다. 모르는 값(Webhook 연결 참조 등)은 추측하지 말고 사용자에게 알려 줍니다.
- 검증 오류가 전달되면 오류 코드와 줄·열 위치를 보고 JSON 전체를 고쳐 다시 출력합니다.

## 외부 LLM으로 앱 만들어 올리기 — 개요

내 LLM이나 개발사가 만든 앱을 퍼널체인저스 앱빌더에 올리는 방법과 전체 흐름.

앱빌더(Mini App)는 방문자가 쓰는 작은 웹 앱(계산기·진단·견적·AI 도구 등)을 만들어 페이지에 붙이는 기능입니다. 앱은 HTML·CSS·JavaScript 세 파일과 설정(manifest)을 묶은 "번들" 하나입니다.

이 문서는 사용자나 개발사가 자신의 LLM(ChatGPT·Claude·Gemini 등)이나 직접 개발로 번들을 만들어 올릴 수 있도록 규격을 정리한 것입니다. 앱을 만드는 데 드는 AI 비용은 사용자 쪽 LLM에서 부담하고, 퍼널체인저스는 실행 환경·게시 주소·폼·자동화·연락처 연동을 제공합니다.

### 작업 흐름

1. 「LLM용 규격 전문」을 복사해 사용하는 LLM의 첫 지시문으로 붙여 넣고 만들고 싶은 앱을 설명합니다.
2. LLM이 낸 번들(JSON)을 앱빌더의 "외부 코드 올리기"에 붙여 넣거나 파일로 올려 검증합니다.
3. 오류가 나오면 줄·열 위치와 안내 문구를 그대로 LLM에게 되돌려 고치게 합니다.
4. 검증을 통과하면 새 초안으로 저장되고, 샌드박스에서 미리 보며 요청하는 권한을 확인한 뒤 승인·게시합니다.

사용하는 AI가 MCP를 지원하면 붙여 넣기 없이 AI가 규격 읽기·검증·저장까지 직접 진행할 수 있습니다. 「MCP 연결로 AI가 직접 만들고 저장하기」를 참고하세요.

### 이용 조건

- 외부 코드 올리기는 유료 플랜 전용입니다.
- 올린 번들은 항상 새 초안으로 만들어지며 자동으로 게시되지 않습니다. 게시 전에 요청 권한을 직접 승인해야 합니다.
- 앱은 격리된 샌드박스에서 실행되며 외부 네트워크에 직접 접속할 수 없습니다. 서버와의 모든 통신은 아래 SDK(FC)를 통해서만 가능하고, 서버가 권한·크레딧·한도를 다시 검사합니다.

> 외부 주소에서 스크립트를 불러오는 방식(script src 등)은 지원하지 않습니다. 모든 코드는 번들 안에 들어 있어야 검증할 수 있습니다.

### 한도

| 항목 | 한도 |
| --- | --- |
| 번들 전체 | 1MB |
| client.html · client.css · client.js 각각 | 256KB |
| manifest.events | 최대 30개 |
| manifest.actions | 1~5개 |
| manifest.state | 최대 50개 (format_version 2) |
| AI Action 지시문(instruction) | 2000자 이내 |
| AI·Webhook Action 필드(fields) | 1~12개 |
| 이미지 Action 공통 지시(instruction) | 400자 이내 |
| 검색 Action top_n | 1~10 |
| 내보내기 파일(.fcapp.json) 가져오기 | 2MB |

## LLM에 붙여 넣는 규격 전문

사용하는 LLM에 그대로 붙여 넣어 규격에 맞는 앱 번들을 만들게 하는 단일 문서.

아래 파일을 내려받거나 본문을 복사해 LLM의 시스템 지시문(또는 첫 메시지)에 붙여 넣으세요. 같은 내용이 이 문서의 모든 항목에 들어 있습니다.

### 사용 방법

- 규격 전문 뒤에 "다음 앱을 만들어 줘: …" 하고 원하는 앱을 설명합니다.
- 결과는 JSON 하나만 달라고 요청하면 그대로 올릴 수 있습니다. 설명 글이 섞이면 JSON 부분만 올립니다.
- 검증 오류는 그대로 붙여 "이 오류를 고쳐서 JSON 전체를 다시 줘" 하고 요청합니다.

## 번들 구조와 한도

번들 JSON의 최상위 구조, client 파일, settings, 크기 한도.

번들은 JSON 객체 하나이며 최상위 키는 정확히 네 개입니다. 다른 키가 있으면 거절됩니다.

| 키 | 타입 | 설명 |
| --- | --- | --- |
| format_version | 정수 1 또는 2 | 1 이 기본입니다. 2 는 manifest.state(메모리 상태)와 화면 config 를 쓸 때만 필요합니다. |
| manifest | 객체 | 앱 이름·권한·화면·이벤트·변수·Action 선언. 아래 「manifest」 문서 참고 |
| settings | 객체 | 변수의 현재 값. 모든 키는 manifest.variables 에 선언되어 있어야 합니다. 앱 안에서는 FC.settings 로 읽기 전용으로 접근합니다. |
| client | 객체 | { html, css, js } 세 문자열. 다른 키는 거절됩니다. |

### 한도

| 항목 | 한도 |
| --- | --- |
| 번들 전체 | 1MB |
| client.html · client.css · client.js 각각 | 256KB |
| manifest.events | 최대 30개 |
| manifest.actions | 1~5개 |
| manifest.state | 최대 50개 (format_version 2) |
| AI Action 지시문(instruction) | 2000자 이내 |
| AI·Webhook Action 필드(fields) | 1~12개 |
| 이미지 Action 공통 지시(instruction) | 400자 이내 |
| 검색 Action top_n | 1~10 |
| 내보내기 파일(.fcapp.json) 가져오기 | 2MB |

### client 파일

- html 은 완전한 문서가 아니라 조각(fragment)입니다. <html>·<head>·<body> 없이 <main> 같은 루트 요소 하나로 시작하세요.
- css 는 일반 CSS 입니다. @import·url(...)·image-set 은 쓸 수 없습니다.
- js 는 한 번 실행되는 스크립트입니다. 즉시 실행 함수 (() => { ... })(); 로 감싸는 것을 권장합니다. DOM 은 document.querySelector 로 직접 찾습니다.

#### 가장 작은 앱(이벤트 하나 보내기)

```json
{
  "format_version": 1,
  "manifest": {
    "name": "클릭 카운터",
    "sdk_version": "1",
    "permissions": [
      "events.emit"
    ],
    "screens": [
      {
        "key": "main",
        "label": "메인"
      }
    ],
    "events": [
      {
        "name": "example.clicked",
        "fields": {
          "count": {
            "type": "number",
            "required": true
          }
        }
      }
    ],
    "variables": [
      {
        "name": "title",
        "type": "text",
        "default": "클릭 카운터"
      }
    ]
  },
  "settings": {
    "title": "클릭 카운터"
  },
  "client": {
    "html": "<main class=\"app\">\n  <h2 data-title>클릭 카운터</h2>\n  <p data-count>0</p>\n  <button type=\"button\" data-action=\"add\">누르기</button>\n</main>\n",
    "css": ".app{font-family:Arial,sans-serif;max-width:480px;margin:auto;padding:24px}\nbutton{padding:10px 16px;border:0;border-radius:8px;background:#4A3094;color:#fff;cursor:pointer}\n",
    "js": "(() => {\n  let count = 0;\n  document.querySelector(\"[data-title]\").textContent = FC.settings.title;\n  document.querySelector(\"[data-action=add]\").addEventListener(\"click\", () => {\n    count += 1;\n    document.querySelector(\"[data-count]\").textContent = String(count);\n    FC.event.emit(\"example.clicked\", { count });\n  });\n})();\n"
  }
}
```

## manifest 작성법

권한·화면·이벤트·변수·상태·Action 선언의 모양과 규칙.

manifest 의 필수 키는 name·sdk_version·permissions·screens·events·variables 입니다. 선택 키는 actions·form_binding 이고, format_version 2 에서만 state 를 더 쓸 수 있습니다.

| 키 | 모양 | 규칙 |
| --- | --- | --- |
| name | 문자열 | 비어 있지 않아야 합니다. |
| sdk_version | "1" | 문자열 "1" 고정. |
| permissions | 문자열 배열 | 아래 권한 표의 값만. 중복 불가. JS 가 쓰는 FC 호출에 필요한 권한은 모두 선언해야 합니다. |
| screens | [{ key, label }] | 화면 이름 목록. HTML 에서 data-screen="key" 요소로 대응시키는 것이 관례입니다. |
| events | [{ name, fields }] | 최대 30개. name 은 소문자로 시작하는 소문자·숫자·점·밑줄(64자). contact.·lead.·order.·tag. 로 시작하는 이름은 예약. |
| variables | [{ name, type, default, ... }] | 사용자가 앱빌더에서 바꿀 수 있는 값. type 은 text·color·number·boolean·select. |
| actions | [{ key, kind, ... }] | 서버가 실행하는 기능(검색·AI·Webhook·이미지·PDF·업로드) 선언. 1~5개. |
| state | [{ name, type, default?, persist? }] | format_version 2 전용. 메모리 상태. persist 는 false 만 가능. |

### 권한(permissions)

| 권한 | 설명 | 비고 |
| --- | --- | --- |
| ai.generate | 방문자가 입력한 글을 FC AI가 처리한다(요약·분석·분류·추천·대화). 호출마다 크레딧이 든다. | 짝 Action: ai.generate |
| automation.trigger | 앱 이벤트로 자동화를 깨운다. events.emit 이 함께 필요하다(자동으로 포함된다). | 자동 포함: events.emit |
| contacts.tag | 연락처에 태그를 붙인다. 붙일 수 있는 태그는 앱 소유자가 설치 화면에서 미리 허용한 것뿐이다. | - |
| contacts.write | 연락처(이메일·이름·전화 등)를 만든다. | - |
| events.emit | 앱 이벤트를 기록한다(통계·자동화 연결용). | - |
| forms.read | 연결된 Form의 필드 구성을 읽는다. | - |
| forms.submit | 연결된 Form로 제출한다. | - |
| image.generate | 방문자 입력으로 AI 이미지를 만든다. 호출마다 이미지 크레딧이 든다. | 짝 Action: image.generate |
| page.redirect | 방문자를 다른 주소로 이동시킨다. | - |
| pdf.generate | 서버가 PDF 문서를 만들어 방문자가 저장하게 한다. | 짝 Action: pdf.generate |
| search.web | 방문자의 검색어로 웹·뉴스를 검색한다. | 짝 Action: search.web |
| upload.file | 방문자의 이미지·PDF 파일을 받는다(서류 제출·AI 입력 자료). | 짝 Action: upload.file |
| webhook.send | 앱 소유자가 연결해 둔 Webhook 주소로 선언한 필드만 보낸다. | 짝 Action: webhook.post |

> 권한과 Action 은 짝입니다. search.web·ai.generate·webhook.send·image.generate·pdf.generate·upload.file 권한을 선언하면 같은 종류의 Action 이 있어야 하고, 반대로 Action 이 있으면 그 권한이 있어야 합니다. 권한을 필요 이상으로 늘리지 마세요 — 앱 소유자가 승인 화면에서 모두 보게 됩니다.

### events(이벤트) 선언

이벤트는 앱이 보내는 결과 신호(예 quiz.completed)입니다. 선언한 이름·필드와 정확히 맞는 값만 FC.event.emit 으로 보낼 수 있습니다.

```json
{ "name": "roi.completed",
  "fields": { "roi_percent": { "type": "number", "required": true },
              "revenue":     { "type": "number", "required": true, "minimum": 1 } } }
```

필드 type 은 string·number·boolean 이고 required·enum·minimum·maximum 을 쓸 수 있습니다.

### variables(변수)와 settings

변수는 사용자가 앱빌더에서 문구·색상 등을 바꿀 수 있게 열어 두는 값입니다. settings 에 현재 값을 넣고, 앱 JS 는 FC.settings.이름 으로 읽습니다.

| type | 쓸 수 있는 키 |
| --- | --- |
| text | name, default, min_length, max_length |
| number | name, default, minimum, maximum |
| boolean | name, default |
| color | name, default (#RRGGBB) |
| select | name, default, options |

### Action(actions)

검색·AI·Webhook·이미지·PDF·업로드처럼 서버가 대신 실행하는 기능은 번들에 "선언"만 담습니다. API 키·모델·주소는 서버가 정하며 번들에 쓰지 않습니다.

#### search.web Action

방문자의 검색어로 웹을 검색한다. 앱 JS 는 FC.search.web 로 호출하고, 권한 search.web 이 필요합니다.

필수 키: key, kind, connection / 선택 키: source, top_n, locale

```json
{
  "key": "web_search",
  "kind": "search.web",
  "connection": {
    "mode": "managed"
  },
  "source": "web",
  "top_n": 5,
  "locale": "ko"
}
```

- connection 은 { "mode": "managed" } 만 쓴다(검색 API 키·주소를 코드에 쓰지 않는다).
- source 는 web 또는 news, locale 은 ko 또는 en, top_n 은 1~10이다.

#### ai.generate Action

방문자 입력을 FC AI가 처리한다. 모델·키는 서버가 정한다. 앱 JS 는 FC.ai.generate / FC.ai.chat 로 호출하고, 권한 ai.generate 이 필요합니다.

필수 키: key, kind, connection, instruction / 선택 키: output, fields, mode, search, model

```json
{
  "key": "summarize",
  "kind": "ai.generate",
  "connection": {
    "mode": "managed"
  },
  "instruction": "입력된 글을 한국어로 세 문장 이내로 요약한다.",
  "output": "text"
}
```

- instruction 은 2000자 이내, 방문자에게 보이지 않는 지시문이다. 역할·출력 형식·길이를 구체적으로 적는다.
- output 이 "json" 이면 fields(1~12개, { key, type, description? })를 선언하고 결과는 { data } 로 온다. type 은 string·number·boolean·string_list.
- 대화형은 "mode": "chat" 이고 구조화 출력("json")과 함께 쓸 수 없다.
- "search": { "action": "검색_액션_키" } 로 같은 번들의 search.web Action 을 먼저 실행해 그 결과를 AI에 넘기는 체인을 만들 수 있다.

#### webhook.post Action

선언한 필드만 앱 소유자가 연결한 Webhook 주소로 보낸다. 앱 JS 는 FC.webhook.send 로 호출하고, 권한 webhook.send 이 필요합니다.

필수 키: key, kind, connection, fields

```json
{
  "key": "send_result",
  "kind": "webhook.post",
  "connection": {
    "mode": "connection",
    "ref": "mac-xxxxxxxx"
  },
  "fields": [
    {
      "key": "email",
      "type": "string"
    },
    {
      "key": "score",
      "type": "number"
    }
  ]
}
```

- connection.ref 는 앱 소유자가 앱빌더 연결 화면에서 Webhook 을 만들 때 생기는 값이다. 외부에서 만든 번들은 그 값을 모르므로 올린 뒤 연결 화면에서 고르게 안내한다. 주소(URL)·토큰을 코드에 쓰지 않는다.
- fields 는 1~12개이며 선언하지 않은 키는 전달되지 않는다.

#### image.generate Action

방문자 입력으로 AI 이미지를 만든다. 호출마다 크레딧이 든다. 앱 JS 는 FC.image.generate 로 호출하고, 권한 image.generate 이 필요합니다.

필수 키: key, kind, connection / 선택 키: style, instruction, reference

```json
{
  "key": "draw",
  "kind": "image.generate",
  "connection": {
    "mode": "managed"
  },
  "style": "illustration"
}
```

- style 은 realistic·illustration·minimal·flat·3d·watercolor·abstract 중 하나(선택), instruction 은 400자 이하(선택)다.
- 업로드한 사진을 참고하려면 "reference": true 를 선언한다.

#### pdf.generate Action

견적서·결과 리포트 같은 문서를 서버가 PDF 로 만든다. 앱 JS 는 FC.pdf.generate / FC.pdf.save 로 호출하고, 권한 pdf.generate 이 필요합니다.

필수 키: key, kind, connection / 선택 키: accent, filename

```json
{
  "key": "make_pdf",
  "kind": "pdf.generate",
  "connection": {
    "mode": "managed"
  },
  "accent": "#4A3094",
  "filename": "견적서"
}
```

- 문서는 최대 약 60KB·30쪽이다. 이모지는 지워진다.

#### upload.file Action

방문자의 이미지·PDF 파일을 받는다. 앱 JS 는 FC.upload.send 로 호출하고, 권한 upload.file 이 필요합니다.

필수 키: key, kind, connection, purpose / 선택 키: accept, max_files

```json
{
  "key": "docs",
  "kind": "upload.file",
  "connection": {
    "mode": "managed"
  },
  "purpose": "submission",
  "accept": [
    "image",
    "pdf"
  ],
  "max_files": 3
}
```

- purpose 는 submission(서류 제출, 소유자가 받아 봄·90일 보관) 또는 ai_input(AI 입력 자료, 이미지만·24시간 보관)이다.
- max_files 는 1~10, accept 는 image·pdf 이며 ai_input 은 image 만 가능하다.

## HTML·CSS·JS 작성 규칙

보안 검증을 통과하기 위해 지켜야 하는 금지·허용 규칙과 권장 패턴.

번들은 저장·게시 전에 정적 검증을 받습니다. 아래를 어기면 거절됩니다.

### HTML

- script·form·iframe·object·embed·link·base·meta 요소를 쓰지 않습니다. 입력은 <input>·<button type="button"> 으로 구성하고 제출은 JS 에서 처리합니다.
- onclick 같은 on* 속성과 srcdoc·ping 속성을 쓰지 않습니다. 이벤트는 JS 의 addEventListener 로 연결합니다.
- src·href·action 등 URL 속성에는 같은 페이지 앵커(#…)만 씁니다. 외부 주소·data URL 은 거절됩니다. 이미지는 JS 로 <img> 를 만들어 src 에 넣습니다(생성 결과 dataUrl 등).
- 화면 전환은 data-screen 요소의 hidden 속성을 JS 로 토글하는 방식을 권장합니다.

### CSS

- @import·url(...)·image-set·@namespace url 을 쓰지 않습니다. 글꼴·배경 이미지를 외부에서 불러올 수 없으므로 시스템 글꼴과 색·그라디언트로 꾸밉니다.
- 앱 영역 밖(페이지 전체)에는 영향을 주지 않는 격리된 문서에서 실행됩니다.

### JavaScript

- eval·new Function·외부 URL 호출(fetch·XMLHttpRequest·WebSocket 등 네트워크 접근)을 쓰지 않습니다. 서버와의 통신은 SDK(FC)만 사용합니다.
- window·parent·top·self·globalThis·frames·opener 와 contentWindow·ownerDocument 같은 프레임 탈출 접근을 쓰지 않습니다.
- FC 를 다른 변수에 담거나 FC[이름] 처럼 동적으로 접근하지 않습니다. FC.contact.create(...) 처럼 직접 호출합니다. 허용된 호출 목록은 SDK 표에 있습니다.
- document 는 document.querySelector(...) 처럼 바로 호출합니다. const d = document 같은 별칭은 거절됩니다.
- FC.settings 는 읽기 전용입니다.

### 권장 패턴

- 결과·오류 문구는 textContent 로 넣습니다(innerHTML 로 방문자 입력이나 서버 응답을 넣지 않습니다).
- 서버에 요청을 보내는 버튼은 요청 중 disabled 로 잠가 중복 호출을 막고, 진행 중·성공·실패 상태를 화면에 보입니다.
- 개인정보는 방문자가 입력한 범위에서만 다루고, 입력 안내 문구에 수집 목적을 적습니다.
- 호출마다 크레딧이 드는 기능(AI·이미지)은 자동 반복 호출하지 않고 버튼 클릭에 한 번만 보냅니다.

## SDK 호출 목록

앱 JS 에서 쓸 수 있는 FC.* 호출의 시그니처·반환값·필요 권한.

앱 안에서는 전역 FC 객체만 서버와 통신하는 통로입니다. 아래 목록에 없는 호출은 검증에서 거절되며, 있어도 필요한 권한을 manifest.permissions 에 선언해야 합니다. 서버는 호출마다 권한·한도·크레딧을 다시 검사합니다.

| 호출 | 필요 권한 | 반환 | 설명 |
| --- | --- | --- | --- |
| FC.ready(callback?) | 필요 없음 | Promise<void> | 앱 초기화 시점. 콜백을 주면 즉시 실행한다. 콜백 없이 await FC.ready() 로 써도 된다. 권한이 필요 없다. |
| FC.contact.create(data, options?) | contacts.write | Promise<object> | 연락처를 만든다. data 는 { email, first_name, last_name, phone, ... } 처럼 단순 값 객체다. 같은 요청을 재시도할 때 중복 생성되지 않게 options 로 { operationId: "UUID" } 를 줄 수 있다(같은 operationId 는 한 번만 처리된다). operationId 는 UUID(소문자 36자) 형식이어야 한다. |
| FC.contact.tag(tagKey, options?) | contacts.tag | Promise<object> | 방금 만든 연락처에 태그를 붙인다. tagKey 는 앱 소유자가 설치 때 허용한 태그 키여야 한다. 허용되지 않은 키는 거절된다. |
| FC.event.emit(name, payload?, options?) | events.emit | Promise<object> | manifest.events 에 선언한 이름·필드와 정확히 맞는 이벤트를 기록한다. 선언하지 않은 필드나 타입이 다르면 거절된다. FC.event.track 은 같은 함수의 별칭이다. |
| FC.event.track(name, payload?, options?) | events.emit | Promise<object> | FC.event.emit 의 별칭. |
| FC.automation.trigger(name, payload?, options?) | automation.trigger | Promise<object> | 자동화를 깨우는 이벤트를 보낸다. manifest.events 에 같은 이름을 선언하고 automation.trigger 권한을 선언해야 한다(events.emit 은 자동으로 함께 승인된다). |
| FC.page.redirect(url) | page.redirect | Promise<object> | 방문자를 이동시킨다. 이동 가능한 주소는 서버가 검사한다. |
| FC.form.get() | forms.read | Promise<object> | 앱에 연결된 Form 의 필드 구성을 읽는다. 앱 소유자가 허용한 Form 에 한한다. |
| FC.form.submit(data, options?) | forms.submit | Promise<object> | 연결된 Form 로 제출한다. manifest.form_binding.field_map 으로 앱 필드를 Form 필드·연락처 항목에 연결한다. |
| FC.search.web(query, { action, request_key? }) | search.web | Promise<{ query, results: [{ title, url, domain, snippet, published_at, image, source_type }], mocked }> | manifest.actions 의 search.web Action 으로 웹을 검색한다. 결과의 url 은 링크(<a href>)로만, 제목·요약은 textContent 로 넣는다(innerHTML 금지). |
| FC.ai.generate(input, { action, request_key? }) | ai.generate | Promise<{ text, mocked }> (output "json" 이면 { data, mocked }) | 입력글(4000자 이내)을 ai.generate Action 의 지시문대로 처리한다. 모델·키·비용은 서버가 정한다. 실패하면 거절되며 오류의 reason·message 를 쓸 수 있다. |
| FC.ai.chat(messages, { action, request_key? }) | ai.generate | Promise<{ text, mocked }> | 대화형 Action("mode":"chat") 전용. messages 는 [{ role: "user"\|"assistant", content }] 이고 최대 20개, 마지막은 user, 합쳐 4000자 이내다. 대화 기록은 앱이 들고 있다가 매번 전부 보낸다. |
| FC.image.generate(prompt, { action, request_key?, reference? }) | image.generate | Promise<{ dataUrl, mime, mocked }> | AI 이미지를 만든다. dataUrl 은 data:image/…;base64,… 문자열이며 JS 로 만든 <img> 의 src 에만 넣는다. HTML 정적 마크업의 src·href 에는 쓸 수 없다. reference 는 upload.file(purpose ai_input) 로 올린 파일 id 다. |
| FC.pdf.generate(document, { action, request_key? }) | pdf.generate | Promise<{ handle, filename, size }> | 문서 객체({ title, subtitle, meta, sections, footer, filename })를 서버가 PDF 로 그린다. 글자·표·목록만 가능하고 HTML·이미지·링크는 안 된다. 파일은 바깥 화면이 보관하고 앱에는 handle 만 준다. |
| FC.pdf.save(handle) | pdf.generate | Promise<{ ok: true }> | FC.pdf.generate 가 준 handle 의 파일을 방문자가 저장하게 한다. 버튼 클릭 같은 사용자 동작 안에서 부른다. |
| FC.upload.send(file, { action, request_key?, note? }) | upload.file | Promise<{ id, filename, size, kind, mime }> | 방문자가 고른 File 을 올린다. 올리기 전에 바깥 화면이 보관 안내와 동의를 받는다. 이미지(8MB)·PDF(10MB)만 받으며 서버가 파일 내용으로 확인한다. |
| FC.webhook.send(data, { action, request_key? }) | webhook.send | Promise<{ ok: true, mocked }> | 선언한 fields 만 앱 소유자의 Webhook 주소로 POST 한다. 주소·헤더·재시도는 서버가 정하고 응답 본문은 돌려주지 않는다. |
| FC.state.get(name) | 필요 없음 | string \| number \| boolean \| null | format_version 2 전용. manifest.state 에 선언한 값을 읽는다. 서버로 나가지 않고 새로고침하면 초기화된다. |
| FC.state.set(name, value) | 필요 없음 | void | format_version 2 전용. 선언한 이름·타입과 맞아야 한다(맞지 않으면 STATE_TYPE_MISMATCH). |
| FC.state.reset(name?) | 필요 없음 | void | format_version 2 전용. 이름을 주면 그 값만, 안 주면 전부 선언한 기본값으로 되돌린다. |

### 실패 처리

서버 호출은 모두 Promise 이며 실패하면 거절됩니다. 오류 객체의 code·reason·message 를 읽을 수 있고, message 는 방문자에게 그대로 보여 줘도 되는 문구입니다. HTTP 상태가 있는 오류는 status 가 4xx(408·429 제외)이면 입력을 고쳐야 하는 영구 실패, 그 외(네트워크·5xx·408·429)는 같은 요청을 다시 시도해도 되는 일시 실패로 다루면 됩니다.

### 지원하지 않는 호출

FC.contact.update, FC.storage.*, FC.http.request, FC.payment.checkout 은 현재 지원하지 않으며 검증에서 거절됩니다(unsupported_sdk_api).

## 예제 번들

검증을 통과하는 동작 예제 3개 — 기본, 연락처 저장, AI 요약.

아래 예제는 모두 실제 검증기를 통과하는 번들입니다. LLM에 "이 형식을 따라 줘" 하고 함께 주면 결과가 안정적입니다.

#### 가장 작은 앱(이벤트 하나 보내기)

```json
{
  "format_version": 1,
  "manifest": {
    "name": "클릭 카운터",
    "sdk_version": "1",
    "permissions": [
      "events.emit"
    ],
    "screens": [
      {
        "key": "main",
        "label": "메인"
      }
    ],
    "events": [
      {
        "name": "example.clicked",
        "fields": {
          "count": {
            "type": "number",
            "required": true
          }
        }
      }
    ],
    "variables": [
      {
        "name": "title",
        "type": "text",
        "default": "클릭 카운터"
      }
    ]
  },
  "settings": {
    "title": "클릭 카운터"
  },
  "client": {
    "html": "<main class=\"app\">\n  <h2 data-title>클릭 카운터</h2>\n  <p data-count>0</p>\n  <button type=\"button\" data-action=\"add\">누르기</button>\n</main>\n",
    "css": ".app{font-family:Arial,sans-serif;max-width:480px;margin:auto;padding:24px}\nbutton{padding:10px 16px;border:0;border-radius:8px;background:#4A3094;color:#fff;cursor:pointer}\n",
    "js": "(() => {\n  let count = 0;\n  document.querySelector(\"[data-title]\").textContent = FC.settings.title;\n  document.querySelector(\"[data-action=add]\").addEventListener(\"click\", () => {\n    count += 1;\n    document.querySelector(\"[data-count]\").textContent = String(count);\n    FC.event.emit(\"example.clicked\", { count });\n  });\n})();\n"
  }
}
```

#### 이메일을 받아 연락처로 저장하기

```json
{
  "format_version": 1,
  "manifest": {
    "name": "소식 받기",
    "sdk_version": "1",
    "permissions": [
      "contacts.write"
    ],
    "screens": [
      {
        "key": "form",
        "label": "입력"
      },
      {
        "key": "done",
        "label": "완료"
      }
    ],
    "events": [],
    "variables": [
      {
        "name": "title",
        "type": "text",
        "default": "소식 받기"
      }
    ]
  },
  "settings": {
    "title": "소식 받기"
  },
  "client": {
    "html": "<main class=\"app\">\n  <section data-screen=\"form\">\n    <h2 data-title>소식 받기</h2>\n    <label>이메일 <input name=\"email\" type=\"email\" required maxlength=\"320\"></label>\n    <p data-error role=\"alert\"></p>\n    <button type=\"button\" data-action=\"submit\">신청하기</button>\n  </section>\n  <section data-screen=\"done\" hidden><p>신청해 주셔서 감사합니다.</p></section>\n</main>\n",
    "css": ".app{font-family:Arial,sans-serif;max-width:480px;margin:auto;padding:24px}\ninput{display:block;width:100%;padding:10px;margin:8px 0}\n",
    "js": "(() => {\n  const screen = (key) => document.querySelector(\"[data-screen=\" + key + \"]\");\n  document.querySelector(\"[data-action=submit]\").addEventListener(\"click\", async () => {\n    const email = document.querySelector(\"input[name=email]\").value.trim();\n    const error = document.querySelector(\"[data-error]\");\n    if (!email) { error.textContent = \"이메일을 입력해 주세요.\"; return; }\n    try {\n      await FC.contact.create({ email });\n      screen(\"form\").hidden = true;\n      screen(\"done\").hidden = false;\n    } catch (e) {\n      error.textContent = \"잠시 후 다시 시도해 주세요.\";\n    }\n  });\n})();\n"
  }
}
```

#### 방문자 글을 AI가 요약하기(AI Action)

```json
{
  "format_version": 1,
  "manifest": {
    "name": "글 요약기",
    "sdk_version": "1",
    "permissions": [
      "ai.generate"
    ],
    "screens": [
      {
        "key": "main",
        "label": "메인"
      }
    ],
    "events": [],
    "variables": [
      {
        "name": "title",
        "type": "text",
        "default": "글 요약기"
      }
    ],
    "actions": [
      {
        "key": "summarize",
        "kind": "ai.generate",
        "connection": {
          "mode": "managed"
        },
        "instruction": "입력된 글을 한국어로 세 문장 이내로 요약한다. 없는 내용은 만들지 않는다.",
        "output": "text"
      }
    ]
  },
  "settings": {
    "title": "글 요약기"
  },
  "client": {
    "html": "<main class=\"app\">\n  <h2 data-title>글 요약기</h2>\n  <textarea name=\"body\" rows=\"6\" maxlength=\"4000\"></textarea>\n  <button type=\"button\" data-action=\"run\">요약하기</button>\n  <p data-status role=\"status\"></p>\n  <div data-result></div>\n</main>\n",
    "css": ".app{font-family:Arial,sans-serif;max-width:560px;margin:auto;padding:24px}\ntextarea{width:100%;padding:10px;box-sizing:border-box}\n",
    "js": "(() => {\n  const button = document.querySelector(\"[data-action=run]\");\n  const status = document.querySelector(\"[data-status]\");\n  const result = document.querySelector(\"[data-result]\");\n  button.addEventListener(\"click\", async () => {\n    const input = document.querySelector(\"textarea[name=body]\").value.trim().slice(0, 4000);\n    if (!input) { status.textContent = \"글을 입력해 주세요.\"; return; }\n    button.disabled = true;\n    status.textContent = \"요약 중입니다…\";\n    result.textContent = \"\";\n    try {\n      const answer = await FC.ai.generate(input, { action: \"summarize\" });\n      result.textContent = answer.text;\n      status.textContent = \"\";\n    } catch (e) {\n      status.textContent = (e && e.message) || \"요약하지 못했습니다.\";\n    } finally {\n      button.disabled = false;\n    }\n  });\n})();\n"
  }
}
```

## 검증 오류 코드와 고치는 법

번들 검증에서 나오는 오류 코드별 의미와 해결 방법.

검증은 번들 구조(manifest·권한·Action)와 client 코드(HTML·CSS·JS)를 모두 확인합니다. 올릴 때 나오는 오류의 code 를 아래 표에서 찾아 고치세요. 코드 오류는 줄·열 위치가 함께 표시됩니다.

| 오류 코드 | 의미와 고치는 법 |
| --- | --- |
| action_without_permission | Action 이 있는데 그 종류에 필요한 권한이 permissions 에 없다. |
| bundle_too_large | 번들 전체가 1MB를 넘었다. 이미지·큰 데이터를 빼고 코드를 줄인다. |
| client_validator_failed | 검증기 실행에 실패했다. 잠시 후 다시 시도한다(코드 문제가 아닐 수 있다). |
| computed_destructure_not_allowed | 계산된 키로 객체를 분해해 전역·SDK 에 접근할 수 없다. |
| constructor_access_not_allowed | .constructor 로 생성자에 접근할 수 없다. |
| css_escape_not_allowed | CSS 에 우회용 이스케이프 표기를 쓸 수 없다. |
| css_image_set_not_allowed | CSS image-set 을 쓸 수 없다. |
| css_import_not_allowed | CSS 에 @import 를 쓸 수 없다. |
| css_namespace_url_not_allowed | CSS @namespace url 을 쓸 수 없다. |
| css_syntax_error | CSS 구문 오류다. 줄·열 위치를 확인한다. |
| css_too_large | client.css 가 256KB 를 넘었다. |
| css_url_not_allowed | CSS 에 url(...) 을 쓸 수 없다. 이미지는 CSS 배경으로도 불러올 수 없다. |
| document_access_not_allowed | document 를 변수에 담아 쓸 수 없다. document.querySelector(...) 처럼 바로 호출한다. |
| duplicate_action_key | Action key 가 중복됐다. |
| duplicate_event_name | 이벤트 이름이 중복됐다. |
| duplicate_permission | 같은 권한을 두 번 선언했다. |
| duplicate_variable_name | 변수 이름이 중복됐다. |
| dynamic_global_access_not_allowed | 이름을 문자열로 조립해 전역에 접근할 수 없다. |
| dynamic_sdk_api | FC[이름] 같은 동적 접근은 쓸 수 없다. 호출을 직접 쓴다. |
| eval_not_allowed | eval 을 쓸 수 없다. |
| form_binding_without_permission | form_binding 에는 forms.read 또는 forms.submit 권한이 필요하다. |
| function_constructor_not_allowed | new Function() 을 쓸 수 없다. |
| global_object_access_not_allowed | window·parent·top·globalThis·self 같은 전역 객체나 contentWindow 같은 프레임 접근을 쓸 수 없다. DOM 은 document.querySelector 로 직접 접근한다. |
| global_this_access_not_allowed | 최상위에서 this 로 전역 객체에 접근할 수 없다. 화살표 함수나 지역 변수를 쓴다. |
| html_external_url | 외부 주소(http:·https:·// 등)는 쓸 수 없다. 이미지·글꼴·스크립트를 외부에서 불러올 수 없다. |
| html_forbidden_attribute | srcdoc·ping 같은 속성은 쓸 수 없다. |
| html_forbidden_element | script·form·iframe·object·embed·link·base·meta 요소는 쓸 수 없다. 이벤트는 JS 에서 addEventListener 로 연결하고 입력은 form 없이 button type="button" 으로 처리한다. |
| html_inline_handler | onclick 같은 on* 속성은 쓸 수 없다. JS 에서 addEventListener 로 연결한다. |
| html_parse_error | HTML 구문 오류가 있다. |
| html_too_large | client.html 이 256KB 를 넘었다. |
| html_url_not_allowed | src·href 등 URL 속성에는 같은 페이지 앵커(#…)만 쓸 수 있다. 이미지는 JS 로 만든 <img> 의 src 로 넣는다. |
| invalid_actions | manifest.actions 모양이 틀렸다(1~5개, 종류별 필수 키). Action 표를 확인한다. |
| invalid_bundle | 번들이 JSON 객체가 아니거나 최상위 구조가 틀렸다. format_version·manifest·settings·client 네 키만 둔다. |
| invalid_client_files | client 는 html·css·js 세 문자열 키만 가진 객체여야 한다. |
| invalid_event | 이벤트에 name·fields 외의 키가 있거나 모양이 틀렸다. |
| invalid_event_field | 이벤트 필드의 type 은 string·number·boolean 이고 허용된 키(type·required·enum·minimum·maximum)만 쓴다. |
| invalid_event_fields | 이벤트 fields 모양이 틀렸다. { 필드: { type, required } } 로 쓴다. |
| invalid_event_name | 이벤트 이름은 소문자로 시작하고 소문자·숫자·점·밑줄만(최대 64자) 쓴다. |
| invalid_events | manifest.events 는 { name, fields } 배열이어야 한다. |
| invalid_form_binding | manifest.form_binding 모양이 틀렸다. |
| invalid_format_version | format_version 은 정수 1 또는 2 여야 한다. |
| invalid_html | HTML 을 해석하지 못했다. |
| invalid_html_style | HTML 의 style 속성이나 <style> 안에 허용되지 않는 CSS 가 있다. |
| invalid_manifest | manifest 에 필수 키(name·sdk_version·permissions·screens·events·variables)가 빠졌거나 허용되지 않는 키가 있다. |
| invalid_manifest_name | manifest.name 은 비어 있지 않은 문자열이어야 한다. |
| invalid_permissions | manifest.permissions 는 문자열 배열이어야 한다. |
| invalid_screens | manifest.screens 는 { key, label } 배열이어야 한다. |
| invalid_sdk_version | manifest.sdk_version 은 문자열 "1" 이어야 한다. |
| invalid_setting_value | settings 값이 변수의 타입·범위와 맞지 않는다. |
| invalid_settings | settings 는 JSON 객체여야 한다. |
| invalid_state | manifest.state(format_version 2)는 { name, type, default?, persist? } 배열이며 persist 는 false 만 가능하다. |
| invalid_variable | 변수 선언 모양이 틀렸다. { name, type, default } 이고 type 은 text·color·number·boolean·select 다. |
| invalid_variables | manifest.variables 는 배열이어야 한다. |
| js_syntax_error | JavaScript 구문 오류다. 줄·열 위치를 확인한다. |
| js_too_large | client.js 가 256KB 를 넘었다. |
| missing_form_policy | 앱 소유자가 설치 화면에서 허용 Form 을 정해야 한다(번들 문제가 아님). |
| missing_sdk_permission | JS 가 쓰는 FC 호출에 필요한 권한이 manifest.permissions 에 없다. SDK 표의 권한 열을 확인한다. |
| missing_tag_policy | 앱 소유자가 설치 화면에서 허용 태그를 정해야 한다(번들 문제가 아님). |
| optional_sdk_api_not_allowed | FC?.contact?.create 같은 옵셔널 체이닝으로 SDK 를 부를 수 없다. FC.contact.create(...) 로 직접 호출한다. |
| permission_without_action | Action 권한(search.web·ai.generate·webhook.send·image.generate·pdf.generate·upload.file)은 같은 종류의 Action 과 항상 짝이다. |
| readonly_settings | FC.settings 는 읽기 전용이다. 값을 바꾸지 않는다. |
| reflect_access_not_allowed | Reflect.* 로 객체를 조작해 전역·SDK 에 접근할 수 없다. |
| reflection_access_not_allowed | Object.getOwnPropertyDescriptor 같은 리플렉션을 쓸 수 없다. |
| reserved_event_name | contact. · lead. · order. · tag. 로 시작하는 이름은 예약돼 있다. 다른 이름을 쓴다. |
| sdk_alias_not_allowed | FC 를 다른 변수에 담거나 동적으로 접근할 수 없다. FC.contact.create(...) 처럼 직접 호출한다. |
| too_many_event_fields | 이벤트 하나의 필드가 너무 많다. |
| too_many_events | 이벤트는 최대 30개다. |
| unknown_bundle_keys | 최상위에 허용되지 않는 키가 있다. format_version·manifest·settings·client 만 쓴다. |
| unknown_permission | 사용 가능한 권한 어휘에 없는 권한이다. 권한 표를 확인한다. |
| unknown_setting | settings 에 manifest.variables 로 선언하지 않은 키가 있다. 모든 settings 키를 variables 에 선언한다. |
| unsupported_sdk_api | 허용된 SDK 호출이 아니다. SDK 표에 있는 호출만 쓴다. |

## 올리는 방법과 이용 한도

외부 코드 올리기 화면 사용법, 유료 플랜 조건, 보안과 한도.

### 올리기

1. 앱빌더 목록에서 "외부 코드 올리기"를 엽니다.
2. 번들 JSON 을 붙여 넣거나 .json 파일을 올립니다. 내보내기 파일(.fcapp.json)도 그대로 올릴 수 있습니다.
3. "검증만 하기"로 저장 없이 오류를 확인하고, 통과하면 "새 앱으로 저장"을 누릅니다.
4. 저장된 초안을 미리 보고, 요청하는 권한 목록을 확인해 승인한 뒤 게시합니다.

### 이용 조건

- 유료 플랜 전용입니다. 무료 플랜에서는 이 화면이 열리지 않습니다.
- 올린 번들은 항상 새 초안입니다. 기존 앱을 덮어쓰거나 자동 게시되지 않습니다.
- 폼·자동화 연결, Webhook 연결, 허용 태그 같은 환경 설정은 번들에 담기지 않으며 올린 뒤 앱 소유자가 직접 연결합니다.

### 보안 모델

- 앱은 게시 페이지와 분리된 별도 도메인의 샌드박스에서 실행되며 부모 페이지·쿠키·저장소에 접근할 수 없습니다.
- 외부 네트워크 접속·외부 리소스 로딩이 막혀 있고, 서버 통신은 SDK 호출만 가능합니다.
- 서버는 모든 SDK 호출에서 권한·계정 한도·일일 한도·크레딧을 다시 검사합니다. 번들이 조작돼도 우회할 수 없습니다.

### 비용

- 번들을 만드는 데 쓰는 LLM 비용은 퍼널체인저스 크레딧과 무관합니다.
- 앱이 방문자에게 AI·이미지·검색을 제공할 때(ai.generate·image.generate·search.web)는 실행 시점에 소유자의 크레딧이 사용됩니다. 소유자가 앱을 게시하기 전에 한도를 확인하도록 안내하세요.

## MCP 연결로 AI가 직접 만들고 저장하기

Claude·ChatGPT 같은 MCP 지원 AI에 퍼널체인저스를 연결해 규격 읽기부터 검증·저장까지 대화로 진행하는 방법.

사용하는 AI가 MCP(Model Context Protocol)를 지원하면 번들을 복사해 붙여 넣지 않아도 됩니다. AI가 규격을 직접 읽고, 번들을 만들고, 검증 오류를 스스로 고쳐 새 앱 초안으로 저장합니다. 번들을 만드는 데 드는 AI 비용은 사용하는 AI 쪽에서 부담합니다.

### 준비

- 설정의 "MCP 연동"에서 AI를 연결하고, 쓰기 권한(mcp:write)을 허용합니다. 읽기 전용으로 연결하면 규격 읽기만 되고 저장은 되지 않습니다.
- 저장(create_app_from_bundle)은 외부 코드 올리기와 같은 조건입니다. 유료 플랜에서만 열립니다.

### 도구

| 도구 | 하는 일 |
| --- | --- |
| get_app_spec | 번들 작성 규격 전문(markdown) 또는 JSON Schema를 돌려줍니다. 앱을 만들기 전에 한 번 읽습니다. |
| create_app_from_bundle | 번들을 검증하고, 통과하면 새 앱 초안으로 저장합니다. dry_run=true 면 저장 없이 검증만 합니다. |
| validate_app | 이미 만든 앱의 초안이나 수정 후보 번들을 검증합니다. |
| update_app | 앱 초안을 부분 수정합니다. 저장 전에 전체를 다시 검증합니다. |
| publish_app | 초안을 게시합니다. 늘어나는 권한은 사용자가 확인해야 합니다. |

### 작업 흐름

1. AI가 get_app_spec 으로 규격을 읽습니다.
2. 사용자가 원하는 앱을 설명하면 AI가 번들을 만들어 create_app_from_bundle 을 dry_run=true 로 호출합니다.
3. 오류가 돌아오면 errors(오류 코드와 고치는 법)와 diagnostics(파일·줄·열)를 보고 AI가 번들을 고쳐 다시 호출합니다.
4. 통과하면 dry_run 없이 새 idempotency_key 와 함께 호출해 저장합니다. 미리 보기와 게시는 앱빌더 화면이나 publish_app 으로 합니다.

### 안전장치

- 저장은 항상 새 초안입니다. 기존 앱을 덮어쓰지 않고 자동 게시하지 않습니다.
- 같은 idempotency_key 로 재시도하면 앱이 하나만 만들어집니다. 번들을 고쳐 다시 저장할 때는 새 키를 씁니다.
- 서버 기록에는 업로더, 크기, 해시만 남기고 번들 본문은 남기지 않습니다.
- 검증기는 외부 올리기 화면과 같습니다. 화면에서 통과하는 번들은 MCP 에서도 통과합니다.

> MCP를 지원하지 않는 LLM이나 외주 개발사가 만든 결과는 "외부 코드 올리기" 화면에 붙여 넣거나 파일로 올립니다.

이 페이지가 도움이 되었나요?