connectbase-client 1.9.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -172,6 +172,136 @@ interface AnalyticsEvent {
172
172
  properties?: Record<string, unknown>;
173
173
  timestamp?: number;
174
174
  }
175
+ /**
176
+ * 인기 페이지 목록 응답.
177
+ *
178
+ * 백엔드 `dto.PopularPagesResponse` 와 1:1 매핑.
179
+ */
180
+ interface PopularPagesResponse {
181
+ pages: Array<{
182
+ page_path: string;
183
+ page_views: number;
184
+ }>;
185
+ start_date: number;
186
+ end_date: number;
187
+ }
188
+ /**
189
+ * 방문자 목록 응답.
190
+ *
191
+ * 백엔드 `dto.VisitorListResponse` 와 1:1 매핑. `app_member_id` 는 게스트 방문자에는
192
+ * 없을 수 있다.
193
+ */
194
+ interface VisitorListResponse {
195
+ visitors: Array<{
196
+ id: string;
197
+ visitor_uid: string;
198
+ app_member_id?: string;
199
+ total_visits: number;
200
+ total_page_views: number;
201
+ referrer?: string;
202
+ last_ip?: string;
203
+ country?: string;
204
+ is_bot: boolean;
205
+ first_visit_at: string;
206
+ last_visit_at: string;
207
+ }>;
208
+ total: number;
209
+ limit: number;
210
+ offset: number;
211
+ has_more: boolean;
212
+ }
213
+ /**
214
+ * 페이지 전환 플로우(Sankey) 응답.
215
+ *
216
+ * 백엔드 `dto.NavigationFlowResponse` 와 1:1 매핑. `nodes` 는 페이지, `links` 는 전환.
217
+ */
218
+ interface NavigationFlowResponse {
219
+ nodes: Array<{
220
+ id: string;
221
+ label: string;
222
+ value: number;
223
+ }>;
224
+ links: Array<{
225
+ source: string;
226
+ target: string;
227
+ value: number;
228
+ }>;
229
+ }
230
+ /**
231
+ * 조회 메서드 공통 옵션.
232
+ *
233
+ * `start_date` / `end_date` 는 백엔드가 정수형 epoch-day 또는 yyyymmdd 로 다루는 값
234
+ * (서비스 메서드 시그니처 기준 int). 0 또는 미지정 시 기본 기간을 사용.
235
+ */
236
+ interface AnalyticsRangeOptions {
237
+ start_date?: number;
238
+ end_date?: number;
239
+ limit?: number;
240
+ }
241
+ interface VisitorListOptions {
242
+ limit?: number;
243
+ offset?: number;
244
+ /**
245
+ * 백엔드가 인식하는 정렬 키 — 외 값은 silent 로 default(`last_visit`) 처리됩니다.
246
+ *
247
+ * 1.12.0 에서 백엔드 실제 동작에 맞춰 enum 정정 (1.10/1.11 의 `total_visits`/
248
+ * `total_page_views` 는 백엔드에서 인식되지 않아 항상 default 분기였음).
249
+ */
250
+ sort_by?: 'last_visit' | 'visits' | 'page_views' | 'first_visit';
251
+ }
252
+ /**
253
+ * 멤버별 합산 방문자 그룹 항목.
254
+ *
255
+ * 한 명의 회원이 여러 디바이스/브라우저로 접속했을 때 visitor row 들을 합쳐 단일 row.
256
+ * 익명 visitor 는 `app_member_id == undefined` 로 단일 row 그대로 노출되며 `visitor_count == 1`.
257
+ *
258
+ * `visitor_count` 는 "디바이스 수" 가 아닌 **"추적 브라우저 인스턴스 수"** 를 의미합니다.
259
+ * 같은 디바이스에서 시크릿모드 + 일반모드는 visitor 2 = visitor_count 2 로 카운트됩니다.
260
+ */
261
+ interface VisitorGroupItem {
262
+ app_member_id?: string;
263
+ /** 익명 visitor row 의 경우 단일 visitor_uid. 회원 그룹은 visitor_uids 참조. */
264
+ visitor_uid?: string;
265
+ /** 회원 그룹에 속한 visitor_uid 목록. 익명 row 는 undefined. */
266
+ visitor_uids?: string[];
267
+ visitor_count: number;
268
+ total_visits: number;
269
+ total_page_views: number;
270
+ first_visit_at: string;
271
+ last_visit_at: string;
272
+ country?: string;
273
+ is_bot: boolean;
274
+ }
275
+ interface VisitorGroupListResponse {
276
+ groups: VisitorGroupItem[];
277
+ total: number;
278
+ limit: number;
279
+ offset: number;
280
+ has_more: boolean;
281
+ }
282
+ interface VisitorByMemberResponse {
283
+ app_member_id: string;
284
+ visitor_uids: string[];
285
+ visitor_count: number;
286
+ total_visits: number;
287
+ total_page_views: number;
288
+ first_visit_at: string;
289
+ last_visit_at: string;
290
+ country?: string;
291
+ is_bot: boolean;
292
+ }
293
+ interface MergeVisitorsRequest {
294
+ source_visitor_uid: string;
295
+ /** target_visitor_uid 또는 target_member_id 중 하나는 필수. */
296
+ target_visitor_uid?: string;
297
+ target_member_id?: string;
298
+ }
299
+ interface MergeVisitorsResponse {
300
+ success: boolean;
301
+ target_visitor_id: string;
302
+ moved_records: number;
303
+ message?: string;
304
+ }
175
305
  declare class SessionManager {
176
306
  private _sessionId;
177
307
  private _visitorUid;
@@ -231,15 +361,38 @@ declare class AnalyticsAPI {
231
361
  */
232
362
  trackEvent(name: string, properties?: Record<string, unknown>): void;
233
363
  /**
234
- * 사용자 식별 (로그인 )
235
- * 이후 모든 방문 배치에 `app_member_id`가 첨부되어 게스트 방문자가 회원으로 연결됩니다.
364
+ * 사용자 식별 (로그인 직후 호출).
365
+ *
366
+ * 이후 모든 방문 배치에 `app_member_id` 가 첨부되어 새 활동은 회원으로 기록됩니다.
367
+ * 추가로 **현재 visitor_uid 의 기존 익명 활동을 즉시 회원에게 backfill** 하기 위해
368
+ * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
369
+ * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
370
+ *
371
+ * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * // 로그인 성공 직후
376
+ * await cb.auth.signIn({ email, password })
377
+ * cb.analytics.identify(member.id)
378
+ * ```
236
379
  */
237
380
  identify(memberId: string): void;
238
381
  /**
239
- * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출)
240
- * null 을 넘기면 익명 상태로 복귀 (로그아웃).
382
+ * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
383
+ *
384
+ * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
385
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
241
386
  */
242
387
  setMemberId(memberId: string | null): void;
388
+ /**
389
+ * 백엔드 link-member 엔드포인트 한 번 호출 — 즉시 backfill 트리거.
390
+ *
391
+ * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
392
+ * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
393
+ * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
394
+ */
395
+ private linkMemberSilent;
243
396
  /** 현재 설정된 회원 ID 조회 (미설정 시 null) */
244
397
  getMemberId(): string | null;
245
398
  /**
@@ -270,6 +423,79 @@ declare class AnalyticsAPI {
270
423
  * ```
271
424
  */
272
425
  flush(): Promise<void>;
426
+ /**
427
+ * 인기 페이지 조회 (콘솔 JWT 또는 User Secret Key `cb_sk_` 인증 필요).
428
+ *
429
+ * 브라우저 환경에서 Public Key(`cb_pk_`) 만 가진 SDK 인스턴스로는 호출이 차단된다.
430
+ * Functions / 어드민 앱 등 cb_sk_ 환경에서 사용하라.
431
+ *
432
+ * @param storageWebId 조회할 웹스토리지 ID. 미지정 시 `init()` 에 전달된 ID 사용.
433
+ * @param options 기간/limit 옵션
434
+ * @example
435
+ * ```ts
436
+ * const { pages } = await cb.analytics.getPopularPages('019d8...', { limit: 20 })
437
+ * ```
438
+ */
439
+ getPopularPages(storageWebId?: string, options?: AnalyticsRangeOptions): Promise<PopularPagesResponse>;
440
+ /**
441
+ * 페이지 전환 플로우(Sankey) 조회 — JWT/cb_sk_ 인증 필요.
442
+ */
443
+ getNavigationFlow(storageWebId?: string, options?: AnalyticsRangeOptions): Promise<NavigationFlowResponse>;
444
+ /**
445
+ * 방문자 목록 조회 — JWT/cb_sk_ 인증 필요.
446
+ */
447
+ getVisitors(storageWebId?: string, options?: VisitorListOptions): Promise<VisitorListResponse>;
448
+ /**
449
+ * 멤버별 합산 방문자 그룹 조회 — JWT/cb_sk_ 인증 필요. (1.11+)
450
+ *
451
+ * `getVisitors` 와 달리 visitor row 들을 `app_member_id` 로 합쳐 단일 row 로 반환.
452
+ * 같은 사람이 PC + 모바일 + 태블릿으로 접속한 경우 visitor 3개 → 그룹 1개.
453
+ * 익명 visitor 는 단일 row 로 그대로 포함되어 페이지네이션이 일관됨.
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * const { groups } = await cb.analytics.getVisitorGroups('019d8...', { limit: 50 })
458
+ * for (const g of groups) {
459
+ * if (g.app_member_id) console.log(`${g.app_member_id}: ${g.visitor_count} 디바이스`)
460
+ * }
461
+ * ```
462
+ */
463
+ getVisitorGroups(storageWebId?: string, options?: VisitorListOptions): Promise<VisitorGroupListResponse>;
464
+ /**
465
+ * 단건 멤버 합산 방문자 조회 — JWT/cb_sk_ 인증 필요. (1.11+)
466
+ *
467
+ * 어드민 회원 상세 페이지처럼 **한 명만 필요**할 때. 페이지네이션 풀 다운 없이
468
+ * 한 번의 호출로 합산 결과 반환.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * const v = await cb.analytics.getVisitorByMember('019d8...', memberId)
473
+ * console.log(`${v.visitor_count} 디바이스, 총 ${v.total_page_views} pv`)
474
+ * ```
475
+ */
476
+ getVisitorByMember(storageWebId: string | undefined, memberId: string): Promise<VisitorByMemberResponse>;
477
+ /**
478
+ * 두 visitor 를 한 사람으로 통합하는 admin 작업 — JWT/cb_sk_ 인증 필요. (1.11+)
479
+ *
480
+ * 외부 인증 시스템에서 두 visitor 가 동일인임을 알았을 때 사용. source 의 자식 레코드
481
+ * (page_views, daily, custom_events, experiment_assignments, heatmap_events,
482
+ * session_recordings) 를 target 으로 옮기고 source visitor 는 삭제됨.
483
+ *
484
+ * @param request 둘 중 하나 필수: `target_visitor_uid` 또는 `target_member_id`.
485
+ * @example
486
+ * ```ts
487
+ * await cb.analytics.mergeVisitors('019d8...', {
488
+ * source_visitor_uid: 'old-uid',
489
+ * target_member_id: '01a...',
490
+ * })
491
+ * ```
492
+ */
493
+ mergeVisitors(storageWebId: string | undefined, request: MergeVisitorsRequest): Promise<MergeVisitorsResponse>;
494
+ /**
495
+ * 조회 메서드 공통 가드 — Public Key 인증 SDK 인스턴스에서는 명확한 에러를 던진다.
496
+ * 백엔드 라우트는 cb_pk_ 를 거부하므로 호출 자체를 막는 것이 디버깅에 유리.
497
+ */
498
+ private requireServerSideStorageId;
273
499
  /**
274
500
  * 세션 매니저 접근 (고급 사용자용).
275
501
  *
@@ -3174,12 +3400,17 @@ interface GetAuthorizationURLResponse {
3174
3400
  }
3175
3401
  /**
3176
3402
  * OAuth 콜백 응답
3403
+ *
3404
+ * `email` 은 1.10 부터 추가된 선택적 필드. Apple private relay 또는 이메일 권한 미동의
3405
+ * 시 빈 문자열 / undefined 일 수 있다. 값이 있으면 첫 로그인 시점에 이메일을 확보할 수 있어
3406
+ * 별도 `getMember()` 호출이 불필요하다.
3177
3407
  */
3178
3408
  interface OAuthCallbackResponse {
3179
3409
  member_id: string;
3180
3410
  access_token: string;
3181
3411
  refresh_token: string;
3182
3412
  is_new_member: boolean;
3413
+ email?: string;
3183
3414
  }
3184
3415
 
3185
3416
  /**
@@ -3284,6 +3515,7 @@ declare class OAuthAPI {
3284
3515
  refresh_token?: string;
3285
3516
  member_id?: string;
3286
3517
  is_new_member?: boolean;
3518
+ email?: string;
3287
3519
  state?: string;
3288
3520
  error?: string;
3289
3521
  } | null;
@@ -3308,6 +3540,7 @@ declare class OAuthAPI {
3308
3540
  refresh_token: string;
3309
3541
  member_id: string;
3310
3542
  is_new_member: boolean;
3543
+ email?: string;
3311
3544
  state?: string;
3312
3545
  } | null>;
3313
3546
  }
@@ -4095,6 +4328,35 @@ interface WebPushSubscription {
4095
4328
  };
4096
4329
  }
4097
4330
 
4331
+ /**
4332
+ * `cb.push.sendToMembers` 페이로드. 백엔드 `dto.SendPushRequest` 의 멤버 발송 케이스
4333
+ * 에 매핑되며, target_type / target_member_ids 는 SDK 가 자동 채운다.
4334
+ */
4335
+ interface SendToMembersPayload {
4336
+ title: string;
4337
+ body: string;
4338
+ imageUrl?: string;
4339
+ data?: Record<string, unknown>;
4340
+ /** 'ios' | 'android' | 'web' 중 선택. 빈 배열/미지정 시 전체 플랫폼 발송. */
4341
+ platforms?: Array<'ios' | 'android' | 'web'>;
4342
+ /** TTL(초). 기본 86400 (24h). */
4343
+ ttlSeconds?: number;
4344
+ priority?: 'normal' | 'high';
4345
+ clickAction?: string;
4346
+ /** ISO8601 형식 예약 발송 시각. 미지정 시 즉시 발송. */
4347
+ scheduledAt?: string;
4348
+ }
4349
+ /**
4350
+ * 발송 결과. 백엔드 `dto.SendResultResponse` 와 1:1 매핑.
4351
+ */
4352
+ interface SendPushResult {
4353
+ message_id: string;
4354
+ total_target: number;
4355
+ sent_count: number;
4356
+ failed_count: number;
4357
+ status: string;
4358
+ error_message?: string;
4359
+ }
4098
4360
  /**
4099
4361
  * 푸시 알림 API
4100
4362
  *
@@ -4233,6 +4495,29 @@ declare class PushAPI {
4233
4495
  * @param deviceToken Web Push endpoint URL (등록 시 사용한 endpoint)
4234
4496
  */
4235
4497
  unregisterWebPush(deviceToken: string): Promise<void>;
4498
+ /**
4499
+ * 회원 ID 목록으로 푸시 발송 — Functions / cb_sk_ 인증 환경 전용.
4500
+ *
4501
+ * 백엔드 라우트 `POST /v1/apps/:appID/push/send` 는 콘솔 JWT 또는 User Secret Key
4502
+ * (`cb_sk_`) 만 받는다. 브라우저 SDK 의 Public Key(`cb_pk_`) 인스턴스에서는 호출
4503
+ * 자체가 차단되며, 권한 누설을 막기 위해 명확한 에러를 던진다.
4504
+ *
4505
+ * @param appId 발송 대상 앱 ID (cb_sk_ 환경은 user 단위 권한이므로 명시 전달).
4506
+ * @param memberIds 받는 회원 ID 배열 (UUID).
4507
+ * @param payload 푸시 내용/옵션.
4508
+ *
4509
+ * @example
4510
+ * ```ts
4511
+ * // ConnectBase Function (Node.js, secrets.CB_SECRET_KEY 주입)
4512
+ * const result = await cb.push.sendToMembers(APP_ID, [memberId], {
4513
+ * title: '새 알림',
4514
+ * body: '확인해보세요',
4515
+ * data: { route: '/notifications' },
4516
+ * })
4517
+ * console.log(result.message_id, result.sent_count)
4518
+ * ```
4519
+ */
4520
+ sendToMembers(appId: string, memberIds: string[], payload: SendToMembersPayload): Promise<SendPushResult>;
4236
4521
  /**
4237
4522
  * 브라우저 고유 ID 생성 (localStorage에 저장)
4238
4523
  */
package/dist/index.js CHANGED
@@ -4474,11 +4474,13 @@ var OAuthAPI = class {
4474
4474
  reject(new Error(event.data.error));
4475
4475
  return;
4476
4476
  }
4477
+ const rawEmail = event.data.email;
4477
4478
  const result = {
4478
4479
  member_id: event.data.member_id,
4479
4480
  access_token: event.data.access_token,
4480
4481
  refresh_token: event.data.refresh_token,
4481
- is_new_member: event.data.is_new_member === "true" || event.data.is_new_member === true
4482
+ is_new_member: event.data.is_new_member === "true" || event.data.is_new_member === true,
4483
+ ...typeof rawEmail === "string" && rawEmail.length > 0 ? { email: rawEmail } : {}
4482
4484
  };
4483
4485
  this.http.setTokens(result.access_token, result.refresh_token);
4484
4486
  resolve(result);
@@ -4546,11 +4548,13 @@ var OAuthAPI = class {
4546
4548
  if (!accessToken || !refreshToken || !memberId) {
4547
4549
  return null;
4548
4550
  }
4551
+ const emailParam = params.get("email");
4549
4552
  const result = {
4550
4553
  access_token: accessToken,
4551
4554
  refresh_token: refreshToken,
4552
4555
  member_id: memberId,
4553
4556
  is_new_member: params.get("is_new_member") === "true",
4557
+ ...emailParam ? { email: emailParam } : {},
4554
4558
  state: params.get("state") || void 0
4555
4559
  };
4556
4560
  if (window.opener) {
@@ -4588,6 +4592,7 @@ var OAuthAPI = class {
4588
4592
  refresh_token: resp.refresh_token,
4589
4593
  member_id: resp.member_id,
4590
4594
  is_new_member: resp.is_new_member,
4595
+ ...resp.email ? { email: resp.email } : {},
4591
4596
  state: params.get("state") || void 0
4592
4597
  };
4593
4598
  if (window.opener) {
@@ -5264,6 +5269,53 @@ var PushAPI = class {
5264
5269
  const prefix = this.getPublicPrefix();
5265
5270
  await this.http.delete(`${prefix}/push/devices/${encodeURIComponent(deviceToken)}`);
5266
5271
  }
5272
+ // ============ Server-side Send (Functions / cb_sk_ 전용) ============
5273
+ /**
5274
+ * 회원 ID 목록으로 푸시 발송 — Functions / cb_sk_ 인증 환경 전용.
5275
+ *
5276
+ * 백엔드 라우트 `POST /v1/apps/:appID/push/send` 는 콘솔 JWT 또는 User Secret Key
5277
+ * (`cb_sk_`) 만 받는다. 브라우저 SDK 의 Public Key(`cb_pk_`) 인스턴스에서는 호출
5278
+ * 자체가 차단되며, 권한 누설을 막기 위해 명확한 에러를 던진다.
5279
+ *
5280
+ * @param appId 발송 대상 앱 ID (cb_sk_ 환경은 user 단위 권한이므로 명시 전달).
5281
+ * @param memberIds 받는 회원 ID 배열 (UUID).
5282
+ * @param payload 푸시 내용/옵션.
5283
+ *
5284
+ * @example
5285
+ * ```ts
5286
+ * // ConnectBase Function (Node.js, secrets.CB_SECRET_KEY 주입)
5287
+ * const result = await cb.push.sendToMembers(APP_ID, [memberId], {
5288
+ * title: '새 알림',
5289
+ * body: '확인해보세요',
5290
+ * data: { route: '/notifications' },
5291
+ * })
5292
+ * console.log(result.message_id, result.sent_count)
5293
+ * ```
5294
+ */
5295
+ async sendToMembers(appId, memberIds, payload) {
5296
+ if (this.http.hasPublicKey()) {
5297
+ throw new Error(
5298
+ "cb.push.sendToMembers() \uB294 User Secret Key(cb_sk_) \uB610\uB294 \uCF58\uC194 JWT \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Public Key(cb_pk_) SDK \uC778\uC2A4\uD134\uC2A4\uB85C\uB294 \uD638\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Functions \uD658\uACBD\uC5D0\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."
5299
+ );
5300
+ }
5301
+ if (memberIds.length === 0) {
5302
+ throw new Error("cb.push.sendToMembers(): memberIds \uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
5303
+ }
5304
+ const body = {
5305
+ target_type: "members",
5306
+ target_member_ids: memberIds,
5307
+ title: payload.title,
5308
+ body: payload.body,
5309
+ ...payload.imageUrl !== void 0 ? { image_url: payload.imageUrl } : {},
5310
+ ...payload.data !== void 0 ? { data: payload.data } : {},
5311
+ ...payload.platforms !== void 0 ? { platforms: payload.platforms } : {},
5312
+ ...payload.ttlSeconds !== void 0 ? { ttl: payload.ttlSeconds } : {},
5313
+ ...payload.priority !== void 0 ? { priority: payload.priority } : {},
5314
+ ...payload.clickAction !== void 0 ? { click_action: payload.clickAction } : {},
5315
+ ...payload.scheduledAt !== void 0 ? { scheduled_at: payload.scheduledAt } : {}
5316
+ };
5317
+ return this.http.post(`/v1/apps/${appId}/push/send`, body);
5318
+ }
5267
5319
  // ============ Helper Methods ============
5268
5320
  /**
5269
5321
  * 브라우저 고유 ID 생성 (localStorage에 저장)
@@ -8173,6 +8225,15 @@ var SessionManager = class {
8173
8225
  return generateId();
8174
8226
  }
8175
8227
  };
8228
+ function buildAnalyticsQuery(options) {
8229
+ if (!options) return "";
8230
+ const params = new URLSearchParams();
8231
+ if (options.start_date !== void 0) params.set("start_date", String(options.start_date));
8232
+ if (options.end_date !== void 0) params.set("end_date", String(options.end_date));
8233
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
8234
+ const qs = params.toString();
8235
+ return qs ? `?${qs}` : "";
8236
+ }
8176
8237
  function parseUTM() {
8177
8238
  if (typeof window === "undefined") return {};
8178
8239
  try {
@@ -8323,15 +8384,31 @@ var AnalyticsAPI = class {
8323
8384
  this.enqueue(event);
8324
8385
  }
8325
8386
  /**
8326
- * 사용자 식별 (로그인 )
8327
- * 이후 모든 방문 배치에 `app_member_id`가 첨부되어 게스트 방문자가 회원으로 연결됩니다.
8387
+ * 사용자 식별 (로그인 직후 호출).
8388
+ *
8389
+ * 이후 모든 방문 배치에 `app_member_id` 가 첨부되어 새 활동은 회원으로 기록됩니다.
8390
+ * 추가로 **현재 visitor_uid 의 기존 익명 활동을 즉시 회원에게 backfill** 하기 위해
8391
+ * 백엔드 `link-member` 엔드포인트를 한 번 호출합니다 (1.11.0+). 호출 실패는
8392
+ * silent — 다음 batch 가 닿을 때 백엔드 자동 매핑이 동일하게 처리하므로 자가 복구.
8393
+ *
8394
+ * 산업 표준(GA4 User-ID, Mixpanel/PostHog `identify`) 과 동작 정합.
8395
+ *
8396
+ * @example
8397
+ * ```ts
8398
+ * // 로그인 성공 직후
8399
+ * await cb.auth.signIn({ email, password })
8400
+ * cb.analytics.identify(member.id)
8401
+ * ```
8328
8402
  */
8329
8403
  identify(memberId) {
8330
8404
  this.setMemberId(memberId);
8405
+ this.linkMemberSilent(memberId);
8331
8406
  }
8332
8407
  /**
8333
- * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출)
8334
- * null 을 넘기면 익명 상태로 복귀 (로그아웃).
8408
+ * 방문자 트래커에 현재 회원 ID 설정 (로그인/게스트 가입 시 호출).
8409
+ *
8410
+ * `identify()` 와 달리 즉시 backfill 호출은 하지 않습니다 — 단순히 이후 batch 의
8411
+ * `app_member_id` 값만 갱신. null 을 넘기면 익명 상태로 복귀 (로그아웃).
8335
8412
  */
8336
8413
  setMemberId(memberId) {
8337
8414
  this.memberId = memberId || null;
@@ -8348,6 +8425,25 @@ var AnalyticsAPI = class {
8348
8425
  }
8349
8426
  this.log("Member id set", { memberId });
8350
8427
  }
8428
+ /**
8429
+ * 백엔드 link-member 엔드포인트 한 번 호출 — 즉시 backfill 트리거.
8430
+ *
8431
+ * - 첫 페이지뷰가 아직 백엔드에 닿기 전이면 visitor 가 없어 404. 무시 — 다음 batch 가
8432
+ * 가면 백엔드 BatchRecordVisit 가 자동 LinkMember 를 호출.
8433
+ * - storage_web_id 가 init 안 됐거나 모든 종류의 네트워크 오류 — silent fail.
8434
+ */
8435
+ linkMemberSilent(memberId) {
8436
+ if (!this.storageWebId) return;
8437
+ const prefix = this.http.hasPublicKey() ? "/v1/public" : "/v1";
8438
+ this.http.post(
8439
+ `${prefix}/storages/web/${this.storageWebId}/visitors/link-member`,
8440
+ {
8441
+ visitor_uid: this.session.visitorUid,
8442
+ app_member_id: memberId
8443
+ }
8444
+ ).catch(() => {
8445
+ });
8446
+ }
8351
8447
  /** 현재 설정된 회원 ID 조회 (미설정 시 null) */
8352
8448
  getMemberId() {
8353
8449
  return this.memberId;
@@ -8441,6 +8537,120 @@ var AnalyticsAPI = class {
8441
8537
  async flush() {
8442
8538
  await this.flushQueue();
8443
8539
  }
8540
+ /**
8541
+ * 인기 페이지 조회 (콘솔 JWT 또는 User Secret Key `cb_sk_` 인증 필요).
8542
+ *
8543
+ * 브라우저 환경에서 Public Key(`cb_pk_`) 만 가진 SDK 인스턴스로는 호출이 차단된다.
8544
+ * Functions / 어드민 앱 등 cb_sk_ 환경에서 사용하라.
8545
+ *
8546
+ * @param storageWebId 조회할 웹스토리지 ID. 미지정 시 `init()` 에 전달된 ID 사용.
8547
+ * @param options 기간/limit 옵션
8548
+ * @example
8549
+ * ```ts
8550
+ * const { pages } = await cb.analytics.getPopularPages('019d8...', { limit: 20 })
8551
+ * ```
8552
+ */
8553
+ async getPopularPages(storageWebId, options) {
8554
+ const id = this.requireServerSideStorageId(storageWebId, "getPopularPages");
8555
+ const qs = buildAnalyticsQuery(options);
8556
+ return this.http.get(`/v1/storages/web/${id}/popular-pages${qs}`);
8557
+ }
8558
+ /**
8559
+ * 페이지 전환 플로우(Sankey) 조회 — JWT/cb_sk_ 인증 필요.
8560
+ */
8561
+ async getNavigationFlow(storageWebId, options) {
8562
+ const id = this.requireServerSideStorageId(storageWebId, "getNavigationFlow");
8563
+ const qs = buildAnalyticsQuery(options);
8564
+ return this.http.get(`/v1/storages/web/${id}/navigation/flow${qs}`);
8565
+ }
8566
+ /**
8567
+ * 방문자 목록 조회 — JWT/cb_sk_ 인증 필요.
8568
+ */
8569
+ async getVisitors(storageWebId, options) {
8570
+ const id = this.requireServerSideStorageId(storageWebId, "getVisitors");
8571
+ const params = new URLSearchParams();
8572
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
8573
+ if (options?.offset !== void 0) params.set("offset", String(options.offset));
8574
+ if (options?.sort_by) params.set("sort_by", options.sort_by);
8575
+ const qs = params.toString() ? `?${params.toString()}` : "";
8576
+ return this.http.get(`/v1/storages/web/${id}/visitors${qs}`);
8577
+ }
8578
+ /**
8579
+ * 멤버별 합산 방문자 그룹 조회 — JWT/cb_sk_ 인증 필요. (1.11+)
8580
+ *
8581
+ * `getVisitors` 와 달리 visitor row 들을 `app_member_id` 로 합쳐 단일 row 로 반환.
8582
+ * 같은 사람이 PC + 모바일 + 태블릿으로 접속한 경우 visitor 3개 → 그룹 1개.
8583
+ * 익명 visitor 는 단일 row 로 그대로 포함되어 페이지네이션이 일관됨.
8584
+ *
8585
+ * @example
8586
+ * ```ts
8587
+ * const { groups } = await cb.analytics.getVisitorGroups('019d8...', { limit: 50 })
8588
+ * for (const g of groups) {
8589
+ * if (g.app_member_id) console.log(`${g.app_member_id}: ${g.visitor_count} 디바이스`)
8590
+ * }
8591
+ * ```
8592
+ */
8593
+ async getVisitorGroups(storageWebId, options) {
8594
+ const id = this.requireServerSideStorageId(storageWebId, "getVisitorGroups");
8595
+ const params = new URLSearchParams();
8596
+ if (options?.limit !== void 0) params.set("limit", String(options.limit));
8597
+ if (options?.offset !== void 0) params.set("offset", String(options.offset));
8598
+ if (options?.sort_by) params.set("sort_by", options.sort_by);
8599
+ const qs = params.toString() ? `?${params.toString()}` : "";
8600
+ return this.http.get(`/v1/storages/web/${id}/visitor-groups${qs}`);
8601
+ }
8602
+ /**
8603
+ * 단건 멤버 합산 방문자 조회 — JWT/cb_sk_ 인증 필요. (1.11+)
8604
+ *
8605
+ * 어드민 회원 상세 페이지처럼 **한 명만 필요**할 때. 페이지네이션 풀 다운 없이
8606
+ * 한 번의 호출로 합산 결과 반환.
8607
+ *
8608
+ * @example
8609
+ * ```ts
8610
+ * const v = await cb.analytics.getVisitorByMember('019d8...', memberId)
8611
+ * console.log(`${v.visitor_count} 디바이스, 총 ${v.total_page_views} pv`)
8612
+ * ```
8613
+ */
8614
+ async getVisitorByMember(storageWebId, memberId) {
8615
+ const id = this.requireServerSideStorageId(storageWebId, "getVisitorByMember");
8616
+ return this.http.get(`/v1/storages/web/${id}/members/${memberId}/visitor`);
8617
+ }
8618
+ /**
8619
+ * 두 visitor 를 한 사람으로 통합하는 admin 작업 — JWT/cb_sk_ 인증 필요. (1.11+)
8620
+ *
8621
+ * 외부 인증 시스템에서 두 visitor 가 동일인임을 알았을 때 사용. source 의 자식 레코드
8622
+ * (page_views, daily, custom_events, experiment_assignments, heatmap_events,
8623
+ * session_recordings) 를 target 으로 옮기고 source visitor 는 삭제됨.
8624
+ *
8625
+ * @param request 둘 중 하나 필수: `target_visitor_uid` 또는 `target_member_id`.
8626
+ * @example
8627
+ * ```ts
8628
+ * await cb.analytics.mergeVisitors('019d8...', {
8629
+ * source_visitor_uid: 'old-uid',
8630
+ * target_member_id: '01a...',
8631
+ * })
8632
+ * ```
8633
+ */
8634
+ async mergeVisitors(storageWebId, request) {
8635
+ const id = this.requireServerSideStorageId(storageWebId, "mergeVisitors");
8636
+ return this.http.post(`/v1/storages/web/${id}/visitors/merge`, request);
8637
+ }
8638
+ /**
8639
+ * 조회 메서드 공통 가드 — Public Key 인증 SDK 인스턴스에서는 명확한 에러를 던진다.
8640
+ * 백엔드 라우트는 cb_pk_ 를 거부하므로 호출 자체를 막는 것이 디버깅에 유리.
8641
+ */
8642
+ requireServerSideStorageId(storageWebId, methodName) {
8643
+ if (this.http.hasPublicKey()) {
8644
+ throw new Error(
8645
+ `cb.analytics.${methodName}() \uB294 \uCF58\uC194 JWT \uB610\uB294 User Secret Key(cb_sk_) \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800 SDK \uC758 Public Key(cb_pk_) \uB85C\uB294 \uD638\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Functions \uD658\uACBD\uC5D0\uC11C \uC0AC\uC6A9\uD558\uC138\uC694.`
8646
+ );
8647
+ }
8648
+ const id = storageWebId ?? this.storageWebId;
8649
+ if (!id) {
8650
+ throw new Error(`cb.analytics.${methodName}() \uD638\uCD9C \uC2DC storageWebId \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 (init \uB610\uB294 \uC778\uC790\uB85C \uC804\uB2EC).`);
8651
+ }
8652
+ return id;
8653
+ }
8444
8654
  /**
8445
8655
  * 세션 매니저 접근 (고급 사용자용).
8446
8656
  *