자유응답이나 댓글처럼 정성적인 텍스트 묶음을 보내 주시면, 의미가 가까운 응답끼리 묶은 클러스터와 그 묶음을 설명하는 라벨·메타 라벨, 감정 점수, 그리고 시각화에 바로 사용할 수 있는 2D 좌표가 담긴 JSON을 돌려드립니다.
↓ 샘플 코드 보기
🤖 LLM·에이전트 연동 시: 이 문서 전체(레퍼런스 + 샘플)를 한 파일로 담은
/llms.txt 를 그대로 읽히세요.
설문 주관식 응답, SNS 댓글, 상품 리뷰처럼 수백에서 수천 개에 이르는 짧은 글은 한눈에 파악하기가 어렵습니다. 이 API는 텍스트 배열을 받아 의미가 비슷한 응답끼리 자동으로 묶고, 각 묶음에 이해하기 쉬운 이름과 감정 점수를 붙여 다음과 같은 구조화된 결과로 정리해 드립니다.
돌려드린 결과는 버블 차트 라이브러리에 그대로 넣어 시각화하실 수 있습니다. 연동 방법은 샘플 코드 섹션을 참고해 주세요.
https://api.affinitybubble.com
모든 호출에는 서비스 키가 필요합니다. 키는 운영자에게 요청해 주시면 발급해 드립니다.
Authorization: Bearer ab_live_...
발급받으신 키가 정상인지 다음과 같이 확인하실 수 있습니다.
curl https://api.affinitybubble.com/v1/whoami \
-H "Authorization: Bearer ab_live_..."
# { "keyId": "svckey_...", "name": "Acme Inc", "prefix": "ab_live_a1b2" }
서비스 상태를 확인합니다. 인증이 필요하지 않습니다.
텍스트를 즉시 분석해 결과를 그대로 돌려드립니다. 보통 5~15초 정도 소요되며(1,000개 기준), 입력이 많지 않을 때 적합합니다.
분석이 진행되는 동안 렌더 가능한 중간 스냅샷을 SSE(server-sent
events)로 흘려보내, 버블이 단계적으로 조립되는 모습을 그대로 보여줄 수
있습니다. 요청 형식은 /v1/bubbles/sync와 동일합니다.
자세한 내용은 아래 스트리밍 섹션을 참고해 주세요.
분석을 비동기로 시작합니다. 요청을 받으면 즉시 작업 번호(instanceId)를
돌려드리고, 분석은 백그라운드에서 진행됩니다. 입력이 많을 때(수백 개
이상) 권장하며, 진행 상황은 아래 폴링 엔드포인트로 확인하실 수
있습니다.
비동기 작업의 진행 상황을 조회합니다. 현재 상태(status:
queued · running · complete · errored)와 진행률(progress,
0~100)을 돌려드리며, 완료되면 결과의 bubbleId가 채워집니다.
완성된 분석 결과를 ID로 조회합니다.
요청하실 때 함께 보내신 식별자(externalKey)로 결과를
조회합니다. 설문 결과 페이지처럼 "이미 분석한 적이 있으면 그대로
보여주기" 흐름에 사용하시면 편리합니다. 해당 결과가 없으면
404를 돌려드립니다.
분석 결과에 대한 짧은 인사이트 리포트를 마크다운 문자열로 돌려드립니다. 자세한 내용은 아래 인사이트 리포트 섹션을 참고해 주세요.
호출 키로 저장된 버블 목록을 최신순으로 가져옵니다. 가벼운 메타데이터(id · title · createdAt · externalKey · hasInsight)만 돌려 드리며, 전체 데이터·인사이트 본문은 포함하지 않습니다. 자세한 내용은 아래 목록 조회 섹션을 참고해 주세요.
저장된 버블 한 건을 영구 삭제합니다 (텍스트·라벨·메타·인사이트 모두 포함). 본인 키로 만든 버블만 삭제할 수 있으며, 복구는 불가합니다. 자세한 내용은 아래 삭제 섹션을 참고해 주세요.
호출 키 + externalKey 조합으로 저장된 버블을 삭제합니다. id를 따로 관리하지 않는 통합 환경에서 편리합니다.
POST /v1/bubbles와 POST /v1/bubbles/sync의
요청 형식은 동일합니다.
{
"title": "치폴레 한국 출시 반응",
"texts": ["커피 좋아요", "스벅 별로...", "..."],
"externalKey": "survey-abc-q4", // 선택: 캐싱·재조회에 사용하실 식별자
"params": {
"labelOption": "5단어 이내, ~해요체로 종결",
"metaLabelOption": "4단어 이내"
}
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
title | string | ✅ | 분석 제목입니다. 1~200자. |
texts | string[] | ✅ | 분석할 텍스트 배열입니다. 무료 1~100개 (비즈니스 5,000개). |
weights | number[] | — | texts와 1:1로 정렬된 버블 크기 가중치입니다. 생략하시면 모두 1 — 같은 텍스트가 여러 번 나오면 그 빈도가 크기가 됩니다. 같은 텍스트는 weight를 합산하고, 0·음수는 1로 처리합니다. 원시 지표는 압축 변환을 권장합니다(댓글 수 → 1 + Math.sqrt(n), 좋아요 → 1 + Math.log1p(likes) — 기준선 1을 더해 0과 1을 구분하고 항상 양수 유지). 공유 버블에도 그대로 반영됩니다. |
externalKey | string | — | 1~200자. 같은 키로 다시 호출하시면 저장된 결과를 그대로 돌려드립니다. |
force | boolean | — | true로 보내시면 저장된 결과를 지우고 다시 분석합니다. |
report | boolean | — | true면 분석과 함께 인사이트 리포트도 생성해 응답 insight(마크다운)로 돌려드리고 캐시에 저장합니다. (생략 시 나중에 GET /:id/insight로 가져오셔도 됩니다.) |
params.labelOption | string | — | 라벨의 톤과 길이를 자연어로 지정하실 수 있습니다. |
params.metaLabelOption | string | — | 메타 라벨의 톤과 길이를 자연어로 지정하실 수 있습니다. |
params.reportPreset | string | — | 인사이트 리포트의 형식을 고릅니다. bubble(기본)·summary·executive·story·briefing·custom. 자세한 형식은 아래 인사이트 리포트 참고. |
params.reportOption | string | — | 리포트의 톤·관점·길이·강조점을 자연어로 지정합니다 (최대 500자). reportPreset=custom일 때는 이 값이 리포트 작성 지시문 전체가 됩니다. |
분석이 완료되면 아래 형태의 JSON을 돌려드립니다.
{
"id": "rO_EFF1huT3B1k6Y",
"title": "치폴레 한국 출시 반응",
"data": {
"texts": [
{ "text": "...", "label": "치폴레 오픈 기대감", "embedding2D": [1.23, -0.4] }
],
"labels": [
{
"label": "치폴레 오픈 기대감",
"metaLabel": "브랜드 상륙 기대 및 환호",
"embedding2D": [0.5, 0.2],
"sentimentScore": 5
}
],
"metaLabels": [
{ "metaLabel": "브랜드 상륙 기대 및 환호", "embedding2D": [0, 0] }
],
"meta": { "sampleSize": 179, "subgroupCount": 23, "metaLabelCount": 5 }
}
}
sentimentScore는 묶음의 감정 강도입니다 (1=매우 부정 … 3=중립 … 5=매우 긍정).embedding2D는 시각화에 사용하는 2D 좌표입니다.
GET /v1/bubbles/:id/insight는 분석 결과의 주요 클러스터 비율과
대표 발화를 LLM에 전달해 짧은 한국어 요약 리포트를 만들어 드립니다.
리포트는 처음 호출하실 때 생성되어 저장되며, 이후 같은 ID로 다시
요청하시면 캐시된 동일한 결과를 즉시 돌려드립니다.
curl https://api.affinitybubble.com/v1/bubbles/<id>/insight \
-H "Authorization: Bearer ab_live_..."
응답 형식은 다음과 같습니다.
{
"id": "rO_EFF1huT3B1k6Y",
"insight": "**한눈에 보기**\n\n…\n\n**핵심 인사이트**\n\n- …\n\n**흥미로운 관찰**\n\n- …\n\n**주요 사용자 목소리**\n\n> \"…\"\n",
"cached": false
}
리포트는 네 섹션으로 구성됩니다.
reportPreset)
버블을 만드실 때(POST /v1/bubbles) report: true와 함께
params.reportPreset을 보내 리포트의 형식을 고르실 수 있습니다.
지정하지 않으시면 위에서 설명한 bubble 형식으로 작성됩니다.
(나중에 GET /:id/insight로 가져오실 때는 형식 선택 없이 기본 리포트와
reportOption 자유 지시문만 지원합니다.)
| 값 | 형식 |
|---|---|
bubble (기본) | 차트 설명형 — 한눈에 보기 / 핵심 인사이트 / 흥미로운 관찰 / 주요 사용자 목소리 (위 네 섹션). |
summary | 한줄 · 세줄 · 상세 3단 요약 (데이터에 맞춰 분량 적응). 응답에 구조화된 report{oneLine, threeLine, detailed}도 함께 돌려드립니다. |
executive | 같은 3단 요약을 임원 브리핑 톤으로 작성합니다. |
story | 같은 3단 요약을 기자 내러티브 톤으로 작성합니다. |
briefing | 같은 3단 요약을 실무 브리핑 톤으로 작성합니다. |
custom | params.reportOption에 적으신 지시문을 그대로 받아 자유형 마크다운 리포트를 작성합니다 (reportOption 필수). |
| 쿼리 | 예시 | 설명 |
|---|---|---|
force |
?force=1 |
캐시를 무시하고 새로 생성합니다. 생성된 결과는 캐시에 덮어쓰여집니다. |
reportOption |
?reportOption=<자연어 규칙> |
자연어로 톤·관점·길이·강조점 등을 지정합니다 (최대 500자).
reportOption이 있는 호출은 캐시를 사용하지 않고 매번 새로 생성하며,
결과는 캐시에 저장하지도 않으니 기본 리포트는 유지됩니다.
|
예) "B2B 의사결정자가 30초 만에 읽고, 다음 분기 의사결정에 쓸 수 있게 작성"
# reportOption 값은 URL 인코딩해서 전달하세요 (아래는 가독성을 위해 디코딩 표기)
curl "https://api.affinitybubble.com/v1/bubbles/<id>/insight?reportOption=B2B 의사결정자 관점, 30초 요약" \
-H "Authorization: Bearer ab_live_..."
insight 필드는 마크다운 문자열입니다.
서버에서 HTML로 미리 렌더링하지 않으니, 사용 환경에 맞는 마크다운
파서(예: marked, markdown-it)로 표시해 주세요.
**),
불릿(-), 인용(>) 세 가지뿐이라
간단한 변환기로도 충분히 표시하실 수 있습니다.
GET /v1/bubbles는 호출 키 소유의 버블을 최신순으로
반환합니다. 전체 데이터·인사이트 본문은 포함하지 않고, 식별·정렬에
필요한 메타데이터만 돌려드립니다.
curl "https://api.affinitybubble.com/v1/bubbles?limit=20&offset=0" \
-H "Authorization: Bearer ab_live_..."
응답 예시:
{
"items": [
{
"id": "rO_EFF1huT3B1k6Y",
"title": "치폴레 한국 출시 반응",
"createdAt": 1780304510,
"externalKey": "survey-abc-q4",
"hasInsight": true
}
],
"total": 46,
"limit": 20,
"offset": 0
}
| 쿼리 | 기본값 | 설명 |
|---|---|---|
limit | 20 | 한 페이지당 항목 수. 1~100. |
offset | 0 | 건너뛸 항목 수. 페이지네이션에 사용하세요. |
저장된 버블을 영구 삭제합니다. 텍스트·라벨·메타 좌표·인사이트 등 해당 row의 모든 데이터가 함께 사라지며, 복구는 불가합니다. 설문 응답자 요청, 보관 기간 정책, 잘못된 입력의 회수 등 보안·개인정보 목적에 사용하세요.
# id로 삭제
curl -X DELETE "https://api.affinitybubble.com/v1/bubbles/<id>" \
-H "Authorization: Bearer ab_live_..."
# externalKey로 삭제
curl -X DELETE "https://api.affinitybubble.com/v1/bubbles/by-key/<externalKey>" \
-H "Authorization: Bearer ab_live_..."
응답 예시:
{ "id": "rO_EFF1huT3B1k6Y", "deleted": 1 }
{ "externalKey": "survey-abc-q4", "deleted": 1 }
403.404가 돌아옵니다.bubbles.delete)로 기록되어 감사 추적이 가능합니다.
오류가 발생하면 { "error": { "type", "message" } } 형태로
돌려드립니다.
| HTTP | type | 의미 |
|---|---|---|
| 400 | invalid_request | 요청 본문이 형식에 맞지 않습니다. |
| 401 | unauthorized | 키가 없거나 형식이 잘못되었거나 만료되었습니다. |
| 404 | not_found | 요청하신 결과나 작업을 찾을 수 없습니다. |
| 500 | pipeline_error | 분석 처리 중 오류가 발생했습니다. |
texts: 한 요청당 무료 1~100개입니다 (비즈니스 등급
5,000개). 100개를 넘는 규모 분석은
biz@affinitybubble.com으로
문의해 주세요.
429 quota_exceeded). 이미 분석된 결과를 다시 보는 것(캐시
조회)은 횟수에 포함되지 않습니다. 상향은 비즈니스 등급으로 문의해 주세요.
title: 1~200자*로 열려
있어도 프론트에서 직접 호출하라는 뜻이 아닙니다 — ab_live_
키가 페이지에 노출되면 누구나 여러분 쿼터로 호출할 수 있습니다. 키는
서버에만 두고, 브라우저는 여러분 백엔드 프록시를 통해
호출합니다 — 아래 프론트·백엔드 예시를 그대로 쓰시면 됩니다.
아래 프론트는 키 없이 여러분 서버 프록시(url)를
호출하고, 프록시가 서버 키를 붙여 API로 SSE를 그대로 패스스루합니다.
페이지를 열면 회색 자리표시 버블이 먼저 나타나고, 분석이
단계적으로 조립되어 실제 버블로 교체됩니다(스트리밍).
externalKey를 주면 다음 방문 때 캐시에서 바로 그려집니다.
프론트 (브라우저 — 키 없음)
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<style>
body { margin: 0; font-family: "Noto Sans KR", system-ui; background: #f6f6f3; }
#chart { display: flex; justify-content: center; padding: 20px; }
</style>
</head>
<body>
<div id="chart"></div>
<script src="https://cdn.jsdelivr.net/gh/pxd-uxtech/voronoi-bubble-dist@53bb2ae/dist/voronoi-bubble.standalone.umd.js"></script>
<script src="https://api.affinitybubble.com/affinitybubble-client.js"></script>
<script>
// apiKey 없음 — url 로 우리 서버 프록시를 호출 (프록시가 서버 키를 붙임)
AffinityBubble.createStream({
url: "/api/ab/stream", // 우리 백엔드 프록시 (아래)
target: "#chart",
title: "분석 제목",
texts: ["여기에 분석할 텍스트", "두 번째 텍스트" /* ... */],
externalKey: "survey-2026q1", // (선택) 다음 방문 때 캐시에서 즉시
params: {
labelOption: "5단어 이내, ~해요체로 종결",
metaLabelOption: "4단어 이내",
// 메타 이름 앞 이모지는 기본 on — 끄려면 emoji: "off"
},
width: 1200, height: 900,
}).then(({ response }) =>
// 완성 후 인사이트 리포트 — 이것도 프록시 경유. 기본은 경량 렌더이고,
// 정식 마크다운이 필요하면 render: marked.parse 를 넘기세요.
AffinityBubble.renderInsight({ target: "#report", url: "/api/ab/insight/" + response.id })
);
</script>
</body>
</html>
백엔드 프록시 (서버 — 키 보관 + SSE 패스스루)
// 우리 백엔드: 키는 환경변수에만. SSE 는 버퍼링 없이 그대로 흘려보낸다.
app.post("/api/ab/stream", async (req, res) => {
// (여기서 우리 서비스 로그인·권한 검증)
const upstream = await fetch("https://api.affinitybubble.com/v1/bubbles/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AB_LIVE_KEY}`, // 서버 키 (환경변수)
},
body: JSON.stringify(req.body),
});
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("X-Accel-Buffering", "no"); // 중간 프록시 버퍼링 차단
for await (const chunk of upstream.body) res.write(chunk); // resp.json() 금지 — 그대로 파이프
res.end();
});
// 리포트도 같은 식으로 GET 프록시
app.get("/api/ab/insight/:id", async (req, res) => {
const r = await fetch(
`https://api.affinitybubble.com/v1/bubbles/${req.params.id}/insight`,
{ headers: { Authorization: `Bearer ${process.env.AB_LIVE_KEY}` } });
res.status(r.status).json(await r.json());
});
markdownToHtml)를 씁니다.
표·링크 등 정식 마크다운이 필요하면 marked를 불러
renderInsight({ …, render: marked.parse })로 넘기세요.
url 대신
apiKey: "ab_live_…"를 직접 넘겨 API를 바로 호출할 수도 있습니다.
단 공개 페이지에는 금지입니다(키 노출).
https://api.affinitybubble.com/affinitybubble-client.js를
voronoi-bubble 라이브러리와 함께 불러오시면
AffinityBubble 객체를 사용하실 수 있습니다.
AffinityBubble.create({
apiKey: "ab_live_...",
target: "#chart",
title: "제목",
texts: ["텍스트1", "텍스트2", /* ... */],
params: { labelOption: "5단어 이내, ~해요체" },
});
분석 시작부터 결과 조회까지 알아서 처리하며, 진행되는 동안 자리표시 버블과 진행 막대를 함께 보여 드립니다.
AffinityBubble.createAsync({
apiKey: "ab_live_...",
target: "#chart",
title: "제목",
texts: [/* ... */],
pollIntervalMs: 1000,
onProgress: ({ status, currentStep, progress }) => {
// 선택: 사용하시는 UI도 함께 업데이트하실 수 있습니다.
},
});
| 옵션 | 기본값 | 의미 |
|---|---|---|
pollIntervalMs | 1000 | 진행 상황을 확인하는 간격(ms)입니다. |
maxWaitMs | 300000 | 최대 대기 시간입니다. 초과하면 오류를 냅니다. |
showProgressUI | true | 자리표시 버블과 진행 막대를 자동으로 표시합니다. |
onProgress | — | 진행 상황이 갱신될 때마다 호출되는 콜백입니다. |
페이지를 열면 먼저 저장된 결과가 있는지 확인하고, 있으면 즉시 보여
드립니다. 없으면 분석한 뒤 같은 externalKey로 저장해 두므로,
다음 방문 때는 다시 분석하지 않고 바로 표시됩니다.
AffinityBubble.createOrShow({
apiKey: "ab_live_...",
target: "#chart",
externalKey: "survey-abc-q4", // 결과를 식별할 안정된 값
title: "4번 문항 — 주관식",
texts: responses.map((r) => r.text),
// force: true // 내용을 갱신해 다시 분석하실 때만
});
// → { svg, response, cached }
AffinityBubble.render({ target: "#chart", response: existingJson });
스트리밍 과정을 보여줄 필요 없이 이미 분석된 결과만 바로 그릴 때 가장 간단합니다. 데이터를 받는 방법은 두 가지입니다.
방법 A — 서버가 데이터 주입, 브라우저는 시각화만 (키 0 · AB 호출 0 · 가장 안전, 권장)
서버가 키로 분석 결과(data)를 받아 페이지에 주입(SSR)하거나
여러분 자체 응답에 실어 내려보냅니다. 브라우저는 그 data로 렌더만
하므로 AB로 가는 요청도, 키도 브라우저에 전혀 없습니다(프록시조차 불필요).
// 서버 (키는 환경변수) — 분석 결과를 받아 페이지에 주입
const { data } = await fetch(
"https://api.affinitybubble.com/v1/bubbles/by-key/survey-2026q1",
{ headers: { Authorization: `Bearer ${process.env.AB_LIVE_KEY}` } },
).then((r) => r.json());
// data 를 SSR 로 <script>window.__BUBBLE__ = …</script> 주입하거나 여러분 API 응답에 포함
// 브라우저 — 주입된 data 로 시각화만 (AB·키 호출 전혀 없음)
AffinityBubble.render({ target: "#chart", response: { data: window.__BUBBLE__ } });
방법 B — 브라우저가 프록시로 조회 후 렌더 (페이지 로드 후 비동기로 가져올 때)
// 프론트 — 프록시로 캐시된 결과를 받아 그대로 렌더 (스트리밍 불필요)
const response = await fetch("/api/ab/bubbles/by-key/survey-2026q1").then((r) => r.json());
AffinityBubble.render({ target: "#chart", response });
// id 를 안다면: fetch("/api/ab/bubbles/" + id)
// 백엔드 — 조회 GET 프록시 (키는 서버에서만)
app.get("/api/ab/bubbles/:id", async (req, res) => {
const r = await fetch("https://api.affinitybubble.com/v1/bubbles/" + req.params.id,
{ headers: { Authorization: `Bearer ${process.env.AB_LIVE_KEY}` } });
res.status(r.status).json(await r.json());
});
app.get("/api/ab/bubbles/by-key/:key", async (req, res) => {
const r = await fetch(
"https://api.affinitybubble.com/v1/bubbles/by-key/" + encodeURIComponent(req.params.key),
{ headers: { Authorization: `Bearer ${process.env.AB_LIVE_KEY}` } });
res.status(r.status).json(await r.json()); // 404 → 아직 분석 전
});
render로
충분합니다(프록시 GET 1~2개). 분석이 처음 도는 과정을 점진적으로
보여주려면 스트리밍(createStream + stream 프록시)을
쓰세요. 둘을 합쳐 "by-key로 먼저 조회 → 없으면 스트리밍" 흐름도 가능합니다.
버블 차트를 안 그리고 분석 결과(data)만 받아 여러분 표·차트·
저장소에 그대로 쓰셔도 됩니다. data에는 군집(labels/
metaLabels), 감성 점수(sentimentScore 1~5), 대표 인용
(quotes), 그리고 UMAP 2D 좌표(embedding2D)까지
들어 있어 좌표 계산도 필요 없습니다. (voronoi-bubble 라이브러리를 안 불러도 됩니다.)
// 프록시로 분석 결과(data)만 받기 — 시각화는 여러분이 직접
const { data } = await fetch("/api/ab/bubbles/by-key/survey-2026q1").then((r) => r.json());
// 예 1) 상위 묶음별 비중 · 평균 감성을 직접 집계 → 표/막대차트에 사용
const byMeta = {};
for (const l of data.labels) {
const m = (byMeta[l.metaLabel] ??= { labels: 0, sentSum: 0 });
m.labels++; m.sentSum += l.sentimentScore;
}
const rows = Object.entries(byMeta).map(([meta, m]) => ({
meta, labelCount: m.labels, avgSentiment: (m.sentSum / m.labels).toFixed(2),
}));
// 예 2) data.texts 의 embedding2D[x,y] 로 산점도 등 커스텀 시각화
const points = data.texts.map((t) => ({ x: t.embedding2D[0], y: t.embedding2D[1], label: t.label }));
POST /api/ab/sync(프록시→
/v1/bubbles/sync)로 받아 res.data를 쓰면 됩니다.
한 번 분석한 결과는 externalKey로 위처럼 재조회(캐시)하세요.
curl https://api.affinitybubble.com/v1/bubbles/sync \
-H "Authorization: Bearer ab_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "고객 NPS 자유응답",
"texts": ["배송이 너무 느려요", "디자인은 만족합니다", "..."]
}'
const res = await fetch("https://api.affinitybubble.com/v1/bubbles/sync", {
method: "POST",
headers: {
Authorization: "Bearer ab_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "demo", texts: ["...", "..."] }),
});
const { data } = await res.json();
// data 를 voronoi-bubble 라이브러리에 그대로 전달해 렌더링하시면 됩니다.
// 1) 분석 시작
const job = await fetch("https://api.affinitybubble.com/v1/bubbles", {
method: "POST",
headers: { Authorization: "Bearer ab_live_...", "Content-Type": "application/json" },
body: JSON.stringify({ title: "demo", texts: [/* ... */] }),
}).then((r) => r.json()); // { instanceId, status: "queued" }
// 2) 진행 상황 확인
let snap;
do {
await new Promise((r) => setTimeout(r, 1000));
snap = await fetch(
`https://api.affinitybubble.com/v1/bubbles/jobs/${job.instanceId}`,
{ headers: { Authorization: "Bearer ab_live_..." } },
).then((r) => r.json());
} while (snap.status === "queued" || snap.status === "running");
// 3) 결과 받기
const bubble = await fetch(
`https://api.affinitybubble.com/v1/bubbles/${snap.bubbleId}`,
{ headers: { Authorization: "Bearer ab_live_..." } },
).then((r) => r.json());
POST /v1/bubbles/stream은 완성된 버블을 한 번에 돌려주는 대신,
분석이 진행되는 동안 렌더 가능한 중간 스냅샷을
text/event-stream(SSE)으로 흘려보냅니다. 클라이언트는 프레임마다
버블을 다시 그려 단계적으로 조립되는 과정을 보여줄 수 있습니다. 요청
형식은 /v1/bubbles/sync와 동일하며, deductive 파이프라인만
지원합니다.
| event | data | 용도 |
|---|---|---|
progress | { step, progress } | 단계 표시 (preparing → embedding → sampling → codebook → classify → grouping → polish) |
stage | { step, labels[], assignment[] } | 렌더 가능한 중간 스냅샷. assignment[i] = 입력 텍스트 i의 labels 인덱스(−1=미배정) |
done | { id, title, description, data } | 최종 결과 (data는 /sync와 동일 구조) |
error | { message } | 실패 |
const res = await fetch("https://api.affinitybubble.com/v1/bubbles/stream", {
method: "POST",
headers: { Authorization: "Bearer ab_live_...", "Content-Type": "application/json" },
body: JSON.stringify({ title: "demo", texts: ["...", "..."] }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const ev = buf.slice(0, i); buf = buf.slice(i + 2);
const name = ev.match(/event: (\w+)/)?.[1];
const data = JSON.parse(ev.match(/data: (.*)/s)?.[1] || "{}");
if (name === "progress") updateProgressBar(data.step, data.progress);
else if (name === "stage") renderFrame(data); // assignment 를 입력 texts 에 조인해 다시 그리기
else if (name === "done") renderFinal(data.data); // 최종 버블
else if (name === "error") showError(data.message);
}
}
분석이 완료된 버블 ID로 짧은 마크다운 리포트를 받아 오실 수 있습니다. 응답은 마크다운 문자열이라 표시 환경에 맞춰 직접 파싱해 주셔야 합니다. 자세한 규약은 /docs §인사이트 리포트 를 참고해 주세요.
const res = await fetch(
`https://api.affinitybubble.com/v1/bubbles/${bubbleId}/insight`,
{ headers: { Authorization: "Bearer ab_live_..." } },
);
const { insight, cached } = await res.json();
// insight 는 마크다운 문자열입니다 (**굵게** · "- 불릿" · "> 인용").
// marked, markdown-it 등 익숙한 마크다운 파서로 표시해 주세요.
container.innerHTML = renderMarkdown(insight);