issue-sticker 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1914 @@
1
+ import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadWithFallback, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, s as syncExternalIssueIntegrations, h as devLogger, C as ConsoleCollector, N as NetworkCollector, i as filterExcludeServiceRelatedLogs, j as filterExcludeServiceRelatedConsoleLogs, k as compactNetworkLogsForTransport, l as sanitizeString, S as ScreenRecordingController, m as SessionReplayCaptureController, p as preloadModernScreenshot } from "./NetworkCollector-BlY2qUKF.js";
2
+ import "react/jsx-runtime";
3
+ import "react";
4
+ import "./createLucideIcon-B457EluY.js";
5
+ class IssueStickerHeadlessError extends Error {
6
+ constructor(code, message, cause) {
7
+ super(message);
8
+ this.code = code;
9
+ this.cause = cause;
10
+ this.name = "IssueStickerHeadlessError";
11
+ }
12
+ }
13
+ function readApiBaseUrlFromEnv() {
14
+ if (typeof process !== "undefined" && process.env && Object.prototype.hasOwnProperty.call(process.env, "VITE_API_BASE_URL")) {
15
+ const value = process.env.VITE_API_BASE_URL;
16
+ return typeof value === "string" && value.trim() && value.trim() !== "undefined" ? value.trim() : void 0;
17
+ }
18
+ const viteValue = "https://api.issuesticker.com";
19
+ return typeof viteValue === "string" && viteValue.trim() && viteValue.trim() !== "undefined" ? viteValue.trim() : void 0;
20
+ }
21
+ function getApiBaseUrl() {
22
+ const baseUrl = readApiBaseUrlFromEnv();
23
+ if (!baseUrl) {
24
+ throw new IssueStickerHeadlessError(
25
+ "NETWORK_ERROR",
26
+ "API 서버 주소(VITE_API_BASE_URL)가 설정되지 않았습니다."
27
+ );
28
+ }
29
+ if (!/^https?:\/\//.test(baseUrl)) {
30
+ throw new IssueStickerHeadlessError(
31
+ "NETWORK_ERROR",
32
+ `API 서버 주소(VITE_API_BASE_URL)가 올바르지 않습니다: ${baseUrl}`
33
+ );
34
+ }
35
+ return baseUrl.replace(/\/+$/, "");
36
+ }
37
+ function createRequestId() {
38
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
39
+ return crypto.randomUUID();
40
+ }
41
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
42
+ }
43
+ function extractErrorMessage(error, fallback) {
44
+ if (!error) return fallback || "요청에 실패했습니다.";
45
+ if (typeof error === "string") return error;
46
+ return error.message || error.code || fallback || "요청에 실패했습니다.";
47
+ }
48
+ class HeadlessTransport {
49
+ constructor(options) {
50
+ this.options = options;
51
+ }
52
+ async request(endpoint, options = {}) {
53
+ const response = await this.requestResponse(endpoint, options);
54
+ return response.data;
55
+ }
56
+ async requestResponse(endpoint, options = {}) {
57
+ if (!endpoint.startsWith("/") || endpoint.startsWith("//")) {
58
+ throw new IssueStickerHeadlessError(
59
+ "NETWORK_ERROR",
60
+ `API endpoint는 상대 경로여야 합니다: ${endpoint}`
61
+ );
62
+ }
63
+ const headers = {
64
+ "Content-Type": "application/json",
65
+ "x-client-info": "issue-sticker-sdk"
66
+ };
67
+ if (!options.skipAuth) {
68
+ const authHeader = await this.options.resolveAuthorizationHeader();
69
+ if (authHeader) headers.Authorization = authHeader;
70
+ }
71
+ const execute = (nextHeaders) => fetch(`${getApiBaseUrl()}${endpoint}`, {
72
+ method: options.method ?? "GET",
73
+ headers: nextHeaders,
74
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
75
+ });
76
+ let response = await execute(headers);
77
+ if (!options.skipAuth && response.status === 401 && this.options.getAuthMode() === "member") {
78
+ const retryHeaders = { ...headers };
79
+ const authHeader = await this.options.resolveAuthorizationHeader(true).catch(() => null);
80
+ if (authHeader) {
81
+ retryHeaders.Authorization = authHeader;
82
+ response = await execute(retryHeaders);
83
+ }
84
+ }
85
+ if (response.status === 204) {
86
+ return { success: true };
87
+ }
88
+ const json = await response.json().catch(() => null);
89
+ if (!response.ok || !(json == null ? void 0 : json.success)) {
90
+ throw new IssueStickerHeadlessError(
91
+ response.status === 401 ? "MEMBER_AUTH_REQUIRED" : "NETWORK_ERROR",
92
+ extractErrorMessage(json == null ? void 0 : json.error, (json == null ? void 0 : json.message) || `요청 실패: ${response.status}`),
93
+ json
94
+ );
95
+ }
96
+ return json;
97
+ }
98
+ async uploadScreenshotPreviews(previews) {
99
+ const uploads = await uploadScreenshotDataUrls(
100
+ [...previews],
101
+ (file, mediaType) => this.uploadSignedMedia(file, mediaType)
102
+ );
103
+ return uploads.filter((upload) => Boolean(upload));
104
+ }
105
+ async uploadSignedMedia(file, mediaType) {
106
+ try {
107
+ return await uploadSignedMediaWithApi(
108
+ {
109
+ post: (endpoint, body) => this.request(endpoint, {
110
+ method: "POST",
111
+ body
112
+ }),
113
+ directUpload: directUploadWithFallback
114
+ },
115
+ file,
116
+ mediaType
117
+ );
118
+ } catch (error) {
119
+ if (error instanceof IssueStickerHeadlessError) {
120
+ throw error;
121
+ }
122
+ throw new IssueStickerHeadlessError(
123
+ "CAPTURE_FAILED",
124
+ error instanceof Error ? error.message : "미디어 업로드에 실패했습니다.",
125
+ error
126
+ );
127
+ }
128
+ }
129
+ }
130
+ const BRIDGE_READY = "ISSUE_STICKER_SDK_AUTH_BRIDGE_READY";
131
+ const BRIDGE_REQUEST = "ISSUE_STICKER_SDK_AUTH_REQUEST";
132
+ const BRIDGE_RESPONSE = "ISSUE_STICKER_SDK_AUTH_RESPONSE";
133
+ const BRIDGE_LOGIN_SUCCESS = "ISSUE_STICKER_SDK_AUTH_LOGIN_SUCCESS";
134
+ const BRIDGE_LOGIN_ERROR = "ISSUE_STICKER_SDK_AUTH_LOGIN_ERROR";
135
+ const TOKEN_REFRESH_MARGIN_MS = 6e4;
136
+ const BRIDGE_TIMEOUT_MS = 15e3;
137
+ function buildBridgeUrl(bridgeUrl, params) {
138
+ const url = new URL(bridgeUrl);
139
+ url.searchParams.set("apiKey", params.apiKey);
140
+ if (params.tenantId) url.searchParams.set("tenantId", params.tenantId);
141
+ if (params.projectId) url.searchParams.set("projectId", params.projectId);
142
+ if (typeof window !== "undefined") {
143
+ url.searchParams.set("parentOrigin", window.location.origin);
144
+ url.searchParams.set("hostOrigin", window.location.origin);
145
+ }
146
+ if (params.interactive) {
147
+ url.searchParams.set("interactive", "1");
148
+ }
149
+ return url.toString();
150
+ }
151
+ class HeadlessAuthBridge {
152
+ constructor(options) {
153
+ this.options = options;
154
+ this.bridgeReady = false;
155
+ this.bridgeOrigin = null;
156
+ this.hiddenBridgeFrame = null;
157
+ this.loginBridgeFrame = null;
158
+ this.bridgeWaiters = [];
159
+ this.pendingBridgeRequests = /* @__PURE__ */ new Map();
160
+ this.memberToken = null;
161
+ this.memberTokenPromise = null;
162
+ this.authBridgeUrl = null;
163
+ this.handleBridgeMessage = this.handleBridgeMessage.bind(this);
164
+ }
165
+ get canUseBridge() {
166
+ return Boolean(this.authBridgeUrl);
167
+ }
168
+ setAuthBridgeUrl(authBridgeUrl) {
169
+ if (this.authBridgeUrl === authBridgeUrl) return;
170
+ this.authBridgeUrl = authBridgeUrl;
171
+ this.bridgeOrigin = authBridgeUrl ? new URL(authBridgeUrl).origin : null;
172
+ this.bridgeReady = false;
173
+ }
174
+ clearMemberToken() {
175
+ this.memberToken = null;
176
+ }
177
+ mountHiddenBridgeFrame() {
178
+ if (!this.authBridgeUrl || this.hiddenBridgeFrame || typeof document === "undefined" || typeof window === "undefined") {
179
+ return;
180
+ }
181
+ window.addEventListener("message", this.handleBridgeMessage);
182
+ const frame = document.createElement("iframe");
183
+ frame.title = "IssueSticker auth bridge";
184
+ frame.src = buildBridgeUrl(this.authBridgeUrl, {
185
+ apiKey: this.options.apiKey,
186
+ tenantId: this.options.getTenantId(),
187
+ projectId: this.options.getProjectId()
188
+ });
189
+ frame.style.display = "none";
190
+ frame.setAttribute("aria-hidden", "true");
191
+ frame.dataset.issueStickerHeadlessAuth = "true";
192
+ document.body.appendChild(frame);
193
+ this.hiddenBridgeFrame = frame;
194
+ }
195
+ postLogout() {
196
+ this.postBridgeMessage({
197
+ type: BRIDGE_REQUEST,
198
+ requestId: createRequestId(),
199
+ command: "logout"
200
+ });
201
+ }
202
+ destroy() {
203
+ var _a, _b;
204
+ this.clearMemberToken();
205
+ this.memberTokenPromise = null;
206
+ this.bridgeReady = false;
207
+ this.bridgeWaiters = [];
208
+ (_a = this.hiddenBridgeFrame) == null ? void 0 : _a.remove();
209
+ (_b = this.loginBridgeFrame) == null ? void 0 : _b.remove();
210
+ this.hiddenBridgeFrame = null;
211
+ this.loginBridgeFrame = null;
212
+ if (typeof window !== "undefined") {
213
+ window.removeEventListener("message", this.handleBridgeMessage);
214
+ }
215
+ this.pendingBridgeRequests.forEach((request) => {
216
+ if (typeof window !== "undefined") {
217
+ window.clearTimeout(request.timeoutId);
218
+ }
219
+ request.reject(new Error("Headless auth bridge destroyed"));
220
+ });
221
+ this.pendingBridgeRequests.clear();
222
+ }
223
+ async getValidMemberToken(options = {}) {
224
+ const cached = this.memberToken;
225
+ if (!options.forceRefresh && cached && cached.expiresAtMs - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
226
+ return cached.accessToken;
227
+ }
228
+ if (!this.memberTokenPromise) {
229
+ this.memberTokenPromise = this.requestBridgeMemberToken(Boolean(options.forceRefresh)).then((result) => {
230
+ this.applyToken(result);
231
+ return result.accessToken;
232
+ }).catch(() => null).finally(() => {
233
+ this.memberTokenPromise = null;
234
+ });
235
+ }
236
+ return this.memberTokenPromise;
237
+ }
238
+ async openInteractiveLoginFrame() {
239
+ if (!this.authBridgeUrl) {
240
+ throw new IssueStickerHeadlessError(
241
+ "MEMBER_AUTH_REQUIRED",
242
+ "SDK auth bridge를 사용할 수 없습니다."
243
+ );
244
+ }
245
+ if (typeof document === "undefined" || typeof window === "undefined") {
246
+ throw new IssueStickerHeadlessError(
247
+ "MEMBER_AUTH_REQUIRED",
248
+ "브라우저 DOM에서만 멤버 로그인을 시작할 수 있습니다."
249
+ );
250
+ }
251
+ await new Promise((resolve, reject) => {
252
+ const wrapper = document.createElement("div");
253
+ wrapper.dataset.issueStickerHeadlessAuth = "true";
254
+ wrapper.style.position = "fixed";
255
+ wrapper.style.inset = "0";
256
+ wrapper.style.zIndex = "2147483647";
257
+ wrapper.style.background = "rgba(15, 23, 42, 0.42)";
258
+ wrapper.style.display = "grid";
259
+ wrapper.style.placeItems = "center";
260
+ const frame = document.createElement("iframe");
261
+ frame.title = "IssueSticker member login";
262
+ frame.src = buildBridgeUrl(this.authBridgeUrl, {
263
+ apiKey: this.options.apiKey,
264
+ tenantId: this.options.getTenantId(),
265
+ projectId: this.options.getProjectId(),
266
+ interactive: true
267
+ });
268
+ frame.style.width = "min(420px, calc(100vw - 32px))";
269
+ frame.style.height = "min(640px, calc(100vh - 32px))";
270
+ frame.style.border = "0";
271
+ frame.style.borderRadius = "12px";
272
+ frame.style.boxShadow = "0 24px 80px rgba(15, 23, 42, 0.32)";
273
+ wrapper.appendChild(frame);
274
+ document.body.appendChild(wrapper);
275
+ this.loginBridgeFrame = frame;
276
+ const cleanup = () => {
277
+ window.removeEventListener("message", onMessage);
278
+ wrapper.remove();
279
+ if (this.loginBridgeFrame === frame) this.loginBridgeFrame = null;
280
+ };
281
+ const onMessage = (event) => {
282
+ var _a, _b;
283
+ if (!this.bridgeOrigin || event.origin !== this.bridgeOrigin) return;
284
+ const message = event.data;
285
+ if (message.type === BRIDGE_LOGIN_SUCCESS && message.data) {
286
+ this.applyToken(message.data);
287
+ cleanup();
288
+ resolve();
289
+ }
290
+ if (message.type === BRIDGE_LOGIN_ERROR) {
291
+ cleanup();
292
+ reject(
293
+ new IssueStickerHeadlessError(
294
+ "MEMBER_AUTH_REQUIRED",
295
+ ((_a = message.error) == null ? void 0 : _a.message) || ((_b = message.error) == null ? void 0 : _b.code) || "멤버 로그인에 실패했습니다.",
296
+ message.error
297
+ )
298
+ );
299
+ }
300
+ };
301
+ window.addEventListener("message", onMessage);
302
+ });
303
+ }
304
+ requestBridgeMemberToken(forceRefresh = false) {
305
+ if (!this.authBridgeUrl || !this.bridgeOrigin) {
306
+ throw new IssueStickerHeadlessError(
307
+ "MEMBER_AUTH_REQUIRED",
308
+ "SDK auth bridge URL이 초기화되지 않았습니다."
309
+ );
310
+ }
311
+ if (typeof window === "undefined") {
312
+ throw new IssueStickerHeadlessError(
313
+ "MEMBER_AUTH_REQUIRED",
314
+ "브라우저 DOM에서만 멤버 토큰을 요청할 수 있습니다."
315
+ );
316
+ }
317
+ return this.waitForBridgeReady().then(
318
+ () => new Promise((resolve, reject) => {
319
+ const requestId = createRequestId();
320
+ const timeoutId = window.setTimeout(() => {
321
+ this.pendingBridgeRequests.delete(requestId);
322
+ reject(
323
+ new IssueStickerHeadlessError(
324
+ "MEMBER_AUTH_REQUIRED",
325
+ "SDK auth bridge 응답 시간이 초과되었습니다."
326
+ )
327
+ );
328
+ }, BRIDGE_TIMEOUT_MS);
329
+ this.pendingBridgeRequests.set(requestId, {
330
+ resolve: (result) => {
331
+ if (!(result == null ? void 0 : result.accessToken)) {
332
+ reject(
333
+ new IssueStickerHeadlessError(
334
+ "MEMBER_AUTH_REQUIRED",
335
+ "SDK member token을 발급받지 못했습니다."
336
+ )
337
+ );
338
+ return;
339
+ }
340
+ resolve(result);
341
+ },
342
+ reject,
343
+ timeoutId
344
+ });
345
+ this.postBridgeMessage({
346
+ type: BRIDGE_REQUEST,
347
+ requestId,
348
+ command: "get-member-token",
349
+ apiKey: this.options.apiKey,
350
+ tenantId: this.options.getTenantId(),
351
+ projectId: this.options.getProjectId(),
352
+ hostOrigin: typeof window !== "undefined" ? window.location.origin : void 0,
353
+ forceRefresh
354
+ });
355
+ })
356
+ );
357
+ }
358
+ waitForBridgeReady() {
359
+ if (this.bridgeReady) return Promise.resolve();
360
+ if (typeof window === "undefined") {
361
+ return Promise.reject(
362
+ new IssueStickerHeadlessError(
363
+ "MEMBER_AUTH_REQUIRED",
364
+ "브라우저 DOM에서만 SDK auth bridge를 사용할 수 있습니다."
365
+ )
366
+ );
367
+ }
368
+ return new Promise((resolve, reject) => {
369
+ const timeoutId = window.setTimeout(() => {
370
+ this.bridgeWaiters = this.bridgeWaiters.filter((waiter) => waiter !== done);
371
+ reject(
372
+ new IssueStickerHeadlessError(
373
+ "MEMBER_AUTH_REQUIRED",
374
+ "SDK auth bridge 준비 시간이 초과되었습니다."
375
+ )
376
+ );
377
+ }, BRIDGE_TIMEOUT_MS);
378
+ const done = () => {
379
+ window.clearTimeout(timeoutId);
380
+ resolve();
381
+ };
382
+ this.bridgeWaiters.push(done);
383
+ });
384
+ }
385
+ handleBridgeMessage(event) {
386
+ var _a, _b, _c;
387
+ if (!this.bridgeOrigin || event.origin !== this.bridgeOrigin) return;
388
+ const message = event.data;
389
+ if (message.type === BRIDGE_READY && event.source === ((_a = this.hiddenBridgeFrame) == null ? void 0 : _a.contentWindow)) {
390
+ this.bridgeReady = true;
391
+ const waiters = this.bridgeWaiters;
392
+ this.bridgeWaiters = [];
393
+ waiters.forEach((waiter) => waiter());
394
+ return;
395
+ }
396
+ if (message.type === BRIDGE_RESPONSE && message.requestId) {
397
+ const pending = this.pendingBridgeRequests.get(message.requestId);
398
+ if (!pending) return;
399
+ window.clearTimeout(pending.timeoutId);
400
+ this.pendingBridgeRequests.delete(message.requestId);
401
+ if (message.success && message.data) {
402
+ pending.resolve(message.data);
403
+ } else {
404
+ pending.reject(
405
+ new IssueStickerHeadlessError(
406
+ "MEMBER_AUTH_REQUIRED",
407
+ ((_b = message.error) == null ? void 0 : _b.message) || ((_c = message.error) == null ? void 0 : _c.code) || "멤버 인증이 필요합니다.",
408
+ message.error
409
+ )
410
+ );
411
+ }
412
+ }
413
+ }
414
+ postBridgeMessage(message) {
415
+ var _a;
416
+ if (!this.bridgeOrigin || !((_a = this.hiddenBridgeFrame) == null ? void 0 : _a.contentWindow)) return;
417
+ this.hiddenBridgeFrame.contentWindow.postMessage(message, this.bridgeOrigin);
418
+ }
419
+ applyToken(result) {
420
+ const expiresAtMs = new Date(result.expiresAt).getTime();
421
+ this.memberToken = {
422
+ accessToken: result.accessToken,
423
+ expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : Date.now() + Math.max(result.expiresIn - 60, 60) * 1e3
424
+ };
425
+ this.options.onToken(result);
426
+ }
427
+ }
428
+ const JIRA_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
429
+ "status",
430
+ "priority",
431
+ "assignee",
432
+ "reporter",
433
+ "fixVersions",
434
+ "issuetype",
435
+ "project_key",
436
+ "sprint_id",
437
+ "sprint_name",
438
+ "epic_link",
439
+ "story_points"
440
+ ]);
441
+ const NOTION_FILTERABLE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multi_select", "status", "checkbox"]);
442
+ function getJiraConfig(integration) {
443
+ if (integration.service_type !== "JIRA") return null;
444
+ return integration.config ?? null;
445
+ }
446
+ function getNotionConfig(integration) {
447
+ if (integration.service_type !== "NOTION") return null;
448
+ return integration.config ?? null;
449
+ }
450
+ function getExposedFields(config) {
451
+ if (!(config == null ? void 0 : config.exposed_fields)) return [];
452
+ return normalizeExposedJiraFields(
453
+ Array.isArray(config.exposed_fields) ? config.exposed_fields : void 0
454
+ );
455
+ }
456
+ function getLastKnownJiraFields(config) {
457
+ const fields = config == null ? void 0 : config.last_known_fields;
458
+ if (!Array.isArray(fields)) return [];
459
+ return fields.filter(
460
+ (field) => Boolean(field) && typeof field === "object" && typeof field.id === "string" && typeof field.name === "string"
461
+ );
462
+ }
463
+ function getSelectedNotionFields(config) {
464
+ const fields = config == null ? void 0 : config.selected_fields;
465
+ if (!Array.isArray(fields)) return [];
466
+ return fields.filter((field) => typeof field === "string");
467
+ }
468
+ function getLastKnownNotionFields(config) {
469
+ const fields = config == null ? void 0 : config.last_known_fields;
470
+ if (!Array.isArray(fields)) return [];
471
+ return fields.filter(
472
+ (field) => Boolean(field) && typeof field === "object" && typeof field.id === "string" && typeof field.name === "string" && typeof field.type === "string"
473
+ );
474
+ }
475
+ const OPTION_CUSTOM_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multi_select"]);
476
+ function getSelectableCustomFieldOptions(field) {
477
+ var _a;
478
+ if (!OPTION_CUSTOM_FIELD_TYPES.has(field.field_type)) return [];
479
+ const rawOptions = (_a = field.field_config) == null ? void 0 : _a.options;
480
+ if (!Array.isArray(rawOptions)) return [];
481
+ return rawOptions.filter((option) => typeof option === "string" && option.trim().length > 0).map((option) => ({ label: option, value: option }));
482
+ }
483
+ function getJiraOptionValue(option) {
484
+ return option.id;
485
+ }
486
+ function getJiraFieldFilterOptions(field) {
487
+ const options = field.options ?? [];
488
+ if (!options.length) return [];
489
+ return options.map((option) => ({
490
+ label: option.name || option.value || option.id,
491
+ value: getJiraOptionValue(option)
492
+ })).filter((option) => option.value.length > 0);
493
+ }
494
+ function getNotionFieldFilterOptions(field) {
495
+ if (field.type === "checkbox") {
496
+ return [
497
+ { label: "체크됨", value: "true" },
498
+ { label: "체크 안 됨", value: "false" }
499
+ ];
500
+ }
501
+ const options = field.options ?? [];
502
+ if (!options.length) return [];
503
+ return options.map((option) => ({
504
+ label: option.name,
505
+ value: option.name
506
+ })).filter((option) => option.value.length > 0);
507
+ }
508
+ function mergeOptions(current, next) {
509
+ const byValue = new Map(current.map((option) => [option.value, option]));
510
+ for (const option of next) {
511
+ if (!byValue.has(option.value)) {
512
+ byValue.set(option.value, option);
513
+ }
514
+ }
515
+ return Array.from(byValue.values()).sort((a, b) => a.label.localeCompare(b.label));
516
+ }
517
+ function upsertFilterGroup(groups, group) {
518
+ const existing = groups.get(group.id);
519
+ if (!existing) {
520
+ groups.set(group.id, {
521
+ ...group,
522
+ options: mergeOptions([], group.options)
523
+ });
524
+ return;
525
+ }
526
+ groups.set(group.id, {
527
+ ...existing,
528
+ options: mergeOptions(existing.options, group.options)
529
+ });
530
+ }
531
+ function appendCustomFieldFilterGroups(groups, exposedFieldIds, customFields) {
532
+ for (const field of customFields) {
533
+ if (!exposedFieldIds.has(normalizeJiraFieldId(field.field_key))) continue;
534
+ const options = getSelectableCustomFieldOptions(field);
535
+ if (!options.length) continue;
536
+ upsertFilterGroup(groups, {
537
+ kind: "custom",
538
+ id: `custom:${field.id}`,
539
+ fieldDefinitionId: field.id,
540
+ label: field.field_name,
541
+ options
542
+ });
543
+ }
544
+ }
545
+ function appendJiraFieldFilterGroups(groups, exposedFieldIds, fields) {
546
+ for (const field of fields) {
547
+ const normalizedFieldId = normalizeJiraFieldId(field.id);
548
+ if (!exposedFieldIds.has(normalizedFieldId) || !JIRA_FILTERABLE_FIELD_IDS.has(normalizedFieldId)) {
549
+ continue;
550
+ }
551
+ const options = getJiraFieldFilterOptions(field);
552
+ if (!options.length) continue;
553
+ upsertFilterGroup(groups, {
554
+ kind: "jira",
555
+ id: `jira:${normalizedFieldId}`,
556
+ fieldName: normalizedFieldId,
557
+ label: field.name,
558
+ options
559
+ });
560
+ }
561
+ }
562
+ function appendNotionFieldFilterGroups(groups, selectedFields, fields) {
563
+ for (const field of fields) {
564
+ if (!selectedFields.has(field.id) && !selectedFields.has(field.name)) continue;
565
+ if (!NOTION_FILTERABLE_FIELD_TYPES.has(field.type)) continue;
566
+ const options = getNotionFieldFilterOptions(field);
567
+ if (!options.length) continue;
568
+ upsertFilterGroup(groups, {
569
+ kind: "notion",
570
+ id: `notion:${field.id}`,
571
+ fieldName: field.name,
572
+ label: field.name,
573
+ options
574
+ });
575
+ }
576
+ }
577
+ function composeIssueFilterGroups(integrations, customFields) {
578
+ const groups = /* @__PURE__ */ new Map();
579
+ const jiraIntegration = integrations.find((integration) => {
580
+ const config = getJiraConfig(integration);
581
+ return integration.is_active !== false && config;
582
+ });
583
+ const jiraConfig = jiraIntegration ? getJiraConfig(jiraIntegration) : null;
584
+ const configExposedFields = getExposedFields(jiraConfig);
585
+ if (configExposedFields.length) {
586
+ const exposedFieldIds = new Set(configExposedFields);
587
+ appendCustomFieldFilterGroups(groups, exposedFieldIds, customFields);
588
+ appendJiraFieldFilterGroups(groups, exposedFieldIds, getLastKnownJiraFields(jiraConfig));
589
+ }
590
+ for (const integration of integrations) {
591
+ if (integration.is_active === false) continue;
592
+ const notionConfig = getNotionConfig(integration);
593
+ if (!notionConfig) continue;
594
+ appendNotionFieldFilterGroups(
595
+ groups,
596
+ new Set(getSelectedNotionFields(notionConfig)),
597
+ getLastKnownNotionFields(notionConfig)
598
+ );
599
+ }
600
+ return Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label));
601
+ }
602
+ function buildHeadlessCaptureContext({
603
+ target,
604
+ event,
605
+ videoFile,
606
+ createVideoPreviewUrl,
607
+ sessionReplayEnabled
608
+ }) {
609
+ const mediaMode = videoFile ? "video" : "screenshot";
610
+ const position = getElementPosition(target, event);
611
+ return {
612
+ mediaMode,
613
+ position,
614
+ screenshotPreviewUrls: [],
615
+ videoPreviewUrl: mediaMode === "video" && videoFile ? createVideoPreviewUrl(videoFile) : null,
616
+ targetSelector: position.selector,
617
+ sessionReplayEnabled,
618
+ sessionReplayPreview: null
619
+ };
620
+ }
621
+ async function captureHeadlessScreenshots({
622
+ target,
623
+ excludeSelectors
624
+ }) {
625
+ const screenshots = await Promise.all([
626
+ captureWithModernScreenshot({
627
+ element: document.documentElement,
628
+ excludeSelectors,
629
+ width: window.innerWidth,
630
+ height: window.innerHeight
631
+ }).catch(() => void 0),
632
+ captureWithModernScreenshot({
633
+ element: target,
634
+ excludeSelectors
635
+ }).catch(() => void 0)
636
+ ]);
637
+ return screenshots.filter(
638
+ (dataUrl) => typeof dataUrl === "string" && dataUrl.length > 0
639
+ );
640
+ }
641
+ function normalizeCustomInputType(fieldType) {
642
+ if (fieldType === "number") return "number";
643
+ if (fieldType === "select") return "select";
644
+ if (fieldType === "multi_select") return "multi_select";
645
+ if (fieldType === "date") return "date";
646
+ if (fieldType === "checkbox" || fieldType === "boolean") return "checkbox";
647
+ return "text";
648
+ }
649
+ function buildCustomFieldOptions(field) {
650
+ var _a;
651
+ const rawOptions = (_a = field.field_config) == null ? void 0 : _a.options;
652
+ if (!Array.isArray(rawOptions)) return [];
653
+ return rawOptions.map((option) => {
654
+ if (typeof option === "string") {
655
+ const value = option.trim();
656
+ return value ? { label: value, value } : null;
657
+ }
658
+ if (option && typeof option === "object") {
659
+ const candidate = option;
660
+ const converted = toOption(candidate);
661
+ return converted.value ? converted : null;
662
+ }
663
+ return null;
664
+ }).filter((option) => Boolean(option));
665
+ }
666
+ function buildCustomFieldDescriptors(fields) {
667
+ return fields.map((field) => ({
668
+ id: field.id,
669
+ source: "custom",
670
+ label: field.field_name || field.field_key || field.id,
671
+ inputType: normalizeCustomInputType(field.field_type),
672
+ options: buildCustomFieldOptions(field)
673
+ }));
674
+ }
675
+ function normalizeJiraInputType(inputType) {
676
+ if (inputType === "user" || inputType === "issue") return inputType;
677
+ if (inputType === "number") return "number";
678
+ if (inputType === "select") return "select";
679
+ if (inputType === "readonly") return "readonly";
680
+ return "text";
681
+ }
682
+ function normalizeNotionInputType(inputType) {
683
+ if (inputType === "number" || inputType === "select" || inputType === "multi_select" || inputType === "date" || inputType === "checkbox" || inputType === "people" || inputType === "readonly") {
684
+ return inputType;
685
+ }
686
+ return "text";
687
+ }
688
+ function toOption(option) {
689
+ const value = option.value || option.id || option.name || option.label || "";
690
+ return {
691
+ value,
692
+ label: option.label || option.name || option.value || option.id || value,
693
+ description: option.description
694
+ };
695
+ }
696
+ function getFilledDynamicValues(draft, fields, source) {
697
+ return Object.fromEntries(
698
+ fields.filter((field) => field.source === source).map((field) => [field.id, draft.dynamicFields[field.id]]).filter(([, value]) => value !== void 0 && value !== null && value !== "")
699
+ );
700
+ }
701
+ function getJiraUserLabel(user) {
702
+ return user.displayName || user.emailAddress || user.accountId;
703
+ }
704
+ function getJiraIssueLabel(issue) {
705
+ return issue.summary ? `${issue.key} ${issue.summary}` : issue.key;
706
+ }
707
+ async function buildIssueFormSchema(values, deps) {
708
+ var _a, _b;
709
+ if (deps.authMode !== "member") {
710
+ return { fields: [], unavailableReason: "public-mode" };
711
+ }
712
+ const [active, customFields] = await Promise.all([
713
+ deps.getActiveIntegration(),
714
+ deps.getCustomFieldDefinitions()
715
+ ]);
716
+ const dynamicFields = [];
717
+ if ((active == null ? void 0 : active.service_type) === "JIRA") {
718
+ const response = await deps.requestResponse(
719
+ `/api/jira/integration-fields/${encodeURIComponent(active.id)}`
720
+ );
721
+ const exposedFields = Array.isArray((_a = response.data) == null ? void 0 : _a.exposed_fields) ? response.data.exposed_fields : active.config.exposed_fields;
722
+ const fields = buildJiraCreateFields(exposedFields, ((_b = response.data) == null ? void 0 : _b.fields) ?? [], values);
723
+ dynamicFields.push(
724
+ ...fields.map((field) => ({
725
+ id: field.id,
726
+ source: "jira",
727
+ label: field.label,
728
+ inputType: normalizeJiraInputType(field.inputType),
729
+ options: field.options.map(toOption),
730
+ ...field.inputType === "user" ? {
731
+ search: (query) => deps.searchJiraProjectUsers(query).then((items) => items.map(toOption)),
732
+ resolveValueLabel: (value) => deps.resolveJiraProjectUserLabel(value)
733
+ } : {},
734
+ ...field.inputType === "issue" ? {
735
+ search: (query) => deps.searchJiraProjectIssues(query).then((items) => items.map(toOption)),
736
+ resolveValueLabel: (value) => deps.resolveJiraProjectIssueLabel(value)
737
+ } : {}
738
+ }))
739
+ );
740
+ } else if ((active == null ? void 0 : active.service_type) === "NOTION") {
741
+ const selectedFields = Array.isArray(active.config.selected_fields) ? active.config.selected_fields : [];
742
+ const fieldDefinitions = Array.isArray(active.config.last_known_fields) ? active.config.last_known_fields : [];
743
+ const fields = buildNotionCreateFields(selectedFields, fieldDefinitions, values);
744
+ dynamicFields.push(
745
+ ...fields.map((field) => ({
746
+ id: field.id,
747
+ source: "notion",
748
+ label: field.name,
749
+ inputType: normalizeNotionInputType(field.inputType),
750
+ options: field.options.map(toOption),
751
+ ...field.inputType === "people" ? {
752
+ search: (query) => deps.searchNotionPeople(query).then(
753
+ (items) => items.map((item) => ({
754
+ label: item.name,
755
+ value: item.id,
756
+ description: item.email
757
+ }))
758
+ )
759
+ } : {}
760
+ }))
761
+ );
762
+ }
763
+ dynamicFields.push(...buildCustomFieldDescriptors(customFields));
764
+ if (dynamicFields.length === 0) {
765
+ return { fields: [], unavailableReason: "not-configured" };
766
+ }
767
+ return { fields: dynamicFields };
768
+ }
769
+ function recordToFilterArray(record, keyFieldName) {
770
+ if (!record) return null;
771
+ const entries = Object.entries(record).filter(([, values]) => values.length > 0);
772
+ if (entries.length === 0) return null;
773
+ return entries.map(
774
+ ([key, values]) => ({ [keyFieldName]: key, values })
775
+ );
776
+ }
777
+ function hasIssueListFilters(filters) {
778
+ var _a;
779
+ return Boolean(
780
+ ((_a = filters.search) == null ? void 0 : _a.trim()) || filters.dateFrom || filters.dateTo || filters.page || hasRecordFilters(filters.customFieldValues) || hasRecordFilters(filters.jiraFieldValues) || hasRecordFilters(filters.notionFieldValues)
781
+ );
782
+ }
783
+ function hasRecordFilters(record) {
784
+ return Boolean(record && Object.values(record).some((values) => values.length > 0));
785
+ }
786
+ function buildIssueQueryParams(filters, context) {
787
+ var _a;
788
+ const params = new URLSearchParams();
789
+ params.set("project_id", context.projectId);
790
+ if (filters.url && filters.url !== "all") {
791
+ params.set("url", Array.isArray(filters.url) ? filters.url.join(",") : filters.url);
792
+ } else if (!filters.url && context.currentUrl) {
793
+ params.set("url", context.currentUrl);
794
+ }
795
+ if ((_a = filters.search) == null ? void 0 : _a.trim()) params.set("search", filters.search.trim());
796
+ if (filters.dateFrom) params.set("date_from", filters.dateFrom);
797
+ if (filters.dateTo) params.set("date_to", filters.dateTo);
798
+ if (filters.page) params.set("page", String(filters.page));
799
+ if (filters.pageSize) params.set("page_size", String(filters.pageSize));
800
+ if (filters.sortOrder) params.set("sort_order", filters.sortOrder);
801
+ const externalUserId = context.authMode === "public" ? filters.externalUserId || context.defaultExternalUserId : filters.externalUserId;
802
+ if (externalUserId) params.set("external_user_id", externalUserId);
803
+ const custom = recordToFilterArray(filters.customFieldValues, "fieldDefinitionId");
804
+ const jira = recordToFilterArray(filters.jiraFieldValues, "fieldName");
805
+ const notion = recordToFilterArray(filters.notionFieldValues, "fieldName");
806
+ if (custom) params.set("custom_field_filters", JSON.stringify(custom));
807
+ if (jira) params.set("jira_field_filters", JSON.stringify(jira));
808
+ if (notion) params.set("notion_field_filters", JSON.stringify(notion));
809
+ return params;
810
+ }
811
+ const STICKER_PAGE_SIZE = 500;
812
+ const STICKER_FETCH_LIMIT = 2e3;
813
+ class HeadlessIssueRepository {
814
+ constructor(options) {
815
+ this.options = options;
816
+ }
817
+ request(endpoint, options = {}) {
818
+ return this.options.transport.request(endpoint, options);
819
+ }
820
+ requestResponse(endpoint, options = {}) {
821
+ return this.options.transport.requestResponse(endpoint, options);
822
+ }
823
+ openIssue(issueId) {
824
+ return this.request(
825
+ `/api/sdk/issues/${encodeURIComponent(issueId)}`
826
+ );
827
+ }
828
+ createIssue(issueData) {
829
+ return this.request("/api/sdk/issues", {
830
+ method: "POST",
831
+ body: issueData
832
+ });
833
+ }
834
+ async loadFilterSchema() {
835
+ if (this.options.getAuthMode() !== "member") {
836
+ return { groups: [], unavailableReason: "public-mode" };
837
+ }
838
+ const [integrations, customFields] = await Promise.all([
839
+ this.getProjectIntegrations(),
840
+ this.getCustomFieldDefinitions()
841
+ ]);
842
+ const groups = composeIssueFilterGroups(integrations, customFields);
843
+ return { groups };
844
+ }
845
+ async getActiveIntegration() {
846
+ const integrations = await this.getProjectIntegrations();
847
+ return integrations.find((integration) => integration.service_type === "JIRA") ?? integrations.find((integration) => integration.service_type === "NOTION") ?? null;
848
+ }
849
+ async getProjectIntegrations() {
850
+ const integrations = await this.request(
851
+ `/api/integrations/${this.options.getProjectId()}`
852
+ );
853
+ return Array.isArray(integrations) ? integrations.filter((integration) => integration.is_active !== false) : [];
854
+ }
855
+ async getCustomFieldDefinitions() {
856
+ const fields = await this.request(
857
+ `/api/integrations/fields/definitions?projectId=${encodeURIComponent(this.options.getProjectId())}`
858
+ );
859
+ return Array.isArray(fields) ? fields : [];
860
+ }
861
+ /**
862
+ * 이슈 저장 이후의 외부 연동(Jira/Notion/커스텀 필드)을 이어서 실행한다.
863
+ *
864
+ * 실패는 이슈 저장을 되돌리지 않으므로 throw 하지 않고 결과로 돌려준다. headless 는
865
+ * UI 가 없어 toast 를 띄울 수 없으니, 통합하는 쪽이 이 값을 보고 판단해야 한다.
866
+ */
867
+ async syncDynamicFields(createdIssue, draft, schema, context) {
868
+ if (this.options.getAuthMode() !== "member") {
869
+ return { failedServices: [] };
870
+ }
871
+ const active = await this.getActiveIntegration().catch(() => null);
872
+ const fields = (schema == null ? void 0 : schema.fields) ?? [];
873
+ const jiraValues = getFilledDynamicValues(draft, fields, "jira");
874
+ const notionValues = getFilledDynamicValues(draft, fields, "notion");
875
+ const customValues = getFilledDynamicValues(draft, fields, "custom");
876
+ return syncExternalIssueIntegrations({
877
+ issueId: createdIssue.id,
878
+ title: draft.title.trim(),
879
+ description: draft.description.trim(),
880
+ url: this.options.getCurrentUrl() ?? "",
881
+ screenshotUrl: context.screenshotUrl,
882
+ videoUrl: context.videoUrl,
883
+ jira: {
884
+ enabled: (active == null ? void 0 : active.service_type) === "JIRA",
885
+ fields: jiraValues,
886
+ createIssue: (input) => this.request("/api/jira/issues", {
887
+ method: "POST",
888
+ body: {
889
+ projectId: this.options.getProjectId(),
890
+ issueId: input.issueId,
891
+ issueData: {
892
+ title: input.title,
893
+ description: input.description,
894
+ url: input.url,
895
+ screenshotUrl: input.screenshotUrl,
896
+ videoUrl: input.videoUrl,
897
+ fields: input.fields
898
+ }
899
+ }
900
+ })
901
+ },
902
+ notion: {
903
+ enabled: (active == null ? void 0 : active.service_type) === "NOTION",
904
+ properties: notionValues,
905
+ createPage: (input) => this.request("/api/notion/pages", {
906
+ method: "POST",
907
+ body: {
908
+ projectId: this.options.getProjectId(),
909
+ issueId: input.issueId,
910
+ properties: input.properties
911
+ }
912
+ })
913
+ },
914
+ customFieldValues: customValues,
915
+ upsertCustomFieldValue: (fieldDefinitionId, value) => this.request(
916
+ `/api/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
917
+ {
918
+ method: "PUT",
919
+ body: {
920
+ field_definition_id: fieldDefinitionId,
921
+ value
922
+ }
923
+ }
924
+ ),
925
+ onError: (service, error) => {
926
+ devLogger.error("headless", `${service} 연동 실패 (이슈 저장은 성공):`, error);
927
+ }
928
+ });
929
+ }
930
+ async fetchAllIssueStickers(filters) {
931
+ var _a, _b, _c;
932
+ const projectId = this.options.getProjectId();
933
+ const externalUserId = this.options.getAuthMode() === "public" ? filters.externalUserId || this.options.getDefaultExternalUserId() : void 0;
934
+ if (this.options.getAuthMode() === "public" && !externalUserId) {
935
+ return [];
936
+ }
937
+ if (hasIssueListFilters(filters)) {
938
+ const params = buildIssueQueryParams(filters, {
939
+ projectId,
940
+ authMode: this.options.getAuthMode(),
941
+ defaultExternalUserId: this.options.getDefaultExternalUserId(),
942
+ currentUrl: this.options.getCurrentUrl()
943
+ });
944
+ const response = await this.requestResponse(
945
+ `/api/sdk/issues?${params.toString()}`
946
+ );
947
+ return ((_a = response.data) == null ? void 0 : _a.items) ?? [];
948
+ }
949
+ const issues = [];
950
+ let page = 1;
951
+ let hasMore = true;
952
+ while (hasMore && issues.length <= STICKER_FETCH_LIMIT) {
953
+ const params = new URLSearchParams();
954
+ params.set("project_id", projectId);
955
+ params.set("page", String(page));
956
+ params.set("page_size", String(filters.pageSize ?? STICKER_PAGE_SIZE));
957
+ if (filters.url && filters.url !== "all") {
958
+ params.set("url", Array.isArray(filters.url) ? filters.url.join(",") : filters.url);
959
+ } else if (!filters.url && this.options.getCurrentUrl()) {
960
+ params.set("url", this.options.getCurrentUrl());
961
+ }
962
+ if (externalUserId) params.set("external_user_id", externalUserId);
963
+ if (filters.sortOrder) params.set("sort_order", filters.sortOrder);
964
+ const response = await this.requestResponse(`/api/sdk/issues/stickers?${params.toString()}`);
965
+ const pageItems = ((_b = response.data) == null ? void 0 : _b.items) ?? [];
966
+ issues.push(...pageItems);
967
+ hasMore = Boolean((_c = response.data) == null ? void 0 : _c.hasMore) && pageItems.length > 0;
968
+ page += 1;
969
+ }
970
+ return issues;
971
+ }
972
+ searchJiraProjectUsers(query) {
973
+ return this.request(
974
+ `/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/users/search?query=${encodeURIComponent(query)}`
975
+ ).then(
976
+ (users) => users.map((user) => ({
977
+ value: user.accountId,
978
+ label: getJiraUserLabel(user),
979
+ description: user.emailAddress
980
+ }))
981
+ );
982
+ }
983
+ resolveJiraProjectUserLabel(accountId) {
984
+ return this.request(
985
+ `/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/users/${encodeURIComponent(accountId)}`
986
+ ).then(getJiraUserLabel).catch(() => null);
987
+ }
988
+ searchJiraProjectIssues(query) {
989
+ return this.request(
990
+ `/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/issues?query=${encodeURIComponent(query)}`
991
+ ).then(
992
+ (issues) => issues.map((issue) => ({
993
+ value: issue.key,
994
+ label: getJiraIssueLabel(issue),
995
+ description: issue.status
996
+ }))
997
+ );
998
+ }
999
+ async resolveJiraProjectIssueLabel(issueKey) {
1000
+ var _a, _b;
1001
+ const issues = await this.searchJiraProjectIssues(issueKey).catch(() => []);
1002
+ return ((_a = issues.find((issue) => issue.value === issueKey)) == null ? void 0 : _a.label) ?? ((_b = issues[0]) == null ? void 0 : _b.label) ?? null;
1003
+ }
1004
+ searchNotionPeople(query) {
1005
+ return this.request(
1006
+ `/api/notion/users?projectId=${encodeURIComponent(this.options.getProjectId())}&query=${encodeURIComponent(query)}`
1007
+ );
1008
+ }
1009
+ }
1010
+ const defaultDeps = {
1011
+ createNetworkCollector: (apiBaseUrl) => new NetworkCollector(apiBaseUrl),
1012
+ createConsoleCollector: () => new ConsoleCollector()
1013
+ };
1014
+ class HeadlessLogCollectors {
1015
+ constructor(config, getApiBaseUrl2, deps = defaultDeps) {
1016
+ this.config = config;
1017
+ this.getApiBaseUrl = getApiBaseUrl2;
1018
+ this.deps = deps;
1019
+ this.networkCollector = null;
1020
+ this.consoleCollector = null;
1021
+ }
1022
+ start() {
1023
+ var _a, _b;
1024
+ if (((_a = this.config.capture) == null ? void 0 : _a.networkLogs) !== false && !this.networkCollector) {
1025
+ this.networkCollector = this.deps.createNetworkCollector(this.getApiBaseUrl());
1026
+ this.networkCollector.start();
1027
+ }
1028
+ if (((_b = this.config.capture) == null ? void 0 : _b.consoleLogs) !== false && !this.consoleCollector) {
1029
+ this.consoleCollector = this.deps.createConsoleCollector();
1030
+ this.consoleCollector.start();
1031
+ }
1032
+ }
1033
+ stop() {
1034
+ var _a, _b;
1035
+ (_a = this.networkCollector) == null ? void 0 : _a.stop();
1036
+ (_b = this.consoleCollector) == null ? void 0 : _b.stop();
1037
+ this.networkCollector = null;
1038
+ this.consoleCollector = null;
1039
+ }
1040
+ collect() {
1041
+ var _a, _b;
1042
+ const networkLogs = filterExcludeServiceRelatedLogs(
1043
+ ((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
1044
+ ).slice(-50);
1045
+ const consoleLogs = filterExcludeServiceRelatedConsoleLogs(
1046
+ ((_b = this.consoleCollector) == null ? void 0 : _b.getFilteredLogs()) ?? []
1047
+ ).slice(-50);
1048
+ return {
1049
+ networkLogs: compactNetworkLogsForTransport(networkLogs) ?? [],
1050
+ consoleLogs
1051
+ };
1052
+ }
1053
+ }
1054
+ class HeadlessPendingMedia {
1055
+ constructor() {
1056
+ this.file = null;
1057
+ this.objectUrl = null;
1058
+ }
1059
+ get videoFile() {
1060
+ return this.file;
1061
+ }
1062
+ get previewUrl() {
1063
+ return this.objectUrl;
1064
+ }
1065
+ get hasVideo() {
1066
+ return Boolean(this.file);
1067
+ }
1068
+ setVideoFile(file) {
1069
+ this.file = file;
1070
+ }
1071
+ createVideoPreviewUrl(file) {
1072
+ if (this.objectUrl) {
1073
+ URL.revokeObjectURL(this.objectUrl);
1074
+ }
1075
+ this.file = file;
1076
+ this.objectUrl = URL.createObjectURL(file);
1077
+ return this.objectUrl;
1078
+ }
1079
+ clear() {
1080
+ if (this.objectUrl) {
1081
+ URL.revokeObjectURL(this.objectUrl);
1082
+ this.objectUrl = null;
1083
+ }
1084
+ this.file = null;
1085
+ }
1086
+ }
1087
+ function createReplayFile(snapshot) {
1088
+ return new File([snapshot.payload], "session-replay.gz", {
1089
+ type: "application/gzip"
1090
+ });
1091
+ }
1092
+ function createSessionReplayMeta(snapshot) {
1093
+ return {
1094
+ window_seconds: snapshot.windowSeconds,
1095
+ event_count: snapshot.eventCount,
1096
+ size_bytes: snapshot.sizeBytes,
1097
+ rrweb_version: snapshot.rrwebVersion,
1098
+ privacy_config_version: snapshot.privacyConfigVersion
1099
+ };
1100
+ }
1101
+ function createInitialHeadlessSnapshot() {
1102
+ return {
1103
+ phase: "idle",
1104
+ auth: {
1105
+ mode: "public",
1106
+ isReady: false,
1107
+ canUseIntegrations: false,
1108
+ tenantId: null,
1109
+ projectId: null,
1110
+ accountId: null
1111
+ },
1112
+ issues: [],
1113
+ activeIssue: null,
1114
+ draft: null,
1115
+ videoCapture: {
1116
+ isRecording: false,
1117
+ hasRecordedVideo: false,
1118
+ error: null
1119
+ },
1120
+ sessionReplay: {
1121
+ available: false,
1122
+ canAttach: false,
1123
+ enabled: true,
1124
+ error: null
1125
+ },
1126
+ formSchema: null,
1127
+ filterSchema: null,
1128
+ error: null
1129
+ };
1130
+ }
1131
+ function assertScreenshotCaptureOptions(options) {
1132
+ const legacyOptions = options;
1133
+ if (!legacyOptions) return;
1134
+ if (legacyOptions.mediaMode && legacyOptions.mediaMode !== "screenshot") {
1135
+ throw new IssueStickerHeadlessError(
1136
+ "VALIDATION_ERROR",
1137
+ "영상 캡쳐는 startVideoCapture()와 stopVideoCapture()를 사용해야 합니다."
1138
+ );
1139
+ }
1140
+ if ("videoFile" in legacyOptions) {
1141
+ throw new IssueStickerHeadlessError(
1142
+ "VALIDATION_ERROR",
1143
+ "headless API는 videoFile을 직접 받지 않습니다. startVideoCapture()로 녹화하세요."
1144
+ );
1145
+ }
1146
+ if ("sessionReplay" in legacyOptions) {
1147
+ throw new IssueStickerHeadlessError(
1148
+ "VALIDATION_ERROR",
1149
+ "세션 리플레이 첨부 여부는 setSessionReplayEnabled()로 설정해야 합니다."
1150
+ );
1151
+ }
1152
+ }
1153
+ class HeadlessClientState {
1154
+ constructor() {
1155
+ this.snapshot = createInitialHeadlessSnapshot();
1156
+ this.listeners = /* @__PURE__ */ new Set();
1157
+ }
1158
+ get phase() {
1159
+ return this.snapshot.phase;
1160
+ }
1161
+ getSnapshot() {
1162
+ return this.snapshot;
1163
+ }
1164
+ subscribe(listener) {
1165
+ this.listeners.add(listener);
1166
+ listener(this.snapshot);
1167
+ return () => this.listeners.delete(listener);
1168
+ }
1169
+ setSnapshot(patch) {
1170
+ this.snapshot = {
1171
+ ...this.snapshot,
1172
+ ...patch,
1173
+ auth: patch.auth ? { ...this.snapshot.auth, ...patch.auth } : this.snapshot.auth
1174
+ };
1175
+ this.listeners.forEach((listener) => listener(this.snapshot));
1176
+ }
1177
+ reset() {
1178
+ this.listeners.clear();
1179
+ this.snapshot = createInitialHeadlessSnapshot();
1180
+ }
1181
+ applyBridgeToken(result) {
1182
+ var _a, _b, _c;
1183
+ this.setSnapshot({
1184
+ auth: {
1185
+ mode: "member",
1186
+ isReady: true,
1187
+ canUseIntegrations: true,
1188
+ tenantId: ((_a = result.context) == null ? void 0 : _a.tenantId) || this.snapshot.auth.tenantId,
1189
+ projectId: ((_b = result.context) == null ? void 0 : _b.projectId) || this.snapshot.auth.projectId,
1190
+ accountId: ((_c = result.context) == null ? void 0 : _c.accountId) || this.snapshot.auth.accountId
1191
+ }
1192
+ });
1193
+ }
1194
+ setAuthMode(mode) {
1195
+ this.setSnapshot({
1196
+ auth: {
1197
+ ...this.snapshot.auth,
1198
+ mode,
1199
+ canUseIntegrations: mode === "member",
1200
+ accountId: mode === "member" ? this.snapshot.auth.accountId : null
1201
+ }
1202
+ });
1203
+ }
1204
+ requireProjectId() {
1205
+ const projectId = this.snapshot.auth.projectId;
1206
+ if (!projectId) {
1207
+ throw new IssueStickerHeadlessError(
1208
+ "NOT_INITIALIZED",
1209
+ "프로젝트 컨텍스트가 준비되지 않았습니다."
1210
+ );
1211
+ }
1212
+ return projectId;
1213
+ }
1214
+ requireDraft() {
1215
+ if (!this.snapshot.draft) {
1216
+ throw new IssueStickerHeadlessError("INVALID_PHASE", "작성 중인 이슈가 없습니다.");
1217
+ }
1218
+ return this.snapshot.draft;
1219
+ }
1220
+ assertPhase(allowed, action) {
1221
+ if (!allowed.includes(this.snapshot.phase)) {
1222
+ throw new IssueStickerHeadlessError(
1223
+ "INVALID_PHASE",
1224
+ `${action}은 현재 phase(${this.snapshot.phase})에서 호출할 수 없습니다.`
1225
+ );
1226
+ }
1227
+ }
1228
+ }
1229
+ function buildIssueCreatePayload({
1230
+ draft,
1231
+ auth,
1232
+ config,
1233
+ projectId,
1234
+ uploads,
1235
+ logs,
1236
+ currentUrl,
1237
+ userAgent
1238
+ }) {
1239
+ var _a, _b;
1240
+ return sanitizeString({
1241
+ url: currentUrl,
1242
+ title: draft.title.trim(),
1243
+ description: draft.description.trim(),
1244
+ position: draft.position,
1245
+ ...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
1246
+ ...uploads.videoUpload ? { video_upload_id: uploads.videoUpload.id } : {},
1247
+ ...uploads.replayUpload ? {
1248
+ session_replay_upload_id: uploads.replayUpload.uploadId,
1249
+ session_replay_meta: uploads.replayUpload.meta
1250
+ } : {},
1251
+ network_logs: logs.networkLogs.length > 0 ? logs.networkLogs : void 0,
1252
+ console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
1253
+ user_agent: userAgent,
1254
+ tenant_id: auth.tenantId,
1255
+ project_id: projectId,
1256
+ account_id: auth.accountId ?? void 0,
1257
+ external_user_id: ((_a = config.user) == null ? void 0 : _a.id) ?? null,
1258
+ external_user_name: ((_b = config.user) == null ? void 0 : _b.name) ?? null
1259
+ });
1260
+ }
1261
+ async function submitHeadlessIssueDraft({
1262
+ draft,
1263
+ pendingVideoFile,
1264
+ transport,
1265
+ issueRepository,
1266
+ formSchema,
1267
+ auth,
1268
+ config,
1269
+ projectId,
1270
+ logs,
1271
+ currentUrl,
1272
+ userAgent,
1273
+ uploadSessionReplaySnapshot
1274
+ }) {
1275
+ var _a;
1276
+ const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
1277
+ const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
1278
+ const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
1279
+ const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
1280
+ const issueData = buildIssueCreatePayload({
1281
+ draft,
1282
+ auth,
1283
+ config,
1284
+ projectId,
1285
+ uploads: { screenshotUploadIds, videoUpload, replayUpload },
1286
+ logs,
1287
+ currentUrl,
1288
+ userAgent
1289
+ });
1290
+ const createdIssue = await issueRepository.createIssue(issueData);
1291
+ await issueRepository.syncDynamicFields(createdIssue, draft, formSchema, {
1292
+ screenshotUrl: (_a = screenshotUploads[0]) == null ? void 0 : _a.url,
1293
+ videoUrl: videoUpload == null ? void 0 : videoUpload.url
1294
+ });
1295
+ return createdIssue;
1296
+ }
1297
+ const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
1298
+ const HEADLESS_CAPTURE_STYLE = `
1299
+ body.issue-sticker-mode-active,
1300
+ body.issue-sticker-mode-active * {
1301
+ cursor: crosshair !important;
1302
+ }
1303
+
1304
+ body.issue-sticker-mode-active [data-issue-sticker-capture-ignore="true"],
1305
+ body.issue-sticker-mode-active [data-issue-sticker-capture-ignore="true"] *,
1306
+ body.issue-sticker-mode-active #issue-sticker-host,
1307
+ body.issue-sticker-mode-active #issue-sticker-host * {
1308
+ cursor: default !important;
1309
+ }
1310
+
1311
+ body.issue-sticker-mode-active .issue-sticker-mode-highlight {
1312
+ outline: 3px solid #00856f !important;
1313
+ outline-offset: 3px !important;
1314
+ box-shadow:
1315
+ 0 0 0 7px rgba(0, 133, 111, 0.18),
1316
+ 0 10px 28px rgba(0, 0, 0, 0.26) !important;
1317
+ transition:
1318
+ outline-color 120ms ease,
1319
+ box-shadow 120ms ease !important;
1320
+ }
1321
+ `;
1322
+ function isHTMLElement(value) {
1323
+ return typeof HTMLElement !== "undefined" && value instanceof HTMLElement;
1324
+ }
1325
+ function resolveHeadlessCaptureTarget(target) {
1326
+ if (!isHTMLElement(target)) return null;
1327
+ return target.closest('[data-issue-sticker-capture-target="true"]') ?? target;
1328
+ }
1329
+ function ensureHeadlessCaptureStyles() {
1330
+ if (typeof document === "undefined") return;
1331
+ if (document.getElementById(HEADLESS_CAPTURE_STYLE_ID)) return;
1332
+ const style = document.createElement("style");
1333
+ style.id = HEADLESS_CAPTURE_STYLE_ID;
1334
+ style.textContent = HEADLESS_CAPTURE_STYLE;
1335
+ document.head.appendChild(style);
1336
+ }
1337
+ function startHeadlessTargetSelection({
1338
+ isIgnoredTarget,
1339
+ onCapture
1340
+ }) {
1341
+ ensureHeadlessCaptureStyles();
1342
+ document.body.classList.add("issue-sticker-mode-active");
1343
+ let highlightedElement = null;
1344
+ const clearHighlight = () => {
1345
+ highlightedElement == null ? void 0 : highlightedElement.classList.remove("issue-sticker-mode-highlight");
1346
+ highlightedElement = null;
1347
+ };
1348
+ const handleMouseOver = (event) => {
1349
+ const target = resolveHeadlessCaptureTarget(event.target);
1350
+ if (!target || isIgnoredTarget(target)) return;
1351
+ clearHighlight();
1352
+ target.classList.add("issue-sticker-mode-highlight");
1353
+ highlightedElement = target;
1354
+ };
1355
+ const handleMouseOut = (event) => {
1356
+ if (event.target === highlightedElement) {
1357
+ clearHighlight();
1358
+ }
1359
+ };
1360
+ const handleClick = (event) => {
1361
+ const clickTarget = resolveHeadlessCaptureTarget(event.target);
1362
+ if (!clickTarget || isIgnoredTarget(clickTarget)) return;
1363
+ event.preventDefault();
1364
+ event.stopImmediatePropagation();
1365
+ onCapture(highlightedElement ?? clickTarget, event);
1366
+ };
1367
+ document.addEventListener("mouseover", handleMouseOver, { capture: true, passive: true });
1368
+ document.addEventListener("mouseout", handleMouseOut, { capture: true, passive: true });
1369
+ document.addEventListener("click", handleClick, { capture: true, passive: false });
1370
+ return () => {
1371
+ document.removeEventListener("mouseover", handleMouseOver, {
1372
+ capture: true
1373
+ });
1374
+ document.removeEventListener("mouseout", handleMouseOut, {
1375
+ capture: true
1376
+ });
1377
+ document.removeEventListener("click", handleClick, { capture: true });
1378
+ clearHighlight();
1379
+ document.body.classList.remove("issue-sticker-mode-active");
1380
+ };
1381
+ }
1382
+ const DEFAULT_EXCLUDE_SELECTORS = [
1383
+ '[data-issue-sticker-capture-ignore="true"]',
1384
+ "#issue-sticker-host",
1385
+ '[data-issue-sticker-headless-auth="true"]'
1386
+ ];
1387
+ class HeadlessClientImpl {
1388
+ constructor(config) {
1389
+ var _a;
1390
+ this.config = config;
1391
+ this.state = new HeadlessClientState();
1392
+ this.pendingMedia = new HeadlessPendingMedia();
1393
+ this.initializePromise = null;
1394
+ this.captureCleanup = null;
1395
+ this.captureSession = null;
1396
+ this.suppressNextVideoStopSelection = false;
1397
+ if (!((_a = config.apiKey) == null ? void 0 : _a.trim())) {
1398
+ throw new IssueStickerHeadlessError("VALIDATION_ERROR", "apiKey가 필요합니다.");
1399
+ }
1400
+ this.authBridge = new HeadlessAuthBridge({
1401
+ apiKey: config.apiKey,
1402
+ getTenantId: () => this.snapshot.auth.tenantId,
1403
+ getProjectId: () => this.snapshot.auth.projectId,
1404
+ onToken: (result) => this.state.applyBridgeToken(result)
1405
+ });
1406
+ this.transport = new HeadlessTransport({
1407
+ getAuthMode: () => this.snapshot.auth.mode,
1408
+ resolveAuthorizationHeader: (forceRefreshMemberToken) => this.resolveAuthorizationHeader(forceRefreshMemberToken)
1409
+ });
1410
+ this.issueRepository = new HeadlessIssueRepository({
1411
+ transport: this.transport,
1412
+ getProjectId: () => this.state.requireProjectId(),
1413
+ getAuthMode: () => this.snapshot.auth.mode,
1414
+ getDefaultExternalUserId: () => {
1415
+ var _a2;
1416
+ return (_a2 = this.config.user) == null ? void 0 : _a2.id;
1417
+ },
1418
+ getCurrentUrl: () => typeof window !== "undefined" ? window.location.href : void 0
1419
+ });
1420
+ this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
1421
+ this.screenRecordingController = new ScreenRecordingController({
1422
+ onStopped: (file) => {
1423
+ void this.handleRecordedVideoStopped(file);
1424
+ }
1425
+ });
1426
+ this.screenRecordingController.subscribe((recording) => {
1427
+ this.state.setSnapshot({
1428
+ videoCapture: {
1429
+ isRecording: recording.isRecording,
1430
+ hasRecordedVideo: Boolean(recording.recordedFile || this.pendingMedia.hasVideo),
1431
+ error: recording.error
1432
+ }
1433
+ });
1434
+ });
1435
+ this.sessionReplayController = new SessionReplayCaptureController({
1436
+ loadCapabilities: () => this.request("/api/session-replays/capabilities")
1437
+ });
1438
+ this.sessionReplayController.subscribe((sessionReplay) => {
1439
+ this.applySessionReplaySnapshot(sessionReplay);
1440
+ });
1441
+ }
1442
+ get snapshot() {
1443
+ return this.state.getSnapshot();
1444
+ }
1445
+ get phase() {
1446
+ return this.state.phase;
1447
+ }
1448
+ getSnapshot() {
1449
+ return this.state.getSnapshot();
1450
+ }
1451
+ subscribe(listener) {
1452
+ return this.state.subscribe(listener);
1453
+ }
1454
+ async initialize() {
1455
+ if (this.snapshot.auth.isReady) {
1456
+ return;
1457
+ }
1458
+ if (this.initializePromise) {
1459
+ return this.initializePromise;
1460
+ }
1461
+ this.initializePromise = this.initializeInternal().finally(() => {
1462
+ this.initializePromise = null;
1463
+ });
1464
+ return this.initializePromise;
1465
+ }
1466
+ async refreshIssues(filters = {}) {
1467
+ await this.ensureInitialized();
1468
+ this.state.assertPhase(["idle", "viewingIssue"], "refreshIssues");
1469
+ this.state.setSnapshot({ phase: "loadingIssues", error: null });
1470
+ try {
1471
+ const issues = await this.issueRepository.fetchAllIssueStickers(filters);
1472
+ this.state.setSnapshot({ issues, phase: "idle" });
1473
+ } catch (error) {
1474
+ const nextError = this.toHeadlessError(error);
1475
+ this.state.setSnapshot({ phase: "idle", error: nextError });
1476
+ throw nextError;
1477
+ }
1478
+ }
1479
+ async refreshFilterSchema() {
1480
+ await this.ensureInitialized();
1481
+ try {
1482
+ this.state.setSnapshot({ filterSchema: await this.issueRepository.loadFilterSchema() });
1483
+ } catch (error) {
1484
+ this.state.setSnapshot({
1485
+ filterSchema: { groups: [], unavailableReason: "loading-failed" },
1486
+ error: this.toHeadlessError(error)
1487
+ });
1488
+ }
1489
+ }
1490
+ async openIssue(issueId) {
1491
+ await this.ensureInitialized();
1492
+ this.state.assertPhase(["idle", "viewingIssue"], "openIssue");
1493
+ this.state.setSnapshot({ phase: "viewingIssue", error: null });
1494
+ try {
1495
+ const activeIssue = await this.issueRepository.openIssue(issueId);
1496
+ this.state.setSnapshot({ activeIssue, phase: "viewingIssue" });
1497
+ } catch (error) {
1498
+ const nextError = this.toHeadlessError(error);
1499
+ this.state.setSnapshot({ activeIssue: null, phase: "idle", error: nextError });
1500
+ throw nextError;
1501
+ }
1502
+ }
1503
+ closeIssue() {
1504
+ if (this.snapshot.phase === "viewingIssue") {
1505
+ this.state.setSnapshot({ activeIssue: null, phase: "idle" });
1506
+ }
1507
+ }
1508
+ async startIssueCapture(options = {}) {
1509
+ await this.ensureInitialized();
1510
+ this.state.assertPhase(["idle", "viewingIssue"], "startIssueCapture");
1511
+ if (typeof document === "undefined" || typeof window === "undefined") {
1512
+ throw new IssueStickerHeadlessError(
1513
+ "CAPTURE_FAILED",
1514
+ "브라우저 DOM에서만 이슈 캡쳐를 시작할 수 있습니다."
1515
+ );
1516
+ }
1517
+ assertScreenshotCaptureOptions(options);
1518
+ this.closeIssue();
1519
+ this.clearCaptureListeners();
1520
+ this.clearPendingMedia();
1521
+ this.beginTargetSelection();
1522
+ }
1523
+ async startVideoCapture() {
1524
+ await this.ensureInitialized();
1525
+ this.state.assertPhase(["idle", "viewingIssue"], "startVideoCapture");
1526
+ if (typeof document === "undefined" || typeof window === "undefined") {
1527
+ throw new IssueStickerHeadlessError(
1528
+ "CAPTURE_FAILED",
1529
+ "브라우저 DOM에서만 영상 캡쳐를 시작할 수 있습니다."
1530
+ );
1531
+ }
1532
+ this.closeIssue();
1533
+ this.clearCaptureListeners();
1534
+ this.clearPendingMedia();
1535
+ this.captureSession = null;
1536
+ this.suppressNextVideoStopSelection = false;
1537
+ this.state.setSnapshot({ phase: "recordingVideo", draft: null, formSchema: null, error: null });
1538
+ try {
1539
+ await this.screenRecordingController.startRecording();
1540
+ } catch (error) {
1541
+ const nextError = this.toHeadlessError(error, "CAPTURE_FAILED");
1542
+ this.clearPendingMedia();
1543
+ this.state.setSnapshot({ phase: "idle", error: nextError });
1544
+ throw nextError;
1545
+ }
1546
+ }
1547
+ async stopVideoCapture() {
1548
+ await this.ensureInitialized();
1549
+ this.state.assertPhase(["recordingVideo"], "stopVideoCapture");
1550
+ const file = await this.screenRecordingController.stopRecording();
1551
+ if (!file && this.snapshot.phase === "recordingVideo") {
1552
+ this.clearPendingMedia();
1553
+ this.state.setSnapshot({ phase: "idle" });
1554
+ }
1555
+ }
1556
+ setSessionReplayEnabled(enabled) {
1557
+ this.sessionReplayController.setEnabled(enabled);
1558
+ }
1559
+ beginTargetSelection() {
1560
+ if (typeof document === "undefined") {
1561
+ throw new IssueStickerHeadlessError(
1562
+ "CAPTURE_FAILED",
1563
+ "브라우저 DOM에서만 이슈 캡쳐를 시작할 수 있습니다."
1564
+ );
1565
+ }
1566
+ preloadModernScreenshot();
1567
+ this.captureSession = null;
1568
+ this.state.setSnapshot({
1569
+ phase: "selectingTarget",
1570
+ draft: null,
1571
+ formSchema: null,
1572
+ error: null
1573
+ });
1574
+ this.captureCleanup = startHeadlessTargetSelection({
1575
+ isIgnoredTarget: (target) => this.isIgnoredTarget(target),
1576
+ onCapture: (target, event) => {
1577
+ void this.captureTarget(target, event);
1578
+ }
1579
+ });
1580
+ }
1581
+ cancelIssueCapture() {
1582
+ if (this.snapshot.phase !== "recordingVideo" && this.snapshot.phase !== "selectingTarget" && this.snapshot.phase !== "draftingIssue") {
1583
+ return;
1584
+ }
1585
+ this.clearCaptureListeners();
1586
+ if (this.snapshot.phase === "recordingVideo") {
1587
+ this.suppressNextVideoStopSelection = true;
1588
+ this.screenRecordingController.discard();
1589
+ }
1590
+ this.clearPendingMedia();
1591
+ this.captureSession = null;
1592
+ this.state.setSnapshot({ phase: "idle", draft: null, formSchema: null, error: null });
1593
+ }
1594
+ async submitDraft(input) {
1595
+ await this.ensureInitialized();
1596
+ this.state.assertPhase(["draftingIssue"], "submitDraft");
1597
+ this.state.requireDraft();
1598
+ if (!input.title.trim()) {
1599
+ throw new IssueStickerHeadlessError("VALIDATION_ERROR", "제목을 입력해주세요.");
1600
+ }
1601
+ if (!input.description.trim()) {
1602
+ throw new IssueStickerHeadlessError("VALIDATION_ERROR", "설명을 입력해주세요.");
1603
+ }
1604
+ this.state.setSnapshot({ phase: "submittingIssue", error: null });
1605
+ try {
1606
+ const session = this.captureSession;
1607
+ const screenshotPreviewUrls = (session == null ? void 0 : session.screenshots) ? await session.screenshots : this.state.requireDraft().screenshotPreviewUrls;
1608
+ const formSchema = this.snapshot.formSchema ?? ((session == null ? void 0 : session.formSchema) ? await session.formSchema : null);
1609
+ const submission = {
1610
+ ...this.state.requireDraft(),
1611
+ screenshotPreviewUrls,
1612
+ title: input.title,
1613
+ description: input.description,
1614
+ dynamicFields: input.dynamicFields ?? {}
1615
+ };
1616
+ const createdIssue = await submitHeadlessIssueDraft({
1617
+ draft: submission,
1618
+ pendingVideoFile: this.pendingMedia.videoFile,
1619
+ transport: this.transport,
1620
+ issueRepository: this.issueRepository,
1621
+ formSchema,
1622
+ auth: this.snapshot.auth,
1623
+ config: this.config,
1624
+ projectId: this.state.requireProjectId(),
1625
+ logs: this.logCollectors.collect(),
1626
+ currentUrl: typeof window !== "undefined" ? window.location.href : "",
1627
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1628
+ uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
1629
+ });
1630
+ this.clearCaptureListeners();
1631
+ this.clearPendingMedia();
1632
+ this.captureSession = null;
1633
+ this.state.setSnapshot({
1634
+ phase: "idle",
1635
+ draft: null,
1636
+ formSchema: null,
1637
+ issues: [createdIssue, ...this.snapshot.issues]
1638
+ });
1639
+ return createdIssue;
1640
+ } catch (error) {
1641
+ const nextError = this.toHeadlessError(error);
1642
+ this.state.setSnapshot({ phase: "draftingIssue", error: nextError });
1643
+ throw nextError;
1644
+ }
1645
+ }
1646
+ async loginAsMember() {
1647
+ await this.ensureInitialized();
1648
+ if (!this.authBridge.canUseBridge) {
1649
+ throw new IssueStickerHeadlessError(
1650
+ "MEMBER_AUTH_REQUIRED",
1651
+ "SDK auth bridge를 사용할 수 없습니다."
1652
+ );
1653
+ }
1654
+ const silentToken = await this.authBridge.getValidMemberToken({ forceRefresh: true }).catch(() => null);
1655
+ if (silentToken) {
1656
+ this.state.setAuthMode("member");
1657
+ await this.refreshFilterSchema();
1658
+ await this.sessionReplayController.refresh();
1659
+ return;
1660
+ }
1661
+ await this.authBridge.openInteractiveLoginFrame();
1662
+ await this.refreshFilterSchema();
1663
+ await this.sessionReplayController.refresh();
1664
+ }
1665
+ async logoutMember() {
1666
+ await this.ensureInitialized();
1667
+ this.authBridge.clearMemberToken();
1668
+ this.state.setAuthMode("public");
1669
+ this.authBridge.postLogout();
1670
+ this.state.setSnapshot({
1671
+ filterSchema: { groups: [], unavailableReason: "public-mode" },
1672
+ formSchema: null
1673
+ });
1674
+ void this.sessionReplayController.refresh();
1675
+ }
1676
+ destroy() {
1677
+ this.clearCaptureListeners();
1678
+ this.suppressNextVideoStopSelection = true;
1679
+ this.captureSession = null;
1680
+ this.screenRecordingController.destroy();
1681
+ this.sessionReplayController.destroy();
1682
+ this.clearPendingMedia();
1683
+ this.logCollectors.stop();
1684
+ this.authBridge.destroy();
1685
+ this.state.reset();
1686
+ }
1687
+ async initializeInternal() {
1688
+ this.state.setSnapshot({ phase: "initializing", error: null });
1689
+ try {
1690
+ const result = await this.request("/api/sdk/auth/public-init", {
1691
+ method: "POST",
1692
+ body: { apiKey: this.config.apiKey },
1693
+ skipAuth: true
1694
+ });
1695
+ this.authBridge.setAuthBridgeUrl(result.authBridgeUrl ?? null);
1696
+ this.state.setSnapshot({
1697
+ phase: "idle",
1698
+ auth: {
1699
+ mode: "public",
1700
+ isReady: true,
1701
+ canUseIntegrations: false,
1702
+ tenantId: result.tenantId,
1703
+ projectId: result.projectId,
1704
+ accountId: null
1705
+ },
1706
+ filterSchema: { groups: [], unavailableReason: "public-mode" }
1707
+ });
1708
+ this.logCollectors.start();
1709
+ this.authBridge.mountHiddenBridgeFrame();
1710
+ void this.sessionReplayController.initialize();
1711
+ } catch (error) {
1712
+ const nextError = this.toHeadlessError(error);
1713
+ this.state.setSnapshot({ phase: "idle", error: nextError });
1714
+ throw nextError;
1715
+ }
1716
+ }
1717
+ async takeSessionReplayPreview() {
1718
+ const sessionReplay = this.sessionReplayController.getSnapshot();
1719
+ if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
1720
+ try {
1721
+ const snapshot = await this.sessionReplayController.takeSnapshot();
1722
+ return snapshot ? createSessionReplayMeta(snapshot) : null;
1723
+ } catch {
1724
+ return null;
1725
+ }
1726
+ }
1727
+ async uploadSessionReplaySnapshot() {
1728
+ const sessionReplay = this.sessionReplayController.getSnapshot();
1729
+ if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
1730
+ try {
1731
+ const snapshot = await this.sessionReplayController.takeSnapshot();
1732
+ if (!snapshot) return null;
1733
+ if (snapshot.eventCount <= 0) return null;
1734
+ const upload = await this.transport.uploadSignedMedia(createReplayFile(snapshot), "replay");
1735
+ return {
1736
+ uploadId: upload.id,
1737
+ url: upload.url,
1738
+ meta: createSessionReplayMeta(snapshot)
1739
+ };
1740
+ } catch {
1741
+ return null;
1742
+ }
1743
+ }
1744
+ async handleRecordedVideoStopped(file) {
1745
+ if (this.suppressNextVideoStopSelection) {
1746
+ this.suppressNextVideoStopSelection = false;
1747
+ this.clearPendingMedia();
1748
+ return;
1749
+ }
1750
+ this.pendingMedia.setVideoFile(file);
1751
+ this.state.setSnapshot({
1752
+ videoCapture: {
1753
+ isRecording: false,
1754
+ hasRecordedVideo: true,
1755
+ error: null
1756
+ }
1757
+ });
1758
+ if (this.snapshot.phase === "recordingVideo") {
1759
+ try {
1760
+ this.beginTargetSelection();
1761
+ } catch (error) {
1762
+ const nextError = this.toHeadlessError(error, "CAPTURE_FAILED");
1763
+ this.clearPendingMedia();
1764
+ this.state.setSnapshot({ phase: "idle", error: nextError });
1765
+ }
1766
+ }
1767
+ }
1768
+ applySessionReplaySnapshot(sessionReplay) {
1769
+ const canAttachCurrentDraft = Boolean(sessionReplay.enabled && sessionReplay.canAttach);
1770
+ this.state.setSnapshot({
1771
+ sessionReplay: {
1772
+ available: sessionReplay.available,
1773
+ canAttach: sessionReplay.canAttach,
1774
+ enabled: sessionReplay.enabled,
1775
+ error: sessionReplay.error
1776
+ },
1777
+ draft: this.snapshot.draft ? {
1778
+ ...this.snapshot.draft,
1779
+ sessionReplayEnabled: canAttachCurrentDraft,
1780
+ sessionReplayPreview: canAttachCurrentDraft ? this.snapshot.draft.sessionReplayPreview : null
1781
+ } : this.snapshot.draft
1782
+ });
1783
+ }
1784
+ clearPendingMedia() {
1785
+ this.pendingMedia.clear();
1786
+ this.screenRecordingController.clearRecordedFile();
1787
+ this.state.setSnapshot({
1788
+ videoCapture: {
1789
+ isRecording: this.screenRecordingController.getSnapshot().isRecording,
1790
+ hasRecordedVideo: false,
1791
+ error: this.screenRecordingController.getSnapshot().error
1792
+ }
1793
+ });
1794
+ }
1795
+ captureTarget(target, event) {
1796
+ if (this.snapshot.phase !== "selectingTarget") {
1797
+ return;
1798
+ }
1799
+ this.clearCaptureListeners();
1800
+ const sessionReplay = this.sessionReplayController.getSnapshot();
1801
+ const draft = buildHeadlessCaptureContext({
1802
+ target,
1803
+ event,
1804
+ videoFile: this.pendingMedia.videoFile,
1805
+ createVideoPreviewUrl: (file) => this.pendingMedia.createVideoPreviewUrl(file),
1806
+ sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
1807
+ });
1808
+ this.state.setSnapshot({ phase: "draftingIssue", draft, formSchema: null, error: null });
1809
+ this.beginBackgroundCapturePreparation(draft, target);
1810
+ }
1811
+ beginBackgroundCapturePreparation(draft, target) {
1812
+ var _a, _b;
1813
+ const token = {};
1814
+ this.captureSession = {
1815
+ token,
1816
+ screenshots: draft.mediaMode === "screenshot" ? captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() }) : null,
1817
+ formSchema: this.loadFormSchema({})
1818
+ };
1819
+ const isCurrentSession = () => {
1820
+ var _a2;
1821
+ return ((_a2 = this.captureSession) == null ? void 0 : _a2.token) === token;
1822
+ };
1823
+ void ((_a = this.captureSession.screenshots) == null ? void 0 : _a.then((screenshotPreviewUrls) => {
1824
+ if (!isCurrentSession()) return;
1825
+ this.mergeDraft({ screenshotPreviewUrls });
1826
+ }));
1827
+ void ((_b = this.captureSession.formSchema) == null ? void 0 : _b.then((formSchema) => {
1828
+ if (!isCurrentSession()) return;
1829
+ this.state.setSnapshot({ formSchema });
1830
+ }));
1831
+ if (draft.sessionReplayEnabled) {
1832
+ void this.takeSessionReplayPreview().then((sessionReplayPreview) => {
1833
+ var _a2;
1834
+ if (!isCurrentSession()) return;
1835
+ if (!((_a2 = this.snapshot.draft) == null ? void 0 : _a2.sessionReplayEnabled)) return;
1836
+ this.mergeDraft({ sessionReplayPreview });
1837
+ });
1838
+ }
1839
+ }
1840
+ mergeDraft(patch) {
1841
+ const draft = this.snapshot.draft;
1842
+ if (!draft) return;
1843
+ if (this.snapshot.phase !== "draftingIssue" && this.snapshot.phase !== "submittingIssue") {
1844
+ return;
1845
+ }
1846
+ this.state.setSnapshot({ draft: { ...draft, ...patch } });
1847
+ }
1848
+ async loadFormSchema(values) {
1849
+ try {
1850
+ return await buildIssueFormSchema(values, {
1851
+ authMode: this.snapshot.auth.mode,
1852
+ getActiveIntegration: () => this.issueRepository.getActiveIntegration(),
1853
+ getCustomFieldDefinitions: () => this.issueRepository.getCustomFieldDefinitions(),
1854
+ requestResponse: (endpoint) => this.issueRepository.requestResponse(endpoint),
1855
+ searchJiraProjectUsers: (query) => this.issueRepository.searchJiraProjectUsers(query),
1856
+ resolveJiraProjectUserLabel: (accountId) => this.issueRepository.resolveJiraProjectUserLabel(accountId),
1857
+ searchJiraProjectIssues: (query) => this.issueRepository.searchJiraProjectIssues(query),
1858
+ resolveJiraProjectIssueLabel: (issueKey) => this.issueRepository.resolveJiraProjectIssueLabel(issueKey),
1859
+ searchNotionPeople: (query) => this.issueRepository.searchNotionPeople(query)
1860
+ });
1861
+ } catch (error) {
1862
+ this.state.setSnapshot({ error: this.toHeadlessError(error) });
1863
+ return { fields: [], unavailableReason: "loading-failed" };
1864
+ }
1865
+ }
1866
+ async request(endpoint, options = {}) {
1867
+ return this.transport.request(endpoint, options);
1868
+ }
1869
+ async resolveAuthorizationHeader(forceRefreshMemberToken = false) {
1870
+ if (this.snapshot.auth.mode === "member") {
1871
+ const token = await this.authBridge.getValidMemberToken({
1872
+ forceRefresh: forceRefreshMemberToken
1873
+ });
1874
+ if (!token) {
1875
+ throw new IssueStickerHeadlessError("MEMBER_AUTH_REQUIRED", "멤버 로그인이 필요합니다.");
1876
+ }
1877
+ return `Bearer ${token}`;
1878
+ }
1879
+ return `ApiKey ${this.config.apiKey}`;
1880
+ }
1881
+ ensureInitialized() {
1882
+ return this.snapshot.auth.isReady ? Promise.resolve() : this.initialize();
1883
+ }
1884
+ clearCaptureListeners() {
1885
+ var _a;
1886
+ (_a = this.captureCleanup) == null ? void 0 : _a.call(this);
1887
+ this.captureCleanup = null;
1888
+ }
1889
+ isIgnoredTarget(target) {
1890
+ if (target.closest(".issue-sticker-marker")) return true;
1891
+ return this.getExcludeSelectors().some((selector) => target.closest(selector));
1892
+ }
1893
+ getExcludeSelectors() {
1894
+ var _a;
1895
+ return [...DEFAULT_EXCLUDE_SELECTORS, ...((_a = this.config.capture) == null ? void 0 : _a.excludeSelectors) ?? []];
1896
+ }
1897
+ toHeadlessError(error, fallbackCode = "NETWORK_ERROR") {
1898
+ if (error instanceof IssueStickerHeadlessError) {
1899
+ return error;
1900
+ }
1901
+ if (error instanceof Error) {
1902
+ return new IssueStickerHeadlessError(fallbackCode, error.message, error);
1903
+ }
1904
+ return new IssueStickerHeadlessError(fallbackCode, "알 수 없는 오류가 발생했습니다.", error);
1905
+ }
1906
+ }
1907
+ function createIssueStickerHeadless(config) {
1908
+ return new HeadlessClientImpl(config);
1909
+ }
1910
+ export {
1911
+ IssueStickerHeadlessError,
1912
+ createIssueStickerHeadless,
1913
+ createIssueStickerHeadless as default
1914
+ };