@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.cjs CHANGED
@@ -64,6 +64,7 @@ __export(index_exports, {
64
64
  TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
65
65
  WidgetController: () => WidgetController,
66
66
  buildContext: () => buildContext,
67
+ buildLassoContext: () => buildLassoContext,
67
68
  buildLassoEnvelope: () => buildLassoEnvelope,
68
69
  buildLinearIssue: () => buildLinearIssue,
69
70
  buildNotionPage: () => buildNotionPage,
@@ -1455,6 +1456,8 @@ var ReportModalController = class {
1455
1456
  this.lastReport = null;
1456
1457
  /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */
1457
1458
  this.captureGeneration = 0;
1459
+ /** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
1460
+ this.pinAnchor = "picture";
1458
1461
  this.state = {
1459
1462
  open: false,
1460
1463
  element: null,
@@ -1523,12 +1526,18 @@ var ReportModalController = class {
1523
1526
  this.queue.stop?.();
1524
1527
  }
1525
1528
  // ── 열기/닫기 ─────────────────────────────────────────────────────────────
1529
+ /**
1530
+ * @param opts.pin 요소를 지목해서 열 때의 **클릭 지점**(뷰포트 기준 0~1).
1531
+ * 여기로 받아야 뒤늦게 끝나는 자동 캡처가 좌표를 지우지 않는다 — `setPin` 으로
1532
+ * 따로 넣으면 캡처와 경쟁해서 진다(그 경쟁에 계속 져서 좌표가 전부 유실됐다).
1533
+ */
1526
1534
  async open(opts = {}) {
1527
1535
  if (this.state.open) return;
1528
1536
  const wasPending = this.state.submitStatus === "pending" && this.lastReport !== null;
1529
1537
  const pendingReport = wasPending ? this.lastReport : null;
1530
1538
  this.touched = false;
1531
1539
  this.submitAttempted = false;
1540
+ this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
1532
1541
  this.patch({
1533
1542
  open: true,
1534
1543
  comment: pendingReport?.comment ?? "",
@@ -1538,7 +1547,7 @@ var ReportModalController = class {
1538
1547
  screenshot: pendingReport?.screenshot ?? null,
1539
1548
  screenshotStatus: "none",
1540
1549
  screenshotMessage: null,
1541
- pin: pendingReport?.pin ?? null,
1550
+ pin: pendingReport?.pin ?? opts.pin ?? null,
1542
1551
  submitStatus: wasPending ? "pending" : "idle",
1543
1552
  submitMessage: wasPending ? SUBMIT_PENDING_MESSAGE : null,
1544
1553
  closeConfirmVisible: false,
@@ -1587,6 +1596,7 @@ var ReportModalController = class {
1587
1596
  };
1588
1597
  this.touched = false;
1589
1598
  this.submitAttempted = false;
1599
+ if (!keepDraft) this.pinAnchor = "picture";
1590
1600
  this.patch({
1591
1601
  open: false,
1592
1602
  closeConfirmVisible: false,
@@ -1606,11 +1616,16 @@ var ReportModalController = class {
1606
1616
  if (this.isDraftLocked()) return;
1607
1617
  this.patch({ priority });
1608
1618
  }
1609
- setPin(pin) {
1619
+ /**
1620
+ * @param anchor 이 좌표의 기준. 기본은 `picture`(그림 위 탭) — 앱 핀 화면이 쓰는 값이다.
1621
+ * 웹에서 페이지를 클릭해 얻은 좌표는 `viewport` 로 넣어야 캡처에 지워지지 않는다.
1622
+ */
1623
+ setPin(pin, anchor = "picture") {
1610
1624
  if (this.isDraftLocked()) return;
1625
+ this.pinAnchor = anchor;
1611
1626
  this.patch({ pin });
1612
1627
  }
1613
- /** 스크린샷 제거 핀은 스크린샷 좌표라 함께 사라진다. */
1628
+ /** 스크린샷 제거. 그림 위에 찍은 핀은 가리킬 대상이 사라지므로 함께 버린다. */
1614
1629
  removeScreenshot() {
1615
1630
  if (this.isDraftLocked()) return;
1616
1631
  this.captureGeneration += 1;
@@ -1618,7 +1633,7 @@ var ReportModalController = class {
1618
1633
  screenshot: null,
1619
1634
  screenshotStatus: "none",
1620
1635
  screenshotMessage: null,
1621
- pin: null
1636
+ ...this.pinAnchor === "picture" ? { pin: null } : {}
1622
1637
  });
1623
1638
  }
1624
1639
  async recapture() {
@@ -1651,15 +1666,21 @@ var ReportModalController = class {
1651
1666
  }
1652
1667
  }
1653
1668
  applyScreenshot(shot, failed) {
1669
+ const dropPin = this.pinAnchor === "picture" ? { pin: null } : {};
1654
1670
  if (shot && !failed) {
1655
- this.patch({ screenshot: shot, screenshotStatus: "ready", screenshotMessage: null, pin: null });
1671
+ this.patch({
1672
+ screenshot: shot,
1673
+ screenshotStatus: "ready",
1674
+ screenshotMessage: null,
1675
+ ...dropPin
1676
+ });
1656
1677
  return;
1657
1678
  }
1658
1679
  this.patch({
1659
1680
  screenshot: null,
1660
1681
  screenshotStatus: "failed",
1661
1682
  screenshotMessage: SCREENSHOT_FAILED_MESSAGE,
1662
- pin: null
1683
+ ...dropPin
1663
1684
  });
1664
1685
  }
1665
1686
  // ── 포커스 ────────────────────────────────────────────────────────────────
@@ -1855,6 +1876,8 @@ var WidgetController = class {
1855
1876
  * @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
1856
1877
  * `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
1857
1878
  * `picking` 에 머물러 **모달이 렌더되지 않는다**.
1879
+ * @param opts.pin 지목한 클릭 지점. 열기 인자로 넘겨야 자동 캡처가 좌표를 지우지 않는다
1880
+ * (열고 난 뒤 `modal.setPin` 으로 넣으면 캡처와 경쟁한다).
1858
1881
  */
1859
1882
  async openReport(opts = {}) {
1860
1883
  this.screen = "modal";
@@ -2210,6 +2233,47 @@ function idFrom(parsed) {
2210
2233
  }
2211
2234
  return null;
2212
2235
  }
2236
+ function toPercent(value) {
2237
+ return Math.round(value * 1e5) / 1e3;
2238
+ }
2239
+ function buildLassoContext(report) {
2240
+ const c = report.context;
2241
+ const el = report.element;
2242
+ const user = c.user;
2243
+ const source = el ? sourceFromElement(el) : c.source;
2244
+ const hasSource = Boolean(source.screenId || source.sourceFile);
2245
+ return {
2246
+ kind: report.kind,
2247
+ app: c.app,
2248
+ platform: c.platform,
2249
+ sessionId: c.sessionId,
2250
+ clientTimestamp: c.clientTimestamp,
2251
+ timezone: c.timezone,
2252
+ user: user ? {
2253
+ id: user.id,
2254
+ name: user.name ?? null,
2255
+ email: user.email,
2256
+ role: user.role ?? null,
2257
+ isGuest: user.isGuest
2258
+ } : null,
2259
+ source: hasSource ? {
2260
+ screenId: source.screenId,
2261
+ sourceFile: source.sourceFile,
2262
+ sourceLine: source.sourceLine ?? null
2263
+ } : null,
2264
+ // 속성이 하나도 없으면 빈 객체를 보내지 않는다 — 화면에 빈 항목이 늘어난다.
2265
+ element: el && (el.id !== null || Object.keys(el.attributes).length > 0) ? { id: el.id, attributes: el.attributes } : null,
2266
+ native: c.native ? {
2267
+ screenPath: c.native.screenPath,
2268
+ navPath: c.native.navPath,
2269
+ routeParams: c.native.routeParams,
2270
+ appInfo: { ...c.native.appInfo },
2271
+ device: { ...c.native.device },
2272
+ display: { ...c.native.display }
2273
+ } : null,
2274
+ extra: c.extra
2275
+ };
2276
+ }
2213
2277
  function buildLassoEnvelope(report) {
2214
2278
  const c = report.context;
2215
2279
  const clientType = c.platform === "native" ? "app" : "web";
@@ -2225,8 +2289,9 @@ function buildLassoEnvelope(report) {
2225
2289
  url: c.url ?? (clientType === "app" ? `app://${c.screen ?? "unknown"}` : ""),
2226
2290
  priority: report.priority,
2227
2291
  report: {
2228
- comment: renderReportBody(report),
2229
- pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,
2292
+ // 서버가 1자 이상을 요구한다. 코멘트 없는 제보를 400 으로 잃지 않게 한 줄을 채운다.
2293
+ comment: report.comment.trim().length > 0 ? report.comment.trim() : "(\uCF54\uBA58\uD2B8 \uC5C6\uC74C)",
2294
+ pin: report.pin ? { x: toPercent(report.pin.x), y: toPercent(report.pin.y) } : null,
2230
2295
  sourceFile: el?.attributes["data-feedback-source"] ?? null,
2231
2296
  element: el ? {
2232
2297
  tag: el.tag,
@@ -2248,7 +2313,8 @@ function buildLassoEnvelope(report) {
2248
2313
  screenshotContentType: report.screenshot?.contentType ?? null,
2249
2314
  // 서버가 없으면 빈 배열로 취급하지만, 미연동과 "없었음"을 구분해 보낸다.
2250
2315
  network: report.context.diagnostics?.network ?? [],
2251
- logs: report.context.diagnostics?.logs ?? []
2316
+ logs: report.context.diagnostics?.logs ?? [],
2317
+ context: buildLassoContext(report)
2252
2318
  }
2253
2319
  };
2254
2320
  }
@@ -2383,6 +2449,7 @@ function createLassoAdapter(opts = {}) {
2383
2449
  TITLE_MAX_CHARS,
2384
2450
  WidgetController,
2385
2451
  buildContext,
2452
+ buildLassoContext,
2386
2453
  buildLassoEnvelope,
2387
2454
  buildLinearIssue,
2388
2455
  buildNotionPage,