issue-sticker 0.3.0 → 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,45 @@
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
+
34
+ ## 0.3.1 - 2026-08-17
35
+
36
+ ### 수정
37
+
38
+ - `issue-sticker/headless`의 타입 선언이 패키지에 함께 배포되지 않는 내부 모듈을 참조해, 이 경로를 타입과 함께 사용하는 프로젝트에서 타입 검사가 실패하던 문제를 수정했습니다. 공개 타입은 이제 패키지 안에서 자체 선언하며, 내부 식별자 컬럼은 공개 타입에 포함하지 않습니다.
39
+
40
+ ### 주의가 필요한 변경
41
+
42
+ - `issue-sticker/react.js`, `issue-sticker/headless.js`, `issue-sticker/headless/react.js` 경로를 제거했습니다. 문서에 없던 중복 경로이며 타입 정의도 연결되어 있지 않았습니다. 공개 경로는 `issue-sticker/react`, `issue-sticker/headless`, `issue-sticker/headless/react` 세 개입니다.
43
+
5
44
  ## 0.3.0 - 2026-08-12
6
45
 
7
46
  ### 추가
package/README.md CHANGED
@@ -1,122 +1,111 @@
1
- ## 개요
1
+ # issue-sticker
2
2
 
3
+ 이 문서는 이슈스티커 SDK(IssueSticker SDK for JavaScript) 사용 방법을 안내합니다.
3
4
 
4
- 이 README는 `issue-sticker` SDK의 설치 방법과 공개 API 표면만 다룹니다.
5
+ 사용자가 화면을 클릭해 이슈를 남기면 스크린샷과 콘솔·네트워크 로그, 브라우저 정보가 함께 기록됩니다.
5
6
 
7
+ ## 준비
6
8
 
7
- 프로젝트 생성, 허용 도메인 등록, API key 발급, 제품별 운영 방식은 아래 가이드에서 확인하세요.
9
+ ### 지원 환경
8
10
 
11
+ `issue-sticker/react`와 `issue-sticker/headless/react`는 React 어댑터입니다. 앱에 `react`, `react-dom` 18 또는 19가 설치되어 있어야 합니다.
9
12
 
10
- | 가이드 | 내용 |
11
- | ---------------------------------------------------------------- | --------------------------------------------- |
12
- | [시작 준비](https://issuesticker.com/guide/products/setup) | 프로젝트 생성, 허용 도메인 등록, API key 발급 |
13
- | [SDK Widget](https://issuesticker.com/guide/products/sdk) | 위젯 설치, 초기화, 운영 환경에서 켜는 조건 |
14
- | [SDK Headless](https://issuesticker.com/guide/products/headless) | Headless 클라이언트 전체 레퍼런스 |
15
- | [외부 연동](https://issuesticker.com/guide/integrations) | Jira·Notion 연결과 필드 동기화 |
16
- | [FAQ](https://issuesticker.com/guide/faq) | 권한, 도메인, key, 필드 동기화 문제 해결 |
13
+ `issue-sticker/headless`는 React를 요구하지 않습니다. 프레임워크에 상관없이 사용할 수 있습니다.
17
14
 
15
+ ### API key 발급
16
+
17
+ [관리자 콘솔](https://app.issuesticker.com)에서 프로젝트를 만들고, 위젯을 노출할 도메인을 허용 도메인으로 등록한 뒤 SDK API key를 발급받아야 합니다. SDK는 API key와 등록된 도메인을 기준으로 초기화합니다. 둘 중 하나라도 빠지면 위젯이 나타나지 않습니다.
18
+
19
+ 발급 절차는 [시작 준비](https://issuesticker.com/guide/products/setup)에 있습니다.
18
20
 
19
21
  ## 설치
20
22
 
23
+ 앱에 `issue-sticker` 패키지를 설치합니다.
21
24
 
22
25
  ```bash
23
26
  npm install issue-sticker
24
27
  ```
25
28
 
26
29
 
27
- `issue-sticker/react`와 `issue-sticker/headless/react`를 사용하려면 연동하는 앱에 `react`, `react-dom` 18 또는 19가 설치되어 있어야 합니다.
28
-
29
-
30
- ## 엔트리포인트
31
-
30
+ | 경로 | 용도 |
31
+ | ------------------------------ | ------------------------------------------------------- |
32
+ | `issue-sticker/react` | 위젯 UI(툴바, 이슈 폼, 마커)를 마운트하는 React 어댑터 |
33
+ | `issue-sticker/headless` | UI 없이 이슈 흐름을 구독하고 제어하는 컨트롤러 API |
34
+ | `issue-sticker/headless/react` | Headless UI를 직접 만들 때 사용하는 최소 React 헬퍼 |
32
35
 
33
- | 경로 | 용도 |
34
- | ------------------------------ | ----------------------------------------------------- |
35
- | `issue-sticker/react` | 위젯 UI(FAB, 이슈 폼, 마커)를 마운트하는 React 어댑터 |
36
- | `issue-sticker/headless` | UI 없이 이슈 흐름을 구독하고 제어하는 컨트롤러 API |
37
- | `issue-sticker/headless/react` | Headless UI를 직접 만들 때 사용하는 최소 React 헬퍼 |
38
-
39
-
40
- 패키지 루트(`issue-sticker`)와 `issue-sticker/runtime`은 공개 엔트리포인트가 아닙니다.
41
-
42
-
43
- ## 빠른 시작 — 위젯
36
+ ## 초기화
44
37
 
38
+ 이슈스티커 초기화를 위한 컴포넌트를 생성합니다.
45
39
 
46
40
  ```tsx
47
41
  'use client'; // Next.js App Router
48
42
 
49
43
  import { useIssueSticker } from 'issue-sticker/react';
50
44
 
51
- function App() {
45
+ export function IssueStickerWidget() {
46
+ const { user } = useAuth(); // 앱의 사용자 정보
47
+
52
48
  useIssueSticker({
53
49
  apiKey: process.env.NEXT_PUBLIC_ISSUE_STICKER_SDK_KEY!,
54
- user: { id: 'user-123', name: 'Hong Gildong' }, // 선택: 작성자 식별
55
- enabled: isStaging || isAdmin, // 선택: 노출 조건 (기본 true)
50
+ user: user ? { id: user.id, name: user.name } : undefined, // 사용자 식별
51
+ enabled: isStaging || user?.role === 'admin', // 노출 조건
56
52
  });
57
53
 
58
- return <div>My Application</div>;
54
+ return null;
59
55
  }
60
56
  ```
61
57
 
58
+ ### 사용자 식별
62
59
 
63
- 앱 최상위에서 한 번만 호출하세요.
64
- 훅은 마운트 시 SDK를 초기화하고, 언마운트 시 정리합니다.
65
-
66
-
67
- 오른쪽 아래에 표시되는 FAB는 드래그로 위치를 옮길 수 있습니다.
68
- FAB에서는 이슈 생성, 화면 녹화, 마커 표시 토글, 마커 기준 전환(생성 위치 기준 / 대상 요소 기준), 미표시 스티커 확인, 가이드 다시 보기, 멤버 로그인·로그아웃을 사용할 수 있습니다.
69
-
70
-
71
- 이슈 생성 모드와 화면 녹화는 동시에 켤 수 없습니다.
72
-
73
-
74
- ## 빠른 시작 — Headless
75
-
76
-
77
- ```typescript
78
- import { createIssueStickerHeadless } from 'issue-sticker/headless';
79
-
80
- const client = createIssueStickerHeadless({
81
- apiKey: 'YOUR_API_KEY',
82
- user: { id: 'user-123', name: 'Hong Gildong' },
83
- });
84
-
85
- client.subscribe((snapshot) => renderIssueUI(snapshot));
86
-
87
- await client.initialize();
88
- await client.refreshIssues();
89
- ```
90
-
91
-
92
- 현재 상태는 읽기 전용 snapshot으로 제공되며, 이슈 생성은 `startIssueCapture()`로 시작합니다.
93
-
60
+ | 모드 | 설정 | 이슈 조회 범위 | 수정·삭제 |
61
+ | ----------- | ------------------- | ------------------------------------------------- | ------------------------- |
62
+ | 익명 | `user` 없이 초기화 | 조회 없음 (이번 세션에서 만든 이슈만 화면에 표시) | 불가 (자기가 만든 이슈도) |
63
+ | 사용자 식별 | `user` 전달 | 해당 사용자가 만든 이슈 | 자기가 만든 이슈만 |
64
+ | 멤버 | 툴바의 "멤버 로그인" | 프로젝트의 모든 이슈 | 프로젝트의 모든 이슈 |
94
65
 
95
- 전체 메서드, snapshot 타입, 동적 필드 descriptor, 에러 코드는 [Headless 가이드](https://issuesticker.com/guide/products/headless)를 참고하세요.
66
+ ### 위젯 기능
96
67
 
68
+ 초기화를 마치면 화면 모서리(기본 오른쪽 아래)에 툴바가 표시됩니다. 툴바는 아래 기능을 제공하며, 드래그로 위치를 옮길 수 있습니다.
69
+ - 이슈 생성
70
+ - 화면 녹화
71
+ - 마커 표시 토글
72
+ - 마커 기준 전환(생성 위치 기준 / 캡처 대상 요소 기준)
73
+ - 미표시 스티커 확인
74
+ - 가이드 다시 보기
75
+ - 멤버 로그인·로그아웃
97
76
 
98
- ## API — `issue-sticker/react`
99
77
 
78
+ ### 단축키
100
79
 
101
- ### `useIssueSticker(config)`
80
+ | 기능 | macOS | Windows·Linux |
81
+ | ------------------- | ----------- | ------------- |
82
+ | 이슈 생성 모드 토글 | `⌘⌥1` `⌃⌥1` | `Ctrl+Alt+1` |
83
+ | 화면 녹화 시작/중지 | `⌘⌥2` `⌃⌥2` | `Ctrl+Alt+2` |
84
+ | 이슈 생성 모드 종료 | `Esc` | `Esc` |
102
85
 
103
86
 
104
- React 훅입니다.
105
- `apiKey` 또는 `enabled`가 바뀌면 위젯을 다시 초기화하고, `user`가 바뀌면 위젯은 유지한 채 설정만 갱신합니다.
106
87
 
88
+ ### 설정
107
89
 
108
- ### `IssueSticker`
90
+ | 속성 | 타입 | 필수 | 설명 |
91
+ | ----------------------- | ------------------------------------ | ---- | ------------------------------------------------------------------------------------------- |
92
+ | `apiKey` | `string` | O | 관리자 콘솔에서 발급한 SDK API key |
93
+ | `user` | `SDKUser` | X | 앱의 사용자 정보. 제공하면 작성자가 기록되고 자기 이슈를 조회·수정·삭제할 수 있습니다 |
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 }`). 사용자가 드래그해 저장한 위치가 있으면 그쪽이 우선합니다 |
96
+ | `screenshotCompression` | `{ useWebWorker?: boolean }` | X | 스크린샷 압축을 Web Worker에서 할지 여부 |
97
+ | `enabled` | `boolean` | X | 위젯 활성화 여부. 기본값 `true`. `useIssueSticker` 전용 |
109
98
 
99
+ ### 툴바 셀렉터
110
100
 
111
- 훅을 사용할 수 없는 환경에서 직접 제어할 때 사용합니다.
101
+ 호스트 페이지에서 툴바 위치를 알아야 한다면 `[data-issue-sticker-toolbar]`로 찾으세요.
102
+ 이 속성은 공개 계약이며 버전 간 유지됩니다. 내부 클래스명은 계약이 아니므로 의존하지 마세요.
112
103
 
104
+ 컴포넌트가 마운트되면 위젯을 초기화하고, 언마운트되면 정리합니다. `apiKey`나 `enabled`가 바뀌면 위젯을 다시 초기화하고, `user`만 바뀌면 위젯을 유지한 채 설정만 갱신합니다.
113
105
 
114
- | 메서드 | 설명 |
115
- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
116
- | `init(config: SDKConfig)` | SDK를 초기화하고 위젯을 마운트합니다. 이미 초기화된 상태라면 설정을 갱신하고 위젯을 다시 마운트합니다 |
117
- | `updateConfig(config: SDKConfig)` | 위젯을 유지한 채 설정만 교체합니다. 초기화 전이면 `init`과 동일하게 동작합니다 |
118
- | `destroy()` | 위젯을 언마운트하고 SDK를 종료합니다 |
106
+ ### 훅 없이 초기화하기
119
107
 
108
+ React 훅을 사용할 수 없는 경우 `IssueSticker`로 직접 제어합니다.
120
109
 
121
110
  ```typescript
122
111
  import { IssueSticker } from 'issue-sticker/react';
@@ -124,92 +113,83 @@ import { IssueSticker } from 'issue-sticker/react';
124
113
  IssueSticker.init({ apiKey: 'YOUR_API_KEY' });
125
114
  ```
126
115
 
116
+ #### init(config)
127
117
 
128
- 이슈 생성·제출 흐름을 연동하는 앱에서 직접 구독하거나 제어해야 한다면 Headless 클라이언트를 사용하세요.
129
- 상태는 `subscribe()`로 구독하고, 캡처와 제출은 메서드로 제어할 수 있습니다.
130
-
131
-
132
- 자세한 내용은 [Headless 가이드](https://issuesticker.com/guide/products/headless)를 참고하세요.
133
-
134
-
135
- ### `SDKConfig`
136
-
137
-
138
- | 속성 | 타입 | 필수 | 설명 |
139
- | ----------------------- | ------------------------------------ | ---- | ------------------------------------------------------------------------------------------- |
140
- | `apiKey` | `string` | Yes | [관리자 콘솔](https://app.issuesticker.com)에서 발급한 SDK API key |
141
- | `user` | `SDKUser` | No | 연동하는 앱의 사용자 정보. 전달하면 작성자가 기록되고 자신이 만든 이슈를 조회·수정·삭제할 수 있습니다. 전달하지 않으면(익명) 이번 세션에서 만든 이슈라도 수정·삭제할 수 없습니다 |
142
- | `onboarding` | `'off' \| 'onMount' \| 'onFABClick'` | No | 온보딩 가이드 트리거 (기본 `'onMount'`). `'off'`면 "가이드 다시 보기"도 숨깁니다 |
143
- | `screenshotCompression` | `{ useWebWorker?: boolean }` | No | 스크린샷 압축을 Web Worker에서 수행할지 여부 |
144
- | `enabled` | `boolean` | No | 위젯 활성화 여부 (기본 `true`). `useIssueSticker` 전용 옵션 |
145
-
118
+ ```typescript
119
+ init(config: SDKConfig): void
120
+ ```
146
121
 
147
- `SDKUser`는 `id`, `name` 두 필드가 모두 필요합니다.
122
+ SDK를 초기화하고 위젯을 마운트합니다. 이미 초기화된 상태에서 다시 호출하면 설정을 갱신하고 위젯을 다시 마운트합니다.
148
123
 
124
+ #### updateConfig(config)
149
125
 
150
- ### 키보드 단축키
126
+ ```typescript
127
+ updateConfig(config: SDKConfig): void
128
+ ```
151
129
 
130
+ 위젯을 유지한 채 설정만 교체합니다. 초기화 전에 호출하면 `init`과 같게 동작합니다.
152
131
 
153
- | 기능 | macOS | Windows·Linux |
154
- | ------------------- | ----------- | ------------- |
155
- | 이슈 생성 모드 토글 | `⌘⌥1` `⌃⌥1` | `Ctrl+Alt+1` |
156
- | 화면 녹화 시작/중지 | `⌘⌥2` `⌃⌥2` | `Ctrl+Alt+2` |
157
- | 이슈 생성 모드 종료 | `Esc` | `Esc` |
132
+ #### destroy()
158
133
 
134
+ ```typescript
135
+ destroy(): void
136
+ ```
159
137
 
160
- ## 이슈에 담기는 정보
138
+ 위젯을 언마운트하고 SDK를 종료합니다.
161
139
 
140
+ ## Headless 클라이언트
162
141
 
163
- - 스크린샷 — 화면 전체와 클릭한 요소 영역 (클릭 직후 백그라운드에서 캡처)
164
- - 화면 녹화 영상 — 녹화 모드로 생성한 경우
165
- - 네트워크·콘솔 로그 — 각각 최근 50개, IssueSticker 자체 트래픽은 제외
166
- - 브라우저·OS 정보, 발생 URL, 클릭한 요소의 CSS 선택자와 XPath
167
- - 세션 리플레이 — 워크스페이스에서 사용할 수 있는 경우, 이슈 폼에서 첨부 여부 선택
142
+ 이슈 리포팅 버튼과 폼을 제품 디자인에 맞춰 직접 만들 때 사용합니다. 클라이언트를 만들고, 상태를 구독하고, 초기화하면 준비가 끝납니다.
168
143
 
144
+ ```typescript
145
+ import { createIssueStickerHeadless } from 'issue-sticker/headless';
169
146
 
170
- 화면의 마커를 클릭하면 이슈 팝오버에서 제목과 설명을 수정하고, 이슈를 삭제하고, 코딩 에이전트에 붙여 넣을 LLM 프롬프트를 복사할 수 있습니다.
171
- 연동된 Jira·Notion 정보는 멤버로 로그인했을 때 함께 표시됩니다.
147
+ const client = createIssueStickerHeadless({
148
+ apiKey: 'YOUR_API_KEY',
149
+ user: { id: currentUser.id, name: currentUser.name },
150
+ });
172
151
 
152
+ const unsubscribe = client.subscribe((snapshot) => renderYourUI(snapshot));
173
153
 
174
- 화면 녹화는 브라우저 `getDisplayMedia` API를 사용합니다.
175
- 녹화 중 페이지를 새로고침하거나 다른 페이지로 이동하면 녹화가 중단되고 데이터가 유실됩니다.
154
+ await client.initialize();
155
+ await client.refreshIssues();
176
156
 
157
+ captureButton.onclick = () => client.startIssueCapture();
158
+ ```
177
159
 
178
- 페이지 이동 중에도 끊기지 않는 녹화가 필요하면 [Chrome Extension](https://issuesticker.com/guide/products/extension)을 사용하세요.
160
+ 클라이언트는 현재 상태를 읽기 전용 snapshot으로 넘겨줍니다. 메서드 전체, phase와 snapshot 모델, 동적 필드 descriptor, 에러 코드는 [Headless 가이드](https://issuesticker.com/guide/products/headless)에 있습니다.
179
161
 
180
162
 
181
- ## 사용자 식별
163
+ ## 수집하는 정보
182
164
 
165
+ `init()`이 끝난 시점부터 위젯이 호스트 페이지의 `fetch`, `XMLHttpRequest`, `console`을 감싸 네트워크 요청과 콘솔 출력을 메모리에 수집합니다. 이슈를 만들 때 그 시점의 최근 기록이 함께 첨부됩니다. 사용자가 이슈를 만들지 않으면 아무것도 전송되지 않습니다.
183
166
 
184
- | 모드 | 설정 | 이슈 조회 범위 | 수정·삭제 |
185
- | ----------- | ------------------- | ----------------------------------------------------- | ----------------------------------- |
186
- | 익명 | `user` 없이 초기화 | 조회 없음 (이번 세션에서 생성한 이슈만 화면에 표시됨) | 불가 (자신이 만든 이슈도 불가) |
187
- | 사용자 식별 | `user` 전달 | 해당 사용자가 생성한 이슈 | 자신이 만든 이슈만 가능 |
188
- | 멤버 | FAB의 "멤버 로그인" | 프로젝트의 모든 이슈 | 프로젝트의 모든 이슈 가능 |
167
+ 호스트 앱에 영향을 주지 않도록 다음을 지킵니다.
189
168
 
169
+ - 다른 라이브러리가 이미 감싼 `fetch`·`XMLHttpRequest` 래퍼를 보존합니다. `destroy()`로 정리할 때도 타 라이브러리 래퍼를 지우지 않습니다.
170
+ - 스트리밍 응답의 본문은 캡처하지 않습니다. 응답은 먼저 호출자에게 돌려주고 본문은 나중에 채웁니다.
171
+ - 계측 내부에서 난 예외는 호출자에게 전파하지 않습니다. 예외가 반복되면 수집기가 스스로 패치를 걷어냅니다.
172
+ - 자격증명으로 보이는 값, 국내 식별번호, 결제 관련 필드는 저장 전에 제거합니다.
190
173
 
191
174
  ## 문제 해결
192
175
 
176
+ - 위젯이 보이지 않으면 프로젝트에 등록한 허용 도메인과 현재 `window.location.origin`이 같은지 확인하세요.
177
+ - 특정 사용자에게만 노출했다면 `enabled` 조건이 실제 로그인 상태와 맞는지 확인하세요.
178
+ - `Invalid hook call` 또는 `Cannot read properties of null (reading 'useRef')`는 대부분 React가 두 벌 로드된 경우입니다. `npm ls react react-dom`으로 확인하고, Vite는 `resolve.dedupe: ['react', 'react-dom']`을, Webpack·Next.js는 alias로 React 경로를 하나로 맞추세요.
179
+ - `Missing "./react" specifier in "issue-sticker" package`는 설치된 패키지가 오래된 경우입니다. `node_modules`와 lockfile을 지우고 다시 설치하세요.
193
180
 
194
- `Invalid hook call` 또는 `Cannot read properties of null (reading 'useRef')` 오류는 대부분 React 인스턴스가 중복으로 로드될 때 발생합니다.
195
-
196
-
197
- `npm ls react react-dom`으로 중복 설치 여부를 확인하세요.
198
- Vite는 `resolve.dedupe: ['react', 'react-dom']`을 설정하고, Webpack·Next.js는 alias로 React 경로를 하나로 맞추면 됩니다.
199
-
200
-
201
- `Missing "./react" specifier in "issue-sticker" package` 오류는 설치된 패키지가 오래된 경우에 발생합니다.
202
- `node_modules`와 lockfile을 갱신한 뒤 다시 설치하세요.
203
-
204
-
205
- 그 밖의 문제는 [FAQ](https://issuesticker.com/guide/faq)를 참고하세요.
181
+ ## 더 보기
206
182
 
183
+ | 가이드 | 내용 |
184
+ | ---------------------------------------------------------------- | --------------------------------------------- |
185
+ | [시작 준비](https://issuesticker.com/guide/products/setup) | 프로젝트 생성, 허용 도메인 등록, API key 발급 |
186
+ | [SDK Widget](https://issuesticker.com/guide/products/sdk) | 위젯 설치, 초기화, 운영 환경에서 켜는 조건 |
187
+ | [SDK Headless](https://issuesticker.com/guide/products/headless) | 메서드, phase와 snapshot, 에러 코드 레퍼런스 |
188
+ | [외부 연동](https://issuesticker.com/guide/integrations) | Jira·Notion 연결과 필드 동기화 |
189
+ | [FAQ](https://issuesticker.com/guide/faq) | 권한, 도메인, key, 필드 동기화 문제 해결 |
207
190
 
208
191
  ## 라이선스
209
192
 
210
-
211
193
  Copyright (c) 2026 maana. All rights reserved.
212
194
 
213
-
214
- 이 소프트웨어는 maana의 독점 소유물입니다.
215
- 자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요.
195
+ 이 소프트웨어는 maana의 독점 소유물입니다. 자세한 내용은 [LICENSE](./LICENSE)를 참조하세요.
@@ -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,