Partner SDK API 레퍼런스 안내
이 문서는 Open Echo Partner SDK 공개 표면(공개적으로 지원되는 함수·타입·속성)의 레퍼런스입니다.
이 킷에 동봉된 SDK 버전은 0.7.0 이며, 아래 내용은 이 버전을 기준으로 작성되었습니다.
사용법과 예제는 guide 문서에서 안내합니다.
SDK 버전 간 호환 여부는 COMPATIBILITY 문서에서 안내합니다.
이 문서에 이름이 없는 함수·타입·속성은 공개 계약이 아니며, 예고 없이 추가·변경·제거될 수 있습니다.
Partner SDK 공개 인터페이스 계약 (API Contract)
대상 SDK 버전: 0.7.0 · 패키지 @openecho/partner-sdk · preview (0.x)
0. 이 문서의 지위
이 문서는 Partner SDK 공개 인터페이스의 규범(normative) 문서다. SDK 는 이 문서를 기준으로 버전별로 유지된다.
- 공개 계약은 이 문서의 표면 정의 절과 버전 이력이 정의하며, 비공개로 명시된 표면은 계약이 아니다. 코드에서 도달 가능하더라도 그 절들에 없으면 공개 계약이 아니며, 비공개 표면으로 열거된 항목은 예고 없이 변경될 수 있다.
- 사용법·튜토리얼은
README.md와docs/partner-kit/guide/(11편)가 담당한다 — 그 문서들은 설명적(informative)이며, 충돌 시 이 문서가 우선한다.
버전 규칙 (0.x preview)
| 변경 종류 | 버전 | 의무 |
|---|---|---|
| 표면 제거·시그니처 비호환 변경 (breaking) | minor ↑ | 이 문서 버전 이력 기록 + CHANGELOG 명시 + COMPATIBILITY.md 매트릭스 갱신 |
| 표면 추가 (additive) | minor ↑ | 이 문서에 X.Y.Z+ 주석으로 도입 버전 표기 |
| 필수 필드 → XOR-optional 완화 | additive 로 판정하되 3조건 전부 충족 시에만: ① 기존 타입-유효 호출이 전수 그대로 유효 ② 런타임 불변(직렬화·idempotency·서버 검증 결과 동일) ③ 구 SDK 가 동일 응답 수신. 하나라도 깨지면 breaking | 선례: 0.2.0 CreateTemplateInput(버전 이력 참조) |
| 동작 수정 (표면 불변) | patch ↑ | CHANGELOG 만 |
GA(1.0.0) 이후 breaking 은 major 로 승격된다. preview 기간에도 breaking 은 버전 이력에 반드시 기록한다 — 서버 min-version 게이트(§5 SdkVersionUnsupported)와 킷 호환 매트릭스가 이 이력에 의존한다.
표기 규약
- 항목 옆
0.2.0+= 그 버전에 도입. 무표기 = 0.1.0(최초 릴리스)부터 존재. ?(optional) 필드 중 "서버 확장" 주석이 있는 것은 구버전 서버가 내려주지 않으면undefined— 소비자는 undefined 를 처리해야 한다 (SDK 가 아니라 서버 버전에 의존하는 필드).
1. 진입점
1.1 npm (ESM / CJS) — 번들러 통합
import { createClient, SDK_VERSION } from "@openecho/partner-sdk";
const oe = createClient(config); // OpenEchoClient
- MIC 구현은
mic.start()/mic.preload()시 동적 청크(@openecho/partner-sdk/mic)로 로드된다 — core 번들에 실시간 미디어 엔진 없음.
1.2 UMD (self-host / CDN) — script 태그 통합
<script src="/assets/openecho-sdk.<version>.umd.js"></script>
<script> const oe = window.OpenEcho.createClient(config); </script>
- 전역 네임스페이스
window.OpenEcho에 npm 표면과 동일한 export 가 실린다 (createClient·SDK_VERSION·에러 클래스·mapError·ERROR_BIJECTION). - MIC 포함 all-in-one 단일 파일 — 동적 청크 없음.
<script>는 classic 으로 로드해야 한다(type="module"금지).
1.3 createClient(config: ClientConfig): OpenEchoClient
ClientConfig 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
baseUrl | string | ✅ | Open Echo API 호스트. trailing slash 는 정규화 제거. 누락 시 즉시 Error throw |
publicKey | string | ✅ | 도메인 잠금 공개 키 (pk_live_…/pk_test_…). 누락 시 즉시 Error throw |
fetch | typeof fetch | — | fetch 주입(테스트·특수 런타임). 미주입 + 전역 fetch 부재 시 즉시 Error throw |
micChunkUrl | string | — | MIC 청크 URL 수동 지정 (npm 전용 — UMD 는 MIC inline) |
scriptNonce | string | — | CSP nonce — MIC 청크 <script> 주입 시 사용 (npm 전용) |
1.4 SDK_VERSION: string
package.json 버전과 일치하는 상수 (테스트로 강제). 0.2.1 이상에서만 신뢰 가능 — 0.2.0 배포본에 이전 값이 남은 결함이 있었다 (버전 이력 참조).
1.5 프로토콜 동작 (표면 아님, 계약임)
0.3.0+모든 요청(REST·SSE·MIC lease)에X-OpenEcho-Sdk-Version요청 헤더 자동 부착. 서버 min-version 게이트가 이를 판독하며, 미달 시 HTTP 426 →SdkVersionUnsupported(§5).0.3.0+서버 응답의X-OpenEcho-Sdk-Warn-Below헤더 감지 시console.warn1회 (차단 예고).
2. 클라이언트 표면 전체 트리
OpenEchoClient — createClient() 반환 타입. _ 접두 멤버는 타입에서 제외되며 비공개 표면이다.
oe.ping() → Promise<PingResult>
oe.targets.list() → Promise<DeviceView[]>
oe.groups.create(req) → Promise<GroupView>
oe.groups.list(opts?) 0.6.0+ → Promise<GroupView[]>
oe.groups.get(handle) → Promise<GroupView>
oe.groups.addMembers(handle, deviceHandles) → Promise<void>
oe.groups.removeMembers(handle, deviceHandles) → Promise<void>
oe.groups.members(handle) → Promise<DeviceView[]>
oe.groups.delete(handle) → Promise<void>
oe.contents.list(opts?) 0.6.0+ → Promise<ContentView[]>
oe.contents.get(handle) → Promise<ContentView>
oe.contents.create(req) 0.2.0+ → Promise<ContentView>
oe.contents.update(handle, req) 0.2.0+ → Promise<ContentView>
oe.templates.create(req) → Promise<TemplateView>
oe.templates.list() → Promise<TemplateView[]>
oe.templates.get(handle) → Promise<TemplateView>
oe.templates.update(handle, req) → Promise<TemplateView>
oe.templates.delete(handle) → Promise<void>
oe.schedules.create(req) → Promise<ScheduleView>
oe.schedules.list(opts?) 0.6.0+ → Promise<ScheduleView[]>
oe.schedules.get(handle) → Promise<ScheduleView>
oe.schedules.update(handle, req) → Promise<ScheduleView>
oe.schedules.delete(handle) → Promise<void>
oe.history.list(opts?) 0.6.0+ → Promise<Page<HistoryView>>
oe.history.get(sessionId) → Promise<HistoryView>
oe.languages.list() 0.2.0+ → Promise<LanguageOption[]>
oe.broadcast.tts.send(input) → Promise<BroadcastReceipt>
oe.broadcast.tts.stop(sessionId) 0.2.0+ → Promise<void>
oe.broadcast.tts.fromContent(input) 0.2.0+ → Promise<BroadcastReceipt>
oe.broadcast.subscribe(sessionId, handlers) → { close(): void }
oe.broadcast.mic.isSupported() → boolean
oe.broadcast.mic.preload() → Promise<void>
oe.broadcast.mic.start(input) → Promise<MicSession>
제거된 표면: contents.clone(sourceHandle) — 0.1.0 에 존재, 0.2.0 에서 제거 (버전 이력 참조).
3. 네임스페이스 레퍼런스
모든 메서드는 실패 시 OpenEchoError 하위 클래스를 throw 한다 (§5). SSE 만 예외적으로 onError 콜백으로 전달한다.
3.1 ping()
GET /partner/v1/ping — 키·origin 유효성 즉시 확인. 반환 PingResult { appName, appId, origin }.
3.2 targets — 노출 디바이스 읽기 전용
list()— 앱에 노출된 디바이스 목록. 방송 대상 선택의 소스.- 전체 위임(
FULL_DELEGATION) 앱은 명시 노출 없이도 현장의 사용 가능한 디바이스가 자동으로 목록에 나타난다. handle 은 불투명 문자열이며 접두(tgt_/atg_)에 의미를 두지 말 것. 목록은 조회 시점 스냅샷이다. - 자동 노출 handle(
targets.list()가 반환하는 것 포함)은 방송 대상 지정(broadcast.*)·그룹 멤버 구성·재방송·**템플릿 개별 디바이스 지정(deviceHandles)**에 모두 사용할 수 있다(전 경로 관통).
3.3 groups — 앱 소유 디바이스 그룹 CRUD (update 없음)
create({ name, description? })—description?은0.2.0+list({ search? }?)—search는 그룹 이름 부분일치(대소문자무시), 미지정 시 전량. 필터 파라미터는0.6.0+— 이 필터를 포함한 api-server 릴리스 이후에만 서버가 적용한다(구서버는 파라미터를 무시하고 전량 반환).get(handle)·members(handle)addMembers(handle, deviceHandles[])·removeMembers(handle, deviceHandles[])delete(handle)— 논리 삭제. 그룹 이름 변경(update)은 제공하지 않음.
3.4 contents — 방송 컨텐츠 (노출분 read + 자작 create)
list({ search? }?)·get(handle)— SITE 노출분 + 자작분. 노출분의body는null(원문 비노출 정책).search는 컨텐츠 제목 부분일치(대소문자무시), 미지정 시 전량. 필터 파라미터는0.6.0+— 이 필터를 포함한 api-server 릴리스 이후에만 서버가 적용한다(구서버는 파라미터를 무시하고 전량 반환).create({ title, text, languages[] })0.2.0+— 자작 컨텐츠. 원문은 한국어, 워커가 번역+합성.update(handle, { title, addLanguages? })0.2.0+— 제목 변경 + 언어 추가만(add-only, 원문·기존 언어 불변). 자작분만 가능.
3.5 templates — 방송 템플릿 CRUD
create(CreateTemplateInput)— 대상은groupHandle(그룹) 또는deviceHandles(개별 다중) 중 정확히 하나 (서버 XOR 검증 400).deviceHandles모드는0.2.0+(0.1.0 은groupHandle필수 단일 모드).- derived(자동 노출) handle 수용 (전체 위임·서버 동작, SDK 표면 무변경): 명시 노출 없이
deviceHandles에 자동 노출 handle(atg_)을 그대로 넣어 생성할 수 있다(broadcast.*와 동일하게 전 경로 관통). 생성은 all-or-none — 하나라도 현재 실효 노출 밖이면404 TARGET_UNRESOLVED(부분 생성 없음). 조회(get/list)의deviceHandles는 조회 시점의 실효 노출 기준으로 재도출된다(저장 시점 고정 아님): explicit(tgt_) 우선 · 자동 노출은atg_· 노출이 사라진(미할당·전환) 디바이스는 제외(존재 은닉, §3.7 이력과 동형). 예약 발화 시에는 발화 시점 실효 노출분만 대상이 되고 나머지는 조용히 제외된다(부분 발화) — 즉 create=all-or-none 이지만 fire=partial-drop 인 의도된 비대칭. list()·get(handle)·update(handle, UpdateTemplateInput)·delete(handle)— delete 는 참조 예약 존재 시 409.- 알려진 계약 비대칭:
CreateTemplateInput.languageOrder는string(CSV, 예"ko,en") 인 반면FromContentInput.languageOrder는string[]다. 역사적 사실로 유지되는 계약이며, 통일 시도는 breaking 판정 대상이다.
3.6 schedules — 예약 방송 CRUD
create(CreateScheduleInput)·list({ search? }?)·get(handle)·update(handle, UpdateScheduleInput)·delete(handle)search는 예약 이름 부분일치(대소문자무시), 미지정 시 전량. 필터 파라미터는0.6.0+— 이 필터를 포함한 api-server 릴리스 이후에만 서버가 적용한다(구서버는 파라미터를 무시하고 전량 반환).UpdateScheduleInput은Partial기반으로 타입이 서버 검증보다 느슨하다 — 최종 authority 는 서버(400 응답). (알려진 DX 갭 — 후속 강화 후보)
3.7 history — 방송 이력 읽기 전용
list({ page?, size?, type?, search? }?)— 페이지네이션 + 필터, 반환Page<HistoryView>. 정렬은 서버 고정(createdAt,id DESC) — client sort 지정 불가, 클라이언트 재정렬은 소비자 책임.type?: 'MIC' | 'TTS'— 공개 계약값. 서버는 wiretype을 이 값으로 양방향 매핑한다(요청:'TTS'→내부AUDIO·'MIC'→MIC, 미지정=전체 / 응답: 아래HistoryView.type참조).search?: string— 컨텐츠 제목 부분일치(대소문자무시), 미지정 시 전량.- 필터 파라미터(
type·search)는0.6.0+— 이 필터를 포함한 api-server 릴리스 이후에만 서버가 적용한다(구서버는 파라미터를 무시하고 전량 반환, 정렬도 서버 기본값을 따른다).
get(sessionId)— 상세.deviceHandles(당시 대상)는 상세에서만 채워진다.deviceHandles는 조회 시점의 실효 노출 기준이다. 운영 방식 전환(전체 위임→선별)으로 노출이 사라진 디바이스는 목록에서 제외되며, 전부 사라진 경우 빈 배열이 정상이다.- 응답
HistoryView.type은'MIC' | 'TTS'0.6.0+(narrowing — 이전은string). SDK 가list()/get()응답을 단일 지점에서 정규화한다(wireAUDIO→공개TTS). 서버가 알 수 없는 wire type 값을 내려주면 조용히 오분류하지 않고ProtocolError(§5.2)를 throw 한다.
3.8 languages 0.2.0+ — 현장 활성 언어 읽기 전용
list()— 앱이 바인딩된 현장의 활성 TTS 언어. 언어 선택 UI 의 소스.- 정렬:
ko(원문 언어) 우선, 이후name가나다 오름차순.name은 한국어 표기 — 서버 카탈로그 V068 이후 기준(이전 서버는 자체 표기 native + 코드순 정렬). 값 시맨틱은 서버 릴리스 소관, SDK 타입 표면 무변경.
3.9 broadcast.tts
send(TtsSendInput)— 직접 텍스트 TTS 방송. 대상은targetHandle또는deviceHandles[]중 하나 (서버 XOR 400,deviceHandles모드0.4.0+).Idempotency-Key헤더로input.idempotencyKey전송. 반환BroadcastReceipt— 판정은status로 (admitted?는 파생 필드).stop(sessionId)0.2.0+— 자기 소유 TTS 방송 중단(재생 중 세션만). 종결 관찰은 SSEphase=STOPPED.fromContent(FromContentInput)0.2.0+— 노출/자작 컨텐츠 즉시 방송. 대상은targetHandle또는deviceHandles[]중 하나 (서버 XOR 400).- 방송은 보낸 handle 집합 그대로 대상이 되며, 입장 시점에 사용 가능 여부가 재검사된다. 예약 방송은 생성 시점 device set 이 발화 시점에 노출 재검사된다 — 전체 노출 해제된 예약 발화는 파트너 이력에 남지 않는다.
3.10 broadcast.subscribe(sessionId, handlers) → { close() }
SSE 기반 진행 구독 (스냅샷 우선 emit → 스트림).
SubscribeHandlers:onPhase(PhaseSnapshot)필수 ·onError?(OpenEchoError)·onClose?()(1회 보장)- 내부 재연결(backoff 최대 30s)·중복 제거(rank + per-device 내용 기준)·terminal 시 자동 종료.
close()는 멱등. - 에러는 throw 되지 않고
onError로 전달된다. 426(SdkVersionUnsupported)은 재연결 없이 즉시 종결 (0.3.0+).
3.11 broadcast.mic
isSupported(): boolean— WebRTC(RTCPeerConnection) 존재 확인.preload(): Promise<void>— MIC 청크 선로드 (single-flight, 중복 호출 안전, 실패 시 재시도 가능).start(MicStartInput): Promise<MicSession>— 마이크 선확보(0.5.0+) → MIC lease 획득 → 실시간 오디오 룸 연결·발행. 대상은targetHandle또는deviceHandles[]중 하나 (서버 XOR 400,deviceHandles모드0.4.0+). 마이크 획득 실패 시MicUnavailable(서버 미접촉 — 세션·점유 없음,0.5.0+). 연결·발행 실패 시 SDK 가 자원 정리 + 보상 stop 을 수행한 뒤 원 에러를 던진다(0.5.0+). npm 경로는 이 시점에 청크 동적 로드(실패 시MicChunkLoadFailed). replay 주의: 로컬 마이크 선확보가 서버 idempotency replay 에 선행 — 동일Idempotency-Key재시도라도 마이크 실패 시 서버 결과를 재현하지 않고MicUnavailable을 반환한다(서버 상태 불변).MicSession:sessionId·resolvedDeviceCount: number(lease 시점 대상 디바이스 수 —BroadcastReceipt.resolvedDeviceCount동형,0.7.0+) ·stop(): Promise<void>(멱등).- (api-server 0.38.0) 부분 활성화: 대상 디바이스 일부가 조인에 실패해도 1대 이상 조인하면 방송이
시작된다(조인 실패 디바이스는 제외). 전 디바이스가 실패하면 세션이
MIC_JOIN_FAILED로 종착한다. 진행 중 이력(history)의 fail 집계에 조인 실패가 반영된다. SDK 코드 변경 불요 — 서버측 동작 변경(코드 상세는 §5.4 "파트너 MIC 세션 전용" 참조). - (api-server 0.38.0) heartbeat 허용 확장: 내부 자동 heartbeat(15초 간격,
stop()전까지 계속)가 세션이 아직 조인 대기 중(CREATING)이어도 200 으로 수락된다(TTL·grant 연장 전용 — 상태 전이 없음). 종결된 세션(STOPPED/PARTIAL등)에 대한 heartbeat 는 여전히 거부되며err.code로 종착 사유를 구분할 수 있다 (MIC_SESSION_ENDED/MIC_JOIN_FAILED/MIC_SESSION_EXPIRED). heartbeat 는 SDK 내부 자동 호출이라 파트너 코드가 직접 다룰 필요는 없다. - 종착 자동정리 +
onSessionEnded(0.7.0+): 내부 heartbeat 가 서버 세션 종착 4종 (MIC_SESSION_NOT_FOUND/MIC_SESSION_ENDED/MIC_JOIN_FAILED/MIC_SESSION_EXPIRED, §5.4) 또는 연속 인증 실패(401/403 이 연속 4회 도달)를 감지하면, 콜백 지정 여부와 무관하게 마이크 트랙·실시간 오디오 룸을 즉시 로컬 정리한다(서버는 이미 종착 상태이므로stop()재호출은 생략). 정리 후 이stop()은 멱등 no-op 이 된다. 정리 직후onSessionEnded?.(reason)을 최대 1회 호출한다 —reason은 위 4종 서버 code ∪"AUTH_FAILED"(auth-lost 스트릭)이며 값 집합은 확장될 수 있으므로 알 수 없는 값도 종료로 처리할 것. 콜백이 던지는 예외는 SDK 밖으로 전파되지 않는다. 로컬stop()호출 경로에서는 미발화. 0.6.x 대비 행동 변경 — 이전에는 heartbeat 실패가 완전히 침묵했다(마이크가 죽은 세션에 계속 열려 있었다). CHANGELOG 0.7.0 참조.
4. 공개 타입
"서버 확장" = 구버전 서버에서 undefined 가능 (§0 표기 규약). 이 규약은 §4.1 뷰 타입뿐 아니라 §4.3 방송 진행 타입(예: PhaseSnapshot.devices)을 포함한 서버 응답을 담는 모든 공개 타입에 적용된다.
4.1 뷰 타입 (서버 응답)
DeviceView — handle · name · status · online?: boolean 0.2.0+(서버 확장 — 정보 라벨, 방송 시점 상태 비보장)
GroupView — handle · label · memberCount · status? 0.2.0+ · description?: string | null 0.2.0+ (서버 확장)
ContentView — handle · title · scopeType · 0.2.0+(서버 확장): languages?: string[](재생순 canonical) · body?: string | null(자작만, 노출분 null) · durationSeconds?: number | null(합성 전 null)
TemplateView — handle · name · contentHandle · targetMode · 0.2.0+(서버 확장): languageOrder? · groupHandle? · deviceHandles?: string[] | null
ScheduleView — handle · name · templateHandle · recurrence · executeTime · executionAvailability · 0.2.0+(서버 확장, 수정 폼 프리필용): executeDate? · daysOfWeek? · monthlyDay? · isLastDay? · startDate? · endDate? · notes?
HistoryView — sessionId · type: 'MIC' | 'TTS'(0.6.0+ narrowing — 0.5.0 까지는 string. SDK 가 wire AUDIO→TTS 로 정규화하며 unknown wire 값은 ProtocolError, §3.7·§5.2) · status · totalDevices · successDevices · failDevices · createdAt · triggeredBy · 0.2.0+(서버 확장): contentTitle? · contentHandle?(접근 가능 컨텐츠만, 재방송 조합용) · deviceHandles?(상세만)
Page<T> — content: T[] · page · size · totalElements
LanguageOption — code(canonical lowercase) · name(표시명 — 한국어 표기, §3.8). 타입 export 는 0.2.2+ (0.2.0~0.2.1 은 export 누락 결함).
PingResult — appName · appId · origin
4.2 입력 타입
TtsSendInput — targetHandle? | deviceHandles?: string[](XOR — deviceHandles 모드 0.4.0+) · text · language · idempotencyKey(필수) · additionalLanguages?: string[] 0.2.0+(다국어, 재생 순서 유지) · partial?: boolean · expectedRevision?: number
FromContentInput 0.2.0+ — contentHandle · targetHandle? | deviceHandles?: string[](XOR) · languageOrder?: string[](컨텐츠 언어와 어긋나면 서버 lenient 병합) · partial? · expectedRevision? · idempotencyKey(필수)
MicStartInput — targetHandle? | deviceHandles?: string[](XOR — deviceHandles 모드 0.4.0+) · partial? · idempotencyKey(필수) · onSessionEnded?: (reason: string) => void(0.7.0+ — 서버 세션 종착 또는 인증 상실을 heartbeat 로 감지했을 때 최대 1회 호출. reason = 서버 code ∪ "AUTH_FAILED" — 값 집합 확장 가능, 알 수 없는 값도 종료로 처리할 것. 로컬 stop() 시에는 미발화. 상세는 §3.11)
CreateContentInput 0.2.0+ — title · text · languages: string[]
UpdateContentInput 0.2.0+ — title · addLanguages?: string[]
CreateTemplateInput — name · contentHandle · groupHandle? | deviceHandles?: string[](XOR — deviceHandles 모드 0.2.0+) · languageOrder?: string
UpdateTemplateInput — name · languageOrder?: string
CreateScheduleInput — templateHandle · name · recurrence: "ONCE"|"DAILY"|"WEEKLY"|"MONTHLY" · executeTime · executeDate? · daysOfWeek? · monthlyDay? · isLastDay: boolean · startDate · endDate? · notes?
UpdateScheduleInput — Partial<Omit<CreateScheduleInput, "isLastDay">> & { isLastDay?: boolean }
4.3 방송 진행 타입
BroadcastReceipt — status: "ADMITTED" | "DENIED_BUSINESS"(canonical 판정 신호) · sessionId: string | null · resolvedDeviceCount · revision · denyReason: string | null · skipped: string[] · admitted?: boolean 0.2.2+(서버 직렬화 파생 필드 = status === "ADMITTED" — 판정에 쓰지 말 것)
PhaseSnapshot — sessionId · phase · terminal: boolean · reason: string | null · lastEventId · receipt: BroadcastReceipt | null · devices?: DeviceProgress[] 0.2.0+(서버 확장) · skippedAtDispatch?: number 0.7.0+(서버 확장 — 송출 시점 skip 된 대상 수, 구서버는 undefined)
DeviceProgress 0.2.0+ — deviceName · status(open string, 미지 값은 무시할 것). 어휘는 소스별로 다르다:
- TTS:
PENDING|PLAYING|DONE|FAILED - MIC(
0.7.0+):PENDING|JOINED|FAILED|TIMEOUT—SKIPPED는 이 배열에서 제외되며 아래skippedAtDispatch가 별도 집계한다
MIC 의 devices[] 는 "조인 결과"다 — 세션이 종결(terminal)된 이후에도 각 행은 마지막 join_state 를 유지한다. TTS 처럼 실시간 재생 상태를 반영하는 것이 아니라 "누가 조인했는가"의 결과 스냅샷이다.
MIC devices[] 빈 배열 판별: PhaseSnapshot.skippedAtDispatch?: number(0.7.0+, 서버 확장 — §0 규약, 구서버는 undefined)로 원인을 구분한다. MIC 세션의 devices 가 빈 배열로 도착하면: skippedAtDispatch > 0 이면 전 대상이 송출 시점에 skip 되어 정상적으로 빈 것이고, skippedAtDispatch 가 0 이거나 undefined 면 devices[] 투영을 지원하지 않는 구서버(api-server 0.39.0 미만 — MIC 는 항상 빈 배열 반환)의 응답일 가능성을 의심한다.
StreamTicket — ticket · expiresInSeconds (subscribe 내부에서 소비 — 직접 사용 경로는 비공개 표면)
SubscribeHandlers — §3.10
MicSession — §3.11
4.4 클라이언트 타입
OpenEchoClient — createClient 반환 타입. _ 접두 멤버 제외는 0.2.2+ (그 전 타입에는 _http 가 보였으나 계약이 아니었음).
ClientConfig — §1.3
5. 에러 계약
모든 에러는 OpenEchoError 추상 클래스를 상속하며 code(wire 코드)·httpStatus·message·name(클래스명)을 보존한다. instanceof 판별을 지원한다.
§5.1 의 표는 "매핑된 16종의 무결성(코드↔클래스 1:1)"을 보장할 뿐, 서버가 방출 가능한 코드의 전수가 아니다. 미매핑 코드는 UnknownOpenEchoError 로 도착하며 code 를 보존한다 (§5.3·§5.4).
5.1 매핑된 wire 에러 (코드 ↔ 클래스 bijection — 비전수)
| 클래스 | wire code | HTTP | 의미 |
|---|---|---|---|
Unauthorized | PARTNER_UNAUTHORIZED | 401 | publicKey 무효/거부 |
TicketUnauthorized | PARTNER_TICKET_UNAUTHORIZED | 401 | SSE 스트림 티켓 인증 실패 |
OriginForbidden | PARTNER_ORIGIN_FORBIDDEN | 403 | 등록 도메인 밖 호출 (도메인 잠금) |
CapabilityDenied | CAPABILITY_DENIED | 403 | 기능 capability 미허가 |
TargetUnresolved | TARGET_UNRESOLVED | 404 | 대상 handle 해석 불가 |
ContentNotReady | BROADCAST_CONTENT_NOT_READY | 400 | 컨텐츠 오디오 미준비 — MIC 경로에서는 api-server 0.38.0+ 부터 더 이상 발생하지 않는다(조인 부분 활성화 도입으로 대상 판정이 이동, TTS 경로는 무변화) |
BroadcastIneligible | BROADCAST_INELIGIBLE | 409 | 방송 불가 상태 (동시 방송 등) |
IdempotencyKeyReused | IDEMPOTENCY_KEY_REUSED | 409 | 동일 키가 다른 페이로드로 재사용 |
TargetRevisionStale | TARGET_REVISION_STALE | 409 | 대상 revision 변경 — expectedRevision 갱신 필요 |
SessionExpired | STREAM_GONE | 410 | SSE 세션 만료 |
UsageCapExceeded | USAGE_CAP_EXCEEDED | 429 | 일일 TTS quota 초과 |
RateLimited | RATE_LIMITED | 429 | 요청 속도 제한 초과 |
StreamLimitExceeded | STREAM_LIMIT_EXCEEDED | 429 | SSE 동시 스트림 한도 초과 |
RateLimitBackendUnavailable | RATE_LIMIT_BACKEND_UNAVAILABLE | 503 | 속도 제한 백엔드 일시 불가 |
SdkVersionUnsupported 0.3.0+ | SDK_VERSION_UNSUPPORTED | 426 | 서버 최소 SDK 버전 미만 — 재시도 무의미, SDK 업그레이드가 유일한 해소 |
DeviceInUse 0.4.0+ | DEVICE_IN_USE | 409 | 대상 중 사용 중 디바이스 포함 — 방송 전체 거절 (§5.4 승격 규칙의 첫 적용) |
5.2 클라이언트 합성 에러 (wire 코드 없음)
| 클래스 | 발생 지점 | 의미 |
|---|---|---|
MicChunkLoadFailed | mic.start()/preload() | MIC 청크 로드 실패 (네트워크/CSP) |
MicUnavailable | mic.start() | 마이크 획득 실패 (0.5.0+, 클라이언트측 — wire 아님). reason: "permission-denied" | "not-found" | "other" · causeName · causeMessage 필드 |
GatewayMasked | 전 요청 | 게이트웨이가 에러를 HTML 200 등으로 마스킹 — API 응답 아님 |
InvalidApiResponse | 전 요청 | 응답이 API 봉투 형식이 아님 |
ProtocolError 0.6.0+ | history.list()/get() 응답 정규화 | 서버가 알 수 없는 HistoryView.type wire 값을 내려줌 — 조용한 오분류 대신 즉시 실패 (httpStatus 502, 클라이언트측 — wire 응답 그 자체는 200이었을 수 있음) |
5.3 forward-compatibility
UnknownOpenEchoError— bijection 에 없는 wire 코드는 이 클래스로 도착한다. 서버가 새 에러 코드를 추가해도 SDK 는 깨지지 않는다. 소비자는 최종 fallback 으로instanceof OpenEchoError를 두어야 한다.- 새 wire 코드의 전용 클래스 추가는 additive(minor) — 단, 그 순간부터 해당 코드는
UnknownOpenEchoError가 아닌 전용 클래스로 도착하므로 CHANGELOG 에 명시한다.
5.4 미매핑 wire 코드 (비전수 채널)
서버 에러 채널 구조상 코드 단위 전수 목록은 원리적으로 유지 불가능하다 — BE GlobalExceptionHandler 의 BusinessException catch-all 이 모든 도메인 ErrorCode 를 통과시키므로, 코드 단위 목록은 작성 즉시 드리프트가 시작된다. 따라서 이 절은 채널 단위로 기술한다:
| 방출 채널 | 대표 코드 (예시, 비전수) | 도착 형태 |
|---|---|---|
| Spring generic 핸들러 (요청 형식 오류) | VALIDATION_FAILED(400) · INVALID_REQUEST_BODY(400) · MISSING_HEADER | UnknownOpenEchoError (code 보존) |
BusinessException catch-all (도메인 ErrorCode 전반) | 예: DEVICE_ALREADY_REGISTERED(409) | UnknownOpenEchoError (code 보존) — 승격된 코드(DEVICE_IN_USE 등, §5.1)는 제외 |
파트너 MIC 세션 전용 (api-server 0.38.0, BusinessException 하위집합 — 아래 상세) | MIC_SESSION_NOT_FOUND 외 5종 | UnknownOpenEchoError (code 보존) |
파트너 MIC 세션 코드 상세 (api-server 0.38.0 — 조인 부분 활성화 도입에 따른 세분화, §3.11 참조):
| wire code | HTTP | 의미 |
|---|---|---|
MIC_SESSION_NOT_FOUND | 404 | MIC 세션 부재·비소유 |
MIC_NO_ELIGIBLE_DEVICE | 422 | 시도 가능 디바이스 없음 — 세션 미생성 |
MIC_SESSION_ENDED | 409 | 종료된 MIC 세션 호출 — 파트너 stop()·heartbeat 만료·기타 서버 종료의 union이며 원인을 구분하지 않는다(구분이 필요하면 Phase 3 terminalReason 표면화로 위임) |
MIC_JOIN_FAILED | 409 | 전 디바이스 조인 실패로 종착된 세션 |
MIC_SESSION_EXPIRED | 409 | 최대 시간 초과 종착 |
MIC_LEASE_HELD | 409 | lease 발급 불가 상태 |
위 6종은 전용 클래스가 없다 — 전부 UnknownOpenEchoError 로 도착(code 보존). 프로그램적 분기가
필요하면 err.code 문자열 비교(승격 전 상태 — 아래 승격 규칙 참조).
위 4종(MIC_SESSION_NOT_FOUND/MIC_SESSION_ENDED/MIC_JOIN_FAILED/MIC_SESSION_EXPIRED)은 SDK
0.7.0+ 의 mic.start() 내부 heartbeat 가 종착 코드로 구조적 판별(err.code 문자열 비교, instanceof
금지 — 청크·core 별도 번들 그래프)해 자동 로컬 정리 + onSessionEnded 트리거를 수행한다(§3.11).
MIC_LEASE_HELD 는 비종착 취급(무시, 다음 heartbeat 재시도) — 위 4종과 혼동하지 말 것.
승격 규칙: 미매핑 코드가 파트너 코드의 프로그램적 분기 대상이 되고 구조화 payload 가 확정되면, 전용 공개 클래스로 승격한다 = additive(minor, §5.3). 승격 전까지 판별 경로는 err.code 문자열 비교다.
DEVICE_IN_USE+busyHandlespayload 는 partner-multi-target 트랙에서 0.4.0 에서 적용됨 — §5.1DeviceInUse참조.
5.5 에러 객체 계약 (현재 규범)
OpenEchoError 의 계약 필드는 code · httpStatus · message (+ name = 클래스명) · details?: Record<string, unknown> 0.4.0+(서버 error body 의 code/message 외 필드가 보존된다 — 결손 시 undefined) — 이 필드들이 계약의 전부다.
5.5.1 비규범 예약 — 구조화 payload (0.4.0 에서 §5.5 로 승격됨)
이 절은 details? 도입 전 예고문을 담고 있었다. 0.4.0 에서 §5.5 규범으로 승격되어 이 절은 더 이상 별도 내용을 갖지 않는다.
5.6 보조 export (고급)
mapError(httpStatus, code, message, details?): OpenEchoError 와 ERROR_BIJECTION(코드→클래스 레코드)은 공개 export 다. 매핑 조회 용도로만 안정 계약이며, 레코드를 변형하는 사용은 지원하지 않는다. details?: Record<string, unknown> 은 0.4.0+.
5.7 전달 경로
REST 메서드 = throw · SSE(subscribe) = onError 콜백 · createClient 의 설정 검증 = 일반 Error throw (§1.3 — wire 이전 단계이므로 OpenEchoError 아님).

