@adstage/web-sdk 2.0.0 → 2.1.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.
@@ -6,10 +6,13 @@
6
6
  import { AdStageConfig, BaseModule } from '../../types/config';
7
7
  import { AdType, AdEventType } from '../../types/advertisement';
8
8
  import type { AdSlot, Advertisement } from '../../types/advertisement';
9
- import { CarouselSliderManager } from '../../managers/carousel-slider-manager';
10
- import { TextTransitionManager } from '../../managers/text-transition-manager';
11
- import { ImpressionTracker } from '../../managers/impression-tracker';
12
- import { EventTracker } from '../../managers/event-tracker';
9
+ import { CarouselSliderManager } from '../../managers/ads/carousel-slider-manager';
10
+ import { TextTransitionManager } from '../../managers/ads/text-transition-manager';
11
+ import { ViewableEventTracker } from '../../managers/ads/viewable-event-tracker';
12
+ import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
13
+ import { ViewabilityTracker, VIEWABILITY_STANDARDS } from '../../managers/ads/viewability-tracker';
14
+ import { BasicFraudDetector } from '../../managers/ads/basic-fraud-detector';
15
+ import type { ViewabilityMetrics } from '../../managers/ads/viewability-tracker';
13
16
  import { endpoints } from '../../constants/endpoints';
14
17
  import { ApiHeaders } from '../../utils/api-headers';
15
18
 
@@ -23,13 +26,18 @@ export interface AdOptions {
23
26
  autoplay?: boolean;
24
27
  muted?: boolean;
25
28
  onClick?: (adData: any) => void;
29
+ // 광고 필터링 옵션 (백엔드 API에서 실제 지원하는 것들만)
30
+ language?: 'ko' | 'en' | 'ja' | 'zh';
31
+ deviceType?: 'MOBILE' | 'DESKTOP';
32
+ country?: 'KR' | 'US' | 'JP' | 'CN' | 'DE';
26
33
  }
27
34
 
28
35
  export class AdsModule implements BaseModule {
29
36
  private _isReady = false;
30
37
  private _config: AdStageConfig | null = null;
31
38
  private slots = new Map<string, AdSlot>();
32
- private eventTracker: EventTracker | null = null;
39
+ // Advertisement 이벤트 추적 관련
40
+ private advertisementEventTracker: AdvertisementEventTracker | null = null;
33
41
 
34
42
  /**
35
43
  * Ads 모듈 초기화 (동기)
@@ -37,8 +45,8 @@ export class AdsModule implements BaseModule {
37
45
  init(config: AdStageConfig): void {
38
46
  this._config = config;
39
47
 
40
- // EventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
41
- this.eventTracker = new EventTracker(
48
+ // AdvertisementEventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
49
+ this.advertisementEventTracker = new AdvertisementEventTracker(
42
50
  endpoints.getBaseUrl(),
43
51
  config.apiKey,
44
52
  config.debug || false,
@@ -77,7 +85,11 @@ export class AdsModule implements BaseModule {
77
85
  height: options?.height || 250,
78
86
  autoSlide: options?.autoSlide || false,
79
87
  slideInterval: options?.slideInterval || 5000,
80
- onClick: options?.onClick
88
+ onClick: options?.onClick,
89
+ // 필터링 옵션들 전달
90
+ language: options?.language,
91
+ deviceType: options?.deviceType,
92
+ country: options?.country
81
93
  };
82
94
 
83
95
  return this.createAd(containerId, AdType.BANNER, adstageOptions);
@@ -92,7 +104,11 @@ export class AdsModule implements BaseModule {
92
104
  const adstageOptions = {
93
105
  maxLines: options?.maxLines || 3,
94
106
  style: options?.style || 'default',
95
- onClick: options?.onClick
107
+ onClick: options?.onClick,
108
+ // 필터링 옵션들 전달
109
+ language: options?.language,
110
+ deviceType: options?.deviceType,
111
+ country: options?.country
96
112
  };
97
113
 
98
114
  return this.createAd(containerId, AdType.TEXT, adstageOptions);
@@ -163,6 +179,16 @@ export class AdsModule implements BaseModule {
163
179
  throw new Error(`Ad slot not found: ${slotId}`);
164
180
  }
165
181
 
182
+ // ViewabilityTracker 정리
183
+ if ((slot as any).viewabilityTracker) {
184
+ (slot as any).viewabilityTracker.destroy();
185
+ }
186
+
187
+ // BasicFraudDetector 정리
188
+ if ((slot as any).fraudDetector) {
189
+ (slot as any).fraudDetector.destroy();
190
+ }
191
+
166
192
  // DOM에서 제거
167
193
  const container = document.getElementById(slot.containerId);
168
194
  if (container) {
@@ -239,8 +265,8 @@ export class AdsModule implements BaseModule {
239
265
  this.loadAdContentInBackground(slot);
240
266
 
241
267
  // 이벤트 추적 준비
242
- if (this.eventTracker && this._config?.debug) {
243
- console.log(`📊 Event tracking enabled for slot: ${slotId}`);
268
+ if (this.advertisementEventTracker && this._config?.debug) {
269
+ console.log(`📊 Advertisement event tracking enabled for slot: ${slotId}`);
244
270
  }
245
271
 
246
272
  return slotId;
@@ -290,6 +316,9 @@ export class AdsModule implements BaseModule {
290
316
  // 광고가 1개면 일반 렌더링
291
317
  slot.advertisement = adstageData[0];
292
318
  await this.renderAdElement(slot, adstageData[0]);
319
+
320
+ // ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
321
+ this.startBasicViewabilityTracking(slot, adstageData[0]);
293
322
  }
294
323
 
295
324
  slot.isLoaded = true;
@@ -304,18 +333,92 @@ export class AdsModule implements BaseModule {
304
333
  }
305
334
 
306
335
  /**
307
- * Fallback 광고 렌더링
336
+ * 기본 viewability 추적 시작
337
+ */
338
+ private startBasicViewabilityTracking(slot: AdSlot, ad: Advertisement): void {
339
+ const element = document.getElementById(slot.id);
340
+ if (!element) return;
341
+
342
+ // 기본 fraud 검사
343
+ const fraudDetector = new BasicFraudDetector();
344
+
345
+ // viewability 추적
346
+ const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
347
+ this.handleViewableEvent(ad, slot, metrics, fraudDetector);
348
+ });
349
+
350
+ // 정리를 위해 저장
351
+ (slot as any).viewabilityTracker = tracker;
352
+ (slot as any).fraudDetector = fraudDetector;
353
+
354
+ if (this._config?.debug) {
355
+ console.log(`🎯 Viewability tracking started for slot: ${slot.id}`);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Viewable 이벤트 처리
361
+ */
362
+ private async handleViewableEvent(
363
+ ad: Advertisement,
364
+ slot: AdSlot,
365
+ metrics: ViewabilityMetrics,
366
+ fraudDetector: BasicFraudDetector
367
+ ): Promise<void> {
368
+ try {
369
+ const fraudScore = fraudDetector.calculateFraudScore();
370
+
371
+ // 높은 위험도면 차단
372
+ if (fraudScore.riskLevel === 'CRITICAL') {
373
+ if (this._config?.debug) {
374
+ console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
375
+ }
376
+ return;
377
+ }
378
+
379
+ // VIEWABLE 이벤트 전송
380
+ if (this.advertisementEventTracker) {
381
+ await this.advertisementEventTracker.trackAdvertisementEvent(
382
+ ad._id,
383
+ slot.id,
384
+ AdEventType.VIEWABLE,
385
+ {
386
+ viewabilityMetrics: metrics,
387
+ fraudScore: fraudScore.score,
388
+ fraudReasons: fraudScore.reasons,
389
+ riskLevel: fraudScore.riskLevel
390
+ }
391
+ );
392
+
393
+ if (this._config?.debug) {
394
+ console.log(`✅ Viewable impression tracked for ad ${ad._id} (fraud score: ${fraudScore.score})`);
395
+ }
396
+ }
397
+
398
+ } catch (error) {
399
+ console.error(`❌ Failed to track viewable impression:`, error);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Fallback 광고 렌더링 - DOM에서 완전 제거
308
405
  */
309
406
  private renderFallback(slot: AdSlot): void {
310
407
  const element = document.getElementById(slot.id);
311
408
  if (element) {
312
- element.innerHTML = `<span>Ad not available</span>`;
313
- element.style.color = '#999';
314
-
315
- if (this._config?.debug) {
316
- console.warn(`⚠️ Fallback rendered for slot: ${slot.id}`);
409
+ // 부모 컨테이너에서 광고 슬롯을 완전히 제거
410
+ const parentContainer = element.parentNode;
411
+ if (parentContainer) {
412
+ parentContainer.removeChild(element);
413
+
414
+ if (this._config?.debug) {
415
+ console.warn(`⚠️ Ad slot completely removed from DOM: ${slot.id}`);
416
+ }
317
417
  }
318
418
  }
419
+
420
+ // 슬롯 맵에서도 제거
421
+ this.slots.delete(slot.id);
319
422
  }
320
423
 
321
424
  /**
@@ -330,8 +433,18 @@ export class AdsModule implements BaseModule {
330
433
  const params = new URLSearchParams();
331
434
  params.append('adType', type);
332
435
 
333
- // userAgent와 url은 header나 자동으로 처리되므로 query에서 제외
334
- // 기타 옵션들을 필요시 query parameter로 추가 가능
436
+ // 백엔드 API에서 실제 지원하는 필터링 옵션들만 추가
437
+ if (options.language) {
438
+ params.append('language', options.language);
439
+ }
440
+
441
+ if (options.deviceType) {
442
+ params.append('deviceType', options.deviceType);
443
+ }
444
+
445
+ if (options.country) {
446
+ params.append('country', options.country);
447
+ }
335
448
 
336
449
  const url = `${endpoints.advertisements.list()}?${params.toString()}`;
337
450
 
@@ -345,7 +458,17 @@ export class AdsModule implements BaseModule {
345
458
  }
346
459
 
347
460
  const result = await response.json();
348
- return result.advertisements || [];
461
+ const advertisements = result.advertisements || [];
462
+
463
+ if (this._config?.debug) {
464
+ console.log(`📊 Fetched ${advertisements.length} ads for type: ${type}, filters:`, {
465
+ language: options.language,
466
+ deviceType: options.deviceType,
467
+ country: options.country
468
+ });
469
+ }
470
+
471
+ return advertisements;
349
472
  }
350
473
 
351
474
  /**
@@ -360,21 +483,21 @@ export class AdsModule implements BaseModule {
360
483
  // 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
361
484
  const trackEventCallback = (adId: string, slotId: string, eventType: AdEventType) => {
362
485
  // 노출 이벤트인 경우 중복 확인
363
- if (eventType === AdEventType.IMPRESSION) {
364
- if (ImpressionTracker.isDuplicateImpression(adId, slotId, this._config?.debug)) {
486
+ if (eventType === AdEventType.VIEWABLE) {
487
+ if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this._config?.debug)) {
365
488
  if (this._config?.debug) {
366
- console.log(`🚫 Duplicate impression blocked for ad ${adId} in slot ${slotId}`);
489
+ console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
367
490
  }
368
491
  return; // 중복 노출이면 추적하지 않음
369
492
  }
370
493
 
371
494
  if (this._config?.debug) {
372
- console.log(`✅ New impression recorded for ad ${adId} in slot ${slotId}`);
495
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
373
496
  }
374
497
  }
375
498
 
376
- if (this.eventTracker && this._config?.debug) {
377
- console.log(`📊 Event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
499
+ if (this.advertisementEventTracker && this._config?.debug) {
500
+ console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
378
501
  }
379
502
  };
380
503
 
@@ -505,8 +628,8 @@ export class AdsModule implements BaseModule {
505
628
  await this.renderAd(slot);
506
629
 
507
630
  // 새로운 노출 추적
508
- if (this.eventTracker) {
509
- console.log('New impression tracked for slot:', slot.id);
631
+ if (this.advertisementEventTracker) {
632
+ console.log('New advertisement viewable tracked for slot:', slot.id);
510
633
  }
511
634
  }
512
635
  } catch (error) {
@@ -16,15 +16,8 @@ export enum Platform {
16
16
 
17
17
  // 광고 이벤트 타입
18
18
  export enum AdEventType {
19
- IMPRESSION = 'IMPRESSION',
20
- CLICK = 'CLICK',
21
- HOVER = 'HOVER',
22
19
  VIEWABLE = 'VIEWABLE',
23
- VIEWABLE_IMPRESSION = 'VIEWABLE_IMPRESSION',
24
- COMPLETED = 'COMPLETED',
25
- VIDEO_START = 'VIDEO_START',
26
- VIDEO_COMPLETE = 'VIDEO_COMPLETE',
27
- ERROR = 'ERROR',
20
+ CLICK = 'CLICK',
28
21
  }
29
22
 
30
23
  // 디바이스 타입
@@ -79,7 +72,7 @@ export interface ViewabilityMetrics {
79
72
  isViewable: boolean;
80
73
  visibilityRatio: number;
81
74
  duration: number;
82
- impressions: number;
75
+ viewables: number;
83
76
  attentionTime: number;
84
77
  scrollDepth: number;
85
78
  completionRate: number;
@@ -171,7 +164,7 @@ export interface AdSlot {
171
164
  config?: AdSlotConfig;
172
165
  advertisement?: Advertisement;
173
166
  isViewable?: boolean;
174
- impressionSent?: boolean;
167
+ viewableSent?: boolean;
175
168
  loadTime?: number;
176
169
  renderTime?: number;
177
170
  events?: AdEvent[];
@@ -185,13 +178,7 @@ export interface AdSlot {
185
178
 
186
179
  // 광고 성과 지표
187
180
  export interface AdAnalytics {
188
- impressions: number;
181
+ viewables: number;
189
182
  clicks: number;
190
- hovers: number;
191
- viewableImpressions: number;
192
- errors: number;
193
183
  ctr: number; // Click Through Rate
194
- viewabilityRate: number;
195
- averageViewTime: number;
196
- totalViewTime: number;
197
184
  }
package/src/types/api.ts CHANGED
@@ -76,13 +76,9 @@ export interface AnalyticsRequest {
76
76
 
77
77
  // 광고 분석 응답
78
78
  export interface AnalyticsResponse {
79
- impressions: number;
79
+ viewables: number;
80
80
  clicks: number;
81
- hovers: number;
82
- viewableImpressions: number;
83
- errors: number;
84
81
  ctr: number;
85
- viewabilityRate: number;
86
82
  breakdown?: Record<string, AdAnalytics>;
87
83
  }
88
84
 
@@ -1,129 +0,0 @@
1
- import { AdEventType } from '../types/advertisement';
2
- import type { AdSlot } from '../types/advertisement';
3
- import { ImpressionTracker } from './impression-tracker';
4
- import { DeviceInfoCollector } from './device-info-collector';
5
- import { DOMUtils } from '../utils/dom-utils';
6
- import { ApiHeaders } from '../utils/api-headers';
7
-
8
- /**
9
- * 이벤트 추적 관리 클래스
10
- * - 광고 이벤트 추적 및 전송
11
- * - 중복 노출 방지 통합
12
- * - 서버 API 통신
13
- */
14
- export class EventTracker {
15
- private baseUrl: string;
16
- private apiKey: string;
17
- private debug: boolean;
18
- private slots: Map<string, AdSlot>;
19
-
20
- constructor(baseUrl: string, apiKey: string, debug: boolean, slots: Map<string, AdSlot>) {
21
- this.baseUrl = baseUrl;
22
- this.apiKey = apiKey;
23
- this.debug = debug;
24
- this.slots = slots;
25
- }
26
-
27
- /**
28
- * 이벤트 추적
29
- */
30
- async trackEvent(adId: string, slotId: string, eventType: AdEventType): Promise<void> {
31
- try {
32
- // 노출 이벤트의 경우 중복 확인
33
- if (eventType === AdEventType.IMPRESSION) {
34
- if (ImpressionTracker.isDuplicateImpression(adId, slotId, this.debug)) {
35
- return; // 중복 노출이므로 추적하지 않음
36
- }
37
- }
38
-
39
- // 현재 슬롯 정보 가져오기
40
- const slot = this.slots.get(slotId);
41
-
42
- // 디바이스 정보 수집
43
- const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
44
-
45
- // 이벤트 데이터 구성 (MongoDB 스키마에 맞춤)
46
- const eventData = {
47
- // 서버에서 자동 설정: orgId, advertisementId, action
48
- // 하지만 DTO 검증을 위해 임시값 제공
49
- orgId: 'temp', // 서버에서 API 키로부터 덮어씀
50
- advertisementId: adId, // 서버에서 URL 파라미터로부터 덮어씀
51
- action: eventType, // 서버에서 URL 파라미터로부터 덮어씀
52
-
53
- // 필수 필드들
54
- adType: slot?.adType || 'BANNER',
55
- platform: deviceInfo.platform,
56
-
57
- // 디바이스 정보를 최상위로 플래튼
58
- deviceId: deviceInfo.deviceId,
59
- osVersion: deviceInfo.osVersion,
60
- deviceModel: deviceInfo.deviceModel,
61
- appVersion: deviceInfo.appVersion,
62
- sdkVersion: deviceInfo.sdkVersion,
63
- language: deviceInfo.language,
64
- country: deviceInfo.country,
65
- ipAddress: deviceInfo.ipAddress,
66
- userAgent: deviceInfo.userAgent,
67
- timezone: deviceInfo.timezone,
68
- viewportWidth: deviceInfo.viewportWidth,
69
- viewportHeight: deviceInfo.viewportHeight,
70
- screenWidth: deviceInfo.screenWidth,
71
- screenHeight: deviceInfo.screenHeight,
72
- connectionType: deviceInfo.connectionType,
73
-
74
- // 페이지 및 슬롯 정보
75
- pageUrl: DOMUtils.getPageInfo().url,
76
- pageTitle: DOMUtils.getPageInfo().title,
77
- referrer: DOMUtils.getPageInfo().referrer,
78
- slotId,
79
- slotPosition: DeviceInfoCollector.getSlotPosition(slot?.containerId || ''),
80
- slotWidth: EventTracker.parseNumericValue(slot?.width),
81
- slotHeight: EventTracker.parseNumericValue(slot?.height),
82
- sessionId: deviceInfo.sessionId,
83
-
84
- // 성능 메트릭
85
- pageLoadTime: performance.now(),
86
- timestamp: new Date().toISOString(),
87
-
88
- // 추가 메타데이터
89
- metadata: {
90
- eventType,
91
- sdkVersion: '1.0.0',
92
- timestamp: Date.now(),
93
- },
94
- };
95
-
96
- await fetch(
97
- `${this.baseUrl}/advertisements/events/${adId}/${eventType}`,
98
- {
99
- method: 'POST',
100
- headers: ApiHeaders.createForEvents(this.apiKey, eventData),
101
- body: JSON.stringify(eventData),
102
- }
103
- );
104
-
105
- if (this.debug) {
106
- console.log(`Tracked event: ${eventType} for ad ${adId}`, eventData);
107
- }
108
- } catch (error) {
109
- console.error('Failed to track event:', error);
110
- }
111
- }
112
-
113
- /**
114
- * 크기 값을 숫자로 변환 (서버 API용)
115
- */
116
- private static parseNumericValue(value: number | string | undefined): number {
117
- if (typeof value === 'number') {
118
- return value;
119
- }
120
-
121
- if (typeof value === 'string') {
122
- // px 단위 제거하고 숫자만 추출
123
- const numericValue = parseFloat(value.replace(/px$/, ''));
124
- return isNaN(numericValue) ? 0 : numericValue;
125
- }
126
-
127
- return 0; // 기본값
128
- }
129
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * 노출 추적 및 중복 방지 관리 클래스
3
- * - 메모리 기반 중복 확인
4
- * - 세션 스토리지 기반 영구 추적
5
- * - 자동 정리 기능
6
- */
7
- export class ImpressionTracker {
8
- private static impressionTracker = new Map<string, number>();
9
- private static readonly IMPRESSION_COOLDOWN = 300000; // 5분 쿨다운
10
-
11
- /**
12
- * 중복 노출 여부 확인
13
- */
14
- static isDuplicateImpression(adId: string, slotId: string, debug = false): boolean {
15
- const key = `${adId}_${slotId}`;
16
- const now = Date.now();
17
-
18
- // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
19
- const lastImpression = ImpressionTracker.impressionTracker.get(key);
20
- if (lastImpression && (now - lastImpression) < ImpressionTracker.IMPRESSION_COOLDOWN) {
21
- if (debug) {
22
- console.log(`Duplicate impression blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ImpressionTracker.IMPRESSION_COOLDOWN - (now - lastImpression)) / 1000)}s remaining`);
23
- }
24
- return true;
25
- }
26
-
27
- // 세션 스토리지 기반 중복 확인 (새로고침 시에도 유지)
28
- const sessionKey = `adstage_impression_${key}`;
29
- const sessionImpression = sessionStorage.getItem(sessionKey);
30
- if (sessionImpression) {
31
- const sessionTime = parseInt(sessionImpression, 10);
32
- if (!isNaN(sessionTime) && (now - sessionTime) < ImpressionTracker.IMPRESSION_COOLDOWN) {
33
- if (debug) {
34
- console.log(`Session-based duplicate impression blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ImpressionTracker.IMPRESSION_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
35
- }
36
- // 메모리에도 기록하여 이후 요청 최적화
37
- ImpressionTracker.impressionTracker.set(key, sessionTime);
38
- return true;
39
- }
40
- }
41
-
42
- // 노출 시점 기록 (메모리 + 세션 스토리지)
43
- ImpressionTracker.impressionTracker.set(key, now);
44
- sessionStorage.setItem(sessionKey, now.toString());
45
-
46
- // 오래된 세션 스토리지 데이터 정리 (선택적)
47
- ImpressionTracker.cleanupOldImpressions();
48
-
49
- return false;
50
- }
51
-
52
- /**
53
- * 오래된 노출 추적 데이터 정리
54
- */
55
- private static cleanupOldImpressions(): void {
56
- const now = Date.now();
57
- const cleanupThreshold = ImpressionTracker.IMPRESSION_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
58
-
59
- // 세션 스토리지 정리
60
- for (let i = 0; i < sessionStorage.length; i++) {
61
- const key = sessionStorage.key(i);
62
- if (key && key.startsWith('adstage_impression_')) {
63
- const timestamp = sessionStorage.getItem(key);
64
- if (timestamp) {
65
- const time = parseInt(timestamp, 10);
66
- if (!isNaN(time) && (now - time) > cleanupThreshold) {
67
- sessionStorage.removeItem(key);
68
- i--; // 인덱스 조정
69
- }
70
- }
71
- }
72
- }
73
-
74
- // 메모리 정리
75
- for (const [key, timestamp] of ImpressionTracker.impressionTracker.entries()) {
76
- if ((now - timestamp) > cleanupThreshold) {
77
- ImpressionTracker.impressionTracker.delete(key);
78
- }
79
- }
80
- }
81
-
82
- /**
83
- * 모든 추적 데이터 정리
84
- */
85
- static clear(): void {
86
- ImpressionTracker.impressionTracker.clear();
87
- }
88
- }