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 +39 -0
- package/README.md +111 -131
- package/dist/{createLucideIcon-CmSNiW63.js → createLucideIcon-C0Ldhp96.js} +90 -87
- package/dist/{externalUserPayload-hJjuUx6q.js → externalUserPayload-BVNl_35M.js} +2659 -1315
- package/dist/issue-sticker-headless-react.js +129 -72
- package/dist/issue-sticker-headless.js +97 -78
- package/dist/issue-sticker-react.js +2534 -2071
- package/dist/{tooltip-DciWyUWs.js → tooltip-CEc_RGfI.js} +1 -1
- package/headless.d.ts +142 -11
- package/package.json +1 -4
- package/react.d.ts +9 -1
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
|
-
|
|
5
|
+
사용자가 화면을 클릭해 이슈를 남기면 스크린샷과 콘솔·네트워크 로그, 브라우저 정보가 함께 기록됩니다.
|
|
5
6
|
|
|
7
|
+
## 준비
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
55
|
-
enabled: isStaging ||
|
|
50
|
+
user: user ? { id: user.id, name: user.name } : undefined, // 사용자 식별
|
|
51
|
+
enabled: isStaging || user?.role === 'admin', // 노출 조건
|
|
56
52
|
});
|
|
57
53
|
|
|
58
|
-
return
|
|
54
|
+
return null;
|
|
59
55
|
}
|
|
60
56
|
```
|
|
61
57
|
|
|
58
|
+
### 사용자 식별
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
66
|
+
### 위젯 기능
|
|
96
67
|
|
|
68
|
+
초기화를 마치면 화면 모서리(기본 오른쪽 아래)에 툴바가 표시됩니다. 툴바는 아래 기능을 제공하며, 드래그로 위치를 옮길 수 있습니다.
|
|
69
|
+
- 이슈 생성
|
|
70
|
+
- 화면 녹화
|
|
71
|
+
- 마커 표시 토글
|
|
72
|
+
- 마커 기준 전환(생성 위치 기준 / 캡처 대상 요소 기준)
|
|
73
|
+
- 미표시 스티커 확인
|
|
74
|
+
- 가이드 다시 보기
|
|
75
|
+
- 멤버 로그인·로그아웃
|
|
97
76
|
|
|
98
|
-
## API — `issue-sticker/react`
|
|
99
77
|
|
|
78
|
+
### 단축키
|
|
100
79
|
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
154
|
+
await client.initialize();
|
|
155
|
+
await client.refreshIssues();
|
|
176
156
|
|
|
157
|
+
captureButton.onclick = () => client.startIssueCapture();
|
|
158
|
+
```
|
|
177
159
|
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
21616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27397
|
+
OVERLAY_LAYER_INDEX as d,
|
|
27395
27398
|
useControllableState as e,
|
|
27396
27399
|
useId as f,
|
|
27397
27400
|
composeEventHandlers as g,
|