issue-sticker 0.11.0 → 0.12.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.
@@ -1,4 +1,4 @@
1
- import { t as getCanvasSource, v as onCanvasSourceRegistered, C as CANVAS_INTERACTION_TAG, w as gzip_1, R as REPLAY_EVENT_TYPE, x as record, a as cn, p as useCallbackRef$1, i as useComposedRefs, f as Primitive, P as Presence, j as Portal$1, g as composeEventHandlers, k as createContextScope, l as createSlot, D as DismissableLayer, y as Content, z as createPopperScope, A as Anchor, m as useControllableState, B as Root2$1, n as useId, E as Arrow, d as usePortalContainer, e as OVERLAY_LAYER, s as stopEscapePropagation, o as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, F as clsx, S as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-DQdLNs7v.js";
1
+ import { t as getCanvasSource, v as onCanvasSourceRegistered, C as CANVAS_INTERACTION_TAG, w as gzip_1, R as REPLAY_EVENT_TYPE, x as record, a as cn, p as useCallbackRef$1, k as useComposedRefs, h as Primitive, P as Presence, l as Portal$1, i as composeEventHandlers, m as createContextScope, n as createSlot, D as DismissableLayer, y as Content, z as createPopperScope, A as Anchor, f as useControllableState, B as Root2$1, g as useId, E as Arrow, d as usePortalContainer, e as OVERLAY_LAYER, s as stopEscapePropagation, o as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, F as clsx, S as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-B228j4M6.js";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import React__default, { useState, useRef, useEffect, useCallback, createContext, useContext, useLayoutEffect, useMemo } from "react";
@@ -83,7 +83,8 @@ class ScreenRecordingController {
83
83
  const mimeType = chooseMimeType(this.isTypeSupported);
84
84
  const mediaRecorder = this.createMediaRecorder(stream, {
85
85
  mimeType,
86
- videoBitsPerSecond: 25e5
86
+ // 실측(VP9, 2배율, 계속 스크롤): 1.5Mbps 에서 12px 글자 손상 없음, 1.0Mbps 는 잔상.
87
+ videoBitsPerSecond: 15e5
87
88
  });
88
89
  this.mediaRecorder = mediaRecorder;
89
90
  mediaRecorder.ondataavailable = (event) => {
@@ -894,7 +895,7 @@ function settleFrame() {
894
895
  async function resolveNodeId(canvas) {
895
896
  var _a;
896
897
  try {
897
- const { record: record2 } = await import("./three-DQdLNs7v.js").then((n) => n.I);
898
+ const { record: record2 } = await import("./three-B228j4M6.js").then((n) => n.I);
898
899
  const id = (_a = record2.mirror) == null ? void 0 : _a.getId(canvas);
899
900
  return typeof id === "number" && id > 0 ? id : null;
900
901
  } catch {
@@ -913,7 +914,7 @@ function startCanvasInteractionTracking(options = {}) {
913
914
  const lastSnapshot = /* @__PURE__ */ new WeakMap();
914
915
  let pending = null;
915
916
  const emit2 = options.emit ?? ((tag, payload) => {
916
- void import("./three-DQdLNs7v.js").then((n) => n.I).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
917
+ void import("./three-B228j4M6.js").then((n) => n.I).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
917
918
  });
918
919
  const pointerOf = (started, rect) => [
919
920
  (started.x - rect.left) / rect.width * 2 - 1,
@@ -1184,10 +1185,6 @@ function dropEventsAfter(segments, cutoff) {
1184
1185
  return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
1185
1186
  }
1186
1187
  const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1187
- async function sha256Hex$1(bytes) {
1188
- const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
1189
- return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
1190
- }
1191
1188
  async function serializeSnapshot(input) {
1192
1189
  const redact = input.redactEvents ?? ((e2) => e2);
1193
1190
  const maxBytes = input.maxBytes ?? REPLAY_SNAPSHOT_MAX_BYTES;
@@ -1201,10 +1198,8 @@ async function serializeSnapshot(input) {
1201
1198
  segments = segments.slice(1);
1202
1199
  ({ events, payload } = compress2(segments));
1203
1200
  }
1204
- const checksumSha256 = await sha256Hex$1(payload);
1205
1201
  return {
1206
1202
  payload,
1207
- checksumSha256,
1208
1203
  windowSeconds: spanSeconds(segments),
1209
1204
  eventCount: events.length,
1210
1205
  sizeBytes: payload.byteLength,
@@ -2065,8 +2060,8 @@ class WebSocketCollector {
2065
2060
  }
2066
2061
  const LOG_BACKUP_SESSION_KEY = "__issueStickerLogSession";
2067
2062
  const DB_NAME$1 = "issue-sticker-logs";
2068
- const DB_VERSION$1 = 1;
2069
- const STORE_NAME$1 = "sessions";
2063
+ const DB_VERSION$2 = 1;
2064
+ const STORE_NAME$2 = "sessions";
2070
2065
  const LOG_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
2071
2066
  const LOG_BACKUP_BUDGET_BYTES = 8 * 1024 * 1024;
2072
2067
  function jsonLength(value) {
@@ -2147,12 +2142,12 @@ function requestToPromise$1(request) {
2147
2142
  request.onerror = () => reject(request.error);
2148
2143
  });
2149
2144
  }
2150
- function openDatabase$1() {
2145
+ function openDatabase$2() {
2151
2146
  return new Promise((resolve, reject) => {
2152
- const request = indexedDB.open(DB_NAME$1, DB_VERSION$1);
2147
+ const request = indexedDB.open(DB_NAME$1, DB_VERSION$2);
2153
2148
  request.onupgradeneeded = () => {
2154
- if (!request.result.objectStoreNames.contains(STORE_NAME$1)) {
2155
- request.result.createObjectStore(STORE_NAME$1, { keyPath: "sessionId" });
2149
+ if (!request.result.objectStoreNames.contains(STORE_NAME$2)) {
2150
+ request.result.createObjectStore(STORE_NAME$2, { keyPath: "sessionId" });
2156
2151
  }
2157
2152
  };
2158
2153
  request.onsuccess = () => {
@@ -2167,10 +2162,10 @@ function openDatabase$1() {
2167
2162
  function createIndexedDbLogBackup() {
2168
2163
  return {
2169
2164
  async load(sessionId) {
2170
- const db = await openDatabase$1();
2165
+ const db = await openDatabase$2();
2171
2166
  try {
2172
2167
  const record2 = await requestToPromise$1(
2173
- db.transaction(STORE_NAME$1, "readonly").objectStore(STORE_NAME$1).get(sessionId)
2168
+ db.transaction(STORE_NAME$2, "readonly").objectStore(STORE_NAME$2).get(sessionId)
2174
2169
  );
2175
2170
  if (!record2) return null;
2176
2171
  if (record2.expiresAt <= Date.now()) return null;
@@ -2180,7 +2175,7 @@ function createIndexedDbLogBackup() {
2180
2175
  }
2181
2176
  },
2182
2177
  async save(sessionId, payload) {
2183
- const db = await openDatabase$1();
2178
+ const db = await openDatabase$2();
2184
2179
  try {
2185
2180
  const record2 = {
2186
2181
  sessionId,
@@ -2188,16 +2183,16 @@ function createIndexedDbLogBackup() {
2188
2183
  expiresAt: Date.now() + LOG_BACKUP_TTL_MS
2189
2184
  };
2190
2185
  await requestToPromise$1(
2191
- db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).put(record2)
2186
+ db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2).put(record2)
2192
2187
  );
2193
2188
  } finally {
2194
2189
  db.close();
2195
2190
  }
2196
2191
  },
2197
2192
  async prune() {
2198
- const db = await openDatabase$1();
2193
+ const db = await openDatabase$2();
2199
2194
  try {
2200
- const store = db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1);
2195
+ const store = db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2);
2201
2196
  const records = await requestToPromise$1(store.getAll());
2202
2197
  const now = Date.now();
2203
2198
  const expired = records.filter((record2) => record2.expiresAt <= now);
@@ -2210,10 +2205,10 @@ function createIndexedDbLogBackup() {
2210
2205
  }
2211
2206
  },
2212
2207
  async clear(sessionId) {
2213
- const db = await openDatabase$1();
2208
+ const db = await openDatabase$2();
2214
2209
  try {
2215
2210
  await requestToPromise$1(
2216
- db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).delete(sessionId)
2211
+ db.transaction(STORE_NAME$2, "readwrite").objectStore(STORE_NAME$2).delete(sessionId)
2217
2212
  );
2218
2213
  } finally {
2219
2214
  db.close();
@@ -2283,8 +2278,8 @@ function createSafeLogBackup(store) {
2283
2278
  };
2284
2279
  }
2285
2280
  const DB_NAME = "issue-sticker-replay";
2286
- const DB_VERSION = 1;
2287
- const STORE_NAME = "segments";
2281
+ const DB_VERSION$1 = 1;
2282
+ const STORE_NAME$1 = "segments";
2288
2283
  const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
2289
2284
  const REPLAY_BACKUP_SIZE_BUDGET = 32 * 1024 * 1024;
2290
2285
  const eventSizes = /* @__PURE__ */ new WeakMap();
@@ -2336,12 +2331,12 @@ function requestToPromise(request) {
2336
2331
  request.onerror = () => reject(request.error);
2337
2332
  });
2338
2333
  }
2339
- function openDatabase() {
2334
+ function openDatabase$1() {
2340
2335
  return new Promise((resolve, reject) => {
2341
- const request = indexedDB.open(DB_NAME, DB_VERSION);
2336
+ const request = indexedDB.open(DB_NAME, DB_VERSION$1);
2342
2337
  request.onupgradeneeded = () => {
2343
- if (!request.result.objectStoreNames.contains(STORE_NAME)) {
2344
- request.result.createObjectStore(STORE_NAME, { keyPath: "sessionId" });
2338
+ if (!request.result.objectStoreNames.contains(STORE_NAME$1)) {
2339
+ request.result.createObjectStore(STORE_NAME$1, { keyPath: "sessionId" });
2345
2340
  }
2346
2341
  };
2347
2342
  request.onsuccess = () => {
@@ -2356,10 +2351,10 @@ function openDatabase() {
2356
2351
  function createIndexedDbReplayBackup() {
2357
2352
  return {
2358
2353
  async load(sessionId) {
2359
- const db = await openDatabase();
2354
+ const db = await openDatabase$1();
2360
2355
  try {
2361
2356
  const record2 = await requestToPromise(
2362
- db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(sessionId)
2357
+ db.transaction(STORE_NAME$1, "readonly").objectStore(STORE_NAME$1).get(sessionId)
2363
2358
  );
2364
2359
  if (!record2) return null;
2365
2360
  if (record2.expiresAt <= Date.now()) return null;
@@ -2369,7 +2364,7 @@ function createIndexedDbReplayBackup() {
2369
2364
  }
2370
2365
  },
2371
2366
  async save(sessionId, segments) {
2372
- const db = await openDatabase();
2367
+ const db = await openDatabase$1();
2373
2368
  try {
2374
2369
  const record2 = {
2375
2370
  sessionId,
@@ -2377,16 +2372,16 @@ function createIndexedDbReplayBackup() {
2377
2372
  expiresAt: Date.now() + REPLAY_BACKUP_TTL_MS
2378
2373
  };
2379
2374
  await requestToPromise(
2380
- db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record2)
2375
+ db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).put(record2)
2381
2376
  );
2382
2377
  } finally {
2383
2378
  db.close();
2384
2379
  }
2385
2380
  },
2386
2381
  async prune() {
2387
- const db = await openDatabase();
2382
+ const db = await openDatabase$1();
2388
2383
  try {
2389
- const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
2384
+ const store = db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1);
2390
2385
  const records = await requestToPromise(store.getAll());
2391
2386
  const now = Date.now();
2392
2387
  const expired = records.filter((record2) => record2.expiresAt <= now);
@@ -2399,10 +2394,10 @@ function createIndexedDbReplayBackup() {
2399
2394
  }
2400
2395
  },
2401
2396
  async clear(sessionId) {
2402
- const db = await openDatabase();
2397
+ const db = await openDatabase$1();
2403
2398
  try {
2404
2399
  await requestToPromise(
2405
- db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).delete(sessionId)
2400
+ db.transaction(STORE_NAME$1, "readwrite").objectStore(STORE_NAME$1).delete(sessionId)
2406
2401
  );
2407
2402
  } finally {
2408
2403
  db.close();
@@ -2546,6 +2541,26 @@ function createReplaySession(options = {}) {
2546
2541
  }
2547
2542
  };
2548
2543
  }
2544
+ function readReleaseMarker() {
2545
+ try {
2546
+ const g = globalThis.__ISSUE_STICKER_RELEASE__;
2547
+ if (g && typeof g.id === "string" && g.id.length > 0) {
2548
+ return {
2549
+ release: g.id,
2550
+ release_source: typeof g.source === "string" && g.source.length > 0 ? g.source : "global"
2551
+ };
2552
+ }
2553
+ if (typeof document !== "undefined") {
2554
+ const meta = document.querySelector('meta[name="issue-sticker:release"]');
2555
+ const content = meta == null ? void 0 : meta.getAttribute("content");
2556
+ if (content && content.length > 0) {
2557
+ return { release: content, release_source: "meta" };
2558
+ }
2559
+ }
2560
+ } catch {
2561
+ }
2562
+ return null;
2563
+ }
2549
2564
  function createInitialSnapshot() {
2550
2565
  return {
2551
2566
  available: false,
@@ -2651,6 +2666,24 @@ class SessionReplayCaptureController {
2651
2666
  this.listeners.forEach((listener) => listener(this.snapshot));
2652
2667
  }
2653
2668
  }
2669
+ function dataUrlToBlob(dataUrl) {
2670
+ var _a;
2671
+ const comma = dataUrl.indexOf(",");
2672
+ const header = dataUrl.slice(0, comma);
2673
+ const mime = ((_a = /^data:([^;,]+)/.exec(header)) == null ? void 0 : _a[1]) ?? "application/octet-stream";
2674
+ const binary = atob(dataUrl.slice(comma + 1));
2675
+ const bytes = new Uint8Array(binary.length);
2676
+ for (let i2 = 0; i2 < binary.length; i2 += 1) bytes[i2] = binary.charCodeAt(i2);
2677
+ return new Blob([bytes], { type: mime });
2678
+ }
2679
+ function blobToDataUrl(blob) {
2680
+ return new Promise((resolve, reject) => {
2681
+ const reader = new FileReader();
2682
+ reader.onload = () => resolve(String(reader.result));
2683
+ reader.onerror = () => reject(reader.error ?? new Error("스크린샷을 dataUrl 로 변환하지 못했습니다."));
2684
+ reader.readAsDataURL(blob);
2685
+ });
2686
+ }
2654
2687
  const SCREENSHOT_DATA_URL_QUALITY = 0.95;
2655
2688
  let nextCaptureRunId = 0;
2656
2689
  function createCaptureAttributeNames() {
@@ -2861,14 +2894,6 @@ function createModernStickySnapshot(root, attributeNames) {
2861
2894
  }
2862
2895
  };
2863
2896
  }
2864
- function blobToDataUrl(blob) {
2865
- return new Promise((resolve, reject) => {
2866
- const reader = new FileReader();
2867
- reader.onload = () => resolve(String(reader.result));
2868
- reader.onerror = () => reject(reader.error ?? new Error("스크린샷을 dataUrl 로 변환하지 못했습니다."));
2869
- reader.readAsDataURL(blob);
2870
- });
2871
- }
2872
2897
  function captureOrigin(element) {
2873
2898
  if (element === document.documentElement || element === document.body) {
2874
2899
  return { x: 0, y: 0 };
@@ -2920,53 +2945,836 @@ async function captureWithModernScreenshot(options) {
2920
2945
  SCREENSHOT_DATA_URL_QUALITY
2921
2946
  )
2922
2947
  );
2923
- } finally {
2924
- canvasFrames.forEach((frame) => frame.bitmap.close());
2925
- excludeSnapshot.cleanup();
2926
- stickySnapshot.cleanup();
2948
+ } finally {
2949
+ canvasFrames.forEach((frame) => frame.bitmap.close());
2950
+ excludeSnapshot.cleanup();
2951
+ stickySnapshot.cleanup();
2952
+ }
2953
+ }
2954
+ const ELEMENT_CROP_PADDING_PX = 8;
2955
+ const ELEMENT_CROP_QUALITY = 0.95;
2956
+ function measureElementCropTarget(element) {
2957
+ var _a;
2958
+ if (element.ownerDocument !== document) return null;
2959
+ if (element === document.documentElement || element === document.body) return null;
2960
+ if ((((_a = window.visualViewport) == null ? void 0 : _a.scale) ?? 1) !== 1) return null;
2961
+ const rect = element.getBoundingClientRect();
2962
+ return {
2963
+ rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
2964
+ viewport: { width: window.innerWidth, height: window.innerHeight }
2965
+ };
2966
+ }
2967
+ function computeElementCropRect(rect, viewport, image, padding = ELEMENT_CROP_PADDING_PX) {
2968
+ if (viewport.width <= 0 || viewport.height <= 0 || image.width <= 0 || image.height <= 0) {
2969
+ return null;
2970
+ }
2971
+ const visibleWidth = Math.min(rect.right, viewport.width) - Math.max(rect.left, 0);
2972
+ const visibleHeight = Math.min(rect.bottom, viewport.height) - Math.max(rect.top, 0);
2973
+ if (visibleWidth < 1 || visibleHeight < 1) return null;
2974
+ const left = Math.max(rect.left - padding, 0);
2975
+ const top = Math.max(rect.top - padding, 0);
2976
+ const right = Math.min(rect.right + padding, viewport.width);
2977
+ const bottom = Math.min(rect.bottom + padding, viewport.height);
2978
+ const kx = image.width / viewport.width;
2979
+ const ky = image.height / viewport.height;
2980
+ const sx = Math.round(left * kx);
2981
+ const sy = Math.round(top * ky);
2982
+ const sw = Math.min(Math.round(right * kx), image.width) - sx;
2983
+ const sh = Math.min(Math.round(bottom * ky), image.height) - sy;
2984
+ return sw >= 1 && sh >= 1 ? { sx, sy, sw, sh } : null;
2985
+ }
2986
+ async function cropElementFromViewportCapture(viewportDataUrl, target) {
2987
+ const bitmap = await createImageBitmap(dataUrlToBlob(viewportDataUrl));
2988
+ try {
2989
+ const crop = computeElementCropRect(target.rect, target.viewport, {
2990
+ width: bitmap.width,
2991
+ height: bitmap.height
2992
+ });
2993
+ if (!crop) return void 0;
2994
+ const canvas = new OffscreenCanvas(crop.sw, crop.sh);
2995
+ const context = canvas.getContext("2d");
2996
+ if (!context) return void 0;
2997
+ context.drawImage(bitmap, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, crop.sw, crop.sh);
2998
+ return await blobToDataUrl(
2999
+ await canvas.convertToBlob({ type: "image/webp", quality: ELEMENT_CROP_QUALITY })
3000
+ );
3001
+ } finally {
3002
+ bitmap.close();
3003
+ }
3004
+ }
3005
+ async function captureViewportWithElementCrop({
3006
+ element,
3007
+ excludeSelectors = []
3008
+ }) {
3009
+ const target = measureElementCropTarget(element);
3010
+ const fullDataUrl = await captureWithModernScreenshot({
3011
+ element: document.documentElement,
3012
+ excludeSelectors,
3013
+ width: window.innerWidth,
3014
+ height: window.innerHeight
3015
+ }).catch(() => void 0);
3016
+ if (!fullDataUrl || !target) return { fullDataUrl };
3017
+ const elementDataUrl = await cropElementFromViewportCapture(fullDataUrl, target).catch(
3018
+ () => void 0
3019
+ );
3020
+ return { fullDataUrl, elementDataUrl };
3021
+ }
3022
+ const UPLOAD_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3023
+ const TUS_CHUNK_SIZE = 6 * 1024 * 1024;
3024
+ const TUS_RETRY_DELAYS = [0, 3e3, 5e3, 1e4, 2e4];
3025
+ class PermanentUploadError extends Error {
3026
+ }
3027
+ class UploadStoppedError extends Error {
3028
+ }
3029
+ const RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([401, 408, 423, 429]);
3030
+ function readStatus(error) {
3031
+ if (!error || typeof error !== "object") return void 0;
3032
+ const status = error.status;
3033
+ if (typeof status === "number") return status;
3034
+ const response = error.originalResponse;
3035
+ return typeof (response == null ? void 0 : response.getStatus) === "function" ? response.getStatus() : void 0;
3036
+ }
3037
+ function isPermanentStatus(status) {
3038
+ return status !== void 0 && status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
3039
+ }
3040
+ function toApiFailure(error) {
3041
+ return isPermanentStatus(readStatus(error)) ? new PermanentUploadError(error instanceof Error ? error.message : String(error)) : error;
3042
+ }
3043
+ const SIGNATURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
3044
+ function toTusFailure(error) {
3045
+ if (error instanceof UploadStoppedError) return error;
3046
+ const status = readStatus(error);
3047
+ if (status !== void 0 && SIGNATURE_STATUSES.has(status)) {
3048
+ return new PermanentUploadError(`tus ${status}`);
3049
+ }
3050
+ if (status === 409) return error;
3051
+ return isPermanentStatus(status) ? new PermanentUploadError(`tus ${status}`) : error;
3052
+ }
3053
+ function stripMimeParameters(mimeType) {
3054
+ return mimeType.split(";", 1)[0].trim().toLowerCase();
3055
+ }
3056
+ function callSafely(call) {
3057
+ try {
3058
+ call();
3059
+ } catch {
3060
+ }
3061
+ }
3062
+ function readBlob(blob) {
3063
+ if (typeof blob.arrayBuffer === "function") return blob.arrayBuffer();
3064
+ return new Promise((resolve, reject) => {
3065
+ const reader = new FileReader();
3066
+ reader.onload = () => resolve(reader.result);
3067
+ reader.onerror = () => reject(reader.error ?? new Error("업로드 파일을 읽지 못했습니다."));
3068
+ reader.readAsArrayBuffer(blob);
3069
+ });
3070
+ }
3071
+ function lockName(recordId) {
3072
+ return `issue-sticker-upload:${recordId}`;
3073
+ }
3074
+ function lockManager() {
3075
+ return typeof navigator !== "undefined" ? navigator.locks : void 0;
3076
+ }
3077
+ async function heldLockNames() {
3078
+ var _a;
3079
+ try {
3080
+ const snapshot = await ((_a = lockManager()) == null ? void 0 : _a.query());
3081
+ return new Set(((snapshot == null ? void 0 : snapshot.held) ?? []).flatMap((lock) => lock.name ? [lock.name] : []));
3082
+ } catch {
3083
+ return /* @__PURE__ */ new Set();
3084
+ }
3085
+ }
3086
+ async function sha256Hex(blob) {
3087
+ const digest = await crypto.subtle.digest("SHA-256", await readBlob(blob));
3088
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
3089
+ }
3090
+ class MediaUploadQueue {
3091
+ constructor(deps) {
3092
+ this.deps = deps;
3093
+ this.records = /* @__PURE__ */ new Map();
3094
+ this.states = /* @__PURE__ */ new Map();
3095
+ this.listeners = /* @__PURE__ */ new Set();
3096
+ this.running = false;
3097
+ this.stopped = false;
3098
+ this.cancelCurrent = null;
3099
+ this.finished = /* @__PURE__ */ new Set();
3100
+ this.failureNotified = /* @__PURE__ */ new Set();
3101
+ this.persisted = /* @__PURE__ */ new Set();
3102
+ this.held = /* @__PURE__ */ new Set();
3103
+ this.resolveStopped = () => void 0;
3104
+ this.handleOnline = () => this.requeueFailed();
3105
+ this.now = deps.now ?? Date.now;
3106
+ this.whenStopped = new Promise((resolve) => {
3107
+ this.resolveStopped = resolve;
3108
+ });
3109
+ if (typeof window !== "undefined") window.addEventListener("online", this.handleOnline);
3110
+ }
3111
+ /**
3112
+ * 저장소에 넣은 뒤 resolve 한다. 저장이 실패하면 메모리로만 진행한다.
3113
+ * reject 하지 않는다 — 이슈는 이미 생성됐다. 기록조차 만들지 못하면 이슈 id 로 abort 해
3114
+ * 서버가 이슈를 바로 failed 로 내리게 한다.
3115
+ * 기록은 호출한 틱에 생긴다(첫 await 전). 같은 이슈의 다른 업로드와 함께 넣을 때 이 순서에 기댄다.
3116
+ */
3117
+ async enqueue(input) {
3118
+ let record2;
3119
+ try {
3120
+ record2 = {
3121
+ id: crypto.randomUUID(),
3122
+ ...input,
3123
+ mimeType: stripMimeParameters(input.mimeType),
3124
+ createdAt: this.now()
3125
+ };
3126
+ } catch {
3127
+ await this.deps.api.abortUpload({ uploadId: null, issueId: input.issueId, mediaType: input.mediaType }).catch(() => void 0);
3128
+ this.emit({ type: "aborted", issueId: input.issueId, mediaType: input.mediaType });
3129
+ return;
3130
+ }
3131
+ this.records.set(record2.id, record2);
3132
+ this.setState(record2, "queued", 0);
3133
+ await this.persist(record2);
3134
+ this.kick();
3135
+ }
3136
+ /**
3137
+ * 저장소의 이 프로젝트 기록을 메모리로 가져와 붙잡아 둔다. 이미 돌고 있는 처리가 있어도
3138
+ * requeueFailed 가 이 프로젝트를 풀 때까지 가져온 기록은 시작하지 않는다. 새로 enqueue 한
3139
+ * 기록은 붙잡지 않는다.
3140
+ * 다른 탭이 잠금을 쥐고 올리는 기록은 가져오지도 세지도 않는다.
3141
+ * 새로 읽어 온 기록 중 만료되지 않은 수를 돌려준다.
3142
+ * 이어 올리기는 두 저장소를 모두 읽은 뒤 시작하는 `IssueUploadQueues.resume` 으로 한다.
3143
+ */
3144
+ async load(projectKey) {
3145
+ const held = await heldLockNames();
3146
+ const stored = await this.deps.store.list().catch(() => []);
3147
+ let resumable = 0;
3148
+ for (const record2 of stored) {
3149
+ if (record2.projectKey !== projectKey || this.records.has(record2.id) || this.finished.has(record2.id) || held.has(lockName(record2.id))) {
3150
+ continue;
3151
+ }
3152
+ this.persisted.add(record2.id);
3153
+ const normalized = { ...record2, mediaType: record2.mediaType ?? "video" };
3154
+ this.records.set(normalized.id, normalized);
3155
+ this.held.add(normalized.id);
3156
+ this.setState(normalized, "queued", 0);
3157
+ if (!this.isExpired(normalized)) resumable += 1;
3158
+ }
3159
+ return resumable;
3160
+ }
3161
+ /**
3162
+ * 모든 프로젝트·워크스페이스의 만료 기록을 저장소에서 지운다. 서버에는 알리지 않는다 —
3163
+ * 생성 24시간이 지난 업로드 중 이슈는 서버 cron 이 failed 로 내린다. 이 대기열이 들고 있는
3164
+ * 기록은 처리 순서에서 abort 하므로 건드리지 않는다. 지운 수를 돌려준다.
3165
+ */
3166
+ async pruneExpired() {
3167
+ const stored = await this.deps.store.list().catch(() => []);
3168
+ const expired = stored.filter(
3169
+ (record2) => this.isExpired(record2) && !this.records.has(record2.id) && !this.finished.has(record2.id)
3170
+ );
3171
+ await Promise.all(
3172
+ expired.map((record2) => this.deps.store.delete(record2.id).catch(() => void 0))
3173
+ );
3174
+ return expired.length;
3175
+ }
3176
+ /** 그 이슈의 실패 기록을 전부 다시 올린다. 첨부 대기열은 한 이슈에 리플레이·소켓 두 기록을 둔다. */
3177
+ retry(issueId) {
3178
+ var _a;
3179
+ let requeued = false;
3180
+ for (const record2 of this.records.values()) {
3181
+ if (record2.issueId !== issueId || ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) !== "failed") continue;
3182
+ this.failureNotified.delete(record2.id);
3183
+ this.setState(record2, "queued", 0);
3184
+ requeued = true;
3185
+ }
3186
+ if (requeued) this.kick();
3187
+ }
3188
+ /** 그 이슈에 아직 처리할(queued·uploading) 기록이 있는가. 실패로 멈춘 기록은 세지 않는다. */
3189
+ isPending(issueId) {
3190
+ return [...this.records.values()].some(
3191
+ (record2) => {
3192
+ var _a;
3193
+ return record2.issueId === issueId && ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) !== "failed";
3194
+ }
3195
+ );
3196
+ }
3197
+ subscribe(listener) {
3198
+ this.listeners.add(listener);
3199
+ callSafely(() => listener(this.snapshot()));
3200
+ return () => {
3201
+ this.listeners.delete(listener);
3202
+ };
3203
+ }
3204
+ hasActiveUploads() {
3205
+ return [...this.states.values()].some((state) => state.status !== "failed");
3206
+ }
3207
+ stop() {
3208
+ var _a;
3209
+ this.stopped = true;
3210
+ this.resolveStopped();
3211
+ if (typeof window !== "undefined") window.removeEventListener("online", this.handleOnline);
3212
+ (_a = this.cancelCurrent) == null ? void 0 : _a.call(this);
3213
+ this.listeners.clear();
3214
+ }
3215
+ /**
3216
+ * 기록을(projectKey 를 주면 그 프로젝트만) 처리 대상으로 돌린다. load 가 붙잡아 둔 기록을 풀고,
3217
+ * failed 로 멈춘 기록은 다시 대기시킨 뒤 처리를 시작한다.
3218
+ */
3219
+ requeueFailed(projectKey) {
3220
+ var _a;
3221
+ for (const record2 of this.records.values()) {
3222
+ if (projectKey !== void 0 && record2.projectKey !== projectKey) continue;
3223
+ this.held.delete(record2.id);
3224
+ if (((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) === "failed") this.setState(record2, "queued", 0);
3225
+ }
3226
+ this.kick();
3227
+ }
3228
+ kick() {
3229
+ if (this.running || this.stopped) return;
3230
+ this.running = true;
3231
+ void this.drain().finally(() => {
3232
+ this.running = false;
3233
+ if (this.nextQueued()) this.kick();
3234
+ });
3235
+ }
3236
+ nextQueued() {
3237
+ return [...this.records.values()].find(
3238
+ (record2) => {
3239
+ var _a;
3240
+ return !this.held.has(record2.id) && ((_a = this.states.get(record2.id)) == null ? void 0 : _a.status) === "queued";
3241
+ }
3242
+ );
3243
+ }
3244
+ async drain() {
3245
+ for (let record2 = this.nextQueued(); record2 && !this.stopped; record2 = this.nextQueued()) {
3246
+ await this.process(record2);
3247
+ }
3248
+ }
3249
+ /**
3250
+ * 저장소는 같은 사이트의 모든 탭이 함께 쓴다. 탭마다 resume 하면 같은 기록을 두 탭이 동시에
3251
+ * 올리게 되므로 기록 id 잠금을 쥔 탭만 처리한다. 잠금을 못 얻으면 이 탭 메모리에서만 내려놓고
3252
+ * 저장소 기록은 그대로 둔다. 탭이 닫히면 잠금이 풀리므로 다음 resume 에서 다시 가져간다.
3253
+ * navigator.locks 가 없거나 요청이 거절되는 환경(비보안 컨텍스트 등)은 잠금 없이 처리한다.
3254
+ */
3255
+ async process(record2) {
3256
+ const locks = lockManager();
3257
+ if (!locks) return this.processRecord(record2);
3258
+ let entered = false;
3259
+ try {
3260
+ await locks.request(lockName(record2.id), { ifAvailable: true }, async (lock) => {
3261
+ entered = true;
3262
+ if (!lock || !await this.refresh(record2)) {
3263
+ this.release(record2);
3264
+ return;
3265
+ }
3266
+ await Promise.race([this.processRecord(record2), this.whenStopped]);
3267
+ });
3268
+ } catch {
3269
+ if (!entered) await this.processRecord(record2);
3270
+ }
3271
+ }
3272
+ /**
3273
+ * 이 탭이 기록을 읽은 뒤 잠금을 얻기까지 다른 탭이 그 기록을 처리했을 수 있다. 저장소에서
3274
+ * 사라졌으면 다른 탭이 끝낸 것이라 false, 남아 있으면 그 탭이 받은 uploadId·이어 올릴 URL 을
3275
+ * 가져온다. 저장하지 못해 메모리에만 있는 기록과 저장소를 읽지 못한 경우는 그대로 진행한다.
3276
+ */
3277
+ async refresh(record2) {
3278
+ if (!this.persisted.has(record2.id)) return true;
3279
+ const fresh = await this.deps.store.load(record2.id).catch(() => void 0);
3280
+ if (fresh === null) return false;
3281
+ if (fresh) Object.assign(record2, fresh, { mediaType: fresh.mediaType ?? record2.mediaType });
3282
+ return true;
3283
+ }
3284
+ /** 다른 탭이 맡은 기록을 이 대기열에서만 뺀다. 알림·서버 호출·저장소 삭제는 하지 않는다. */
3285
+ release(record2) {
3286
+ this.failureNotified.delete(record2.id);
3287
+ this.persisted.delete(record2.id);
3288
+ this.records.delete(record2.id);
3289
+ this.states.delete(record2.id);
3290
+ this.notify();
3291
+ }
3292
+ async processRecord(record2) {
3293
+ var _a;
3294
+ if (this.isExpired(record2)) {
3295
+ await this.discard(record2);
3296
+ return;
3297
+ }
3298
+ this.setState(record2, "uploading", 0);
3299
+ try {
3300
+ await this.ensureInitialized(record2);
3301
+ await this.uploadWithResign(record2);
3302
+ this.throwIfStopped();
3303
+ await this.complete(record2);
3304
+ } catch (error) {
3305
+ if (this.stopped) return;
3306
+ if (error instanceof PermanentUploadError) {
3307
+ await this.discard(record2);
3308
+ return;
3309
+ }
3310
+ this.setState(record2, "failed", ((_a = this.states.get(record2.id)) == null ? void 0 : _a.bytesSent) ?? 0);
3311
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return;
3312
+ if (!this.failureNotified.has(record2.id)) {
3313
+ this.failureNotified.add(record2.id);
3314
+ this.emit({ type: "failed", issueId: record2.issueId, mediaType: record2.mediaType });
3315
+ }
3316
+ return;
3317
+ }
3318
+ if (this.stopped) return;
3319
+ await this.finish(record2, "completed");
3320
+ }
3321
+ async ensureInitialized(record2) {
3322
+ if (record2.uploadId) return;
3323
+ const checksumSha256 = await sha256Hex(record2.blob).catch((error) => {
3324
+ throw new PermanentUploadError(error instanceof Error ? error.message : String(error));
3325
+ });
3326
+ this.throwIfStopped();
3327
+ const init = await this.callApi(
3328
+ () => this.deps.api.initUpload({
3329
+ mediaType: record2.mediaType,
3330
+ issueId: record2.issueId,
3331
+ mimeType: record2.mimeType,
3332
+ sizeBytes: record2.blob.size,
3333
+ checksumSha256
3334
+ })
3335
+ );
3336
+ Object.assign(record2, init);
3337
+ await this.persistProgress(record2);
3338
+ }
3339
+ /**
3340
+ * complete 의 409 는 다른 요청(같은 기록을 이어 올린 다른 탭 등)이 먼저 연결해 상태가 바뀐 경우다.
3341
+ * 이미 이슈에 붙었으므로 완료로 본다.
3342
+ */
3343
+ async complete(record2) {
3344
+ try {
3345
+ await this.deps.api.completeUpload({
3346
+ uploadId: record2.uploadId,
3347
+ issueId: record2.issueId,
3348
+ mediaType: record2.mediaType,
3349
+ replayMeta: record2.replayMeta
3350
+ });
3351
+ } catch (error) {
3352
+ if (readStatus(error) === 409) return;
3353
+ throw toApiFailure(error);
3354
+ }
3355
+ }
3356
+ /** stop() 뒤에는 진행 중이던 처리도 서버를 더 부르지 않는다. 기록은 저장소에 남겨 다음 resume 에 맡긴다. */
3357
+ throwIfStopped() {
3358
+ if (this.stopped) throw new UploadStoppedError();
3359
+ }
3360
+ async callApi(call) {
3361
+ try {
3362
+ return await call();
3363
+ } catch (error) {
3364
+ throw toApiFailure(error);
3365
+ }
3366
+ }
3367
+ /** 업로드 토큰은 2시간이면 만료된다. 400·401·403 이면 같은 path 로 다시 서명받아 한 번만 이어 간다. */
3368
+ async uploadWithResign(record2) {
3369
+ try {
3370
+ await this.runTus(record2);
3371
+ return;
3372
+ } catch (error) {
3373
+ const status = readStatus(error);
3374
+ if (status === void 0 || !SIGNATURE_STATUSES.has(status)) throw toTusFailure(error);
3375
+ }
3376
+ let token;
3377
+ try {
3378
+ ({ token } = await this.deps.api.resignUpload(record2.uploadId));
3379
+ } catch (error) {
3380
+ if (readStatus(error) === 409) return;
3381
+ throw toApiFailure(error);
3382
+ }
3383
+ record2.token = token;
3384
+ await this.persistProgress(record2);
3385
+ try {
3386
+ await this.runTus(record2);
3387
+ } catch (error) {
3388
+ throw toTusFailure(error);
3389
+ }
3390
+ }
3391
+ async runTus(record2) {
3392
+ const { Upload } = await this.deps.loadTus();
3393
+ this.throwIfStopped();
3394
+ await new Promise((resolve, reject) => {
3395
+ const upload = new Upload(record2.blob, {
3396
+ endpoint: record2.resumableUrl,
3397
+ uploadUrl: record2.tusUploadUrl ?? null,
3398
+ chunkSize: TUS_CHUNK_SIZE,
3399
+ retryDelays: TUS_RETRY_DELAYS,
3400
+ // 이어 올릴 URL 은 우리 기록에 둔다. tus 의 localStorage 저장은 쓰지 않는다.
3401
+ storeFingerprintForResuming: false,
3402
+ headers: { "x-signature": record2.token ?? "", "x-client-info": this.deps.clientInfo },
3403
+ metadata: {
3404
+ bucketName: record2.bucket ?? "",
3405
+ objectName: record2.path ?? "",
3406
+ contentType: record2.mimeType,
3407
+ cacheControl: "3600"
3408
+ },
3409
+ onUploadUrlAvailable: () => {
3410
+ record2.tusUploadUrl = upload.url ?? void 0;
3411
+ void this.persistProgress(record2);
3412
+ },
3413
+ onProgress: (bytesSent, bytesTotal) => this.setState(record2, "uploading", bytesSent, bytesTotal),
3414
+ onSuccess: () => resolve(),
3415
+ onError: (error) => reject(error)
3416
+ });
3417
+ this.cancelCurrent = () => {
3418
+ void upload.abort(false);
3419
+ reject(new UploadStoppedError());
3420
+ };
3421
+ upload.start();
3422
+ }).finally(() => {
3423
+ this.cancelCurrent = null;
3424
+ });
3425
+ }
3426
+ async discard(record2) {
3427
+ await this.deps.api.abortUpload({
3428
+ uploadId: record2.uploadId ?? null,
3429
+ issueId: record2.issueId,
3430
+ mediaType: record2.mediaType
3431
+ }).catch(() => void 0);
3432
+ await this.finish(record2, "aborted");
3433
+ }
3434
+ /**
3435
+ * 끝낸 기록을 저장소에서 지운 뒤 메모리에서 빼고 알린다. 메모리에서 빼는 것과 알림 사이에
3436
+ * await 를 두지 않는다 — 알리미는 두 대기열의 메모리 기록으로 그 이슈가 끝났는지 보므로, 그
3437
+ * 사이 다른 대기열이 끝나면 양쪽이 서로 끝난 것으로 보고 따로 알린다.
3438
+ */
3439
+ async finish(record2, type) {
3440
+ this.finished.add(record2.id);
3441
+ await this.deps.store.delete(record2.id).catch(() => void 0);
3442
+ this.persisted.delete(record2.id);
3443
+ this.failureNotified.delete(record2.id);
3444
+ this.records.delete(record2.id);
3445
+ this.states.delete(record2.id);
3446
+ this.notify();
3447
+ this.emit({ type, issueId: record2.issueId, mediaType: record2.mediaType });
3448
+ }
3449
+ async persist(record2) {
3450
+ await this.deps.store.save(record2).then(
3451
+ () => this.persisted.add(record2.id),
3452
+ () => void 0
3453
+ );
3454
+ }
3455
+ /**
3456
+ * 처리 중에 받은 결과(init·재서명·이어 올릴 URL)를 저장한다. stop() 뒤에는 쓰지 않는다 —
3457
+ * 잠금을 넘겨받은 새 대기열이 저장한 내용을 덮어쓰지 않게 한다.
3458
+ */
3459
+ async persistProgress(record2) {
3460
+ if (this.stopped) return;
3461
+ await this.persist(record2);
3462
+ }
3463
+ isExpired(record2) {
3464
+ return this.now() - record2.createdAt >= UPLOAD_MAX_AGE_MS;
3465
+ }
3466
+ setState(record2, status, bytesSent, bytesTotal = record2.blob.size) {
3467
+ this.states.set(record2.id, {
3468
+ issueId: record2.issueId,
3469
+ mediaType: record2.mediaType,
3470
+ status,
3471
+ bytesSent,
3472
+ bytesTotal
3473
+ });
3474
+ this.notify();
3475
+ }
3476
+ snapshot() {
3477
+ return [...this.records.keys()].flatMap((id) => {
3478
+ const state = this.states.get(id);
3479
+ return state ? [state] : [];
3480
+ });
3481
+ }
3482
+ notify() {
3483
+ const states = this.snapshot();
3484
+ this.listeners.forEach((listener) => callSafely(() => listener(states)));
3485
+ }
3486
+ emit(event) {
3487
+ callSafely(() => {
3488
+ var _a, _b;
3489
+ return (_b = (_a = this.deps).onEvent) == null ? void 0 : _b.call(_a, event);
3490
+ });
3491
+ }
3492
+ }
3493
+ const VIDEO_UPLOAD_DB_NAME = "issue-sticker-video-uploads";
3494
+ const ATTACHMENT_UPLOAD_DB_NAME = "issue-sticker-attachment-uploads";
3495
+ const DB_VERSION = 1;
3496
+ const STORE_NAME = "uploads";
3497
+ function openDatabase(dbName) {
3498
+ return new Promise((resolve, reject) => {
3499
+ const request = indexedDB.open(dbName, DB_VERSION);
3500
+ request.onupgradeneeded = () => {
3501
+ if (!request.result.objectStoreNames.contains(STORE_NAME)) {
3502
+ request.result.createObjectStore(STORE_NAME, { keyPath: "id" });
3503
+ }
3504
+ };
3505
+ request.onsuccess = () => {
3506
+ const db = request.result;
3507
+ db.onversionchange = () => db.close();
3508
+ resolve(db);
3509
+ };
3510
+ request.onerror = () => reject(request.error);
3511
+ request.onblocked = () => reject(new Error("indexedDB open blocked"));
3512
+ });
3513
+ }
3514
+ async function withStore(dbName, mode, run) {
3515
+ const db = await openDatabase(dbName);
3516
+ try {
3517
+ return await new Promise((resolve, reject) => {
3518
+ const tx = db.transaction(STORE_NAME, mode);
3519
+ const request = run(tx.objectStore(STORE_NAME));
3520
+ tx.oncomplete = () => resolve(request.result);
3521
+ tx.onabort = () => reject(tx.error ?? request.error ?? new Error("indexedDB aborted"));
3522
+ tx.onerror = () => reject(tx.error ?? request.error);
3523
+ });
3524
+ } finally {
3525
+ db.close();
3526
+ }
3527
+ }
3528
+ function createIndexedDbPendingUploadStore(dbName) {
3529
+ return {
3530
+ async load(id) {
3531
+ const found = await withStore(
3532
+ dbName,
3533
+ "readonly",
3534
+ (store) => store.get(id)
3535
+ );
3536
+ return found ?? null;
3537
+ },
3538
+ async save(record2) {
3539
+ await withStore(dbName, "readwrite", (store) => store.put(record2));
3540
+ },
3541
+ async delete(id) {
3542
+ await withStore(dbName, "readwrite", (store) => store.delete(id));
3543
+ },
3544
+ async list() {
3545
+ const all = await withStore(dbName, "readonly", (store) => store.getAll());
3546
+ return all.sort((a2, b) => a2.createdAt - b.createdAt);
3547
+ }
3548
+ };
3549
+ }
3550
+ const loadTusClient = () => import("./index-DA6SoSp7.js");
3551
+ class IssueUploadNotifier {
3552
+ constructor(deps) {
3553
+ this.deps = deps;
3554
+ this.outcomes = /* @__PURE__ */ new Map();
3555
+ }
3556
+ handle(event) {
3557
+ const notice = this.outcomes.get(event.issueId) ?? {
3558
+ issueId: event.issueId,
3559
+ completed: [],
3560
+ failed: [],
3561
+ aborted: []
3562
+ };
3563
+ notice[event.type].push(event.mediaType);
3564
+ this.outcomes.set(event.issueId, notice);
3565
+ if (this.deps.isPending(event.issueId)) return;
3566
+ this.outcomes.delete(event.issueId);
3567
+ try {
3568
+ this.deps.onNotice(notice);
3569
+ } catch {
3570
+ }
3571
+ }
3572
+ }
3573
+ function createIssueUploadQueues(deps) {
3574
+ const { onEvent, onNotice } = deps;
3575
+ const notifier = onNotice ? new IssueUploadNotifier({ isPending: (issueId) => isPending(issueId), onNotice }) : null;
3576
+ const create = (store) => new MediaUploadQueue({
3577
+ api: deps.api,
3578
+ store,
3579
+ loadTus: deps.loadTus,
3580
+ clientInfo: deps.clientInfo,
3581
+ onEvent: (event) => {
3582
+ onEvent == null ? void 0 : onEvent(event);
3583
+ notifier == null ? void 0 : notifier.handle(event);
3584
+ }
3585
+ });
3586
+ const video = create(deps.videoStore);
3587
+ const attachments = create(deps.attachmentStore);
3588
+ const isPending = (issueId) => video.isPending(issueId) || attachments.isPending(issueId);
3589
+ return {
3590
+ video,
3591
+ attachments,
3592
+ async resume(projectKeys) {
3593
+ const [videoCounts] = await Promise.all([
3594
+ Promise.all(projectKeys.map((key) => video.load(key))),
3595
+ Promise.all(projectKeys.map((key) => attachments.load(key)))
3596
+ ]);
3597
+ for (const key of projectKeys) {
3598
+ video.requeueFailed(key);
3599
+ attachments.requeueFailed(key);
3600
+ }
3601
+ return videoCounts.reduce((sum, count2) => sum + count2, 0);
3602
+ },
3603
+ retry(issueId) {
3604
+ video.retry(issueId);
3605
+ attachments.retry(issueId);
3606
+ },
3607
+ stop() {
3608
+ video.stop();
3609
+ attachments.stop();
3610
+ },
3611
+ isPending
3612
+ };
3613
+ }
3614
+ let nextId = 0;
3615
+ let toasts = [];
3616
+ const listeners = /* @__PURE__ */ new Set();
3617
+ function emit() {
3618
+ listeners.forEach((l2) => l2());
3619
+ }
3620
+ function addToast(message2, type, options) {
3621
+ const id = `toast-${++nextId}`;
3622
+ const t2 = {
3623
+ id,
3624
+ message: message2,
3625
+ type,
3626
+ title: options == null ? void 0 : options.title,
3627
+ action: options == null ? void 0 : options.action,
3628
+ duration: (options == null ? void 0 : options.duration) ?? 2e3,
3629
+ createdAt: Date.now()
3630
+ };
3631
+ toasts = [...toasts, t2];
3632
+ emit();
3633
+ return id;
3634
+ }
3635
+ function dismiss(id) {
3636
+ toasts = toasts.filter((t2) => t2.id !== id);
3637
+ emit();
3638
+ }
3639
+ function dismissAll() {
3640
+ toasts = [];
3641
+ emit();
3642
+ }
3643
+ function toast(message2, options) {
3644
+ return addToast(message2, "default", options);
3645
+ }
3646
+ toast.success = (message2, options) => addToast(message2, "success", options);
3647
+ toast.warning = (message2, options) => addToast(message2, "warning", options);
3648
+ toast.error = (message2, options) => addToast(message2, "error", options);
3649
+ toast.info = (message2, options) => addToast(message2, "info", options);
3650
+ toast.dismiss = dismiss;
3651
+ toast.dismissAll = dismissAll;
3652
+ function subscribe(listener) {
3653
+ listeners.add(listener);
3654
+ return () => listeners.delete(listener);
3655
+ }
3656
+ function getSnapshot() {
3657
+ return toasts;
3658
+ }
3659
+ function dropOldestFrames(connections, bytesToDrop) {
3660
+ const order = connections.flatMap(
3661
+ (connection, index) => connection.frames.map((frame) => {
3662
+ var _a;
3663
+ return { index, t: frame.t, bytes: ((_a = frame.data) == null ? void 0 : _a.length) ?? 0 };
3664
+ })
3665
+ ).sort((a2, b) => a2.t - b.t);
3666
+ const dropCount = connections.map(() => 0);
3667
+ const dropBytes = connections.map(() => 0);
3668
+ let removed = 0;
3669
+ for (const entry of order) {
3670
+ if (removed >= bytesToDrop) break;
3671
+ dropCount[entry.index] += 1;
3672
+ dropBytes[entry.index] += entry.bytes;
3673
+ removed += entry.bytes;
3674
+ }
3675
+ return connections.map((connection, index) => {
3676
+ const count2 = dropCount[index];
3677
+ if (count2 === 0) return connection;
3678
+ return {
3679
+ ...connection,
3680
+ frames: connection.frames.slice(count2),
3681
+ droppedFrames: connection.droppedFrames + count2,
3682
+ droppedBytes: connection.droppedBytes + dropBytes[index]
3683
+ };
3684
+ });
3685
+ }
3686
+ const FIT_ATTEMPTS = 6;
3687
+ async function serializeSocketTranscript(connections, maxBytes = SOCKET_TRANSCRIPT_MAX_BYTES) {
3688
+ const frameCount = connections.reduce(
3689
+ (sum, connection) => sum + connection.frames.length + connection.droppedFrames,
3690
+ 0
3691
+ );
3692
+ if (connections.length === 0 || frameCount === 0) return null;
3693
+ let current = connections;
3694
+ let payload = gzip_1(JSON.stringify({ connections: current }));
3695
+ for (let attempt = 0; attempt < FIT_ATTEMPTS && payload.byteLength > maxBytes; attempt += 1) {
3696
+ const storedBytes = current.reduce(
3697
+ (sum, connection) => sum + storedFrameBytes(connection.frames),
3698
+ 0
3699
+ );
3700
+ if (storedBytes === 0) break;
3701
+ const overRatio = 1 - maxBytes / payload.byteLength;
3702
+ current = dropOldestFrames(current, storedBytes * Math.min(1, overRatio + 0.1));
3703
+ payload = gzip_1(JSON.stringify({ connections: current }));
2927
3704
  }
3705
+ return {
3706
+ payload,
3707
+ sizeBytes: payload.byteLength,
3708
+ connectionCount: connections.length,
3709
+ frameCount
3710
+ };
2928
3711
  }
2929
- const ISSUE_STICKER_INTERNAL_LOG_PREFIX = "__ISSUE_STICKER_INTERNAL_DEBUG__";
2930
- function isInternalDebugLog$1(message2) {
2931
- return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
2932
- }
2933
- function isIssueStickerInternalConsoleLog$1(log) {
2934
- return isInternalDebugLog$1(log.message);
3712
+ const ATTACHMENT_MIME_TYPE = "application/gzip";
3713
+ function toSessionReplayMeta(replay) {
3714
+ const marker = readReleaseMarker();
3715
+ return {
3716
+ window_seconds: replay.windowSeconds,
3717
+ event_count: replay.eventCount,
3718
+ size_bytes: replay.sizeBytes,
3719
+ rrweb_version: replay.rrwebVersion,
3720
+ privacy_config_version: replay.privacyConfigVersion,
3721
+ ...marker ? { release: marker.release, release_source: marker.release_source } : {}
3722
+ };
2935
3723
  }
2936
- const __vite_import_meta_env__ = { "DEV": false };
2937
- function isDevLoggingEnabled() {
2938
- const env = __vite_import_meta_env__;
2939
- {
2940
- return env.DEV;
3724
+ function toAttachmentUploads(input) {
3725
+ const uploads = [];
3726
+ const { replay, socket } = input;
3727
+ if (replay && replay.eventCount > 0) {
3728
+ uploads.push({
3729
+ mediaType: "replay",
3730
+ bytes: replay.payload,
3731
+ mimeType: ATTACHMENT_MIME_TYPE,
3732
+ replayMeta: toSessionReplayMeta(replay)
3733
+ });
3734
+ }
3735
+ if (socket && socket.sizeBytes <= SOCKET_TRANSCRIPT_MAX_BYTES) {
3736
+ uploads.push({ mediaType: "socket", bytes: socket.payload, mimeType: ATTACHMENT_MIME_TYPE });
2941
3737
  }
3738
+ return uploads;
2942
3739
  }
2943
- function isInternalDebugLog(message2) {
2944
- return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
3740
+ async function collectAttachmentUploads(input) {
3741
+ const { takeReplay, getConnections, onError } = input;
3742
+ const attempt = (kind, run) => Promise.resolve().then(run).catch((error) => {
3743
+ onError == null ? void 0 : onError(kind, error);
3744
+ return null;
3745
+ });
3746
+ const replay = takeReplay ? await attempt("replay", takeReplay) : null;
3747
+ const socket = await attempt(
3748
+ "socket",
3749
+ async () => serializeSocketTranscript(await getConnections())
3750
+ );
3751
+ return toAttachmentUploads({ replay, socket });
2945
3752
  }
2946
- function write(method, scope, args) {
2947
- if (!isDevLoggingEnabled()) {
2948
- return;
2949
- }
2950
- const log = console[method];
2951
- log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
3753
+ function hasReplayUpload(uploads) {
3754
+ return uploads.some((upload) => upload.mediaType === "replay");
2952
3755
  }
2953
- const devLogger = {
2954
- log(scope, ...args) {
2955
- write("log", scope, args);
2956
- },
2957
- info(scope, ...args) {
2958
- write("info", scope, args);
2959
- },
2960
- warn(scope, ...args) {
2961
- write("warn", scope, args);
2962
- },
2963
- error(scope, ...args) {
2964
- write("error", scope, args);
2965
- },
2966
- debug(scope, ...args) {
2967
- write("debug", scope, args);
3756
+ async function enqueueIssueUploads(input) {
3757
+ const { issueId, projectKey } = input;
3758
+ const pending = [];
3759
+ if (input.video) {
3760
+ pending.push(
3761
+ input.videoQueue.enqueue({ issueId, projectKey, mediaType: "video", ...input.video })
3762
+ );
2968
3763
  }
2969
- };
3764
+ for (const attachment of input.attachments) {
3765
+ pending.push(
3766
+ input.attachmentQueue.enqueue({
3767
+ issueId,
3768
+ projectKey,
3769
+ mediaType: attachment.mediaType,
3770
+ blob: new Blob([attachment.bytes], { type: attachment.mimeType }),
3771
+ mimeType: attachment.mimeType,
3772
+ replayMeta: attachment.replayMeta
3773
+ })
3774
+ );
3775
+ }
3776
+ await Promise.all(pending);
3777
+ }
2970
3778
  class DirectUploadError extends Error {
2971
3779
  constructor(message2, status, data) {
2972
3780
  super(message2);
@@ -3014,7 +3822,8 @@ async function directUploadToSignedUrl(signedUrl, file, contentType) {
3014
3822
  const response = await fetch(signedUrl, {
3015
3823
  method: "PUT",
3016
3824
  headers: {
3017
- "Content-Type": contentType
3825
+ "Content-Type": contentType,
3826
+ "x-client-info": "issue-sticker-sdk"
3018
3827
  },
3019
3828
  body: file
3020
3829
  });
@@ -3066,6 +3875,83 @@ async function uploadSignedMediaWithApi(api, file, mediaType, options = {}) {
3066
3875
  url: publicUrl
3067
3876
  };
3068
3877
  }
3878
+ function createMediaUploadApi(api) {
3879
+ return {
3880
+ async initUpload({ mediaType, issueId, mimeType, sizeBytes, checksumSha256 }) {
3881
+ const { uploads } = await api.post(
3882
+ "/api/media/uploads/init",
3883
+ { files: [{ mediaType, mimeType, sizeBytes, checksumSha256, issueId }] }
3884
+ );
3885
+ const upload = uploads[0];
3886
+ if (!(upload == null ? void 0 : upload.id) || !upload.resumableUrl) {
3887
+ throw new Error("업로드 URL 발급 응답이 올바르지 않습니다.");
3888
+ }
3889
+ return {
3890
+ uploadId: upload.id,
3891
+ bucket: upload.bucket,
3892
+ path: upload.path,
3893
+ token: upload.token,
3894
+ resumableUrl: upload.resumableUrl
3895
+ };
3896
+ },
3897
+ resignUpload: (uploadId) => api.post(`/api/media/uploads/${encodeURIComponent(uploadId)}/resign`),
3898
+ async completeUpload({ uploadId, issueId, replayMeta }) {
3899
+ await api.post("/api/media/uploads/complete", {
3900
+ uploadIds: [uploadId],
3901
+ issueId,
3902
+ ...replayMeta ? { replayMeta } : {}
3903
+ });
3904
+ },
3905
+ async abortUpload({ uploadId, issueId, mediaType }) {
3906
+ await api.post("/api/media/uploads/abort", {
3907
+ issueId,
3908
+ mediaType,
3909
+ ...uploadId ? { uploadId } : {}
3910
+ });
3911
+ }
3912
+ };
3913
+ }
3914
+ const ISSUE_STICKER_INTERNAL_LOG_PREFIX = "__ISSUE_STICKER_INTERNAL_DEBUG__";
3915
+ function isInternalDebugLog$1(message2) {
3916
+ return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
3917
+ }
3918
+ function isIssueStickerInternalConsoleLog$1(log) {
3919
+ return isInternalDebugLog$1(log.message);
3920
+ }
3921
+ const __vite_import_meta_env__ = { "DEV": false };
3922
+ function isDevLoggingEnabled() {
3923
+ const env = __vite_import_meta_env__;
3924
+ {
3925
+ return env.DEV;
3926
+ }
3927
+ }
3928
+ function isInternalDebugLog(message2) {
3929
+ return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
3930
+ }
3931
+ function write(method, scope, args) {
3932
+ if (!isDevLoggingEnabled()) {
3933
+ return;
3934
+ }
3935
+ const log = console[method];
3936
+ log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
3937
+ }
3938
+ const devLogger = {
3939
+ log(scope, ...args) {
3940
+ write("log", scope, args);
3941
+ },
3942
+ info(scope, ...args) {
3943
+ write("info", scope, args);
3944
+ },
3945
+ warn(scope, ...args) {
3946
+ write("warn", scope, args);
3947
+ },
3948
+ error(scope, ...args) {
3949
+ write("error", scope, args);
3950
+ },
3951
+ debug(scope, ...args) {
3952
+ write("debug", scope, args);
3953
+ }
3954
+ };
3069
3955
  function _mergeNamespaces$1(e2, t2) {
3070
3956
  return t2.forEach((function(t3) {
3071
3957
  t3 && "string" != typeof t3 && !Array.isArray(t3) && Object.keys(t3).forEach((function(r2) {
@@ -5435,6 +6321,7 @@ class NetworkCollector {
5435
6321
  var _a;
5436
6322
  if (!this.running) return;
5437
6323
  if (this.isServiceRequest(log.url)) return;
6324
+ if (hasSelfTrafficHeader(log.requestHeaders)) return;
5438
6325
  this.logs.push(log);
5439
6326
  if (this.logs.length > NETWORK_LOG_CLIENT_BUFFER_SIZE) {
5440
6327
  this.logs = this.logs.slice(-NETWORK_LOG_CLIENT_BUFFER_SIZE);
@@ -5649,64 +6536,6 @@ class NetworkCollector {
5649
6536
  this.patchedXHR = null;
5650
6537
  }
5651
6538
  }
5652
- async function sha256Hex(bytes) {
5653
- const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
5654
- return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
5655
- }
5656
- function dropOldestFrames(connections, bytesToDrop) {
5657
- const order = connections.flatMap(
5658
- (connection, index) => connection.frames.map((frame) => {
5659
- var _a;
5660
- return { index, t: frame.t, bytes: ((_a = frame.data) == null ? void 0 : _a.length) ?? 0 };
5661
- })
5662
- ).sort((a2, b) => a2.t - b.t);
5663
- const dropCount = connections.map(() => 0);
5664
- const dropBytes = connections.map(() => 0);
5665
- let removed = 0;
5666
- for (const entry of order) {
5667
- if (removed >= bytesToDrop) break;
5668
- dropCount[entry.index] += 1;
5669
- dropBytes[entry.index] += entry.bytes;
5670
- removed += entry.bytes;
5671
- }
5672
- return connections.map((connection, index) => {
5673
- const count2 = dropCount[index];
5674
- if (count2 === 0) return connection;
5675
- return {
5676
- ...connection,
5677
- frames: connection.frames.slice(count2),
5678
- droppedFrames: connection.droppedFrames + count2,
5679
- droppedBytes: connection.droppedBytes + dropBytes[index]
5680
- };
5681
- });
5682
- }
5683
- const FIT_ATTEMPTS = 6;
5684
- async function serializeSocketTranscript(connections, maxBytes = SOCKET_TRANSCRIPT_MAX_BYTES) {
5685
- const frameCount = connections.reduce(
5686
- (sum, connection) => sum + connection.frames.length + connection.droppedFrames,
5687
- 0
5688
- );
5689
- if (connections.length === 0 || frameCount === 0) return null;
5690
- let current = connections;
5691
- let payload = gzip_1(JSON.stringify({ connections: current }));
5692
- for (let attempt = 0; attempt < FIT_ATTEMPTS && payload.byteLength > maxBytes; attempt += 1) {
5693
- const storedBytes = current.reduce(
5694
- (sum, connection) => sum + storedFrameBytes(connection.frames),
5695
- 0
5696
- );
5697
- if (storedBytes === 0) break;
5698
- const overRatio = 1 - maxBytes / payload.byteLength;
5699
- current = dropOldestFrames(current, storedBytes * Math.min(1, overRatio + 0.1));
5700
- payload = gzip_1(JSON.stringify({ connections: current }));
5701
- }
5702
- return {
5703
- payload,
5704
- checksumSha256: await sha256Hex(payload),
5705
- sizeBytes: payload.byteLength,
5706
- connectionCount: connections.length,
5707
- frameCount
5708
- };
5709
- }
5710
6539
  var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
5711
6540
  NetworkLogType2["MAIN_FRAME"] = "main_frame";
5712
6541
  NetworkLogType2["SUB_FRAME"] = "sub_frame";
@@ -9112,6 +9941,8 @@ var Trigger = PopoverTrigger$1;
9112
9941
  var Portal = PopoverPortal;
9113
9942
  var Content2 = PopoverContent$1;
9114
9943
  const FLOATING_LAYER_SELECTOR = "[data-radix-popper-content-wrapper]";
9944
+ const OUTSIDE_EVENTS = ["pointerdown", "focusin"];
9945
+ const roots = /* @__PURE__ */ new Map();
9115
9946
  function isInsideLayer(path, content) {
9116
9947
  return path.some((node) => {
9117
9948
  if (!(node instanceof Element)) return false;
@@ -9120,7 +9951,48 @@ function isInsideLayer(path, content) {
9120
9951
  return node.matches(FLOATING_LAYER_SELECTOR) && !node.contains(content);
9121
9952
  });
9122
9953
  }
9123
- function useShadowOutsideDismiss(onDismiss) {
9954
+ function dismissTopLayer(root, event) {
9955
+ var _a;
9956
+ const layers = (_a = roots.get(root)) == null ? void 0 : _a.layers;
9957
+ const top = layers == null ? void 0 : layers[layers.length - 1];
9958
+ if (!top) return;
9959
+ if (event.type === "focusin" && !top.focusOutside) return;
9960
+ if (isInsideLayer(event.composedPath(), top.content)) return;
9961
+ top.dismiss();
9962
+ }
9963
+ function listen(root) {
9964
+ const doc = root.ownerDocument;
9965
+ const onRoot = (event) => dismissTopLayer(root, event);
9966
+ const onDocument = (event) => {
9967
+ if (!event.composedPath().includes(root)) dismissTopLayer(root, event);
9968
+ };
9969
+ OUTSIDE_EVENTS.forEach((type) => {
9970
+ root.addEventListener(type, onRoot);
9971
+ doc.addEventListener(type, onDocument);
9972
+ });
9973
+ return () => OUTSIDE_EVENTS.forEach((type) => {
9974
+ root.removeEventListener(type, onRoot);
9975
+ doc.removeEventListener(type, onDocument);
9976
+ });
9977
+ }
9978
+ function registerLayer(root, layer) {
9979
+ let entry = roots.get(root);
9980
+ if (!entry) {
9981
+ entry = { layers: [], detach: listen(root) };
9982
+ roots.set(root, entry);
9983
+ }
9984
+ entry.layers.push(layer);
9985
+ return () => {
9986
+ const current = roots.get(root);
9987
+ if (!current) return;
9988
+ current.layers = current.layers.filter((item) => item !== layer);
9989
+ if (current.layers.length === 0) {
9990
+ current.detach();
9991
+ roots.delete(root);
9992
+ }
9993
+ };
9994
+ }
9995
+ function useShadowOutsideDismiss(onDismiss, { focusOutside = true } = {}) {
9124
9996
  const onDismissRef = useRef(onDismiss);
9125
9997
  const detachRef = useRef(null);
9126
9998
  useEffect(() => {
@@ -9130,45 +10002,67 @@ function useShadowOutsideDismiss(onDismiss) {
9130
10002
  var _a;
9131
10003
  return (_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
9132
10004
  }, []);
9133
- return useCallback((content) => {
9134
- var _a;
9135
- (_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
9136
- detachRef.current = null;
9137
- if (!content) return;
9138
- const root = content.getRootNode();
9139
- if (!(root instanceof ShadowRoot)) return;
9140
- const handle = (event) => {
9141
- var _a2;
9142
- if (isInsideLayer(event.composedPath(), content)) return;
9143
- (_a2 = onDismissRef.current) == null ? void 0 : _a2.call(onDismissRef);
9144
- };
9145
- const types = ["pointerdown", "focusin"];
9146
- types.forEach((type) => root.addEventListener(type, handle));
9147
- detachRef.current = () => types.forEach((type) => root.removeEventListener(type, handle));
9148
- }, []);
10005
+ return useCallback(
10006
+ (content) => {
10007
+ var _a;
10008
+ (_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
10009
+ detachRef.current = null;
10010
+ if (!content) return;
10011
+ const root = content.getRootNode();
10012
+ if (!(root instanceof ShadowRoot)) return;
10013
+ detachRef.current = registerLayer(root, {
10014
+ content,
10015
+ focusOutside,
10016
+ dismiss: () => {
10017
+ var _a2;
10018
+ return (_a2 = onDismissRef.current) == null ? void 0 : _a2.call(onDismissRef);
10019
+ }
10020
+ });
10021
+ },
10022
+ [focusOutside]
10023
+ );
9149
10024
  }
9150
- const PopoverOpenChangeContext = React.createContext(
9151
- void 0
9152
- );
9153
- function Popover(props) {
9154
- return /* @__PURE__ */ jsx(PopoverOpenChangeContext.Provider, { value: props.onOpenChange, children: /* @__PURE__ */ jsx(Root2, { ...props }) });
10025
+ function createShadowDismissRoot(Root3, options) {
10026
+ const DismissContext = React.createContext(void 0);
10027
+ function ShadowDismissRoot({ open, defaultOpen = false, onOpenChange, ...rest }) {
10028
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
10029
+ const isControlled = open !== void 0;
10030
+ const setOpen = React.useCallback(
10031
+ (next) => {
10032
+ if (!isControlled) setUncontrolledOpen(next);
10033
+ onOpenChange == null ? void 0 : onOpenChange(next);
10034
+ },
10035
+ [isControlled, onOpenChange]
10036
+ );
10037
+ const dismiss2 = React.useCallback(() => setOpen(false), [setOpen]);
10038
+ return /* @__PURE__ */ jsx(DismissContext.Provider, { value: dismiss2, children: /* @__PURE__ */ jsx(
10039
+ Root3,
10040
+ {
10041
+ ...rest,
10042
+ open: isControlled ? open : uncontrolledOpen,
10043
+ onOpenChange: setOpen
10044
+ }
10045
+ ) });
10046
+ }
10047
+ function useContentDismissRef(forwardedRef) {
10048
+ const dismissRef = useShadowOutsideDismiss(React.useContext(DismissContext), options);
10049
+ return React.useCallback(
10050
+ (node) => {
10051
+ dismissRef(node);
10052
+ if (typeof forwardedRef === "function") forwardedRef(node);
10053
+ else if (forwardedRef) forwardedRef.current = node;
10054
+ },
10055
+ [dismissRef, forwardedRef]
10056
+ );
10057
+ }
10058
+ return [ShadowDismissRoot, useContentDismissRef];
9155
10059
  }
10060
+ const [Popover, usePopoverContentDismissRef] = createShadowDismissRoot(Root2);
9156
10061
  const PopoverTrigger = Trigger;
9157
10062
  const PopoverAnchor = Anchor2;
9158
10063
  const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 6, onEscapeKeyDown, ...props }, ref) => {
9159
10064
  const portalContainer = usePortalContainer();
9160
- const onOpenChange = React.useContext(PopoverOpenChangeContext);
9161
- const dismissRef = useShadowOutsideDismiss(
9162
- React.useCallback(() => onOpenChange == null ? void 0 : onOpenChange(false), [onOpenChange])
9163
- );
9164
- const contentRef = React.useCallback(
9165
- (node) => {
9166
- dismissRef(node);
9167
- if (typeof ref === "function") ref(node);
9168
- else if (ref) ref.current = node;
9169
- },
9170
- [dismissRef, ref]
9171
- );
10065
+ const contentRef = usePopoverContentDismissRef(ref);
9172
10066
  return /* @__PURE__ */ jsx(Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
9173
10067
  Content2,
9174
10068
  {
@@ -10554,7 +11448,9 @@ const X = createLucideIcon("X", [
10554
11448
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
10555
11449
  ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
10556
11450
  ]);
10557
- const Select$1 = Select$2;
11451
+ const [Select$1, useSelectContentDismissRef] = createShadowDismissRoot(Select$2, {
11452
+ focusOutside: false
11453
+ });
10558
11454
  const SelectValue = SelectValue$1;
10559
11455
  const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
10560
11456
  SelectTrigger$1,
@@ -10606,10 +11502,11 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
10606
11502
  SelectScrollDownButton.displayName = SelectScrollDownButton$1.displayName;
10607
11503
  const SelectContent = React.forwardRef(({ className, children, position = "popper", onEscapeKeyDown, ...props }, ref) => {
10608
11504
  const portalContainer = usePortalContainer();
11505
+ const contentRef = useSelectContentDismissRef(ref);
10609
11506
  return /* @__PURE__ */ jsx(SelectPortal, { container: portalContainer, children: /* @__PURE__ */ jsxs(
10610
11507
  SelectContent$1,
10611
11508
  {
10612
- ref,
11509
+ ref: contentRef,
10613
11510
  "data-testid": "select-content",
10614
11511
  sideOffset: 6,
10615
11512
  onEscapeKeyDown: stopEscapePropagation(onEscapeKeyDown),
@@ -16311,50 +17208,6 @@ function useOnboardingFlow() {
16311
17208
  [context]
16312
17209
  );
16313
17210
  }
16314
- let nextId = 0;
16315
- let toasts = [];
16316
- const listeners = /* @__PURE__ */ new Set();
16317
- function emit() {
16318
- listeners.forEach((l2) => l2());
16319
- }
16320
- function addToast(message2, type, options) {
16321
- const id = `toast-${++nextId}`;
16322
- const t2 = {
16323
- id,
16324
- message: message2,
16325
- type,
16326
- action: options == null ? void 0 : options.action,
16327
- duration: (options == null ? void 0 : options.duration) ?? 2e3,
16328
- createdAt: Date.now()
16329
- };
16330
- toasts = [...toasts, t2];
16331
- emit();
16332
- return id;
16333
- }
16334
- function dismiss(id) {
16335
- toasts = toasts.filter((t2) => t2.id !== id);
16336
- emit();
16337
- }
16338
- function dismissAll() {
16339
- toasts = [];
16340
- emit();
16341
- }
16342
- function toast(message2, options) {
16343
- return addToast(message2, "default", options);
16344
- }
16345
- toast.success = (message2, options) => addToast(message2, "success", options);
16346
- toast.warning = (message2, options) => addToast(message2, "warning", options);
16347
- toast.error = (message2, options) => addToast(message2, "error", options);
16348
- toast.info = (message2, options) => addToast(message2, "info", options);
16349
- toast.dismiss = dismiss;
16350
- toast.dismissAll = dismissAll;
16351
- function subscribe(listener) {
16352
- listeners.add(listener);
16353
- return () => listeners.delete(listener);
16354
- }
16355
- function getSnapshot() {
16356
- return toasts;
16357
- }
16358
17211
  ({
16359
17212
  type: [NetworkLogType.XMLHTTPREQUEST, NetworkLogType.FETCH]
16360
17213
  });
@@ -16496,20 +17349,6 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
16496
17349
  return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
16497
17350
  });
16498
17351
  }
16499
- const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
16500
- async function uploadSocketTranscript(api, snapshot, options = {}) {
16501
- if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
16502
- try {
16503
- const file = new File([snapshot.payload], "socket-transcript.json.gz", {
16504
- type: TRANSCRIPT_CONTENT_TYPE
16505
- });
16506
- const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
16507
- return { uploadId: uploaded.id };
16508
- } catch (error) {
16509
- devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
16510
- return null;
16511
- }
16512
- }
16513
17352
  function buildExternalUserPayload(user) {
16514
17353
  if (!user) return {};
16515
17354
  return {
@@ -16518,103 +17357,115 @@ function buildExternalUserPayload(user) {
16518
17357
  };
16519
17358
  }
16520
17359
  export {
16521
- Portal as $,
16522
- trimLogBackupPayload as A,
16523
- createSafeLogBackup as B,
17360
+ SelectValue as $,
17361
+ SessionReplayCaptureController as A,
17362
+ preloadModernScreenshot as B,
16524
17363
  ConsoleCollector as C,
16525
- getLogBackupSessionId as D,
16526
- createIndexedDbLogBackup as E,
16527
- PopoverTrigger as F,
16528
- Input as G,
16529
- PopoverContent as H,
17364
+ enqueueIssueUploads as D,
17365
+ normalizeUploadMimeType as E,
17366
+ toSessionReplayMeta as F,
17367
+ loadTusClient as G,
17368
+ createIndexedDbPendingUploadStore as H,
16530
17369
  ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
16531
- Select$1 as J,
16532
- SelectTrigger as K,
16533
- Label as L,
16534
- SelectValue as M,
17370
+ ATTACHMENT_UPLOAD_DB_NAME as J,
17371
+ createMediaUploadApi as K,
17372
+ toast as L,
17373
+ trimLogBackupPayload as M,
16535
17374
  NetworkCollector as N,
16536
- SelectContent as O,
16537
- Popover as P,
16538
- SelectItem as Q,
16539
- JIRA_ISSUE_TYPE_VALUE_KEY as R,
17375
+ createSafeLogBackup as O,
17376
+ getLogBackupSessionId as P,
17377
+ createIndexedDbLogBackup as Q,
17378
+ Popover as R,
16540
17379
  ScreenRecordingController as S,
16541
- getJiraMetadata as T,
16542
- buildJiraIssueFields as U,
16543
- getJiraIssueTypeLabel as V,
17380
+ PopoverTrigger as T,
17381
+ Input as U,
17382
+ VIDEO_UPLOAD_DB_NAME as V,
16544
17383
  WebSocketCollector as W,
16545
- Button$1 as X,
16546
- Root2 as Y,
16547
- Trigger as Z,
16548
- ChevronDown as _,
17384
+ PopoverContent as X,
17385
+ Label as Y,
17386
+ Select$1 as Z,
17387
+ SelectTrigger as _,
16549
17388
  uploadSignedMediaWithApi as a,
16550
- Content2 as a0,
16551
- Check as a1,
16552
- X as a2,
16553
- DatePicker as a3,
16554
- getNotionPageMetadata as a4,
16555
- buildNotionFieldViews as a5,
16556
- DirectUploadError as a6,
16557
- toast as a7,
16558
- getSnapshot as a8,
16559
- subscribe as a9,
16560
- sameDigitGroups as aA,
16561
- labelSimilarityKeepingDigits as aB,
16562
- landmarkRole as aC,
16563
- findElementBySelector as aD,
16564
- findElementByXPath as aE,
16565
- OnboardingProvider as aF,
16566
- useOnboardingTarget as aG,
16567
- useOnboardingFlow as aH,
16568
- PopoverAnchor as aI,
16569
- getRegisteredUploadId as aa,
16570
- ChevronRight as ab,
16571
- ReactRemoveScroll as ac,
16572
- hideOthers as ad,
16573
- useFocusGuards as ae,
16574
- FocusScope as af,
16575
- getExposedJiraFields as ag,
16576
- isJiraFieldExposed as ah,
16577
- normalizeExposedJiraFields as ai,
16578
- resolveJiraIssueTypeId as aj,
16579
- pickJiraIssueTypeFields as ak,
16580
- switchJiraIssueType as al,
16581
- getPrimaryJiraMapping as am,
16582
- createCollection as an,
16583
- useDirection as ao,
16584
- getPrimaryNotionMapping as ap,
16585
- buttonVariants as aq,
16586
- TEST_ATTR_KEYS as ar,
16587
- jaccard as as,
16588
- textSimilarity as at,
16589
- collectHeadings as au,
16590
- buildElementLocator as av,
16591
- normalizeForCompare as aw,
16592
- CONTAINER_SELECTOR as ax,
16593
- safeRole as ay,
16594
- safeAccessibleName as az,
16595
- buildJiraCreateFields as b,
16596
- captureWithModernScreenshot as c,
17389
+ SelectContent as a0,
17390
+ SelectItem as a1,
17391
+ JIRA_ISSUE_TYPE_VALUE_KEY as a2,
17392
+ getJiraMetadata as a3,
17393
+ buildJiraIssueFields as a4,
17394
+ getJiraIssueTypeLabel as a5,
17395
+ Button$1 as a6,
17396
+ Root2 as a7,
17397
+ Trigger as a8,
17398
+ ChevronDown as a9,
17399
+ useShadowOutsideDismiss as aA,
17400
+ getPrimaryNotionMapping as aB,
17401
+ buttonVariants as aC,
17402
+ TEST_ATTR_KEYS as aD,
17403
+ jaccard as aE,
17404
+ textSimilarity as aF,
17405
+ collectHeadings as aG,
17406
+ buildElementLocator as aH,
17407
+ normalizeForCompare as aI,
17408
+ CONTAINER_SELECTOR as aJ,
17409
+ safeRole as aK,
17410
+ safeAccessibleName as aL,
17411
+ sameDigitGroups as aM,
17412
+ labelSimilarityKeepingDigits as aN,
17413
+ landmarkRole as aO,
17414
+ findElementBySelector as aP,
17415
+ findElementByXPath as aQ,
17416
+ OnboardingProvider as aR,
17417
+ useOnboardingTarget as aS,
17418
+ useOnboardingFlow as aT,
17419
+ PopoverAnchor as aU,
17420
+ Portal as aa,
17421
+ Content2 as ab,
17422
+ Check as ac,
17423
+ X as ad,
17424
+ DatePicker as ae,
17425
+ getNotionPageMetadata as af,
17426
+ buildNotionFieldViews as ag,
17427
+ DirectUploadError as ah,
17428
+ getRegisteredUploadId as ai,
17429
+ getSnapshot as aj,
17430
+ subscribe as ak,
17431
+ ChevronRight as al,
17432
+ ReactRemoveScroll as am,
17433
+ hideOthers as an,
17434
+ useFocusGuards as ao,
17435
+ FocusScope as ap,
17436
+ createShadowDismissRoot as aq,
17437
+ getExposedJiraFields as ar,
17438
+ isJiraFieldExposed as as,
17439
+ normalizeExposedJiraFields as at,
17440
+ resolveJiraIssueTypeId as au,
17441
+ pickJiraIssueTypeFields as av,
17442
+ switchJiraIssueType as aw,
17443
+ getPrimaryJiraMapping as ax,
17444
+ createCollection as ay,
17445
+ useDirection as az,
17446
+ captureWithModernScreenshot as b,
17447
+ captureViewportWithElementCrop as c,
16597
17448
  directUploadToSignedUrl as d,
16598
- buildNotionCreateFields as e,
16599
- buildGuestOwnerQuery as f,
17449
+ buildJiraCreateFields as e,
17450
+ buildNotionCreateFields as f,
16600
17451
  getElementPosition as g,
16601
- composeIssueFilterGroups as h,
16602
- buildIssueIntegrationsPayload as i,
16603
- devLogger as j,
16604
- filterExcludeServiceRelatedLogs as k,
16605
- filterExcludeServiceRelatedConsoleLogs as l,
16606
- compactNetworkLogsForTransport as m,
16607
- CONSOLE_LOG_SUBMIT_LIMIT as n,
16608
- buildExternalUserPayload as o,
17452
+ buildGuestOwnerQuery as h,
17453
+ composeIssueFilterGroups as i,
17454
+ buildIssueIntegrationsPayload as j,
17455
+ devLogger as k,
17456
+ filterExcludeServiceRelatedLogs as l,
17457
+ filterExcludeServiceRelatedConsoleLogs as m,
17458
+ compactNetworkLogsForTransport as n,
17459
+ CONSOLE_LOG_SUBMIT_LIMIT as o,
16609
17460
  pristineFetch as p,
16610
- shrinkNetworkLogsToFit as q,
16611
- measureJsonBytes as r,
17461
+ buildExternalUserPayload as q,
17462
+ shrinkNetworkLogsToFit as r,
16612
17463
  selectNetworkLogsForSubmit as s,
16613
- sanitizeString as t,
17464
+ measureJsonBytes as t,
16614
17465
  uploadScreenshotDataUrls as u,
16615
- serializeSocketTranscript as v,
16616
- uploadSocketTranscript as w,
16617
- getCurrentPageUrl as x,
16618
- SessionReplayCaptureController as y,
16619
- preloadModernScreenshot as z
17466
+ sanitizeString as v,
17467
+ collectAttachmentUploads as w,
17468
+ hasReplayUpload as x,
17469
+ getCurrentPageUrl as y,
17470
+ createIssueUploadQueues as z
16620
17471
  };