@adstage/web-sdk 2.4.11 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adstage/web-sdk",
3
- "version": "2.4.11",
3
+ "version": "2.5.0",
4
4
  "description": "AdStage Web SDK - Production-ready marketing platform SDK with React Provider support for seamless integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -4,7 +4,7 @@ import { ViewableEventTracker } from './viewable-event-tracker';
4
4
  import { DeviceInfoCollector } from '../device-info-collector';
5
5
  import { DOMUtils } from '../../utils/dom-utils';
6
6
  import { ApiHeaders } from '../../utils/api-headers';
7
- import type { ViewabilityMetrics } from './viewability-tracker';
7
+ import { getSDKVersion } from '../../utils/version';
8
8
 
9
9
  /**
10
10
  * 광고 이벤트 추적 관리 클래스
@@ -26,41 +26,44 @@ export class AdvertisementEventTracker {
26
26
  }
27
27
 
28
28
  /**
29
- * 광고 이벤트 추적 - viewability 데이터 지원
29
+ * 광고 이벤트 추적 - 단순화된 viewable 처리
30
30
  */
31
31
  async trackAdvertisementEvent(
32
32
  adId: string,
33
33
  slotId: string,
34
- eventType: AdEventType,
35
- additionalData?: {
36
- viewabilityMetrics?: ViewabilityMetrics;
37
- fraudScore?: number;
38
- fraudReasons?: string[];
39
- riskLevel?: string;
40
- }
34
+ eventType: AdEventType
41
35
  ): Promise<void> {
42
36
  try {
43
37
  if (this.debug) {
44
38
  console.log(`🚀 AdvertisementEventTracker: Processing ${eventType} event for ad ${adId} in slot ${slotId}`);
45
39
  }
46
40
 
41
+ // VIEWABLE 이벤트 중복 확인
42
+ if (eventType === AdEventType.VIEWABLE) {
43
+ if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
44
+ if (this.debug) {
45
+ console.log(`⏭️ Skipping duplicate viewable event for ad ${adId} in slot ${slotId}`);
46
+ }
47
+ return;
48
+ }
49
+ }
50
+
47
51
  // 현재 슬롯 정보 가져오기
48
52
  const slot = this.slots.get(slotId);
49
53
 
50
54
  // 디바이스 정보 수집
51
55
  const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
52
56
 
53
- // 광고 이벤트 데이터 구성 (DTO 구조에 맞춤)
54
- // 서버에서 자동 설정: orgId (API 키로부터), advertisementId, action (URL 파라미터로부터)
55
- const eventData = {
56
- // 필수 필드들 (DTO 검증용)
57
- adType: slot?.adType || 'BANNER',
58
- platform: deviceInfo.platform,
59
- deviceId: deviceInfo.deviceId, // DTO 검증을 위해 deviceId 직접 전송
60
-
61
- // 디바이스 정보는 deviceInfo 객체로 래핑
62
- deviceInfo: deviceInfo,
57
+ // 광고 이벤트 데이터 구성 (단순화됨)
58
+ const eventData = {
59
+ // 필수 필드들 (DTO 검증용)
60
+ adType: slot?.adType || 'BANNER',
61
+ platform: deviceInfo.platform,
62
+ deviceId: deviceInfo.deviceId,
63
63
 
64
+ // 디바이스 정보는 deviceInfo 객체로 래핑
65
+ deviceInfo: deviceInfo,
66
+
64
67
  // 페이지 및 슬롯 정보
65
68
  pageUrl: DOMUtils.getPageInfo().url,
66
69
  pageTitle: DOMUtils.getPageInfo().title,
@@ -77,29 +80,17 @@ export class AdvertisementEventTracker {
77
80
  // 추가 메타데이터
78
81
  metadata: {
79
82
  eventType,
80
- sdkVersion: '1.0.0',
83
+ sdkVersion: getSDKVersion(),
81
84
  timestamp: Date.now(),
82
85
  },
83
86
 
84
- // viewable 관련 추가 데이터 (DTO 필드명과 매칭)
85
- ...(additionalData?.viewabilityMetrics && {
86
- isViewable: additionalData.viewabilityMetrics.isViewable,
87
- exposureTime: additionalData.viewabilityMetrics.exposureTime,
88
- maxVisibilityRatio: additionalData.viewabilityMetrics.maxVisibilityRatio,
89
- firstViewableTime: additionalData.viewabilityMetrics.firstViewableTime,
90
- // IAB 표준 준수 여부
91
- iabCompliant: additionalData.viewabilityMetrics.isViewable,
92
- }),
93
-
94
- // fraud 관련 데이터 (DTO 필드명과 매칭)
95
- ...(additionalData?.fraudScore !== undefined && {
96
- fraudScore: additionalData.fraudScore,
97
- fraudReasons: additionalData.fraudReasons,
98
- riskLevel: additionalData.riskLevel,
87
+ // VIEWABLE 이벤트의 경우 단순한 플래그만 설정
88
+ ...(eventType === AdEventType.VIEWABLE && {
89
+ isViewable: true,
90
+ iabCompliant: true, // 50% 노출 기준으로 단순 판정
99
91
  }),
100
92
  };
101
93
 
102
-
103
94
  const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
104
95
  const headers = ApiHeaders.createForEvents(this.apiKey, eventData);
105
96
 
@@ -1,80 +1,21 @@
1
1
  /**
2
- * IAB 표준 준수 viewable impression 측정
2
+ * 단순한 광고 노출 추적 (50% 노출시 즉시 VIEWABLE 이벤트)
3
3
  */
4
4
 
5
- export interface ViewabilityConfig {
6
- threshold: number; // 노출 비율 (0.5 = 50%)
7
- minDuration: number; // 최소 지속 시간 (ms)
8
- maxMeasureTime: number; // 최대 측정 시간 (ms)
9
- }
10
-
11
- export interface ViewabilityMetrics {
12
- isViewable: boolean;
13
- exposureTime: number;
14
- maxVisibilityRatio: number;
15
- firstViewableTime: number | null;
16
- measureStartTime: number;
17
- }
18
-
19
- // 광고 타입별 IAB 표준 설정
20
- export const VIEWABILITY_STANDARDS: Record<string, ViewabilityConfig> = {
21
- BANNER: {
22
- threshold: 0.5,
23
- minDuration: 1000,
24
- maxMeasureTime: 30000
25
- },
26
- VIDEO: {
27
- threshold: 0.5,
28
- minDuration: 2000,
29
- maxMeasureTime: 60000
30
- },
31
- NATIVE: {
32
- threshold: 0.5,
33
- minDuration: 1000,
34
- maxMeasureTime: 30000
35
- },
36
- INTERSTITIAL: {
37
- threshold: 0.5,
38
- minDuration: 1000,
39
- maxMeasureTime: 10000
40
- },
41
- TEXT: {
42
- threshold: 0.5,
43
- minDuration: 1000,
44
- maxMeasureTime: 30000
45
- },
46
- POPUP: {
47
- threshold: 0.5,
48
- minDuration: 1000,
49
- maxMeasureTime: 10000
50
- }
51
- };
52
-
53
- export class ViewabilityTracker {
54
- private config: ViewabilityConfig;
5
+ export class SimpleViewabilityTracker {
55
6
  private element: HTMLElement;
56
7
  private observer: IntersectionObserver | null = null;
57
- private viewabilityTimer: NodeJS.Timeout | null = null;
58
- private maxVisibilityTimer: NodeJS.Timeout | null = null;
59
- private startTime: number = 0;
60
- private maxVisibilityRatio: number = 0;
61
- private firstViewableTime: number | null = null;
62
- private isViewableAchieved: boolean = false;
63
-
64
- private onViewableCallback?: (metrics: ViewabilityMetrics) => void;
8
+ private isViewableTriggered: boolean = false;
9
+ private onViewableCallback?: () => void;
65
10
 
66
11
  constructor(
67
12
  element: HTMLElement,
68
- adType: string,
69
- onViewable?: (metrics: ViewabilityMetrics) => void
13
+ onViewable?: () => void
70
14
  ) {
71
15
  this.element = element;
72
- this.config = VIEWABILITY_STANDARDS[adType] || VIEWABILITY_STANDARDS.BANNER;
73
16
  this.onViewableCallback = onViewable;
74
17
 
75
- this.startTime = performance.now();
76
18
  this.initIntersectionObserver();
77
- this.initMaxMeasureTimer();
78
19
  }
79
20
 
80
21
  private initIntersectionObserver(): void {
@@ -87,7 +28,7 @@ export class ViewabilityTracker {
87
28
  this.observer = new IntersectionObserver(
88
29
  (entries) => this.handleIntersection(entries),
89
30
  {
90
- threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0],
31
+ threshold: 0.5, // 50% 노출
91
32
  rootMargin: '0px'
92
33
  }
93
34
  );
@@ -97,90 +38,25 @@ export class ViewabilityTracker {
97
38
 
98
39
  private handleIntersection(entries: IntersectionObserverEntry[]): void {
99
40
  entries.forEach(entry => {
100
- const visibilityRatio = entry.intersectionRatio;
101
- const isVisible = this.isDocumentVisible();
102
-
103
- // 최대 가시성 비율 추적
104
- this.maxVisibilityRatio = Math.max(this.maxVisibilityRatio, visibilityRatio);
105
-
106
- // Viewable 조건 확인 (50% 이상 + 문서 가시성)
107
- if (visibilityRatio >= this.config.threshold && isVisible) {
108
- this.startViewabilityTimer();
109
- } else {
110
- this.stopViewabilityTimer();
41
+ // 50% 이상 노출되고 문서가 가시상태이며 아직 트리거되지 않은 경우
42
+ if (entry.intersectionRatio >= 0.5 &&
43
+ this.isDocumentVisible() &&
44
+ !this.isViewableTriggered) {
45
+
46
+ this.isViewableTriggered = true;
47
+
48
+ if (this.onViewableCallback) {
49
+ this.onViewableCallback();
50
+ }
111
51
  }
112
52
  });
113
53
  }
114
54
 
115
55
  private isDocumentVisible(): boolean {
116
- // 단순한 문서 가시성 확인
117
56
  return !document.hidden && document.visibilityState === 'visible';
118
57
  }
119
58
 
120
- private startViewabilityTimer(): void {
121
- if (this.viewabilityTimer || this.isViewableAchieved) return;
122
-
123
- if (this.firstViewableTime === null) {
124
- this.firstViewableTime = performance.now();
125
- }
126
-
127
- this.viewabilityTimer = setTimeout(() => {
128
- this.onViewabilityAchieved();
129
- }, this.config.minDuration);
130
- }
131
-
132
- private stopViewabilityTimer(): void {
133
- if (this.viewabilityTimer) {
134
- clearTimeout(this.viewabilityTimer);
135
- this.viewabilityTimer = null;
136
- }
137
- }
138
-
139
- private initMaxMeasureTimer(): void {
140
- // 최대 측정 시간 후 자동 종료
141
- this.maxVisibilityTimer = setTimeout(() => {
142
- this.destroy();
143
- }, this.config.maxMeasureTime);
144
- }
145
-
146
- private onViewabilityAchieved(): void {
147
- if (this.isViewableAchieved) return;
148
-
149
- this.isViewableAchieved = true;
150
- const metrics = this.calculateMetrics();
151
-
152
- if (this.onViewableCallback) {
153
- this.onViewableCallback(metrics);
154
- }
155
- }
156
-
157
- private calculateMetrics(): ViewabilityMetrics {
158
- const currentTime = performance.now();
159
- const exposureTime = this.firstViewableTime
160
- ? currentTime - this.firstViewableTime
161
- : 0;
162
-
163
- return {
164
- isViewable: this.isViewableAchieved,
165
- exposureTime,
166
- maxVisibilityRatio: this.maxVisibilityRatio,
167
- firstViewableTime: this.firstViewableTime,
168
- measureStartTime: this.startTime,
169
- };
170
- }
171
-
172
- public getMetrics(): ViewabilityMetrics {
173
- return this.calculateMetrics();
174
- }
175
-
176
59
  public destroy(): void {
177
- this.stopViewabilityTimer();
178
-
179
- if (this.maxVisibilityTimer) {
180
- clearTimeout(this.maxVisibilityTimer);
181
- this.maxVisibilityTimer = null;
182
- }
183
-
184
60
  if (this.observer) {
185
61
  this.observer.disconnect();
186
62
  this.observer = null;
@@ -1,110 +1,40 @@
1
1
  /**
2
- * VIEWABLE 이벤트 추적 및 중복 방지 관리 클래스
3
- * - 메모리 기반 중복 확인
4
- * - 세션 스토리지 기반 영구 추적
5
- * - 자동 정리 기능
6
- * - ViewabilityTracker와 함께 사용하여 중복 viewable 이벤트 방지
2
+ * 단순한 VIEWABLE 이벤트 중복 방지 관리 클래스
3
+ * - 세션당 동일 광고 1회만 VIEWABLE 이벤트 허용
4
+ * - 메모리 기반 추적으로 단순화
7
5
  */
8
6
  export class ViewableEventTracker {
9
- private static viewableTracker = new Map<string, number>();
10
- private static readonly VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
11
- private static readonly VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
7
+ private static viewableTracker = new Set<string>();
12
8
 
13
9
  /**
14
10
  * 중복 viewable 이벤트 여부 확인
15
11
  */
16
12
  static isDuplicateViewable(adId: string, slotId: string, debug = false): boolean {
17
13
  const key = `${adId}_${slotId}`;
18
- const now = Date.now();
19
14
 
20
- // 디버그 모드에 따른 쿨다운 시간 결정
21
- const cooldownTime = debug ?
22
- ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
23
- ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
24
-
25
- // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
26
- const lastViewable = ViewableEventTracker.viewableTracker.get(key);
27
- if (lastViewable && (now - lastViewable) < cooldownTime) {
15
+ // 이미 VIEWABLE 이벤트가 발생한 광고인지 확인
16
+ if (ViewableEventTracker.viewableTracker.has(key)) {
28
17
  if (debug) {
29
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
18
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
30
19
  }
31
20
  return true;
32
21
  }
33
22
 
34
- // 세션 스토리지 기반 중복 확인 (새로고침 시에도 유지)
35
- const sessionKey = `adstage_viewable_${key}`;
36
- const sessionViewable = sessionStorage.getItem(sessionKey);
37
- if (sessionViewable) {
38
- const sessionTime = parseInt(sessionViewable, 10);
39
- if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
40
- if (debug) {
41
- console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - sessionTime)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
42
- }
43
- // 메모리에도 기록하여 이후 요청 최적화
44
- ViewableEventTracker.viewableTracker.set(key, sessionTime);
45
- return true;
46
- }
47
- }
48
-
49
- // viewable 이벤트 시점 기록 (메모리 + 세션 스토리지)
50
- ViewableEventTracker.viewableTracker.set(key, now);
51
- sessionStorage.setItem(sessionKey, now.toString());
52
-
53
- // 오래된 세션 스토리지 데이터 정리 (선택적)
54
- ViewableEventTracker.cleanupOldViewables();
23
+ // 새로운 VIEWABLE 이벤트 기록
24
+ ViewableEventTracker.viewableTracker.add(key);
55
25
 
56
26
  if (debug) {
57
- console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
27
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
58
28
  }
59
29
 
60
30
  return false;
61
31
  }
62
32
 
63
- /**
64
- * 오래된 viewable 추적 데이터 정리
65
- */
66
- private static cleanupOldViewables(): void {
67
- const now = Date.now();
68
- // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
69
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
70
-
71
- // 세션 스토리지 정리
72
- for (let i = 0; i < sessionStorage.length; i++) {
73
- const key = sessionStorage.key(i);
74
- if (key && key.startsWith('adstage_viewable_')) {
75
- const timestamp = sessionStorage.getItem(key);
76
- if (timestamp) {
77
- const time = parseInt(timestamp, 10);
78
- if (!isNaN(time) && (now - time) > cleanupThreshold) {
79
- sessionStorage.removeItem(key);
80
- i--; // 인덱스 조정
81
- }
82
- }
83
- }
84
- }
85
-
86
- // 메모리 정리
87
- for (const [key, timestamp] of ViewableEventTracker.viewableTracker.entries()) {
88
- if ((now - timestamp) > cleanupThreshold) {
89
- ViewableEventTracker.viewableTracker.delete(key);
90
- }
91
- }
92
- }
93
-
94
33
  /**
95
34
  * 모든 추적 데이터 정리 (디버그용)
96
35
  */
97
36
  static clear(): void {
98
37
  ViewableEventTracker.viewableTracker.clear();
99
- // 세션 스토리지에서도 adstage_viewable_ 키들 제거
100
- const keysToRemove: string[] = [];
101
- for (let i = 0; i < sessionStorage.length; i++) {
102
- const key = sessionStorage.key(i);
103
- if (key && key.startsWith('adstage_viewable_')) {
104
- keysToRemove.push(key);
105
- }
106
- }
107
- keysToRemove.forEach(key => sessionStorage.removeItem(key));
108
38
  }
109
39
 
110
40
  /**
@@ -113,7 +43,5 @@ export class ViewableEventTracker {
113
43
  static clearAdViewable(adId: string, slotId: string): void {
114
44
  const key = `${adId}_${slotId}`;
115
45
  ViewableEventTracker.viewableTracker.delete(key);
116
- const sessionKey = `adstage_viewable_${key}`;
117
- sessionStorage.removeItem(sessionKey);
118
46
  }
119
47
  }
@@ -253,7 +253,7 @@ export class AdRenderer {
253
253
  if (this.debug) {
254
254
  console.log(`🔄 Starting advertisement event tracking: ${eventType} for ad ${adId} in slot ${slotId}`);
255
255
  }
256
- await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType, {});
256
+ await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType);
257
257
  if (this.debug) {
258
258
  console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
259
259
  }
@@ -8,9 +8,7 @@ import { AdType, AdEventType } from '../../types/advertisement';
8
8
  import type { AdSlot, Advertisement } from '../../types/advertisement';
9
9
  // 렌더링 관련 매니저/뷰 트래커는 AdRenderer로 이전됨
10
10
  import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
11
- import { ViewabilityTracker, VIEWABILITY_STANDARDS } from '../../managers/ads/viewability-tracker';
12
- import { BasicFraudDetector } from '../../managers/ads/basic-fraud-detector';
13
- import type { ViewabilityMetrics } from '../../managers/ads/viewability-tracker';
11
+ import { SimpleViewabilityTracker } from '../../managers/ads/viewability-tracker';
14
12
  import { endpoints } from '../../constants/endpoints';
15
13
  import { ApiHeaders } from '../../utils/api-headers';
16
14
  // 새로 분리된 클래스들
@@ -190,16 +188,11 @@ export class AdsModule implements BaseModule {
190
188
  throw new Error(`Ad slot not found: ${slotId}`);
191
189
  }
192
190
 
193
- // ViewabilityTracker 정리
191
+ // SimpleViewabilityTracker 정리
194
192
  if ((slot as any).viewabilityTracker) {
195
193
  (slot as any).viewabilityTracker.destroy();
196
194
  }
197
195
 
198
- // BasicFraudDetector 정리
199
- if ((slot as any).fraudDetector) {
200
- (slot as any).fraudDetector.destroy();
201
- }
202
-
203
196
  // DOM에서 제거
204
197
  const container = document.getElementById(slot.containerId);
205
198
  if (container) {
@@ -283,23 +276,6 @@ export class AdsModule implements BaseModule {
283
276
  return slotId;
284
277
  }
285
278
 
286
- // createAdSlot 제거: AdRenderer.createPlaceholder 사용
287
-
288
- /**
289
- * 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
290
- */
291
- // calculateOptimalContainerSize 제거: AdRenderer.calculateOptimalContainerSize 사용
292
-
293
- /**
294
- * 최적 크기 조정 전략 선택
295
- */
296
- // selectOptimalSizeStrategy 제거: AdRenderer 내부 구현 사용
297
-
298
- /**
299
- * 전략에 따른 최적 높이 계산
300
- */
301
- // calculateOptimalHeight 제거: AdRenderer 내부 구현 사용
302
-
303
279
  /**
304
280
  * 백그라운드에서 광고 콘텐츠 로드
305
281
  */
@@ -321,13 +297,20 @@ export class AdsModule implements BaseModule {
321
297
  // 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
322
298
  if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
323
299
  await this.adRenderer?.renderAdSlider(slot, adstageData);
300
+
301
+ // 🔧 슬라이더의 첫 번째 광고도 자동 노출 추적 시작
302
+ if (adstageData.length > 0) {
303
+ setTimeout(() => {
304
+ this.startSimpleViewabilityTracking(slot, adstageData[0]);
305
+ }, 100); // 슬라이더 렌더링 완료 후 추적 시작
306
+ }
324
307
  } else {
325
308
  // 광고가 1개면 일반 렌더링
326
309
  slot.advertisement = adstageData[0];
327
310
  await this.adRenderer?.renderAdElement(slot, adstageData[0]);
328
311
 
329
312
  // ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
330
- this.startBasicViewabilityTracking(slot, adstageData[0]);
313
+ this.startSimpleViewabilityTracking(slot, adstageData[0]);
331
314
  }
332
315
 
333
316
  slot.isLoaded = true;
@@ -347,65 +330,59 @@ export class AdsModule implements BaseModule {
347
330
  // optimizeContainerForBannerAds 제거: AdRenderer.optimizeContainerForBannerAds 사용
348
331
 
349
332
  /**
350
- * 기본 viewability 추적 시작
333
+ * 단순한 노출 추적 시작 (재시도 로직 포함)
351
334
  */
352
- private startBasicViewabilityTracking(slot: AdSlot, ad: Advertisement): void {
353
- const element = document.getElementById(slot.id);
354
- if (!element) return;
355
-
356
- // 기본 fraud 검사
357
- const fraudDetector = new BasicFraudDetector();
358
-
359
- // viewability 추적
360
- const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
361
- await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
362
- });
363
-
364
- // 정리를 위해 저장
365
- (slot as any).viewabilityTracker = tracker;
366
- (slot as any).fraudDetector = fraudDetector;
335
+ private startSimpleViewabilityTracking(slot: AdSlot, ad: Advertisement): void {
336
+ const tryStartTracking = (retryCount = 0) => {
337
+ const element = document.getElementById(slot.id);
338
+
339
+ if (!element) {
340
+ if (retryCount < 5) {
341
+ // 최대 5번 재시도 (총 1.5초)
342
+ setTimeout(() => tryStartTracking(retryCount + 1), 300);
343
+ if (this._config?.debug) {
344
+ console.log(`🔄 Retrying viewability tracking for slot: ${slot.id} (attempt ${retryCount + 1})`);
345
+ }
346
+ } else {
347
+ console.warn(`❌ Failed to find element for viewability tracking: ${slot.id}`);
348
+ }
349
+ return;
350
+ }
351
+
352
+ // 단순한 노출 추적
353
+ const tracker = new SimpleViewabilityTracker(element, async () => {
354
+ await this.handleViewableEvent(ad, slot);
355
+ });
356
+
357
+ // 정리를 위해 저장
358
+ (slot as any).viewabilityTracker = tracker;
367
359
 
368
- if (this._config?.debug) {
369
- console.log(`🎯 Viewability tracking started for slot: ${slot.id}`);
370
- }
360
+ if (this._config?.debug) {
361
+ console.log(`🎯 Simple viewability tracking started for slot: ${slot.id} (element found)`);
362
+ }
363
+ };
364
+
365
+ tryStartTracking();
371
366
  }
372
367
 
373
368
  /**
374
- * Viewable 이벤트 처리
369
+ * Viewable 이벤트 처리 (단순화됨)
375
370
  */
376
371
  private async handleViewableEvent(
377
372
  ad: Advertisement,
378
- slot: AdSlot,
379
- metrics: ViewabilityMetrics,
380
- fraudDetector: BasicFraudDetector
373
+ slot: AdSlot
381
374
  ): Promise<void> {
382
375
  try {
383
- const fraudScore = fraudDetector.calculateFraudScore();
384
-
385
- // 높은 위험도면 차단
386
- if (fraudScore.riskLevel === 'CRITICAL') {
387
- if (this._config?.debug) {
388
- console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
389
- }
390
- return;
391
- }
392
-
393
376
  // VIEWABLE 이벤트 전송
394
377
  if (this.advertisementEventTracker) {
395
378
  await this.advertisementEventTracker.trackAdvertisementEvent(
396
379
  ad._id,
397
380
  slot.id,
398
- AdEventType.VIEWABLE,
399
- {
400
- viewabilityMetrics: metrics,
401
- fraudScore: fraudScore.score,
402
- fraudReasons: fraudScore.reasons,
403
- riskLevel: fraudScore.riskLevel
404
- }
381
+ AdEventType.VIEWABLE
405
382
  );
406
383
 
407
384
  if (this._config?.debug) {
408
- console.log(`✅ Viewable impression tracked for ad ${ad._id} (fraud score: ${fraudScore.score})`);
385
+ console.log(`✅ Simple viewable impression tracked for ad ${ad._id}`);
409
386
  }
410
387
  }
411
388
 
@@ -414,16 +391,6 @@ export class AdsModule implements BaseModule {
414
391
  }
415
392
  }
416
393
 
417
- /**
418
- * Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
419
- */
420
- // renderFallback 제거: AdRenderer.renderFallback 사용
421
-
422
- /**
423
- * 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
424
- */
425
- // createEmptyContainer 제거: AdRenderer 내부 구현 사용
426
-
427
394
  /**
428
395
  * 광고 데이터 가져오기
429
396
  */
@@ -500,21 +467,6 @@ export class AdsModule implements BaseModule {
500
467
  return advertisements;
501
468
  }
502
469
 
503
- /**
504
- * 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
505
- */
506
- // renderAdSlider 제거: AdRenderer.renderAdSlider 사용
507
-
508
- /**
509
- * 광고 렌더링 (단일 광고용)
510
- */
511
- // renderAd 제거: AdRenderer.renderAd 사용
512
-
513
- /**
514
- * 광고 요소 렌더링 (기본 구현)
515
- */
516
- // renderAdElement 제거: AdRenderer.renderAdElement 사용
517
-
518
470
  /**
519
471
  * 광고 슬롯 새로고침
520
472
  */