@solhun/feedback-kit-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/uuid.ts","../src/guest-id.ts","../src/context.ts","../src/report.ts","../src/queue.ts","../src/keepalive.ts","../src/ring-buffer.ts","../src/diagnostics.ts","../src/config.ts","../src/source-attr.ts","../src/widget/screenshot.ts","../src/widget/pin.ts","../src/widget/focus.ts","../src/widget/modal.ts","../src/widget/controller.ts","../src/adapters/body.ts","../src/adapters/linear.ts","../src/adapters/notion.ts","../src/adapters/lasso.ts"],"sourcesContent":["// @solhun/feedback-kit-core 공개 진입점.\n//\n// 코어는 플랫폼에 의존하지 않는다(DOM·React Native API를 참조하지 않는다).\n// 웹/앱 패키지가 이 위에 각자의 UI를 얹고, 어댑터가 전송 대상을 결정한다.\n\nexport type {\n AppInfo,\n ContextUser,\n DeviceInfo,\n DiagLogEntry,\n DiagNetworkEntry,\n DiagRequestRef,\n DiagnosticsExcludeMatcher,\n DiagnosticsPayload,\n DisplayInfo,\n ElementInfo,\n FeedbackAdapter,\n FeedbackContext,\n FeedbackKind,\n FeedbackPin,\n FeedbackPriority,\n FeedbackReport,\n FeedbackScreenshot,\n FeedbackStorage,\n FeedbackUser,\n GetCurrentScreenFn,\n GetUserFn,\n NativeContext,\n SourceMapping,\n SubmitResult,\n WebContext,\n} from \"./types.js\";\n\nexport type {\n BuildContextOpts,\n ContextProviders,\n WebContextInput,\n} from \"./context.js\";\nexport { buildContext, normalizeUser, resolveContextUser } from \"./context.js\";\n\nexport type { BuildReportOpts, ReportParts } from \"./report.js\";\nexport { buildReport } from \"./report.js\";\n\nexport type {\n FeedbackQueueOpts,\n QueueStatus,\n QueueStatusListener,\n SubmitOutcome,\n} from \"./queue.js\";\nexport {\n BACKOFF_BASE_MS,\n BACKOFF_MAX_MS,\n defaultBackoff,\n FeedbackQueue,\n MAX_QUEUE_AGE_MS,\n MAX_QUEUE_ITEMS,\n RATE_LIMIT_MIN_DELAY_MS,\n} from \"./queue.js\";\n\nexport {\n byteLengthOf,\n canUseKeepalive,\n KEEPALIVE_BODY_LIMIT_BYTES,\n} from \"./keepalive.js\";\n\nexport type { DiagnosticsInstallOpts } from \"./diagnostics.js\";\nexport {\n DiagnosticsCollector,\n LOG_BUFFER_LIMIT,\n MAX_LOG_MESSAGE_CHARS,\n NETWORK_BUFFER_LIMIT,\n REDACTED,\n sanitizeUrl,\n sharedDiagnostics,\n} from \"./diagnostics.js\";\nexport { RingBuffer } from \"./ring-buffer.js\";\n\nexport type {\n AdapterName,\n ConfigWarning,\n ConfigWarningCode,\n DiagnosticsSource,\n FeedbackConfig,\n ResolvedConfig,\n Visibility,\n VisibilityEnv,\n VisibilityFn,\n WidgetCorner,\n WidgetPosition,\n} from \"./config.js\";\nexport {\n DEFAULT_ADAPTER,\n DEFAULT_INTERNAL_ROLES,\n DEFAULT_POSITION,\n detectDevBuild,\n diagnosticsProviderFor,\n ENDPOINT_FROM_ADAPTER,\n isInternalUser,\n resolveConfig,\n shouldShowWidget,\n} from \"./config.js\";\n\nexport type { SourceLocation } from \"./source-attr.js\";\nexport { parseSourceAttr, SOURCE_ATTR, sourceFromElement } from \"./source-attr.js\";\n\n// 위젯 헤드리스 계층. 화면 지도·리포트 모달·핀 지정·스크린샷 정책·포커스 순서.\nexport type {\n CaptureWithinLimitOpts,\n ModalQueueLike,\n ModalSubmitStatus,\n PixelPoint,\n PixelSize,\n ReportModalListener,\n ReportModalOpts,\n ReportModalState,\n ScreenshotCapture,\n ScreenshotOutcome,\n ScreenshotReencode,\n ScreenshotStatus,\n WidgetControllerOpts,\n WidgetListener,\n WidgetPlatform,\n WidgetScreen,\n WidgetState,\n} from \"./widget/index.js\";\nexport {\n captureWithinLimit,\n COMMENT_MAX_CHARS,\n COMMENT_REQUIRED_MESSAGE,\n COMMENT_TOO_LONG_MESSAGE,\n denormalizePin,\n FLOATING_BUTTON_ID,\n FocusRing,\n MODAL_ACTION_ATTACH,\n MODAL_ACTION_CANCEL,\n MODAL_ACTION_PICK,\n MODAL_ACTION_PIN,\n MODAL_ACTION_REMOVE_SCREENSHOT,\n MODAL_ACTION_RETRY,\n MODAL_ACTION_SEND,\n MODAL_FIELD_COMMENT,\n MODAL_FIELD_PRIORITY,\n normalizePin,\n PinController,\n ReportModalController,\n SCREENSHOT_FAILED_MESSAGE,\n SCREENSHOT_MAX_BASE64_BYTES,\n SCREENSHOT_QUALITY_STEPS,\n screenshotBytes,\n SUBMIT_DONE_MESSAGE,\n SUBMIT_FAILED_MESSAGE,\n SUBMIT_PENDING_MESSAGE,\n WidgetController,\n} from \"./widget/index.js\";\n\n// 어댑터. 코어의 다른 모듈은 이 아래를 import 하지 않는다 — 의존 방향은\n// \"어댑터 → 코어\" 한쪽뿐이고, 되짚어 참조하는 순간 어댑터 교체가 거짓이 된다.\nexport type {\n LassoAdapterOpts,\n LassoEnvelope,\n LassoFetch,\n LassoRequestInit,\n LassoResponseLike,\n LinearIssueInput,\n NotionPageInput,\n} from \"./adapters/index.js\";\nexport {\n buildLassoEnvelope,\n buildLinearIssue,\n buildNotionPage,\n buildTitle,\n createLassoAdapter,\n LASSO_DEFAULT_ENDPOINT,\n renderReportBody,\n TITLE_MAX_CHARS,\n} from \"./adapters/index.js\";\n\nexport { getOrCreateGuestId, guestUser } from \"./guest-id.js\";\nexport { uuidv4 } from \"./uuid.js\";\n","// UUID v4 생성기 — 폴리필/의존성 없이 자체 구현.\n//\n// 설계 이유:\n// - crypto.randomUUID는 보안 컨텍스트(https/localhost)에서만 있고, 구형 런타임엔 없다.\n// 표준 API가 없어도 멱등 키(clientSubmissionId)를 만들 수 있어야 한다(TC5).\n// - 따라서 getRandomValues를 우선 쓰고, 그마저 없으면 Math.random으로 폴백한다.\n// - 코어는 globalThis.crypto만 참조(document/window 아님) → 플랫폼 무관 유지.\n\nfunction pickRandomBytes(): (arr: Uint8Array) => Uint8Array {\n const g: typeof globalThis =\n typeof globalThis !== \"undefined\" ? globalThis : ({} as typeof globalThis);\n // DOM lib에 의존하지 않으려고 Crypto 전역 타입 대신 필요한 모양만 선언한다.\n // 코어는 lib.dom 없이도 타입체크가 통과해야 한다(플랫폼 무관 요구사항).\n const crypto = (g as { crypto?: { getRandomValues?(array: Uint8Array): Uint8Array } }).crypto;\n const getRandomValues = crypto?.getRandomValues;\n if (typeof getRandomValues === \"function\") {\n // crypto에 바인딩해서 호출한다(구현체가 this를 요구한다).\n return (arr) => getRandomValues.call(crypto, arr);\n }\n // 폴백: Math.random. 멱등 키 용도라 충분한 균등분포.\n return (arr) => {\n for (let i = 0; i < arr.length; i++) {\n arr[i] = Math.floor(Math.random() * 256);\n }\n return arr;\n };\n}\n\n/**\n * RFC 4122 v4 UUID 문자열을 만든다(8-4-4-4-16).\n * crypto.getRandomValues가 있으면 그것을, 없으면 Math.random을 쓴다.\n */\nexport function uuidv4(): string {\n const fill = pickRandomBytes();\n const b = new Uint8Array(16);\n fill(b);\n\n // 버전(4)과 변형(10xx) 비트 고정.\n b[6] = (b[6] & 0x0f) | 0x40;\n b[8] = (b[8] & 0x3f) | 0x80;\n\n const h = Array.from(b, (n) => n.toString(16).padStart(2, \"0\"));\n return (\n h.slice(0, 4).join(\"\") +\n \"-\" +\n h.slice(4, 6).join(\"\") +\n \"-\" +\n h.slice(6, 8).join(\"\") +\n \"-\" +\n h.slice(8, 10).join(\"\") +\n \"-\" +\n h.slice(10, 16).join(\"\")\n );\n}\n","// 게스트 영구 식별자 — getUser prop이 없을 때 사용.\n//\n// 요구사항(TC3): getUser가 없으면 게스트로 취급하되, 식별자는 영구적이어야 한다.\n// 즉 같은 저장소(localStorage/AsyncStorage)를 쓰는 한, 위젯을 다시 띄워도 같은 게스트 ID.\n// 그래서 최초 1회 생성해 저장소에 넣고, 이후엔 그 값을 재사용한다.\n\nimport type { FeedbackStorage, FeedbackUser } from \"./types.js\";\nimport { uuidv4 } from \"./uuid.js\";\n\nconst GUEST_KEY = \"feedback-kit:guest-id\";\n\n/**\n * 저장소에서 게스트 id를 찾아 반환하고, 없으면 새로 만들어 저장한다.\n * 반환값은 사용자가 \"이미 알고 있던\" 식별자이므로 멱등(같은 저장소 → 같은 id).\n */\nexport async function getOrCreateGuestId(\n storage: FeedbackStorage\n): Promise<string> {\n const existing = await storage.get(GUEST_KEY);\n if (existing && existing.length > 0) {\n return existing;\n }\n const id = `guest_${uuidv4()}`;\n await storage.set(GUEST_KEY, id);\n return id;\n}\n\n/**\n * 게스트 id로 FeedbackUser 객체를 만든다.\n *\n * `isGuest: true` 를 반드시 붙인다. 게스트를 `user: null` 로 보내면 받는 쪽에서\n * \"신원 수집이 안 된 제보\"와 \"게스트가 남긴 제보\"를 구분할 수 없다.\n */\nexport function guestUser(id: string): FeedbackUser {\n return { id, isGuest: true };\n}\n","// 제보 컨텍스트 조립 — \"이 제보가 어디서, 누가, 어떤 상태에서 났는가\"를 모은다.\n//\n// 왜 report.ts 에서 분리했나: 컨텍스트 수집은 플랫폼 공급자(앱 정보·기기·디스플레이·\n// 진단 버퍼)를 여러 개 받는다. 제보 조립(report.ts)은 그 결과를 쓰기만 한다.\n//\n// 설계 원칙:\n// - 코어는 여전히 플랫폼 무관이다. `expo-constants` 같은 걸 import 하지 않고,\n// **함수 공급자를 주입받아** 호출만 한다. 앱/웹 패키지가 그 공급자를 채운다.\n// - 공급자가 던지거나 이상한 값을 줘도 제보를 잃지 않는다. 각 항목은 독립적으로\n// 실패하고 그 자리만 null 이 된다.\n// - \"값이 없다\"와 \"수집이 배선되지 않았다\"를 구분한다. 공급자가 아예 없으면\n// 해당 블록(`native`/`web`/`diagnostics`)이 통째로 null 이고, 공급자는 있는데\n// 값을 못 구했으면 블록은 있고 그 안의 필드가 null 이다.\n\nimport { getOrCreateGuestId } from \"./guest-id.js\";\nimport type {\n AppInfo,\n ContextUser,\n DeviceInfo,\n DiagnosticsPayload,\n DisplayInfo,\n FeedbackContext,\n FeedbackStorage,\n FeedbackUser,\n GetCurrentScreenFn,\n GetUserFn,\n NativeContext,\n SourceMapping,\n WebContext,\n} from \"./types.js\";\n\n/**\n * 웹 공급자가 돌려주는 원자료.\n * 모든 칸이 선택이다 — 못 구한 값은 빼면 되고, 코어가 null 로 채운다.\n */\nexport interface WebContextInput {\n viewport?: {\n width?: number | null;\n height?: number | null;\n devicePixelRatio?: number | null;\n };\n userAgent?: string | null;\n}\n\n/** 앱/웹 컨텍스트를 채우는 공급자 묶음. 전부 선택이다. */\nexport interface ContextProviders {\n /** 현재 URL(웹) 또는 딥링크(앱). */\n getUrl?: () => string | null;\n /** 라우트 스택. 바깥(루트)부터 안쪽(현재 화면) 순서. */\n getNavPath?: () => string[] | null;\n /** 현재 화면 파라미터 원본. JSON 왕복으로 정제해서 싣는다. */\n getRouteParams?: () => unknown;\n /** 앱 빌드 정보(version/channel/updateId/runtimeVersion). */\n getAppInfo?: () => Partial<AppInfo> | null;\n /** 기기 정보(model/osName/osVersion/deviceType). */\n getDevice?: () => Partial<DeviceInfo> | null;\n /** 화면 크기·배율(width/height/pixelRatio/fontScale). */\n getDisplay?: () => Partial<DisplayInfo> | null;\n /** 웹 뷰포트·userAgent. */\n getWebContext?: () => WebContextInput | null;\n /**\n * 진단 스냅샷 공급자.\n * 주지 않으면 `diagnostics` 는 **빈 배열이 아니라 null** 이다\n * (수집이 배선되지 않았다는 뜻과 \"그 세션에 아무 일도 없었다\"는 뜻을 구분한다).\n */\n getDiagnostics?: () => DiagnosticsPayload | null;\n /** 화면 이름 → 소스 파일 경로. 빌드 플러그인이 생성한다. 없으면 screenPath: null. */\n screenSourceMap?: Record<string, string> | null;\n /** 타임존 결정(테스트 주입용). 없으면 Intl 로 알아낸다. */\n getTimezone?: () => string | null;\n}\n\nexport interface BuildContextOpts extends ContextProviders {\n app: string;\n sessionId: string;\n storage: FeedbackStorage;\n platform: FeedbackContext[\"platform\"];\n /** 제출 시점 사용자 결정. 없으면 게스트. */\n getUser?: GetUserFn;\n /** 현재 화면 이름. 없거나 null 반환 → screen: null. */\n getCurrentScreen?: GetCurrentScreenFn;\n /** 빌드 플러그인 매핑. 없으면 screenId/sourceFile 이 null. */\n source?: SourceMapping;\n /** 자유 메타. 기본 {}. */\n extra?: Record<string, unknown>;\n /** 시간 주입(테스트용). */\n now?: () => number;\n iso?: (n: number) => string;\n}\n\n/**\n * getUser prop을 **제출 시점에** 호출해 사용자를 결정한다(캐싱하지 않는다).\n * - getUser 없음 → 게스트(기기에 영구 저장된 id).\n * - getUser 는 있지만 빈 결과/예외 → 게스트 폴백(제보 자체는 잃지 않는다).\n * - getUser 결과에 id 있음 → 그대로 사용.\n *\n * 게스트도 `isGuest: true` 로 **객체를 채운다**. 반환이 null 인 경우는 저장소 접근이\n * 실패해 게스트 식별자조차 만들지 못한 최초 실행 순간뿐이다.\n */\nexport async function resolveContextUser(\n getUser: GetUserFn | undefined,\n storage: FeedbackStorage\n): Promise<ContextUser | null> {\n if (typeof getUser === \"function\") {\n try {\n const u = await Promise.resolve(getUser());\n if (u && typeof u.id === \"string\" && u.id.length > 0) {\n return normalizeUser(u);\n }\n } catch {\n // getUser 가 던지면 게스트로 폴백한다.\n }\n }\n try {\n const id = await getOrCreateGuestId(storage);\n return { id, email: null, isGuest: true };\n } catch {\n // 저장소를 읽지도 쓰지도 못한 상태 — 신원을 만들 수 없다.\n return null;\n }\n}\n\n/** 호스트가 준 FeedbackUser 를 제보에 실리는 ContextUser 로 정규화한다. */\nexport function normalizeUser(u: FeedbackUser): ContextUser {\n const out: ContextUser = {\n id: u.id,\n email: typeof u.email === \"string\" ? u.email : null,\n // isGuest 생략 = 로그인 사용자.\n isGuest: u.isGuest === true,\n };\n if (typeof u.name === \"string\") out.name = u.name;\n if (typeof u.role === \"string\") out.role = u.role;\n return out;\n}\n\n/** FeedbackReport.context 를 조립한다. 사용자·화면·기기·진단 수집을 모두 포함. */\nexport async function buildContext(\n opts: BuildContextOpts\n): Promise<FeedbackContext> {\n const now = opts.now ?? ((): number => Date.now());\n const iso = opts.iso ?? ((n: number): string => new Date(n).toISOString());\n\n const user = await resolveContextUser(opts.getUser, opts.storage);\n const screen = safeCall(opts.getCurrentScreen, null);\n\n return {\n app: opts.app,\n screen: typeof screen === \"string\" ? screen : null,\n url: nullableString(safeCall(opts.getUrl, null)),\n sessionId: opts.sessionId,\n user,\n source: opts.source ?? { screenId: null, sourceFile: null },\n platform: opts.platform,\n timezone: resolveTimezone(opts.getTimezone),\n clientTimestamp: iso(now()),\n native: buildNative(opts, typeof screen === \"string\" ? screen : null),\n web: buildWeb(opts),\n // 공급자가 없으면 null — 빈 배열을 상수로 보내지 않는다.\n diagnostics: opts.getDiagnostics\n ? normalizeDiagnostics(safeCall(opts.getDiagnostics, null))\n : null,\n extra: opts.extra ?? {},\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 블록 조립\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction hasNativeProvider(opts: BuildContextOpts): boolean {\n return Boolean(\n opts.getAppInfo ||\n opts.getDevice ||\n opts.getDisplay ||\n opts.getNavPath ||\n opts.getRouteParams ||\n opts.screenSourceMap\n );\n}\n\nfunction buildNative(\n opts: BuildContextOpts,\n screen: string | null\n): NativeContext | null {\n if (opts.platform !== \"native\" || !hasNativeProvider(opts)) return null;\n\n const navPathRaw = safeCall(opts.getNavPath, null);\n const navPath = Array.isArray(navPathRaw)\n ? navPathRaw.filter((s): s is string => typeof s === \"string\")\n : // 스택을 못 구했으면 최소한 현재 화면 한 칸이라도 남긴다.\n screen\n ? [screen]\n : [];\n\n const app = safeCall(opts.getAppInfo, null) ?? {};\n const device = safeCall(opts.getDevice, null) ?? {};\n const display = safeCall(opts.getDisplay, null) ?? {};\n\n return {\n // 매핑이 없으면 여기만 null 이고 나머지 컨텍스트는 그대로 수집된다.\n screenPath: lookupScreenPath(opts.screenSourceMap, screen),\n navPath,\n routeParams: toPlainRecord(safeCall(opts.getRouteParams, undefined)),\n appInfo: {\n version: nullableString(app.version),\n channel: nullableString(app.channel),\n updateId: nullableString(app.updateId),\n runtimeVersion: nullableString(app.runtimeVersion),\n },\n device: {\n model: nullableString(device.model),\n osName: nullableString(device.osName),\n osVersion: nullableString(device.osVersion),\n deviceType: nullableString(device.deviceType),\n },\n display: {\n width: nullableNumber(display.width),\n height: nullableNumber(display.height),\n pixelRatio: nullableNumber(display.pixelRatio),\n fontScale: nullableNumber(display.fontScale),\n },\n };\n}\n\nfunction buildWeb(opts: BuildContextOpts): WebContext | null {\n if (opts.platform !== \"web\" || !opts.getWebContext) return null;\n const w = safeCall(opts.getWebContext, null) ?? {};\n const vp = w.viewport ?? {};\n return {\n viewport: {\n width: nullableNumber(vp.width),\n height: nullableNumber(vp.height),\n devicePixelRatio: nullableNumber(vp.devicePixelRatio),\n },\n userAgent: nullableString(w.userAgent),\n };\n}\n\nfunction lookupScreenPath(\n map: Record<string, string> | null | undefined,\n screen: string | null\n): string | null {\n if (!map || !screen) return null;\n const hit = map[screen];\n return typeof hit === \"string\" && hit.length > 0 ? hit : null;\n}\n\n/**\n * 공급자가 준 스냅샷을 그대로 통과시킨다(배열만 확인).\n * 여기서 배열을 새로 만들어 채우지 않는다 — 이 값은 수집기가 모은 결과여야 한다.\n */\nfunction normalizeDiagnostics(\n snap: DiagnosticsPayload | null\n): DiagnosticsPayload | null {\n if (!snap || !Array.isArray(snap.network) || !Array.isArray(snap.logs)) {\n return null;\n }\n return snap;\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 헬퍼\n// ────────────────────────────────────────────────────────────────────────────\n\n/** 공급자를 호출하되, 없거나 던지면 fallback 을 쓴다. 한 항목의 실패가 제보를 막지 않는다. */\nfunction safeCall<T>(fn: (() => T) | undefined, fallback: T): T {\n if (typeof fn !== \"function\") return fallback;\n try {\n return fn();\n } catch {\n return fallback;\n }\n}\n\nfunction nullableString(v: unknown): string | null {\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\n\nfunction nullableNumber(v: unknown): number | null {\n return typeof v === \"number\" && Number.isFinite(v) ? v : null;\n}\n\nfunction resolveTimezone(getTimezone?: () => string | null): string | null {\n const injected = safeCall(getTimezone, null);\n if (typeof injected === \"string\" && injected.length > 0) return injected;\n try {\n // Intl 은 ES 표준이라 웹/RN(Hermes intl 빌드) 모두에서 쓸 수 있다.\n const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;\n return typeof tz === \"string\" && tz.length > 0 ? tz : null;\n } catch {\n return null;\n }\n}\n\n/**\n * 라우트 파라미터를 JSON 왕복으로 정제한다.\n * 함수는 버리고, 순환참조는 `[Circular]` 로 바꾼다 — 그대로 두면 제보 직렬화가 통째로\n * 실패한다. (같은 객체가 형제로 두 번 나오면 두 번째도 [Circular] 로 보이는데,\n * 안전한 쪽으로 과하게 판단하는 것이라 그대로 둔다.)\n */\nfunction toPlainRecord(value: unknown): Record<string, unknown> | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const seen = new WeakSet<object>();\n let json: string | undefined;\n try {\n json = JSON.stringify(value, (_key: string, v: unknown) => {\n if (typeof v === \"function\") return undefined;\n if (v && typeof v === \"object\") {\n if (seen.has(v as object)) return \"[Circular]\";\n seen.add(v as object);\n }\n return v;\n });\n } catch {\n return null;\n }\n if (typeof json !== \"string\") return null;\n try {\n const parsed: unknown = JSON.parse(json);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n","// 제보 조립 — 호스트가 준 부분 데이터로 FeedbackReport(전체 계약)를 만든다.\n//\n// 핵심 규칙:\n// - 호스트는 kind/comment/priority/screenshot/pin/element 만 넘긴다.\n// clientSubmissionId / createdAt / context 는 코어가 여기서 채운다.\n// - context(사용자·화면·기기·진단) 조립은 context.ts 가 담당한다. 여기서는 쓰기만 한다.\n// - getUser는 prop(함수). 제출 시점에 호출한다. 캐싱하지 않는다 → 로그인/로그아웃으로\n// 반환값이 바뀌어도 제출 순간의 최신 신원이 들어간다.\n\nimport type { BuildContextOpts } from \"./context.js\";\nimport { buildContext } from \"./context.js\";\nimport type {\n ElementInfo,\n FeedbackKind,\n FeedbackPin,\n FeedbackPriority,\n FeedbackReport,\n FeedbackScreenshot,\n} from \"./types.js\";\nimport { uuidv4 } from \"./uuid.js\";\n\n/** 호스트가 submit() 호출 시 넘기는 부분 데이터. 계약의 나머지는 코어가 채운다. */\nexport interface ReportParts {\n kind: FeedbackKind;\n comment: string;\n priority: FeedbackPriority;\n screenshot: FeedbackScreenshot | null;\n pin: FeedbackPin | null;\n element: ElementInfo | null;\n}\n\n/** 제보 조립 옵션. 컨텍스트 조립 옵션과 동일하다(제보 쪽에 추가 입력이 없다). */\nexport type BuildReportOpts = BuildContextOpts;\n\n/**\n * 부분 데이터 + 옵션으로 FeedbackReport 하나를 만든다.\n * clientSubmissionId(UUID)와 createdAt(ISO)은 매 호출마다 새로 생성.\n */\nexport async function buildReport(\n parts: ReportParts,\n opts: BuildReportOpts\n): Promise<FeedbackReport> {\n const now = opts.now ?? ((): number => Date.now());\n const iso = opts.iso ?? ((n: number): string => new Date(n).toISOString());\n const context = await buildContext(opts);\n\n return {\n clientSubmissionId: uuidv4(),\n kind: parts.kind,\n comment: parts.comment,\n priority: parts.priority,\n screenshot: parts.screenshot,\n pin: parts.pin,\n element: parts.element,\n context,\n createdAt: iso(now()),\n };\n}\n","// 영속 제출 큐 — 저장소(FeedbackStorage)에 제보를 쌓고, 어댑터로 비운다.\n//\n// 이 모듈이 지키는 약속(DP-252 전송 보장):\n// 1. 제보는 **전송 전에** 큐에 들어간다. 큐에서 빠지는 유일한 조건은 어댑터가\n// `ok=true`(= HTTP 2xx **그리고** 본문 ok)를 돌려준 것이다.\n// 2. 재시도는 항상 **같은 `clientSubmissionId`** 로 나간다. 서버가 이 값을 멱등 키로\n// 쓰므로 몇 번을 재시도해도 서버에 제보는 1건만 남는다.\n// 3. 보관 한도는 **최대 50건 / 최대 7일**. 넘치면 오래된 것부터 버리고 개발자 콘솔에\n// 경고한다(사용자에게는 노출하지 않는다).\n// 4. 저장소 한도에 걸리면 **스크린샷만 떼어내고 제보 본문은 보관**한다. 스크린샷은\n// 다시 만들 수 있지만 사용자가 쓴 글은 다시 만들 수 없다.\n// 5. 백오프는 5초에서 시작해 2배씩, **상한 5분**. 단 429(요청 한도 초과)는 이 상한의\n// 예외로 **최소 10분** 뒤에 다시 시도한다.\n// 6. 대기 중인 제보가 여러 건이면 **한 건씩 순서대로** 보낸다(서버 부하와 순서 보존).\n//\n// 재시도 트리거는 3가지다: 마운트(start) / 네트워크 복귀(`online` 이벤트 · 앱 포그라운드\n// 복귀 시 호스트가 `retryNow()` 호출) / 사용자의 [다시 보내기](`retryNow()`).\n//\n// 인스턴스 독립: 큐는 주입받은 storage 인스턴스에만 의존한다. 저장소 구현\n// (localStorage/AsyncStorage/메모리)이 바뀌어도 동작은 동일하다(인터페이스만 안다).\n//\n// 코어는 글로벌을 globalThis 로만 참조한다(document/window/RN 모듈 직접 접근 금지).\n\nimport type {\n FeedbackAdapter,\n FeedbackReport,\n FeedbackStorage,\n SubmitResult,\n} from \"./types.js\";\n\nconst QUEUE_KEY = \"feedback-kit:queue\";\n\n/** 대기 큐 최대 길이. 넘치면 오래된 것부터 버린다. */\nexport const MAX_QUEUE_ITEMS = 50;\n\n/** 대기 큐 최대 보관 기간(ms). 7일. */\nexport const MAX_QUEUE_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\n/** 백오프 시작 간격(ms). */\nexport const BACKOFF_BASE_MS = 5 * 1000;\n\n/** 백오프 상한(ms). 429 는 이 상한의 예외다. */\nexport const BACKOFF_MAX_MS = 5 * 60 * 1000;\n\n/** 429 를 받았을 때의 최소 대기(ms). 상한 5분을 넘겨 잡는다. */\nexport const RATE_LIMIT_MIN_DELAY_MS = 10 * 60 * 1000;\n\n/** 저장된 큐 항목. JSON 직렬화 가능한 값으로만 구성한다. */\ninterface QueuedReport {\n /** 조립된 전체 제보. */\n report: FeedbackReport;\n /** 지금까지 시도한 횟수. */\n attempts: number;\n /** 다음 전송 시도 에포크(ms). null = 즉시. */\n nextAttemptAt: number | null;\n /** 큐에 들어온 시각(에포크 ms). 7일 보관 한도의 기준. */\n enqueuedAt: number;\n /** 마지막 실패가 429 였는지. true 면 수동 재시도로도 대기를 앞당기지 않는다. */\n rateLimited?: boolean;\n /** 저장소 한도 때문에 스크린샷을 떼어냈는지(진단용). */\n screenshotDropped?: boolean;\n}\n\n/** UI 가 구독하는 전송 상태. */\nexport interface QueueStatus {\n /** idle=대기 중 제보 없음, sending=전송 중, pending=대기 중(연결되면 자동 전송). */\n state: \"idle\" | \"sending\" | \"pending\";\n /** 대기 중인 제보 수. */\n pending: number;\n /** 마지막 실패 요약(사용자 노출용 아님, 개발자 진단용). 성공/초기엔 null. */\n lastError: string | null;\n}\n\n/** `submit()` 한 건의 결과. */\nexport interface SubmitOutcome {\n /** 서버가 수락(2xx + ok=true)했는가. */\n delivered: boolean;\n /** 서버가 부여한 id(표시용). 수락 전이면 null. */\n id: string | null;\n /** 아직 큐에 남아 있는가(=나중에 자동 재전송). */\n queued: boolean;\n}\n\nexport type QueueStatusListener = (status: QueueStatus) => void;\n\nexport interface FeedbackQueueOpts {\n storage: FeedbackStorage;\n adapter: FeedbackAdapter;\n /** 주기적 flush 간격(ms). 기본 30000. 0/음수면 주기 flush 를 끈다. */\n flushIntervalMs?: number;\n /** 시각 주입(테스트용). */\n now?: () => number;\n /** 백오프 계산 주입(테스트용). */\n backoff?: (\n attempts: number,\n retryAfterMs: number | null,\n rateLimited: boolean\n ) => number;\n /**\n * 개발자 콘솔 경고 싱크. 기본은 `console.warn`.\n * 진단 버퍼가 콘솔을 패치한 환경에서는 원본 콘솔(`DiagnosticsCollector.originalConsole.warn`)을\n * 넘겨 위젯 자신의 경고가 다시 수집되는 되먹임을 막는다.\n */\n warn?: (...a: unknown[]) => void;\n}\n\n/**\n * 지수 백오프 계산.\n * - 429(rateLimited)면 상한 예외: `max(10분, 서버 권장)`.\n * - 서버가 `retryAfterMs` 를 줬으면 그것을 쓰되 5분으로 자른다.\n * - 아니면 5초 * 2^(attempts-1), 상한 5분.\n *\n * attempts=10 이면 5s*2^9 = 2560s 지만 상한에 걸려 정확히 5분이다(연속 실패해도 간격이\n * 5분을 넘지 않는다).\n */\nexport function defaultBackoff(\n attempts: number,\n retryAfterMs: number | null,\n rateLimited = false\n): number {\n const serverHint =\n typeof retryAfterMs === \"number\" && retryAfterMs >= 0 ? retryAfterMs : null;\n\n if (rateLimited) {\n return Math.max(RATE_LIMIT_MIN_DELAY_MS, serverHint ?? 0);\n }\n if (serverHint !== null) {\n return Math.min(serverHint, BACKOFF_MAX_MS);\n }\n const v = BACKOFF_BASE_MS * Math.pow(2, Math.max(0, attempts - 1));\n return Math.min(v, BACKOFF_MAX_MS);\n}\n\nexport class FeedbackQueue {\n private storage: FeedbackStorage;\n private adapter: FeedbackAdapter;\n private flushIntervalMs: number;\n private now: () => number;\n private backoff: (a: number, r: number | null, rl: boolean) => number;\n private warn: (...a: unknown[]) => void;\n\n /**\n * 저장소 접근 직렬화 체인.\n *\n * 왜 필요한가: `load → 수정 → save` 사이에 다른 경로가 끼어들면 그 사이 쓰기가 통째로\n * 사라진다(flush 가 [A]를 읽는 동안 enqueue 가 [A,B]를 저장 → flush 가 []를 저장 →\n * B 유실). 단순한 `flushing` 불리언은 재진입을 \"무시\"할 뿐이라 이 경합을 못 막고,\n * 무시된 flush 를 기다리던 `submit()` 이 자기 전송을 await 할 수도 없다.\n */\n private chain: Promise<unknown> = Promise.resolve();\n /** 아직 시작하지 않은 flush 요청(있으면 재사용해서 타이머 연타를 합친다). */\n private queuedFlush: Promise<void> | null = null;\n\n private timer: unknown | null = null;\n private onlineHandler: (() => void) | null = null;\n\n private pending = 0;\n private sending = false;\n private lastError: string | null = null;\n private listeners = new Set<QueueStatusListener>();\n\n /** 제출 단위 결과 캐시. `submit()` 이 자기 건의 결말을 알아내는 데 쓴다. */\n private outcomes = new Map<string, { delivered: boolean; id: string | null }>();\n\n constructor(opts: FeedbackQueueOpts) {\n this.storage = opts.storage;\n this.adapter = opts.adapter;\n this.flushIntervalMs = opts.flushIntervalMs ?? 30000;\n this.now = opts.now ?? (() => Date.now());\n this.backoff = opts.backoff ?? defaultBackoff;\n this.warn = opts.warn ?? defaultWarn;\n }\n\n // ── 공개 API ──────────────────────────────────────────────\n\n /**\n * 제보를 큐에 넣고 전송을 시도한 뒤, 그 한 건의 결말을 돌려준다.\n * 실패해도 예외를 던지지 않는다 — 제보는 큐에 남고 `queued: true` 로 알린다.\n */\n async submit(report: FeedbackReport): Promise<SubmitOutcome> {\n this.outcomes.delete(report.clientSubmissionId);\n await this.serialize(() => this.enqueueInner(report));\n await this.flush();\n const outcome = this.outcomes.get(report.clientSubmissionId);\n if (outcome) {\n return { delivered: outcome.delivered, id: outcome.id, queued: false };\n }\n return { delivered: false, id: null, queued: true };\n }\n\n /** 큐에 제보를 추가하고, 전송을 백그라운드로 시도한다. */\n async enqueue(report: FeedbackReport): Promise<void> {\n await this.serialize(() => this.enqueueInner(report));\n void this.flush();\n }\n\n /**\n * 만기된 큐 항목들을 한 건씩 순서대로 전송한다.\n * 이미 진행 중이면 그 뒤에 한 번만 예약해 붙는다(타이머 연타로 flush 가 쌓이지 않는다).\n */\n flush(): Promise<void> {\n if (this.queuedFlush) return this.queuedFlush;\n const p = this.serialize(async () => {\n this.queuedFlush = null;\n await this.doFlush();\n });\n this.queuedFlush = p;\n return p;\n }\n\n /**\n * 지금 즉시 재시도한다. 네트워크 복귀·앱 포그라운드 복귀·[다시 보내기] 용.\n * 백오프 대기를 앞당기되, **429 로 묶인 항목은 앞당기지 않는다**(서버가 명시한 한도라\n * 무시하면 다시 429 만 받는다).\n */\n async retryNow(): Promise<void> {\n await this.serialize(async () => {\n const queue = await this.load();\n let changed = false;\n for (const item of queue) {\n if (item.rateLimited === true) continue;\n if (item.nextAttemptAt !== null) {\n item.nextAttemptAt = null;\n changed = true;\n }\n }\n if (changed) await this.persist(queue);\n });\n await this.flush();\n }\n\n /** 현재 큐 길이(대기 중 제보 수). 검증/진단용. */\n async size(): Promise<number> {\n const queue = await this.serialize(() => this.load());\n this.setPending(queue.length);\n return queue.length;\n }\n\n /** 현재 전송 상태 스냅샷. */\n getStatus(): QueueStatus {\n return {\n state: this.sending ? \"sending\" : this.pending > 0 ? \"pending\" : \"idle\",\n pending: this.pending,\n lastError: this.lastError,\n };\n }\n\n /** 특정 clientSubmissionId의 마지막 확정 결말. 큐에 있거나 아직 시도 전이면 null이다. */\n getOutcome(\n clientSubmissionId: string\n ): Pick<SubmitOutcome, \"delivered\" | \"id\"> | null {\n return this.outcomes.get(clientSubmissionId) ?? null;\n }\n\n /** 전송 상태 구독. 반환값을 호출하면 해지된다. 구독 즉시 현재 상태를 1회 통지한다. */\n subscribe(listener: QueueStatusListener): () => void {\n this.listeners.add(listener);\n try {\n listener(this.getStatus());\n } catch {\n /* 구독자 예외가 큐를 멈추게 하지 않는다. */\n }\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * 마운트 시 호출. 만기 항목 정리 + 즉시 재시도 + 주기 flush + `online` 재시도를 건다.\n * 앱(RN)에는 `online` 이벤트가 없으므로, 호스트가 포그라운드 복귀 시 `retryNow()` 를\n * 직접 부른다(코어는 플랫폼 API 를 모른다).\n */\n start(): void {\n this.stop();\n\n const g = globalThis as {\n addEventListener?: (t: string, h: () => void) => void;\n setInterval?: (h: () => void, ms: number) => unknown;\n };\n\n if (typeof g.addEventListener === \"function\") {\n const handler = (): void => {\n void this.retryNow();\n };\n g.addEventListener(\"online\", handler);\n this.onlineHandler = handler;\n }\n\n if (this.flushIntervalMs > 0 && typeof g.setInterval === \"function\") {\n this.timer = g.setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n void this.retryNow();\n }\n\n /** 타이머와 이벤트 구독을 해제한다. 큐 내용은 그대로 남는다. */\n stop(): void {\n const g = globalThis as {\n removeEventListener?: (t: string, h: () => void) => void;\n clearInterval?: (t: unknown) => void;\n };\n if (this.timer !== null) {\n if (typeof g.clearInterval === \"function\") g.clearInterval(this.timer);\n this.timer = null;\n }\n if (this.onlineHandler !== null) {\n if (typeof g.removeEventListener === \"function\") {\n g.removeEventListener(\"online\", this.onlineHandler);\n }\n this.onlineHandler = null;\n }\n }\n\n // ── 내부: 적재 / 전송 ─────────────────────────────────────\n\n private async enqueueInner(report: FeedbackReport): Promise<void> {\n const queue = this.purgeExpired(await this.load());\n queue.push({\n report,\n attempts: 0,\n nextAttemptAt: null,\n enqueuedAt: this.now(),\n });\n\n // 보관 한도(50건) — 오래된 것부터 버리고 개발자 콘솔에 경고한다.\n while (queue.length > MAX_QUEUE_ITEMS) {\n const dropped = queue.shift();\n this.warn(\n `feedback-kit: 대기 큐가 ${MAX_QUEUE_ITEMS}건을 넘어 가장 오래된 제보를 버렸다`,\n dropped?.report.clientSubmissionId\n );\n }\n\n await this.persist(queue);\n }\n\n private async doFlush(): Promise<void> {\n let queue = await this.load();\n\n const kept = this.purgeExpired(queue);\n if (kept.length !== queue.length) {\n queue = kept;\n await this.persist(queue);\n }\n this.setPending(queue.length);\n if (queue.length === 0) {\n this.emit();\n return;\n }\n\n const now = this.now();\n const due = queue.filter(\n (item) => item.nextAttemptAt === null || now >= item.nextAttemptAt\n );\n if (due.length === 0) {\n this.emit();\n return;\n }\n\n this.sending = true;\n this.emit();\n try {\n // 한 건씩 순서대로. 앞 건이 끝나야 다음 건을 보낸다.\n for (const item of due) {\n if (queue.indexOf(item) === -1) continue; // 저장 한도로 밀려나 사라진 항목.\n\n const result = await this.submitOne(item.report);\n const at = this.now();\n const index = queue.indexOf(item);\n\n if (result.ok) {\n if (index !== -1) queue.splice(index, 1);\n this.recordOutcome(item.report.clientSubmissionId, true, result.id);\n this.lastError = null;\n } else if (!result.retryable) {\n // 영구 실패(예: 스키마 거부) — 재시도해도 결과가 같으므로 데드레터.\n if (index !== -1) queue.splice(index, 1);\n this.recordOutcome(item.report.clientSubmissionId, false, null);\n this.lastError = \"non-retryable\";\n this.warn(\n \"feedback-kit: 재시도 불가 실패로 제보를 버렸다\",\n item.report.clientSubmissionId\n );\n } else {\n // 일시적 실패 — 같은 clientSubmissionId 로 나중에 다시 보낸다.\n item.attempts += 1;\n item.rateLimited = result.rateLimited === true;\n item.nextAttemptAt =\n at + this.backoff(item.attempts, result.retryAfterMs, item.rateLimited);\n this.lastError = item.rateLimited ? \"rate-limited\" : \"retryable\";\n }\n\n await this.persist(queue);\n this.setPending(queue.length);\n this.emit();\n }\n } finally {\n this.sending = false;\n this.emit();\n }\n }\n\n /** 어댑터 호출. 어댑터가 예외를 던져도 결과값으로 흡수한다(전송 보장 우선). */\n private async submitOne(report: FeedbackReport): Promise<SubmitResult> {\n try {\n return await this.adapter.submit(report);\n } catch (err) {\n this.warn(\"feedback-kit: 어댑터가 예외를 던졌다(일시 실패로 처리)\", err);\n return { ok: false, id: null, retryable: true, retryAfterMs: null };\n }\n }\n\n // ── 내부: 저장소 ──────────────────────────────────────────\n\n private async load(): Promise<QueuedReport[]> {\n let raw: string | null = null;\n try {\n raw = await this.storage.get(QUEUE_KEY);\n } catch (err) {\n this.warn(\"feedback-kit: 큐를 읽지 못했다\", err);\n return [];\n }\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(isQueuedReport).map(normalizeEntry(this.now()));\n } catch {\n // 손상된 큐는 빈 것으로 취급(복구 불가).\n this.warn(\"feedback-kit: 큐가 손상돼 초기화했다\");\n return [];\n }\n }\n\n /**\n * 큐를 저장한다. 저장소 한도(쿼터)에 걸리면 **스크린샷부터** 떼어내고, 그래도 안 되면\n * 가장 오래된 제보를 버린다. 사용자가 쓴 글이 스크린샷보다 우선이다.\n * 인자로 받은 배열을 제자리에서 수정한다(호출부가 들고 있는 항목 참조는 유지된다).\n */\n private async persist(queue: QueuedReport[]): Promise<void> {\n for (;;) {\n try {\n await this.storage.set(QUEUE_KEY, JSON.stringify(queue));\n this.setPending(queue.length);\n return;\n } catch (err) {\n if (queue.length === 0) {\n this.warn(\"feedback-kit: 큐를 저장하지 못했다\", err);\n return;\n }\n const shotIndex = queue.findIndex(\n (item) => item.report.screenshot !== null\n );\n if (shotIndex !== -1) {\n const entry = queue[shotIndex] as QueuedReport;\n // 원본 report 객체를 건드리지 않고 새 객체로 교체한다(호출자 소유물 보호).\n entry.report = { ...entry.report, screenshot: null };\n entry.screenshotDropped = true;\n this.warn(\n \"feedback-kit: 저장 한도를 넘어 스크린샷을 떼어내고 제보만 보관한다\",\n entry.report.clientSubmissionId\n );\n continue;\n }\n const evicted = queue.shift();\n this.warn(\n \"feedback-kit: 저장 한도를 넘어 가장 오래된 제보를 버렸다\",\n evicted?.report.clientSubmissionId\n );\n }\n }\n }\n\n /** 7일이 지난 항목을 걸러낸다. 만기 항목은 전송을 시도하지 않는다. */\n private purgeExpired(queue: QueuedReport[]): QueuedReport[] {\n const now = this.now();\n const kept = queue.filter((item) => now - item.enqueuedAt <= MAX_QUEUE_AGE_MS);\n if (kept.length !== queue.length) {\n this.warn(\n `feedback-kit: 보관 기간(7일)이 지난 제보 ${queue.length - kept.length}건을 정리했다`\n );\n }\n return kept;\n }\n\n // ── 내부: 상태 통지 ───────────────────────────────────────\n\n private recordOutcome(id: string, delivered: boolean, serverId: string | null): void {\n // 무한 증가를 막는다. 오래된 것부터 버린다(결말은 UI 가 즉시 읽고 버리는 값).\n if (this.outcomes.size >= 100) {\n const oldest = this.outcomes.keys().next();\n if (!oldest.done) this.outcomes.delete(oldest.value);\n }\n this.outcomes.set(id, { delivered, id: serverId });\n }\n\n private setPending(n: number): void {\n if (this.pending === n) return;\n this.pending = n;\n }\n\n private emit(): void {\n if (this.listeners.size === 0) return;\n const status = this.getStatus();\n for (const listener of this.listeners) {\n try {\n listener(status);\n } catch {\n /* 구독자 예외가 큐를 멈추게 하지 않는다. */\n }\n }\n }\n\n private serialize<T>(fn: () => Promise<T>): Promise<T> {\n const next = this.chain.then(fn, fn);\n this.chain = next.catch(() => undefined);\n return next;\n }\n}\n\n// ── 모듈 내부 헬퍼 ──────────────────────────────────────────\n\nfunction defaultWarn(...a: unknown[]): void {\n const g = globalThis as { console?: { warn?: (...a: unknown[]) => void } };\n g.console?.warn?.(...a);\n}\n\nfunction isQueuedReport(v: unknown): v is QueuedReport {\n if (typeof v !== \"object\" || v === null) return false;\n const report = (v as { report?: unknown }).report;\n if (typeof report !== \"object\" || report === null) return false;\n return typeof (report as { clientSubmissionId?: unknown }).clientSubmissionId === \"string\";\n}\n\n/** 이전 버전이 저장한 항목에도 견디도록 필드를 보정한다. */\nfunction normalizeEntry(now: number): (v: QueuedReport) => QueuedReport {\n return (v) => ({\n ...v,\n attempts: typeof v.attempts === \"number\" && v.attempts >= 0 ? v.attempts : 0,\n nextAttemptAt: typeof v.nextAttemptAt === \"number\" ? v.nextAttemptAt : null,\n // enqueuedAt 이 없던 시절의 항목은 \"방금 들어온 것\"으로 봐서 즉시 버리지 않는다.\n enqueuedAt: typeof v.enqueuedAt === \"number\" ? v.enqueuedAt : now,\n });\n}\n","// 이탈(탭 닫기 / 화면 전환) 중에도 전송을 마치기 위한 keepalive 판단.\n//\n// 왜 별도 모듈인가:\n// `fetch(..., { keepalive: true })` 는 브라우저가 **요청 본문 64 KiB** 로 제한한다.\n// 한도를 넘기면 요청 자체가 TypeError 로 즉시 실패한다 — 즉 \"일단 keepalive 를 켜두면\n// 안전하다\"가 성립하지 않는다. 스크린샷이 붙은 제보는 수 MiB 라서 대부분 한도를 넘는다.\n//\n// 그래서 전송 계층은 본문 크기를 재고 이 함수로 갈라야 한다.\n// - 한도 이하(대부분의 텍스트 제보): keepalive 로 보내 이탈해도 전송이 끝난다.\n// - 한도 초과(스크린샷 동반): keepalive 없이 보낸다. 이탈로 중단되더라도 제보는 큐에\n// 남아 있으므로(2xx + ok=true 전에는 제거하지 않는다) 다음 마운트에서 자동 재전송된다.\n// 유실이 아니라 지연으로 떨어지는 것이 이 설계의 의도다.\n\n/** `fetch` keepalive 요청 본문 한도(바이트). Fetch 표준의 inflight keepalive quota. */\nexport const KEEPALIVE_BODY_LIMIT_BYTES = 64 * 1024;\n\n/**\n * 이 본문을 keepalive 로 보내도 되는지.\n * @param bodyByteLength 직렬화된 요청 본문의 바이트 수.\n */\nexport function canUseKeepalive(bodyByteLength: number): boolean {\n if (!Number.isFinite(bodyByteLength) || bodyByteLength < 0) return false;\n return bodyByteLength <= KEEPALIVE_BODY_LIMIT_BYTES;\n}\n\n/**\n * 문자열 본문의 UTF-8 바이트 수. `TextEncoder` 가 없으면(구형 RN 등) 보수적으로\n * 근사한다 — 근사는 항상 실제보다 크거나 같게 잡아 한도를 넘겨 켜는 일이 없게 한다.\n */\nexport function byteLengthOf(body: string): number {\n const g = globalThis as { TextEncoder?: new () => { encode(s: string): { length: number } } };\n if (typeof g.TextEncoder === \"function\") {\n return new g.TextEncoder().encode(body).length;\n }\n // 폴백: 코드 유닛당 최대 3바이트(서로게이트 쌍은 2유닛 → 4바이트라 여전히 상한).\n return body.length * 3;\n}\n","// 고정 용량 링버퍼.\n//\n// 진단 수집은 \"최근 것\"만 있으면 된다. 상한이 없으면 오래 켜둔 화면에서 메모리가\n// 계속 늘고, 상한을 넘겼을 때 새 항목을 버리면 정작 사고 직전의 기록이 안 남는다.\n// 그래서 상한 초과 시 **가장 오래된 것부터** 밀어낸다.\n\nexport class RingBuffer<T> {\n /** 보관 상한. 0 이하/비정상 값이면 0(아무것도 담지 않음)으로 떨어진다. */\n readonly capacity: number;\n\n private items: T[] = [];\n\n constructor(capacity: number) {\n this.capacity =\n Number.isFinite(capacity) && capacity > 0 ? Math.floor(capacity) : 0;\n }\n\n push(item: T): void {\n if (this.capacity === 0) {\n return;\n }\n this.items.push(item);\n const overflow = this.items.length - this.capacity;\n if (overflow > 0) {\n // 한 번에 여러 개가 넘칠 일은 없지만(push 마다 검사), 방어적으로 잘라낸다.\n this.items.splice(0, overflow);\n }\n }\n\n /** 현재 내용의 복사본. 호출자가 들고 있어도 이후 push 에 영향받지 않는다. */\n toArray(): T[] {\n return this.items.slice();\n }\n\n get size(): number {\n return this.items.length;\n }\n\n clear(): void {\n this.items = [];\n }\n}\n","// 진단 링버퍼 — 호스트 앱의 네트워크 요청과 경고·에러 로그를 모아 제보에 첨부한다.\n//\n// 왜 링버퍼인가: 제보는 \"문제가 난 뒤\"에 작성된다. 그래서 수집은 항상 켜져 있어야 하고,\n// 메모리는 유한해야 한다. 고정 길이 버퍼에 계속 밀어 넣으면 **직전 N건**이 언제나 남는다.\n//\n// 이 모듈이 지키는 약속(DP-253 진단 컨텍스트 수집):\n// 1. 버퍼는 **모듈 스코프에 두 개**(network 30 / logs 50). 화면마다 상태를 연결할 필요가\n// 없어야 한다 — 어느 화면에서 제보를 열든 같은 버퍼를 본다.\n// 2. 기록 항목은 **메서드·URL·상태코드·소요시간**까지다. `Authorization` 헤더·쿠키·\n// 요청 본문·응답 본문은 **애초에 읽지 않는다**(가리는 게 아니라 접근 자체를 안 한다).\n// URL 쿼리의 자격증명형 파라미터(`token`, `api_key`, …)와 `user:pass@` 는 값을 가린다.\n// 3. 스냅샷의 두 배열은 **실제로 모은 결과**다. 하드코딩된 빈 배열을 보내지 않는다.\n// 수집이 배선되지 않은 경우는 빈 배열이 아니라 컨텍스트의 `diagnostics` 자체가 null 이다.\n// 4. 원래 동작을 절대 바꾸지 않는다. 래핑된 fetch/XHR/console 은 원본을 그대로 호출하고\n// 결과·예외를 손대지 않고 돌려준다.\n// 5. 위젯 자신의 요청·로그는 캡처에서 제외한다(제보를 보내는 행위가 진단을 오염시키지 않게).\n// - 요청: `excludeMatcher` 가 true 를 주면 제외(예: 수집 백엔드 URL).\n// - 로그: 위젯은 `originalConsole` 로 직접 찍어 캡처를 우회한다.\n// 6. 코어는 document/window/react-native 를 직접 참조하지 않는다. 글로벌은 globalThis 로만.\n\nimport { RingBuffer } from \"./ring-buffer.js\";\nimport type {\n DiagLogEntry,\n DiagNetworkEntry,\n DiagnosticsExcludeMatcher,\n DiagnosticsPayload,\n} from \"./types.js\";\n\n/** 네트워크 링버퍼 용량. 넘치면 오래된 것부터 밀려난다. */\nexport const NETWORK_BUFFER_LIMIT = 30;\n\n/** 콘솔 로그(warn/error) 링버퍼 용량. 넘치면 오래된 것부터 밀려난다. */\nexport const LOG_BUFFER_LIMIT = 50;\n\n/** 로그 한 건의 최대 길이. 스택 덤프 하나가 버퍼를 통째로 먹는 걸 막는다. */\nexport const MAX_LOG_MESSAGE_CHARS = 2000;\n\nexport interface DiagnosticsInstallOpts {\n /** true 를 반환하면 그 요청은 기록하지 않는다(위젯 자신의 전송 등). */\n excludeMatcher?: DiagnosticsExcludeMatcher;\n /** 시각 주입(테스트용). */\n now?: () => number;\n iso?: (n: number) => string;\n}\n\n/** 최소한의 콘솔 형태. DOM 의 `Console` 타입에 의존하지 않기 위해 구조적으로 정의한다. */\ninterface ConsoleLike {\n warn?: (...a: unknown[]) => void;\n error?: (...a: unknown[]) => void;\n log?: (...a: unknown[]) => void;\n}\n\n/**\n * 진단 수집기. `install()` 로 글로벌을 패치하고 `uninstall()` 로 원복한다.\n * 생성 자체는 부작용이 없다 — `install()` 을 호출해야 패치가 일어난다.\n */\nexport class DiagnosticsCollector {\n readonly network: RingBuffer<DiagNetworkEntry>;\n readonly logs: RingBuffer<DiagLogEntry>;\n\n private installed = false;\n\n // 원본 참조(원복용 + 위젯 우회용).\n private originalFetch: ((...a: unknown[]) => Promise<unknown>) | null = null;\n private originalXhrOpen: ((...a: unknown[]) => unknown) | null = null;\n private originalXhrSend: ((...a: unknown[]) => unknown) | null = null;\n private originalWarn: ((...a: unknown[]) => void) | null = null;\n private originalError: ((...a: unknown[]) => void) | null = null;\n\n private excludeMatcher: DiagnosticsExcludeMatcher | null = null;\n private now: () => number;\n private iso: (n: number) => string;\n\n /**\n * 패치된 fetch 안에서 동기적으로 XHR 이 열리는 깊이.\n *\n * React Native 의 `fetch` 는 whatwg-fetch 폴리필이라 내부에서 XMLHttpRequest 를 쓴다.\n * 둘 다 패치해두면 요청 1건이 2건으로 기록된다. fetch 폴리필은 Promise 실행자 안에서\n * **동기적으로** `send()` 를 부르므로, 그 구간을 세어 XHR 쪽 기록만 건너뛴다.\n */\n private fetchDepth = 0;\n\n constructor(\n opts: {\n now?: () => number;\n iso?: (n: number) => string;\n networkLimit?: number;\n logLimit?: number;\n } = {}\n ) {\n this.now = opts.now ?? ((): number => Date.now());\n this.iso = opts.iso ?? ((n): string => new Date(n).toISOString());\n this.network = new RingBuffer<DiagNetworkEntry>(\n opts.networkLimit ?? NETWORK_BUFFER_LIMIT\n );\n this.logs = new RingBuffer<DiagLogEntry>(opts.logLimit ?? LOG_BUFFER_LIMIT);\n }\n\n /** 위젯이 자신의 로그를 캡처 없이 찍을 때 쓰는 원본 console. */\n readonly originalConsole = {\n warn: (...a: unknown[]): void => {\n (this.originalWarn ?? consoleMethod(\"warn\"))(...a);\n },\n error: (...a: unknown[]): void => {\n (this.originalError ?? consoleMethod(\"error\"))(...a);\n },\n log: (...a: unknown[]): void => {\n consoleMethod(\"log\")(...a);\n },\n };\n\n /** 글로벌 fetch/XHR/console 을 패치한다. 이미 설치했으면 no-op. */\n install(opts: DiagnosticsInstallOpts = {}): void {\n if (this.installed) return;\n this.installed = true;\n this.excludeMatcher = opts.excludeMatcher ?? null;\n if (opts.now) this.now = opts.now;\n if (opts.iso) this.iso = opts.iso;\n\n this.patchFetch();\n this.patchXhr();\n this.patchConsole();\n }\n\n /** 글로벌 패치를 모두 원복한다. 버퍼 내용은 그대로 남는다. */\n uninstall(): void {\n if (!this.installed) return;\n this.installed = false;\n\n const g = globalThis as Record<string, unknown>;\n\n if (this.originalFetch) {\n g.fetch = this.originalFetch;\n this.originalFetch = null;\n }\n\n const XHR = g.XMLHttpRequest as { prototype?: Record<string, unknown> } | undefined;\n if (XHR?.prototype && this.originalXhrOpen && this.originalXhrSend) {\n XHR.prototype.open = this.originalXhrOpen;\n XHR.prototype.send = this.originalXhrSend;\n }\n this.originalXhrOpen = null;\n this.originalXhrSend = null;\n\n const c = g.console as ConsoleLike | undefined;\n if (c && this.originalWarn) c.warn = this.originalWarn;\n if (c && this.originalError) c.error = this.originalError;\n this.originalWarn = null;\n this.originalError = null;\n\n this.excludeMatcher = null;\n this.fetchDepth = 0;\n }\n\n /**\n * 지금까지 모인 것을 그대로 스냅샷으로 낸다.\n * 반환 배열은 사본이라 이후 수집이 제보 페이로드를 바꾸지 않는다.\n */\n snapshot(): DiagnosticsPayload {\n return { network: this.network.toArray(), logs: this.logs.toArray() };\n }\n\n /** 두 버퍼를 비운다. */\n clear(): void {\n this.network.clear();\n this.logs.clear();\n }\n\n // ── 내부: 기록 ────────────────────────────────────────────\n\n private recordNetwork(\n method: string,\n url: string,\n status: number | null,\n startedAt: number\n ): void {\n this.network.push({\n method,\n url,\n status,\n durationMs: Math.max(0, Math.round(this.now() - startedAt)),\n at: this.iso(startedAt),\n });\n }\n\n private recordLog(level: \"warn\" | \"error\", args: unknown[]): void {\n this.logs.push({\n level,\n message: safeStringify(args).slice(0, MAX_LOG_MESSAGE_CHARS),\n at: this.iso(this.now()),\n });\n }\n\n private isExcluded(url: string, method: string): boolean {\n if (!this.excludeMatcher) return false;\n try {\n return this.excludeMatcher({ url, method }) === true;\n } catch {\n // 호스트가 준 매처가 던져도 수집을 멈추지 않는다(기록하는 쪽으로 판단).\n return false;\n }\n }\n\n // ── 내부: 패치 ────────────────────────────────────────────\n\n private patchFetch(): void {\n const g = globalThis as Record<string, unknown>;\n if (typeof g.fetch !== \"function\") return;\n\n this.originalFetch = g.fetch as (...a: unknown[]) => Promise<unknown>;\n const originalFetch = this.originalFetch;\n const self = this;\n\n g.fetch = function patchedFetch(\n input: unknown,\n // init 은 `method` 만 읽는다. headers/body 는 **의도적으로 건드리지 않는다**.\n init?: { method?: string }\n ): Promise<unknown> {\n const { url, method } = describeRequest(input, init);\n const excluded = self.isExcluded(url, method);\n const start = self.now();\n\n let pending: Promise<unknown>;\n self.fetchDepth += 1;\n try {\n pending = Promise.resolve(originalFetch(input, init));\n } catch (err) {\n // 동기 throw 도 실패 1건으로 남기고 그대로 전파한다.\n self.fetchDepth -= 1;\n if (!excluded) self.recordNetwork(method, url, null, start);\n throw err;\n }\n self.fetchDepth -= 1;\n\n return pending.then(\n (raw: unknown) => {\n if (!excluded) {\n const status = (raw as { status?: unknown } | null)?.status;\n self.recordNetwork(\n method,\n url,\n typeof status === \"number\" ? status : null,\n start\n );\n }\n return raw;\n },\n (err: unknown) => {\n if (!excluded) self.recordNetwork(method, url, null, start);\n throw err;\n }\n );\n };\n }\n\n private patchXhr(): void {\n const g = globalThis as Record<string, unknown>;\n const XHR = g.XMLHttpRequest as\n | { prototype?: Record<string, unknown> }\n | undefined;\n if (typeof g.XMLHttpRequest !== \"function\" || !XHR?.prototype) return;\n\n const proto = XHR.prototype;\n if (typeof proto.open !== \"function\" || typeof proto.send !== \"function\") return;\n\n this.originalXhrOpen = proto.open as (...a: unknown[]) => unknown;\n this.originalXhrSend = proto.send as (...a: unknown[]) => unknown;\n const oOpen = this.originalXhrOpen;\n const oSend = this.originalXhrSend;\n const self = this;\n\n proto.open = function patchedOpen(\n method: unknown,\n url: unknown,\n ...rest: unknown[]\n ): unknown {\n // 인스턴스에 메타를 저장해 send/loadend 에서 쓴다.\n (this as Record<string, unknown>).__fk_meta = {\n method: String(method ?? \"GET\").toUpperCase(),\n url: sanitizeUrl(String(url ?? \"\")),\n };\n return oOpen.call(this, method, url, ...rest);\n };\n\n // send 의 인자는 요청 본문이다 — 읽지 않고 원본에 그대로 넘기기만 한다.\n proto.send = function patchedSend(...args: unknown[]): unknown {\n const meta = (this as Record<string, unknown>).__fk_meta as\n | { method: string; url: string }\n | undefined;\n const viaFetch = self.fetchDepth > 0;\n const addEv = (this as {\n addEventListener?: (t: string, cb: () => void) => void;\n }).addEventListener;\n\n if (meta && !viaFetch && typeof addEv === \"function\") {\n const excluded = self.isExcluded(meta.url, meta.method);\n const start = self.now();\n let recorded = false;\n // loadend 는 성공·실패·중단 모두에서 발생한다. 한 번만 기록한다.\n addEv.call(this, \"loadend\", () => {\n if (recorded || excluded) return;\n recorded = true;\n const status = (this as { status?: unknown }).status;\n // XHR 은 실패 시 status 0 을 준다 — \"응답 못 받음\"이므로 null 로 남긴다.\n self.recordNetwork(\n meta.method,\n meta.url,\n typeof status === \"number\" && status > 0 ? status : null,\n start\n );\n });\n }\n\n return oSend.apply(this, args);\n };\n }\n\n private patchConsole(): void {\n const c = (globalThis as { console?: ConsoleLike }).console;\n if (!c) return;\n const self = this;\n\n if (typeof c.warn === \"function\") {\n const orig = c.warn.bind(c) as (...a: unknown[]) => void;\n this.originalWarn = orig;\n c.warn = function patchedWarn(...args: unknown[]): void {\n self.recordLog(\"warn\", args);\n orig(...args);\n };\n }\n if (typeof c.error === \"function\") {\n const orig = c.error.bind(c) as (...a: unknown[]) => void;\n this.originalError = orig;\n c.error = function patchedError(...args: unknown[]): void {\n self.recordLog(\"error\", args);\n orig(...args);\n };\n }\n }\n}\n\n/**\n * 모듈 스코프 공유 수집기.\n *\n * 화면·컴포넌트마다 수집기를 연결할 필요가 없어야 한다는 요구사항 때문에 여기 하나를 둔다.\n * 위젯이 어느 화면에서 열리든 이 인스턴스의 스냅샷을 싣는다.\n */\nexport const sharedDiagnostics = new DiagnosticsCollector();\n\n// ────────────────────────────────────────────────────────────────────────────\n// 헬퍼\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction consoleMethod(name: \"warn\" | \"error\" | \"log\"): (...a: unknown[]) => void {\n return (...a: unknown[]): void => {\n const c = (globalThis as { console?: ConsoleLike }).console;\n c?.[name]?.(...a);\n };\n}\n\nfunction safeStringify(args: unknown[]): string {\n // 인자들을 사람이 읽을 수 있는 한 줄 문자열로. 직렬화 실패는 toString 으로.\n return args\n .map((a) => {\n if (typeof a === \"string\") return a;\n if (a instanceof Error) return `${a.name}: ${a.message}`;\n try {\n return JSON.stringify(a) ?? String(a);\n } catch {\n try {\n return String(a);\n } catch {\n return \"[unserializable]\";\n }\n }\n })\n .join(\" \");\n}\n\n/**\n * 요청에서 **메서드와 URL만** 뽑는다.\n *\n * `init.headers` / `init.body` 는 읽지 않는다. Request 객체가 와도 `url`·`method` 만\n * 본다 — 그래야 `Authorization` 헤더나 본문이 실수로도 버퍼에 닿지 않는다.\n */\nfunction describeRequest(\n input: unknown,\n init?: { method?: string }\n): { url: string; method: string } {\n let url = \"\";\n let method = \"GET\";\n\n if (typeof input === \"string\") {\n url = input;\n } else if (input && typeof input === \"object\") {\n const maybeUrl = (input as { url?: unknown }).url;\n const maybeHref = (input as { href?: unknown }).href;\n if (typeof maybeUrl === \"string\") url = maybeUrl;\n else if (typeof maybeHref === \"string\") url = maybeHref;\n else {\n try {\n url = String(input);\n } catch {\n url = \"\";\n }\n }\n const maybeMethod = (input as { method?: unknown }).method;\n if (typeof maybeMethod === \"string\") method = maybeMethod;\n }\n\n if (init && typeof init.method === \"string\") method = init.method;\n\n return { url: sanitizeUrl(url), method: method.toUpperCase() };\n}\n\n/** 값을 가려야 하는 쿼리 파라미터 이름(정규화 후 비교). */\nconst CREDENTIAL_PARAMS = new Set([\n \"accesskey\",\n \"accesstoken\",\n \"apikey\",\n \"apisecret\",\n \"auth\",\n \"authorization\",\n \"authtoken\",\n \"clientsecret\",\n \"code\",\n \"credential\",\n \"credentials\",\n \"idtoken\",\n \"jwt\",\n \"key\",\n \"password\",\n \"passwd\",\n \"privatekey\",\n \"pwd\",\n \"refreshtoken\",\n \"secret\",\n \"secretkey\",\n \"session\",\n \"sessionid\",\n \"sessiontoken\",\n \"sid\",\n \"sig\",\n \"signature\",\n \"token\",\n]);\n\n/** 가려진 값 표시. 파라미터가 \"있었다\"는 사실은 남기고 값만 지운다. */\nexport const REDACTED = \"REDACTED\";\n\nfunction isCredentialParam(rawName: string): boolean {\n const name = rawName.toLowerCase().replace(/[-_.]/g, \"\");\n if (CREDENTIAL_PARAMS.has(name)) return true;\n return (\n name.endsWith(\"token\") ||\n name.endsWith(\"secret\") ||\n name.endsWith(\"password\") ||\n name.endsWith(\"apikey\")\n );\n}\n\n/**\n * URL 에서 자격증명을 걷어낸다.\n * - `#fragment` 는 통째로 버린다(진단 가치는 없고 토큰이 실리는 경우가 있다).\n * - `scheme://user:pass@host` 의 userinfo 를 지운다.\n * - 자격증명형 쿼리 파라미터의 **값만** `REDACTED` 로 바꾼다.\n */\nexport function sanitizeUrl(raw: string): string {\n if (!raw) return \"\";\n\n const hashAt = raw.indexOf(\"#\");\n const withoutHash = hashAt >= 0 ? raw.slice(0, hashAt) : raw;\n\n const queryAt = withoutHash.indexOf(\"?\");\n const base = stripUserInfo(\n queryAt >= 0 ? withoutHash.slice(0, queryAt) : withoutHash\n );\n if (queryAt < 0) return base;\n\n const query = withoutHash.slice(queryAt + 1);\n if (query.length === 0) return base;\n\n const parts = query.split(\"&\").map((pair) => {\n const eq = pair.indexOf(\"=\");\n if (eq < 0) return pair;\n const name = pair.slice(0, eq);\n return isCredentialParam(name) ? `${name}=${REDACTED}` : pair;\n });\n return `${base}?${parts.join(\"&\")}`;\n}\n\nfunction stripUserInfo(url: string): string {\n const schemeAt = url.indexOf(\"://\");\n if (schemeAt < 0) return url;\n const authorityStart = schemeAt + 3;\n const pathAt = url.indexOf(\"/\", authorityStart);\n const authority = pathAt < 0 ? url.slice(authorityStart) : url.slice(authorityStart, pathAt);\n const at = authority.lastIndexOf(\"@\");\n if (at < 0) return url;\n const cleaned = authority.slice(at + 1);\n return url.slice(0, authorityStart) + cleaned + (pathAt < 0 ? \"\" : url.slice(pathAt));\n}\n","// 설정과 토큰 — 붙이는 데 필요한 것을 한 줄로 줄인다.\n//\n// <FeedbackKit token={TOKEN} />\n//\n// 위 한 줄이 성립하려면 나머지 항목이 **전부 기본값을 갖고 있어야** 한다. 그래서 이\n// 모듈의 본체는 `resolveConfig` 하나이고, 나머지는 그 결과를 읽는 판정 함수들이다.\n//\n// ## 수집 토큰은 비밀이 아니다\n//\n// 토큰은 클라이언트 번들에 **상수로 인라인**된다(웹 `VITE_*`/`NEXT_PUBLIC_*`, 앱\n// `EXPO_PUBLIC_*`). 누구나 번들을 열어 꺼낼 수 있으므로 토큰 자체는 접근 제어가 아니다.\n// 실제 방어선은 서버에 있다 — 웹은 등록된 Origin 에서 온 요청만 받고, 앱은 Origin 검사가\n// 불가능하므로 시간당 한도가 방어선이다.\n//\n// 이 사실을 코드 주석과 문서에 남기는 이유는, 토큰이 노출됐을 때 \"유출 사고\"로 오인해\n// 전면 회전 같은 과잉 대응을 하는 걸 막기 위해서다. 저장소에 커밋하지 않는 것은\n// **위생 문제이지 보안 경계가 아니다.**\n//\n// 또 하나: 환경변수는 빌드 타임에 인라인되므로 값을 바꾸면 **다시 빌드해야** 반영된다.\n// 앱은 OTA 로 반영되지 않는다. 그래서 아래 `resolveConfig` 는 치환되지 않은 환경변수\n// 문자열(`undefined`, `import.meta.env.VITE_TOKEN` 등)을 토큰으로 받으면 경고를 남긴다.\n// 이건 실제로 가장 자주 나는 사고이고, 조용히 미귀속으로 흘러가면 알아채기 어렵다.\n\nimport type { ContextUser, DiagnosticsPayload, FeedbackAdapter, FeedbackUser } from \"./types.js\";\n\n// ────────────────────────────────────────────────────────────────────────────\n// 노출 범위\n// ────────────────────────────────────────────────────────────────────────────\n\n/** 노출 판정에 쓰이는 현재 상황. 함수형 `visibility` 가 그대로 받는다. */\nexport interface VisibilityEnv {\n /** 현재 사용자. 로그인 전이면 게스트이거나 null. */\n user: ContextUser | FeedbackUser | null;\n /** 개발 빌드 여부. */\n isDev: boolean;\n platform: \"web\" | \"native\" | \"unknown\";\n}\n\n/** 프로젝트가 직접 판정하는 형태. true 면 노출. */\nexport type VisibilityFn = (env: VisibilityEnv) => boolean;\n\n/**\n * 누구에게 위젯을 노출할지.\n *\n * 기본값은 `all` 이다. 실사용자 제보가 실제로 가치를 냈기 때문에 패키지가 먼저 막지\n * 않는다. 좁혀야 하는 프로젝트가 `internal`·`dev-only`·함수로 좁힌다.\n */\nexport type Visibility = \"all\" | \"internal\" | \"dev-only\" | VisibilityFn;\n\nconst VISIBILITY_KEYWORDS = [\"all\", \"internal\", \"dev-only\"] as const;\n\n/** `internal` 판정의 기본 근거. 프로젝트가 `internalRoles` 로 갈아끼운다. */\nexport const DEFAULT_INTERNAL_ROLES: readonly string[] = [\"internal\", \"admin\", \"staff\"];\n\n// ────────────────────────────────────────────────────────────────────────────\n// 위치 / 어댑터\n// ────────────────────────────────────────────────────────────────────────────\n\nexport type WidgetCorner = \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\";\n\nconst CORNERS: readonly WidgetCorner[] = [\n \"bottom-right\",\n \"bottom-left\",\n \"top-right\",\n \"top-left\",\n];\n\nexport interface WidgetPosition {\n corner: WidgetCorner;\n /** 모서리로부터의 가로 여백(px). */\n offsetX: number;\n /** 모서리로부터의 세로 여백(px). */\n offsetY: number;\n}\n\nexport const DEFAULT_POSITION: WidgetPosition = {\n corner: \"bottom-right\",\n offsetX: 16,\n offsetY: 16,\n};\n\n/** 내장 어댑터 이름. 실제 구현과 전송 규약은 어댑터 섹션이 갖는다. */\nexport type AdapterName = \"lasso\" | \"linear\" | \"notion\";\n\nconst ADAPTER_NAMES: readonly AdapterName[] = [\"lasso\", \"linear\", \"notion\"];\n\nexport const DEFAULT_ADAPTER: AdapterName = \"lasso\";\n\n/**\n * 수집 엔드포인트 기본값은 **어댑터가 갖는다.**\n *\n * 코어가 특정 백엔드 URL 을 상수로 들고 있으면 어댑터 교체가 무의미해지고, 코어가\n * 어댑터별 사정을 아는 셈이 된다. 그래서 여기서는 \"지정 안 함\"을 `null` 로 표현하고,\n * 어댑터가 자기 내장 기본값을 쓰도록 넘긴다.\n */\nexport const ENDPOINT_FROM_ADAPTER = null;\n\n// ────────────────────────────────────────────────────────────────────────────\n// 경고\n// ────────────────────────────────────────────────────────────────────────────\n\nexport type ConfigWarningCode =\n /** 토큰이 없다 — 전송은 되지만 미귀속이다. */\n | \"missing-token\"\n /** 환경변수가 치환되지 않은 채로 들어왔다(빌드 설정 사고). */\n | \"unsubstituted-token\"\n /** 모르는 어댑터 이름 — 기본 어댑터로 되돌린다. */\n | \"unknown-adapter\"\n /** 모르는 visibility 값 — `all` 로 되돌린다. */\n | \"unknown-visibility\"\n /** 모르는 위치 값 — 우하단으로 되돌린다. */\n | \"unknown-position\"\n /** 사용자가 준 판정 함수가 예외를 던졌다 — 숨김으로 처리한다. */\n | \"visibility-threw\";\n\nexport interface ConfigWarning {\n code: ConfigWarningCode;\n message: string;\n}\n\n/**\n * 치환되지 않은 환경변수처럼 보이는 값.\n *\n * `EXPO_PUBLIC_` 접두사를 빠뜨리거나 `.env` 를 안 만들면 번들에 `undefined` 라는\n * **문자열**이 그대로 박히는 일이 흔하다. 눈으로는 토큰이 있는 것처럼 보이는데 서버는\n * 미귀속으로 받는다.\n */\nfunction looksUnsubstituted(token: string): boolean {\n const t = token.trim();\n if (t === \"undefined\" || t === \"null\" || t === \"NaN\") return true;\n if (t.includes(\"process.env.\") || t.includes(\"import.meta.env\")) return true;\n // `${VITE_TOKEN}` / `%NEXT_PUBLIC_TOKEN%` 같은 미치환 플레이스홀더.\n if (/^\\$\\{.*\\}$/.test(t) || /^%.*%$/.test(t)) return true;\n return false;\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 설정\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface FeedbackConfig {\n /** 수집 토큰. 없으면 미귀속으로 전송된다(막지 않는다). */\n token?: string | null;\n /** 수집 엔드포인트. 없으면 어댑터 내장 기본값. */\n endpoint?: string | null;\n /** 어댑터 이름 또는 어댑터 구현체. */\n adapter?: AdapterName | FeedbackAdapter;\n /** 노출 범위. 기본 `all`. */\n visibility?: Visibility;\n /** 모달을 열 때 화면을 자동 캡처할지. */\n captureScreenshot?: boolean;\n /** network·logs 링버퍼를 수집할지. false 면 아예 설치하지 않는다. */\n captureDiagnostics?: boolean;\n /** 플로팅 버튼 위치. 모서리만 주면 여백은 기본값. */\n position?: WidgetCorner | Partial<WidgetPosition>;\n /** `internal` 판정을 프로젝트가 직접 할 때. 주면 `internalRoles` 보다 우선한다. */\n isInternal?: VisibilityFn;\n /** `internal` 로 볼 역할 목록. 기본 {@link DEFAULT_INTERNAL_ROLES}. */\n internalRoles?: readonly string[];\n /** 개발 빌드 여부. 안 주면 런타임 전역에서 추론한다. */\n isDev?: boolean;\n /** 경고 출구. 안 주면 `resolveConfig` 결과의 `warnings` 로만 남는다. */\n onWarn?: (warning: ConfigWarning) => void;\n}\n\n/** 기본값이 모두 채워진 설정. 위젯은 이것만 본다. */\nexport interface ResolvedConfig {\n token: string | null;\n endpoint: string | null;\n adapter: AdapterName | FeedbackAdapter;\n visibility: Visibility;\n captureScreenshot: boolean;\n captureDiagnostics: boolean;\n position: WidgetPosition;\n isInternal: VisibilityFn | null;\n internalRoles: readonly string[];\n isDev: boolean;\n warnings: ConfigWarning[];\n}\n\n/**\n * 개발 빌드 여부 추론.\n *\n * 코어는 플랫폼 무관이라 `__DEV__`(RN)·`process.env.NODE_ENV`(웹/노드)를 **globalThis\n * 를 통해서만** 본다. 어느 쪽도 없으면 프로덕션으로 본다 — 모르는 상태에서 개발 빌드로\n * 단정하면 `dev-only` 가 실사용자에게 새기 때문이다.\n */\nexport function detectDevBuild(): boolean {\n const g = globalThis as Record<string, unknown>;\n if (typeof g.__DEV__ === \"boolean\") return g.__DEV__;\n const proc = g.process as { env?: Record<string, string | undefined> } | undefined;\n const nodeEnv = proc?.env?.NODE_ENV;\n if (typeof nodeEnv === \"string\") return nodeEnv !== \"production\";\n return false;\n}\n\nfunction normalizePosition(\n input: FeedbackConfig[\"position\"],\n warn: (code: ConfigWarningCode, message: string) => void\n): WidgetPosition {\n if (input === undefined || input === null) return { ...DEFAULT_POSITION };\n\n if (typeof input === \"string\") {\n if (CORNERS.includes(input)) {\n return { ...DEFAULT_POSITION, corner: input };\n }\n warn(\"unknown-position\", `모르는 position \"${input}\" — 우하단으로 되돌린다.`);\n return { ...DEFAULT_POSITION };\n }\n\n const corner =\n input.corner !== undefined && CORNERS.includes(input.corner)\n ? input.corner\n : DEFAULT_POSITION.corner;\n if (input.corner !== undefined && !CORNERS.includes(input.corner)) {\n warn(\"unknown-position\", `모르는 position.corner \"${String(input.corner)}\" — 우하단으로 되돌린다.`);\n }\n return {\n corner,\n offsetX: numberOr(input.offsetX, DEFAULT_POSITION.offsetX),\n offsetY: numberOr(input.offsetY, DEFAULT_POSITION.offsetY),\n };\n}\n\nfunction numberOr(value: unknown, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nfunction normalizeAdapter(\n input: FeedbackConfig[\"adapter\"],\n warn: (code: ConfigWarningCode, message: string) => void\n): AdapterName | FeedbackAdapter {\n if (input === undefined || input === null) return DEFAULT_ADAPTER;\n // 구현체를 직접 준 경우 — 이름 검사 대상이 아니다.\n if (typeof input === \"object\") return input;\n if (ADAPTER_NAMES.includes(input)) return input;\n warn(\"unknown-adapter\", `모르는 어댑터 \"${String(input)}\" — \"${DEFAULT_ADAPTER}\" 로 되돌린다.`);\n return DEFAULT_ADAPTER;\n}\n\nfunction normalizeVisibility(\n input: FeedbackConfig[\"visibility\"],\n warn: (code: ConfigWarningCode, message: string) => void\n): Visibility {\n if (input === undefined || input === null) return \"all\";\n if (typeof input === \"function\") return input;\n if ((VISIBILITY_KEYWORDS as readonly string[]).includes(input)) return input;\n warn(\"unknown-visibility\", `모르는 visibility \"${String(input)}\" — \"all\" 로 되돌린다.`);\n return \"all\";\n}\n\nfunction normalizeToken(\n input: FeedbackConfig[\"token\"],\n warn: (code: ConfigWarningCode, message: string) => void\n): string | null {\n const token = typeof input === \"string\" ? input.trim() : \"\";\n if (token === \"\") {\n warn(\n \"missing-token\",\n \"수집 토큰이 없다 — 제보는 그대로 전송되지만 프로젝트에 귀속되지 않는다.\"\n );\n return null;\n }\n if (looksUnsubstituted(token)) {\n warn(\n \"unsubstituted-token\",\n `토큰이 환경변수 치환 전 값으로 보인다(\"${token}\"). 환경변수는 빌드 타임에 인라인되므로 ` +\n \"값을 넣은 뒤 다시 빌드해야 하고, 앱은 OTA 로 반영되지 않는다.\"\n );\n return null;\n }\n return token;\n}\n\n/**\n * 부분 설정을 기본값으로 채운다.\n *\n * **던지지 않는다.** 설정이 틀렸다고 위젯이 호스트 앱을 깨거나 사용자 화면에 오류를\n * 띄우면, 붙이는 비용이 한 줄이라는 전제가 무너진다. 모르는 값은 기본값으로 되돌리고\n * 경고만 남긴다.\n */\nexport function resolveConfig(config: FeedbackConfig = {}): ResolvedConfig {\n const warnings: ConfigWarning[] = [];\n const warn = (code: ConfigWarningCode, message: string): void => {\n const warning: ConfigWarning = { code, message };\n warnings.push(warning);\n // 호스트가 준 출구도 깨질 수 있다 — 경고 때문에 설정 해석이 실패하면 안 된다.\n try {\n config.onWarn?.(warning);\n } catch {\n /* 무시 */\n }\n };\n\n const endpoint = typeof config.endpoint === \"string\" && config.endpoint.trim() !== \"\"\n ? config.endpoint.trim()\n : ENDPOINT_FROM_ADAPTER;\n\n return {\n token: normalizeToken(config.token, warn),\n endpoint,\n adapter: normalizeAdapter(config.adapter, warn),\n visibility: normalizeVisibility(config.visibility, warn),\n captureScreenshot: config.captureScreenshot ?? true,\n captureDiagnostics: config.captureDiagnostics ?? true,\n position: normalizePosition(config.position, warn),\n isInternal: typeof config.isInternal === \"function\" ? config.isInternal : null,\n internalRoles: config.internalRoles ?? DEFAULT_INTERNAL_ROLES,\n isDev: config.isDev ?? detectDevBuild(),\n warnings,\n };\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 노출 판정\n// ────────────────────────────────────────────────────────────────────────────\n\nexport function isInternalUser(resolved: ResolvedConfig, env: VisibilityEnv): boolean {\n if (resolved.isInternal) return resolved.isInternal(env) === true;\n\n const user = env.user;\n if (!user) return false;\n // 게스트는 내부 계정일 수 없다.\n if (user.isGuest === true) return false;\n const role = typeof user.role === \"string\" ? user.role.toLowerCase() : null;\n if (role === null) return false;\n return resolved.internalRoles.some((r) => r.toLowerCase() === role);\n}\n\n/**\n * 지금 이 사용자에게 플로팅 버튼을 보여줄지.\n *\n * 판정에 실패하면(사용자가 준 함수가 던지면) **숨긴다.** 노출 범위를 좁힌 프로젝트가\n * 있다는 건 보이면 안 되는 이유가 있다는 뜻이므로, 애매할 때 새는 쪽보다 안 보이는\n * 쪽이 안전하다.\n */\nexport function shouldShowWidget(\n resolved: ResolvedConfig,\n input: Partial<VisibilityEnv> = {}\n): boolean {\n const env: VisibilityEnv = {\n user: input.user ?? null,\n isDev: input.isDev ?? resolved.isDev,\n platform: input.platform ?? \"unknown\",\n };\n\n const visibility = resolved.visibility;\n // `isInternal` 도 사용자가 준 함수라, 판정 전체를 한 번에 감싼다.\n try {\n if (typeof visibility === \"function\") return visibility(env) === true;\n\n switch (visibility) {\n case \"all\":\n return true;\n case \"dev-only\":\n return env.isDev;\n case \"internal\":\n return isInternalUser(resolved, env);\n default:\n return true;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n resolved.warnings.push({\n code: \"visibility-threw\",\n message: `visibility 판정 함수가 실패해 위젯을 숨긴다: ${message}`,\n });\n return false;\n }\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// 진단 수집 배선\n// ────────────────────────────────────────────────────────────────────────────\n\n/** `DiagnosticsCollector` 중 설정 배선에 필요한 최소 형태. */\nexport interface DiagnosticsSource {\n install(): void;\n snapshot(): DiagnosticsPayload;\n}\n\n/**\n * `captureDiagnostics` 를 실제 배선으로 옮긴다.\n *\n * 꺼져 있으면 **공급자를 만들지 않고 수집기도 설치하지 않는다.** 빈 배열을 상수로\n * 돌려주는 공급자를 만들면 \"수집했는데 아무 일도 없었다\"와 \"애초에 수집하지 않았다\"가\n * 구분되지 않는다. 공급자가 없으면 `buildContext` 가 `diagnostics: null` 을 쓴다.\n */\nexport function diagnosticsProviderFor(\n resolved: Pick<ResolvedConfig, \"captureDiagnostics\">,\n collector: DiagnosticsSource\n): (() => DiagnosticsPayload) | undefined {\n if (!resolved.captureDiagnostics) return undefined;\n collector.install();\n return () => collector.snapshot();\n}\n","// 소스 경로 읽기 — 빌드 플러그인이 요소에 심어둔 속성을 해석한다.\n//\n// 왜 속성인가: 런타임에 React 내부(`_debugSource` 등)에서 얻는 소스 정보는 **개발\n// 빌드에서만** 채워진다. 프로덕션 번들에는 없다. 반면 빌드 시점에 심은 문자열 속성은\n// 미니파이에 훼손되지 않고 React 내부 구조에도 의존하지 않아서, 프로덕션에서도 원본\n// 파일 경로와 줄 번호가 그대로 남는다.\n//\n// 이 모듈은 **읽기만** 한다. 심는 쪽은 `@solhun/feedback-kit-build` 이고, 그 패키지는\n// 선택 사항이다. 속성이 없으면 여기서는 조용히 null 을 돌려주고 위젯은 그대로 동작한다\n// (소스 경로만 빈다).\n\nimport type { ElementInfo, SourceMapping } from \"./types.js\";\n\n/** 빌드 플러그인이 심는 속성 이름. `data-` 라서 DOM 이 그대로 통과시킨다. */\nexport const SOURCE_ATTR = \"data-fk-source\";\n\n/** 해석된 소스 위치. */\nexport interface SourceLocation {\n /** 원본 파일 경로(번들 경로가 아니다). */\n file: string;\n /** 1부터 시작하는 줄 번호. */\n line: number;\n /** 1부터 시작하는 칼럼. 없으면 null. */\n column: number | null;\n}\n\n/**\n * `\"src/pages/Home.tsx:42:7\"` 을 해석한다.\n *\n * 뒤에서부터 숫자 꼬리를 떼어낸다 — 파일 경로 자체에 콜론이 들어갈 수 있어서\n * (윈도우 드라이브 문자 `C:\\`) 앞에서부터 자르면 깨진다.\n */\nexport function parseSourceAttr(value: unknown): SourceLocation | null {\n if (typeof value !== \"string\") return null;\n const raw = value.trim();\n if (raw === \"\") return null;\n\n let rest = raw;\n let line: number | null = null;\n let column: number | null = null;\n\n for (let i = 0; i < 2; i += 1) {\n const at = rest.lastIndexOf(\":\");\n if (at <= 0) break;\n const tail = rest.slice(at + 1);\n if (!/^\\d+$/.test(tail)) break;\n const n = Number.parseInt(tail, 10);\n if (!Number.isFinite(n) || n <= 0) break;\n rest = rest.slice(0, at);\n // 뒤에서부터 읽으므로 첫 번째로 떼어낸 숫자가 칼럼, 두 번째가 줄이다.\n if (line === null) line = n;\n else {\n column = line;\n line = n;\n }\n }\n\n // `rest` 가 콜론으로 시작하면(`\":12:3\"`) 파일 자리가 비어 있다는 뜻이라 경로가 아니다.\n if (line === null || rest === \"\" || rest.startsWith(\":\")) return null;\n return { file: rest, line, column };\n}\n\n/**\n * 지목된 요소에서 소스 매핑을 뽑는다.\n *\n * `base` 는 화면 단위 매핑(빌드 플러그인의 screen-map)이다. 요소에서 얻은 파일 경로가\n * 더 구체적이므로 그걸 우선하고, 화면 식별자는 base 것을 유지한다.\n */\nexport function sourceFromElement(\n element: Pick<ElementInfo, \"attributes\"> | null | undefined,\n base?: SourceMapping | null\n): SourceMapping {\n const fallback: SourceMapping = {\n screenId: base?.screenId ?? null,\n sourceFile: base?.sourceFile ?? null,\n sourceLine: base?.sourceLine ?? null,\n };\n\n const attrs = element?.attributes;\n if (!attrs) return fallback;\n\n const loc = parseSourceAttr(attrs[SOURCE_ATTR]);\n if (!loc) return fallback;\n\n return { screenId: fallback.screenId, sourceFile: loc.file, sourceLine: loc.line };\n}\n","// 스크린샷 용량 정책.\n//\n// 캡처는 \"있으면 좋은 것\"이지 제출의 전제가 아니다. 그래서 이 모듈의 모든 실패 경로는\n// 예외를 던지지 않고 `failed: true` 로 내려온다 — 호출부가 제출을 막지 않게 하려는 것이다.\n\nimport type { FeedbackScreenshot } from \"../types.js\";\n\n/** 계약상 스크린샷 상한. base64 문자열 기준 8 MiB. */\nexport const SCREENSHOT_MAX_BASE64_BYTES = 8 * 1024 * 1024;\n\n/** 캡처가 불가능할 때 사용자에게 보여줄 문구. */\nexport const SCREENSHOT_FAILED_MESSAGE = \"캡처 실패 — 파일로 첨부할 수 있습니다\";\n\n/** 상한을 넘겼을 때 차례로 시도할 재인코딩 품질. 앞에서부터 시도하고 처음 통과하는 값을 쓴다. */\nexport const SCREENSHOT_QUALITY_STEPS: readonly number[] = [0.7, 0.5, 0.3];\n\n/**\n * base64 페이로드의 바이트 수.\n * base64 는 ASCII 라 문자 수가 곧 바이트 수다(디코딩 후 원본 크기가 아니라 전송량 기준).\n */\nexport function screenshotBytes(shot: FeedbackScreenshot | null | undefined): number {\n if (!shot || typeof shot.base64 !== \"string\") return 0;\n return shot.base64.length;\n}\n\nexport type ScreenshotCapture = () =>\n | FeedbackScreenshot\n | null\n | Promise<FeedbackScreenshot | null>;\n\nexport type ScreenshotReencode = (\n shot: FeedbackScreenshot,\n quality: number,\n) => FeedbackScreenshot | null | Promise<FeedbackScreenshot | null>;\n\nexport interface CaptureWithinLimitOpts {\n capture: ScreenshotCapture;\n /** 상한 초과 시 다시 인코딩하는 훅. 없으면 재인코딩 없이 바로 포기한다. */\n reencode?: ScreenshotReencode | null;\n qualitySteps?: readonly number[];\n limitBytes?: number;\n}\n\nexport interface ScreenshotOutcome {\n screenshot: FeedbackScreenshot | null;\n /** 캡처를 끝내 얻지 못했다. 제출은 계속 가능해야 한다. */\n failed: boolean;\n /** 실패했을 때만 채워지는 안내 문구. */\n message: string | null;\n /** 품질을 낮춰 다시 인코딩한 결과인가. */\n reencoded: boolean;\n}\n\nfunction failure(): ScreenshotOutcome {\n return {\n screenshot: null,\n failed: true,\n message: SCREENSHOT_FAILED_MESSAGE,\n reencoded: false,\n };\n}\n\nfunction usable(shot: FeedbackScreenshot | null | undefined): shot is FeedbackScreenshot {\n return !!shot && typeof shot.base64 === \"string\" && shot.base64.length > 0;\n}\n\n/**\n * 캡처한 뒤 용량 상한 안으로 들여보낸다.\n *\n * 1. 캡처 → 실패하거나 빈 결과면 곧장 실패로 내린다.\n * 2. 상한 이내면 그대로 쓴다.\n * 3. 초과하면 품질을 단계별로 낮춰 재인코딩하고, 처음 통과한 결과를 쓴다.\n * 4. 끝내 못 맞추면 스크린샷을 버리고 실패 문구로 대체한다(제출은 막지 않는다).\n */\nexport async function captureWithinLimit(\n opts: CaptureWithinLimitOpts,\n): Promise<ScreenshotOutcome> {\n const limit = opts.limitBytes ?? SCREENSHOT_MAX_BASE64_BYTES;\n\n let shot: FeedbackScreenshot | null = null;\n try {\n shot = (await opts.capture()) ?? null;\n } catch {\n // 캡처 구현이 던지는 예외는 여기서 흡수한다. 위로 새면 모달이 열리다 만다.\n shot = null;\n }\n if (!usable(shot)) return failure();\n\n if (screenshotBytes(shot) <= limit) {\n return { screenshot: shot, failed: false, message: null, reencoded: false };\n }\n\n const steps = opts.qualitySteps ?? SCREENSHOT_QUALITY_STEPS;\n if (opts.reencode) {\n for (const quality of steps) {\n let next: FeedbackScreenshot | null = null;\n try {\n next = (await opts.reencode(shot, quality)) ?? null;\n } catch {\n next = null;\n }\n if (usable(next) && screenshotBytes(next) <= limit) {\n return { screenshot: next, failed: false, message: null, reencoded: true };\n }\n }\n }\n\n return failure();\n}\n","// 핀 지정.\n//\n// 핀은 \"한 화면에 하나\"다. 그래서 컬렉션이 아니라 단일 값으로 들고, 새로 찍으면 덮어쓴다.\n// 좌표는 항상 0~1 상대값으로 보관한다 — 기기 해상도가 달라도 같은 지점을 가리키게 하려는 것이다.\n\nimport type { FeedbackPin } from \"../types.js\";\n\nexport interface PixelPoint {\n x: number;\n y: number;\n}\n\nexport interface PixelSize {\n width: number;\n height: number;\n}\n\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n\n/**\n * 픽셀 좌표를 스크린샷 기준 0~1 상대 좌표로 바꾼다.\n * 크기가 0 이하이면(아직 레이아웃 전) 나눗셈이 무의미하므로 0 으로 떨어뜨린다.\n */\nexport function normalizePin(point: PixelPoint, size: PixelSize): FeedbackPin {\n const x = size.width > 0 ? point.x / size.width : 0;\n const y = size.height > 0 ? point.y / size.height : 0;\n return { x: clamp01(x), y: clamp01(y) };\n}\n\n/** 상대 좌표를 다시 픽셀로 되돌린다(마커를 그릴 때 쓴다). */\nexport function denormalizePin(pin: FeedbackPin, size: PixelSize): PixelPoint {\n return { x: pin.x * size.width, y: pin.y * size.height };\n}\n\n/**\n * 핀 지정 화면의 상태기계.\n *\n * `draft` 는 지정 화면에서 만지는 중인 값, `confirmed` 는 모달로 돌아간 확정값이다.\n * 둘을 나눠 둔 이유는 취소했을 때 이전 확정값이 살아남아야 하기 때문이다.\n */\nexport class PinController {\n private confirmedPin: FeedbackPin | null;\n private draftPin: FeedbackPin | null;\n private opened = false;\n\n constructor(initial: FeedbackPin | null = null) {\n this.confirmedPin = initial;\n this.draftPin = initial;\n }\n\n get isOpen(): boolean {\n return this.opened;\n }\n\n get draft(): FeedbackPin | null {\n return this.draftPin;\n }\n\n get confirmed(): FeedbackPin | null {\n return this.confirmedPin;\n }\n\n /** 화면에 떠 있는 핀 개수. 단일 핀 규칙이라 0 아니면 1이다. */\n get count(): number {\n return this.draftPin ? 1 : 0;\n }\n\n open(): void {\n this.draftPin = this.confirmedPin;\n this.opened = true;\n }\n\n /** 탭한 자리에 핀을 찍는다. 기존 핀이 있으면 그 자리로 옮긴다(추가가 아니다). */\n place(point: PixelPoint, size: PixelSize): FeedbackPin {\n const pin = normalizePin(point, size);\n this.draftPin = pin;\n return pin;\n }\n\n /** 이미 상대 좌표를 갖고 있을 때 쓰는 경로. */\n placeRelative(pin: FeedbackPin): FeedbackPin {\n const next = { x: clamp01(pin.x), y: clamp01(pin.y) };\n this.draftPin = next;\n return next;\n }\n\n confirm(): FeedbackPin | null {\n this.confirmedPin = this.draftPin;\n this.opened = false;\n return this.confirmedPin;\n }\n\n /** 취소 — 이전에 확정한 좌표로 되돌린다. */\n cancel(): FeedbackPin | null {\n this.draftPin = this.confirmedPin;\n this.opened = false;\n return this.confirmedPin;\n }\n\n reset(): void {\n this.confirmedPin = null;\n this.draftPin = null;\n this.opened = false;\n }\n}\n","// 모달 포커스 순서.\n//\n// DOM 없이도 검증할 수 있도록 \"포커스 가능한 것들의 순서\"를 문자열 id 목록으로 들고 돈다.\n// 실제 렌더러는 이 id 를 자기 노드에 매달아 두고 focus() 를 걸면 된다.\n\n/** 위젯 플로팅 버튼의 고정 id. 모달을 닫으면 여기로 포커스를 되돌린다. */\nexport const FLOATING_BUTTON_ID = \"feedback-kit-floating-button\";\n\n/**\n * 순환하는 포커스 링. 끝에서 Tab 하면 처음으로 돌아온다 = 포커스가 모달 밖으로 새지 않는다.\n */\nexport class FocusRing {\n private items: string[];\n private index: number;\n\n constructor(items: readonly string[] = []) {\n this.items = [...items];\n this.index = this.items.length > 0 ? 0 : -1;\n }\n\n get order(): readonly string[] {\n return this.items;\n }\n\n get current(): string | null {\n return this.index >= 0 && this.index < this.items.length ? this.items[this.index]! : null;\n }\n\n /**\n * 항목 목록을 바꾼다. 지금 포커스된 항목이 새 목록에도 있으면 그 자리를 유지한다\n * (스크린샷을 지웠다고 포커스가 처음으로 튀면 키보드 사용자가 길을 잃는다).\n */\n setItems(items: readonly string[]): void {\n const focused = this.current;\n this.items = [...items];\n if (this.items.length === 0) {\n this.index = -1;\n return;\n }\n const keep = focused === null ? -1 : this.items.indexOf(focused);\n this.index = keep >= 0 ? keep : 0;\n }\n\n focus(id: string): boolean {\n const at = this.items.indexOf(id);\n if (at < 0) return false;\n this.index = at;\n return true;\n }\n\n next(): string | null {\n if (this.items.length === 0) return null;\n this.index = (this.index + 1) % this.items.length;\n return this.current;\n }\n\n prev(): string | null {\n if (this.items.length === 0) return null;\n this.index = (this.index - 1 + this.items.length) % this.items.length;\n return this.current;\n }\n\n contains(id: string): boolean {\n return this.items.includes(id);\n }\n}\n","// 리포트 모달의 상태기계.\n//\n// 여기엔 DOM 도 RN 도 없다. 렌더러는 `getState()` 를 그리고 사용자의 동작을 메서드로 되돌려준다.\n// 전송·재시도·멱등키는 전부 `FeedbackQueue` 에 맡긴다 — 이 모듈이 전송을 다시 구현하지 않는다.\n\nimport type {\n FeedbackPin,\n FeedbackPriority,\n FeedbackReport,\n FeedbackScreenshot,\n} from \"../types.js\";\nimport type { ReportParts } from \"../report.js\";\nimport type { QueueStatus, SubmitOutcome } from \"../queue.js\";\nimport {\n captureWithinLimit,\n SCREENSHOT_FAILED_MESSAGE,\n type ScreenshotCapture,\n type ScreenshotReencode,\n} from \"./screenshot.js\";\nimport { FLOATING_BUTTON_ID, FocusRing } from \"./focus.js\";\n\n/** 코멘트 상한. 넘으면 자르지 않고 거부한다(사용자가 쓴 글을 임의로 삭제하지 않는다). */\nexport const COMMENT_MAX_CHARS = 4000;\n\nexport const COMMENT_REQUIRED_MESSAGE = \"의견을 입력해주세요\";\nexport const COMMENT_TOO_LONG_MESSAGE = \"4,000자 이하로 입력해주세요\";\nexport const SUBMIT_DONE_MESSAGE = \"보냈습니다\";\nexport const SUBMIT_PENDING_MESSAGE = \"대기 중\";\nexport const SUBMIT_FAILED_MESSAGE = \"보내지 못했습니다\";\n\nexport type WidgetPlatform = \"web\" | \"native\";\nexport type ScreenshotStatus = \"none\" | \"capturing\" | \"ready\" | \"failed\";\nexport type ModalSubmitStatus = \"idle\" | \"sending\" | \"sent\" | \"pending\" | \"failed\";\n\n/** 모달 안에서 포커스를 받는 요소들의 고정 id. 렌더러가 그대로 매단다. */\nexport const MODAL_FIELD_COMMENT = \"comment\";\nexport const MODAL_FIELD_PRIORITY = \"priority\";\nexport const MODAL_ACTION_ATTACH = \"attach\";\nexport const MODAL_ACTION_REMOVE_SCREENSHOT = \"remove-screenshot\";\nexport const MODAL_ACTION_PIN = \"pin\";\nexport const MODAL_ACTION_PICK = \"pick\";\nexport const MODAL_ACTION_RETRY = \"retry\";\nexport const MODAL_ACTION_CANCEL = \"cancel\";\nexport const MODAL_ACTION_SEND = \"send\";\n\nexport interface ReportModalState {\n open: boolean;\n comment: string;\n priority: FeedbackPriority;\n screenshot: FeedbackScreenshot | null;\n screenshotStatus: ScreenshotStatus;\n screenshotMessage: string | null;\n pin: FeedbackPin | null;\n submitStatus: ModalSubmitStatus;\n submitMessage: string | null;\n commentError: string | null;\n canSubmit: boolean;\n /** 핀 찍기를 열 수 있는가. 스크린샷이 있어야 좌표가 의미를 갖는다. */\n canPin: boolean;\n showRetry: boolean;\n /** 대기 큐에 남아 있는 건수. */\n pending: number;\n closeConfirmVisible: boolean;\n /** 이 플랫폼에서 노출되는 부가 동작들. 웹은 요소 지목, 앱은 핀 찍기. */\n actions: readonly string[];\n focusOrder: readonly string[];\n focused: string | null;\n /** 모달이 닫힌 뒤 포커스를 돌려줄 곳. */\n restoreFocusTo: string | null;\n}\n\n/** 모달이 큐에게 요구하는 최소 계약. `FeedbackQueue` 가 그대로 만족한다. */\nexport interface ModalQueueLike {\n submit(report: FeedbackReport): Promise<SubmitOutcome>;\n retryNow(): Promise<void>;\n size(): Promise<number>;\n subscribe(listener: (status: QueueStatus) => void): () => void;\n /** 현재 모달 제보만 다른 대기 항목과 구분하기 위한 제출 단위 결말. */\n getOutcome?(clientSubmissionId: string): Pick<SubmitOutcome, \"delivered\" | \"id\"> | null;\n /** 실제 FeedbackQueue의 online/주기 재시도 lifecycle. 테스트 대역은 생략할 수 있다. */\n start?(): void;\n stop?(): void;\n}\n\nexport interface ReportModalOpts {\n queue: ModalQueueLike;\n /** 제보 조립. 보통 `buildReport` 를 부분 적용해 넘긴다. */\n createReport: (parts: ReportParts) => Promise<FeedbackReport>;\n platform: WidgetPlatform;\n /** 모달을 열 때 자동 캡처. 없으면 캡처 단계를 건너뛴다. */\n capture?: ScreenshotCapture | null;\n reencode?: ScreenshotReencode | null;\n screenshotLimitBytes?: number;\n}\n\nexport type ReportModalListener = (state: ReportModalState) => void;\n\nexport class ReportModalController {\n private readonly queue: ModalQueueLike;\n private readonly createReport: (parts: ReportParts) => Promise<FeedbackReport>;\n private readonly platform: WidgetPlatform;\n private readonly capture: ScreenshotCapture | null;\n private readonly reencode: ScreenshotReencode | null;\n private readonly screenshotLimitBytes: number | undefined;\n\n private readonly listeners = new Set<ReportModalListener>();\n private readonly ring = new FocusRing();\n private readonly unsubscribeQueue: () => void;\n\n /** 사용자가 입력란을 건드렸는가. 열자마자 빨간 문구가 뜨는 걸 막으려고 둔다. */\n private touched = false;\n private submitAttempted = false;\n private lastReport: FeedbackReport | null = null;\n /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */\n private captureGeneration = 0;\n\n private state: ReportModalState = {\n open: false,\n comment: \"\",\n priority: \"unset\",\n screenshot: null,\n screenshotStatus: \"none\",\n screenshotMessage: null,\n pin: null,\n submitStatus: \"idle\",\n submitMessage: null,\n commentError: null,\n canSubmit: false,\n canPin: false,\n showRetry: false,\n pending: 0,\n closeConfirmVisible: false,\n actions: [],\n focusOrder: [],\n focused: null,\n restoreFocusTo: null,\n };\n\n constructor(opts: ReportModalOpts) {\n this.queue = opts.queue;\n this.createReport = opts.createReport;\n this.platform = opts.platform;\n this.capture = opts.capture ?? null;\n this.reencode = opts.reencode ?? null;\n this.screenshotLimitBytes = opts.screenshotLimitBytes;\n\n // 현재 제보의 결말만 본다. 큐 전체 길이로 판단하면 다른 제보가 남았을 때 성공을 놓치거나,\n // 현재 제보가 영구 실패로 빠졌을 때 큐가 비었다는 이유로 성공 처리하게 된다.\n this.unsubscribeQueue = this.queue.subscribe((status) => {\n this.patch({ pending: status.pending });\n if (this.state.submitStatus !== \"pending\" || !this.lastReport) return;\n\n const outcome = this.queue.getOutcome?.(this.lastReport.clientSubmissionId) ?? null;\n const delivered = outcome?.delivered === true;\n const rejected = outcome?.delivered === false;\n // 오래된 테스트 대역처럼 제출 단위 API가 없는 경우에만 기존의 단일 큐 가정을 쓴다.\n const legacyDrained = !this.queue.getOutcome && status.pending === 0;\n if (delivered || legacyDrained) {\n this.patch({ submitStatus: \"sent\", submitMessage: SUBMIT_DONE_MESSAGE });\n this.close({ keepResult: true });\n } else if (rejected) {\n this.patch({ submitStatus: \"failed\", submitMessage: SUBMIT_FAILED_MESSAGE });\n }\n });\n this.recompute();\n }\n\n // ── 조회 ──────────────────────────────────────────────────────────────────\n\n getState(): ReportModalState {\n return this.state;\n }\n\n /** 마지막으로 조립한 제보. 재시도가 같은 멱등키를 쓰는지 확인할 때 쓴다. */\n getLastReport(): FeedbackReport | null {\n return this.lastReport;\n }\n\n subscribe(listener: ReportModalListener): () => void {\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n dispose(): void {\n this.unsubscribeQueue();\n this.listeners.clear();\n }\n\n /** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */\n stopQueue(): void {\n this.queue.stop?.();\n }\n\n // ── 열기/닫기 ─────────────────────────────────────────────────────────────\n\n async open(): Promise<void> {\n if (this.state.open) return;\n const wasPending = this.state.submitStatus === \"pending\" && this.lastReport !== null;\n const pendingReport = wasPending ? this.lastReport : null;\n this.touched = false;\n this.submitAttempted = false;\n this.patch({\n open: true,\n comment: pendingReport?.comment ?? \"\",\n priority: pendingReport?.priority ?? \"unset\",\n screenshot: pendingReport?.screenshot ?? null,\n screenshotStatus: \"none\",\n screenshotMessage: null,\n pin: pendingReport?.pin ?? null,\n submitStatus: wasPending ? \"pending\" : \"idle\",\n submitMessage: wasPending ? SUBMIT_PENDING_MESSAGE : null,\n closeConfirmVisible: false,\n restoreFocusTo: null,\n });\n this.ring.focus(MODAL_FIELD_COMMENT);\n this.patch({ focused: this.ring.current });\n // 대기 항목을 다시 연 경우에는 최초 입력을 그대로 보여줘야 하므로 새 캡처로 덮지 않는다.\n if (this.capture && !wasPending) await this.runCapture();\n }\n\n /**\n * 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.\n * @returns 실제로 닫혔으면 `\"closed\"`, 확인이 필요하면 `\"confirm\"`.\n */\n requestClose(): \"closed\" | \"confirm\" {\n if (this.state.comment.length >= 1) {\n this.patch({ closeConfirmVisible: true });\n return \"confirm\";\n }\n this.close();\n return \"closed\";\n }\n\n /** 확인 대화에서 \"닫기\"를 고른 경우. */\n confirmClose(): void {\n this.close();\n }\n\n /** 확인 대화에서 \"계속 쓰기\"를 고른 경우. 입력은 그대로 남는다. */\n cancelClose(): void {\n this.patch({ closeConfirmVisible: false });\n }\n\n /** 확인 없이 닫는다(요소 지목 모드로 넘어갈 때처럼 흐름이 이어지는 경우). */\n dismiss(): void {\n this.close();\n }\n\n private close(opts?: { keepResult?: boolean }): void {\n this.captureGeneration += 1;\n const keepDraft = opts?.keepResult || this.state.submitStatus === \"pending\";\n const cleared = keepDraft\n ? {}\n : {\n comment: \"\",\n priority: \"unset\" as FeedbackPriority,\n screenshot: null,\n screenshotStatus: \"none\" as ScreenshotStatus,\n screenshotMessage: null,\n pin: null,\n submitStatus: \"idle\" as ModalSubmitStatus,\n submitMessage: null,\n };\n this.touched = false;\n this.submitAttempted = false;\n this.patch({\n open: false,\n closeConfirmVisible: false,\n focused: null,\n restoreFocusTo: FLOATING_BUTTON_ID,\n ...cleared,\n });\n }\n\n // ── 입력 ──────────────────────────────────────────────────────────────────\n\n /** 상한을 넘겨도 값을 자르지 않는다. 거부는 하되 사용자가 쓴 글은 보존한다. */\n setComment(value: string): void {\n if (this.isDraftLocked()) return;\n this.touched = true;\n this.patch({ comment: value });\n }\n\n setPriority(priority: FeedbackPriority): void {\n if (this.isDraftLocked()) return;\n this.patch({ priority });\n }\n\n setPin(pin: FeedbackPin | null): void {\n if (this.isDraftLocked()) return;\n this.patch({ pin });\n }\n\n /** 스크린샷 제거 — 핀은 스크린샷 위 좌표라 함께 사라진다. */\n removeScreenshot(): void {\n if (this.isDraftLocked()) return;\n this.captureGeneration += 1;\n this.patch({\n screenshot: null,\n screenshotStatus: \"none\",\n screenshotMessage: null,\n pin: null,\n });\n }\n\n async recapture(): Promise<void> {\n if (!this.capture || this.isDraftLocked()) return;\n await this.runCapture();\n }\n\n /** 캡처가 실패했을 때의 대체 경로 — 사용자가 직접 고른 파일. */\n async attachFile(shot: FeedbackScreenshot): Promise<void> {\n if (this.isDraftLocked()) return;\n const generation = ++this.captureGeneration;\n const outcome = await captureWithinLimit({\n capture: () => shot,\n reencode: this.reencode,\n limitBytes: this.screenshotLimitBytes,\n });\n if (generation === this.captureGeneration && this.state.open && !this.isDraftLocked()) {\n this.applyScreenshot(outcome.screenshot, outcome.failed);\n }\n }\n\n private async runCapture(): Promise<void> {\n const generation = ++this.captureGeneration;\n this.patch({ screenshotStatus: \"capturing\", screenshotMessage: null });\n const outcome = await captureWithinLimit({\n capture: this.capture!,\n reencode: this.reencode,\n limitBytes: this.screenshotLimitBytes,\n });\n if (generation === this.captureGeneration && this.state.open) {\n this.applyScreenshot(outcome.screenshot, outcome.failed);\n }\n }\n\n private applyScreenshot(shot: FeedbackScreenshot | null, failed: boolean): void {\n if (shot && !failed) {\n // 핀은 이전 이미지 기준 좌표이므로 이미지가 바뀌면 함께 폐기한다.\n this.patch({ screenshot: shot, screenshotStatus: \"ready\", screenshotMessage: null, pin: null });\n return;\n }\n this.patch({\n screenshot: null,\n screenshotStatus: \"failed\",\n screenshotMessage: SCREENSHOT_FAILED_MESSAGE,\n pin: null,\n });\n }\n\n // ── 포커스 ────────────────────────────────────────────────────────────────\n\n tabNext(): string | null {\n const next = this.ring.next();\n this.patch({ focused: next });\n return next;\n }\n\n tabPrev(): string | null {\n const prev = this.ring.prev();\n this.patch({ focused: prev });\n return prev;\n }\n\n focus(id: string): boolean {\n const ok = this.ring.focus(id);\n if (ok) this.patch({ focused: this.ring.current });\n return ok;\n }\n\n // ── 전송 ──────────────────────────────────────────────────────────────────\n\n /**\n * 제출. 실패해도 예외를 던지지 않는다 — 대기 큐에 남기고 입력을 보존한다.\n * @returns 검증에 걸려 아무것도 보내지 않았으면 `null`.\n */\n async submit(): Promise<SubmitOutcome | null> {\n this.submitAttempted = true;\n if (this.state.submitStatus === \"sending\" || this.state.submitStatus === \"pending\") return null;\n if (!this.isCommentValid()) {\n this.recompute();\n this.emit();\n return null;\n }\n\n this.patch({ submitStatus: \"sending\", submitMessage: null });\n\n const parts: ReportParts = {\n kind: \"report\",\n comment: this.state.comment,\n priority: this.state.priority,\n screenshot: this.state.screenshot,\n pin: this.state.pin,\n element: null,\n };\n\n let outcome: SubmitOutcome;\n try {\n const report = await this.createReport(parts);\n this.lastReport = report;\n outcome = await this.queue.submit(report);\n } catch {\n // 조립되지 않은 제보는 큐에 들어간 적이 없다. 대기 중으로 가장하지 않고 재시도 가능한\n // 명시 실패로 남겨 사용자의 입력을 보존한다.\n outcome = { delivered: false, id: null, queued: false };\n }\n\n if (outcome.delivered) {\n this.patch({ submitStatus: \"sent\", submitMessage: SUBMIT_DONE_MESSAGE });\n this.close({ keepResult: true });\n } else if (outcome.queued) {\n this.patch({ submitStatus: \"pending\", submitMessage: SUBMIT_PENDING_MESSAGE });\n } else {\n this.patch({ submitStatus: \"failed\", submitMessage: SUBMIT_FAILED_MESSAGE });\n }\n return outcome;\n }\n\n /** [다시 보내기]. 큐가 비면 완료로 넘어간다. */\n async retry(): Promise<void> {\n if (this.state.submitStatus !== \"pending\" && this.state.pending === 0) return;\n const retryingCurrent = this.state.submitStatus === \"pending\";\n if (retryingCurrent) this.patch({ submitStatus: \"sending\", submitMessage: null });\n try {\n await this.queue.retryNow();\n } catch {\n // retryNow 는 삼키는 게 정상이지만 방어적으로 한 번 더 막는다.\n }\n const remaining = await this.queue.size();\n if (!retryingCurrent) {\n this.patch({ pending: remaining });\n } else {\n const outcome = this.lastReport\n ? this.queue.getOutcome?.(this.lastReport.clientSubmissionId) ?? null\n : null;\n const delivered = outcome?.delivered === true;\n const rejected = outcome?.delivered === false;\n const legacyDrained = !this.queue.getOutcome && remaining === 0;\n if (delivered || legacyDrained) {\n this.patch({ submitStatus: \"sent\", submitMessage: SUBMIT_DONE_MESSAGE });\n this.close({ keepResult: true });\n } else if (rejected) {\n this.patch({ submitStatus: \"failed\", submitMessage: SUBMIT_FAILED_MESSAGE });\n } else {\n this.patch({ submitStatus: \"pending\", submitMessage: SUBMIT_PENDING_MESSAGE });\n }\n }\n }\n\n // ── 내부 ──────────────────────────────────────────────────────────────────\n\n private isCommentValid(): boolean {\n const value = this.state.comment;\n return value.trim().length > 0 && value.length <= COMMENT_MAX_CHARS;\n }\n\n private isDraftLocked(): boolean {\n return this.state.submitStatus === \"sending\" || this.state.submitStatus === \"pending\";\n }\n\n private commentErrorFor(): string | null {\n const value = this.state.comment;\n if (value.length > COMMENT_MAX_CHARS) return COMMENT_TOO_LONG_MESSAGE;\n if (value.trim().length === 0) {\n // 아직 아무것도 안 건드린 상태에서 경고를 띄우지 않는다.\n return this.touched || this.submitAttempted ? COMMENT_REQUIRED_MESSAGE : null;\n }\n return null;\n }\n\n private actionsFor(): string[] {\n // 웹은 DOM 요소를 직접 지목할 수 있고, 앱은 좌표(핀)로만 가리킨다.\n return this.platform === \"web\" ? [MODAL_ACTION_PICK] : [MODAL_ACTION_PIN];\n }\n\n private focusOrderFor(): string[] {\n const order: string[] = [MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY];\n if (this.state.screenshot) order.push(MODAL_ACTION_REMOVE_SCREENSHOT);\n else order.push(MODAL_ACTION_ATTACH);\n order.push(...this.actionsFor());\n if (this.state.submitStatus === \"pending\") order.push(MODAL_ACTION_RETRY);\n order.push(MODAL_ACTION_CANCEL, MODAL_ACTION_SEND);\n return order;\n }\n\n private recompute(): void {\n const commentError = this.commentErrorFor();\n const canSubmit =\n this.isCommentValid() &&\n this.state.screenshotStatus !== \"capturing\" &&\n (this.state.submitStatus === \"idle\" || this.state.submitStatus === \"failed\");\n const canPin =\n this.platform === \"native\" &&\n this.state.screenshot !== null &&\n this.state.screenshotStatus !== \"capturing\" &&\n !this.isDraftLocked() &&\n this.state.open;\n const actions = this.actionsFor();\n const focusOrder = this.state.open ? this.focusOrderFor() : [];\n this.ring.setItems(focusOrder);\n this.state = {\n ...this.state,\n commentError,\n canSubmit,\n canPin,\n showRetry: this.state.submitStatus === \"pending\" || this.state.pending > 0,\n actions,\n focusOrder,\n focused: this.state.open ? this.ring.current : null,\n };\n }\n\n private patch(partial: Partial<ReportModalState>): void {\n this.state = { ...this.state, ...partial };\n this.recompute();\n this.emit();\n }\n\n private emit(): void {\n for (const listener of this.listeners) listener(this.state);\n }\n}\n","// 화면 지도.\n//\n// 위젯이 가질 수 있는 화면은 넷뿐이다: 플로팅 버튼 / 리포트 모달 / 핀 지정 / 요소 지목.\n// 어느 화면에서 어느 화면으로 갈 수 있는지를 여기 한 곳에 모아 둔다 — 흩어지면\n// \"취소했는데 이전 좌표가 날아간다\" 같은 사고가 화면마다 따로 생긴다.\n\nimport type { FeedbackPin } from \"../types.js\";\nimport type { SubmitOutcome } from \"../queue.js\";\nimport {\n ReportModalController,\n type ReportModalOpts,\n type ReportModalState,\n type WidgetPlatform,\n} from \"./modal.js\";\nimport { PinController, type PixelPoint, type PixelSize } from \"./pin.js\";\n\n/** 위젯이 보여줄 수 있는 화면. */\nexport type WidgetScreen = \"button\" | \"modal\" | \"pin\" | \"picking\";\n\nexport interface WidgetControllerOpts extends ReportModalOpts {\n /** 저장돼 있던 요소 지목 모드를 복원할 때 쓴다(새로고침·페이지 이동 후). */\n initialPicking?: boolean;\n /** 지목 모드가 켜지고 꺼질 때 호출. 호스트가 저장소에 기록한다. */\n onPickingChange?: (active: boolean) => void;\n}\n\nexport interface WidgetState {\n screen: WidgetScreen;\n modal: ReportModalState;\n /** 핀 지정 화면에서 만지는 중인 좌표. */\n pinDraft: FeedbackPin | null;\n /** 모달로 돌아간 확정 좌표. */\n pinConfirmed: FeedbackPin | null;\n pickingActive: boolean;\n}\n\nexport type WidgetListener = (state: WidgetState) => void;\n\nexport class WidgetController {\n readonly modal: ReportModalController;\n readonly pin: PinController;\n\n private readonly platform: WidgetPlatform;\n private readonly onPickingChange: ((active: boolean) => void) | null;\n private readonly listeners = new Set<WidgetListener>();\n private readonly unsubscribeModal: () => void;\n\n private screen: WidgetScreen;\n private picking: boolean;\n\n constructor(opts: WidgetControllerOpts) {\n this.platform = opts.platform;\n this.onPickingChange = opts.onPickingChange ?? null;\n this.modal = new ReportModalController(opts);\n this.pin = new PinController(null);\n opts.queue.start?.();\n this.picking = opts.initialPicking ?? false;\n this.screen = this.picking ? \"picking\" : \"button\";\n\n // 모달이 스스로 닫히는 경로(전송 성공, 큐 자동 배출)도 화면 지도에 반영해야 한다.\n this.unsubscribeModal = this.modal.subscribe((state) => {\n // 스크린샷을 제거하거나 모달을 새로 열면 그 이미지에 속했던 확정 핀도 함께 폐기한다.\n // modal.pin만 비우면 PinController의 confirmed 값이 다음 핀 화면에서 되살아난다.\n if (state.pin === null && this.pin.confirmed !== null) this.pin.reset();\n if (!state.open && (this.screen === \"modal\" || this.screen === \"pin\")) {\n this.screen = this.picking ? \"picking\" : \"button\";\n }\n this.emit();\n });\n }\n\n // ── 조회 ──────────────────────────────────────────────────────────────────\n\n getScreen(): WidgetScreen {\n return this.screen;\n }\n\n getState(): WidgetState {\n return {\n screen: this.screen,\n modal: this.modal.getState(),\n pinDraft: this.pin.draft,\n pinConfirmed: this.pin.confirmed,\n pickingActive: this.picking,\n };\n }\n\n get isPicking(): boolean {\n return this.picking;\n }\n\n subscribe(listener: WidgetListener): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n dispose(): void {\n this.unsubscribeModal();\n this.modal.dispose();\n this.modal.stopQueue();\n this.listeners.clear();\n }\n\n // ── 플로팅 버튼 ↔ 모달 ────────────────────────────────────────────────────\n\n async openReport(): Promise<void> {\n this.screen = \"modal\";\n await this.modal.open();\n this.emit();\n }\n\n /** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */\n closeReport(): \"closed\" | \"confirm\" {\n const result = this.modal.requestClose();\n if (result === \"closed\") {\n this.pin.reset();\n this.screen = this.picking ? \"picking\" : \"button\";\n }\n this.emit();\n return result;\n }\n\n confirmCloseReport(): void {\n this.modal.confirmClose();\n this.pin.reset();\n this.screen = this.picking ? \"picking\" : \"button\";\n this.emit();\n }\n\n cancelCloseReport(): void {\n this.modal.cancelClose();\n this.emit();\n }\n\n async submitReport(): Promise<SubmitOutcome | null> {\n const outcome = await this.modal.submit();\n if (outcome?.delivered) this.pin.reset();\n this.emit();\n return outcome;\n }\n\n async retryReport(): Promise<void> {\n await this.modal.retry();\n this.emit();\n }\n\n // ── 핀 지정 ───────────────────────────────────────────────────────────────\n\n /**\n * 핀 지정 화면 열기. 스크린샷이 없으면 좌표가 가리킬 대상이 없으므로 열지 않는다.\n * @returns 열렸으면 true.\n */\n openPinScreen(): boolean {\n if (!this.modal.getState().canPin) return false;\n this.pin.open();\n this.screen = \"pin\";\n this.emit();\n return true;\n }\n\n /** 핀 지정 화면에서 탭. 기존 핀이 있으면 그 자리로 옮긴다. */\n placePin(point: PixelPoint, size: PixelSize): FeedbackPin | null {\n if (this.screen !== \"pin\") return null;\n const placed = this.pin.place(point, size);\n this.emit();\n return placed;\n }\n\n /** 확정 — 모달로 돌아가고 좌표가 제출에 실린다. */\n confirmPin(): FeedbackPin | null {\n if (this.screen !== \"pin\") return this.pin.confirmed;\n const confirmed = this.pin.confirm();\n this.modal.setPin(confirmed);\n this.screen = \"modal\";\n this.emit();\n return confirmed;\n }\n\n /** 취소 — 이전 확정 좌표를 그대로 두고 모달로 돌아간다. */\n cancelPin(): FeedbackPin | null {\n if (this.screen !== \"pin\") return this.pin.confirmed;\n const previous = this.pin.cancel();\n this.modal.setPin(previous);\n this.screen = \"modal\";\n this.emit();\n return previous;\n }\n\n // ── 요소 지목 (웹 전용) ───────────────────────────────────────────────────\n\n /**\n * 요소 지목 모드 시작. 모달에서 넘어오는 흐름이라 닫기 확인을 묻지 않는다\n * (사용자가 이미 \"지목하러 가겠다\"고 밝힌 상태다).\n */\n startPicking(): boolean {\n if (this.platform !== \"web\") return false;\n this.modal.dismiss();\n this.setPicking(true);\n this.screen = \"picking\";\n this.emit();\n return true;\n }\n\n /** [지목 종료]. 플로팅 버튼 화면으로 돌아가고 저장된 모드 표시도 지운다. */\n stopPicking(): void {\n this.setPicking(false);\n if (this.screen === \"picking\") this.screen = \"button\";\n this.emit();\n }\n\n private setPicking(active: boolean): void {\n if (this.picking === active) return;\n this.picking = active;\n this.onPickingChange?.(active);\n }\n\n private emit(): void {\n const state = this.getState();\n for (const listener of this.listeners) listener(state);\n }\n}\n","// 어댑터 공용 본문 렌더러.\n//\n// 왜 어댑터마다 따로 안 쓰고 여기 한 곳에 모으는가:\n// 제보를 어느 대상(라쏘런·Linear·Notion)으로 보내든 **사람이 읽는 내용은 같아야 한다.**\n// 어댑터마다 본문을 따로 조립하면 \"Linear 로 보낸 제보에는 소스 경로가 없다\" 같은\n// 차이가 조용히 생긴다. 그래서 어댑터는 이 함수가 만든 문자열을 자기 필드명에 실어\n// 나르기만 하고, 내용 구성에는 관여하지 않는다(DP-255 TC3·TC4).\n//\n// 접는 규칙도 여기 하나뿐이다:\n// - 대상이 받아주지 못하는 구조화 필드(진단·기기·요소 등)는 **본문 하단의 접힌 블록**에 넣는다.\n// - 접힌 블록은 제보 하나당 **정확히 하나**다. 어댑터가 추가로 접지 않는다.\n// - 사용자가 쓴 코멘트는 접지 않는다 — 읽는 사람이 제일 먼저 봐야 하는 내용이다.\n\nimport type {\n AppInfo,\n ContextUser,\n DeviceInfo,\n DisplayInfo,\n ElementInfo,\n FeedbackKind,\n FeedbackPriority,\n FeedbackReport,\n SourceMapping,\n WebContext,\n} from \"../types.js\";\nimport { sourceFromElement } from \"../source-attr.js\";\n\n/** 접힌 블록의 여는 태그. 테스트와 어댑터가 같은 문자열을 보게 상수로 둔다. */\nexport const DETAILS_OPEN = \"<details>\";\n\n/** 접힌 블록의 요약줄. 대상에서 이 문구만 보이고 나머지는 접힌다. */\nexport const DETAILS_SUMMARY = \"<summary>진단 정보</summary>\";\n\n/** 접힌 블록의 닫는 태그. */\nexport const DETAILS_CLOSE = \"</details>\";\n\n/** 제목 최대 길이. 대상마다 제한이 다르지만, 한 줄로 읽히는 길이가 실질 상한이다. */\nexport const TITLE_MAX_CHARS = 80;\n\nconst KIND_LABEL: Record<FeedbackKind, string> = {\n report: \"제보\",\n annotation: \"요소 주석\",\n};\n\nconst PRIORITY_LABEL: Record<FeedbackPriority, string> = {\n unset: \"미지정\",\n normal: \"보통\",\n high: \"높음\",\n urgent: \"긴급\",\n};\n\n/**\n * 접힌 블록 **안에** 들어가는 자유 텍스트를 안전하게 만든다.\n *\n * `<` 만 막는 이유: 로그 메시지나 요소 텍스트에 `</details>` 가 들어 있으면 그 지점에서\n * 블록이 닫혀 뒤쪽 진단이 통째로 펼쳐진다(제보 하나 = 접힌 블록 하나 규칙이 깨진다).\n * `&` 까지 이스케이프하면 URL 쿼리(`?a=1&b=2`)가 읽기 어려워지는데, `&` 만으로는\n * 블록이 깨지지 않으므로 그대로 둔다.\n *\n * 줄바꿈은 공백으로 접는다. 목록 한 줄이 여러 줄로 쪼개지면 표가 무너진다.\n */\nfunction inline(value: string): string {\n return value.replace(/</g, \"&lt;\").replace(/[\\r\\n\\t]+/g, \" \").trim();\n}\n\n/** `- 라벨: 값` 한 줄. 값이 비면 줄 자체를 만들지 않는다(빈 항목으로 지면을 채우지 않는다). */\nfunction kv(label: string, value: string | null | undefined): string | null {\n if (value === null || value === undefined) return null;\n const text = inline(value);\n return text.length > 0 ? `- ${label}: ${text}` : null;\n}\n\n/** 객체를 한 줄 JSON 으로. 접힌 블록 안이라 이스케이프까지 거친다. */\nfunction json(value: unknown): string | null {\n try {\n const text = JSON.stringify(value);\n return text === undefined || text === \"{}\" ? null : text;\n } catch {\n // 순환 참조 등. 제보 전체를 실패시킬 이유는 아니다.\n return \"(직렬화 불가)\";\n }\n}\n\n/** base64 길이에서 원본 바이트 수를 되돌린다(패딩 제외). */\nfunction approxBytesOfBase64(b64: string): number {\n const padding = b64.endsWith(\"==\") ? 2 : b64.endsWith(\"=\") ? 1 : 0;\n return Math.max(0, Math.floor((b64.length * 3) / 4) - padding);\n}\n\nfunction humanBytes(n: number): string {\n return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;\n}\n\n/** `src/pages/Home.tsx:42` 형태. SourceMapping 에 칼럼이 없으므로 파일:줄까지다. */\nfunction formatSource(s: SourceMapping): string | null {\n const file = s.sourceFile;\n const line = typeof s.sourceLine === \"number\" ? s.sourceLine : null;\n const filePart = file ? (line === null ? file : `${file}:${line}`) : null;\n if (filePart && s.screenId) return `${filePart} (화면 ${s.screenId})`;\n if (filePart) return filePart;\n return s.screenId ? `화면 ${s.screenId}` : null;\n}\n\nfunction formatUser(u: ContextUser | null): string | null {\n if (!u) return null;\n const bits: string[] = [u.id ?? \"(미상)\"];\n if (u.name) bits.push(u.name);\n if (u.email) bits.push(u.email);\n if (u.role) bits.push(`역할 ${u.role}`);\n if (u.isGuest) bits.push(\"게스트\");\n return bits.join(\" · \");\n}\n\n/** `button#save.primary` 형태의 셀렉터. 읽는 사람이 요소를 특정할 수 있으면 충분하다. */\nfunction elementSelector(el: ElementInfo): string {\n if (el.selector) return el.selector;\n let out = el.tag;\n if (el.id) out += `#${el.id}`;\n if (el.className) {\n for (const cls of el.className.split(/\\s+/)) {\n if (cls) out += `.${cls}`;\n }\n }\n return out;\n}\n\nfunction formatAppInfo(a: AppInfo): string | null {\n const bits: string[] = [];\n if (a.version) bits.push(a.version);\n if (a.channel) bits.push(`채널 ${a.channel}`);\n if (a.runtimeVersion) bits.push(`런타임 ${a.runtimeVersion}`);\n if (a.updateId) bits.push(`업데이트 ${a.updateId}`);\n return bits.length > 0 ? bits.join(\" · \") : null;\n}\n\nfunction formatDevice(d: DeviceInfo): string | null {\n const bits: string[] = [];\n if (d.model) bits.push(d.model);\n if (d.osName || d.osVersion) bits.push([d.osName, d.osVersion].filter(Boolean).join(\" \"));\n if (d.deviceType) bits.push(d.deviceType);\n return bits.length > 0 ? bits.join(\" · \") : null;\n}\n\nfunction formatDisplay(d: DisplayInfo): string | null {\n const bits: string[] = [];\n if (d.width !== null && d.height !== null) bits.push(`${d.width}×${d.height}`);\n if (d.pixelRatio !== null) bits.push(`@${d.pixelRatio}x`);\n if (d.fontScale !== null) bits.push(`글꼴 배율 ${d.fontScale}`);\n return bits.length > 0 ? bits.join(\" · \") : null;\n}\n\nfunction formatViewport(v: WebContext[\"viewport\"]): string | null {\n const bits: string[] = [];\n if (v.width !== null && v.height !== null) bits.push(`${v.width}×${v.height}`);\n if (v.devicePixelRatio !== null) bits.push(`@${v.devicePixelRatio}x`);\n return bits.length > 0 ? bits.join(\" · \") : null;\n}\n\n/** 접힌 블록 안의 목록. 값이 없는 항목은 줄이 아예 없다. */\nfunction detailLines(report: FeedbackReport): string[] {\n const c = report.context;\n const out: (string | null)[] = [];\n\n out.push(kv(\"종류\", KIND_LABEL[report.kind]));\n out.push(kv(\"우선순위\", PRIORITY_LABEL[report.priority]));\n out.push(kv(\"제출 ID\", report.clientSubmissionId));\n out.push(kv(\"작성 시각\", report.createdAt));\n\n out.push(kv(\"앱\", c.app));\n out.push(kv(\"플랫폼\", c.platform));\n out.push(kv(\"화면\", c.screen));\n out.push(kv(\"URL\", c.url));\n out.push(kv(\"소스\", formatSource(c.source)));\n out.push(kv(\"사용자\", formatUser(c.user)));\n out.push(kv(\"세션\", c.sessionId));\n out.push(\n kv(\"수집 시각\", c.timezone ? `${c.clientTimestamp} (${c.timezone})` : c.clientTimestamp)\n );\n\n if (report.element) {\n const el = report.element;\n out.push(kv(\"요소\", elementSelector(el)));\n out.push(kv(\"요소 CSS 경로\", el.selector ?? null));\n out.push(kv(\"요소 소스\", formatSource(sourceFromElement(el))));\n out.push(kv(\"요소 텍스트\", el.text));\n if (el.boundingBox) {\n const b = el.boundingBox;\n out.push(\n kv(\n \"요소 위치\",\n `x ${Math.round(b.x)} · y ${Math.round(b.y)} · ${Math.round(b.width)}×${Math.round(\n b.height\n )}`\n )\n );\n }\n out.push(kv(\"요소 속성\", json(el.attributes)));\n }\n\n if (report.pin) {\n // 정규화 좌표(0~1)라 그대로 쓰면 0.123456789 같은 값이 나온다. 세 자리면 충분하다.\n out.push(kv(\"핀\", `x ${report.pin.x.toFixed(3)} · y ${report.pin.y.toFixed(3)} (0~1 정규화)`));\n }\n\n if (report.screenshot) {\n const bytes = approxBytesOfBase64(report.screenshot.base64);\n out.push(kv(\"스크린샷\", `${report.screenshot.contentType} · 약 ${humanBytes(bytes)}`));\n }\n\n if (c.native) {\n const n = c.native;\n out.push(kv(\"화면 경로\", n.screenPath));\n out.push(kv(\"내비 스택\", n.navPath.length > 0 ? n.navPath.join(\" › \") : null));\n out.push(kv(\"라우트 파라미터\", n.routeParams ? json(n.routeParams) : null));\n out.push(kv(\"앱 버전\", formatAppInfo(n.appInfo)));\n out.push(kv(\"기기\", formatDevice(n.device)));\n out.push(kv(\"디스플레이\", formatDisplay(n.display)));\n }\n\n if (c.web) {\n out.push(kv(\"뷰포트\", formatViewport(c.web.viewport)));\n out.push(kv(\"UA\", c.web.userAgent));\n }\n\n const extra = json(c.extra);\n out.push(kv(\"추가\", extra));\n\n // 진단은 \"수집 안 함(null)\"과 \"수집했는데 0건([])\"을 구분해서 적는다.\n // 둘을 똑같이 적으면 읽는 사람이 버그인지 설정인지 판단할 수 없다.\n if (c.diagnostics === null) {\n out.push(\"- 진단: 수집하지 않음\");\n } else {\n const { network, logs } = c.diagnostics;\n out.push(`- 진단 네트워크 (${network.length}건)`);\n for (const e of network) {\n const status = e.status === null ? \"실패\" : String(e.status);\n out.push(` - ${inline(`${e.method} ${e.url}`)} → ${status} · ${e.durationMs}ms · ${e.at}`);\n }\n out.push(`- 진단 로그 (${logs.length}건)`);\n for (const e of logs) {\n out.push(` - [${e.level}] ${inline(e.message)} · ${e.at}`);\n }\n }\n\n return out.filter((line): line is string => line !== null);\n}\n\n/**\n * 제보 하나를 사람이 읽는 본문으로 만든다.\n *\n * 형태: `사용자 코멘트` + 빈 줄 + `접힌 진단 블록 하나`.\n * 코멘트는 **이스케이프하지 않는다** — 접힌 블록보다 앞에 있어서 우리 블록을 깨뜨릴 수 없고,\n * 사람이 쓴 문장을 우리가 변형하는 쪽이 더 나쁘다.\n */\nexport function renderReportBody(report: FeedbackReport): string {\n const comment = report.comment.trim().length > 0 ? report.comment.trimEnd() : \"(코멘트 없음)\";\n return [\n comment,\n \"\",\n DETAILS_OPEN,\n DETAILS_SUMMARY,\n \"\",\n ...detailLines(report),\n \"\",\n DETAILS_CLOSE,\n ].join(\"\\n\");\n}\n\n/**\n * 제목. 코멘트 첫 줄을 쓰되, 비어 있으면 종류와 화면으로 대신한다.\n * 제목이 비면 대상에서 목록이 읽히지 않으므로 항상 비어 있지 않은 문자열을 돌려준다.\n */\nexport function buildTitle(report: FeedbackReport): string {\n const firstLine = report.comment.split(\"\\n\").find((l) => l.trim().length > 0);\n const fallback = `${KIND_LABEL[report.kind]} — ${\n report.context.screen ?? report.context.url ?? report.context.app\n }`;\n const base = inline(firstLine ?? fallback) || fallback;\n return base.length > TITLE_MAX_CHARS ? `${base.slice(0, TITLE_MAX_CHARS - 1)}…` : base;\n}\n","// Linear 어댑터 — **인터페이스만 열어둔다. 전송은 구현하지 않는다.**\n//\n// 왜 껍데기를 미리 두는가:\n// 어댑터 교체가 실제로 가능한지는 두 번째 대상을 만들어봐야 드러난다. 여기서 확인하는 건\n// \"라쏘런에만 있는 모양을 코어가 몰래 전제하고 있지 않은가\"이고, 그건 전송 없이\n// **payload 를 조립하는 것만으로** 확인된다(DP-255 TC4·TC5).\n//\n// 전송을 안 쓰는 이유는 게을러서가 아니다. Linear 로 실제로 보내려면 팀 ID·상태 ID·\n// API 키 같은 워크스페이스별 값이 필요한데, 그건 이 wave 에 명세가 없다. 없는 명세를\n// 상상해서 만들어두면 나중에 전부 지워야 한다. 그래서 **모양만** 맞춰둔다.\n\nimport type { FeedbackReport } from \"../types.js\";\nimport { buildTitle, renderReportBody } from \"./body.js\";\n\n/**\n * Linear 이슈 생성 입력의 최소 형태.\n *\n * 필드명이 라쏘런 봉투와 다르다는 게 핵심이다 — 같은 제보라도 대상마다 이름이 다르고,\n * 코어는 그 어느 쪽도 모른다.\n */\nexport interface LinearIssueInput {\n /** 이슈 제목. 목록에서 한 줄로 보인다. */\n title: string;\n /** 이슈 본문(마크다운). 대상이 못 받는 값은 전부 여기 접힌 블록으로 들어간다. */\n description: string;\n}\n\n/**\n * 제보를 Linear 이슈 입력으로 바꾼다.\n *\n * 본문은 공용 렌더러가 만든 것을 **그대로** 쓴다. 여기서 다시 조립하면\n * \"Linear 로 간 제보에는 소스 경로가 없다\" 같은 차이가 조용히 생긴다.\n */\nexport function buildLinearIssue(report: FeedbackReport): LinearIssueInput {\n return {\n title: buildTitle(report),\n description: renderReportBody(report),\n };\n}\n","// Notion 어댑터 — **인터페이스만 열어둔다. 전송은 구현하지 않는다.**\n//\n// linear.ts 와 같은 이유로 payload 조립까지만 둔다. Notion 은 본문을 블록 배열로 받는\n// 대상이라 \"마크다운 문자열 하나\"와 모양이 가장 많이 다른데, 그래서 오히려 좋은 대조군이다\n// — 이 대상까지 같은 렌더러로 채워진다면 본문 구성이 특정 대상에 묶여 있지 않다는 뜻이다.\n\nimport type { FeedbackReport } from \"../types.js\";\nimport { buildTitle, renderReportBody } from \"./body.js\";\n\n/** Notion 페이지 생성 입력의 최소 형태. */\nexport interface NotionPageInput {\n /** 페이지 제목 속성. */\n title: string;\n /**\n * 페이지 본문(마크다운 원문).\n *\n * 블록 배열로 쪼개지 않는 이유: 쪼개는 규칙(어디서 문단을 나눌지, 접힌 블록을\n * toggle 로 옮길지)은 실제 Notion API 계약이 정해진 뒤에야 의미가 있다.\n * 지금 추측으로 쪼개두면 본문 내용이 대상마다 갈라지고, 그게 이 wave 가 막으려는 것이다.\n */\n content: string;\n}\n\n/** 제보를 Notion 페이지 입력으로 바꾼다. 본문은 공용 렌더러 결과 그대로다. */\nexport function buildNotionPage(report: FeedbackReport): NotionPageInput {\n return {\n title: buildTitle(report),\n content: renderReportBody(report),\n };\n}\n","// 라쏘런(doc-platform) 어댑터 — 기본 전송 대상.\n//\n// 이 파일이 지는 책임과 지지 않는 책임:\n// - 진다: 제보를 라쏘런이 받는 모양으로 **바꿔서** 보내고, 결과를 SubmitResult 로 **돌려준다.**\n// - 안 진다: 재시도·백오프·큐 적재. 그건 코어(queue.ts)가 한다. 여기서 재시도하면\n// 같은 제보가 두 층에서 각자 재시도돼 서버가 중복을 받는다.\n//\n// 절대 규칙 두 가지:\n// 1. **던지지 않는다.** 네트워크 오류든 파싱 실패든 SubmitResult 로 바꿔 돌려준다.\n// (큐가 예외를 흡수하긴 하지만, 그때는 전부 \"일시 실패\"로 뭉뚱그려져 4xx 가 영원히 재시도된다.)\n// 2. **본문·헤더를 로그에 남기지 않는다.** 경고에는 상태 코드와 사유만 적는다.\n// `Authorization` 헤더와 응답 본문은 어떤 경로로도 콘솔에 닿지 않는다.\n\nimport { byteLengthOf, canUseKeepalive } from \"../keepalive.js\";\nimport type { FeedbackAdapter, FeedbackReport, SubmitResult } from \"../types.js\";\n// 라쏘런은 headline 필드가 없다(comment 하나로 받는다) → buildTitle 을 쓰지 않는다.\nimport { renderReportBody } from \"./body.js\";\n\n/**\n * 엔드포인트 기본값. 코어가 아니라 **어댑터**가 갖는다(config.ts 의 ENDPOINT_FROM_ADAPTER).\n *\n * 상대 경로인 이유: 웹은 같은 오리진의 수집 라우트로 보내는 게 기본이고, 그래야\n * Origin 허용 목록이라는 실제 방어선이 그대로 작동한다. 앱(RN)에는 오리진이 없으므로\n * `endpoint` 를 절대 URL 로 지정해야 한다 — 지정 없이 앱에서 쓰면 아래에서 설정 오류로 끊는다.\n */\nexport const LASSO_DEFAULT_ENDPOINT = \"/api/feedback\";\n\n/** 최소한의 응답 형태. 코어는 DOM 타입(lib)에 의존하지 않으므로 구조적으로 정의한다. */\nexport interface LassoResponseLike {\n status: number;\n headers?: { get(name: string): string | null } | undefined;\n text(): Promise<string>;\n}\n\n/** 최소한의 요청 형태. 표준 fetch 의 부분집합이라 그대로 꽂힌다. */\nexport interface LassoRequestInit {\n method: string;\n headers: Record<string, string>;\n body: string;\n keepalive?: boolean;\n}\n\nexport type LassoFetch = (url: string, init: LassoRequestInit) => Promise<LassoResponseLike>;\n\nexport interface LassoAdapterOpts {\n /** 수집 토큰. 없으면 `Authorization` 헤더 자체를 붙이지 않는다(미귀속 전송). */\n token?: string | null;\n /** 수집 엔드포인트. 없으면 LASSO_DEFAULT_ENDPOINT. */\n endpoint?: string | null;\n /** 전송 함수 주입(테스트용). 없으면 globalThis.fetch. */\n fetch?: LassoFetch | null;\n /** 경고 출력 주입. 위젯은 캡처를 우회하는 원본 console 을 넘긴다. */\n warn?: ((message: string, ...rest: unknown[]) => void) | null;\n}\n\n/**\n * 라쏘런으로 보내는 봉투.\n *\n * 필드명이 코어의 FeedbackReport 와 일부러 다르다. 코어는 이 이름들을 **모른다** —\n * 대상마다 받는 모양이 다르다는 게 어댑터를 두는 이유 자체다(DP-255 TC5).\n */\n/**\n * 라쏘런 `dp-agentation-ingest` 가 실제로 요구하는 봉투.\n *\n * 이 모양은 **상상하지 않고 실호출로 확정했다.** 이전 버전은 필드명을 지어내서\n * (submissionKey/bodyMarkdown/severity …) 실 엔드포인트에 100% 400 으로 거절당했고,\n * 400 은 재시도 불가라 모든 제보가 조용히 데드레터로 사라졌다.\n * 계약을 바꿀 때는 반드시 실호출로 다시 확인한다(adapters.live.test.ts).\n */\nexport interface LassoEnvelope {\n /** UUID. **Idempotency-Key 헤더와 반드시 같은 값**이어야 서버가 받는다. */\n clientSubmissionId: string;\n /**\n * 수집 소스의 종류와 같아야 한다(서버가 대조한다). 코어의 platform 을 옮긴 값이다:\n * web → \"web\", native → \"app\". 앱은 report.create 만 보낼 수 있다(요소 지목은 DOM 이 필요).\n */\n clientType: \"web\" | \"app\";\n /** 지목 주석은 annotation.add, 리포트 모달 제출은 report.create. */\n event: \"annotation.add\" | \"report.create\";\n /** epoch ms 정수. ISO 문자열을 보내면 거절된다. */\n timestamp: number;\n /** 절대 HTTP(S) URL. 소스의 host_patterns 에 등록된 origin 이어야 한다. */\n url: string;\n priority: \"unset\" | \"normal\" | \"high\" | \"urgent\";\n annotation?: {\n id: string;\n comment: string;\n x: number | null;\n y: number | null;\n element: string | null;\n elementPath: string | null;\n selectedText: string | null;\n boundingBox: { x: number; y: number; width: number; height: number } | null;\n timestamp: number;\n };\n report?: {\n comment: string;\n pin: { x: number; y: number } | null;\n sourceFile: string | null;\n screenshotBase64: string | null;\n screenshotContentType: \"image/jpeg\" | \"image/png\" | null;\n network: unknown[];\n logs: unknown[];\n };\n}\n\n\nconst RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\n\nfunction resolveFetch(injected: LassoFetch | null | undefined): LassoFetch | null {\n if (injected) return injected;\n const g = globalThis as { fetch?: unknown };\n return typeof g.fetch === \"function\" ? (g.fetch as LassoFetch) : null;\n}\n\n/**\n * 상대 경로를 절대 URL 로 만든다. 오리진을 못 찾으면 null(= 설정 오류).\n *\n * 왜 굳이 절대 URL 로 바꾸나: 진단 수집기가 \"위젯 자신의 요청\"을 제외할 때 URL 로 매칭하는데,\n * 상대/절대가 섞이면 그 매칭이 헐거워진다. 어느 층에서 보든 같은 문자열이 되게 맞춘다.\n */\nfunction absoluteEndpoint(endpoint: string): string | null {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(endpoint)) return endpoint;\n const origin = (globalThis as { location?: { origin?: string } }).location?.origin;\n if (typeof origin !== \"string\" || origin.length === 0) return null;\n return endpoint.startsWith(\"/\") ? `${origin}${endpoint}` : `${origin}/${endpoint}`;\n}\n\n/** `Retry-After: 30`(초)만 해석한다. HTTP-date 형식은 코어의 기본 백오프에 맡긴다. */\nfunction parseRetryAfterMs(res: LassoResponseLike): number | null {\n const raw = res.headers?.get(\"Retry-After\") ?? null;\n if (raw === null) return null;\n const seconds = Number(raw.trim());\n return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1000) : null;\n}\n\nfunction fail(retryable: boolean, extra?: Partial<SubmitResult>): SubmitResult {\n return { ok: false, id: null, retryable, retryAfterMs: null, ...extra };\n}\n\n/** 응답 본문에서 서버가 준 id 를 꺼낸다. 없으면 null(성공 판정과는 무관하다). */\nfunction idFrom(parsed: Record<string, unknown>): string | null {\n // 라쏘런은 `annotationId` 로 준다(실호출로 확인). `id`/`data.id` 는 다른 대상 대비 폴백.\n const annotationId = parsed.annotationId;\n if (typeof annotationId === \"string\" && annotationId.length > 0) return annotationId;\n const direct = parsed.id;\n if (typeof direct === \"string\" && direct.length > 0) return direct;\n const data = parsed.data;\n if (data && typeof data === \"object\") {\n const nested = (data as Record<string, unknown>).id;\n if (typeof nested === \"string\" && nested.length > 0) return nested;\n }\n return null;\n}\n\n/** 제보를 라쏘런 봉투로 바꾼다. 순수 함수라 테스트가 전송 없이 모양만 볼 수 있다. */\nexport function buildLassoEnvelope(report: FeedbackReport): LassoEnvelope {\n const c = report.context;\n const clientType: \"web\" | \"app\" = c.platform === \"native\" ? \"app\" : \"web\";\n // 서버가 epoch ms 정수만 받는다. createdAt(ISO)이 깨져 있어도 전송을 막지 않는다.\n const parsed = Date.parse(report.createdAt);\n const timestamp = Number.isFinite(parsed) ? parsed : 0;\n const el = report.element;\n\n // 요소를 지목한 제보는 annotation.add 로, 모달 제출은 report.create 로 간다.\n // 대상이 둘을 같은 라우트에서 event 로 가른다.\n if (report.kind === \"annotation\") {\n return {\n clientSubmissionId: report.clientSubmissionId,\n clientType,\n event: \"annotation.add\",\n timestamp,\n url: c.url ?? \"\",\n priority: report.priority,\n annotation: {\n id: report.clientSubmissionId,\n comment: renderReportBody(report),\n x: report.pin?.x ?? null,\n y: report.pin?.y ?? null,\n element: el ? el.tag : null,\n elementPath: el?.attributes[\"data-feedback-source\"] ?? null,\n selectedText: el?.text ?? null,\n boundingBox: el?.boundingBox ?? null,\n timestamp,\n },\n };\n }\n\n return {\n clientSubmissionId: report.clientSubmissionId,\n clientType,\n event: \"report.create\",\n timestamp,\n // 앱에는 URL 이 없다 — 화면 이름을 app:// 경로로 싣는다(서버가 그대로 저장한다).\n url: c.url ?? (clientType === \"app\" ? `app://${c.screen ?? \"unknown\"}` : \"\"),\n priority: report.priority,\n report: {\n comment: renderReportBody(report),\n pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,\n sourceFile: el?.attributes[\"data-feedback-source\"] ?? null,\n screenshotBase64: report.screenshot?.base64 ?? null,\n screenshotContentType: report.screenshot?.contentType ?? null,\n // 서버가 없으면 빈 배열로 취급하지만, 미연동과 \"없었음\"을 구분해 보낸다.\n network: report.context.diagnostics?.network ?? [],\n logs: report.context.diagnostics?.logs ?? [],\n },\n };\n}\n\n/**\n * 라쏘런 어댑터를 만든다.\n *\n * 실패 분류(코어가 이 값만 보고 재시도 여부를 정한다):\n * - 429 → 재시도 가능 + rateLimited. 코어가 최소 10분을 띄운다.\n * - 408/425/5xx/네트워크 오류 → 재시도 가능(일시적).\n * - 그 밖의 4xx → **재시도 불가.** 400/401/413 은 다시 보내도 같은 답이라 큐만 막는다.\n * - 2xx 인데 본문의 `ok` 가 true 가 아님 → 재시도 불가. 스키마 거절은 저절로 낫지 않는다.\n * - 2xx 인데 본문을 못 읽음 → **재시도 가능.** 프록시·CDN 이 HTML 을 끼워 넣는 일이 실제로 있고,\n * 멱등 키가 있어 다시 보내도 중복이 생기지 않는다.\n */\nexport function createLassoAdapter(opts: LassoAdapterOpts = {}): FeedbackAdapter {\n const token = opts.token ?? null;\n const endpoint = opts.endpoint && opts.endpoint.trim().length > 0\n ? opts.endpoint.trim()\n : LASSO_DEFAULT_ENDPOINT;\n const warn = opts.warn ?? null;\n\n const emit = (message: string): void => {\n if (!warn) return;\n try {\n warn(message);\n } catch {\n // 로깅 실패가 전송 실패로 번지면 안 된다.\n }\n };\n\n return {\n async submit(report: FeedbackReport): Promise<SubmitResult> {\n const url = absoluteEndpoint(endpoint);\n if (url === null) {\n // 앱에서 상대 경로를 그대로 둔 설정 오류. 재시도해도 절대 낫지 않으므로\n // 재시도 불가로 끊는다(큐가 데드레터로 보내고 뒤 제보를 막지 않는다).\n emit(\n `feedback-kit: endpoint \"${endpoint}\" 를 절대 URL 로 만들 수 없다(오리진 없음). ` +\n \"앱에서는 endpoint 를 절대 URL 로 지정해야 한다.\"\n );\n return fail(false);\n }\n\n const sender = resolveFetch(opts.fetch);\n if (sender === null) {\n // fetch 가 없는 런타임. 폴리필이 늦게 들어올 수 있으니 일시 실패로 둔다.\n emit(\"feedback-kit: fetch 를 찾지 못했다(전송을 미룬다).\");\n return fail(true);\n }\n\n const body = JSON.stringify(buildLassoEnvelope(report));\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n // 재시도가 중복 제보가 되지 않게. 코어가 같은 항목을 다시 보내도 키는 그대로다.\n \"Idempotency-Key\": report.clientSubmissionId,\n };\n if (token !== null) headers.Authorization = `Bearer ${token}`;\n\n let res: LassoResponseLike;\n try {\n res = await sender(url, {\n method: \"POST\",\n headers,\n body,\n // 페이지를 즉시 이탈해도 전송이 끝나게. 본문이 한도를 넘으면 브라우저가\n // 요청 자체를 거절하므로 크기를 보고 켠다.\n keepalive: canUseKeepalive(byteLengthOf(body)),\n });\n } catch {\n // 오프라인·DNS·CORS 등. 예외 객체에 요청 본문이 들어있을 수 있어 로그에 싣지 않는다.\n emit(\"feedback-kit: 전송에 실패했다(네트워크). 큐에 남겨 다시 시도한다.\");\n return fail(true);\n }\n\n const status = res.status;\n\n if (status < 200 || status >= 300) {\n if (status === 429) {\n emit(\"feedback-kit: 수집 한도에 걸렸다(429). 잠시 뒤 다시 시도한다.\");\n return fail(true, { retryAfterMs: parseRetryAfterMs(res), rateLimited: true });\n }\n const retryable = RETRYABLE_STATUS.has(status);\n emit(\n `feedback-kit: 수집 응답 ${status}${retryable ? \" (다시 시도한다)\" : \" (재시도하지 않는다)\"}`\n );\n return fail(retryable, { retryAfterMs: retryable ? parseRetryAfterMs(res) : null });\n }\n\n let parsed: Record<string, unknown>;\n try {\n const text = await res.text();\n const value: unknown = JSON.parse(text);\n if (value === null || typeof value !== \"object\") throw new Error(\"객체가 아님\");\n parsed = value as Record<string, unknown>;\n } catch {\n emit(`feedback-kit: 수집 응답 ${status} 의 본문을 읽지 못했다. 다시 시도한다.`);\n return fail(true);\n }\n\n // 2xx 만으로 성공을 단정하지 않는다. 서버가 200 + ok:false 로 거절할 수 있고,\n // 그걸 성공으로 읽으면 제보가 큐에서 지워진 채 어디에도 남지 않는다.\n if (parsed.ok !== true) {\n emit(`feedback-kit: 수집 서버가 제보를 거절했다(${status}). 재시도하지 않는다.`);\n return fail(false);\n }\n\n return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };\n },\n };\n}\n"],"mappings":"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","names":["json"]}