@solhun/feedback-kit-core 0.2.3 → 0.4.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.
package/dist/index.d.cts CHANGED
@@ -891,6 +891,18 @@ declare const SUBMIT_FAILED_MESSAGE = "\uBCF4\uB0B4\uC9C0 \uBABB\uD588\uC2B5\uB2
891
891
  type WidgetPlatform = "web" | "native";
892
892
  type ScreenshotStatus = "none" | "capturing" | "ready" | "failed";
893
893
  type ModalSubmitStatus = "idle" | "sending" | "sent" | "pending" | "failed";
894
+ /**
895
+ * 핀이 **무엇을 기준으로** 찍힌 좌표인가. 이미지가 바뀔 때 핀을 버릴지 말지가 여기서 갈린다.
896
+ *
897
+ * - `picture`: 스크린샷 **그림 위**를 탭해서 찍은 좌표(앱 핀 화면). 그림이 바뀌면 가리키는
898
+ * 대상이 달라지므로 **버려야 한다.**
899
+ * - `viewport`: **페이지 화면**을 클릭해서 나온 좌표(웹 요소 지목). 같은 뷰포트를 찍은
900
+ * 스크린샷이면 어느 장이든 같은 지점을 가리키므로 **살려야 한다.**
901
+ *
902
+ * 이 구분이 없던 동안 지목 좌표가 전부 유실됐다: 클릭 직후 `setPin` 으로 들어온 좌표를
903
+ * 수백 ms 뒤 도착한 자동 캡처가 `pin: null` 로 덮었다(실측 — 서버 `x`/`y` 가 전부 null).
904
+ */
905
+ type PinAnchor = "picture" | "viewport";
894
906
  /** 모달 안에서 포커스를 받는 요소들의 고정 id. 렌더러가 그대로 매단다. */
895
907
  declare const MODAL_FIELD_COMMENT = "comment";
896
908
  declare const MODAL_FIELD_PRIORITY = "priority";
@@ -970,6 +982,8 @@ declare class ReportModalController {
970
982
  private lastReport;
971
983
  /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */
972
984
  private captureGeneration;
985
+ /** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
986
+ private pinAnchor;
973
987
  private state;
974
988
  constructor(opts: ReportModalOpts);
975
989
  getState(): ReportModalState;
@@ -979,8 +993,14 @@ declare class ReportModalController {
979
993
  dispose(): void;
980
994
  /** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */
981
995
  stopQueue(): void;
996
+ /**
997
+ * @param opts.pin 요소를 지목해서 열 때의 **클릭 지점**(뷰포트 기준 0~1).
998
+ * 여기로 받아야 뒤늦게 끝나는 자동 캡처가 좌표를 지우지 않는다 — `setPin` 으로
999
+ * 따로 넣으면 캡처와 경쟁해서 진다(그 경쟁에 계속 져서 좌표가 전부 유실됐다).
1000
+ */
982
1001
  open(opts?: {
983
1002
  element?: ElementInfo | null;
1003
+ pin?: FeedbackPin | null;
984
1004
  }): Promise<void>;
985
1005
  /**
986
1006
  * 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.
@@ -997,8 +1017,12 @@ declare class ReportModalController {
997
1017
  /** 상한을 넘겨도 값을 자르지 않는다. 거부는 하되 사용자가 쓴 글은 보존한다. */
998
1018
  setComment(value: string): void;
999
1019
  setPriority(priority: FeedbackPriority): void;
1000
- setPin(pin: FeedbackPin | null): void;
1001
- /** 스크린샷 제거 핀은 스크린샷좌표라 함께 사라진다. */
1020
+ /**
1021
+ * @param anchor 좌표의 기준. 기본은 `picture`(그림 탭) 핀 화면이 쓰는 값이다.
1022
+ * 웹에서 페이지를 클릭해 얻은 좌표는 `viewport` 로 넣어야 캡처에 지워지지 않는다.
1023
+ */
1024
+ setPin(pin: FeedbackPin | null, anchor?: PinAnchor): void;
1025
+ /** 스크린샷 제거. 그림 위에 찍은 핀은 가리킬 대상이 사라지므로 함께 버린다. */
1002
1026
  removeScreenshot(): void;
1003
1027
  recapture(): Promise<void>;
1004
1028
  /** 캡처가 실패했을 때의 대체 경로 — 사용자가 직접 고른 파일. */
@@ -1062,9 +1086,12 @@ declare class WidgetController {
1062
1086
  * @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
1063
1087
  * `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
1064
1088
  * `picking` 에 머물러 **모달이 렌더되지 않는다**.
1089
+ * @param opts.pin 지목한 클릭 지점. 열기 인자로 넘겨야 자동 캡처가 좌표를 지우지 않는다
1090
+ * (열고 난 뒤 `modal.setPin` 으로 넣으면 캡처와 경쟁한다).
1065
1091
  */
1066
1092
  openReport(opts?: {
1067
1093
  element?: ElementInfo | null;
1094
+ pin?: FeedbackPin | null;
1068
1095
  }): Promise<void>;
1069
1096
  /** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
1070
1097
  closeReport(): "closed" | "confirm";
@@ -1194,6 +1221,55 @@ interface LassoAdapterOpts {
1194
1221
  * 400 은 재시도 불가라 모든 제보가 조용히 데드레터로 사라졌다.
1195
1222
  * 계약을 바꿀 때는 반드시 실호출로 다시 확인한다(adapters.live.test.ts).
1196
1223
  */
1224
+ /**
1225
+ * 구조화 칸이 따로 없는 값들을 담는 자리 → 서버가 `meta` 로 옮긴다.
1226
+ *
1227
+ * 왜 생겼나: 예전에는 이 값들을 **사람이 읽는 마크다운으로 접어 `comment` 에 넣었다.**
1228
+ * 그런데 라쏘런의 `comment` 는 목록·상세의 **제목 자리**다. 5자짜리 코멘트 하나가
1229
+ * 1,399자 덩어리가 되어 화면에 쏟아졌다(실측). 라쏘런은 구조화 칸이 있는 대상이니
1230
+ * 접어 넣을 이유가 없다 — 대상별로 담는 방식이 다른 게 어댑터를 두는 이유다.
1231
+ *
1232
+ * 이름은 **뷰어가 이미 읽는 키를 그대로** 쓴다(`user`·`device`·`appInfo`·`navPath` …).
1233
+ * 새 이름을 지으면 같은 값이 화면에서 두 군데로 갈린다.
1234
+ */
1235
+ interface LassoReportContext {
1236
+ /** 제보 종류(`report`=화면 전체, `annotation`=요소 지목). 서버는 요소 유무로도 가른다. */
1237
+ kind: string;
1238
+ app: string;
1239
+ platform: string;
1240
+ sessionId: string;
1241
+ clientTimestamp: string;
1242
+ timezone: string | null;
1243
+ user: {
1244
+ id: string | null;
1245
+ name: string | null;
1246
+ email: string | null;
1247
+ role: string | null;
1248
+ isGuest: boolean;
1249
+ } | null;
1250
+ /** 빌드 플러그인이 심은 화면·소스 매핑. 없으면 null. */
1251
+ source: {
1252
+ screenId: string | null;
1253
+ sourceFile: string | null;
1254
+ sourceLine: number | null;
1255
+ } | null;
1256
+ /** 지목한 요소의 나머지 정보(id·속성). 태그·선택자·텍스트는 report.element 에 있다. */
1257
+ element: {
1258
+ id: string | null;
1259
+ attributes: Record<string, string>;
1260
+ } | null;
1261
+ /** 앱 전용. 뷰어의 구현 정보 칸(기기·앱 버전·OTA·탐색 경로)이 이걸 읽는다. */
1262
+ native: {
1263
+ screenPath: string | null;
1264
+ navPath: string[];
1265
+ routeParams: Record<string, unknown> | null;
1266
+ appInfo: Record<string, unknown>;
1267
+ device: Record<string, unknown>;
1268
+ display: Record<string, unknown>;
1269
+ } | null;
1270
+ /** 호스트가 넣은 자유 메타. 우리가 해석하지 않는다. */
1271
+ extra: Record<string, unknown>;
1272
+ }
1197
1273
  interface LassoEnvelope {
1198
1274
  /** UUID. **Idempotency-Key 헤더와 반드시 같은 값**이어야 서버가 받는다. */
1199
1275
  clientSubmissionId: string;
@@ -1226,7 +1302,16 @@ interface LassoEnvelope {
1226
1302
  timestamp: number;
1227
1303
  };
1228
1304
  report?: {
1305
+ /**
1306
+ * **사용자가 쓴 문장 그대로.** 진단을 여기에 접어 넣지 않는다 — 이 값이 곧 제목이다.
1307
+ * 나머지는 구조화 칸(`web`·`network`·`logs`·`element`)과 `context` 로 간다.
1308
+ */
1229
1309
  comment: string;
1310
+ /**
1311
+ * 좌표. **0~100 백분율**이다(코어의 0~1 정규화를 여기서 옮긴다).
1312
+ * 라쏘런의 `x`/`y` 컬럼과 뷰어 오버레이가 백분율을 전제한다 — 0~1 로 보내면
1313
+ * 뷰어의 뷰포트 역산이 "0.12%" 로 읽어 조용히 버리거나 터무니없는 값을 낸다.
1314
+ */
1230
1315
  pin: {
1231
1316
  x: number;
1232
1317
  y: number;
@@ -1269,8 +1354,12 @@ interface LassoEnvelope {
1269
1354
  screenshotContentType: "image/jpeg" | "image/png" | null;
1270
1355
  network: unknown[];
1271
1356
  logs: unknown[];
1357
+ /** 위 칸에 자리가 없는 값들. 서버가 `meta` 로 펼친다. */
1358
+ context: LassoReportContext;
1272
1359
  };
1273
1360
  }
1361
+ /** 구조화 칸에 자리가 없는 값들을 모은다. 비어 있어도 키는 남긴다(미수집과 없음을 구분). */
1362
+ declare function buildLassoContext(report: FeedbackReport): LassoReportContext;
1274
1363
  /** 제보를 라쏘런 봉투로 바꾼다. 순수 함수라 테스트가 전송 없이 모양만 볼 수 있다. */
1275
1364
  declare function buildLassoEnvelope(report: FeedbackReport): LassoEnvelope;
1276
1365
  /**
@@ -1305,4 +1394,4 @@ declare function guestUser(id: string): FeedbackUser;
1305
1394
  */
1306
1395
  declare function uuidv4(): string;
1307
1396
 
1308
- export { type AdapterName, type AppInfo, BACKOFF_BASE_MS, BACKOFF_MAX_MS, type BuildContextOpts, type BuildReportOpts, COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, type CaptureWithinLimitOpts, type ConfigWarning, type ConfigWarningCode, type ContextProviders, type ContextUser, DEFAULT_ADAPTER, DEFAULT_INTERNAL_ROLES, DEFAULT_POSITION, type DeviceInfo, type DiagLogEntry, type DiagNetworkEntry, type DiagRequestRef, DiagnosticsCollector, type DiagnosticsExcludeMatcher, type DiagnosticsInstallOpts, type DiagnosticsPayload, type DiagnosticsSource, type DisplayInfo, ENDPOINT_FROM_ADAPTER, type ElementInfo, FLOATING_BUTTON_ID, type FeedbackAdapter, type FeedbackConfig, type FeedbackContext, type FeedbackKind, type FeedbackPin, type FeedbackPriority, FeedbackQueue, type FeedbackQueueOpts, type FeedbackReport, type FeedbackScreenshot, type FeedbackStorage, type FeedbackUser, FocusRing, type GetCurrentScreenFn, type GetUserFn, KEEPALIVE_BODY_LIMIT_BYTES, LASSO_DEFAULT_ENDPOINT, LOG_BUFFER_LIMIT, type LassoAdapterOpts, type LassoEnvelope, type LassoFetch, type LassoRequestInit, type LassoResponseLike, type LinearIssueInput, MAX_LOG_MESSAGE_CHARS, MAX_QUEUE_AGE_MS, MAX_QUEUE_ITEMS, MODAL_ACTION_ATTACH, MODAL_ACTION_CANCEL, MODAL_ACTION_PICK, MODAL_ACTION_PIN, MODAL_ACTION_REMOVE_SCREENSHOT, MODAL_ACTION_RETRY, MODAL_ACTION_SEND, MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY, type ModalQueueLike, type ModalSubmitStatus, NETWORK_BUFFER_LIMIT, type NativeContext, type NotionPageInput, PinController, type PixelPoint, type PixelSize, type QueueStatus, type QueueStatusListener, RATE_LIMIT_MIN_DELAY_MS, REDACTED, ReportModalController, type ReportModalListener, type ReportModalOpts, type ReportModalState, type ReportParts, type ResolvedConfig, RingBuffer, SCREENSHOT_FAILED_MESSAGE, SCREENSHOT_MAX_BASE64_BYTES, SCREENSHOT_QUALITY_STEPS, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_FAILED_MESSAGE, SUBMIT_PENDING_MESSAGE, type ScreenshotCapture, type ScreenshotOutcome, type ScreenshotReencode, type ScreenshotStatus, type SourceLocation, type SourceMapping, type SubmitOutcome, type SubmitResult, TITLE_MAX_CHARS, type Visibility, type VisibilityEnv, type VisibilityFn, type WebContext, type WebContextInput, WidgetController, type WidgetControllerOpts, type WidgetCorner, type WidgetListener, type WidgetPlatform, type WidgetPosition, type WidgetScreen, type WidgetState, buildContext, buildLassoEnvelope, buildLinearIssue, buildNotionPage, buildReport, buildTitle, byteLengthOf, canUseKeepalive, captureWithinLimit, createLassoAdapter, defaultBackoff, denormalizePin, detectDevBuild, diagnosticsProviderFor, getOrCreateGuestId, guestUser, isInternalUser, normalizePin, normalizeUser, parseSourceAttr, renderReportBody, resolveConfig, resolveContextUser, sanitizeUrl, screenshotBytes, sharedDiagnostics, shouldShowWidget, sourceFromElement, uuidv4 };
1397
+ export { type AdapterName, type AppInfo, BACKOFF_BASE_MS, BACKOFF_MAX_MS, type BuildContextOpts, type BuildReportOpts, COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, type CaptureWithinLimitOpts, type ConfigWarning, type ConfigWarningCode, type ContextProviders, type ContextUser, DEFAULT_ADAPTER, DEFAULT_INTERNAL_ROLES, DEFAULT_POSITION, type DeviceInfo, type DiagLogEntry, type DiagNetworkEntry, type DiagRequestRef, DiagnosticsCollector, type DiagnosticsExcludeMatcher, type DiagnosticsInstallOpts, type DiagnosticsPayload, type DiagnosticsSource, type DisplayInfo, ENDPOINT_FROM_ADAPTER, type ElementInfo, FLOATING_BUTTON_ID, type FeedbackAdapter, type FeedbackConfig, type FeedbackContext, type FeedbackKind, type FeedbackPin, type FeedbackPriority, FeedbackQueue, type FeedbackQueueOpts, type FeedbackReport, type FeedbackScreenshot, type FeedbackStorage, type FeedbackUser, FocusRing, type GetCurrentScreenFn, type GetUserFn, KEEPALIVE_BODY_LIMIT_BYTES, LASSO_DEFAULT_ENDPOINT, LOG_BUFFER_LIMIT, type LassoAdapterOpts, type LassoEnvelope, type LassoFetch, type LassoReportContext, type LassoRequestInit, type LassoResponseLike, type LinearIssueInput, MAX_LOG_MESSAGE_CHARS, MAX_QUEUE_AGE_MS, MAX_QUEUE_ITEMS, MODAL_ACTION_ATTACH, MODAL_ACTION_CANCEL, MODAL_ACTION_PICK, MODAL_ACTION_PIN, MODAL_ACTION_REMOVE_SCREENSHOT, MODAL_ACTION_RETRY, MODAL_ACTION_SEND, MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY, type ModalQueueLike, type ModalSubmitStatus, NETWORK_BUFFER_LIMIT, type NativeContext, type NotionPageInput, PinController, type PixelPoint, type PixelSize, type QueueStatus, type QueueStatusListener, RATE_LIMIT_MIN_DELAY_MS, REDACTED, ReportModalController, type ReportModalListener, type ReportModalOpts, type ReportModalState, type ReportParts, type ResolvedConfig, RingBuffer, SCREENSHOT_FAILED_MESSAGE, SCREENSHOT_MAX_BASE64_BYTES, SCREENSHOT_QUALITY_STEPS, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_FAILED_MESSAGE, SUBMIT_PENDING_MESSAGE, type ScreenshotCapture, type ScreenshotOutcome, type ScreenshotReencode, type ScreenshotStatus, type SourceLocation, type SourceMapping, type SubmitOutcome, type SubmitResult, TITLE_MAX_CHARS, type Visibility, type VisibilityEnv, type VisibilityFn, type WebContext, type WebContextInput, WidgetController, type WidgetControllerOpts, type WidgetCorner, type WidgetListener, type WidgetPlatform, type WidgetPosition, type WidgetScreen, type WidgetState, buildContext, buildLassoContext, buildLassoEnvelope, buildLinearIssue, buildNotionPage, buildReport, buildTitle, byteLengthOf, canUseKeepalive, captureWithinLimit, createLassoAdapter, defaultBackoff, denormalizePin, detectDevBuild, diagnosticsProviderFor, getOrCreateGuestId, guestUser, isInternalUser, normalizePin, normalizeUser, parseSourceAttr, renderReportBody, resolveConfig, resolveContextUser, sanitizeUrl, screenshotBytes, sharedDiagnostics, shouldShowWidget, sourceFromElement, uuidv4 };
package/dist/index.d.ts CHANGED
@@ -891,6 +891,18 @@ declare const SUBMIT_FAILED_MESSAGE = "\uBCF4\uB0B4\uC9C0 \uBABB\uD588\uC2B5\uB2
891
891
  type WidgetPlatform = "web" | "native";
892
892
  type ScreenshotStatus = "none" | "capturing" | "ready" | "failed";
893
893
  type ModalSubmitStatus = "idle" | "sending" | "sent" | "pending" | "failed";
894
+ /**
895
+ * 핀이 **무엇을 기준으로** 찍힌 좌표인가. 이미지가 바뀔 때 핀을 버릴지 말지가 여기서 갈린다.
896
+ *
897
+ * - `picture`: 스크린샷 **그림 위**를 탭해서 찍은 좌표(앱 핀 화면). 그림이 바뀌면 가리키는
898
+ * 대상이 달라지므로 **버려야 한다.**
899
+ * - `viewport`: **페이지 화면**을 클릭해서 나온 좌표(웹 요소 지목). 같은 뷰포트를 찍은
900
+ * 스크린샷이면 어느 장이든 같은 지점을 가리키므로 **살려야 한다.**
901
+ *
902
+ * 이 구분이 없던 동안 지목 좌표가 전부 유실됐다: 클릭 직후 `setPin` 으로 들어온 좌표를
903
+ * 수백 ms 뒤 도착한 자동 캡처가 `pin: null` 로 덮었다(실측 — 서버 `x`/`y` 가 전부 null).
904
+ */
905
+ type PinAnchor = "picture" | "viewport";
894
906
  /** 모달 안에서 포커스를 받는 요소들의 고정 id. 렌더러가 그대로 매단다. */
895
907
  declare const MODAL_FIELD_COMMENT = "comment";
896
908
  declare const MODAL_FIELD_PRIORITY = "priority";
@@ -970,6 +982,8 @@ declare class ReportModalController {
970
982
  private lastReport;
971
983
  /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */
972
984
  private captureGeneration;
985
+ /** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
986
+ private pinAnchor;
973
987
  private state;
974
988
  constructor(opts: ReportModalOpts);
975
989
  getState(): ReportModalState;
@@ -979,8 +993,14 @@ declare class ReportModalController {
979
993
  dispose(): void;
980
994
  /** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */
981
995
  stopQueue(): void;
996
+ /**
997
+ * @param opts.pin 요소를 지목해서 열 때의 **클릭 지점**(뷰포트 기준 0~1).
998
+ * 여기로 받아야 뒤늦게 끝나는 자동 캡처가 좌표를 지우지 않는다 — `setPin` 으로
999
+ * 따로 넣으면 캡처와 경쟁해서 진다(그 경쟁에 계속 져서 좌표가 전부 유실됐다).
1000
+ */
982
1001
  open(opts?: {
983
1002
  element?: ElementInfo | null;
1003
+ pin?: FeedbackPin | null;
984
1004
  }): Promise<void>;
985
1005
  /**
986
1006
  * 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.
@@ -997,8 +1017,12 @@ declare class ReportModalController {
997
1017
  /** 상한을 넘겨도 값을 자르지 않는다. 거부는 하되 사용자가 쓴 글은 보존한다. */
998
1018
  setComment(value: string): void;
999
1019
  setPriority(priority: FeedbackPriority): void;
1000
- setPin(pin: FeedbackPin | null): void;
1001
- /** 스크린샷 제거 핀은 스크린샷좌표라 함께 사라진다. */
1020
+ /**
1021
+ * @param anchor 좌표의 기준. 기본은 `picture`(그림 탭) 핀 화면이 쓰는 값이다.
1022
+ * 웹에서 페이지를 클릭해 얻은 좌표는 `viewport` 로 넣어야 캡처에 지워지지 않는다.
1023
+ */
1024
+ setPin(pin: FeedbackPin | null, anchor?: PinAnchor): void;
1025
+ /** 스크린샷 제거. 그림 위에 찍은 핀은 가리킬 대상이 사라지므로 함께 버린다. */
1002
1026
  removeScreenshot(): void;
1003
1027
  recapture(): Promise<void>;
1004
1028
  /** 캡처가 실패했을 때의 대체 경로 — 사용자가 직접 고른 파일. */
@@ -1062,9 +1086,12 @@ declare class WidgetController {
1062
1086
  * @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
1063
1087
  * `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
1064
1088
  * `picking` 에 머물러 **모달이 렌더되지 않는다**.
1089
+ * @param opts.pin 지목한 클릭 지점. 열기 인자로 넘겨야 자동 캡처가 좌표를 지우지 않는다
1090
+ * (열고 난 뒤 `modal.setPin` 으로 넣으면 캡처와 경쟁한다).
1065
1091
  */
1066
1092
  openReport(opts?: {
1067
1093
  element?: ElementInfo | null;
1094
+ pin?: FeedbackPin | null;
1068
1095
  }): Promise<void>;
1069
1096
  /** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
1070
1097
  closeReport(): "closed" | "confirm";
@@ -1194,6 +1221,55 @@ interface LassoAdapterOpts {
1194
1221
  * 400 은 재시도 불가라 모든 제보가 조용히 데드레터로 사라졌다.
1195
1222
  * 계약을 바꿀 때는 반드시 실호출로 다시 확인한다(adapters.live.test.ts).
1196
1223
  */
1224
+ /**
1225
+ * 구조화 칸이 따로 없는 값들을 담는 자리 → 서버가 `meta` 로 옮긴다.
1226
+ *
1227
+ * 왜 생겼나: 예전에는 이 값들을 **사람이 읽는 마크다운으로 접어 `comment` 에 넣었다.**
1228
+ * 그런데 라쏘런의 `comment` 는 목록·상세의 **제목 자리**다. 5자짜리 코멘트 하나가
1229
+ * 1,399자 덩어리가 되어 화면에 쏟아졌다(실측). 라쏘런은 구조화 칸이 있는 대상이니
1230
+ * 접어 넣을 이유가 없다 — 대상별로 담는 방식이 다른 게 어댑터를 두는 이유다.
1231
+ *
1232
+ * 이름은 **뷰어가 이미 읽는 키를 그대로** 쓴다(`user`·`device`·`appInfo`·`navPath` …).
1233
+ * 새 이름을 지으면 같은 값이 화면에서 두 군데로 갈린다.
1234
+ */
1235
+ interface LassoReportContext {
1236
+ /** 제보 종류(`report`=화면 전체, `annotation`=요소 지목). 서버는 요소 유무로도 가른다. */
1237
+ kind: string;
1238
+ app: string;
1239
+ platform: string;
1240
+ sessionId: string;
1241
+ clientTimestamp: string;
1242
+ timezone: string | null;
1243
+ user: {
1244
+ id: string | null;
1245
+ name: string | null;
1246
+ email: string | null;
1247
+ role: string | null;
1248
+ isGuest: boolean;
1249
+ } | null;
1250
+ /** 빌드 플러그인이 심은 화면·소스 매핑. 없으면 null. */
1251
+ source: {
1252
+ screenId: string | null;
1253
+ sourceFile: string | null;
1254
+ sourceLine: number | null;
1255
+ } | null;
1256
+ /** 지목한 요소의 나머지 정보(id·속성). 태그·선택자·텍스트는 report.element 에 있다. */
1257
+ element: {
1258
+ id: string | null;
1259
+ attributes: Record<string, string>;
1260
+ } | null;
1261
+ /** 앱 전용. 뷰어의 구현 정보 칸(기기·앱 버전·OTA·탐색 경로)이 이걸 읽는다. */
1262
+ native: {
1263
+ screenPath: string | null;
1264
+ navPath: string[];
1265
+ routeParams: Record<string, unknown> | null;
1266
+ appInfo: Record<string, unknown>;
1267
+ device: Record<string, unknown>;
1268
+ display: Record<string, unknown>;
1269
+ } | null;
1270
+ /** 호스트가 넣은 자유 메타. 우리가 해석하지 않는다. */
1271
+ extra: Record<string, unknown>;
1272
+ }
1197
1273
  interface LassoEnvelope {
1198
1274
  /** UUID. **Idempotency-Key 헤더와 반드시 같은 값**이어야 서버가 받는다. */
1199
1275
  clientSubmissionId: string;
@@ -1226,7 +1302,16 @@ interface LassoEnvelope {
1226
1302
  timestamp: number;
1227
1303
  };
1228
1304
  report?: {
1305
+ /**
1306
+ * **사용자가 쓴 문장 그대로.** 진단을 여기에 접어 넣지 않는다 — 이 값이 곧 제목이다.
1307
+ * 나머지는 구조화 칸(`web`·`network`·`logs`·`element`)과 `context` 로 간다.
1308
+ */
1229
1309
  comment: string;
1310
+ /**
1311
+ * 좌표. **0~100 백분율**이다(코어의 0~1 정규화를 여기서 옮긴다).
1312
+ * 라쏘런의 `x`/`y` 컬럼과 뷰어 오버레이가 백분율을 전제한다 — 0~1 로 보내면
1313
+ * 뷰어의 뷰포트 역산이 "0.12%" 로 읽어 조용히 버리거나 터무니없는 값을 낸다.
1314
+ */
1230
1315
  pin: {
1231
1316
  x: number;
1232
1317
  y: number;
@@ -1269,8 +1354,12 @@ interface LassoEnvelope {
1269
1354
  screenshotContentType: "image/jpeg" | "image/png" | null;
1270
1355
  network: unknown[];
1271
1356
  logs: unknown[];
1357
+ /** 위 칸에 자리가 없는 값들. 서버가 `meta` 로 펼친다. */
1358
+ context: LassoReportContext;
1272
1359
  };
1273
1360
  }
1361
+ /** 구조화 칸에 자리가 없는 값들을 모은다. 비어 있어도 키는 남긴다(미수집과 없음을 구분). */
1362
+ declare function buildLassoContext(report: FeedbackReport): LassoReportContext;
1274
1363
  /** 제보를 라쏘런 봉투로 바꾼다. 순수 함수라 테스트가 전송 없이 모양만 볼 수 있다. */
1275
1364
  declare function buildLassoEnvelope(report: FeedbackReport): LassoEnvelope;
1276
1365
  /**
@@ -1305,4 +1394,4 @@ declare function guestUser(id: string): FeedbackUser;
1305
1394
  */
1306
1395
  declare function uuidv4(): string;
1307
1396
 
1308
- export { type AdapterName, type AppInfo, BACKOFF_BASE_MS, BACKOFF_MAX_MS, type BuildContextOpts, type BuildReportOpts, COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, type CaptureWithinLimitOpts, type ConfigWarning, type ConfigWarningCode, type ContextProviders, type ContextUser, DEFAULT_ADAPTER, DEFAULT_INTERNAL_ROLES, DEFAULT_POSITION, type DeviceInfo, type DiagLogEntry, type DiagNetworkEntry, type DiagRequestRef, DiagnosticsCollector, type DiagnosticsExcludeMatcher, type DiagnosticsInstallOpts, type DiagnosticsPayload, type DiagnosticsSource, type DisplayInfo, ENDPOINT_FROM_ADAPTER, type ElementInfo, FLOATING_BUTTON_ID, type FeedbackAdapter, type FeedbackConfig, type FeedbackContext, type FeedbackKind, type FeedbackPin, type FeedbackPriority, FeedbackQueue, type FeedbackQueueOpts, type FeedbackReport, type FeedbackScreenshot, type FeedbackStorage, type FeedbackUser, FocusRing, type GetCurrentScreenFn, type GetUserFn, KEEPALIVE_BODY_LIMIT_BYTES, LASSO_DEFAULT_ENDPOINT, LOG_BUFFER_LIMIT, type LassoAdapterOpts, type LassoEnvelope, type LassoFetch, type LassoRequestInit, type LassoResponseLike, type LinearIssueInput, MAX_LOG_MESSAGE_CHARS, MAX_QUEUE_AGE_MS, MAX_QUEUE_ITEMS, MODAL_ACTION_ATTACH, MODAL_ACTION_CANCEL, MODAL_ACTION_PICK, MODAL_ACTION_PIN, MODAL_ACTION_REMOVE_SCREENSHOT, MODAL_ACTION_RETRY, MODAL_ACTION_SEND, MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY, type ModalQueueLike, type ModalSubmitStatus, NETWORK_BUFFER_LIMIT, type NativeContext, type NotionPageInput, PinController, type PixelPoint, type PixelSize, type QueueStatus, type QueueStatusListener, RATE_LIMIT_MIN_DELAY_MS, REDACTED, ReportModalController, type ReportModalListener, type ReportModalOpts, type ReportModalState, type ReportParts, type ResolvedConfig, RingBuffer, SCREENSHOT_FAILED_MESSAGE, SCREENSHOT_MAX_BASE64_BYTES, SCREENSHOT_QUALITY_STEPS, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_FAILED_MESSAGE, SUBMIT_PENDING_MESSAGE, type ScreenshotCapture, type ScreenshotOutcome, type ScreenshotReencode, type ScreenshotStatus, type SourceLocation, type SourceMapping, type SubmitOutcome, type SubmitResult, TITLE_MAX_CHARS, type Visibility, type VisibilityEnv, type VisibilityFn, type WebContext, type WebContextInput, WidgetController, type WidgetControllerOpts, type WidgetCorner, type WidgetListener, type WidgetPlatform, type WidgetPosition, type WidgetScreen, type WidgetState, buildContext, buildLassoEnvelope, buildLinearIssue, buildNotionPage, buildReport, buildTitle, byteLengthOf, canUseKeepalive, captureWithinLimit, createLassoAdapter, defaultBackoff, denormalizePin, detectDevBuild, diagnosticsProviderFor, getOrCreateGuestId, guestUser, isInternalUser, normalizePin, normalizeUser, parseSourceAttr, renderReportBody, resolveConfig, resolveContextUser, sanitizeUrl, screenshotBytes, sharedDiagnostics, shouldShowWidget, sourceFromElement, uuidv4 };
1397
+ export { type AdapterName, type AppInfo, BACKOFF_BASE_MS, BACKOFF_MAX_MS, type BuildContextOpts, type BuildReportOpts, COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, type CaptureWithinLimitOpts, type ConfigWarning, type ConfigWarningCode, type ContextProviders, type ContextUser, DEFAULT_ADAPTER, DEFAULT_INTERNAL_ROLES, DEFAULT_POSITION, type DeviceInfo, type DiagLogEntry, type DiagNetworkEntry, type DiagRequestRef, DiagnosticsCollector, type DiagnosticsExcludeMatcher, type DiagnosticsInstallOpts, type DiagnosticsPayload, type DiagnosticsSource, type DisplayInfo, ENDPOINT_FROM_ADAPTER, type ElementInfo, FLOATING_BUTTON_ID, type FeedbackAdapter, type FeedbackConfig, type FeedbackContext, type FeedbackKind, type FeedbackPin, type FeedbackPriority, FeedbackQueue, type FeedbackQueueOpts, type FeedbackReport, type FeedbackScreenshot, type FeedbackStorage, type FeedbackUser, FocusRing, type GetCurrentScreenFn, type GetUserFn, KEEPALIVE_BODY_LIMIT_BYTES, LASSO_DEFAULT_ENDPOINT, LOG_BUFFER_LIMIT, type LassoAdapterOpts, type LassoEnvelope, type LassoFetch, type LassoReportContext, type LassoRequestInit, type LassoResponseLike, type LinearIssueInput, MAX_LOG_MESSAGE_CHARS, MAX_QUEUE_AGE_MS, MAX_QUEUE_ITEMS, MODAL_ACTION_ATTACH, MODAL_ACTION_CANCEL, MODAL_ACTION_PICK, MODAL_ACTION_PIN, MODAL_ACTION_REMOVE_SCREENSHOT, MODAL_ACTION_RETRY, MODAL_ACTION_SEND, MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY, type ModalQueueLike, type ModalSubmitStatus, NETWORK_BUFFER_LIMIT, type NativeContext, type NotionPageInput, PinController, type PixelPoint, type PixelSize, type QueueStatus, type QueueStatusListener, RATE_LIMIT_MIN_DELAY_MS, REDACTED, ReportModalController, type ReportModalListener, type ReportModalOpts, type ReportModalState, type ReportParts, type ResolvedConfig, RingBuffer, SCREENSHOT_FAILED_MESSAGE, SCREENSHOT_MAX_BASE64_BYTES, SCREENSHOT_QUALITY_STEPS, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_FAILED_MESSAGE, SUBMIT_PENDING_MESSAGE, type ScreenshotCapture, type ScreenshotOutcome, type ScreenshotReencode, type ScreenshotStatus, type SourceLocation, type SourceMapping, type SubmitOutcome, type SubmitResult, TITLE_MAX_CHARS, type Visibility, type VisibilityEnv, type VisibilityFn, type WebContext, type WebContextInput, WidgetController, type WidgetControllerOpts, type WidgetCorner, type WidgetListener, type WidgetPlatform, type WidgetPosition, type WidgetScreen, type WidgetState, buildContext, buildLassoContext, buildLassoEnvelope, buildLinearIssue, buildNotionPage, buildReport, buildTitle, byteLengthOf, canUseKeepalive, captureWithinLimit, createLassoAdapter, defaultBackoff, denormalizePin, detectDevBuild, diagnosticsProviderFor, getOrCreateGuestId, guestUser, isInternalUser, normalizePin, normalizeUser, parseSourceAttr, renderReportBody, resolveConfig, resolveContextUser, sanitizeUrl, screenshotBytes, sharedDiagnostics, shouldShowWidget, sourceFromElement, uuidv4 };
package/dist/index.js CHANGED
@@ -1358,6 +1358,8 @@ var ReportModalController = class {
1358
1358
  this.lastReport = null;
1359
1359
  /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */
1360
1360
  this.captureGeneration = 0;
1361
+ /** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
1362
+ this.pinAnchor = "picture";
1361
1363
  this.state = {
1362
1364
  open: false,
1363
1365
  element: null,
@@ -1426,12 +1428,18 @@ var ReportModalController = class {
1426
1428
  this.queue.stop?.();
1427
1429
  }
1428
1430
  // ── 열기/닫기 ─────────────────────────────────────────────────────────────
1431
+ /**
1432
+ * @param opts.pin 요소를 지목해서 열 때의 **클릭 지점**(뷰포트 기준 0~1).
1433
+ * 여기로 받아야 뒤늦게 끝나는 자동 캡처가 좌표를 지우지 않는다 — `setPin` 으로
1434
+ * 따로 넣으면 캡처와 경쟁해서 진다(그 경쟁에 계속 져서 좌표가 전부 유실됐다).
1435
+ */
1429
1436
  async open(opts = {}) {
1430
1437
  if (this.state.open) return;
1431
1438
  const wasPending = this.state.submitStatus === "pending" && this.lastReport !== null;
1432
1439
  const pendingReport = wasPending ? this.lastReport : null;
1433
1440
  this.touched = false;
1434
1441
  this.submitAttempted = false;
1442
+ this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
1435
1443
  this.patch({
1436
1444
  open: true,
1437
1445
  comment: pendingReport?.comment ?? "",
@@ -1441,7 +1449,7 @@ var ReportModalController = class {
1441
1449
  screenshot: pendingReport?.screenshot ?? null,
1442
1450
  screenshotStatus: "none",
1443
1451
  screenshotMessage: null,
1444
- pin: pendingReport?.pin ?? null,
1452
+ pin: pendingReport?.pin ?? opts.pin ?? null,
1445
1453
  submitStatus: wasPending ? "pending" : "idle",
1446
1454
  submitMessage: wasPending ? SUBMIT_PENDING_MESSAGE : null,
1447
1455
  closeConfirmVisible: false,
@@ -1490,6 +1498,7 @@ var ReportModalController = class {
1490
1498
  };
1491
1499
  this.touched = false;
1492
1500
  this.submitAttempted = false;
1501
+ if (!keepDraft) this.pinAnchor = "picture";
1493
1502
  this.patch({
1494
1503
  open: false,
1495
1504
  closeConfirmVisible: false,
@@ -1509,11 +1518,16 @@ var ReportModalController = class {
1509
1518
  if (this.isDraftLocked()) return;
1510
1519
  this.patch({ priority });
1511
1520
  }
1512
- setPin(pin) {
1521
+ /**
1522
+ * @param anchor 이 좌표의 기준. 기본은 `picture`(그림 위 탭) — 앱 핀 화면이 쓰는 값이다.
1523
+ * 웹에서 페이지를 클릭해 얻은 좌표는 `viewport` 로 넣어야 캡처에 지워지지 않는다.
1524
+ */
1525
+ setPin(pin, anchor = "picture") {
1513
1526
  if (this.isDraftLocked()) return;
1527
+ this.pinAnchor = anchor;
1514
1528
  this.patch({ pin });
1515
1529
  }
1516
- /** 스크린샷 제거 핀은 스크린샷 좌표라 함께 사라진다. */
1530
+ /** 스크린샷 제거. 그림 위에 찍은 핀은 가리킬 대상이 사라지므로 함께 버린다. */
1517
1531
  removeScreenshot() {
1518
1532
  if (this.isDraftLocked()) return;
1519
1533
  this.captureGeneration += 1;
@@ -1521,7 +1535,7 @@ var ReportModalController = class {
1521
1535
  screenshot: null,
1522
1536
  screenshotStatus: "none",
1523
1537
  screenshotMessage: null,
1524
- pin: null
1538
+ ...this.pinAnchor === "picture" ? { pin: null } : {}
1525
1539
  });
1526
1540
  }
1527
1541
  async recapture() {
@@ -1554,15 +1568,21 @@ var ReportModalController = class {
1554
1568
  }
1555
1569
  }
1556
1570
  applyScreenshot(shot, failed) {
1571
+ const dropPin = this.pinAnchor === "picture" ? { pin: null } : {};
1557
1572
  if (shot && !failed) {
1558
- this.patch({ screenshot: shot, screenshotStatus: "ready", screenshotMessage: null, pin: null });
1573
+ this.patch({
1574
+ screenshot: shot,
1575
+ screenshotStatus: "ready",
1576
+ screenshotMessage: null,
1577
+ ...dropPin
1578
+ });
1559
1579
  return;
1560
1580
  }
1561
1581
  this.patch({
1562
1582
  screenshot: null,
1563
1583
  screenshotStatus: "failed",
1564
1584
  screenshotMessage: SCREENSHOT_FAILED_MESSAGE,
1565
- pin: null
1585
+ ...dropPin
1566
1586
  });
1567
1587
  }
1568
1588
  // ── 포커스 ────────────────────────────────────────────────────────────────
@@ -1758,6 +1778,8 @@ var WidgetController = class {
1758
1778
  * @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
1759
1779
  * `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
1760
1780
  * `picking` 에 머물러 **모달이 렌더되지 않는다**.
1781
+ * @param opts.pin 지목한 클릭 지점. 열기 인자로 넘겨야 자동 캡처가 좌표를 지우지 않는다
1782
+ * (열고 난 뒤 `modal.setPin` 으로 넣으면 캡처와 경쟁한다).
1761
1783
  */
1762
1784
  async openReport(opts = {}) {
1763
1785
  this.screen = "modal";
@@ -2113,6 +2135,47 @@ function idFrom(parsed) {
2113
2135
  }
2114
2136
  return null;
2115
2137
  }
2138
+ function toPercent(value) {
2139
+ return Math.round(value * 1e5) / 1e3;
2140
+ }
2141
+ function buildLassoContext(report) {
2142
+ const c = report.context;
2143
+ const el = report.element;
2144
+ const user = c.user;
2145
+ const source = el ? sourceFromElement(el) : c.source;
2146
+ const hasSource = Boolean(source.screenId || source.sourceFile);
2147
+ return {
2148
+ kind: report.kind,
2149
+ app: c.app,
2150
+ platform: c.platform,
2151
+ sessionId: c.sessionId,
2152
+ clientTimestamp: c.clientTimestamp,
2153
+ timezone: c.timezone,
2154
+ user: user ? {
2155
+ id: user.id,
2156
+ name: user.name ?? null,
2157
+ email: user.email,
2158
+ role: user.role ?? null,
2159
+ isGuest: user.isGuest
2160
+ } : null,
2161
+ source: hasSource ? {
2162
+ screenId: source.screenId,
2163
+ sourceFile: source.sourceFile,
2164
+ sourceLine: source.sourceLine ?? null
2165
+ } : null,
2166
+ // 속성이 하나도 없으면 빈 객체를 보내지 않는다 — 화면에 빈 항목이 늘어난다.
2167
+ element: el && (el.id !== null || Object.keys(el.attributes).length > 0) ? { id: el.id, attributes: el.attributes } : null,
2168
+ native: c.native ? {
2169
+ screenPath: c.native.screenPath,
2170
+ navPath: c.native.navPath,
2171
+ routeParams: c.native.routeParams,
2172
+ appInfo: { ...c.native.appInfo },
2173
+ device: { ...c.native.device },
2174
+ display: { ...c.native.display }
2175
+ } : null,
2176
+ extra: c.extra
2177
+ };
2178
+ }
2116
2179
  function buildLassoEnvelope(report) {
2117
2180
  const c = report.context;
2118
2181
  const clientType = c.platform === "native" ? "app" : "web";
@@ -2128,8 +2191,9 @@ function buildLassoEnvelope(report) {
2128
2191
  url: c.url ?? (clientType === "app" ? `app://${c.screen ?? "unknown"}` : ""),
2129
2192
  priority: report.priority,
2130
2193
  report: {
2131
- comment: renderReportBody(report),
2132
- pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,
2194
+ // 서버가 1자 이상을 요구한다. 코멘트 없는 제보를 400 으로 잃지 않게 한 줄을 채운다.
2195
+ comment: report.comment.trim().length > 0 ? report.comment.trim() : "(\uCF54\uBA58\uD2B8 \uC5C6\uC74C)",
2196
+ pin: report.pin ? { x: toPercent(report.pin.x), y: toPercent(report.pin.y) } : null,
2133
2197
  sourceFile: el?.attributes["data-feedback-source"] ?? null,
2134
2198
  element: el ? {
2135
2199
  tag: el.tag,
@@ -2151,7 +2215,8 @@ function buildLassoEnvelope(report) {
2151
2215
  screenshotContentType: report.screenshot?.contentType ?? null,
2152
2216
  // 서버가 없으면 빈 배열로 취급하지만, 미연동과 "없었음"을 구분해 보낸다.
2153
2217
  network: report.context.diagnostics?.network ?? [],
2154
- logs: report.context.diagnostics?.logs ?? []
2218
+ logs: report.context.diagnostics?.logs ?? [],
2219
+ context: buildLassoContext(report)
2155
2220
  }
2156
2221
  };
2157
2222
  }
@@ -2285,6 +2350,7 @@ export {
2285
2350
  TITLE_MAX_CHARS,
2286
2351
  WidgetController,
2287
2352
  buildContext,
2353
+ buildLassoContext,
2288
2354
  buildLassoEnvelope,
2289
2355
  buildLinearIssue,
2290
2356
  buildNotionPage,