issue-sticker 0.10.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,7 +1,7 @@
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, m as useControllableState, A as Root2$1, n as useId, B as Anchor, 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-_bdgDAmI.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
- import React__default, { useState, createContext, useContext, useCallback, useRef, useLayoutEffect, useEffect, useMemo } from "react";
4
+ import React__default, { useState, useRef, useEffect, useCallback, createContext, useContext, useLayoutEffect, useMemo } from "react";
5
5
  import * as ReactDOM from "react-dom";
6
6
  const DEFAULT_MAX_DURATION_MS = 12e4;
7
7
  const MIME_TYPE_CANDIDATES = ["video/webm;codecs=vp9", "video/webm"];
@@ -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-_bdgDAmI.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-_bdgDAmI.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 };
@@ -2926,162 +2951,1023 @@ async function captureWithModernScreenshot(options) {
2926
2951
  stickySnapshot.cleanup();
2927
2952
  }
2928
2953
  }
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);
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
+ };
2935
2966
  }
2936
- const __vite_import_meta_env__ = { "DEV": false };
2937
- function isDevLoggingEnabled() {
2938
- const env = __vite_import_meta_env__;
2939
- {
2940
- return env.DEV;
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();
2941
3003
  }
2942
3004
  }
2943
- function isInternalDebugLog(message2) {
2944
- return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
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 };
2945
3021
  }
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);
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 {
2952
3026
  }
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);
2968
- }
2969
- };
2970
- class DirectUploadError extends Error {
2971
- constructor(message2, status, data) {
2972
- super(message2);
2973
- this.status = status;
2974
- this.data = data;
2975
- this.name = "DirectUploadError";
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}`);
2976
3049
  }
3050
+ if (status === 409) return error;
3051
+ return isPermanentStatus(status) ? new PermanentUploadError(`tus ${status}`) : error;
2977
3052
  }
2978
- const uploadIdByUrl = /* @__PURE__ */ new Map();
2979
- function bytesToHex(bytes) {
2980
- return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3053
+ function stripMimeParameters(mimeType) {
3054
+ return mimeType.split(";", 1)[0].trim().toLowerCase();
2981
3055
  }
2982
- async function readFileArrayBuffer(file) {
2983
- if (typeof file.arrayBuffer === "function") {
2984
- return file.arrayBuffer();
3056
+ function callSafely(call) {
3057
+ try {
3058
+ call();
3059
+ } catch {
2985
3060
  }
3061
+ }
3062
+ function readBlob(blob) {
3063
+ if (typeof blob.arrayBuffer === "function") return blob.arrayBuffer();
2986
3064
  return new Promise((resolve, reject) => {
2987
3065
  const reader = new FileReader();
2988
- reader.onload = () => {
2989
- if (reader.result instanceof ArrayBuffer) {
2990
- resolve(reader.result);
2991
- } else {
2992
- reject(new Error("파일 내용을 ArrayBuffer로 읽지 못했습니다."));
2993
- }
2994
- };
2995
- reader.onerror = () => reject(reader.error ?? new Error("파일 읽기에 실패했습니다."));
2996
- reader.readAsArrayBuffer(file);
3066
+ reader.onload = () => resolve(reader.result);
3067
+ reader.onerror = () => reject(reader.error ?? new Error("업로드 파일을 읽지 못했습니다."));
3068
+ reader.readAsArrayBuffer(blob);
2997
3069
  });
2998
3070
  }
2999
- async function calculateSha256Hex(file) {
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() {
3000
3078
  var _a;
3001
- if (!((_a = globalThis.crypto) == null ? void 0 : _a.subtle)) {
3002
- throw new Error("Web Crypto SHA-256 API를 사용할 수 없어 signed upload를 시작할 수 없습니다.");
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();
3003
3084
  }
3004
- const digest = await globalThis.crypto.subtle.digest("SHA-256", await readFileArrayBuffer(file));
3005
- return bytesToHex(new Uint8Array(digest));
3006
3085
  }
3007
- function normalizeUploadMimeType(mimeType, mediaType) {
3008
- var _a;
3009
- const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" || mediaType === "socket" ? "application/gzip" : "video/webm";
3010
- const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
3011
- return normalized && normalized.length > 0 ? normalized : fallback;
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("");
3012
3089
  }
3013
- async function directUploadToSignedUrl(signedUrl, file, contentType) {
3014
- const response = await fetch(signedUrl, {
3015
- method: "PUT",
3016
- headers: {
3017
- "Content-Type": contentType
3018
- },
3019
- body: file
3020
- });
3021
- if (!response.ok) {
3022
- const body = await response.text().catch(() => "");
3023
- throw new DirectUploadError(
3024
- `스토리지 업로드 실패: ${response.status}${body ? ` ${body}` : ""}`,
3025
- response.status,
3026
- body || void 0
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)
3027
3170
  );
3171
+ await Promise.all(
3172
+ expired.map((record2) => this.deps.store.delete(record2.id).catch(() => void 0))
3173
+ );
3174
+ return expired.length;
3028
3175
  }
3029
- }
3030
- function getRegisteredUploadId(publicUrl, mediaType) {
3031
- const record2 = uploadIdByUrl.get(publicUrl);
3032
- return (record2 == null ? void 0 : record2.mediaType) === mediaType ? record2.id : void 0;
3033
- }
3034
- async function uploadSignedMediaWithApi(api, file, mediaType, options = {}) {
3035
- const checksumSha256 = await calculateSha256Hex(file);
3036
- const contentType = normalizeUploadMimeType(file.type, mediaType);
3037
- const init = await api.post("/api/media/uploads/init", {
3038
- files: [
3039
- {
3040
- mediaType,
3041
- mimeType: contentType,
3042
- sizeBytes: file.size,
3043
- checksumSha256,
3044
- issueId: options.issueId
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";
3045
3194
  }
3046
- ]
3047
- });
3048
- const upload = init.uploads[0];
3049
- if (!(upload == null ? void 0 : upload.id) || !upload.signedUrl) {
3050
- throw new Error("signed upload URL 발급 응답이 올바르지 않습니다.");
3195
+ );
3051
3196
  }
3052
- if (file.size > upload.maxSizeBytes) {
3053
- throw new Error(`${mediaType} 파일 크기가 허용 한도를 초과했습니다.`);
3197
+ subscribe(listener) {
3198
+ this.listeners.add(listener);
3199
+ callSafely(() => listener(this.snapshot()));
3200
+ return () => {
3201
+ this.listeners.delete(listener);
3202
+ };
3054
3203
  }
3055
- await api.directUpload(upload.signedUrl, file, contentType);
3056
- const complete = await api.post("/api/media/uploads/complete", {
3057
- uploadIds: [upload.id],
3058
- issueId: options.issueId
3059
- });
3060
- const completed = complete.uploads[0];
3061
- const publicUrl = (completed == null ? void 0 : completed.publicUrl) || upload.publicUrl;
3062
- const uploadId = (completed == null ? void 0 : completed.id) || upload.id;
3063
- uploadIdByUrl.set(publicUrl, { id: uploadId, mediaType });
3064
- return {
3065
- id: uploadId,
3066
- url: publicUrl
3067
- };
3068
- }
3069
- function _mergeNamespaces$1(e2, t2) {
3070
- return t2.forEach((function(t3) {
3071
- t3 && "string" != typeof t3 && !Array.isArray(t3) && Object.keys(t3).forEach((function(r2) {
3072
- if ("default" !== r2 && !(r2 in e2)) {
3073
- var i2 = Object.getOwnPropertyDescriptor(t3, r2);
3074
- Object.defineProperty(e2, r2, i2.get ? i2 : { enumerable: true, get: function() {
3075
- return t3[r2];
3076
- } });
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";
3077
3241
  }
3078
- }));
3079
- })), Object.freeze(e2);
3080
- }
3081
- function copyExifWithoutOrientation(e2, t2) {
3082
- return new Promise((function(r2, i2) {
3083
- let o2;
3084
- return getApp1Segment(e2).then((function(e3) {
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 }));
3704
+ }
3705
+ return {
3706
+ payload,
3707
+ sizeBytes: payload.byteLength,
3708
+ connectionCount: connections.length,
3709
+ frameCount
3710
+ };
3711
+ }
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
+ };
3723
+ }
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 });
3737
+ }
3738
+ return uploads;
3739
+ }
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 });
3752
+ }
3753
+ function hasReplayUpload(uploads) {
3754
+ return uploads.some((upload) => upload.mediaType === "replay");
3755
+ }
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
+ );
3763
+ }
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
+ }
3778
+ class DirectUploadError extends Error {
3779
+ constructor(message2, status, data) {
3780
+ super(message2);
3781
+ this.status = status;
3782
+ this.data = data;
3783
+ this.name = "DirectUploadError";
3784
+ }
3785
+ }
3786
+ const uploadIdByUrl = /* @__PURE__ */ new Map();
3787
+ function bytesToHex(bytes) {
3788
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3789
+ }
3790
+ async function readFileArrayBuffer(file) {
3791
+ if (typeof file.arrayBuffer === "function") {
3792
+ return file.arrayBuffer();
3793
+ }
3794
+ return new Promise((resolve, reject) => {
3795
+ const reader = new FileReader();
3796
+ reader.onload = () => {
3797
+ if (reader.result instanceof ArrayBuffer) {
3798
+ resolve(reader.result);
3799
+ } else {
3800
+ reject(new Error("파일 내용을 ArrayBuffer로 읽지 못했습니다."));
3801
+ }
3802
+ };
3803
+ reader.onerror = () => reject(reader.error ?? new Error("파일 읽기에 실패했습니다."));
3804
+ reader.readAsArrayBuffer(file);
3805
+ });
3806
+ }
3807
+ async function calculateSha256Hex(file) {
3808
+ var _a;
3809
+ if (!((_a = globalThis.crypto) == null ? void 0 : _a.subtle)) {
3810
+ throw new Error("Web Crypto SHA-256 API를 사용할 수 없어 signed upload를 시작할 수 없습니다.");
3811
+ }
3812
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", await readFileArrayBuffer(file));
3813
+ return bytesToHex(new Uint8Array(digest));
3814
+ }
3815
+ function normalizeUploadMimeType(mimeType, mediaType) {
3816
+ var _a;
3817
+ const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" || mediaType === "socket" ? "application/gzip" : "video/webm";
3818
+ const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
3819
+ return normalized && normalized.length > 0 ? normalized : fallback;
3820
+ }
3821
+ async function directUploadToSignedUrl(signedUrl, file, contentType) {
3822
+ const response = await fetch(signedUrl, {
3823
+ method: "PUT",
3824
+ headers: {
3825
+ "Content-Type": contentType,
3826
+ "x-client-info": "issue-sticker-sdk"
3827
+ },
3828
+ body: file
3829
+ });
3830
+ if (!response.ok) {
3831
+ const body = await response.text().catch(() => "");
3832
+ throw new DirectUploadError(
3833
+ `스토리지 업로드 실패: ${response.status}${body ? ` ${body}` : ""}`,
3834
+ response.status,
3835
+ body || void 0
3836
+ );
3837
+ }
3838
+ }
3839
+ function getRegisteredUploadId(publicUrl, mediaType) {
3840
+ const record2 = uploadIdByUrl.get(publicUrl);
3841
+ return (record2 == null ? void 0 : record2.mediaType) === mediaType ? record2.id : void 0;
3842
+ }
3843
+ async function uploadSignedMediaWithApi(api, file, mediaType, options = {}) {
3844
+ const checksumSha256 = await calculateSha256Hex(file);
3845
+ const contentType = normalizeUploadMimeType(file.type, mediaType);
3846
+ const init = await api.post("/api/media/uploads/init", {
3847
+ files: [
3848
+ {
3849
+ mediaType,
3850
+ mimeType: contentType,
3851
+ sizeBytes: file.size,
3852
+ checksumSha256,
3853
+ issueId: options.issueId
3854
+ }
3855
+ ]
3856
+ });
3857
+ const upload = init.uploads[0];
3858
+ if (!(upload == null ? void 0 : upload.id) || !upload.signedUrl) {
3859
+ throw new Error("signed upload URL 발급 응답이 올바르지 않습니다.");
3860
+ }
3861
+ if (file.size > upload.maxSizeBytes) {
3862
+ throw new Error(`${mediaType} 파일 크기가 허용 한도를 초과했습니다.`);
3863
+ }
3864
+ await api.directUpload(upload.signedUrl, file, contentType);
3865
+ const complete = await api.post("/api/media/uploads/complete", {
3866
+ uploadIds: [upload.id],
3867
+ issueId: options.issueId
3868
+ });
3869
+ const completed = complete.uploads[0];
3870
+ const publicUrl = (completed == null ? void 0 : completed.publicUrl) || upload.publicUrl;
3871
+ const uploadId = (completed == null ? void 0 : completed.id) || upload.id;
3872
+ uploadIdByUrl.set(publicUrl, { id: uploadId, mediaType });
3873
+ return {
3874
+ id: uploadId,
3875
+ url: publicUrl
3876
+ };
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
+ };
3955
+ function _mergeNamespaces$1(e2, t2) {
3956
+ return t2.forEach((function(t3) {
3957
+ t3 && "string" != typeof t3 && !Array.isArray(t3) && Object.keys(t3).forEach((function(r2) {
3958
+ if ("default" !== r2 && !(r2 in e2)) {
3959
+ var i2 = Object.getOwnPropertyDescriptor(t3, r2);
3960
+ Object.defineProperty(e2, r2, i2.get ? i2 : { enumerable: true, get: function() {
3961
+ return t3[r2];
3962
+ } });
3963
+ }
3964
+ }));
3965
+ })), Object.freeze(e2);
3966
+ }
3967
+ function copyExifWithoutOrientation(e2, t2) {
3968
+ return new Promise((function(r2, i2) {
3969
+ let o2;
3970
+ return getApp1Segment(e2).then((function(e3) {
3085
3971
  try {
3086
3972
  return o2 = e3, r2(new Blob([t2.slice(0, 2), o2, t2.slice(2)], { type: "image/jpeg" }));
3087
3973
  } catch (e4) {
@@ -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);
@@ -5622,90 +6509,32 @@ class NetworkCollector {
5622
6509
  * 무조건 되돌리면 우리 뒤에 붙은 계측(Sentry 등)의 래퍼가 통째로 사라진다 — 고객의 에러
5623
6510
  * 추적이 조용히 죽는다. 남의 것이 위에 있으면 우리 래퍼는 체인에 남겨 두고 수집만 멈춘다.
5624
6511
  */
5625
- restoreFetch() {
5626
- if (!this.originalFetch) return;
5627
- if (window.fetch === this.patchedFetch) {
5628
- window.fetch = this.originalFetch;
5629
- }
5630
- this.originalFetch = null;
5631
- this.patchedFetch = null;
5632
- clearPristineFetch();
5633
- }
5634
- /** fetch 와 같은 규칙 — 우리 래퍼가 아직 걸려 있을 때만 되돌린다. */
5635
- restoreXHR() {
5636
- const patched = this.patchedXHR;
5637
- if (this.originalXHROpen && (!patched || XMLHttpRequest.prototype.open === patched.open)) {
5638
- XMLHttpRequest.prototype.open = this.originalXHROpen;
5639
- }
5640
- if (this.originalXHRSend && (!patched || XMLHttpRequest.prototype.send === patched.send)) {
5641
- XMLHttpRequest.prototype.send = this.originalXHRSend;
5642
- }
5643
- if (this.originalXHRSetRequestHeader && (!patched || XMLHttpRequest.prototype.setRequestHeader === patched.setRequestHeader)) {
5644
- XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
5645
- }
5646
- this.originalXHROpen = null;
5647
- this.originalXHRSend = null;
5648
- this.originalXHRSetRequestHeader = null;
5649
- this.patchedXHR = null;
5650
- }
5651
- }
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 }));
6512
+ restoreFetch() {
6513
+ if (!this.originalFetch) return;
6514
+ if (window.fetch === this.patchedFetch) {
6515
+ window.fetch = this.originalFetch;
6516
+ }
6517
+ this.originalFetch = null;
6518
+ this.patchedFetch = null;
6519
+ clearPristineFetch();
6520
+ }
6521
+ /** fetch 와 같은 규칙 — 우리 래퍼가 아직 걸려 있을 때만 되돌린다. */
6522
+ restoreXHR() {
6523
+ const patched = this.patchedXHR;
6524
+ if (this.originalXHROpen && (!patched || XMLHttpRequest.prototype.open === patched.open)) {
6525
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
6526
+ }
6527
+ if (this.originalXHRSend && (!patched || XMLHttpRequest.prototype.send === patched.send)) {
6528
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
6529
+ }
6530
+ if (this.originalXHRSetRequestHeader && (!patched || XMLHttpRequest.prototype.setRequestHeader === patched.setRequestHeader)) {
6531
+ XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
6532
+ }
6533
+ this.originalXHROpen = null;
6534
+ this.originalXHRSend = null;
6535
+ this.originalXHRSetRequestHeader = null;
6536
+ this.patchedXHR = null;
5701
6537
  }
5702
- return {
5703
- payload,
5704
- checksumSha256: await sha256Hex(payload),
5705
- sizeBytes: payload.byteLength,
5706
- connectionCount: connections.length,
5707
- frameCount
5708
- };
5709
6538
  }
5710
6539
  var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
5711
6540
  NetworkLogType2["MAIN_FRAME"] = "main_frame";
@@ -6108,75 +6937,905 @@ function appendCustomFieldFilterGroups(groups, customFields) {
6108
6937
  options
6109
6938
  });
6110
6939
  }
6111
- }
6112
- function appendJiraFieldFilterGroups(groups, exposedFieldIds, fields) {
6113
- for (const field of fields) {
6114
- const normalizedFieldId = normalizeJiraFieldId(field.id);
6115
- if (!exposedFieldIds.has(normalizedFieldId) || !JIRA_FILTERABLE_FIELD_IDS.has(normalizedFieldId)) {
6116
- continue;
6940
+ }
6941
+ function appendJiraFieldFilterGroups(groups, exposedFieldIds, fields) {
6942
+ for (const field of fields) {
6943
+ const normalizedFieldId = normalizeJiraFieldId(field.id);
6944
+ if (!exposedFieldIds.has(normalizedFieldId) || !JIRA_FILTERABLE_FIELD_IDS.has(normalizedFieldId)) {
6945
+ continue;
6946
+ }
6947
+ const options = getJiraFieldFilterOptions(field);
6948
+ if (!options.length) continue;
6949
+ upsertFilterGroup(groups, {
6950
+ kind: "jira",
6951
+ id: `jira:${normalizedFieldId}`,
6952
+ fieldName: normalizedFieldId,
6953
+ label: field.name,
6954
+ options
6955
+ });
6956
+ }
6957
+ }
6958
+ function appendNotionFieldFilterGroups(groups, selectedFields, fields) {
6959
+ for (const field of fields) {
6960
+ if (!selectedFields.has(field.id) && !selectedFields.has(field.name)) continue;
6961
+ if (!NOTION_FILTERABLE_FIELD_TYPES.has(field.type)) continue;
6962
+ const options = getNotionFieldFilterOptions(field);
6963
+ if (!options.length) continue;
6964
+ upsertFilterGroup(groups, {
6965
+ kind: "notion",
6966
+ id: `notion:${field.id}`,
6967
+ fieldName: field.name,
6968
+ label: field.name,
6969
+ options
6970
+ });
6971
+ }
6972
+ }
6973
+ function composeIssueFilterGroups(integrations, customFields) {
6974
+ const groups = /* @__PURE__ */ new Map();
6975
+ const jiraIntegration = integrations.find((integration) => {
6976
+ const config = getJiraConfig(integration);
6977
+ return integration.is_active !== false && config;
6978
+ });
6979
+ appendCustomFieldFilterGroups(groups, customFields);
6980
+ const jiraConfig = jiraIntegration ? getJiraConfig(jiraIntegration) : null;
6981
+ const configExposedFields = getExposedFields(jiraConfig);
6982
+ if (configExposedFields.length) {
6983
+ appendJiraFieldFilterGroups(
6984
+ groups,
6985
+ new Set(configExposedFields),
6986
+ getLastKnownJiraFields(jiraConfig)
6987
+ );
6988
+ }
6989
+ for (const integration of integrations) {
6990
+ if (integration.is_active === false) continue;
6991
+ const notionConfig = getNotionConfig(integration);
6992
+ if (!notionConfig) continue;
6993
+ appendNotionFieldFilterGroups(
6994
+ groups,
6995
+ new Set(getSelectedNotionFields(notionConfig)),
6996
+ getLastKnownNotionFields(notionConfig)
6997
+ );
6998
+ }
6999
+ return Array.from(groups.values()).sort((a2, b) => a2.label.localeCompare(b.label));
7000
+ }
7001
+ const IGNORED_NAME = /^(Primitive\.|Slot|Presence|Portal|Fragment|Suspense|StrictMode)|(Provider|Context|Consumer)$/;
7002
+ const MAX_DEPTH = 300;
7003
+ function fiberOf(element) {
7004
+ for (const key of Object.keys(element)) {
7005
+ if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
7006
+ return element[key] ?? null;
7007
+ }
7008
+ }
7009
+ return null;
7010
+ }
7011
+ function nameOf(type) {
7012
+ if (typeof type === "function") {
7013
+ const fn = type;
7014
+ return fn.displayName ?? fn.name ?? null;
7015
+ }
7016
+ if (type && typeof type === "object") {
7017
+ const wrapper = type;
7018
+ return wrapper.displayName ?? nameOf(wrapper.render) ?? nameOf(wrapper.type);
7019
+ }
7020
+ return null;
7021
+ }
7022
+ const reactAdapter = {
7023
+ framework: "react",
7024
+ collect(element) {
7025
+ let fiber = fiberOf(element);
7026
+ if (!fiber) return null;
7027
+ const names = [];
7028
+ for (let depth = 0; fiber && depth < MAX_DEPTH; depth += 1) {
7029
+ const name = nameOf(fiber.type);
7030
+ if (typeof name === "string" && name && !IGNORED_NAME.test(name)) names.unshift(name);
7031
+ fiber = fiber.return;
7032
+ }
7033
+ return names;
7034
+ }
7035
+ };
7036
+ const ADAPTERS = [reactAdapter];
7037
+ const MIN_NAME_LENGTH = 3;
7038
+ const MAX_NAME_LENGTH = 60;
7039
+ function cleanPath(names) {
7040
+ if (!names) return [];
7041
+ const kept = names.filter((name) => typeof name === "string").map(
7042
+ (name) => name.replace(/\s+/g, " ").trim().slice(0, MAX_NAME_LENGTH).replace(/[\uD800-\uDBFF]$/, "")
7043
+ ).filter((name) => name.length >= MIN_NAME_LENGTH);
7044
+ const deduped = kept.filter((name, index) => name !== kept[index - 1]);
7045
+ return deduped.slice(-8);
7046
+ }
7047
+ function collectComponentPath(element, adapters = ADAPTERS) {
7048
+ for (const adapter of adapters) {
7049
+ let path = [];
7050
+ try {
7051
+ path = cleanPath(adapter.collect(element));
7052
+ } catch {
7053
+ path = [];
7054
+ }
7055
+ if (path.length) return { framework: adapter.framework, path };
7056
+ }
7057
+ return null;
7058
+ }
7059
+ var toStr = Object.prototype.toString;
7060
+ function isCallable(fn) {
7061
+ return typeof fn === "function" || toStr.call(fn) === "[object Function]";
7062
+ }
7063
+ function toInteger(value) {
7064
+ var number = Number(value);
7065
+ if (isNaN(number)) {
7066
+ return 0;
7067
+ }
7068
+ if (number === 0 || !isFinite(number)) {
7069
+ return number;
7070
+ }
7071
+ return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
7072
+ }
7073
+ var maxSafeInteger = Math.pow(2, 53) - 1;
7074
+ function toLength(value) {
7075
+ var len = toInteger(value);
7076
+ return Math.min(Math.max(len, 0), maxSafeInteger);
7077
+ }
7078
+ function arrayFrom(arrayLike, mapFn) {
7079
+ var C = Array;
7080
+ var items = Object(arrayLike);
7081
+ if (arrayLike == null) {
7082
+ throw new TypeError("Array.from requires an array-like object - not null or undefined");
7083
+ }
7084
+ var len = toLength(items.length);
7085
+ var A = isCallable(C) ? Object(new C(len)) : new Array(len);
7086
+ var k = 0;
7087
+ var kValue;
7088
+ while (k < len) {
7089
+ kValue = items[k];
7090
+ {
7091
+ A[k] = kValue;
7092
+ }
7093
+ k += 1;
7094
+ }
7095
+ A.length = len;
7096
+ return A;
7097
+ }
7098
+ function _typeof(o2) {
7099
+ "@babel/helpers - typeof";
7100
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o3) {
7101
+ return typeof o3;
7102
+ } : function(o3) {
7103
+ return o3 && "function" == typeof Symbol && o3.constructor === Symbol && o3 !== Symbol.prototype ? "symbol" : typeof o3;
7104
+ }, _typeof(o2);
7105
+ }
7106
+ function _classCallCheck(instance, Constructor) {
7107
+ if (!(instance instanceof Constructor)) {
7108
+ throw new TypeError("Cannot call a class as a function");
7109
+ }
7110
+ }
7111
+ function _defineProperties(target, props) {
7112
+ for (var i2 = 0; i2 < props.length; i2++) {
7113
+ var descriptor = props[i2];
7114
+ descriptor.enumerable = descriptor.enumerable || false;
7115
+ descriptor.configurable = true;
7116
+ if ("value" in descriptor) descriptor.writable = true;
7117
+ Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
7118
+ }
7119
+ }
7120
+ function _createClass(Constructor, protoProps, staticProps) {
7121
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
7122
+ Object.defineProperty(Constructor, "prototype", { writable: false });
7123
+ return Constructor;
7124
+ }
7125
+ function _defineProperty(obj, key, value) {
7126
+ key = _toPropertyKey(key);
7127
+ if (key in obj) {
7128
+ Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
7129
+ } else {
7130
+ obj[key] = value;
7131
+ }
7132
+ return obj;
7133
+ }
7134
+ function _toPropertyKey(t2) {
7135
+ var i2 = _toPrimitive(t2, "string");
7136
+ return "symbol" == _typeof(i2) ? i2 : i2 + "";
7137
+ }
7138
+ function _toPrimitive(t2, r2) {
7139
+ if ("object" != _typeof(t2) || !t2) return t2;
7140
+ var e2 = t2[Symbol.toPrimitive];
7141
+ if (void 0 !== e2) {
7142
+ var i2 = e2.call(t2, r2);
7143
+ if ("object" != _typeof(i2)) return i2;
7144
+ throw new TypeError("@@toPrimitive must return a primitive value.");
7145
+ }
7146
+ return String(t2);
7147
+ }
7148
+ var SetLike = /* @__PURE__ */ (function() {
7149
+ function SetLike2() {
7150
+ var items = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : [];
7151
+ _classCallCheck(this, SetLike2);
7152
+ _defineProperty(this, "items", void 0);
7153
+ this.items = items;
7154
+ }
7155
+ return _createClass(SetLike2, [{
7156
+ key: "add",
7157
+ value: function add(value) {
7158
+ if (this.has(value) === false) {
7159
+ this.items.push(value);
7160
+ }
7161
+ return this;
7162
+ }
7163
+ }, {
7164
+ key: "clear",
7165
+ value: function clear() {
7166
+ this.items = [];
7167
+ }
7168
+ }, {
7169
+ key: "delete",
7170
+ value: function _delete(value) {
7171
+ var previousLength = this.items.length;
7172
+ this.items = this.items.filter(function(item) {
7173
+ return item !== value;
7174
+ });
7175
+ return previousLength !== this.items.length;
7176
+ }
7177
+ }, {
7178
+ key: "forEach",
7179
+ value: function forEach(callbackfn) {
7180
+ var _this = this;
7181
+ this.items.forEach(function(item) {
7182
+ callbackfn(item, item, _this);
7183
+ });
7184
+ }
7185
+ }, {
7186
+ key: "has",
7187
+ value: function has(value) {
7188
+ return this.items.indexOf(value) !== -1;
7189
+ }
7190
+ }, {
7191
+ key: "size",
7192
+ get: function get() {
7193
+ return this.items.length;
7194
+ }
7195
+ }]);
7196
+ })();
7197
+ const SetLike$1 = typeof Set === "undefined" ? Set : SetLike;
7198
+ function getLocalName(element) {
7199
+ var _element$localName;
7200
+ return (
7201
+ // eslint-disable-next-line no-restricted-properties -- actual guard for environments without localName
7202
+ (_element$localName = element.localName) !== null && _element$localName !== void 0 ? _element$localName : (
7203
+ // eslint-disable-next-line no-restricted-properties -- required for the fallback
7204
+ element.tagName.toLowerCase()
7205
+ )
7206
+ );
7207
+ }
7208
+ var localNameToRoleMappings = {
7209
+ article: "article",
7210
+ aside: "complementary",
7211
+ button: "button",
7212
+ datalist: "listbox",
7213
+ dd: "definition",
7214
+ details: "group",
7215
+ dialog: "dialog",
7216
+ dt: "term",
7217
+ fieldset: "group",
7218
+ figure: "figure",
7219
+ // WARNING: Only with an accessible name
7220
+ form: "form",
7221
+ footer: "contentinfo",
7222
+ h1: "heading",
7223
+ h2: "heading",
7224
+ h3: "heading",
7225
+ h4: "heading",
7226
+ h5: "heading",
7227
+ h6: "heading",
7228
+ header: "banner",
7229
+ hr: "separator",
7230
+ html: "document",
7231
+ legend: "legend",
7232
+ li: "listitem",
7233
+ math: "math",
7234
+ main: "main",
7235
+ menu: "list",
7236
+ nav: "navigation",
7237
+ ol: "list",
7238
+ optgroup: "group",
7239
+ // WARNING: Only in certain context
7240
+ option: "option",
7241
+ output: "status",
7242
+ progress: "progressbar",
7243
+ // WARNING: Only with an accessible name
7244
+ section: "region",
7245
+ summary: "button",
7246
+ table: "table",
7247
+ tbody: "rowgroup",
7248
+ textarea: "textbox",
7249
+ tfoot: "rowgroup",
7250
+ // WARNING: Only in certain context
7251
+ td: "cell",
7252
+ th: "columnheader",
7253
+ thead: "rowgroup",
7254
+ tr: "row",
7255
+ ul: "list"
7256
+ };
7257
+ var prohibitedAttributes = {
7258
+ caption: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7259
+ code: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7260
+ deletion: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7261
+ emphasis: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7262
+ generic: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby", "aria-roledescription"]),
7263
+ insertion: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7264
+ none: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7265
+ paragraph: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7266
+ presentation: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7267
+ strong: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7268
+ subscript: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"]),
7269
+ superscript: /* @__PURE__ */ new Set(["aria-label", "aria-labelledby"])
7270
+ };
7271
+ function hasGlobalAriaAttributes(element, role) {
7272
+ return [
7273
+ "aria-atomic",
7274
+ "aria-busy",
7275
+ "aria-controls",
7276
+ "aria-current",
7277
+ "aria-description",
7278
+ "aria-describedby",
7279
+ "aria-details",
7280
+ // "disabled",
7281
+ "aria-dropeffect",
7282
+ // "errormessage",
7283
+ "aria-flowto",
7284
+ "aria-grabbed",
7285
+ // "haspopup",
7286
+ "aria-hidden",
7287
+ // "invalid",
7288
+ "aria-keyshortcuts",
7289
+ "aria-label",
7290
+ "aria-labelledby",
7291
+ "aria-live",
7292
+ "aria-owns",
7293
+ "aria-relevant",
7294
+ "aria-roledescription"
7295
+ ].some(function(attributeName) {
7296
+ var _prohibitedAttributes;
7297
+ return element.hasAttribute(attributeName) && !((_prohibitedAttributes = prohibitedAttributes[role]) !== null && _prohibitedAttributes !== void 0 && _prohibitedAttributes.has(attributeName));
7298
+ });
7299
+ }
7300
+ function ignorePresentationalRole(element, implicitRole) {
7301
+ return hasGlobalAriaAttributes(element, implicitRole);
7302
+ }
7303
+ function getRole(element) {
7304
+ var explicitRole = getExplicitRole(element);
7305
+ if (explicitRole === null || presentationRoles.indexOf(explicitRole) !== -1) {
7306
+ var implicitRole = getImplicitRole(element);
7307
+ if (presentationRoles.indexOf(explicitRole || "") === -1 || ignorePresentationalRole(element, implicitRole || "")) {
7308
+ return implicitRole;
7309
+ }
7310
+ }
7311
+ return explicitRole;
7312
+ }
7313
+ function getImplicitRole(element) {
7314
+ var mappedByTag = localNameToRoleMappings[getLocalName(element)];
7315
+ if (mappedByTag !== void 0) {
7316
+ return mappedByTag;
7317
+ }
7318
+ switch (getLocalName(element)) {
7319
+ case "a":
7320
+ case "area":
7321
+ case "link":
7322
+ if (element.hasAttribute("href")) {
7323
+ return "link";
7324
+ }
7325
+ break;
7326
+ case "img":
7327
+ if (element.getAttribute("alt") === "" && !ignorePresentationalRole(element, "img")) {
7328
+ return "presentation";
7329
+ }
7330
+ return "img";
7331
+ case "input": {
7332
+ var _ref = element, type = _ref.type;
7333
+ switch (type) {
7334
+ case "button":
7335
+ case "image":
7336
+ case "reset":
7337
+ case "submit":
7338
+ return "button";
7339
+ case "checkbox":
7340
+ case "radio":
7341
+ return type;
7342
+ case "range":
7343
+ return "slider";
7344
+ case "email":
7345
+ case "tel":
7346
+ case "text":
7347
+ case "url":
7348
+ if (element.hasAttribute("list")) {
7349
+ return "combobox";
7350
+ }
7351
+ return "textbox";
7352
+ case "search":
7353
+ if (element.hasAttribute("list")) {
7354
+ return "combobox";
7355
+ }
7356
+ return "searchbox";
7357
+ case "number":
7358
+ return "spinbutton";
7359
+ default:
7360
+ return null;
7361
+ }
7362
+ }
7363
+ case "select":
7364
+ if (element.hasAttribute("multiple") || element.size > 1) {
7365
+ return "listbox";
7366
+ }
7367
+ return "combobox";
7368
+ }
7369
+ return null;
7370
+ }
7371
+ function getExplicitRole(element) {
7372
+ var role = element.getAttribute("role");
7373
+ if (role !== null) {
7374
+ var explicitRole = role.trim().split(" ")[0];
7375
+ if (explicitRole.length > 0) {
7376
+ return explicitRole;
7377
+ }
7378
+ }
7379
+ return null;
7380
+ }
7381
+ var presentationRoles = ["presentation", "none"];
7382
+ function isElement(node) {
7383
+ return node !== null && node.nodeType === node.ELEMENT_NODE;
7384
+ }
7385
+ function isHTMLTableCaptionElement(node) {
7386
+ return isElement(node) && getLocalName(node) === "caption";
7387
+ }
7388
+ function isHTMLInputElement(node) {
7389
+ return isElement(node) && getLocalName(node) === "input";
7390
+ }
7391
+ function isHTMLOptGroupElement(node) {
7392
+ return isElement(node) && getLocalName(node) === "optgroup";
7393
+ }
7394
+ function isHTMLSelectElement(node) {
7395
+ return isElement(node) && getLocalName(node) === "select";
7396
+ }
7397
+ function isHTMLTableElement(node) {
7398
+ return isElement(node) && getLocalName(node) === "table";
7399
+ }
7400
+ function isHTMLTextAreaElement(node) {
7401
+ return isElement(node) && getLocalName(node) === "textarea";
7402
+ }
7403
+ function safeWindow(node) {
7404
+ var _ref = node.ownerDocument === null ? node : node.ownerDocument, defaultView = _ref.defaultView;
7405
+ if (defaultView === null) {
7406
+ throw new TypeError("no window available");
7407
+ }
7408
+ return defaultView;
7409
+ }
7410
+ function isHTMLFieldSetElement(node) {
7411
+ return isElement(node) && getLocalName(node) === "fieldset";
7412
+ }
7413
+ function isHTMLLegendElement(node) {
7414
+ return isElement(node) && getLocalName(node) === "legend";
7415
+ }
7416
+ function isHTMLSlotElement(node) {
7417
+ return isElement(node) && getLocalName(node) === "slot";
7418
+ }
7419
+ function isSVGElement(node) {
7420
+ return isElement(node) && node.ownerSVGElement !== void 0;
7421
+ }
7422
+ function isSVGSVGElement(node) {
7423
+ return isElement(node) && getLocalName(node) === "svg";
7424
+ }
7425
+ function isSVGTitleElement(node) {
7426
+ return isSVGElement(node) && getLocalName(node) === "title";
7427
+ }
7428
+ function queryIdRefs(node, attributeName) {
7429
+ if (isElement(node) && node.hasAttribute(attributeName)) {
7430
+ var ids = node.getAttribute(attributeName).split(" ");
7431
+ var root = node.getRootNode ? node.getRootNode() : node.ownerDocument;
7432
+ return ids.map(function(id) {
7433
+ return root.getElementById(id);
7434
+ }).filter(
7435
+ function(element) {
7436
+ return element !== null;
7437
+ }
7438
+ // TODO: why does this not narrow?
7439
+ );
7440
+ }
7441
+ return [];
7442
+ }
7443
+ function hasAnyConcreteRoles(node, roles) {
7444
+ if (isElement(node)) {
7445
+ return roles.indexOf(getRole(node)) !== -1;
7446
+ }
7447
+ return false;
7448
+ }
7449
+ function asFlatString(s2) {
7450
+ return s2.trim().replace(/\s\s+/g, " ");
7451
+ }
7452
+ function isHidden$1(node, getComputedStyleImplementation) {
7453
+ if (!isElement(node)) {
7454
+ return false;
7455
+ }
7456
+ if (node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true") {
7457
+ return true;
7458
+ }
7459
+ var style = getComputedStyleImplementation(node);
7460
+ return style.getPropertyValue("display") === "none" || style.getPropertyValue("visibility") === "hidden";
7461
+ }
7462
+ function isControl(node) {
7463
+ return hasAnyConcreteRoles(node, ["button", "combobox", "listbox", "textbox"]) || hasAbstractRole(node, "range");
7464
+ }
7465
+ function hasAbstractRole(node, role) {
7466
+ if (!isElement(node)) {
7467
+ return false;
7468
+ }
7469
+ switch (role) {
7470
+ case "range":
7471
+ return hasAnyConcreteRoles(node, ["meter", "progressbar", "scrollbar", "slider", "spinbutton"]);
7472
+ default:
7473
+ throw new TypeError("No knowledge about abstract role '".concat(role, "'. This is likely a bug :("));
7474
+ }
7475
+ }
7476
+ function querySelectorAllSubtree(element, selectors) {
7477
+ var elements = arrayFrom(element.querySelectorAll(selectors));
7478
+ queryIdRefs(element, "aria-owns").forEach(function(root) {
7479
+ elements.push.apply(elements, arrayFrom(root.querySelectorAll(selectors)));
7480
+ });
7481
+ return elements;
7482
+ }
7483
+ function querySelectedOptions(listbox) {
7484
+ if (isHTMLSelectElement(listbox)) {
7485
+ return listbox.selectedOptions || querySelectorAllSubtree(listbox, "[selected]");
7486
+ }
7487
+ return querySelectorAllSubtree(listbox, '[aria-selected="true"]');
7488
+ }
7489
+ function isMarkedPresentational(node) {
7490
+ return hasAnyConcreteRoles(node, presentationRoles);
7491
+ }
7492
+ function isNativeHostLanguageTextAlternativeElement(node) {
7493
+ return isHTMLTableCaptionElement(node);
7494
+ }
7495
+ function allowsNameFromContent(node) {
7496
+ return hasAnyConcreteRoles(node, ["button", "cell", "checkbox", "columnheader", "gridcell", "heading", "label", "legend", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "radio", "row", "rowheader", "switch", "tab", "tooltip", "treeitem"]);
7497
+ }
7498
+ function isDescendantOfNativeHostLanguageTextAlternativeElement(node) {
7499
+ return false;
7500
+ }
7501
+ function getValueOfTextbox(element) {
7502
+ if (isHTMLInputElement(element) || isHTMLTextAreaElement(element)) {
7503
+ return element.value;
7504
+ }
7505
+ return element.textContent || "";
7506
+ }
7507
+ function getTextualContent(declaration) {
7508
+ var content = declaration.getPropertyValue("content");
7509
+ if (/^["'].*["']$/.test(content)) {
7510
+ return content.slice(1, -1);
7511
+ }
7512
+ return "";
7513
+ }
7514
+ function isLabelableElement(element) {
7515
+ var localName = getLocalName(element);
7516
+ return localName === "button" || localName === "input" && element.getAttribute("type") !== "hidden" || localName === "meter" || localName === "output" || localName === "progress" || localName === "select" || localName === "textarea";
7517
+ }
7518
+ function findLabelableElement(element) {
7519
+ if (isLabelableElement(element)) {
7520
+ return element;
7521
+ }
7522
+ var labelableElement = null;
7523
+ element.childNodes.forEach(function(childNode) {
7524
+ if (labelableElement === null && isElement(childNode)) {
7525
+ var descendantLabelableElement = findLabelableElement(childNode);
7526
+ if (descendantLabelableElement !== null) {
7527
+ labelableElement = descendantLabelableElement;
7528
+ }
7529
+ }
7530
+ });
7531
+ return labelableElement;
7532
+ }
7533
+ function getControlOfLabel(label) {
7534
+ if (label.control !== void 0) {
7535
+ return label.control;
7536
+ }
7537
+ var htmlFor = label.getAttribute("for");
7538
+ if (htmlFor !== null) {
7539
+ return label.ownerDocument.getElementById(htmlFor);
7540
+ }
7541
+ return findLabelableElement(label);
7542
+ }
7543
+ function getLabels$1(element) {
7544
+ var labelsProperty = element.labels;
7545
+ if (labelsProperty === null) {
7546
+ return labelsProperty;
7547
+ }
7548
+ if (labelsProperty !== void 0) {
7549
+ return arrayFrom(labelsProperty);
7550
+ }
7551
+ if (!isLabelableElement(element)) {
7552
+ return null;
7553
+ }
7554
+ var document2 = element.ownerDocument;
7555
+ return arrayFrom(document2.querySelectorAll("label")).filter(function(label) {
7556
+ return getControlOfLabel(label) === element;
7557
+ });
7558
+ }
7559
+ function getSlotContents(slot) {
7560
+ var assignedNodes = slot.assignedNodes();
7561
+ if (assignedNodes.length === 0) {
7562
+ return arrayFrom(slot.childNodes);
7563
+ }
7564
+ return assignedNodes;
7565
+ }
7566
+ function computeTextAlternative(root) {
7567
+ var options = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
7568
+ var consultedNodes = new SetLike$1();
7569
+ var computedStyles = typeof Map === "undefined" ? void 0 : /* @__PURE__ */ new Map();
7570
+ var window2 = safeWindow(root);
7571
+ var _options$compute = options.compute, compute = _options$compute === void 0 ? "name" : _options$compute, _options$computedStyl = options.computedStyleSupportsPseudoElements, computedStyleSupportsPseudoElements = _options$computedStyl === void 0 ? options.getComputedStyle !== void 0 : _options$computedStyl, _options$getComputedS = options.getComputedStyle, uncachedGetComputedStyle = _options$getComputedS === void 0 ? window2.getComputedStyle.bind(window2) : _options$getComputedS, _options$hidden = options.hidden, hidden = _options$hidden === void 0 ? false : _options$hidden;
7572
+ var getComputedStyle2 = function getComputedStyle3(el, pseudoElement) {
7573
+ if (pseudoElement !== void 0) {
7574
+ throw new Error("use uncachedGetComputedStyle directly for pseudo elements");
7575
+ }
7576
+ if (computedStyles === void 0) {
7577
+ return uncachedGetComputedStyle(el);
7578
+ }
7579
+ var cachedStyles = computedStyles.get(el);
7580
+ if (cachedStyles) {
7581
+ return cachedStyles;
7582
+ }
7583
+ var style = uncachedGetComputedStyle(el, pseudoElement);
7584
+ computedStyles.set(el, style);
7585
+ return style;
7586
+ };
7587
+ function computeMiscTextAlternative(node, context) {
7588
+ var accumulatedText = "";
7589
+ if (isElement(node) && computedStyleSupportsPseudoElements) {
7590
+ var pseudoBefore = uncachedGetComputedStyle(node, "::before");
7591
+ var beforeContent = getTextualContent(pseudoBefore);
7592
+ accumulatedText = "".concat(beforeContent, " ").concat(accumulatedText);
7593
+ }
7594
+ var childNodes = isHTMLSlotElement(node) ? getSlotContents(node) : arrayFrom(node.childNodes).concat(queryIdRefs(node, "aria-owns"));
7595
+ childNodes.forEach(function(child) {
7596
+ var result = computeTextAlternative2(child, {
7597
+ isEmbeddedInLabel: context.isEmbeddedInLabel,
7598
+ isReferenced: false,
7599
+ recursion: true
7600
+ });
7601
+ var display = isElement(child) ? getComputedStyle2(child).getPropertyValue("display") : "inline";
7602
+ var separator = display !== "inline" ? " " : "";
7603
+ accumulatedText += "".concat(separator).concat(result).concat(separator);
7604
+ });
7605
+ if (isElement(node) && computedStyleSupportsPseudoElements) {
7606
+ var pseudoAfter = uncachedGetComputedStyle(node, "::after");
7607
+ var afterContent = getTextualContent(pseudoAfter);
7608
+ accumulatedText = "".concat(accumulatedText, " ").concat(afterContent);
7609
+ }
7610
+ return accumulatedText.trim();
7611
+ }
7612
+ function useAttribute(element, attributeName) {
7613
+ var attribute = element.getAttributeNode(attributeName);
7614
+ if (attribute !== null && !consultedNodes.has(attribute) && attribute.value.trim() !== "") {
7615
+ consultedNodes.add(attribute);
7616
+ return attribute.value;
7617
+ }
7618
+ return null;
7619
+ }
7620
+ function computeTooltipAttributeValue(node) {
7621
+ if (!isElement(node)) {
7622
+ return null;
7623
+ }
7624
+ return useAttribute(node, "title");
7625
+ }
7626
+ function computeElementTextAlternative(node) {
7627
+ if (!isElement(node)) {
7628
+ return null;
7629
+ }
7630
+ if (isHTMLFieldSetElement(node)) {
7631
+ consultedNodes.add(node);
7632
+ var children = arrayFrom(node.childNodes);
7633
+ for (var i2 = 0; i2 < children.length; i2 += 1) {
7634
+ var child = children[i2];
7635
+ if (isHTMLLegendElement(child)) {
7636
+ return computeTextAlternative2(child, {
7637
+ isEmbeddedInLabel: false,
7638
+ isReferenced: false,
7639
+ recursion: false
7640
+ });
7641
+ }
7642
+ }
7643
+ } else if (isHTMLTableElement(node)) {
7644
+ consultedNodes.add(node);
7645
+ var _children = arrayFrom(node.childNodes);
7646
+ for (var _i = 0; _i < _children.length; _i += 1) {
7647
+ var _child = _children[_i];
7648
+ if (isHTMLTableCaptionElement(_child)) {
7649
+ return computeTextAlternative2(_child, {
7650
+ isEmbeddedInLabel: false,
7651
+ isReferenced: false,
7652
+ recursion: false
7653
+ });
7654
+ }
7655
+ }
7656
+ } else if (isSVGSVGElement(node)) {
7657
+ consultedNodes.add(node);
7658
+ var _children2 = arrayFrom(node.childNodes);
7659
+ for (var _i2 = 0; _i2 < _children2.length; _i2 += 1) {
7660
+ var _child2 = _children2[_i2];
7661
+ if (isSVGTitleElement(_child2)) {
7662
+ return _child2.textContent;
7663
+ }
7664
+ }
7665
+ return null;
7666
+ } else if (getLocalName(node) === "img" || getLocalName(node) === "area") {
7667
+ var nameFromAlt = useAttribute(node, "alt");
7668
+ if (nameFromAlt !== null) {
7669
+ return nameFromAlt;
7670
+ }
7671
+ } else if (isHTMLOptGroupElement(node)) {
7672
+ var nameFromLabel = useAttribute(node, "label");
7673
+ if (nameFromLabel !== null) {
7674
+ return nameFromLabel;
7675
+ }
7676
+ }
7677
+ if (isHTMLInputElement(node) && (node.type === "button" || node.type === "submit" || node.type === "reset")) {
7678
+ var nameFromValue = useAttribute(node, "value");
7679
+ if (nameFromValue !== null) {
7680
+ return nameFromValue;
7681
+ }
7682
+ if (node.type === "submit") {
7683
+ return "Submit";
7684
+ }
7685
+ if (node.type === "reset") {
7686
+ return "Reset";
7687
+ }
7688
+ }
7689
+ var labels = getLabels$1(node);
7690
+ if (labels !== null && labels.length !== 0) {
7691
+ consultedNodes.add(node);
7692
+ return arrayFrom(labels).map(function(element) {
7693
+ return computeTextAlternative2(element, {
7694
+ isEmbeddedInLabel: true,
7695
+ isReferenced: false,
7696
+ recursion: true
7697
+ });
7698
+ }).filter(function(label) {
7699
+ return label.length > 0;
7700
+ }).join(" ");
7701
+ }
7702
+ if (isHTMLInputElement(node) && node.type === "image") {
7703
+ var _nameFromAlt = useAttribute(node, "alt");
7704
+ if (_nameFromAlt !== null) {
7705
+ return _nameFromAlt;
7706
+ }
7707
+ var nameFromTitle = useAttribute(node, "title");
7708
+ if (nameFromTitle !== null) {
7709
+ return nameFromTitle;
7710
+ }
7711
+ return "Submit Query";
7712
+ }
7713
+ if (hasAnyConcreteRoles(node, ["button"])) {
7714
+ var nameFromSubTree = computeMiscTextAlternative(node, {
7715
+ isEmbeddedInLabel: false
7716
+ });
7717
+ if (nameFromSubTree !== "") {
7718
+ return nameFromSubTree;
7719
+ }
7720
+ }
7721
+ return null;
7722
+ }
7723
+ function computeTextAlternative2(current, context) {
7724
+ if (consultedNodes.has(current)) {
7725
+ return "";
7726
+ }
7727
+ if (!hidden && isHidden$1(current, getComputedStyle2) && !context.isReferenced) {
7728
+ consultedNodes.add(current);
7729
+ return "";
7730
+ }
7731
+ var labelAttributeNode = isElement(current) ? current.getAttributeNode("aria-labelledby") : null;
7732
+ var labelElements = labelAttributeNode !== null && !consultedNodes.has(labelAttributeNode) ? queryIdRefs(current, "aria-labelledby") : [];
7733
+ if (compute === "name" && !context.isReferenced && labelElements.length > 0) {
7734
+ consultedNodes.add(labelAttributeNode);
7735
+ return labelElements.map(function(element) {
7736
+ return computeTextAlternative2(element, {
7737
+ isEmbeddedInLabel: context.isEmbeddedInLabel,
7738
+ isReferenced: true,
7739
+ // this isn't recursion as specified, otherwise we would skip
7740
+ // `aria-label` in
7741
+ // <input id="myself" aria-label="foo" aria-labelledby="myself"
7742
+ recursion: false
7743
+ });
7744
+ }).join(" ");
7745
+ }
7746
+ var skipToStep2E = context.recursion && isControl(current) && compute === "name";
7747
+ if (!skipToStep2E) {
7748
+ var ariaLabel = (isElement(current) && current.getAttribute("aria-label") || "").trim();
7749
+ if (ariaLabel !== "" && compute === "name") {
7750
+ consultedNodes.add(current);
7751
+ return ariaLabel;
7752
+ }
7753
+ if (!isMarkedPresentational(current)) {
7754
+ var elementTextAlternative = computeElementTextAlternative(current);
7755
+ if (elementTextAlternative !== null) {
7756
+ consultedNodes.add(current);
7757
+ return elementTextAlternative;
7758
+ }
7759
+ }
6117
7760
  }
6118
- const options = getJiraFieldFilterOptions(field);
6119
- if (!options.length) continue;
6120
- upsertFilterGroup(groups, {
6121
- kind: "jira",
6122
- id: `jira:${normalizedFieldId}`,
6123
- fieldName: normalizedFieldId,
6124
- label: field.name,
6125
- options
6126
- });
7761
+ if (hasAnyConcreteRoles(current, ["menu"])) {
7762
+ consultedNodes.add(current);
7763
+ return "";
7764
+ }
7765
+ if (skipToStep2E || context.isEmbeddedInLabel || context.isReferenced) {
7766
+ if (hasAnyConcreteRoles(current, ["combobox", "listbox"])) {
7767
+ consultedNodes.add(current);
7768
+ var selectedOptions = querySelectedOptions(current);
7769
+ if (selectedOptions.length === 0) {
7770
+ return isHTMLInputElement(current) ? current.value : "";
7771
+ }
7772
+ return arrayFrom(selectedOptions).map(function(selectedOption) {
7773
+ return computeTextAlternative2(selectedOption, {
7774
+ isEmbeddedInLabel: context.isEmbeddedInLabel,
7775
+ isReferenced: false,
7776
+ recursion: true
7777
+ });
7778
+ }).join(" ");
7779
+ }
7780
+ if (hasAbstractRole(current, "range")) {
7781
+ consultedNodes.add(current);
7782
+ if (current.hasAttribute("aria-valuetext")) {
7783
+ return current.getAttribute("aria-valuetext");
7784
+ }
7785
+ if (current.hasAttribute("aria-valuenow")) {
7786
+ return current.getAttribute("aria-valuenow");
7787
+ }
7788
+ return current.getAttribute("value") || "";
7789
+ }
7790
+ if (hasAnyConcreteRoles(current, ["textbox"])) {
7791
+ consultedNodes.add(current);
7792
+ return getValueOfTextbox(current);
7793
+ }
7794
+ }
7795
+ if (allowsNameFromContent(current) || isElement(current) && context.isReferenced || isNativeHostLanguageTextAlternativeElement(current) || isDescendantOfNativeHostLanguageTextAlternativeElement()) {
7796
+ var accumulatedText2F = computeMiscTextAlternative(current, {
7797
+ isEmbeddedInLabel: context.isEmbeddedInLabel
7798
+ });
7799
+ if (accumulatedText2F !== "") {
7800
+ consultedNodes.add(current);
7801
+ return accumulatedText2F;
7802
+ }
7803
+ }
7804
+ if (current.nodeType === current.TEXT_NODE) {
7805
+ consultedNodes.add(current);
7806
+ return current.textContent || "";
7807
+ }
7808
+ if (context.recursion) {
7809
+ consultedNodes.add(current);
7810
+ return computeMiscTextAlternative(current, {
7811
+ isEmbeddedInLabel: context.isEmbeddedInLabel
7812
+ });
7813
+ }
7814
+ var tooltipAttributeValue = computeTooltipAttributeValue(current);
7815
+ if (tooltipAttributeValue !== null) {
7816
+ consultedNodes.add(current);
7817
+ return tooltipAttributeValue;
7818
+ }
7819
+ consultedNodes.add(current);
7820
+ return "";
6127
7821
  }
7822
+ return asFlatString(computeTextAlternative2(root, {
7823
+ isEmbeddedInLabel: false,
7824
+ // by spec computeAccessibleDescription starts with the referenced elements as roots
7825
+ isReferenced: compute === "description",
7826
+ recursion: false
7827
+ }));
6128
7828
  }
6129
- function appendNotionFieldFilterGroups(groups, selectedFields, fields) {
6130
- for (const field of fields) {
6131
- if (!selectedFields.has(field.id) && !selectedFields.has(field.name)) continue;
6132
- if (!NOTION_FILTERABLE_FIELD_TYPES.has(field.type)) continue;
6133
- const options = getNotionFieldFilterOptions(field);
6134
- if (!options.length) continue;
6135
- upsertFilterGroup(groups, {
6136
- kind: "notion",
6137
- id: `notion:${field.id}`,
6138
- fieldName: field.name,
6139
- label: field.name,
6140
- options
6141
- });
6142
- }
7829
+ function prohibitsNaming(node) {
7830
+ return hasAnyConcreteRoles(node, ["caption", "code", "deletion", "emphasis", "generic", "insertion", "none", "paragraph", "presentation", "strong", "subscript", "superscript"]);
6143
7831
  }
6144
- function composeIssueFilterGroups(integrations, customFields) {
6145
- const groups = /* @__PURE__ */ new Map();
6146
- const jiraIntegration = integrations.find((integration) => {
6147
- const config = getJiraConfig(integration);
6148
- return integration.is_active !== false && config;
6149
- });
6150
- appendCustomFieldFilterGroups(groups, customFields);
6151
- const jiraConfig = jiraIntegration ? getJiraConfig(jiraIntegration) : null;
6152
- const configExposedFields = getExposedFields(jiraConfig);
6153
- if (configExposedFields.length) {
6154
- appendJiraFieldFilterGroups(
6155
- groups,
6156
- new Set(configExposedFields),
6157
- getLastKnownJiraFields(jiraConfig)
6158
- );
6159
- }
6160
- for (const integration of integrations) {
6161
- if (integration.is_active === false) continue;
6162
- const notionConfig = getNotionConfig(integration);
6163
- if (!notionConfig) continue;
6164
- appendNotionFieldFilterGroups(
6165
- groups,
6166
- new Set(getSelectedNotionFields(notionConfig)),
6167
- getLastKnownNotionFields(notionConfig)
6168
- );
7832
+ function computeAccessibleName(root) {
7833
+ var options = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
7834
+ if (prohibitsNaming(root)) {
7835
+ return "";
6169
7836
  }
6170
- return Array.from(groups.values()).sort((a2, b) => a2.label.localeCompare(b.label));
7837
+ return computeTextAlternative(root, options);
6171
7838
  }
6172
- const STABLE_ATTR_KEYS = [
6173
- "data-testid",
6174
- "data-test",
6175
- "data-qa",
6176
- "data-cy",
6177
- "name",
6178
- "aria-label"
6179
- ];
6180
7839
  const STATE_CLASS_RE = /^(is-|has-|js-)|^(active|open|selected|disabled|focus|hover|show|hidden|loading|dragging)$/i;
6181
7840
  function looksHashed(segment) {
6182
7841
  if (segment.includes("-")) return false;
@@ -6188,17 +7847,35 @@ function looksHashed(segment) {
6188
7847
  }
6189
7848
  return false;
6190
7849
  }
7850
+ const GENERATED_CLASS_PATTERNS = [
7851
+ /^css-[a-z0-9]+(-|$)/i,
7852
+ // emotion / MUI. 라벨이 붙으면 css-1e6y48t-MuiButtonBase-root
7853
+ /^sc-[a-z0-9]+$/i,
7854
+ // styled-components
7855
+ /^makeStyles-.+-\d+$/,
7856
+ // JSS makeStyles
7857
+ /^jss\d+$/,
7858
+ // JSS 기본 생성기
7859
+ /^ng-tns-c\d+-\d+$/,
7860
+ // Angular 애니메이션
7861
+ /^_[\w-]+_[a-z0-9]{5}_\d+$/
7862
+ // Vite CSS Modules 기본값: _로컬_해시(base36 5자)_줄번호
7863
+ ];
7864
+ function hasCssModulesHash(token) {
7865
+ const index = token.lastIndexOf("__");
7866
+ if (index <= 0) return false;
7867
+ const suffix = token.slice(index + 2);
7868
+ return suffix.length === 5 && /^[\w-]+$/.test(suffix) && /[A-Z0-9_-]/.test(suffix);
7869
+ }
6191
7870
  function isUnstableToken(token) {
6192
7871
  if (!token) return true;
6193
7872
  if (token.includes(":") || token.includes("/")) return true;
6194
7873
  if (token.includes("--")) return true;
6195
- if (/^css-[a-z0-9]+$/i.test(token)) return true;
6196
- if (/^sc-[a-z0-9]+$/i.test(token)) return true;
6197
7874
  if (STATE_CLASS_RE.test(token)) return true;
6198
- const segments = token.split("__");
6199
- if (segments.length > 1 && looksHashed(segments[segments.length - 1] ?? "")) return true;
7875
+ if (GENERATED_CLASS_PATTERNS.some((pattern) => pattern.test(token))) return true;
7876
+ if (hasCssModulesHash(token)) return true;
6200
7877
  if (looksHashed(token)) return true;
6201
- return false;
7878
+ return token.split(/[-_]+/).some(looksHashed);
6202
7879
  }
6203
7880
  function isStableClassName(cls) {
6204
7881
  return !isUnstableToken(cls);
@@ -6211,6 +7888,297 @@ function isUnstableId(id) {
6211
7888
  if (looksHashed(id)) return true;
6212
7889
  return false;
6213
7890
  }
7891
+ function intersection(firstArr, secondArr) {
7892
+ const secondSet = new Set(secondArr);
7893
+ return firstArr.filter((item) => secondSet.has(item));
7894
+ }
7895
+ function uniq(arr) {
7896
+ return [...new Set(arr)];
7897
+ }
7898
+ function union(arr1, arr2) {
7899
+ return uniq(arr1.concat(arr2));
7900
+ }
7901
+ const LOCATOR_TEXT_MAX = 80;
7902
+ const RAW_SLICE_FACTOR = 8;
7903
+ const LOCATOR_RAW_TEXT_MAX = LOCATOR_TEXT_MAX * RAW_SLICE_FACTOR;
7904
+ function dropTrailingLoneSurrogate(value) {
7905
+ return /[\uD800-\uDBFF]$/.test(value) ? value.slice(0, -1) : value;
7906
+ }
7907
+ function cleanLocatorText(value, max2 = LOCATOR_TEXT_MAX) {
7908
+ if (!value) return void 0;
7909
+ const bounded = value.slice(0, max2 * RAW_SLICE_FACTOR);
7910
+ const cleaned = dropTrailingLoneSurrogate(
7911
+ redactSensitiveValues(bounded.replace(/\s+/g, " ").trim()).slice(0, max2)
7912
+ ).trim();
7913
+ return cleaned || void 0;
7914
+ }
7915
+ function normalizeForCompare(value) {
7916
+ return value.toLowerCase().replace(/\d+/g, "#");
7917
+ }
7918
+ function jaccard(a2, b) {
7919
+ const all = union(uniq(a2), uniq(b));
7920
+ if (all.length === 0) return 0;
7921
+ return intersection(uniq(a2), uniq(b)).length / all.length;
7922
+ }
7923
+ function textSimilarity(a2, b) {
7924
+ const left = normalizeForCompare(a2);
7925
+ const right = normalizeForCompare(b);
7926
+ if (left === right) return 1;
7927
+ const [shorter, longer] = left.length <= right.length ? [left, right] : [right, left];
7928
+ const containment = shorter && longer.startsWith(shorter) ? shorter.length / longer.length : 0;
7929
+ return Math.max(jaccard(left.split(" "), right.split(" ")), containment);
7930
+ }
7931
+ function labelSimilarity(stored, observed) {
7932
+ const storedWords = new Set(normalizeForCompare(stored).split(" "));
7933
+ const observedWords = new Set(normalizeForCompare(observed).split(" "));
7934
+ const [smaller, larger] = storedWords.size <= observedWords.size ? [storedWords, observedWords] : [observedWords, storedWords];
7935
+ const isStrictSubset = larger.size > smaller.size && [...smaller].every((w) => larger.has(w));
7936
+ return isStrictSubset ? 0 : textSimilarity(stored, observed);
7937
+ }
7938
+ function digitGroups(value) {
7939
+ return (value.match(/\d+/g) ?? []).join(" ");
7940
+ }
7941
+ function sameDigitGroups(a2, b) {
7942
+ return digitGroups(a2) === digitGroups(b);
7943
+ }
7944
+ function labelSimilarityKeepingDigits(stored, observed) {
7945
+ const left = stored.replace(/\s+/g, " ").trim();
7946
+ const right = observed.replace(/\s+/g, " ").trim();
7947
+ return sameDigitGroups(left, right) ? labelSimilarity(left, right) : 0;
7948
+ }
7949
+ const TEST_ATTR_KEYS = ["data-testid", "data-test", "data-qa", "data-cy"];
7950
+ const OTHER_ATTR_KEYS = ["name", "type", "placeholder", "alt", "title", "href", "src"];
7951
+ const URL_ATTR_KEYS = /* @__PURE__ */ new Set(["href", "src"]);
7952
+ const ATTR_VALUE_MAX = 120;
7953
+ const CLASS_LIMIT = 5;
7954
+ const LOCATOR_SIZE_MAX = 1024;
7955
+ const CONTAINER_ROLES = /* @__PURE__ */ new Set(["dialog", "alertdialog", "menu", "listbox", "tooltip"]);
7956
+ const CONTAINER_SELECTOR = 'dialog, [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], [role="tooltip"]';
7957
+ const LANDMARK_ROLES = /* @__PURE__ */ new Set([
7958
+ "banner",
7959
+ "navigation",
7960
+ "main",
7961
+ "complementary",
7962
+ "contentinfo",
7963
+ "region",
7964
+ "form",
7965
+ "search"
7966
+ ]);
7967
+ const NAMED_ONLY_LANDMARKS = /* @__PURE__ */ new Set(["region", "form"]);
7968
+ const ROW_SELECTOR = 'li, tr, [role="row"], [role="listitem"], [role="option"]';
7969
+ const HEADING_SELECTOR = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
7970
+ function safeRole(element) {
7971
+ try {
7972
+ return getRole(element) ?? void 0;
7973
+ } catch {
7974
+ return void 0;
7975
+ }
7976
+ }
7977
+ function safeAccessibleName(element) {
7978
+ try {
7979
+ return cleanLocatorText(computeAccessibleName(element));
7980
+ } catch {
7981
+ return void 0;
7982
+ }
7983
+ }
7984
+ function landmarkRole(node) {
7985
+ const role = safeRole(node);
7986
+ if (role === void 0 || !LANDMARK_ROLES.has(role)) return void 0;
7987
+ if (NAMED_ONLY_LANDMARKS.has(role) && !safeAccessibleName(node)) return void 0;
7988
+ return role;
7989
+ }
7990
+ function collectHeadings(doc) {
7991
+ return Array.from(doc.querySelectorAll(HEADING_SELECTOR));
7992
+ }
7993
+ function matchesMask(element) {
7994
+ try {
7995
+ return element.matches(SESSION_REPLAY_FULL_MASK_SELECTOR);
7996
+ } catch {
7997
+ return true;
7998
+ }
7999
+ }
8000
+ function hasMaskedDescendant(root) {
8001
+ try {
8002
+ return root.querySelector(SESSION_REPLAY_FULL_MASK_SELECTOR) !== null;
8003
+ } catch {
8004
+ return true;
8005
+ }
8006
+ }
8007
+ function visibleText(root) {
8008
+ if (!hasMaskedDescendant(root)) return root.textContent ?? "";
8009
+ const walker = root.ownerDocument.createTreeWalker(
8010
+ root,
8011
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
8012
+ (node) => node.nodeType === Node.ELEMENT_NODE && matchesMask(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
8013
+ );
8014
+ let text = "";
8015
+ for (let node = walker.nextNode(); node && text.length < LOCATOR_RAW_TEXT_MAX; ) {
8016
+ if (node.nodeType === Node.TEXT_NODE) text += node.nodeValue ?? "";
8017
+ node = walker.nextNode();
8018
+ }
8019
+ return text;
8020
+ }
8021
+ function closestAncestor(element, matches) {
8022
+ let current = element.parentElement;
8023
+ while (current) {
8024
+ if (matches(current)) return current;
8025
+ current = current.parentElement;
8026
+ }
8027
+ return null;
8028
+ }
8029
+ function nearestPrecedingHeading(element, headings) {
8030
+ let found = null;
8031
+ for (const heading of headings) {
8032
+ if (heading === element || heading.contains(element)) continue;
8033
+ if (heading.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING) {
8034
+ found = heading;
8035
+ } else {
8036
+ break;
8037
+ }
8038
+ }
8039
+ return found;
8040
+ }
8041
+ function rowTextOf(element) {
8042
+ const row = element.closest(ROW_SELECTOR);
8043
+ if (!row) return void 0;
8044
+ const label = Array.from(row.children).find(
8045
+ (cell) => !cell.contains(element) && visibleText(cell).trim()
8046
+ );
8047
+ const source = label ?? row;
8048
+ if (isElementFullyMasked(source)) return void 0;
8049
+ return cleanLocatorText(visibleText(source));
8050
+ }
8051
+ function urlPath(value, doc) {
8052
+ try {
8053
+ const url = new URL(value, doc.baseURI);
8054
+ return url.protocol === "http:" || url.protocol === "https:" ? url.pathname : void 0;
8055
+ } catch {
8056
+ return void 0;
8057
+ }
8058
+ }
8059
+ function attrValue(element, key) {
8060
+ const raw = element.getAttribute(key);
8061
+ if (!raw) return void 0;
8062
+ return cleanLocatorText(
8063
+ URL_ATTR_KEYS.has(key) ? urlPath(raw, element.ownerDocument) : raw,
8064
+ ATTR_VALUE_MAX
8065
+ );
8066
+ }
8067
+ function sharesUrlPath(element, key, value) {
8068
+ return Array.from(element.ownerDocument.getElementsByTagName(element.tagName)).some(
8069
+ (other) => other !== element && attrValue(other, key) === value
8070
+ );
8071
+ }
8072
+ function collectAttrs(element, masked, uniqueUrlAttrs) {
8073
+ const attrs = {};
8074
+ const keys = masked ? TEST_ATTR_KEYS : [...TEST_ATTR_KEYS, ...OTHER_ATTR_KEYS];
8075
+ for (const key of keys) {
8076
+ const value = attrValue(element, key);
8077
+ if (!value) continue;
8078
+ if (uniqueUrlAttrs && URL_ATTR_KEYS.has(key) && sharesUrlPath(element, key, value)) continue;
8079
+ attrs[key] = value;
8080
+ }
8081
+ return Object.keys(attrs).length ? attrs : void 0;
8082
+ }
8083
+ function collectClasses(element) {
8084
+ const classes = Array.from(element.classList).filter(isStableClassName).slice(0, CLASS_LIMIT);
8085
+ return classes.length ? classes : void 0;
8086
+ }
8087
+ function tagIndex(element, scope) {
8088
+ return Array.from(scope.getElementsByTagName(element.tagName)).indexOf(element);
8089
+ }
8090
+ function labelSources(element) {
8091
+ var _a;
8092
+ const doc = element.ownerDocument;
8093
+ const ids = ((_a = element.getAttribute("aria-labelledby")) == null ? void 0 : _a.split(/\s+/).filter(Boolean)) ?? [];
8094
+ const sources = ids.map((id) => doc.getElementById(id)).filter((node) => node !== null);
8095
+ const labels = element.labels;
8096
+ if (labels) sources.push(...Array.from(labels));
8097
+ const wrapping = element.closest("label");
8098
+ if (wrapping) sources.push(wrapping);
8099
+ return sources;
8100
+ }
8101
+ function labelSourceMasked(element) {
8102
+ return labelSources(element).some(
8103
+ (node) => isElementFullyMasked(node) || hasMaskedDescendant(node)
8104
+ );
8105
+ }
8106
+ function capSize(locator) {
8107
+ const size = () => JSON.stringify(locator).length;
8108
+ if (size() <= LOCATOR_SIZE_MAX) return locator;
8109
+ delete locator.classes;
8110
+ if (size() <= LOCATOR_SIZE_MAX) return locator;
8111
+ if (locator.context) delete locator.context.heading;
8112
+ if (size() <= LOCATOR_SIZE_MAX) return locator;
8113
+ if (locator.attrs) {
8114
+ for (const key of OTHER_ATTR_KEYS) delete locator.attrs[key];
8115
+ }
8116
+ if (size() <= LOCATOR_SIZE_MAX) return locator;
8117
+ delete locator.context;
8118
+ if (size() <= LOCATOR_SIZE_MAX) return locator;
8119
+ delete locator.text;
8120
+ return locator;
8121
+ }
8122
+ function buildElementLocator(element, options = {}) {
8123
+ try {
8124
+ return buildLocatorUnsafe(element, options);
8125
+ } catch {
8126
+ return { v: 1, tag: element.localName || element.tagName.toLowerCase() };
8127
+ }
8128
+ }
8129
+ function buildLocatorUnsafe(element, options) {
8130
+ const doc = element.ownerDocument;
8131
+ const masked = isElementFullyMasked(element);
8132
+ const locator = { v: 1, tag: element.tagName.toLowerCase() };
8133
+ const role = safeRole(element);
8134
+ if (role) locator.role = role;
8135
+ if (!masked) {
8136
+ const nameMasked = hasMaskedDescendant(element) || labelSourceMasked(element);
8137
+ if (options.includeName !== false && !nameMasked) {
8138
+ const name = safeAccessibleName(element);
8139
+ if (name) locator.name = name;
8140
+ }
8141
+ const text = cleanLocatorText(visibleText(element));
8142
+ if (text) locator.text = text;
8143
+ }
8144
+ const attrs = collectAttrs(element, masked, options.uniqueUrlAttrs !== false);
8145
+ if (attrs) locator.attrs = attrs;
8146
+ const classes = collectClasses(element);
8147
+ if (classes) locator.classes = classes;
8148
+ const container = closestAncestor(element, (node) => {
8149
+ const nodeRole = safeRole(node);
8150
+ return nodeRole !== void 0 && CONTAINER_ROLES.has(nodeRole);
8151
+ });
8152
+ const landmark = closestAncestor(element, (node) => landmarkRole(node) !== void 0);
8153
+ const heading = nearestPrecedingHeading(element, options.headings ?? collectHeadings(doc));
8154
+ const rowText = masked ? void 0 : rowTextOf(element);
8155
+ const context = {};
8156
+ if (container) {
8157
+ const containerName = isElementFullyMasked(container) || hasMaskedDescendant(container) || labelSourceMasked(container) ? void 0 : safeAccessibleName(container);
8158
+ context.container = {
8159
+ role: safeRole(container),
8160
+ ...containerName ? { name: containerName } : {}
8161
+ };
8162
+ }
8163
+ if (landmark) context.landmark = landmarkRole(landmark);
8164
+ const headingText = heading && !isElementFullyMasked(heading) ? cleanLocatorText(visibleText(heading)) : void 0;
8165
+ if (headingText) context.heading = headingText;
8166
+ if (rowText) context.rowText = rowText;
8167
+ if (Object.keys(context).length) locator.context = context;
8168
+ if (options.includeRoleIndex !== false) {
8169
+ const index = tagIndex(element, container ?? doc);
8170
+ if (index >= 0) locator.roleIndex = index;
8171
+ }
8172
+ return capSize(locator);
8173
+ }
8174
+ const STABLE_ATTR_KEYS = [
8175
+ "data-testid",
8176
+ "data-test",
8177
+ "data-qa",
8178
+ "data-cy",
8179
+ "name",
8180
+ "aria-label"
8181
+ ];
6214
8182
  function cssAttrValue(value) {
6215
8183
  return value.replace(/(["\\])/g, "\\$1");
6216
8184
  }
@@ -6279,8 +8247,8 @@ function getElementSelector(element) {
6279
8247
  const segments = [];
6280
8248
  let current = element;
6281
8249
  let depth = 0;
6282
- const MAX_DEPTH = 5;
6283
- while (current && current.nodeType === Node.ELEMENT_NODE && depth < MAX_DEPTH) {
8250
+ const MAX_DEPTH2 = 5;
8251
+ while (current && current.nodeType === Node.ELEMENT_NODE && depth < MAX_DEPTH2) {
6284
8252
  segments.unshift(structuralSegment(current));
6285
8253
  const candidate = segments.join(" > ");
6286
8254
  if (matchesUniquely(candidate, element)) return candidate;
@@ -6320,9 +8288,15 @@ function getXPath(element) {
6320
8288
  }
6321
8289
  return parts.length ? `/html/body/${parts.join("/")}` : "/html/body";
6322
8290
  }
6323
- function getElementPosition(element, event) {
8291
+ function getElementPosition(element, event, options = {}) {
6324
8292
  const rect = element.getBoundingClientRect();
6325
8293
  const scroll = getScrollPosition();
8294
+ let component = null;
8295
+ try {
8296
+ component = (options.collectComponent ?? collectComponentPath)(element);
8297
+ } catch {
8298
+ component = null;
8299
+ }
6326
8300
  return {
6327
8301
  x: event ? event.clientX + scroll.x : rect.left + scroll.x,
6328
8302
  y: event ? event.clientY + scroll.y : rect.top + scroll.y,
@@ -6334,7 +8308,9 @@ function getElementPosition(element, event) {
6334
8308
  top: rect.top + scroll.y,
6335
8309
  width: rect.width,
6336
8310
  height: rect.height
6337
- }
8311
+ },
8312
+ locator: buildElementLocator(element),
8313
+ ...component ? { component } : {}
6338
8314
  };
6339
8315
  }
6340
8316
  function findElementByXPath(xpath) {
@@ -7964,15 +9940,133 @@ var Anchor2 = PopoverAnchor$1;
7964
9940
  var Trigger = PopoverTrigger$1;
7965
9941
  var Portal = PopoverPortal;
7966
9942
  var Content2 = PopoverContent$1;
7967
- const Popover = Root2;
9943
+ const FLOATING_LAYER_SELECTOR = "[data-radix-popper-content-wrapper]";
9944
+ const OUTSIDE_EVENTS = ["pointerdown", "focusin"];
9945
+ const roots = /* @__PURE__ */ new Map();
9946
+ function isInsideLayer(path, content) {
9947
+ return path.some((node) => {
9948
+ if (!(node instanceof Element)) return false;
9949
+ if (node === content) return true;
9950
+ if (content.id && node.getAttribute("aria-controls") === content.id) return true;
9951
+ return node.matches(FLOATING_LAYER_SELECTOR) && !node.contains(content);
9952
+ });
9953
+ }
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 } = {}) {
9996
+ const onDismissRef = useRef(onDismiss);
9997
+ const detachRef = useRef(null);
9998
+ useEffect(() => {
9999
+ onDismissRef.current = onDismiss;
10000
+ }, [onDismiss]);
10001
+ useEffect(() => () => {
10002
+ var _a;
10003
+ return (_a = detachRef.current) == null ? void 0 : _a.call(detachRef);
10004
+ }, []);
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
+ );
10024
+ }
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];
10059
+ }
10060
+ const [Popover, usePopoverContentDismissRef] = createShadowDismissRoot(Root2);
7968
10061
  const PopoverTrigger = Trigger;
7969
10062
  const PopoverAnchor = Anchor2;
7970
10063
  const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 6, onEscapeKeyDown, ...props }, ref) => {
7971
10064
  const portalContainer = usePortalContainer();
10065
+ const contentRef = usePopoverContentDismissRef(ref);
7972
10066
  return /* @__PURE__ */ jsx(Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
7973
10067
  Content2,
7974
10068
  {
7975
- ref,
10069
+ ref: contentRef,
7976
10070
  align,
7977
10071
  sideOffset,
7978
10072
  onEscapeKeyDown: stopEscapePropagation(onEscapeKeyDown),
@@ -9354,7 +11448,9 @@ const X = createLucideIcon("X", [
9354
11448
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
9355
11449
  ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
9356
11450
  ]);
9357
- const Select$1 = Select$2;
11451
+ const [Select$1, useSelectContentDismissRef] = createShadowDismissRoot(Select$2, {
11452
+ focusOutside: false
11453
+ });
9358
11454
  const SelectValue = SelectValue$1;
9359
11455
  const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
9360
11456
  SelectTrigger$1,
@@ -9406,10 +11502,11 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
9406
11502
  SelectScrollDownButton.displayName = SelectScrollDownButton$1.displayName;
9407
11503
  const SelectContent = React.forwardRef(({ className, children, position = "popper", onEscapeKeyDown, ...props }, ref) => {
9408
11504
  const portalContainer = usePortalContainer();
11505
+ const contentRef = useSelectContentDismissRef(ref);
9409
11506
  return /* @__PURE__ */ jsx(SelectPortal, { container: portalContainer, children: /* @__PURE__ */ jsxs(
9410
11507
  SelectContent$1,
9411
11508
  {
9412
- ref,
11509
+ ref: contentRef,
9413
11510
  "data-testid": "select-content",
9414
11511
  sideOffset: 6,
9415
11512
  onEscapeKeyDown: stopEscapePropagation(onEscapeKeyDown),
@@ -13977,6 +16074,7 @@ function DatePicker({
13977
16074
  };
13978
16075
  const showClear = clearable && Boolean(value) && !disabled;
13979
16076
  const selectedDate = fromIso(value);
16077
+ const dismissRef = useShadowOutsideDismiss(React.useCallback(() => setOpen(false), []));
13980
16078
  return /* @__PURE__ */ jsxs(Root2, { open, onOpenChange: setOpen, children: [
13981
16079
  /* @__PURE__ */ jsx(
13982
16080
  DatePickerTriggerField,
@@ -13994,6 +16092,7 @@ function DatePicker({
13994
16092
  /* @__PURE__ */ jsx(Portal, { container: portalContainer, children: /* @__PURE__ */ jsx(
13995
16093
  Content2,
13996
16094
  {
16095
+ ref: dismissRef,
13997
16096
  align,
13998
16097
  side: "bottom",
13999
16098
  sideOffset: 6,
@@ -15109,50 +17208,6 @@ function useOnboardingFlow() {
15109
17208
  [context]
15110
17209
  );
15111
17210
  }
15112
- let nextId = 0;
15113
- let toasts = [];
15114
- const listeners = /* @__PURE__ */ new Set();
15115
- function emit() {
15116
- listeners.forEach((l2) => l2());
15117
- }
15118
- function addToast(message2, type, options) {
15119
- const id = `toast-${++nextId}`;
15120
- const t2 = {
15121
- id,
15122
- message: message2,
15123
- type,
15124
- action: options == null ? void 0 : options.action,
15125
- duration: (options == null ? void 0 : options.duration) ?? 2e3,
15126
- createdAt: Date.now()
15127
- };
15128
- toasts = [...toasts, t2];
15129
- emit();
15130
- return id;
15131
- }
15132
- function dismiss(id) {
15133
- toasts = toasts.filter((t2) => t2.id !== id);
15134
- emit();
15135
- }
15136
- function dismissAll() {
15137
- toasts = [];
15138
- emit();
15139
- }
15140
- function toast(message2, options) {
15141
- return addToast(message2, "default", options);
15142
- }
15143
- toast.success = (message2, options) => addToast(message2, "success", options);
15144
- toast.warning = (message2, options) => addToast(message2, "warning", options);
15145
- toast.error = (message2, options) => addToast(message2, "error", options);
15146
- toast.info = (message2, options) => addToast(message2, "info", options);
15147
- toast.dismiss = dismiss;
15148
- toast.dismissAll = dismissAll;
15149
- function subscribe(listener) {
15150
- listeners.add(listener);
15151
- return () => listeners.delete(listener);
15152
- }
15153
- function getSnapshot() {
15154
- return toasts;
15155
- }
15156
17211
  ({
15157
17212
  type: [NetworkLogType.XMLHTTPREQUEST, NetworkLogType.FETCH]
15158
17213
  });
@@ -15294,20 +17349,6 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
15294
17349
  return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
15295
17350
  });
15296
17351
  }
15297
- const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
15298
- async function uploadSocketTranscript(api, snapshot, options = {}) {
15299
- if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
15300
- try {
15301
- const file = new File([snapshot.payload], "socket-transcript.json.gz", {
15302
- type: TRANSCRIPT_CONTENT_TYPE
15303
- });
15304
- const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
15305
- return { uploadId: uploaded.id };
15306
- } catch (error) {
15307
- devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
15308
- return null;
15309
- }
15310
- }
15311
17352
  function buildExternalUserPayload(user) {
15312
17353
  if (!user) return {};
15313
17354
  return {
@@ -15316,91 +17357,115 @@ function buildExternalUserPayload(user) {
15316
17357
  };
15317
17358
  }
15318
17359
  export {
15319
- Portal as $,
15320
- trimLogBackupPayload as A,
15321
- createSafeLogBackup as B,
17360
+ SelectValue as $,
17361
+ SessionReplayCaptureController as A,
17362
+ preloadModernScreenshot as B,
15322
17363
  ConsoleCollector as C,
15323
- getLogBackupSessionId as D,
15324
- createIndexedDbLogBackup as E,
15325
- PopoverTrigger as F,
15326
- Input as G,
15327
- PopoverContent as H,
17364
+ enqueueIssueUploads as D,
17365
+ normalizeUploadMimeType as E,
17366
+ toSessionReplayMeta as F,
17367
+ loadTusClient as G,
17368
+ createIndexedDbPendingUploadStore as H,
15328
17369
  ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
15329
- Select$1 as J,
15330
- SelectTrigger as K,
15331
- Label as L,
15332
- SelectValue as M,
17370
+ ATTACHMENT_UPLOAD_DB_NAME as J,
17371
+ createMediaUploadApi as K,
17372
+ toast as L,
17373
+ trimLogBackupPayload as M,
15333
17374
  NetworkCollector as N,
15334
- SelectContent as O,
15335
- Popover as P,
15336
- SelectItem as Q,
15337
- JIRA_ISSUE_TYPE_VALUE_KEY as R,
17375
+ createSafeLogBackup as O,
17376
+ getLogBackupSessionId as P,
17377
+ createIndexedDbLogBackup as Q,
17378
+ Popover as R,
15338
17379
  ScreenRecordingController as S,
15339
- getJiraMetadata as T,
15340
- buildJiraIssueFields as U,
15341
- getJiraIssueTypeLabel as V,
17380
+ PopoverTrigger as T,
17381
+ Input as U,
17382
+ VIDEO_UPLOAD_DB_NAME as V,
15342
17383
  WebSocketCollector as W,
15343
- Button$1 as X,
15344
- Root2 as Y,
15345
- Trigger as Z,
15346
- ChevronDown as _,
17384
+ PopoverContent as X,
17385
+ Label as Y,
17386
+ Select$1 as Z,
17387
+ SelectTrigger as _,
15347
17388
  uploadSignedMediaWithApi as a,
15348
- Content2 as a0,
15349
- Check as a1,
15350
- X as a2,
15351
- DatePicker as a3,
15352
- getNotionPageMetadata as a4,
15353
- buildNotionFieldViews as a5,
15354
- DirectUploadError as a6,
15355
- toast as a7,
15356
- getSnapshot as a8,
15357
- subscribe as a9,
15358
- getRegisteredUploadId as aa,
15359
- ChevronRight as ab,
15360
- ReactRemoveScroll as ac,
15361
- hideOthers as ad,
15362
- useFocusGuards as ae,
15363
- FocusScope as af,
15364
- getExposedJiraFields as ag,
15365
- isJiraFieldExposed as ah,
15366
- normalizeExposedJiraFields as ai,
15367
- resolveJiraIssueTypeId as aj,
15368
- pickJiraIssueTypeFields as ak,
15369
- switchJiraIssueType as al,
15370
- getPrimaryJiraMapping as am,
15371
- createCollection as an,
15372
- useDirection as ao,
15373
- getPrimaryNotionMapping as ap,
15374
- buttonVariants as aq,
15375
- findElementBySelector as ar,
15376
- findElementByXPath as as,
15377
- OnboardingProvider as at,
15378
- useOnboardingTarget as au,
15379
- useOnboardingFlow as av,
15380
- PopoverAnchor as aw,
15381
- buildJiraCreateFields as b,
15382
- 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,
15383
17448
  directUploadToSignedUrl as d,
15384
- buildNotionCreateFields as e,
15385
- buildGuestOwnerQuery as f,
17449
+ buildJiraCreateFields as e,
17450
+ buildNotionCreateFields as f,
15386
17451
  getElementPosition as g,
15387
- composeIssueFilterGroups as h,
15388
- buildIssueIntegrationsPayload as i,
15389
- devLogger as j,
15390
- filterExcludeServiceRelatedLogs as k,
15391
- filterExcludeServiceRelatedConsoleLogs as l,
15392
- compactNetworkLogsForTransport as m,
15393
- CONSOLE_LOG_SUBMIT_LIMIT as n,
15394
- 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,
15395
17460
  pristineFetch as p,
15396
- shrinkNetworkLogsToFit as q,
15397
- measureJsonBytes as r,
17461
+ buildExternalUserPayload as q,
17462
+ shrinkNetworkLogsToFit as r,
15398
17463
  selectNetworkLogsForSubmit as s,
15399
- sanitizeString as t,
17464
+ measureJsonBytes as t,
15400
17465
  uploadScreenshotDataUrls as u,
15401
- serializeSocketTranscript as v,
15402
- uploadSocketTranscript as w,
15403
- getCurrentPageUrl as x,
15404
- SessionReplayCaptureController as y,
15405
- preloadModernScreenshot as z
17466
+ sanitizeString as v,
17467
+ collectAttachmentUploads as w,
17468
+ hasReplayUpload as x,
17469
+ getCurrentPageUrl as y,
17470
+ createIssueUploadQueues as z
15406
17471
  };