@adstage/web-sdk 2.4.12 → 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/dist/index.cjs.js +99 -453
- package/dist/index.d.ts +3 -27
- package/dist/index.esm.js +99 -453
- package/dist/index.standalone.js +99 -453
- package/package.json +1 -1
- package/src/managers/ads/advertisement-event-tracker.ts +27 -36
- package/src/managers/ads/viewability-tracker.ts +16 -140
- package/src/managers/ads/viewable-event-tracker.ts +10 -82
- package/src/modules/ads/AdRenderer.ts +1 -1
- package/src/modules/ads/AdsModule.ts +15 -86
- package/src/utils/version.ts +23 -0
- package/src/managers/ads/basic-fraud-detector.ts +0 -191
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
* 광고 이벤트 추적 -
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
83
|
+
sdkVersion: getSDKVersion(),
|
|
81
84
|
timestamp: Date.now(),
|
|
82
85
|
},
|
|
83
86
|
|
|
84
|
-
//
|
|
85
|
-
...(
|
|
86
|
-
isViewable:
|
|
87
|
-
|
|
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
|
-
*
|
|
2
|
+
* 단순한 광고 노출 추적 (50% 노출시 즉시 VIEWABLE 이벤트)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export
|
|
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
|
|
58
|
-
private
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
18
|
+
console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
30
19
|
}
|
|
31
20
|
return true;
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
//
|
|
35
|
-
|
|
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}
|
|
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 {
|
|
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
|
-
//
|
|
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
|
*/
|
|
@@ -322,10 +298,10 @@ export class AdsModule implements BaseModule {
|
|
|
322
298
|
if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
|
|
323
299
|
await this.adRenderer?.renderAdSlider(slot, adstageData);
|
|
324
300
|
|
|
325
|
-
// 🔧 슬라이더의 첫 번째 광고도 자동
|
|
301
|
+
// 🔧 슬라이더의 첫 번째 광고도 자동 노출 추적 시작
|
|
326
302
|
if (adstageData.length > 0) {
|
|
327
303
|
setTimeout(() => {
|
|
328
|
-
this.
|
|
304
|
+
this.startSimpleViewabilityTracking(slot, adstageData[0]);
|
|
329
305
|
}, 100); // 슬라이더 렌더링 완료 후 추적 시작
|
|
330
306
|
}
|
|
331
307
|
} else {
|
|
@@ -334,7 +310,7 @@ export class AdsModule implements BaseModule {
|
|
|
334
310
|
await this.adRenderer?.renderAdElement(slot, adstageData[0]);
|
|
335
311
|
|
|
336
312
|
// ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
|
|
337
|
-
this.
|
|
313
|
+
this.startSimpleViewabilityTracking(slot, adstageData[0]);
|
|
338
314
|
}
|
|
339
315
|
|
|
340
316
|
slot.isLoaded = true;
|
|
@@ -354,9 +330,9 @@ export class AdsModule implements BaseModule {
|
|
|
354
330
|
// optimizeContainerForBannerAds 제거: AdRenderer.optimizeContainerForBannerAds 사용
|
|
355
331
|
|
|
356
332
|
/**
|
|
357
|
-
*
|
|
333
|
+
* 단순한 노출 추적 시작 (재시도 로직 포함)
|
|
358
334
|
*/
|
|
359
|
-
private
|
|
335
|
+
private startSimpleViewabilityTracking(slot: AdSlot, ad: Advertisement): void {
|
|
360
336
|
const tryStartTracking = (retryCount = 0) => {
|
|
361
337
|
const element = document.getElementById(slot.id);
|
|
362
338
|
|
|
@@ -373,20 +349,16 @@ export class AdsModule implements BaseModule {
|
|
|
373
349
|
return;
|
|
374
350
|
}
|
|
375
351
|
|
|
376
|
-
//
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
// viewability 추적
|
|
380
|
-
const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
|
|
381
|
-
await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
|
|
352
|
+
// 단순한 노출 추적
|
|
353
|
+
const tracker = new SimpleViewabilityTracker(element, async () => {
|
|
354
|
+
await this.handleViewableEvent(ad, slot);
|
|
382
355
|
});
|
|
383
356
|
|
|
384
357
|
// 정리를 위해 저장
|
|
385
358
|
(slot as any).viewabilityTracker = tracker;
|
|
386
|
-
(slot as any).fraudDetector = fraudDetector;
|
|
387
359
|
|
|
388
360
|
if (this._config?.debug) {
|
|
389
|
-
console.log(`🎯
|
|
361
|
+
console.log(`🎯 Simple viewability tracking started for slot: ${slot.id} (element found)`);
|
|
390
362
|
}
|
|
391
363
|
};
|
|
392
364
|
|
|
@@ -394,41 +366,23 @@ export class AdsModule implements BaseModule {
|
|
|
394
366
|
}
|
|
395
367
|
|
|
396
368
|
/**
|
|
397
|
-
* Viewable 이벤트 처리
|
|
369
|
+
* Viewable 이벤트 처리 (단순화됨)
|
|
398
370
|
*/
|
|
399
371
|
private async handleViewableEvent(
|
|
400
372
|
ad: Advertisement,
|
|
401
|
-
slot: AdSlot
|
|
402
|
-
metrics: ViewabilityMetrics,
|
|
403
|
-
fraudDetector: BasicFraudDetector
|
|
373
|
+
slot: AdSlot
|
|
404
374
|
): Promise<void> {
|
|
405
375
|
try {
|
|
406
|
-
const fraudScore = fraudDetector.calculateFraudScore();
|
|
407
|
-
|
|
408
|
-
// 높은 위험도면 차단
|
|
409
|
-
if (fraudScore.riskLevel === 'CRITICAL') {
|
|
410
|
-
if (this._config?.debug) {
|
|
411
|
-
console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
376
|
// VIEWABLE 이벤트 전송
|
|
417
377
|
if (this.advertisementEventTracker) {
|
|
418
378
|
await this.advertisementEventTracker.trackAdvertisementEvent(
|
|
419
379
|
ad._id,
|
|
420
380
|
slot.id,
|
|
421
|
-
AdEventType.VIEWABLE
|
|
422
|
-
{
|
|
423
|
-
viewabilityMetrics: metrics,
|
|
424
|
-
fraudScore: fraudScore.score,
|
|
425
|
-
fraudReasons: fraudScore.reasons,
|
|
426
|
-
riskLevel: fraudScore.riskLevel
|
|
427
|
-
}
|
|
381
|
+
AdEventType.VIEWABLE
|
|
428
382
|
);
|
|
429
383
|
|
|
430
384
|
if (this._config?.debug) {
|
|
431
|
-
console.log(`✅
|
|
385
|
+
console.log(`✅ Simple viewable impression tracked for ad ${ad._id}`);
|
|
432
386
|
}
|
|
433
387
|
}
|
|
434
388
|
|
|
@@ -437,16 +391,6 @@ export class AdsModule implements BaseModule {
|
|
|
437
391
|
}
|
|
438
392
|
}
|
|
439
393
|
|
|
440
|
-
/**
|
|
441
|
-
* Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
|
|
442
|
-
*/
|
|
443
|
-
// renderFallback 제거: AdRenderer.renderFallback 사용
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
447
|
-
*/
|
|
448
|
-
// createEmptyContainer 제거: AdRenderer 내부 구현 사용
|
|
449
|
-
|
|
450
394
|
/**
|
|
451
395
|
* 광고 데이터 가져오기
|
|
452
396
|
*/
|
|
@@ -523,21 +467,6 @@ export class AdsModule implements BaseModule {
|
|
|
523
467
|
return advertisements;
|
|
524
468
|
}
|
|
525
469
|
|
|
526
|
-
/**
|
|
527
|
-
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
528
|
-
*/
|
|
529
|
-
// renderAdSlider 제거: AdRenderer.renderAdSlider 사용
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* 광고 렌더링 (단일 광고용)
|
|
533
|
-
*/
|
|
534
|
-
// renderAd 제거: AdRenderer.renderAd 사용
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* 광고 요소 렌더링 (기본 구현)
|
|
538
|
-
*/
|
|
539
|
-
// renderAdElement 제거: AdRenderer.renderAdElement 사용
|
|
540
|
-
|
|
541
470
|
/**
|
|
542
471
|
* 광고 슬롯 새로고침
|
|
543
472
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdStage SDK - 버전 정보 유틸리티
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// package.json에서 버전 정보 가져오기 (빌드 시 자동으로 교체됨)
|
|
6
|
+
export const SDK_VERSION = '__SDK_VERSION__';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SDK 버전 정보 반환
|
|
10
|
+
*/
|
|
11
|
+
export function getSDKVersion(): string {
|
|
12
|
+
return SDK_VERSION;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 버전 정보가 포함된 User-Agent 문자열 생성
|
|
17
|
+
*/
|
|
18
|
+
export function getSDKUserAgent(): string {
|
|
19
|
+
if (typeof navigator !== 'undefined') {
|
|
20
|
+
return `${navigator.userAgent} AdStageSDK/${SDK_VERSION}`;
|
|
21
|
+
}
|
|
22
|
+
return `AdStageSDK/${SDK_VERSION}`;
|
|
23
|
+
}
|