headless 클라이언트의 상태는 하나의 읽기 전용 객체 snapshot으로 표현됩니다. 직접 만든 UI는 이 값을 보고 무엇을 그릴지 결정합니다.
phase 모델
snapshot의 중심에는 snapshot.phase가 있습니다. UI는 phase를 보고 어떤 버튼·폼·로딩을 보여줄지 결정합니다. phase를 직접 바꾸는 API는 없고, 메서드를 호출하면 phase가 알아서 전환됩니다.
| phase | 의미 | 이 상태에서 주로 호출하는 메서드 |
|---|---|---|
initializing | 준비 중 (API key 검증) | — |
idle | 대기 상태 | refreshIssues, openIssue, startIssueCapture, startVideoCapture, loginAsMember |
loadingIssues | 이슈 목록 불러오는 중 | — |
viewingIssue | 이슈 상세 보는 중 | closeIssue, refreshIssues, openIssue, startIssueCapture, startVideoCapture |
recordingVideo | 화면 녹화 중 | stopVideoCapture, cancelIssueCapture |
selectingTarget | 사용자가 캡처할 요소를 고르는 중 | cancelIssueCapture |
draftingIssue | 제목·설명·필드를 입력하는 중 | setSessionReplayEnabled, submitDraft, cancelIssueCapture |
submittingIssue | 이슈를 생성하는 중 (캡처가 진행 중이었다면 완료 대기 포함) | — |
selectingTarget phase에서 요소를 클릭하면 즉시 draftingIssue로 전환됩니다. 스크린샷 캡처와 폼 스키마 로드는 백그라운드에서 진행되어 완료되는 대로 snapshot에 반영됩니다.
각 메서드는 허용되지 않은 phase에서 호출하면 에러를 던집니다. 예를 들어 요소를 아직 고르지 않았는데 submitDraft(...)를 호출하면 code: 'INVALID_PHASE'로 거부됩니다.
snapshot 구조
subscribe·getSnapshot으로 받는 HeadlessSnapshot의 필드입니다. 모두 읽기 전용입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
phase | HeadlessPhase | 현재 동작 단계. UI 분기의 기준 |
auth | HeadlessAuthState | 로그인 모드(public/member)와 준비 여부 |
issues | IssueStickerSummary[] | 불러온 이슈 목록. 스티커 렌더링에 사용 |
activeIssue | Issue | null | 상세 보기 중인 이슈 |
draft | HeadlessIssueDraft | null | 캡처 컨텍스트(위치·스크린샷 프리뷰·미디어 모드 등). 제목·설명·필드 값은 앱이 직접 관리 |
videoCapture | HeadlessVideoCaptureState | 녹화 상태(isRecording 등) |
sessionReplay | HeadlessSessionReplayState | 세션 리플레이 사용 가능·첨부 여부 |
formSchema | IssueFormSchema | null | 생성 폼에 그릴 동적 필드 목록 |
filterSchema | IssueFilterSchema | null | 조회 필터에 그릴 필터 그룹 |
error | IssueStickerHeadlessError | null | 마지막으로 발생한 에러 |
주요 타입
폼·필터를 그릴 때 자주 쓰는 타입입니다.
HeadlessConfig — 클라이언트 생성 옵션
interface HeadlessConfig {
apiKey: string; // 필수: 프로젝트 API key
user?: { id: string; name: string }; // 선택: 사용자 식별
capture?: {
networkLogs?: boolean; // 네트워크 로그 수집 여부
consoleLogs?: boolean; // 콘솔 로그 수집 여부
excludeSelectors?: string[]; // 캡처에서 제외할 요소 선택자
};
}
DynamicFieldDescriptor — 동적 필드 하나를 그리기 위한 정보
Jira·Notion·커스텀 필드는 프로젝트 설정에 따라 런타임에 달라지므로, 고정된 타입 대신 이 descriptor로 내려옵니다. snapshot.formSchema.fields에 담기며 inputType에 맞춰 입력 컴포넌트를 그리면 됩니다.
interface DynamicFieldDescriptor {
id: string;
source: 'jira' | 'notion' | 'custom';
label: string;
inputType:
| 'text'
| 'number'
| 'select'
| 'multi_select'
| 'date'
| 'checkbox'
| 'user'
| 'issue'
| 'people'
| 'readonly';
options: readonly { label: string; value: string }[];
search?: (query: string) => Promise<readonly { label: string; value: string }[]>;
resolveValueLabel?: (value: string) => Promise<string | null>;
}
필드 값은 앱 상태에 담아 두었다가 submitDraft의 dynamicFields로 전달합니다. 키는 field.id입니다.
function renderField(
field: DynamicFieldDescriptor,
value: unknown,
onChange: (fieldId: string, value: unknown) => void
) {
switch (field.inputType) {
case 'select':
return renderSelect({
options: field.options,
value,
onChange: (v) => onChange(field.id, v),
});
case 'user':
case 'issue':
case 'people':
return renderAsyncCombobox({
loadOptions: field.search,
value,
onChange: (v) => onChange(field.id, v),
});
case 'checkbox':
return renderCheckbox({ checked: Boolean(value), onChange: (v) => onChange(field.id, v) });
default:
return renderText({ value: String(value ?? ''), onChange: (v) => onChange(field.id, v) });
}
}
동적 필드는 멤버 모드에서만 제공됩니다. public 모드에서는 formSchema.unavailableReason === 'public-mode'로 표시됩니다.
HeadlessIssueFilters — refreshIssues에 넘기는 조회 조건
interface HeadlessIssueFilters {
url?: string; // 특정 URL 또는 'all'
search?: string; // 제목·설명 검색어
externalUserId?: string; // 특정 사용자의 이슈만
customFieldValues?: Record<string, string[]>;
jiraFieldValues?: Record<string, string[]>;
notionFieldValues?: Record<string, string[]>;
}
이 값들을 실제로 어떻게 호출하는지는 클라이언트 메서드에서 다룹니다.