issue-sticker 0.1.1 → 0.2.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 +44 -0
- package/README.md +82 -415
- package/dist/NetworkCollector-BlY2qUKF.js +12278 -0
- package/dist/createLucideIcon-B457EluY.js +27415 -0
- package/dist/{index-DN49yZjv.js → index-D6SaRFm4.js} +98 -1
- package/dist/issue-sticker-headless-react.js +764 -0
- package/dist/issue-sticker-headless.js +1914 -0
- package/dist/issue-sticker-react.js +17399 -46756
- package/dist/tooltip-BkQcdrki.js +512 -0
- package/headless-react.d.ts +12 -0
- package/headless.d.ts +172 -0
- package/package.json +26 -26
- package/react.d.ts +29 -2
- package/dist/index.d.ts +0 -37
- package/dist/issue-sticker-runtime.js +0 -37
- package/dist/issue-sticker.js +0 -2105
- package/dist/issue-sticker.umd.js +0 -1
- package/dist/sdkEventListeners-B7idjnFi.js +0 -498
- package/runtime.d.ts +0 -11
|
@@ -0,0 +1,1914 @@
|
|
|
1
|
+
import { u as uploadScreenshotDataUrls, a as uploadSignedMediaWithApi, d as directUploadWithFallback, n as normalizeExposedJiraFields, b as normalizeJiraFieldId, g as getElementPosition, c as captureWithModernScreenshot, e as buildJiraCreateFields, f as buildNotionCreateFields, s as syncExternalIssueIntegrations, h as devLogger, C as ConsoleCollector, N as NetworkCollector, i as filterExcludeServiceRelatedLogs, j as filterExcludeServiceRelatedConsoleLogs, k as compactNetworkLogsForTransport, l as sanitizeString, S as ScreenRecordingController, m as SessionReplayCaptureController, p as preloadModernScreenshot } from "./NetworkCollector-BlY2qUKF.js";
|
|
2
|
+
import "react/jsx-runtime";
|
|
3
|
+
import "react";
|
|
4
|
+
import "./createLucideIcon-B457EluY.js";
|
|
5
|
+
class IssueStickerHeadlessError extends Error {
|
|
6
|
+
constructor(code, message, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
this.name = "IssueStickerHeadlessError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function readApiBaseUrlFromEnv() {
|
|
14
|
+
if (typeof process !== "undefined" && process.env && Object.prototype.hasOwnProperty.call(process.env, "VITE_API_BASE_URL")) {
|
|
15
|
+
const value = process.env.VITE_API_BASE_URL;
|
|
16
|
+
return typeof value === "string" && value.trim() && value.trim() !== "undefined" ? value.trim() : void 0;
|
|
17
|
+
}
|
|
18
|
+
const viteValue = "https://api.issuesticker.com";
|
|
19
|
+
return typeof viteValue === "string" && viteValue.trim() && viteValue.trim() !== "undefined" ? viteValue.trim() : void 0;
|
|
20
|
+
}
|
|
21
|
+
function getApiBaseUrl() {
|
|
22
|
+
const baseUrl = readApiBaseUrlFromEnv();
|
|
23
|
+
if (!baseUrl) {
|
|
24
|
+
throw new IssueStickerHeadlessError(
|
|
25
|
+
"NETWORK_ERROR",
|
|
26
|
+
"API 서버 주소(VITE_API_BASE_URL)가 설정되지 않았습니다."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!/^https?:\/\//.test(baseUrl)) {
|
|
30
|
+
throw new IssueStickerHeadlessError(
|
|
31
|
+
"NETWORK_ERROR",
|
|
32
|
+
`API 서버 주소(VITE_API_BASE_URL)가 올바르지 않습니다: ${baseUrl}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return baseUrl.replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
function createRequestId() {
|
|
38
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
39
|
+
return crypto.randomUUID();
|
|
40
|
+
}
|
|
41
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
42
|
+
}
|
|
43
|
+
function extractErrorMessage(error, fallback) {
|
|
44
|
+
if (!error) return fallback || "요청에 실패했습니다.";
|
|
45
|
+
if (typeof error === "string") return error;
|
|
46
|
+
return error.message || error.code || fallback || "요청에 실패했습니다.";
|
|
47
|
+
}
|
|
48
|
+
class HeadlessTransport {
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
}
|
|
52
|
+
async request(endpoint, options = {}) {
|
|
53
|
+
const response = await this.requestResponse(endpoint, options);
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
56
|
+
async requestResponse(endpoint, options = {}) {
|
|
57
|
+
if (!endpoint.startsWith("/") || endpoint.startsWith("//")) {
|
|
58
|
+
throw new IssueStickerHeadlessError(
|
|
59
|
+
"NETWORK_ERROR",
|
|
60
|
+
`API endpoint는 상대 경로여야 합니다: ${endpoint}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const headers = {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"x-client-info": "issue-sticker-sdk"
|
|
66
|
+
};
|
|
67
|
+
if (!options.skipAuth) {
|
|
68
|
+
const authHeader = await this.options.resolveAuthorizationHeader();
|
|
69
|
+
if (authHeader) headers.Authorization = authHeader;
|
|
70
|
+
}
|
|
71
|
+
const execute = (nextHeaders) => fetch(`${getApiBaseUrl()}${endpoint}`, {
|
|
72
|
+
method: options.method ?? "GET",
|
|
73
|
+
headers: nextHeaders,
|
|
74
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
75
|
+
});
|
|
76
|
+
let response = await execute(headers);
|
|
77
|
+
if (!options.skipAuth && response.status === 401 && this.options.getAuthMode() === "member") {
|
|
78
|
+
const retryHeaders = { ...headers };
|
|
79
|
+
const authHeader = await this.options.resolveAuthorizationHeader(true).catch(() => null);
|
|
80
|
+
if (authHeader) {
|
|
81
|
+
retryHeaders.Authorization = authHeader;
|
|
82
|
+
response = await execute(retryHeaders);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (response.status === 204) {
|
|
86
|
+
return { success: true };
|
|
87
|
+
}
|
|
88
|
+
const json = await response.json().catch(() => null);
|
|
89
|
+
if (!response.ok || !(json == null ? void 0 : json.success)) {
|
|
90
|
+
throw new IssueStickerHeadlessError(
|
|
91
|
+
response.status === 401 ? "MEMBER_AUTH_REQUIRED" : "NETWORK_ERROR",
|
|
92
|
+
extractErrorMessage(json == null ? void 0 : json.error, (json == null ? void 0 : json.message) || `요청 실패: ${response.status}`),
|
|
93
|
+
json
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return json;
|
|
97
|
+
}
|
|
98
|
+
async uploadScreenshotPreviews(previews) {
|
|
99
|
+
const uploads = await uploadScreenshotDataUrls(
|
|
100
|
+
[...previews],
|
|
101
|
+
(file, mediaType) => this.uploadSignedMedia(file, mediaType)
|
|
102
|
+
);
|
|
103
|
+
return uploads.filter((upload) => Boolean(upload));
|
|
104
|
+
}
|
|
105
|
+
async uploadSignedMedia(file, mediaType) {
|
|
106
|
+
try {
|
|
107
|
+
return await uploadSignedMediaWithApi(
|
|
108
|
+
{
|
|
109
|
+
post: (endpoint, body) => this.request(endpoint, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body
|
|
112
|
+
}),
|
|
113
|
+
directUpload: directUploadWithFallback
|
|
114
|
+
},
|
|
115
|
+
file,
|
|
116
|
+
mediaType
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof IssueStickerHeadlessError) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
throw new IssueStickerHeadlessError(
|
|
123
|
+
"CAPTURE_FAILED",
|
|
124
|
+
error instanceof Error ? error.message : "미디어 업로드에 실패했습니다.",
|
|
125
|
+
error
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const BRIDGE_READY = "ISSUE_STICKER_SDK_AUTH_BRIDGE_READY";
|
|
131
|
+
const BRIDGE_REQUEST = "ISSUE_STICKER_SDK_AUTH_REQUEST";
|
|
132
|
+
const BRIDGE_RESPONSE = "ISSUE_STICKER_SDK_AUTH_RESPONSE";
|
|
133
|
+
const BRIDGE_LOGIN_SUCCESS = "ISSUE_STICKER_SDK_AUTH_LOGIN_SUCCESS";
|
|
134
|
+
const BRIDGE_LOGIN_ERROR = "ISSUE_STICKER_SDK_AUTH_LOGIN_ERROR";
|
|
135
|
+
const TOKEN_REFRESH_MARGIN_MS = 6e4;
|
|
136
|
+
const BRIDGE_TIMEOUT_MS = 15e3;
|
|
137
|
+
function buildBridgeUrl(bridgeUrl, params) {
|
|
138
|
+
const url = new URL(bridgeUrl);
|
|
139
|
+
url.searchParams.set("apiKey", params.apiKey);
|
|
140
|
+
if (params.tenantId) url.searchParams.set("tenantId", params.tenantId);
|
|
141
|
+
if (params.projectId) url.searchParams.set("projectId", params.projectId);
|
|
142
|
+
if (typeof window !== "undefined") {
|
|
143
|
+
url.searchParams.set("parentOrigin", window.location.origin);
|
|
144
|
+
url.searchParams.set("hostOrigin", window.location.origin);
|
|
145
|
+
}
|
|
146
|
+
if (params.interactive) {
|
|
147
|
+
url.searchParams.set("interactive", "1");
|
|
148
|
+
}
|
|
149
|
+
return url.toString();
|
|
150
|
+
}
|
|
151
|
+
class HeadlessAuthBridge {
|
|
152
|
+
constructor(options) {
|
|
153
|
+
this.options = options;
|
|
154
|
+
this.bridgeReady = false;
|
|
155
|
+
this.bridgeOrigin = null;
|
|
156
|
+
this.hiddenBridgeFrame = null;
|
|
157
|
+
this.loginBridgeFrame = null;
|
|
158
|
+
this.bridgeWaiters = [];
|
|
159
|
+
this.pendingBridgeRequests = /* @__PURE__ */ new Map();
|
|
160
|
+
this.memberToken = null;
|
|
161
|
+
this.memberTokenPromise = null;
|
|
162
|
+
this.authBridgeUrl = null;
|
|
163
|
+
this.handleBridgeMessage = this.handleBridgeMessage.bind(this);
|
|
164
|
+
}
|
|
165
|
+
get canUseBridge() {
|
|
166
|
+
return Boolean(this.authBridgeUrl);
|
|
167
|
+
}
|
|
168
|
+
setAuthBridgeUrl(authBridgeUrl) {
|
|
169
|
+
if (this.authBridgeUrl === authBridgeUrl) return;
|
|
170
|
+
this.authBridgeUrl = authBridgeUrl;
|
|
171
|
+
this.bridgeOrigin = authBridgeUrl ? new URL(authBridgeUrl).origin : null;
|
|
172
|
+
this.bridgeReady = false;
|
|
173
|
+
}
|
|
174
|
+
clearMemberToken() {
|
|
175
|
+
this.memberToken = null;
|
|
176
|
+
}
|
|
177
|
+
mountHiddenBridgeFrame() {
|
|
178
|
+
if (!this.authBridgeUrl || this.hiddenBridgeFrame || typeof document === "undefined" || typeof window === "undefined") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
window.addEventListener("message", this.handleBridgeMessage);
|
|
182
|
+
const frame = document.createElement("iframe");
|
|
183
|
+
frame.title = "IssueSticker auth bridge";
|
|
184
|
+
frame.src = buildBridgeUrl(this.authBridgeUrl, {
|
|
185
|
+
apiKey: this.options.apiKey,
|
|
186
|
+
tenantId: this.options.getTenantId(),
|
|
187
|
+
projectId: this.options.getProjectId()
|
|
188
|
+
});
|
|
189
|
+
frame.style.display = "none";
|
|
190
|
+
frame.setAttribute("aria-hidden", "true");
|
|
191
|
+
frame.dataset.issueStickerHeadlessAuth = "true";
|
|
192
|
+
document.body.appendChild(frame);
|
|
193
|
+
this.hiddenBridgeFrame = frame;
|
|
194
|
+
}
|
|
195
|
+
postLogout() {
|
|
196
|
+
this.postBridgeMessage({
|
|
197
|
+
type: BRIDGE_REQUEST,
|
|
198
|
+
requestId: createRequestId(),
|
|
199
|
+
command: "logout"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
destroy() {
|
|
203
|
+
var _a, _b;
|
|
204
|
+
this.clearMemberToken();
|
|
205
|
+
this.memberTokenPromise = null;
|
|
206
|
+
this.bridgeReady = false;
|
|
207
|
+
this.bridgeWaiters = [];
|
|
208
|
+
(_a = this.hiddenBridgeFrame) == null ? void 0 : _a.remove();
|
|
209
|
+
(_b = this.loginBridgeFrame) == null ? void 0 : _b.remove();
|
|
210
|
+
this.hiddenBridgeFrame = null;
|
|
211
|
+
this.loginBridgeFrame = null;
|
|
212
|
+
if (typeof window !== "undefined") {
|
|
213
|
+
window.removeEventListener("message", this.handleBridgeMessage);
|
|
214
|
+
}
|
|
215
|
+
this.pendingBridgeRequests.forEach((request) => {
|
|
216
|
+
if (typeof window !== "undefined") {
|
|
217
|
+
window.clearTimeout(request.timeoutId);
|
|
218
|
+
}
|
|
219
|
+
request.reject(new Error("Headless auth bridge destroyed"));
|
|
220
|
+
});
|
|
221
|
+
this.pendingBridgeRequests.clear();
|
|
222
|
+
}
|
|
223
|
+
async getValidMemberToken(options = {}) {
|
|
224
|
+
const cached = this.memberToken;
|
|
225
|
+
if (!options.forceRefresh && cached && cached.expiresAtMs - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
|
|
226
|
+
return cached.accessToken;
|
|
227
|
+
}
|
|
228
|
+
if (!this.memberTokenPromise) {
|
|
229
|
+
this.memberTokenPromise = this.requestBridgeMemberToken(Boolean(options.forceRefresh)).then((result) => {
|
|
230
|
+
this.applyToken(result);
|
|
231
|
+
return result.accessToken;
|
|
232
|
+
}).catch(() => null).finally(() => {
|
|
233
|
+
this.memberTokenPromise = null;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return this.memberTokenPromise;
|
|
237
|
+
}
|
|
238
|
+
async openInteractiveLoginFrame() {
|
|
239
|
+
if (!this.authBridgeUrl) {
|
|
240
|
+
throw new IssueStickerHeadlessError(
|
|
241
|
+
"MEMBER_AUTH_REQUIRED",
|
|
242
|
+
"SDK auth bridge를 사용할 수 없습니다."
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
246
|
+
throw new IssueStickerHeadlessError(
|
|
247
|
+
"MEMBER_AUTH_REQUIRED",
|
|
248
|
+
"브라우저 DOM에서만 멤버 로그인을 시작할 수 있습니다."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
await new Promise((resolve, reject) => {
|
|
252
|
+
const wrapper = document.createElement("div");
|
|
253
|
+
wrapper.dataset.issueStickerHeadlessAuth = "true";
|
|
254
|
+
wrapper.style.position = "fixed";
|
|
255
|
+
wrapper.style.inset = "0";
|
|
256
|
+
wrapper.style.zIndex = "2147483647";
|
|
257
|
+
wrapper.style.background = "rgba(15, 23, 42, 0.42)";
|
|
258
|
+
wrapper.style.display = "grid";
|
|
259
|
+
wrapper.style.placeItems = "center";
|
|
260
|
+
const frame = document.createElement("iframe");
|
|
261
|
+
frame.title = "IssueSticker member login";
|
|
262
|
+
frame.src = buildBridgeUrl(this.authBridgeUrl, {
|
|
263
|
+
apiKey: this.options.apiKey,
|
|
264
|
+
tenantId: this.options.getTenantId(),
|
|
265
|
+
projectId: this.options.getProjectId(),
|
|
266
|
+
interactive: true
|
|
267
|
+
});
|
|
268
|
+
frame.style.width = "min(420px, calc(100vw - 32px))";
|
|
269
|
+
frame.style.height = "min(640px, calc(100vh - 32px))";
|
|
270
|
+
frame.style.border = "0";
|
|
271
|
+
frame.style.borderRadius = "12px";
|
|
272
|
+
frame.style.boxShadow = "0 24px 80px rgba(15, 23, 42, 0.32)";
|
|
273
|
+
wrapper.appendChild(frame);
|
|
274
|
+
document.body.appendChild(wrapper);
|
|
275
|
+
this.loginBridgeFrame = frame;
|
|
276
|
+
const cleanup = () => {
|
|
277
|
+
window.removeEventListener("message", onMessage);
|
|
278
|
+
wrapper.remove();
|
|
279
|
+
if (this.loginBridgeFrame === frame) this.loginBridgeFrame = null;
|
|
280
|
+
};
|
|
281
|
+
const onMessage = (event) => {
|
|
282
|
+
var _a, _b;
|
|
283
|
+
if (!this.bridgeOrigin || event.origin !== this.bridgeOrigin) return;
|
|
284
|
+
const message = event.data;
|
|
285
|
+
if (message.type === BRIDGE_LOGIN_SUCCESS && message.data) {
|
|
286
|
+
this.applyToken(message.data);
|
|
287
|
+
cleanup();
|
|
288
|
+
resolve();
|
|
289
|
+
}
|
|
290
|
+
if (message.type === BRIDGE_LOGIN_ERROR) {
|
|
291
|
+
cleanup();
|
|
292
|
+
reject(
|
|
293
|
+
new IssueStickerHeadlessError(
|
|
294
|
+
"MEMBER_AUTH_REQUIRED",
|
|
295
|
+
((_a = message.error) == null ? void 0 : _a.message) || ((_b = message.error) == null ? void 0 : _b.code) || "멤버 로그인에 실패했습니다.",
|
|
296
|
+
message.error
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
window.addEventListener("message", onMessage);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
requestBridgeMemberToken(forceRefresh = false) {
|
|
305
|
+
if (!this.authBridgeUrl || !this.bridgeOrigin) {
|
|
306
|
+
throw new IssueStickerHeadlessError(
|
|
307
|
+
"MEMBER_AUTH_REQUIRED",
|
|
308
|
+
"SDK auth bridge URL이 초기화되지 않았습니다."
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (typeof window === "undefined") {
|
|
312
|
+
throw new IssueStickerHeadlessError(
|
|
313
|
+
"MEMBER_AUTH_REQUIRED",
|
|
314
|
+
"브라우저 DOM에서만 멤버 토큰을 요청할 수 있습니다."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return this.waitForBridgeReady().then(
|
|
318
|
+
() => new Promise((resolve, reject) => {
|
|
319
|
+
const requestId = createRequestId();
|
|
320
|
+
const timeoutId = window.setTimeout(() => {
|
|
321
|
+
this.pendingBridgeRequests.delete(requestId);
|
|
322
|
+
reject(
|
|
323
|
+
new IssueStickerHeadlessError(
|
|
324
|
+
"MEMBER_AUTH_REQUIRED",
|
|
325
|
+
"SDK auth bridge 응답 시간이 초과되었습니다."
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
}, BRIDGE_TIMEOUT_MS);
|
|
329
|
+
this.pendingBridgeRequests.set(requestId, {
|
|
330
|
+
resolve: (result) => {
|
|
331
|
+
if (!(result == null ? void 0 : result.accessToken)) {
|
|
332
|
+
reject(
|
|
333
|
+
new IssueStickerHeadlessError(
|
|
334
|
+
"MEMBER_AUTH_REQUIRED",
|
|
335
|
+
"SDK member token을 발급받지 못했습니다."
|
|
336
|
+
)
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
resolve(result);
|
|
341
|
+
},
|
|
342
|
+
reject,
|
|
343
|
+
timeoutId
|
|
344
|
+
});
|
|
345
|
+
this.postBridgeMessage({
|
|
346
|
+
type: BRIDGE_REQUEST,
|
|
347
|
+
requestId,
|
|
348
|
+
command: "get-member-token",
|
|
349
|
+
apiKey: this.options.apiKey,
|
|
350
|
+
tenantId: this.options.getTenantId(),
|
|
351
|
+
projectId: this.options.getProjectId(),
|
|
352
|
+
hostOrigin: typeof window !== "undefined" ? window.location.origin : void 0,
|
|
353
|
+
forceRefresh
|
|
354
|
+
});
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
waitForBridgeReady() {
|
|
359
|
+
if (this.bridgeReady) return Promise.resolve();
|
|
360
|
+
if (typeof window === "undefined") {
|
|
361
|
+
return Promise.reject(
|
|
362
|
+
new IssueStickerHeadlessError(
|
|
363
|
+
"MEMBER_AUTH_REQUIRED",
|
|
364
|
+
"브라우저 DOM에서만 SDK auth bridge를 사용할 수 있습니다."
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
const timeoutId = window.setTimeout(() => {
|
|
370
|
+
this.bridgeWaiters = this.bridgeWaiters.filter((waiter) => waiter !== done);
|
|
371
|
+
reject(
|
|
372
|
+
new IssueStickerHeadlessError(
|
|
373
|
+
"MEMBER_AUTH_REQUIRED",
|
|
374
|
+
"SDK auth bridge 준비 시간이 초과되었습니다."
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
}, BRIDGE_TIMEOUT_MS);
|
|
378
|
+
const done = () => {
|
|
379
|
+
window.clearTimeout(timeoutId);
|
|
380
|
+
resolve();
|
|
381
|
+
};
|
|
382
|
+
this.bridgeWaiters.push(done);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
handleBridgeMessage(event) {
|
|
386
|
+
var _a, _b, _c;
|
|
387
|
+
if (!this.bridgeOrigin || event.origin !== this.bridgeOrigin) return;
|
|
388
|
+
const message = event.data;
|
|
389
|
+
if (message.type === BRIDGE_READY && event.source === ((_a = this.hiddenBridgeFrame) == null ? void 0 : _a.contentWindow)) {
|
|
390
|
+
this.bridgeReady = true;
|
|
391
|
+
const waiters = this.bridgeWaiters;
|
|
392
|
+
this.bridgeWaiters = [];
|
|
393
|
+
waiters.forEach((waiter) => waiter());
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (message.type === BRIDGE_RESPONSE && message.requestId) {
|
|
397
|
+
const pending = this.pendingBridgeRequests.get(message.requestId);
|
|
398
|
+
if (!pending) return;
|
|
399
|
+
window.clearTimeout(pending.timeoutId);
|
|
400
|
+
this.pendingBridgeRequests.delete(message.requestId);
|
|
401
|
+
if (message.success && message.data) {
|
|
402
|
+
pending.resolve(message.data);
|
|
403
|
+
} else {
|
|
404
|
+
pending.reject(
|
|
405
|
+
new IssueStickerHeadlessError(
|
|
406
|
+
"MEMBER_AUTH_REQUIRED",
|
|
407
|
+
((_b = message.error) == null ? void 0 : _b.message) || ((_c = message.error) == null ? void 0 : _c.code) || "멤버 인증이 필요합니다.",
|
|
408
|
+
message.error
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
postBridgeMessage(message) {
|
|
415
|
+
var _a;
|
|
416
|
+
if (!this.bridgeOrigin || !((_a = this.hiddenBridgeFrame) == null ? void 0 : _a.contentWindow)) return;
|
|
417
|
+
this.hiddenBridgeFrame.contentWindow.postMessage(message, this.bridgeOrigin);
|
|
418
|
+
}
|
|
419
|
+
applyToken(result) {
|
|
420
|
+
const expiresAtMs = new Date(result.expiresAt).getTime();
|
|
421
|
+
this.memberToken = {
|
|
422
|
+
accessToken: result.accessToken,
|
|
423
|
+
expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : Date.now() + Math.max(result.expiresIn - 60, 60) * 1e3
|
|
424
|
+
};
|
|
425
|
+
this.options.onToken(result);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const JIRA_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
|
|
429
|
+
"status",
|
|
430
|
+
"priority",
|
|
431
|
+
"assignee",
|
|
432
|
+
"reporter",
|
|
433
|
+
"fixVersions",
|
|
434
|
+
"issuetype",
|
|
435
|
+
"project_key",
|
|
436
|
+
"sprint_id",
|
|
437
|
+
"sprint_name",
|
|
438
|
+
"epic_link",
|
|
439
|
+
"story_points"
|
|
440
|
+
]);
|
|
441
|
+
const NOTION_FILTERABLE_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multi_select", "status", "checkbox"]);
|
|
442
|
+
function getJiraConfig(integration) {
|
|
443
|
+
if (integration.service_type !== "JIRA") return null;
|
|
444
|
+
return integration.config ?? null;
|
|
445
|
+
}
|
|
446
|
+
function getNotionConfig(integration) {
|
|
447
|
+
if (integration.service_type !== "NOTION") return null;
|
|
448
|
+
return integration.config ?? null;
|
|
449
|
+
}
|
|
450
|
+
function getExposedFields(config) {
|
|
451
|
+
if (!(config == null ? void 0 : config.exposed_fields)) return [];
|
|
452
|
+
return normalizeExposedJiraFields(
|
|
453
|
+
Array.isArray(config.exposed_fields) ? config.exposed_fields : void 0
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
function getLastKnownJiraFields(config) {
|
|
457
|
+
const fields = config == null ? void 0 : config.last_known_fields;
|
|
458
|
+
if (!Array.isArray(fields)) return [];
|
|
459
|
+
return fields.filter(
|
|
460
|
+
(field) => Boolean(field) && typeof field === "object" && typeof field.id === "string" && typeof field.name === "string"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
function getSelectedNotionFields(config) {
|
|
464
|
+
const fields = config == null ? void 0 : config.selected_fields;
|
|
465
|
+
if (!Array.isArray(fields)) return [];
|
|
466
|
+
return fields.filter((field) => typeof field === "string");
|
|
467
|
+
}
|
|
468
|
+
function getLastKnownNotionFields(config) {
|
|
469
|
+
const fields = config == null ? void 0 : config.last_known_fields;
|
|
470
|
+
if (!Array.isArray(fields)) return [];
|
|
471
|
+
return fields.filter(
|
|
472
|
+
(field) => Boolean(field) && typeof field === "object" && typeof field.id === "string" && typeof field.name === "string" && typeof field.type === "string"
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const OPTION_CUSTOM_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multi_select"]);
|
|
476
|
+
function getSelectableCustomFieldOptions(field) {
|
|
477
|
+
var _a;
|
|
478
|
+
if (!OPTION_CUSTOM_FIELD_TYPES.has(field.field_type)) return [];
|
|
479
|
+
const rawOptions = (_a = field.field_config) == null ? void 0 : _a.options;
|
|
480
|
+
if (!Array.isArray(rawOptions)) return [];
|
|
481
|
+
return rawOptions.filter((option) => typeof option === "string" && option.trim().length > 0).map((option) => ({ label: option, value: option }));
|
|
482
|
+
}
|
|
483
|
+
function getJiraOptionValue(option) {
|
|
484
|
+
return option.id;
|
|
485
|
+
}
|
|
486
|
+
function getJiraFieldFilterOptions(field) {
|
|
487
|
+
const options = field.options ?? [];
|
|
488
|
+
if (!options.length) return [];
|
|
489
|
+
return options.map((option) => ({
|
|
490
|
+
label: option.name || option.value || option.id,
|
|
491
|
+
value: getJiraOptionValue(option)
|
|
492
|
+
})).filter((option) => option.value.length > 0);
|
|
493
|
+
}
|
|
494
|
+
function getNotionFieldFilterOptions(field) {
|
|
495
|
+
if (field.type === "checkbox") {
|
|
496
|
+
return [
|
|
497
|
+
{ label: "체크됨", value: "true" },
|
|
498
|
+
{ label: "체크 안 됨", value: "false" }
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
const options = field.options ?? [];
|
|
502
|
+
if (!options.length) return [];
|
|
503
|
+
return options.map((option) => ({
|
|
504
|
+
label: option.name,
|
|
505
|
+
value: option.name
|
|
506
|
+
})).filter((option) => option.value.length > 0);
|
|
507
|
+
}
|
|
508
|
+
function mergeOptions(current, next) {
|
|
509
|
+
const byValue = new Map(current.map((option) => [option.value, option]));
|
|
510
|
+
for (const option of next) {
|
|
511
|
+
if (!byValue.has(option.value)) {
|
|
512
|
+
byValue.set(option.value, option);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return Array.from(byValue.values()).sort((a, b) => a.label.localeCompare(b.label));
|
|
516
|
+
}
|
|
517
|
+
function upsertFilterGroup(groups, group) {
|
|
518
|
+
const existing = groups.get(group.id);
|
|
519
|
+
if (!existing) {
|
|
520
|
+
groups.set(group.id, {
|
|
521
|
+
...group,
|
|
522
|
+
options: mergeOptions([], group.options)
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
groups.set(group.id, {
|
|
527
|
+
...existing,
|
|
528
|
+
options: mergeOptions(existing.options, group.options)
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function appendCustomFieldFilterGroups(groups, exposedFieldIds, customFields) {
|
|
532
|
+
for (const field of customFields) {
|
|
533
|
+
if (!exposedFieldIds.has(normalizeJiraFieldId(field.field_key))) continue;
|
|
534
|
+
const options = getSelectableCustomFieldOptions(field);
|
|
535
|
+
if (!options.length) continue;
|
|
536
|
+
upsertFilterGroup(groups, {
|
|
537
|
+
kind: "custom",
|
|
538
|
+
id: `custom:${field.id}`,
|
|
539
|
+
fieldDefinitionId: field.id,
|
|
540
|
+
label: field.field_name,
|
|
541
|
+
options
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function appendJiraFieldFilterGroups(groups, exposedFieldIds, fields) {
|
|
546
|
+
for (const field of fields) {
|
|
547
|
+
const normalizedFieldId = normalizeJiraFieldId(field.id);
|
|
548
|
+
if (!exposedFieldIds.has(normalizedFieldId) || !JIRA_FILTERABLE_FIELD_IDS.has(normalizedFieldId)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const options = getJiraFieldFilterOptions(field);
|
|
552
|
+
if (!options.length) continue;
|
|
553
|
+
upsertFilterGroup(groups, {
|
|
554
|
+
kind: "jira",
|
|
555
|
+
id: `jira:${normalizedFieldId}`,
|
|
556
|
+
fieldName: normalizedFieldId,
|
|
557
|
+
label: field.name,
|
|
558
|
+
options
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function appendNotionFieldFilterGroups(groups, selectedFields, fields) {
|
|
563
|
+
for (const field of fields) {
|
|
564
|
+
if (!selectedFields.has(field.id) && !selectedFields.has(field.name)) continue;
|
|
565
|
+
if (!NOTION_FILTERABLE_FIELD_TYPES.has(field.type)) continue;
|
|
566
|
+
const options = getNotionFieldFilterOptions(field);
|
|
567
|
+
if (!options.length) continue;
|
|
568
|
+
upsertFilterGroup(groups, {
|
|
569
|
+
kind: "notion",
|
|
570
|
+
id: `notion:${field.id}`,
|
|
571
|
+
fieldName: field.name,
|
|
572
|
+
label: field.name,
|
|
573
|
+
options
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function composeIssueFilterGroups(integrations, customFields) {
|
|
578
|
+
const groups = /* @__PURE__ */ new Map();
|
|
579
|
+
const jiraIntegration = integrations.find((integration) => {
|
|
580
|
+
const config = getJiraConfig(integration);
|
|
581
|
+
return integration.is_active !== false && config;
|
|
582
|
+
});
|
|
583
|
+
const jiraConfig = jiraIntegration ? getJiraConfig(jiraIntegration) : null;
|
|
584
|
+
const configExposedFields = getExposedFields(jiraConfig);
|
|
585
|
+
if (configExposedFields.length) {
|
|
586
|
+
const exposedFieldIds = new Set(configExposedFields);
|
|
587
|
+
appendCustomFieldFilterGroups(groups, exposedFieldIds, customFields);
|
|
588
|
+
appendJiraFieldFilterGroups(groups, exposedFieldIds, getLastKnownJiraFields(jiraConfig));
|
|
589
|
+
}
|
|
590
|
+
for (const integration of integrations) {
|
|
591
|
+
if (integration.is_active === false) continue;
|
|
592
|
+
const notionConfig = getNotionConfig(integration);
|
|
593
|
+
if (!notionConfig) continue;
|
|
594
|
+
appendNotionFieldFilterGroups(
|
|
595
|
+
groups,
|
|
596
|
+
new Set(getSelectedNotionFields(notionConfig)),
|
|
597
|
+
getLastKnownNotionFields(notionConfig)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label));
|
|
601
|
+
}
|
|
602
|
+
function buildHeadlessCaptureContext({
|
|
603
|
+
target,
|
|
604
|
+
event,
|
|
605
|
+
videoFile,
|
|
606
|
+
createVideoPreviewUrl,
|
|
607
|
+
sessionReplayEnabled
|
|
608
|
+
}) {
|
|
609
|
+
const mediaMode = videoFile ? "video" : "screenshot";
|
|
610
|
+
const position = getElementPosition(target, event);
|
|
611
|
+
return {
|
|
612
|
+
mediaMode,
|
|
613
|
+
position,
|
|
614
|
+
screenshotPreviewUrls: [],
|
|
615
|
+
videoPreviewUrl: mediaMode === "video" && videoFile ? createVideoPreviewUrl(videoFile) : null,
|
|
616
|
+
targetSelector: position.selector,
|
|
617
|
+
sessionReplayEnabled,
|
|
618
|
+
sessionReplayPreview: null
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async function captureHeadlessScreenshots({
|
|
622
|
+
target,
|
|
623
|
+
excludeSelectors
|
|
624
|
+
}) {
|
|
625
|
+
const screenshots = await Promise.all([
|
|
626
|
+
captureWithModernScreenshot({
|
|
627
|
+
element: document.documentElement,
|
|
628
|
+
excludeSelectors,
|
|
629
|
+
width: window.innerWidth,
|
|
630
|
+
height: window.innerHeight
|
|
631
|
+
}).catch(() => void 0),
|
|
632
|
+
captureWithModernScreenshot({
|
|
633
|
+
element: target,
|
|
634
|
+
excludeSelectors
|
|
635
|
+
}).catch(() => void 0)
|
|
636
|
+
]);
|
|
637
|
+
return screenshots.filter(
|
|
638
|
+
(dataUrl) => typeof dataUrl === "string" && dataUrl.length > 0
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
function normalizeCustomInputType(fieldType) {
|
|
642
|
+
if (fieldType === "number") return "number";
|
|
643
|
+
if (fieldType === "select") return "select";
|
|
644
|
+
if (fieldType === "multi_select") return "multi_select";
|
|
645
|
+
if (fieldType === "date") return "date";
|
|
646
|
+
if (fieldType === "checkbox" || fieldType === "boolean") return "checkbox";
|
|
647
|
+
return "text";
|
|
648
|
+
}
|
|
649
|
+
function buildCustomFieldOptions(field) {
|
|
650
|
+
var _a;
|
|
651
|
+
const rawOptions = (_a = field.field_config) == null ? void 0 : _a.options;
|
|
652
|
+
if (!Array.isArray(rawOptions)) return [];
|
|
653
|
+
return rawOptions.map((option) => {
|
|
654
|
+
if (typeof option === "string") {
|
|
655
|
+
const value = option.trim();
|
|
656
|
+
return value ? { label: value, value } : null;
|
|
657
|
+
}
|
|
658
|
+
if (option && typeof option === "object") {
|
|
659
|
+
const candidate = option;
|
|
660
|
+
const converted = toOption(candidate);
|
|
661
|
+
return converted.value ? converted : null;
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}).filter((option) => Boolean(option));
|
|
665
|
+
}
|
|
666
|
+
function buildCustomFieldDescriptors(fields) {
|
|
667
|
+
return fields.map((field) => ({
|
|
668
|
+
id: field.id,
|
|
669
|
+
source: "custom",
|
|
670
|
+
label: field.field_name || field.field_key || field.id,
|
|
671
|
+
inputType: normalizeCustomInputType(field.field_type),
|
|
672
|
+
options: buildCustomFieldOptions(field)
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
function normalizeJiraInputType(inputType) {
|
|
676
|
+
if (inputType === "user" || inputType === "issue") return inputType;
|
|
677
|
+
if (inputType === "number") return "number";
|
|
678
|
+
if (inputType === "select") return "select";
|
|
679
|
+
if (inputType === "readonly") return "readonly";
|
|
680
|
+
return "text";
|
|
681
|
+
}
|
|
682
|
+
function normalizeNotionInputType(inputType) {
|
|
683
|
+
if (inputType === "number" || inputType === "select" || inputType === "multi_select" || inputType === "date" || inputType === "checkbox" || inputType === "people" || inputType === "readonly") {
|
|
684
|
+
return inputType;
|
|
685
|
+
}
|
|
686
|
+
return "text";
|
|
687
|
+
}
|
|
688
|
+
function toOption(option) {
|
|
689
|
+
const value = option.value || option.id || option.name || option.label || "";
|
|
690
|
+
return {
|
|
691
|
+
value,
|
|
692
|
+
label: option.label || option.name || option.value || option.id || value,
|
|
693
|
+
description: option.description
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function getFilledDynamicValues(draft, fields, source) {
|
|
697
|
+
return Object.fromEntries(
|
|
698
|
+
fields.filter((field) => field.source === source).map((field) => [field.id, draft.dynamicFields[field.id]]).filter(([, value]) => value !== void 0 && value !== null && value !== "")
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function getJiraUserLabel(user) {
|
|
702
|
+
return user.displayName || user.emailAddress || user.accountId;
|
|
703
|
+
}
|
|
704
|
+
function getJiraIssueLabel(issue) {
|
|
705
|
+
return issue.summary ? `${issue.key} ${issue.summary}` : issue.key;
|
|
706
|
+
}
|
|
707
|
+
async function buildIssueFormSchema(values, deps) {
|
|
708
|
+
var _a, _b;
|
|
709
|
+
if (deps.authMode !== "member") {
|
|
710
|
+
return { fields: [], unavailableReason: "public-mode" };
|
|
711
|
+
}
|
|
712
|
+
const [active, customFields] = await Promise.all([
|
|
713
|
+
deps.getActiveIntegration(),
|
|
714
|
+
deps.getCustomFieldDefinitions()
|
|
715
|
+
]);
|
|
716
|
+
const dynamicFields = [];
|
|
717
|
+
if ((active == null ? void 0 : active.service_type) === "JIRA") {
|
|
718
|
+
const response = await deps.requestResponse(
|
|
719
|
+
`/api/jira/integration-fields/${encodeURIComponent(active.id)}`
|
|
720
|
+
);
|
|
721
|
+
const exposedFields = Array.isArray((_a = response.data) == null ? void 0 : _a.exposed_fields) ? response.data.exposed_fields : active.config.exposed_fields;
|
|
722
|
+
const fields = buildJiraCreateFields(exposedFields, ((_b = response.data) == null ? void 0 : _b.fields) ?? [], values);
|
|
723
|
+
dynamicFields.push(
|
|
724
|
+
...fields.map((field) => ({
|
|
725
|
+
id: field.id,
|
|
726
|
+
source: "jira",
|
|
727
|
+
label: field.label,
|
|
728
|
+
inputType: normalizeJiraInputType(field.inputType),
|
|
729
|
+
options: field.options.map(toOption),
|
|
730
|
+
...field.inputType === "user" ? {
|
|
731
|
+
search: (query) => deps.searchJiraProjectUsers(query).then((items) => items.map(toOption)),
|
|
732
|
+
resolveValueLabel: (value) => deps.resolveJiraProjectUserLabel(value)
|
|
733
|
+
} : {},
|
|
734
|
+
...field.inputType === "issue" ? {
|
|
735
|
+
search: (query) => deps.searchJiraProjectIssues(query).then((items) => items.map(toOption)),
|
|
736
|
+
resolveValueLabel: (value) => deps.resolveJiraProjectIssueLabel(value)
|
|
737
|
+
} : {}
|
|
738
|
+
}))
|
|
739
|
+
);
|
|
740
|
+
} else if ((active == null ? void 0 : active.service_type) === "NOTION") {
|
|
741
|
+
const selectedFields = Array.isArray(active.config.selected_fields) ? active.config.selected_fields : [];
|
|
742
|
+
const fieldDefinitions = Array.isArray(active.config.last_known_fields) ? active.config.last_known_fields : [];
|
|
743
|
+
const fields = buildNotionCreateFields(selectedFields, fieldDefinitions, values);
|
|
744
|
+
dynamicFields.push(
|
|
745
|
+
...fields.map((field) => ({
|
|
746
|
+
id: field.id,
|
|
747
|
+
source: "notion",
|
|
748
|
+
label: field.name,
|
|
749
|
+
inputType: normalizeNotionInputType(field.inputType),
|
|
750
|
+
options: field.options.map(toOption),
|
|
751
|
+
...field.inputType === "people" ? {
|
|
752
|
+
search: (query) => deps.searchNotionPeople(query).then(
|
|
753
|
+
(items) => items.map((item) => ({
|
|
754
|
+
label: item.name,
|
|
755
|
+
value: item.id,
|
|
756
|
+
description: item.email
|
|
757
|
+
}))
|
|
758
|
+
)
|
|
759
|
+
} : {}
|
|
760
|
+
}))
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
dynamicFields.push(...buildCustomFieldDescriptors(customFields));
|
|
764
|
+
if (dynamicFields.length === 0) {
|
|
765
|
+
return { fields: [], unavailableReason: "not-configured" };
|
|
766
|
+
}
|
|
767
|
+
return { fields: dynamicFields };
|
|
768
|
+
}
|
|
769
|
+
function recordToFilterArray(record, keyFieldName) {
|
|
770
|
+
if (!record) return null;
|
|
771
|
+
const entries = Object.entries(record).filter(([, values]) => values.length > 0);
|
|
772
|
+
if (entries.length === 0) return null;
|
|
773
|
+
return entries.map(
|
|
774
|
+
([key, values]) => ({ [keyFieldName]: key, values })
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
function hasIssueListFilters(filters) {
|
|
778
|
+
var _a;
|
|
779
|
+
return Boolean(
|
|
780
|
+
((_a = filters.search) == null ? void 0 : _a.trim()) || filters.dateFrom || filters.dateTo || filters.page || hasRecordFilters(filters.customFieldValues) || hasRecordFilters(filters.jiraFieldValues) || hasRecordFilters(filters.notionFieldValues)
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
function hasRecordFilters(record) {
|
|
784
|
+
return Boolean(record && Object.values(record).some((values) => values.length > 0));
|
|
785
|
+
}
|
|
786
|
+
function buildIssueQueryParams(filters, context) {
|
|
787
|
+
var _a;
|
|
788
|
+
const params = new URLSearchParams();
|
|
789
|
+
params.set("project_id", context.projectId);
|
|
790
|
+
if (filters.url && filters.url !== "all") {
|
|
791
|
+
params.set("url", Array.isArray(filters.url) ? filters.url.join(",") : filters.url);
|
|
792
|
+
} else if (!filters.url && context.currentUrl) {
|
|
793
|
+
params.set("url", context.currentUrl);
|
|
794
|
+
}
|
|
795
|
+
if ((_a = filters.search) == null ? void 0 : _a.trim()) params.set("search", filters.search.trim());
|
|
796
|
+
if (filters.dateFrom) params.set("date_from", filters.dateFrom);
|
|
797
|
+
if (filters.dateTo) params.set("date_to", filters.dateTo);
|
|
798
|
+
if (filters.page) params.set("page", String(filters.page));
|
|
799
|
+
if (filters.pageSize) params.set("page_size", String(filters.pageSize));
|
|
800
|
+
if (filters.sortOrder) params.set("sort_order", filters.sortOrder);
|
|
801
|
+
const externalUserId = context.authMode === "public" ? filters.externalUserId || context.defaultExternalUserId : filters.externalUserId;
|
|
802
|
+
if (externalUserId) params.set("external_user_id", externalUserId);
|
|
803
|
+
const custom = recordToFilterArray(filters.customFieldValues, "fieldDefinitionId");
|
|
804
|
+
const jira = recordToFilterArray(filters.jiraFieldValues, "fieldName");
|
|
805
|
+
const notion = recordToFilterArray(filters.notionFieldValues, "fieldName");
|
|
806
|
+
if (custom) params.set("custom_field_filters", JSON.stringify(custom));
|
|
807
|
+
if (jira) params.set("jira_field_filters", JSON.stringify(jira));
|
|
808
|
+
if (notion) params.set("notion_field_filters", JSON.stringify(notion));
|
|
809
|
+
return params;
|
|
810
|
+
}
|
|
811
|
+
const STICKER_PAGE_SIZE = 500;
|
|
812
|
+
const STICKER_FETCH_LIMIT = 2e3;
|
|
813
|
+
class HeadlessIssueRepository {
|
|
814
|
+
constructor(options) {
|
|
815
|
+
this.options = options;
|
|
816
|
+
}
|
|
817
|
+
request(endpoint, options = {}) {
|
|
818
|
+
return this.options.transport.request(endpoint, options);
|
|
819
|
+
}
|
|
820
|
+
requestResponse(endpoint, options = {}) {
|
|
821
|
+
return this.options.transport.requestResponse(endpoint, options);
|
|
822
|
+
}
|
|
823
|
+
openIssue(issueId) {
|
|
824
|
+
return this.request(
|
|
825
|
+
`/api/sdk/issues/${encodeURIComponent(issueId)}`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
createIssue(issueData) {
|
|
829
|
+
return this.request("/api/sdk/issues", {
|
|
830
|
+
method: "POST",
|
|
831
|
+
body: issueData
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async loadFilterSchema() {
|
|
835
|
+
if (this.options.getAuthMode() !== "member") {
|
|
836
|
+
return { groups: [], unavailableReason: "public-mode" };
|
|
837
|
+
}
|
|
838
|
+
const [integrations, customFields] = await Promise.all([
|
|
839
|
+
this.getProjectIntegrations(),
|
|
840
|
+
this.getCustomFieldDefinitions()
|
|
841
|
+
]);
|
|
842
|
+
const groups = composeIssueFilterGroups(integrations, customFields);
|
|
843
|
+
return { groups };
|
|
844
|
+
}
|
|
845
|
+
async getActiveIntegration() {
|
|
846
|
+
const integrations = await this.getProjectIntegrations();
|
|
847
|
+
return integrations.find((integration) => integration.service_type === "JIRA") ?? integrations.find((integration) => integration.service_type === "NOTION") ?? null;
|
|
848
|
+
}
|
|
849
|
+
async getProjectIntegrations() {
|
|
850
|
+
const integrations = await this.request(
|
|
851
|
+
`/api/integrations/${this.options.getProjectId()}`
|
|
852
|
+
);
|
|
853
|
+
return Array.isArray(integrations) ? integrations.filter((integration) => integration.is_active !== false) : [];
|
|
854
|
+
}
|
|
855
|
+
async getCustomFieldDefinitions() {
|
|
856
|
+
const fields = await this.request(
|
|
857
|
+
`/api/integrations/fields/definitions?projectId=${encodeURIComponent(this.options.getProjectId())}`
|
|
858
|
+
);
|
|
859
|
+
return Array.isArray(fields) ? fields : [];
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* 이슈 저장 이후의 외부 연동(Jira/Notion/커스텀 필드)을 이어서 실행한다.
|
|
863
|
+
*
|
|
864
|
+
* 실패는 이슈 저장을 되돌리지 않으므로 throw 하지 않고 결과로 돌려준다. headless 는
|
|
865
|
+
* UI 가 없어 toast 를 띄울 수 없으니, 통합하는 쪽이 이 값을 보고 판단해야 한다.
|
|
866
|
+
*/
|
|
867
|
+
async syncDynamicFields(createdIssue, draft, schema, context) {
|
|
868
|
+
if (this.options.getAuthMode() !== "member") {
|
|
869
|
+
return { failedServices: [] };
|
|
870
|
+
}
|
|
871
|
+
const active = await this.getActiveIntegration().catch(() => null);
|
|
872
|
+
const fields = (schema == null ? void 0 : schema.fields) ?? [];
|
|
873
|
+
const jiraValues = getFilledDynamicValues(draft, fields, "jira");
|
|
874
|
+
const notionValues = getFilledDynamicValues(draft, fields, "notion");
|
|
875
|
+
const customValues = getFilledDynamicValues(draft, fields, "custom");
|
|
876
|
+
return syncExternalIssueIntegrations({
|
|
877
|
+
issueId: createdIssue.id,
|
|
878
|
+
title: draft.title.trim(),
|
|
879
|
+
description: draft.description.trim(),
|
|
880
|
+
url: this.options.getCurrentUrl() ?? "",
|
|
881
|
+
screenshotUrl: context.screenshotUrl,
|
|
882
|
+
videoUrl: context.videoUrl,
|
|
883
|
+
jira: {
|
|
884
|
+
enabled: (active == null ? void 0 : active.service_type) === "JIRA",
|
|
885
|
+
fields: jiraValues,
|
|
886
|
+
createIssue: (input) => this.request("/api/jira/issues", {
|
|
887
|
+
method: "POST",
|
|
888
|
+
body: {
|
|
889
|
+
projectId: this.options.getProjectId(),
|
|
890
|
+
issueId: input.issueId,
|
|
891
|
+
issueData: {
|
|
892
|
+
title: input.title,
|
|
893
|
+
description: input.description,
|
|
894
|
+
url: input.url,
|
|
895
|
+
screenshotUrl: input.screenshotUrl,
|
|
896
|
+
videoUrl: input.videoUrl,
|
|
897
|
+
fields: input.fields
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
})
|
|
901
|
+
},
|
|
902
|
+
notion: {
|
|
903
|
+
enabled: (active == null ? void 0 : active.service_type) === "NOTION",
|
|
904
|
+
properties: notionValues,
|
|
905
|
+
createPage: (input) => this.request("/api/notion/pages", {
|
|
906
|
+
method: "POST",
|
|
907
|
+
body: {
|
|
908
|
+
projectId: this.options.getProjectId(),
|
|
909
|
+
issueId: input.issueId,
|
|
910
|
+
properties: input.properties
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
},
|
|
914
|
+
customFieldValues: customValues,
|
|
915
|
+
upsertCustomFieldValue: (fieldDefinitionId, value) => this.request(
|
|
916
|
+
`/api/custom-field-values/issues/${encodeURIComponent(createdIssue.id)}/values`,
|
|
917
|
+
{
|
|
918
|
+
method: "PUT",
|
|
919
|
+
body: {
|
|
920
|
+
field_definition_id: fieldDefinitionId,
|
|
921
|
+
value
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
),
|
|
925
|
+
onError: (service, error) => {
|
|
926
|
+
devLogger.error("headless", `${service} 연동 실패 (이슈 저장은 성공):`, error);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
async fetchAllIssueStickers(filters) {
|
|
931
|
+
var _a, _b, _c;
|
|
932
|
+
const projectId = this.options.getProjectId();
|
|
933
|
+
const externalUserId = this.options.getAuthMode() === "public" ? filters.externalUserId || this.options.getDefaultExternalUserId() : void 0;
|
|
934
|
+
if (this.options.getAuthMode() === "public" && !externalUserId) {
|
|
935
|
+
return [];
|
|
936
|
+
}
|
|
937
|
+
if (hasIssueListFilters(filters)) {
|
|
938
|
+
const params = buildIssueQueryParams(filters, {
|
|
939
|
+
projectId,
|
|
940
|
+
authMode: this.options.getAuthMode(),
|
|
941
|
+
defaultExternalUserId: this.options.getDefaultExternalUserId(),
|
|
942
|
+
currentUrl: this.options.getCurrentUrl()
|
|
943
|
+
});
|
|
944
|
+
const response = await this.requestResponse(
|
|
945
|
+
`/api/sdk/issues?${params.toString()}`
|
|
946
|
+
);
|
|
947
|
+
return ((_a = response.data) == null ? void 0 : _a.items) ?? [];
|
|
948
|
+
}
|
|
949
|
+
const issues = [];
|
|
950
|
+
let page = 1;
|
|
951
|
+
let hasMore = true;
|
|
952
|
+
while (hasMore && issues.length <= STICKER_FETCH_LIMIT) {
|
|
953
|
+
const params = new URLSearchParams();
|
|
954
|
+
params.set("project_id", projectId);
|
|
955
|
+
params.set("page", String(page));
|
|
956
|
+
params.set("page_size", String(filters.pageSize ?? STICKER_PAGE_SIZE));
|
|
957
|
+
if (filters.url && filters.url !== "all") {
|
|
958
|
+
params.set("url", Array.isArray(filters.url) ? filters.url.join(",") : filters.url);
|
|
959
|
+
} else if (!filters.url && this.options.getCurrentUrl()) {
|
|
960
|
+
params.set("url", this.options.getCurrentUrl());
|
|
961
|
+
}
|
|
962
|
+
if (externalUserId) params.set("external_user_id", externalUserId);
|
|
963
|
+
if (filters.sortOrder) params.set("sort_order", filters.sortOrder);
|
|
964
|
+
const response = await this.requestResponse(`/api/sdk/issues/stickers?${params.toString()}`);
|
|
965
|
+
const pageItems = ((_b = response.data) == null ? void 0 : _b.items) ?? [];
|
|
966
|
+
issues.push(...pageItems);
|
|
967
|
+
hasMore = Boolean((_c = response.data) == null ? void 0 : _c.hasMore) && pageItems.length > 0;
|
|
968
|
+
page += 1;
|
|
969
|
+
}
|
|
970
|
+
return issues;
|
|
971
|
+
}
|
|
972
|
+
searchJiraProjectUsers(query) {
|
|
973
|
+
return this.request(
|
|
974
|
+
`/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/users/search?query=${encodeURIComponent(query)}`
|
|
975
|
+
).then(
|
|
976
|
+
(users) => users.map((user) => ({
|
|
977
|
+
value: user.accountId,
|
|
978
|
+
label: getJiraUserLabel(user),
|
|
979
|
+
description: user.emailAddress
|
|
980
|
+
}))
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
resolveJiraProjectUserLabel(accountId) {
|
|
984
|
+
return this.request(
|
|
985
|
+
`/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/users/${encodeURIComponent(accountId)}`
|
|
986
|
+
).then(getJiraUserLabel).catch(() => null);
|
|
987
|
+
}
|
|
988
|
+
searchJiraProjectIssues(query) {
|
|
989
|
+
return this.request(
|
|
990
|
+
`/api/jira/projects/${encodeURIComponent(this.options.getProjectId())}/issues?query=${encodeURIComponent(query)}`
|
|
991
|
+
).then(
|
|
992
|
+
(issues) => issues.map((issue) => ({
|
|
993
|
+
value: issue.key,
|
|
994
|
+
label: getJiraIssueLabel(issue),
|
|
995
|
+
description: issue.status
|
|
996
|
+
}))
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
async resolveJiraProjectIssueLabel(issueKey) {
|
|
1000
|
+
var _a, _b;
|
|
1001
|
+
const issues = await this.searchJiraProjectIssues(issueKey).catch(() => []);
|
|
1002
|
+
return ((_a = issues.find((issue) => issue.value === issueKey)) == null ? void 0 : _a.label) ?? ((_b = issues[0]) == null ? void 0 : _b.label) ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
searchNotionPeople(query) {
|
|
1005
|
+
return this.request(
|
|
1006
|
+
`/api/notion/users?projectId=${encodeURIComponent(this.options.getProjectId())}&query=${encodeURIComponent(query)}`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const defaultDeps = {
|
|
1011
|
+
createNetworkCollector: (apiBaseUrl) => new NetworkCollector(apiBaseUrl),
|
|
1012
|
+
createConsoleCollector: () => new ConsoleCollector()
|
|
1013
|
+
};
|
|
1014
|
+
class HeadlessLogCollectors {
|
|
1015
|
+
constructor(config, getApiBaseUrl2, deps = defaultDeps) {
|
|
1016
|
+
this.config = config;
|
|
1017
|
+
this.getApiBaseUrl = getApiBaseUrl2;
|
|
1018
|
+
this.deps = deps;
|
|
1019
|
+
this.networkCollector = null;
|
|
1020
|
+
this.consoleCollector = null;
|
|
1021
|
+
}
|
|
1022
|
+
start() {
|
|
1023
|
+
var _a, _b;
|
|
1024
|
+
if (((_a = this.config.capture) == null ? void 0 : _a.networkLogs) !== false && !this.networkCollector) {
|
|
1025
|
+
this.networkCollector = this.deps.createNetworkCollector(this.getApiBaseUrl());
|
|
1026
|
+
this.networkCollector.start();
|
|
1027
|
+
}
|
|
1028
|
+
if (((_b = this.config.capture) == null ? void 0 : _b.consoleLogs) !== false && !this.consoleCollector) {
|
|
1029
|
+
this.consoleCollector = this.deps.createConsoleCollector();
|
|
1030
|
+
this.consoleCollector.start();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
stop() {
|
|
1034
|
+
var _a, _b;
|
|
1035
|
+
(_a = this.networkCollector) == null ? void 0 : _a.stop();
|
|
1036
|
+
(_b = this.consoleCollector) == null ? void 0 : _b.stop();
|
|
1037
|
+
this.networkCollector = null;
|
|
1038
|
+
this.consoleCollector = null;
|
|
1039
|
+
}
|
|
1040
|
+
collect() {
|
|
1041
|
+
var _a, _b;
|
|
1042
|
+
const networkLogs = filterExcludeServiceRelatedLogs(
|
|
1043
|
+
((_a = this.networkCollector) == null ? void 0 : _a.getFilteredLogs()) ?? []
|
|
1044
|
+
).slice(-50);
|
|
1045
|
+
const consoleLogs = filterExcludeServiceRelatedConsoleLogs(
|
|
1046
|
+
((_b = this.consoleCollector) == null ? void 0 : _b.getFilteredLogs()) ?? []
|
|
1047
|
+
).slice(-50);
|
|
1048
|
+
return {
|
|
1049
|
+
networkLogs: compactNetworkLogsForTransport(networkLogs) ?? [],
|
|
1050
|
+
consoleLogs
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
class HeadlessPendingMedia {
|
|
1055
|
+
constructor() {
|
|
1056
|
+
this.file = null;
|
|
1057
|
+
this.objectUrl = null;
|
|
1058
|
+
}
|
|
1059
|
+
get videoFile() {
|
|
1060
|
+
return this.file;
|
|
1061
|
+
}
|
|
1062
|
+
get previewUrl() {
|
|
1063
|
+
return this.objectUrl;
|
|
1064
|
+
}
|
|
1065
|
+
get hasVideo() {
|
|
1066
|
+
return Boolean(this.file);
|
|
1067
|
+
}
|
|
1068
|
+
setVideoFile(file) {
|
|
1069
|
+
this.file = file;
|
|
1070
|
+
}
|
|
1071
|
+
createVideoPreviewUrl(file) {
|
|
1072
|
+
if (this.objectUrl) {
|
|
1073
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
1074
|
+
}
|
|
1075
|
+
this.file = file;
|
|
1076
|
+
this.objectUrl = URL.createObjectURL(file);
|
|
1077
|
+
return this.objectUrl;
|
|
1078
|
+
}
|
|
1079
|
+
clear() {
|
|
1080
|
+
if (this.objectUrl) {
|
|
1081
|
+
URL.revokeObjectURL(this.objectUrl);
|
|
1082
|
+
this.objectUrl = null;
|
|
1083
|
+
}
|
|
1084
|
+
this.file = null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function createReplayFile(snapshot) {
|
|
1088
|
+
return new File([snapshot.payload], "session-replay.gz", {
|
|
1089
|
+
type: "application/gzip"
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
function createSessionReplayMeta(snapshot) {
|
|
1093
|
+
return {
|
|
1094
|
+
window_seconds: snapshot.windowSeconds,
|
|
1095
|
+
event_count: snapshot.eventCount,
|
|
1096
|
+
size_bytes: snapshot.sizeBytes,
|
|
1097
|
+
rrweb_version: snapshot.rrwebVersion,
|
|
1098
|
+
privacy_config_version: snapshot.privacyConfigVersion
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function createInitialHeadlessSnapshot() {
|
|
1102
|
+
return {
|
|
1103
|
+
phase: "idle",
|
|
1104
|
+
auth: {
|
|
1105
|
+
mode: "public",
|
|
1106
|
+
isReady: false,
|
|
1107
|
+
canUseIntegrations: false,
|
|
1108
|
+
tenantId: null,
|
|
1109
|
+
projectId: null,
|
|
1110
|
+
accountId: null
|
|
1111
|
+
},
|
|
1112
|
+
issues: [],
|
|
1113
|
+
activeIssue: null,
|
|
1114
|
+
draft: null,
|
|
1115
|
+
videoCapture: {
|
|
1116
|
+
isRecording: false,
|
|
1117
|
+
hasRecordedVideo: false,
|
|
1118
|
+
error: null
|
|
1119
|
+
},
|
|
1120
|
+
sessionReplay: {
|
|
1121
|
+
available: false,
|
|
1122
|
+
canAttach: false,
|
|
1123
|
+
enabled: true,
|
|
1124
|
+
error: null
|
|
1125
|
+
},
|
|
1126
|
+
formSchema: null,
|
|
1127
|
+
filterSchema: null,
|
|
1128
|
+
error: null
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function assertScreenshotCaptureOptions(options) {
|
|
1132
|
+
const legacyOptions = options;
|
|
1133
|
+
if (!legacyOptions) return;
|
|
1134
|
+
if (legacyOptions.mediaMode && legacyOptions.mediaMode !== "screenshot") {
|
|
1135
|
+
throw new IssueStickerHeadlessError(
|
|
1136
|
+
"VALIDATION_ERROR",
|
|
1137
|
+
"영상 캡쳐는 startVideoCapture()와 stopVideoCapture()를 사용해야 합니다."
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if ("videoFile" in legacyOptions) {
|
|
1141
|
+
throw new IssueStickerHeadlessError(
|
|
1142
|
+
"VALIDATION_ERROR",
|
|
1143
|
+
"headless API는 videoFile을 직접 받지 않습니다. startVideoCapture()로 녹화하세요."
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
if ("sessionReplay" in legacyOptions) {
|
|
1147
|
+
throw new IssueStickerHeadlessError(
|
|
1148
|
+
"VALIDATION_ERROR",
|
|
1149
|
+
"세션 리플레이 첨부 여부는 setSessionReplayEnabled()로 설정해야 합니다."
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
class HeadlessClientState {
|
|
1154
|
+
constructor() {
|
|
1155
|
+
this.snapshot = createInitialHeadlessSnapshot();
|
|
1156
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1157
|
+
}
|
|
1158
|
+
get phase() {
|
|
1159
|
+
return this.snapshot.phase;
|
|
1160
|
+
}
|
|
1161
|
+
getSnapshot() {
|
|
1162
|
+
return this.snapshot;
|
|
1163
|
+
}
|
|
1164
|
+
subscribe(listener) {
|
|
1165
|
+
this.listeners.add(listener);
|
|
1166
|
+
listener(this.snapshot);
|
|
1167
|
+
return () => this.listeners.delete(listener);
|
|
1168
|
+
}
|
|
1169
|
+
setSnapshot(patch) {
|
|
1170
|
+
this.snapshot = {
|
|
1171
|
+
...this.snapshot,
|
|
1172
|
+
...patch,
|
|
1173
|
+
auth: patch.auth ? { ...this.snapshot.auth, ...patch.auth } : this.snapshot.auth
|
|
1174
|
+
};
|
|
1175
|
+
this.listeners.forEach((listener) => listener(this.snapshot));
|
|
1176
|
+
}
|
|
1177
|
+
reset() {
|
|
1178
|
+
this.listeners.clear();
|
|
1179
|
+
this.snapshot = createInitialHeadlessSnapshot();
|
|
1180
|
+
}
|
|
1181
|
+
applyBridgeToken(result) {
|
|
1182
|
+
var _a, _b, _c;
|
|
1183
|
+
this.setSnapshot({
|
|
1184
|
+
auth: {
|
|
1185
|
+
mode: "member",
|
|
1186
|
+
isReady: true,
|
|
1187
|
+
canUseIntegrations: true,
|
|
1188
|
+
tenantId: ((_a = result.context) == null ? void 0 : _a.tenantId) || this.snapshot.auth.tenantId,
|
|
1189
|
+
projectId: ((_b = result.context) == null ? void 0 : _b.projectId) || this.snapshot.auth.projectId,
|
|
1190
|
+
accountId: ((_c = result.context) == null ? void 0 : _c.accountId) || this.snapshot.auth.accountId
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
setAuthMode(mode) {
|
|
1195
|
+
this.setSnapshot({
|
|
1196
|
+
auth: {
|
|
1197
|
+
...this.snapshot.auth,
|
|
1198
|
+
mode,
|
|
1199
|
+
canUseIntegrations: mode === "member",
|
|
1200
|
+
accountId: mode === "member" ? this.snapshot.auth.accountId : null
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
requireProjectId() {
|
|
1205
|
+
const projectId = this.snapshot.auth.projectId;
|
|
1206
|
+
if (!projectId) {
|
|
1207
|
+
throw new IssueStickerHeadlessError(
|
|
1208
|
+
"NOT_INITIALIZED",
|
|
1209
|
+
"프로젝트 컨텍스트가 준비되지 않았습니다."
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return projectId;
|
|
1213
|
+
}
|
|
1214
|
+
requireDraft() {
|
|
1215
|
+
if (!this.snapshot.draft) {
|
|
1216
|
+
throw new IssueStickerHeadlessError("INVALID_PHASE", "작성 중인 이슈가 없습니다.");
|
|
1217
|
+
}
|
|
1218
|
+
return this.snapshot.draft;
|
|
1219
|
+
}
|
|
1220
|
+
assertPhase(allowed, action) {
|
|
1221
|
+
if (!allowed.includes(this.snapshot.phase)) {
|
|
1222
|
+
throw new IssueStickerHeadlessError(
|
|
1223
|
+
"INVALID_PHASE",
|
|
1224
|
+
`${action}은 현재 phase(${this.snapshot.phase})에서 호출할 수 없습니다.`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function buildIssueCreatePayload({
|
|
1230
|
+
draft,
|
|
1231
|
+
auth,
|
|
1232
|
+
config,
|
|
1233
|
+
projectId,
|
|
1234
|
+
uploads,
|
|
1235
|
+
logs,
|
|
1236
|
+
currentUrl,
|
|
1237
|
+
userAgent
|
|
1238
|
+
}) {
|
|
1239
|
+
var _a, _b;
|
|
1240
|
+
return sanitizeString({
|
|
1241
|
+
url: currentUrl,
|
|
1242
|
+
title: draft.title.trim(),
|
|
1243
|
+
description: draft.description.trim(),
|
|
1244
|
+
position: draft.position,
|
|
1245
|
+
...uploads.screenshotUploadIds.length > 0 ? { screenshot_upload_ids: uploads.screenshotUploadIds } : {},
|
|
1246
|
+
...uploads.videoUpload ? { video_upload_id: uploads.videoUpload.id } : {},
|
|
1247
|
+
...uploads.replayUpload ? {
|
|
1248
|
+
session_replay_upload_id: uploads.replayUpload.uploadId,
|
|
1249
|
+
session_replay_meta: uploads.replayUpload.meta
|
|
1250
|
+
} : {},
|
|
1251
|
+
network_logs: logs.networkLogs.length > 0 ? logs.networkLogs : void 0,
|
|
1252
|
+
console_logs: logs.consoleLogs.length > 0 ? logs.consoleLogs : void 0,
|
|
1253
|
+
user_agent: userAgent,
|
|
1254
|
+
tenant_id: auth.tenantId,
|
|
1255
|
+
project_id: projectId,
|
|
1256
|
+
account_id: auth.accountId ?? void 0,
|
|
1257
|
+
external_user_id: ((_a = config.user) == null ? void 0 : _a.id) ?? null,
|
|
1258
|
+
external_user_name: ((_b = config.user) == null ? void 0 : _b.name) ?? null
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async function submitHeadlessIssueDraft({
|
|
1262
|
+
draft,
|
|
1263
|
+
pendingVideoFile,
|
|
1264
|
+
transport,
|
|
1265
|
+
issueRepository,
|
|
1266
|
+
formSchema,
|
|
1267
|
+
auth,
|
|
1268
|
+
config,
|
|
1269
|
+
projectId,
|
|
1270
|
+
logs,
|
|
1271
|
+
currentUrl,
|
|
1272
|
+
userAgent,
|
|
1273
|
+
uploadSessionReplaySnapshot
|
|
1274
|
+
}) {
|
|
1275
|
+
var _a;
|
|
1276
|
+
const screenshotUploads = draft.mediaMode === "screenshot" ? await transport.uploadScreenshotPreviews(draft.screenshotPreviewUrls) : [];
|
|
1277
|
+
const screenshotUploadIds = screenshotUploads.map((upload) => upload.id);
|
|
1278
|
+
const videoUpload = draft.mediaMode === "video" && pendingVideoFile ? await transport.uploadSignedMedia(pendingVideoFile, "video") : null;
|
|
1279
|
+
const replayUpload = draft.sessionReplayEnabled ? await uploadSessionReplaySnapshot() : null;
|
|
1280
|
+
const issueData = buildIssueCreatePayload({
|
|
1281
|
+
draft,
|
|
1282
|
+
auth,
|
|
1283
|
+
config,
|
|
1284
|
+
projectId,
|
|
1285
|
+
uploads: { screenshotUploadIds, videoUpload, replayUpload },
|
|
1286
|
+
logs,
|
|
1287
|
+
currentUrl,
|
|
1288
|
+
userAgent
|
|
1289
|
+
});
|
|
1290
|
+
const createdIssue = await issueRepository.createIssue(issueData);
|
|
1291
|
+
await issueRepository.syncDynamicFields(createdIssue, draft, formSchema, {
|
|
1292
|
+
screenshotUrl: (_a = screenshotUploads[0]) == null ? void 0 : _a.url,
|
|
1293
|
+
videoUrl: videoUpload == null ? void 0 : videoUpload.url
|
|
1294
|
+
});
|
|
1295
|
+
return createdIssue;
|
|
1296
|
+
}
|
|
1297
|
+
const HEADLESS_CAPTURE_STYLE_ID = "issue-sticker-headless-capture-style";
|
|
1298
|
+
const HEADLESS_CAPTURE_STYLE = `
|
|
1299
|
+
body.issue-sticker-mode-active,
|
|
1300
|
+
body.issue-sticker-mode-active * {
|
|
1301
|
+
cursor: crosshair !important;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
body.issue-sticker-mode-active [data-issue-sticker-capture-ignore="true"],
|
|
1305
|
+
body.issue-sticker-mode-active [data-issue-sticker-capture-ignore="true"] *,
|
|
1306
|
+
body.issue-sticker-mode-active #issue-sticker-host,
|
|
1307
|
+
body.issue-sticker-mode-active #issue-sticker-host * {
|
|
1308
|
+
cursor: default !important;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
body.issue-sticker-mode-active .issue-sticker-mode-highlight {
|
|
1312
|
+
outline: 3px solid #00856f !important;
|
|
1313
|
+
outline-offset: 3px !important;
|
|
1314
|
+
box-shadow:
|
|
1315
|
+
0 0 0 7px rgba(0, 133, 111, 0.18),
|
|
1316
|
+
0 10px 28px rgba(0, 0, 0, 0.26) !important;
|
|
1317
|
+
transition:
|
|
1318
|
+
outline-color 120ms ease,
|
|
1319
|
+
box-shadow 120ms ease !important;
|
|
1320
|
+
}
|
|
1321
|
+
`;
|
|
1322
|
+
function isHTMLElement(value) {
|
|
1323
|
+
return typeof HTMLElement !== "undefined" && value instanceof HTMLElement;
|
|
1324
|
+
}
|
|
1325
|
+
function resolveHeadlessCaptureTarget(target) {
|
|
1326
|
+
if (!isHTMLElement(target)) return null;
|
|
1327
|
+
return target.closest('[data-issue-sticker-capture-target="true"]') ?? target;
|
|
1328
|
+
}
|
|
1329
|
+
function ensureHeadlessCaptureStyles() {
|
|
1330
|
+
if (typeof document === "undefined") return;
|
|
1331
|
+
if (document.getElementById(HEADLESS_CAPTURE_STYLE_ID)) return;
|
|
1332
|
+
const style = document.createElement("style");
|
|
1333
|
+
style.id = HEADLESS_CAPTURE_STYLE_ID;
|
|
1334
|
+
style.textContent = HEADLESS_CAPTURE_STYLE;
|
|
1335
|
+
document.head.appendChild(style);
|
|
1336
|
+
}
|
|
1337
|
+
function startHeadlessTargetSelection({
|
|
1338
|
+
isIgnoredTarget,
|
|
1339
|
+
onCapture
|
|
1340
|
+
}) {
|
|
1341
|
+
ensureHeadlessCaptureStyles();
|
|
1342
|
+
document.body.classList.add("issue-sticker-mode-active");
|
|
1343
|
+
let highlightedElement = null;
|
|
1344
|
+
const clearHighlight = () => {
|
|
1345
|
+
highlightedElement == null ? void 0 : highlightedElement.classList.remove("issue-sticker-mode-highlight");
|
|
1346
|
+
highlightedElement = null;
|
|
1347
|
+
};
|
|
1348
|
+
const handleMouseOver = (event) => {
|
|
1349
|
+
const target = resolveHeadlessCaptureTarget(event.target);
|
|
1350
|
+
if (!target || isIgnoredTarget(target)) return;
|
|
1351
|
+
clearHighlight();
|
|
1352
|
+
target.classList.add("issue-sticker-mode-highlight");
|
|
1353
|
+
highlightedElement = target;
|
|
1354
|
+
};
|
|
1355
|
+
const handleMouseOut = (event) => {
|
|
1356
|
+
if (event.target === highlightedElement) {
|
|
1357
|
+
clearHighlight();
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
const handleClick = (event) => {
|
|
1361
|
+
const clickTarget = resolveHeadlessCaptureTarget(event.target);
|
|
1362
|
+
if (!clickTarget || isIgnoredTarget(clickTarget)) return;
|
|
1363
|
+
event.preventDefault();
|
|
1364
|
+
event.stopImmediatePropagation();
|
|
1365
|
+
onCapture(highlightedElement ?? clickTarget, event);
|
|
1366
|
+
};
|
|
1367
|
+
document.addEventListener("mouseover", handleMouseOver, { capture: true, passive: true });
|
|
1368
|
+
document.addEventListener("mouseout", handleMouseOut, { capture: true, passive: true });
|
|
1369
|
+
document.addEventListener("click", handleClick, { capture: true, passive: false });
|
|
1370
|
+
return () => {
|
|
1371
|
+
document.removeEventListener("mouseover", handleMouseOver, {
|
|
1372
|
+
capture: true
|
|
1373
|
+
});
|
|
1374
|
+
document.removeEventListener("mouseout", handleMouseOut, {
|
|
1375
|
+
capture: true
|
|
1376
|
+
});
|
|
1377
|
+
document.removeEventListener("click", handleClick, { capture: true });
|
|
1378
|
+
clearHighlight();
|
|
1379
|
+
document.body.classList.remove("issue-sticker-mode-active");
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
const DEFAULT_EXCLUDE_SELECTORS = [
|
|
1383
|
+
'[data-issue-sticker-capture-ignore="true"]',
|
|
1384
|
+
"#issue-sticker-host",
|
|
1385
|
+
'[data-issue-sticker-headless-auth="true"]'
|
|
1386
|
+
];
|
|
1387
|
+
class HeadlessClientImpl {
|
|
1388
|
+
constructor(config) {
|
|
1389
|
+
var _a;
|
|
1390
|
+
this.config = config;
|
|
1391
|
+
this.state = new HeadlessClientState();
|
|
1392
|
+
this.pendingMedia = new HeadlessPendingMedia();
|
|
1393
|
+
this.initializePromise = null;
|
|
1394
|
+
this.captureCleanup = null;
|
|
1395
|
+
this.captureSession = null;
|
|
1396
|
+
this.suppressNextVideoStopSelection = false;
|
|
1397
|
+
if (!((_a = config.apiKey) == null ? void 0 : _a.trim())) {
|
|
1398
|
+
throw new IssueStickerHeadlessError("VALIDATION_ERROR", "apiKey가 필요합니다.");
|
|
1399
|
+
}
|
|
1400
|
+
this.authBridge = new HeadlessAuthBridge({
|
|
1401
|
+
apiKey: config.apiKey,
|
|
1402
|
+
getTenantId: () => this.snapshot.auth.tenantId,
|
|
1403
|
+
getProjectId: () => this.snapshot.auth.projectId,
|
|
1404
|
+
onToken: (result) => this.state.applyBridgeToken(result)
|
|
1405
|
+
});
|
|
1406
|
+
this.transport = new HeadlessTransport({
|
|
1407
|
+
getAuthMode: () => this.snapshot.auth.mode,
|
|
1408
|
+
resolveAuthorizationHeader: (forceRefreshMemberToken) => this.resolveAuthorizationHeader(forceRefreshMemberToken)
|
|
1409
|
+
});
|
|
1410
|
+
this.issueRepository = new HeadlessIssueRepository({
|
|
1411
|
+
transport: this.transport,
|
|
1412
|
+
getProjectId: () => this.state.requireProjectId(),
|
|
1413
|
+
getAuthMode: () => this.snapshot.auth.mode,
|
|
1414
|
+
getDefaultExternalUserId: () => {
|
|
1415
|
+
var _a2;
|
|
1416
|
+
return (_a2 = this.config.user) == null ? void 0 : _a2.id;
|
|
1417
|
+
},
|
|
1418
|
+
getCurrentUrl: () => typeof window !== "undefined" ? window.location.href : void 0
|
|
1419
|
+
});
|
|
1420
|
+
this.logCollectors = new HeadlessLogCollectors(this.config, getApiBaseUrl);
|
|
1421
|
+
this.screenRecordingController = new ScreenRecordingController({
|
|
1422
|
+
onStopped: (file) => {
|
|
1423
|
+
void this.handleRecordedVideoStopped(file);
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
this.screenRecordingController.subscribe((recording) => {
|
|
1427
|
+
this.state.setSnapshot({
|
|
1428
|
+
videoCapture: {
|
|
1429
|
+
isRecording: recording.isRecording,
|
|
1430
|
+
hasRecordedVideo: Boolean(recording.recordedFile || this.pendingMedia.hasVideo),
|
|
1431
|
+
error: recording.error
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1435
|
+
this.sessionReplayController = new SessionReplayCaptureController({
|
|
1436
|
+
loadCapabilities: () => this.request("/api/session-replays/capabilities")
|
|
1437
|
+
});
|
|
1438
|
+
this.sessionReplayController.subscribe((sessionReplay) => {
|
|
1439
|
+
this.applySessionReplaySnapshot(sessionReplay);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
get snapshot() {
|
|
1443
|
+
return this.state.getSnapshot();
|
|
1444
|
+
}
|
|
1445
|
+
get phase() {
|
|
1446
|
+
return this.state.phase;
|
|
1447
|
+
}
|
|
1448
|
+
getSnapshot() {
|
|
1449
|
+
return this.state.getSnapshot();
|
|
1450
|
+
}
|
|
1451
|
+
subscribe(listener) {
|
|
1452
|
+
return this.state.subscribe(listener);
|
|
1453
|
+
}
|
|
1454
|
+
async initialize() {
|
|
1455
|
+
if (this.snapshot.auth.isReady) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (this.initializePromise) {
|
|
1459
|
+
return this.initializePromise;
|
|
1460
|
+
}
|
|
1461
|
+
this.initializePromise = this.initializeInternal().finally(() => {
|
|
1462
|
+
this.initializePromise = null;
|
|
1463
|
+
});
|
|
1464
|
+
return this.initializePromise;
|
|
1465
|
+
}
|
|
1466
|
+
async refreshIssues(filters = {}) {
|
|
1467
|
+
await this.ensureInitialized();
|
|
1468
|
+
this.state.assertPhase(["idle", "viewingIssue"], "refreshIssues");
|
|
1469
|
+
this.state.setSnapshot({ phase: "loadingIssues", error: null });
|
|
1470
|
+
try {
|
|
1471
|
+
const issues = await this.issueRepository.fetchAllIssueStickers(filters);
|
|
1472
|
+
this.state.setSnapshot({ issues, phase: "idle" });
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
const nextError = this.toHeadlessError(error);
|
|
1475
|
+
this.state.setSnapshot({ phase: "idle", error: nextError });
|
|
1476
|
+
throw nextError;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async refreshFilterSchema() {
|
|
1480
|
+
await this.ensureInitialized();
|
|
1481
|
+
try {
|
|
1482
|
+
this.state.setSnapshot({ filterSchema: await this.issueRepository.loadFilterSchema() });
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
this.state.setSnapshot({
|
|
1485
|
+
filterSchema: { groups: [], unavailableReason: "loading-failed" },
|
|
1486
|
+
error: this.toHeadlessError(error)
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async openIssue(issueId) {
|
|
1491
|
+
await this.ensureInitialized();
|
|
1492
|
+
this.state.assertPhase(["idle", "viewingIssue"], "openIssue");
|
|
1493
|
+
this.state.setSnapshot({ phase: "viewingIssue", error: null });
|
|
1494
|
+
try {
|
|
1495
|
+
const activeIssue = await this.issueRepository.openIssue(issueId);
|
|
1496
|
+
this.state.setSnapshot({ activeIssue, phase: "viewingIssue" });
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
const nextError = this.toHeadlessError(error);
|
|
1499
|
+
this.state.setSnapshot({ activeIssue: null, phase: "idle", error: nextError });
|
|
1500
|
+
throw nextError;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
closeIssue() {
|
|
1504
|
+
if (this.snapshot.phase === "viewingIssue") {
|
|
1505
|
+
this.state.setSnapshot({ activeIssue: null, phase: "idle" });
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async startIssueCapture(options = {}) {
|
|
1509
|
+
await this.ensureInitialized();
|
|
1510
|
+
this.state.assertPhase(["idle", "viewingIssue"], "startIssueCapture");
|
|
1511
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
1512
|
+
throw new IssueStickerHeadlessError(
|
|
1513
|
+
"CAPTURE_FAILED",
|
|
1514
|
+
"브라우저 DOM에서만 이슈 캡쳐를 시작할 수 있습니다."
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
assertScreenshotCaptureOptions(options);
|
|
1518
|
+
this.closeIssue();
|
|
1519
|
+
this.clearCaptureListeners();
|
|
1520
|
+
this.clearPendingMedia();
|
|
1521
|
+
this.beginTargetSelection();
|
|
1522
|
+
}
|
|
1523
|
+
async startVideoCapture() {
|
|
1524
|
+
await this.ensureInitialized();
|
|
1525
|
+
this.state.assertPhase(["idle", "viewingIssue"], "startVideoCapture");
|
|
1526
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
1527
|
+
throw new IssueStickerHeadlessError(
|
|
1528
|
+
"CAPTURE_FAILED",
|
|
1529
|
+
"브라우저 DOM에서만 영상 캡쳐를 시작할 수 있습니다."
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
this.closeIssue();
|
|
1533
|
+
this.clearCaptureListeners();
|
|
1534
|
+
this.clearPendingMedia();
|
|
1535
|
+
this.captureSession = null;
|
|
1536
|
+
this.suppressNextVideoStopSelection = false;
|
|
1537
|
+
this.state.setSnapshot({ phase: "recordingVideo", draft: null, formSchema: null, error: null });
|
|
1538
|
+
try {
|
|
1539
|
+
await this.screenRecordingController.startRecording();
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
const nextError = this.toHeadlessError(error, "CAPTURE_FAILED");
|
|
1542
|
+
this.clearPendingMedia();
|
|
1543
|
+
this.state.setSnapshot({ phase: "idle", error: nextError });
|
|
1544
|
+
throw nextError;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async stopVideoCapture() {
|
|
1548
|
+
await this.ensureInitialized();
|
|
1549
|
+
this.state.assertPhase(["recordingVideo"], "stopVideoCapture");
|
|
1550
|
+
const file = await this.screenRecordingController.stopRecording();
|
|
1551
|
+
if (!file && this.snapshot.phase === "recordingVideo") {
|
|
1552
|
+
this.clearPendingMedia();
|
|
1553
|
+
this.state.setSnapshot({ phase: "idle" });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
setSessionReplayEnabled(enabled) {
|
|
1557
|
+
this.sessionReplayController.setEnabled(enabled);
|
|
1558
|
+
}
|
|
1559
|
+
beginTargetSelection() {
|
|
1560
|
+
if (typeof document === "undefined") {
|
|
1561
|
+
throw new IssueStickerHeadlessError(
|
|
1562
|
+
"CAPTURE_FAILED",
|
|
1563
|
+
"브라우저 DOM에서만 이슈 캡쳐를 시작할 수 있습니다."
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
preloadModernScreenshot();
|
|
1567
|
+
this.captureSession = null;
|
|
1568
|
+
this.state.setSnapshot({
|
|
1569
|
+
phase: "selectingTarget",
|
|
1570
|
+
draft: null,
|
|
1571
|
+
formSchema: null,
|
|
1572
|
+
error: null
|
|
1573
|
+
});
|
|
1574
|
+
this.captureCleanup = startHeadlessTargetSelection({
|
|
1575
|
+
isIgnoredTarget: (target) => this.isIgnoredTarget(target),
|
|
1576
|
+
onCapture: (target, event) => {
|
|
1577
|
+
void this.captureTarget(target, event);
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
cancelIssueCapture() {
|
|
1582
|
+
if (this.snapshot.phase !== "recordingVideo" && this.snapshot.phase !== "selectingTarget" && this.snapshot.phase !== "draftingIssue") {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
this.clearCaptureListeners();
|
|
1586
|
+
if (this.snapshot.phase === "recordingVideo") {
|
|
1587
|
+
this.suppressNextVideoStopSelection = true;
|
|
1588
|
+
this.screenRecordingController.discard();
|
|
1589
|
+
}
|
|
1590
|
+
this.clearPendingMedia();
|
|
1591
|
+
this.captureSession = null;
|
|
1592
|
+
this.state.setSnapshot({ phase: "idle", draft: null, formSchema: null, error: null });
|
|
1593
|
+
}
|
|
1594
|
+
async submitDraft(input) {
|
|
1595
|
+
await this.ensureInitialized();
|
|
1596
|
+
this.state.assertPhase(["draftingIssue"], "submitDraft");
|
|
1597
|
+
this.state.requireDraft();
|
|
1598
|
+
if (!input.title.trim()) {
|
|
1599
|
+
throw new IssueStickerHeadlessError("VALIDATION_ERROR", "제목을 입력해주세요.");
|
|
1600
|
+
}
|
|
1601
|
+
if (!input.description.trim()) {
|
|
1602
|
+
throw new IssueStickerHeadlessError("VALIDATION_ERROR", "설명을 입력해주세요.");
|
|
1603
|
+
}
|
|
1604
|
+
this.state.setSnapshot({ phase: "submittingIssue", error: null });
|
|
1605
|
+
try {
|
|
1606
|
+
const session = this.captureSession;
|
|
1607
|
+
const screenshotPreviewUrls = (session == null ? void 0 : session.screenshots) ? await session.screenshots : this.state.requireDraft().screenshotPreviewUrls;
|
|
1608
|
+
const formSchema = this.snapshot.formSchema ?? ((session == null ? void 0 : session.formSchema) ? await session.formSchema : null);
|
|
1609
|
+
const submission = {
|
|
1610
|
+
...this.state.requireDraft(),
|
|
1611
|
+
screenshotPreviewUrls,
|
|
1612
|
+
title: input.title,
|
|
1613
|
+
description: input.description,
|
|
1614
|
+
dynamicFields: input.dynamicFields ?? {}
|
|
1615
|
+
};
|
|
1616
|
+
const createdIssue = await submitHeadlessIssueDraft({
|
|
1617
|
+
draft: submission,
|
|
1618
|
+
pendingVideoFile: this.pendingMedia.videoFile,
|
|
1619
|
+
transport: this.transport,
|
|
1620
|
+
issueRepository: this.issueRepository,
|
|
1621
|
+
formSchema,
|
|
1622
|
+
auth: this.snapshot.auth,
|
|
1623
|
+
config: this.config,
|
|
1624
|
+
projectId: this.state.requireProjectId(),
|
|
1625
|
+
logs: this.logCollectors.collect(),
|
|
1626
|
+
currentUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
1627
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
1628
|
+
uploadSessionReplaySnapshot: () => this.uploadSessionReplaySnapshot()
|
|
1629
|
+
});
|
|
1630
|
+
this.clearCaptureListeners();
|
|
1631
|
+
this.clearPendingMedia();
|
|
1632
|
+
this.captureSession = null;
|
|
1633
|
+
this.state.setSnapshot({
|
|
1634
|
+
phase: "idle",
|
|
1635
|
+
draft: null,
|
|
1636
|
+
formSchema: null,
|
|
1637
|
+
issues: [createdIssue, ...this.snapshot.issues]
|
|
1638
|
+
});
|
|
1639
|
+
return createdIssue;
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
const nextError = this.toHeadlessError(error);
|
|
1642
|
+
this.state.setSnapshot({ phase: "draftingIssue", error: nextError });
|
|
1643
|
+
throw nextError;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
async loginAsMember() {
|
|
1647
|
+
await this.ensureInitialized();
|
|
1648
|
+
if (!this.authBridge.canUseBridge) {
|
|
1649
|
+
throw new IssueStickerHeadlessError(
|
|
1650
|
+
"MEMBER_AUTH_REQUIRED",
|
|
1651
|
+
"SDK auth bridge를 사용할 수 없습니다."
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const silentToken = await this.authBridge.getValidMemberToken({ forceRefresh: true }).catch(() => null);
|
|
1655
|
+
if (silentToken) {
|
|
1656
|
+
this.state.setAuthMode("member");
|
|
1657
|
+
await this.refreshFilterSchema();
|
|
1658
|
+
await this.sessionReplayController.refresh();
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
await this.authBridge.openInteractiveLoginFrame();
|
|
1662
|
+
await this.refreshFilterSchema();
|
|
1663
|
+
await this.sessionReplayController.refresh();
|
|
1664
|
+
}
|
|
1665
|
+
async logoutMember() {
|
|
1666
|
+
await this.ensureInitialized();
|
|
1667
|
+
this.authBridge.clearMemberToken();
|
|
1668
|
+
this.state.setAuthMode("public");
|
|
1669
|
+
this.authBridge.postLogout();
|
|
1670
|
+
this.state.setSnapshot({
|
|
1671
|
+
filterSchema: { groups: [], unavailableReason: "public-mode" },
|
|
1672
|
+
formSchema: null
|
|
1673
|
+
});
|
|
1674
|
+
void this.sessionReplayController.refresh();
|
|
1675
|
+
}
|
|
1676
|
+
destroy() {
|
|
1677
|
+
this.clearCaptureListeners();
|
|
1678
|
+
this.suppressNextVideoStopSelection = true;
|
|
1679
|
+
this.captureSession = null;
|
|
1680
|
+
this.screenRecordingController.destroy();
|
|
1681
|
+
this.sessionReplayController.destroy();
|
|
1682
|
+
this.clearPendingMedia();
|
|
1683
|
+
this.logCollectors.stop();
|
|
1684
|
+
this.authBridge.destroy();
|
|
1685
|
+
this.state.reset();
|
|
1686
|
+
}
|
|
1687
|
+
async initializeInternal() {
|
|
1688
|
+
this.state.setSnapshot({ phase: "initializing", error: null });
|
|
1689
|
+
try {
|
|
1690
|
+
const result = await this.request("/api/sdk/auth/public-init", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: { apiKey: this.config.apiKey },
|
|
1693
|
+
skipAuth: true
|
|
1694
|
+
});
|
|
1695
|
+
this.authBridge.setAuthBridgeUrl(result.authBridgeUrl ?? null);
|
|
1696
|
+
this.state.setSnapshot({
|
|
1697
|
+
phase: "idle",
|
|
1698
|
+
auth: {
|
|
1699
|
+
mode: "public",
|
|
1700
|
+
isReady: true,
|
|
1701
|
+
canUseIntegrations: false,
|
|
1702
|
+
tenantId: result.tenantId,
|
|
1703
|
+
projectId: result.projectId,
|
|
1704
|
+
accountId: null
|
|
1705
|
+
},
|
|
1706
|
+
filterSchema: { groups: [], unavailableReason: "public-mode" }
|
|
1707
|
+
});
|
|
1708
|
+
this.logCollectors.start();
|
|
1709
|
+
this.authBridge.mountHiddenBridgeFrame();
|
|
1710
|
+
void this.sessionReplayController.initialize();
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
const nextError = this.toHeadlessError(error);
|
|
1713
|
+
this.state.setSnapshot({ phase: "idle", error: nextError });
|
|
1714
|
+
throw nextError;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
async takeSessionReplayPreview() {
|
|
1718
|
+
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1719
|
+
if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
|
|
1720
|
+
try {
|
|
1721
|
+
const snapshot = await this.sessionReplayController.takeSnapshot();
|
|
1722
|
+
return snapshot ? createSessionReplayMeta(snapshot) : null;
|
|
1723
|
+
} catch {
|
|
1724
|
+
return null;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async uploadSessionReplaySnapshot() {
|
|
1728
|
+
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1729
|
+
if (!sessionReplay.enabled || !sessionReplay.canAttach) return null;
|
|
1730
|
+
try {
|
|
1731
|
+
const snapshot = await this.sessionReplayController.takeSnapshot();
|
|
1732
|
+
if (!snapshot) return null;
|
|
1733
|
+
if (snapshot.eventCount <= 0) return null;
|
|
1734
|
+
const upload = await this.transport.uploadSignedMedia(createReplayFile(snapshot), "replay");
|
|
1735
|
+
return {
|
|
1736
|
+
uploadId: upload.id,
|
|
1737
|
+
url: upload.url,
|
|
1738
|
+
meta: createSessionReplayMeta(snapshot)
|
|
1739
|
+
};
|
|
1740
|
+
} catch {
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
async handleRecordedVideoStopped(file) {
|
|
1745
|
+
if (this.suppressNextVideoStopSelection) {
|
|
1746
|
+
this.suppressNextVideoStopSelection = false;
|
|
1747
|
+
this.clearPendingMedia();
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
this.pendingMedia.setVideoFile(file);
|
|
1751
|
+
this.state.setSnapshot({
|
|
1752
|
+
videoCapture: {
|
|
1753
|
+
isRecording: false,
|
|
1754
|
+
hasRecordedVideo: true,
|
|
1755
|
+
error: null
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
if (this.snapshot.phase === "recordingVideo") {
|
|
1759
|
+
try {
|
|
1760
|
+
this.beginTargetSelection();
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
const nextError = this.toHeadlessError(error, "CAPTURE_FAILED");
|
|
1763
|
+
this.clearPendingMedia();
|
|
1764
|
+
this.state.setSnapshot({ phase: "idle", error: nextError });
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
applySessionReplaySnapshot(sessionReplay) {
|
|
1769
|
+
const canAttachCurrentDraft = Boolean(sessionReplay.enabled && sessionReplay.canAttach);
|
|
1770
|
+
this.state.setSnapshot({
|
|
1771
|
+
sessionReplay: {
|
|
1772
|
+
available: sessionReplay.available,
|
|
1773
|
+
canAttach: sessionReplay.canAttach,
|
|
1774
|
+
enabled: sessionReplay.enabled,
|
|
1775
|
+
error: sessionReplay.error
|
|
1776
|
+
},
|
|
1777
|
+
draft: this.snapshot.draft ? {
|
|
1778
|
+
...this.snapshot.draft,
|
|
1779
|
+
sessionReplayEnabled: canAttachCurrentDraft,
|
|
1780
|
+
sessionReplayPreview: canAttachCurrentDraft ? this.snapshot.draft.sessionReplayPreview : null
|
|
1781
|
+
} : this.snapshot.draft
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
clearPendingMedia() {
|
|
1785
|
+
this.pendingMedia.clear();
|
|
1786
|
+
this.screenRecordingController.clearRecordedFile();
|
|
1787
|
+
this.state.setSnapshot({
|
|
1788
|
+
videoCapture: {
|
|
1789
|
+
isRecording: this.screenRecordingController.getSnapshot().isRecording,
|
|
1790
|
+
hasRecordedVideo: false,
|
|
1791
|
+
error: this.screenRecordingController.getSnapshot().error
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
captureTarget(target, event) {
|
|
1796
|
+
if (this.snapshot.phase !== "selectingTarget") {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
this.clearCaptureListeners();
|
|
1800
|
+
const sessionReplay = this.sessionReplayController.getSnapshot();
|
|
1801
|
+
const draft = buildHeadlessCaptureContext({
|
|
1802
|
+
target,
|
|
1803
|
+
event,
|
|
1804
|
+
videoFile: this.pendingMedia.videoFile,
|
|
1805
|
+
createVideoPreviewUrl: (file) => this.pendingMedia.createVideoPreviewUrl(file),
|
|
1806
|
+
sessionReplayEnabled: Boolean(sessionReplay.enabled && sessionReplay.canAttach)
|
|
1807
|
+
});
|
|
1808
|
+
this.state.setSnapshot({ phase: "draftingIssue", draft, formSchema: null, error: null });
|
|
1809
|
+
this.beginBackgroundCapturePreparation(draft, target);
|
|
1810
|
+
}
|
|
1811
|
+
beginBackgroundCapturePreparation(draft, target) {
|
|
1812
|
+
var _a, _b;
|
|
1813
|
+
const token = {};
|
|
1814
|
+
this.captureSession = {
|
|
1815
|
+
token,
|
|
1816
|
+
screenshots: draft.mediaMode === "screenshot" ? captureHeadlessScreenshots({ target, excludeSelectors: this.getExcludeSelectors() }) : null,
|
|
1817
|
+
formSchema: this.loadFormSchema({})
|
|
1818
|
+
};
|
|
1819
|
+
const isCurrentSession = () => {
|
|
1820
|
+
var _a2;
|
|
1821
|
+
return ((_a2 = this.captureSession) == null ? void 0 : _a2.token) === token;
|
|
1822
|
+
};
|
|
1823
|
+
void ((_a = this.captureSession.screenshots) == null ? void 0 : _a.then((screenshotPreviewUrls) => {
|
|
1824
|
+
if (!isCurrentSession()) return;
|
|
1825
|
+
this.mergeDraft({ screenshotPreviewUrls });
|
|
1826
|
+
}));
|
|
1827
|
+
void ((_b = this.captureSession.formSchema) == null ? void 0 : _b.then((formSchema) => {
|
|
1828
|
+
if (!isCurrentSession()) return;
|
|
1829
|
+
this.state.setSnapshot({ formSchema });
|
|
1830
|
+
}));
|
|
1831
|
+
if (draft.sessionReplayEnabled) {
|
|
1832
|
+
void this.takeSessionReplayPreview().then((sessionReplayPreview) => {
|
|
1833
|
+
var _a2;
|
|
1834
|
+
if (!isCurrentSession()) return;
|
|
1835
|
+
if (!((_a2 = this.snapshot.draft) == null ? void 0 : _a2.sessionReplayEnabled)) return;
|
|
1836
|
+
this.mergeDraft({ sessionReplayPreview });
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
mergeDraft(patch) {
|
|
1841
|
+
const draft = this.snapshot.draft;
|
|
1842
|
+
if (!draft) return;
|
|
1843
|
+
if (this.snapshot.phase !== "draftingIssue" && this.snapshot.phase !== "submittingIssue") {
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
this.state.setSnapshot({ draft: { ...draft, ...patch } });
|
|
1847
|
+
}
|
|
1848
|
+
async loadFormSchema(values) {
|
|
1849
|
+
try {
|
|
1850
|
+
return await buildIssueFormSchema(values, {
|
|
1851
|
+
authMode: this.snapshot.auth.mode,
|
|
1852
|
+
getActiveIntegration: () => this.issueRepository.getActiveIntegration(),
|
|
1853
|
+
getCustomFieldDefinitions: () => this.issueRepository.getCustomFieldDefinitions(),
|
|
1854
|
+
requestResponse: (endpoint) => this.issueRepository.requestResponse(endpoint),
|
|
1855
|
+
searchJiraProjectUsers: (query) => this.issueRepository.searchJiraProjectUsers(query),
|
|
1856
|
+
resolveJiraProjectUserLabel: (accountId) => this.issueRepository.resolveJiraProjectUserLabel(accountId),
|
|
1857
|
+
searchJiraProjectIssues: (query) => this.issueRepository.searchJiraProjectIssues(query),
|
|
1858
|
+
resolveJiraProjectIssueLabel: (issueKey) => this.issueRepository.resolveJiraProjectIssueLabel(issueKey),
|
|
1859
|
+
searchNotionPeople: (query) => this.issueRepository.searchNotionPeople(query)
|
|
1860
|
+
});
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
this.state.setSnapshot({ error: this.toHeadlessError(error) });
|
|
1863
|
+
return { fields: [], unavailableReason: "loading-failed" };
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async request(endpoint, options = {}) {
|
|
1867
|
+
return this.transport.request(endpoint, options);
|
|
1868
|
+
}
|
|
1869
|
+
async resolveAuthorizationHeader(forceRefreshMemberToken = false) {
|
|
1870
|
+
if (this.snapshot.auth.mode === "member") {
|
|
1871
|
+
const token = await this.authBridge.getValidMemberToken({
|
|
1872
|
+
forceRefresh: forceRefreshMemberToken
|
|
1873
|
+
});
|
|
1874
|
+
if (!token) {
|
|
1875
|
+
throw new IssueStickerHeadlessError("MEMBER_AUTH_REQUIRED", "멤버 로그인이 필요합니다.");
|
|
1876
|
+
}
|
|
1877
|
+
return `Bearer ${token}`;
|
|
1878
|
+
}
|
|
1879
|
+
return `ApiKey ${this.config.apiKey}`;
|
|
1880
|
+
}
|
|
1881
|
+
ensureInitialized() {
|
|
1882
|
+
return this.snapshot.auth.isReady ? Promise.resolve() : this.initialize();
|
|
1883
|
+
}
|
|
1884
|
+
clearCaptureListeners() {
|
|
1885
|
+
var _a;
|
|
1886
|
+
(_a = this.captureCleanup) == null ? void 0 : _a.call(this);
|
|
1887
|
+
this.captureCleanup = null;
|
|
1888
|
+
}
|
|
1889
|
+
isIgnoredTarget(target) {
|
|
1890
|
+
if (target.closest(".issue-sticker-marker")) return true;
|
|
1891
|
+
return this.getExcludeSelectors().some((selector) => target.closest(selector));
|
|
1892
|
+
}
|
|
1893
|
+
getExcludeSelectors() {
|
|
1894
|
+
var _a;
|
|
1895
|
+
return [...DEFAULT_EXCLUDE_SELECTORS, ...((_a = this.config.capture) == null ? void 0 : _a.excludeSelectors) ?? []];
|
|
1896
|
+
}
|
|
1897
|
+
toHeadlessError(error, fallbackCode = "NETWORK_ERROR") {
|
|
1898
|
+
if (error instanceof IssueStickerHeadlessError) {
|
|
1899
|
+
return error;
|
|
1900
|
+
}
|
|
1901
|
+
if (error instanceof Error) {
|
|
1902
|
+
return new IssueStickerHeadlessError(fallbackCode, error.message, error);
|
|
1903
|
+
}
|
|
1904
|
+
return new IssueStickerHeadlessError(fallbackCode, "알 수 없는 오류가 발생했습니다.", error);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function createIssueStickerHeadless(config) {
|
|
1908
|
+
return new HeadlessClientImpl(config);
|
|
1909
|
+
}
|
|
1910
|
+
export {
|
|
1911
|
+
IssueStickerHeadlessError,
|
|
1912
|
+
createIssueStickerHeadless,
|
|
1913
|
+
createIssueStickerHeadless as default
|
|
1914
|
+
};
|