issue-sticker 0.3.1 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  npm 패키지 `issue-sticker`의 변경 이력입니다. 설치·업그레이드에 영향을 주는 변경만 기록합니다.
4
4
 
5
+ ## 0.4.0 - 2026-08-27
6
+
7
+ ### 추가
8
+
9
+ - 위젯 UI를 가로 캡슐 툴바로 바꿨습니다. 이슈 생성·화면 녹화·이슈 목록·멤버 로그인을 툴바에서 바로 열고, 나머지는 라벨이 달린 더보기 메뉴에 담았습니다. 툴바는 어디를 잡아도 끌어 옮길 수 있고 위치가 저장됩니다.
10
+ - `toolbar` 설정으로 툴바 초기 위치를 지정합니다. `corner`(`'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'`, 기본 `'bottom-right'`)와 `offset`(코너로부터의 px, 기본 `{ x: 20, y: 20 }`)을 받습니다. 사용자가 끌어 저장한 위치가 있으면 그쪽이 우선합니다.
11
+ - 호스트 페이지에서 툴바를 찾는 `[data-issue-sticker-toolbar]` 속성을 공개 계약으로 고정했습니다. 내부 클래스명은 계약이 아닙니다.
12
+ - 녹화 중에는 툴바에 경과 시간과 중지 버튼이 나타납니다.
13
+ - 이슈 생성 모드에서 커서가 스티커 미리보기로 바뀝니다.
14
+
15
+ ### 개선
16
+
17
+ - `init()` 시점부터 네트워크·콘솔 로그를 모읍니다. 이슈를 만들 때 그때부터 모으는 것이 아니라, 위젯을 띄운 뒤 벌어진 일이 이슈에 담깁니다. 자세한 계측 범위는 README의 "수집하는 정보"를 확인하세요.
18
+ - 페이지를 새로 고치거나 다른 문서로 이동해도 로그와 세션 리플레이가 이어집니다.
19
+ - 네트워크·콘솔 로그와 세션 리플레이에서 자격증명으로 보이는 값, 국내 식별번호, 결제 관련 필드를 지웁니다. 마스킹이 원래 길이를 드러내지 않습니다.
20
+ - 계측이 호스트 앱을 방해하지 않도록 보강했습니다. 다른 라이브러리가 이미 감싼 `fetch`·`XMLHttpRequest` 래퍼를 보존하고, 스트리밍 응답 본문은 캡처하지 않으며, 계측 내부 예외를 호출자에게 전파하지 않습니다. 예외가 반복되면 수집기가 스스로 패치를 걷어냅니다.
21
+
22
+ ### 수정
23
+
24
+ - 네트워크 로그가 너무 커져 이슈 생성이 실패하던 문제를 고쳤습니다.
25
+ - 저장된 툴바 위치가 화면 밖으로 나가거나 더보기 메뉴가 좌우로 잘리던 문제, 온보딩이 막다른 길로 빠지던 문제를 고쳤습니다.
26
+ - 이슈 폼에서 읽기 전용·체크박스 필드가 옆 칸과 어긋나던 문제를 고쳤습니다.
27
+
28
+ ### 주의가 필요한 변경
29
+
30
+ - 이슈를 만들 때 Jira·Notion 생성을 SDK가 직접 호출하지 않고 이슈 생성 요청에 실어 보냅니다. 서버가 대신 만들고, 실패해도 이슈는 남습니다. **백엔드 1.2.0 이상이 필요합니다.**
31
+ - `onboarding` 설정에서 `'onFABClick'` 을 없앴습니다. FAB이 툴바로 바뀌면서 대응하는 동작이 사라졌습니다. 아는 값이 아니면 `'onMount'` 로 떨어지므로 기존 코드가 런타임에서 깨지지는 않지만, 타입은 더 이상 이 값을 받지 않습니다.
32
+ - 이슈 댓글 관련 코드를 제거했습니다. 어느 표면에서도 노출된 적 없는 기능입니다.
33
+
5
34
  ## 0.3.1 - 2026-08-17
6
35
 
7
36
  ### 수정
package/README.md CHANGED
@@ -29,7 +29,7 @@ npm install issue-sticker
29
29
 
30
30
  | 경로 | 용도 |
31
31
  | ------------------------------ | ------------------------------------------------------- |
32
- | `issue-sticker/react` | 위젯 UI(FAB, 이슈 폼, 마커)를 마운트하는 React 어댑터 |
32
+ | `issue-sticker/react` | 위젯 UI(툴바, 이슈 폼, 마커)를 마운트하는 React 어댑터 |
33
33
  | `issue-sticker/headless` | UI 없이 이슈 흐름을 구독하고 제어하는 컨트롤러 API |
34
34
  | `issue-sticker/headless/react` | Headless UI를 직접 만들 때 사용하는 최소 React 헬퍼 |
35
35
 
@@ -61,15 +61,15 @@ export function IssueStickerWidget() {
61
61
  | ----------- | ------------------- | ------------------------------------------------- | ------------------------- |
62
62
  | 익명 | `user` 없이 초기화 | 조회 없음 (이번 세션에서 만든 이슈만 화면에 표시) | 불가 (자기가 만든 이슈도) |
63
63
  | 사용자 식별 | `user` 전달 | 해당 사용자가 만든 이슈 | 자기가 만든 이슈만 |
64
- | 멤버 | FAB의 "멤버 로그인" | 프로젝트의 모든 이슈 | 프로젝트의 모든 이슈 |
64
+ | 멤버 | 툴바의 "멤버 로그인" | 프로젝트의 모든 이슈 | 프로젝트의 모든 이슈 |
65
65
 
66
66
  ### 위젯 기능
67
67
 
68
- 초기화를 마치면 화면 오른쪽 아래에 FAB가 표시됩니다. FAB는 아래 기능을 제공하며, 드래그로 위치를 옮길 수 있습니다.
68
+ 초기화를 마치면 화면 모서리(기본 오른쪽 아래)에 툴바가 표시됩니다. 툴바는 아래 기능을 제공하며, 드래그로 위치를 옮길 수 있습니다.
69
69
  - 이슈 생성
70
70
  - 화면 녹화
71
71
  - 마커 표시 토글
72
- - 마커 기준 전환(생성 위치 기준 / 대상 요소 기준)
72
+ - 마커 기준 전환(생성 위치 기준 / 캡처 대상 요소 기준)
73
73
  - 미표시 스티커 확인
74
74
  - 가이드 다시 보기
75
75
  - 멤버 로그인·로그아웃
@@ -91,10 +91,16 @@ export function IssueStickerWidget() {
91
91
  | ----------------------- | ------------------------------------ | ---- | ------------------------------------------------------------------------------------------- |
92
92
  | `apiKey` | `string` | O | 관리자 콘솔에서 발급한 SDK API key |
93
93
  | `user` | `SDKUser` | X | 앱의 사용자 정보. 제공하면 작성자가 기록되고 자기 이슈를 조회·수정·삭제할 수 있습니다 |
94
- | `onboarding` | `'off' \| 'onMount' \| 'onFABClick'` | X | 온보딩 가이드 트리거. 기본값 `'onMount'`. `'off'`면 "가이드 다시 보기" 버튼도 숨겨집니다 |
94
+ | `onboarding` | `'off' \| 'onMount'` | X | 온보딩 가이드 트리거. 기본값 `'onMount'`. `'off'`면 "가이드 다시 보기" 버튼도 숨겨집니다 |
95
+ | `toolbar` | `{ corner?, offset? }` | X | 툴바 초기 위치. `corner`는 `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'`(기본 `'bottom-right'`), `offset`은 코너로부터의 px(기본 `{ x: 20, y: 20 }`). 사용자가 드래그해 저장한 위치가 있으면 그쪽이 우선합니다 |
95
96
  | `screenshotCompression` | `{ useWebWorker?: boolean }` | X | 스크린샷 압축을 Web Worker에서 할지 여부 |
96
97
  | `enabled` | `boolean` | X | 위젯 활성화 여부. 기본값 `true`. `useIssueSticker` 전용 |
97
98
 
99
+ ### 툴바 셀렉터
100
+
101
+ 호스트 페이지에서 툴바 위치를 알아야 한다면 `[data-issue-sticker-toolbar]`로 찾으세요.
102
+ 이 속성은 공개 계약이며 버전 간 유지됩니다. 내부 클래스명은 계약이 아니므로 의존하지 마세요.
103
+
98
104
  컴포넌트가 마운트되면 위젯을 초기화하고, 언마운트되면 정리합니다. `apiKey`나 `enabled`가 바뀌면 위젯을 다시 초기화하고, `user`만 바뀌면 위젯을 유지한 채 설정만 갱신합니다.
99
105
 
100
106
  ### 훅 없이 초기화하기
@@ -154,6 +160,17 @@ captureButton.onclick = () => client.startIssueCapture();
154
160
  클라이언트는 현재 상태를 읽기 전용 snapshot으로 넘겨줍니다. 메서드 전체, phase와 snapshot 모델, 동적 필드 descriptor, 에러 코드는 [Headless 가이드](https://issuesticker.com/guide/products/headless)에 있습니다.
155
161
 
156
162
 
163
+ ## 수집하는 정보
164
+
165
+ `init()`이 끝난 시점부터 위젯이 호스트 페이지의 `fetch`, `XMLHttpRequest`, `console`을 감싸 네트워크 요청과 콘솔 출력을 메모리에 수집합니다. 이슈를 만들 때 그 시점의 최근 기록이 함께 첨부됩니다. 사용자가 이슈를 만들지 않으면 아무것도 전송되지 않습니다.
166
+
167
+ 호스트 앱에 영향을 주지 않도록 다음을 지킵니다.
168
+
169
+ - 다른 라이브러리가 이미 감싼 `fetch`·`XMLHttpRequest` 래퍼를 보존합니다. `destroy()`로 정리할 때도 타 라이브러리 래퍼를 지우지 않습니다.
170
+ - 스트리밍 응답의 본문은 캡처하지 않습니다. 응답은 먼저 호출자에게 돌려주고 본문은 나중에 채웁니다.
171
+ - 계측 내부에서 난 예외는 호출자에게 전파하지 않습니다. 예외가 반복되면 수집기가 스스로 패치를 걷어냅니다.
172
+ - 자격증명으로 보이는 값, 국내 식별번호, 결제 관련 필드는 저장 전에 제거합니다.
173
+
157
174
  ## 문제 해결
158
175
 
159
176
  - 위젯이 보이지 않으면 프로젝트에 등록한 허용 도메인과 현재 `window.location.origin`이 같은지 확인하세요.
@@ -2,6 +2,23 @@ import * as React from "react";
2
2
  import { useLayoutEffect, forwardRef, createElement } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import * as ReactDOM from "react-dom";
5
+ const OVERLAY_LAYER_INDEX = {
6
+ // 익스텐션·SDK 위젯의 shadow host. isolation:isolate 로 stacking context 를 만들기 때문에
7
+ // 아래 레이어 값들은 호스트 내부 순서만 정한다. 호스트 페이지의 오버레이와 겨루는 것은
8
+ // 이 값 하나뿐이라, 페이지가 쓸 수 있는 어떤 레이어보다 위여야 한다.
9
+ runtimeHost: 2147483647,
10
+ fab: 999999,
11
+ tourMask: 1000006,
12
+ tourPopover: 1000007
13
+ };
14
+ const OVERLAY_LAYER = {
15
+ shell: "z-[1000000]",
16
+ sdkLoginOverlay: "z-[1000003]",
17
+ dialogOverlay: "z-[1000004]",
18
+ dialogContent: "z-[1000005]",
19
+ floating: "z-[1000008]",
20
+ toast: "z-[1000010]"
21
+ };
5
22
  /*! pako 2.2.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */
6
23
  const Z_FIXED$1 = 4;
7
24
  const Z_BINARY = 0;
@@ -21612,23 +21629,9 @@ class Replayer {
21612
21629
  this.config.logger.log(REPLAY_CONSOLE_PREFIX, ...args);
21613
21630
  }
21614
21631
  }
21615
- const OVERLAY_LAYER_INDEX = {
21616
- // 익스텐션·SDK 위젯의 shadow host. isolation:isolate 로 stacking context 를 만들기 때문에
21617
- // 아래 레이어 값들은 호스트 내부 순서만 정한다. 호스트 페이지의 오버레이와 겨루는 것은
21618
- // 이 값 하나뿐이라, 페이지가 쓸 수 있는 어떤 레이어보다 위여야 한다.
21619
- runtimeHost: 2147483647,
21620
- fab: 999999,
21621
- tourMask: 1000006,
21622
- tourPopover: 1000007
21623
- };
21624
- const OVERLAY_LAYER = {
21625
- shell: "z-[1000000]",
21626
- sdkLoginOverlay: "z-[1000003]",
21627
- dialogOverlay: "z-[1000004]",
21628
- dialogContent: "z-[1000005]",
21629
- floating: "z-[1000008]",
21630
- toast: "z-[1000010]"
21631
- };
21632
+ const { addCustomEvent } = record;
21633
+ const { freezePage } = record;
21634
+ const { takeFullSnapshot } = record;
21632
21635
  function r(e2) {
21633
21636
  var t2, f2, n2 = "";
21634
21637
  if ("string" == typeof e2 || "number" == typeof e2) n2 += e2;
@@ -24104,6 +24107,14 @@ const twMerge = /* @__PURE__ */ createTailwindMerge(getDefaultConfig);
24104
24107
  function cn(...inputs) {
24105
24108
  return twMerge(clsx(inputs));
24106
24109
  }
24110
+ function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
24111
+ return function handleEvent(event) {
24112
+ originalEventHandler == null ? void 0 : originalEventHandler(event);
24113
+ if (checkForDefaultPrevented === false || !event.defaultPrevented) {
24114
+ return ourEventHandler == null ? void 0 : ourEventHandler(event);
24115
+ }
24116
+ };
24117
+ }
24107
24118
  function setRef$1(ref, value) {
24108
24119
  if (typeof ref === "function") {
24109
24120
  return ref(value);
@@ -24138,6 +24149,66 @@ function composeRefs(...refs) {
24138
24149
  function useComposedRefs(...refs) {
24139
24150
  return React.useCallback(composeRefs(...refs), refs);
24140
24151
  }
24152
+ function createContextScope(scopeName, createContextScopeDeps = []) {
24153
+ let defaultContexts = [];
24154
+ function createContext3(rootComponentName, defaultContext) {
24155
+ const BaseContext = React.createContext(defaultContext);
24156
+ BaseContext.displayName = rootComponentName + "Context";
24157
+ const index2 = defaultContexts.length;
24158
+ defaultContexts = [...defaultContexts, defaultContext];
24159
+ const Provider = (props) => {
24160
+ var _a2;
24161
+ const { scope, children, ...context } = props;
24162
+ const Context = ((_a2 = scope == null ? void 0 : scope[scopeName]) == null ? void 0 : _a2[index2]) || BaseContext;
24163
+ const value = React.useMemo(() => context, Object.values(context));
24164
+ return /* @__PURE__ */ jsx(Context.Provider, { value, children });
24165
+ };
24166
+ Provider.displayName = rootComponentName + "Provider";
24167
+ function useContext2(consumerName, scope) {
24168
+ var _a2;
24169
+ const Context = ((_a2 = scope == null ? void 0 : scope[scopeName]) == null ? void 0 : _a2[index2]) || BaseContext;
24170
+ const context = React.useContext(Context);
24171
+ if (context) return context;
24172
+ if (defaultContext !== void 0) return defaultContext;
24173
+ throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
24174
+ }
24175
+ return [Provider, useContext2];
24176
+ }
24177
+ const createScope = () => {
24178
+ const scopeContexts = defaultContexts.map((defaultContext) => {
24179
+ return React.createContext(defaultContext);
24180
+ });
24181
+ return function useScope(scope) {
24182
+ const contexts = (scope == null ? void 0 : scope[scopeName]) || scopeContexts;
24183
+ return React.useMemo(
24184
+ () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
24185
+ [scope, contexts]
24186
+ );
24187
+ };
24188
+ };
24189
+ createScope.scopeName = scopeName;
24190
+ return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)];
24191
+ }
24192
+ function composeContextScopes(...scopes) {
24193
+ const baseScope = scopes[0];
24194
+ if (scopes.length === 1) return baseScope;
24195
+ const createScope = () => {
24196
+ const scopeHooks = scopes.map((createScope2) => ({
24197
+ useScope: createScope2(),
24198
+ scopeName: createScope2.scopeName
24199
+ }));
24200
+ return function useComposedScopes(overrideScopes) {
24201
+ const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => {
24202
+ const scopeProps = useScope(overrideScopes);
24203
+ const currentScope = scopeProps[`__scope${scopeName}`];
24204
+ return { ...nextScopes2, ...currentScope };
24205
+ }, {});
24206
+ return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
24207
+ };
24208
+ };
24209
+ createScope.scopeName = baseScope.scopeName;
24210
+ return createScope;
24211
+ }
24141
24212
  // @__NO_SIDE_EFFECTS__
24142
24213
  function createSlot(ownerName) {
24143
24214
  const Slot2 = React.forwardRef((props, forwardedRef) => {
@@ -24300,74 +24371,6 @@ var Primitive = NODES.reduce((primitive, node2) => {
24300
24371
  function dispatchDiscreteCustomEvent(target, event) {
24301
24372
  if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));
24302
24373
  }
24303
- function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
24304
- return function handleEvent(event) {
24305
- originalEventHandler == null ? void 0 : originalEventHandler(event);
24306
- if (checkForDefaultPrevented === false || !event.defaultPrevented) {
24307
- return ourEventHandler == null ? void 0 : ourEventHandler(event);
24308
- }
24309
- };
24310
- }
24311
- function createContextScope(scopeName, createContextScopeDeps = []) {
24312
- let defaultContexts = [];
24313
- function createContext3(rootComponentName, defaultContext) {
24314
- const BaseContext = React.createContext(defaultContext);
24315
- BaseContext.displayName = rootComponentName + "Context";
24316
- const index2 = defaultContexts.length;
24317
- defaultContexts = [...defaultContexts, defaultContext];
24318
- const Provider = (props) => {
24319
- var _a2;
24320
- const { scope, children, ...context } = props;
24321
- const Context = ((_a2 = scope == null ? void 0 : scope[scopeName]) == null ? void 0 : _a2[index2]) || BaseContext;
24322
- const value = React.useMemo(() => context, Object.values(context));
24323
- return /* @__PURE__ */ jsx(Context.Provider, { value, children });
24324
- };
24325
- Provider.displayName = rootComponentName + "Provider";
24326
- function useContext2(consumerName, scope) {
24327
- var _a2;
24328
- const Context = ((_a2 = scope == null ? void 0 : scope[scopeName]) == null ? void 0 : _a2[index2]) || BaseContext;
24329
- const context = React.useContext(Context);
24330
- if (context) return context;
24331
- if (defaultContext !== void 0) return defaultContext;
24332
- throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
24333
- }
24334
- return [Provider, useContext2];
24335
- }
24336
- const createScope = () => {
24337
- const scopeContexts = defaultContexts.map((defaultContext) => {
24338
- return React.createContext(defaultContext);
24339
- });
24340
- return function useScope(scope) {
24341
- const contexts = (scope == null ? void 0 : scope[scopeName]) || scopeContexts;
24342
- return React.useMemo(
24343
- () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
24344
- [scope, contexts]
24345
- );
24346
- };
24347
- };
24348
- createScope.scopeName = scopeName;
24349
- return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)];
24350
- }
24351
- function composeContextScopes(...scopes) {
24352
- const baseScope = scopes[0];
24353
- if (scopes.length === 1) return baseScope;
24354
- const createScope = () => {
24355
- const scopeHooks = scopes.map((createScope2) => ({
24356
- useScope: createScope2(),
24357
- scopeName: createScope2.scopeName
24358
- }));
24359
- return function useComposedScopes(overrideScopes) {
24360
- const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => {
24361
- const scopeProps = useScope(overrideScopes);
24362
- const currentScope = scopeProps[`__scope${scopeName}`];
24363
- return { ...nextScopes2, ...currentScope };
24364
- }, {});
24365
- return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
24366
- };
24367
- };
24368
- createScope.scopeName = baseScope.scopeName;
24369
- return createScope;
24370
- }
24371
24374
  function useCallbackRef(callback) {
24372
24375
  const callbackRef = React.useRef(callback);
24373
24376
  React.useEffect(() => {
@@ -27383,7 +27386,7 @@ export {
27383
27386
  Anchor as A,
27384
27387
  Content as C,
27385
27388
  DismissableLayer as D,
27386
- OVERLAY_LAYER_INDEX as O,
27389
+ OVERLAY_LAYER as O,
27387
27390
  Primitive as P,
27388
27391
  Replayer as R,
27389
27392
  Slot as S,
@@ -27391,7 +27394,7 @@ export {
27391
27394
  cn as a,
27392
27395
  usePortalContainer as b,
27393
27396
  createLucideIcon as c,
27394
- OVERLAY_LAYER as d,
27397
+ OVERLAY_LAYER_INDEX as d,
27395
27398
  useControllableState as e,
27396
27399
  useId as f,
27397
27400
  composeEventHandlers as g,