@adstage/web-sdk 2.0.0 → 2.1.2
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/README.md +1 -1
- package/dist/index.cjs.js +559 -91
- package/dist/index.d.ts +20 -12
- package/dist/index.esm.js +559 -91
- package/dist/index.standalone.js +559 -91
- package/package.json +1 -1
- package/src/managers/ads/advertisement-event-tracker.ts +142 -0
- package/src/managers/ads/basic-fraud-detector.ts +191 -0
- package/src/managers/{carousel-slider-manager.ts → ads/carousel-slider-manager.ts} +4 -4
- package/src/managers/{text-transition-manager.ts → ads/text-transition-manager.ts} +4 -4
- package/src/managers/ads/viewability-tracker.ts +189 -0
- package/src/managers/ads/viewable-event-tracker.ts +89 -0
- package/src/modules/ads/AdsModule.ts +252 -26
- package/src/types/advertisement.ts +4 -17
- package/src/types/api.ts +1 -5
- package/src/managers/event-tracker.ts +0 -129
- package/src/managers/impression-tracker.ts +0 -88
package/dist/index.standalone.js
CHANGED
|
@@ -17,15 +17,8 @@ var Platform;
|
|
|
17
17
|
// 광고 이벤트 타입
|
|
18
18
|
var AdEventType;
|
|
19
19
|
(function (AdEventType) {
|
|
20
|
-
AdEventType["IMPRESSION"] = "IMPRESSION";
|
|
21
|
-
AdEventType["CLICK"] = "CLICK";
|
|
22
|
-
AdEventType["HOVER"] = "HOVER";
|
|
23
20
|
AdEventType["VIEWABLE"] = "VIEWABLE";
|
|
24
|
-
AdEventType["
|
|
25
|
-
AdEventType["COMPLETED"] = "COMPLETED";
|
|
26
|
-
AdEventType["VIDEO_START"] = "VIDEO_START";
|
|
27
|
-
AdEventType["VIDEO_COMPLETE"] = "VIDEO_COMPLETE";
|
|
28
|
-
AdEventType["ERROR"] = "ERROR";
|
|
21
|
+
AdEventType["CLICK"] = "CLICK";
|
|
29
22
|
})(AdEventType || (AdEventType = {}));
|
|
30
23
|
// 디바이스 타입
|
|
31
24
|
var DeviceType;
|
|
@@ -947,7 +940,7 @@ class CarouselSliderManager {
|
|
|
947
940
|
}
|
|
948
941
|
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
949
942
|
if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
950
|
-
trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.
|
|
943
|
+
trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.VIEWABLE);
|
|
951
944
|
}
|
|
952
945
|
};
|
|
953
946
|
// 무한 루프 처리 함수
|
|
@@ -1267,7 +1260,7 @@ class TextTransitionManager {
|
|
|
1267
1260
|
currentSlide = index;
|
|
1268
1261
|
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
1269
1262
|
if (currentSlide > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
1270
|
-
trackEventCallback(advertisements[currentSlide]._id, slot.id, AdEventType.
|
|
1263
|
+
trackEventCallback(advertisements[currentSlide]._id, slot.id, AdEventType.VIEWABLE);
|
|
1271
1264
|
}
|
|
1272
1265
|
};
|
|
1273
1266
|
// 자동 슬라이드
|
|
@@ -1333,57 +1326,58 @@ class TextTransitionManager {
|
|
|
1333
1326
|
}
|
|
1334
1327
|
|
|
1335
1328
|
/**
|
|
1336
|
-
*
|
|
1329
|
+
* VIEWABLE 이벤트 추적 및 중복 방지 관리 클래스
|
|
1337
1330
|
* - 메모리 기반 중복 확인
|
|
1338
1331
|
* - 세션 스토리지 기반 영구 추적
|
|
1339
1332
|
* - 자동 정리 기능
|
|
1333
|
+
* - ViewabilityTracker와 함께 사용하여 중복 viewable 이벤트 방지
|
|
1340
1334
|
*/
|
|
1341
|
-
class
|
|
1335
|
+
class ViewableEventTracker {
|
|
1342
1336
|
/**
|
|
1343
|
-
* 중복
|
|
1337
|
+
* 중복 viewable 이벤트 여부 확인
|
|
1344
1338
|
*/
|
|
1345
|
-
static
|
|
1339
|
+
static isDuplicateViewable(adId, slotId, debug = false) {
|
|
1346
1340
|
const key = `${adId}_${slotId}`;
|
|
1347
1341
|
const now = Date.now();
|
|
1348
1342
|
// 메모리 기반 중복 확인 (새로고침 시 초기화됨)
|
|
1349
|
-
const
|
|
1350
|
-
if (
|
|
1343
|
+
const lastViewable = ViewableEventTracker.viewableTracker.get(key);
|
|
1344
|
+
if (lastViewable && (now - lastViewable) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
|
|
1351
1345
|
if (debug) {
|
|
1352
|
-
console.log(`Duplicate
|
|
1346
|
+
console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - lastViewable)) / 1000)}s remaining`);
|
|
1353
1347
|
}
|
|
1354
1348
|
return true;
|
|
1355
1349
|
}
|
|
1356
1350
|
// 세션 스토리지 기반 중복 확인 (새로고침 시에도 유지)
|
|
1357
|
-
const sessionKey = `
|
|
1358
|
-
const
|
|
1359
|
-
if (
|
|
1360
|
-
const sessionTime = parseInt(
|
|
1361
|
-
if (!isNaN(sessionTime) && (now - sessionTime) <
|
|
1351
|
+
const sessionKey = `adstage_viewable_${key}`;
|
|
1352
|
+
const sessionViewable = sessionStorage.getItem(sessionKey);
|
|
1353
|
+
if (sessionViewable) {
|
|
1354
|
+
const sessionTime = parseInt(sessionViewable, 10);
|
|
1355
|
+
if (!isNaN(sessionTime) && (now - sessionTime) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
|
|
1362
1356
|
if (debug) {
|
|
1363
|
-
console.log(`Session-based duplicate
|
|
1357
|
+
console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
|
|
1364
1358
|
}
|
|
1365
1359
|
// 메모리에도 기록하여 이후 요청 최적화
|
|
1366
|
-
|
|
1360
|
+
ViewableEventTracker.viewableTracker.set(key, sessionTime);
|
|
1367
1361
|
return true;
|
|
1368
1362
|
}
|
|
1369
1363
|
}
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1364
|
+
// viewable 이벤트 시점 기록 (메모리 + 세션 스토리지)
|
|
1365
|
+
ViewableEventTracker.viewableTracker.set(key, now);
|
|
1372
1366
|
sessionStorage.setItem(sessionKey, now.toString());
|
|
1373
1367
|
// 오래된 세션 스토리지 데이터 정리 (선택적)
|
|
1374
|
-
|
|
1368
|
+
ViewableEventTracker.cleanupOldViewables();
|
|
1375
1369
|
return false;
|
|
1376
1370
|
}
|
|
1377
1371
|
/**
|
|
1378
|
-
* 오래된
|
|
1372
|
+
* 오래된 viewable 추적 데이터 정리
|
|
1379
1373
|
*/
|
|
1380
|
-
static
|
|
1374
|
+
static cleanupOldViewables() {
|
|
1381
1375
|
const now = Date.now();
|
|
1382
|
-
const cleanupThreshold =
|
|
1376
|
+
const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
|
|
1383
1377
|
// 세션 스토리지 정리
|
|
1384
1378
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
1385
1379
|
const key = sessionStorage.key(i);
|
|
1386
|
-
if (key && key.startsWith('
|
|
1380
|
+
if (key && key.startsWith('adstage_viewable_')) {
|
|
1387
1381
|
const timestamp = sessionStorage.getItem(key);
|
|
1388
1382
|
if (timestamp) {
|
|
1389
1383
|
const time = parseInt(timestamp, 10);
|
|
@@ -1395,9 +1389,9 @@ class ImpressionTracker {
|
|
|
1395
1389
|
}
|
|
1396
1390
|
}
|
|
1397
1391
|
// 메모리 정리
|
|
1398
|
-
for (const [key, timestamp] of
|
|
1392
|
+
for (const [key, timestamp] of ViewableEventTracker.viewableTracker.entries()) {
|
|
1399
1393
|
if ((now - timestamp) > cleanupThreshold) {
|
|
1400
|
-
|
|
1394
|
+
ViewableEventTracker.viewableTracker.delete(key);
|
|
1401
1395
|
}
|
|
1402
1396
|
}
|
|
1403
1397
|
}
|
|
@@ -1405,11 +1399,11 @@ class ImpressionTracker {
|
|
|
1405
1399
|
* 모든 추적 데이터 정리
|
|
1406
1400
|
*/
|
|
1407
1401
|
static clear() {
|
|
1408
|
-
|
|
1402
|
+
ViewableEventTracker.viewableTracker.clear();
|
|
1409
1403
|
}
|
|
1410
1404
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1405
|
+
ViewableEventTracker.viewableTracker = new Map();
|
|
1406
|
+
ViewableEventTracker.VIEWABLE_COOLDOWN = 300000; // 5분 쿨다운
|
|
1413
1407
|
|
|
1414
1408
|
/**
|
|
1415
1409
|
* 디바이스 정보 수집 클래스
|
|
@@ -1547,12 +1541,12 @@ class ApiHeaders {
|
|
|
1547
1541
|
}
|
|
1548
1542
|
|
|
1549
1543
|
/**
|
|
1550
|
-
* 이벤트 추적 관리 클래스
|
|
1551
|
-
* - 광고 이벤트 추적 및 전송
|
|
1552
|
-
* - 중복
|
|
1553
|
-
* - 서버 API 통신
|
|
1544
|
+
* 광고 이벤트 추적 관리 클래스
|
|
1545
|
+
* - 광고 전용 이벤트 추적 및 전송
|
|
1546
|
+
* - Viewable 이벤트 중복 방지 통합
|
|
1547
|
+
* - 광고 서버 API 통신
|
|
1554
1548
|
*/
|
|
1555
|
-
class
|
|
1549
|
+
class AdvertisementEventTracker {
|
|
1556
1550
|
constructor(baseUrl, apiKey, debug, slots) {
|
|
1557
1551
|
this.baseUrl = baseUrl;
|
|
1558
1552
|
this.apiKey = apiKey;
|
|
@@ -1560,21 +1554,21 @@ class EventTracker {
|
|
|
1560
1554
|
this.slots = slots;
|
|
1561
1555
|
}
|
|
1562
1556
|
/**
|
|
1563
|
-
* 이벤트 추적
|
|
1557
|
+
* 광고 이벤트 추적 - viewability 데이터 지원
|
|
1564
1558
|
*/
|
|
1565
|
-
async
|
|
1559
|
+
async trackAdvertisementEvent(adId, slotId, eventType, additionalData) {
|
|
1566
1560
|
try {
|
|
1567
|
-
//
|
|
1568
|
-
if (eventType === AdEventType.
|
|
1569
|
-
if (
|
|
1570
|
-
return; // 중복
|
|
1561
|
+
// VIEWABLE 이벤트의 경우 중복 확인
|
|
1562
|
+
if (eventType === AdEventType.VIEWABLE) {
|
|
1563
|
+
if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
|
|
1564
|
+
return; // 중복 viewable 이벤트이므로 추적하지 않음
|
|
1571
1565
|
}
|
|
1572
1566
|
}
|
|
1573
1567
|
// 현재 슬롯 정보 가져오기
|
|
1574
1568
|
const slot = this.slots.get(slotId);
|
|
1575
1569
|
// 디바이스 정보 수집
|
|
1576
1570
|
const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
|
|
1577
|
-
// 이벤트 데이터 구성 (
|
|
1571
|
+
// 광고 이벤트 데이터 구성 (DTO 구조에 맞춤)
|
|
1578
1572
|
const eventData = {
|
|
1579
1573
|
// 서버에서 자동 설정: orgId, advertisementId, action
|
|
1580
1574
|
// 하지만 DTO 검증을 위해 임시값 제공
|
|
@@ -1584,40 +1578,40 @@ class EventTracker {
|
|
|
1584
1578
|
// 필수 필드들
|
|
1585
1579
|
adType: slot?.adType || 'BANNER',
|
|
1586
1580
|
platform: deviceInfo.platform,
|
|
1587
|
-
// 디바이스
|
|
1588
|
-
|
|
1589
|
-
osVersion: deviceInfo.osVersion,
|
|
1590
|
-
deviceModel: deviceInfo.deviceModel,
|
|
1591
|
-
appVersion: deviceInfo.appVersion,
|
|
1592
|
-
sdkVersion: deviceInfo.sdkVersion,
|
|
1593
|
-
language: deviceInfo.language,
|
|
1594
|
-
country: deviceInfo.country,
|
|
1595
|
-
ipAddress: deviceInfo.ipAddress,
|
|
1596
|
-
userAgent: deviceInfo.userAgent,
|
|
1597
|
-
timezone: deviceInfo.timezone,
|
|
1598
|
-
viewportWidth: deviceInfo.viewportWidth,
|
|
1599
|
-
viewportHeight: deviceInfo.viewportHeight,
|
|
1600
|
-
screenWidth: deviceInfo.screenWidth,
|
|
1601
|
-
screenHeight: deviceInfo.screenHeight,
|
|
1602
|
-
connectionType: deviceInfo.connectionType,
|
|
1581
|
+
// 디바이스 정보는 deviceInfo 객체로 래핑
|
|
1582
|
+
deviceInfo: deviceInfo,
|
|
1603
1583
|
// 페이지 및 슬롯 정보
|
|
1604
1584
|
pageUrl: DOMUtils.getPageInfo().url,
|
|
1605
1585
|
pageTitle: DOMUtils.getPageInfo().title,
|
|
1606
1586
|
referrer: DOMUtils.getPageInfo().referrer,
|
|
1607
1587
|
slotId,
|
|
1608
1588
|
slotPosition: DeviceInfoCollector.getSlotPosition(slot?.containerId || ''),
|
|
1609
|
-
slotWidth:
|
|
1610
|
-
slotHeight:
|
|
1589
|
+
slotWidth: AdvertisementEventTracker.parseNumericValue(slot?.width),
|
|
1590
|
+
slotHeight: AdvertisementEventTracker.parseNumericValue(slot?.height),
|
|
1611
1591
|
sessionId: deviceInfo.sessionId,
|
|
1612
1592
|
// 성능 메트릭
|
|
1613
1593
|
pageLoadTime: performance.now(),
|
|
1614
|
-
timestamp: new Date().toISOString(),
|
|
1615
1594
|
// 추가 메타데이터
|
|
1616
1595
|
metadata: {
|
|
1617
1596
|
eventType,
|
|
1618
1597
|
sdkVersion: '1.0.0',
|
|
1619
1598
|
timestamp: Date.now(),
|
|
1620
1599
|
},
|
|
1600
|
+
// viewable 관련 추가 데이터 (DTO 필드명과 매칭)
|
|
1601
|
+
...(additionalData?.viewabilityMetrics && {
|
|
1602
|
+
isViewable: additionalData.viewabilityMetrics.isViewable,
|
|
1603
|
+
exposureTime: additionalData.viewabilityMetrics.exposureTime,
|
|
1604
|
+
maxVisibilityRatio: additionalData.viewabilityMetrics.maxVisibilityRatio,
|
|
1605
|
+
firstViewableTime: additionalData.viewabilityMetrics.firstViewableTime,
|
|
1606
|
+
// IAB 표준 준수 여부
|
|
1607
|
+
iabCompliant: additionalData.viewabilityMetrics.isViewable,
|
|
1608
|
+
}),
|
|
1609
|
+
// fraud 관련 데이터 (DTO 필드명과 매칭)
|
|
1610
|
+
...(additionalData?.fraudScore !== undefined && {
|
|
1611
|
+
fraudScore: additionalData.fraudScore,
|
|
1612
|
+
fraudReasons: additionalData.fraudReasons,
|
|
1613
|
+
riskLevel: additionalData.riskLevel,
|
|
1614
|
+
}),
|
|
1621
1615
|
};
|
|
1622
1616
|
await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
|
|
1623
1617
|
method: 'POST',
|
|
@@ -1625,11 +1619,11 @@ class EventTracker {
|
|
|
1625
1619
|
body: JSON.stringify(eventData),
|
|
1626
1620
|
});
|
|
1627
1621
|
if (this.debug) {
|
|
1628
|
-
console.log(`Tracked event: ${eventType} for ad ${adId}`, eventData);
|
|
1622
|
+
console.log(`Tracked advertisement event: ${eventType} for ad ${adId}`, eventData);
|
|
1629
1623
|
}
|
|
1630
1624
|
}
|
|
1631
1625
|
catch (error) {
|
|
1632
|
-
console.error('Failed to track event:', error);
|
|
1626
|
+
console.error('Failed to track advertisement event:', error);
|
|
1633
1627
|
}
|
|
1634
1628
|
}
|
|
1635
1629
|
/**
|
|
@@ -1648,6 +1642,298 @@ class EventTracker {
|
|
|
1648
1642
|
}
|
|
1649
1643
|
}
|
|
1650
1644
|
|
|
1645
|
+
/**
|
|
1646
|
+
* IAB 표준 준수 viewable impression 측정
|
|
1647
|
+
*/
|
|
1648
|
+
// 광고 타입별 IAB 표준 설정
|
|
1649
|
+
const VIEWABILITY_STANDARDS = {
|
|
1650
|
+
BANNER: {
|
|
1651
|
+
threshold: 0.5,
|
|
1652
|
+
minDuration: 1000,
|
|
1653
|
+
maxMeasureTime: 30000
|
|
1654
|
+
},
|
|
1655
|
+
VIDEO: {
|
|
1656
|
+
threshold: 0.5,
|
|
1657
|
+
minDuration: 2000,
|
|
1658
|
+
maxMeasureTime: 60000
|
|
1659
|
+
},
|
|
1660
|
+
NATIVE: {
|
|
1661
|
+
threshold: 0.5,
|
|
1662
|
+
minDuration: 1000,
|
|
1663
|
+
maxMeasureTime: 30000
|
|
1664
|
+
},
|
|
1665
|
+
INTERSTITIAL: {
|
|
1666
|
+
threshold: 0.5,
|
|
1667
|
+
minDuration: 1000,
|
|
1668
|
+
maxMeasureTime: 10000
|
|
1669
|
+
},
|
|
1670
|
+
TEXT: {
|
|
1671
|
+
threshold: 0.5,
|
|
1672
|
+
minDuration: 1000,
|
|
1673
|
+
maxMeasureTime: 30000
|
|
1674
|
+
},
|
|
1675
|
+
POPUP: {
|
|
1676
|
+
threshold: 0.5,
|
|
1677
|
+
minDuration: 1000,
|
|
1678
|
+
maxMeasureTime: 10000
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
class ViewabilityTracker {
|
|
1682
|
+
constructor(element, adType, onViewable) {
|
|
1683
|
+
this.observer = null;
|
|
1684
|
+
this.viewabilityTimer = null;
|
|
1685
|
+
this.maxVisibilityTimer = null;
|
|
1686
|
+
this.startTime = 0;
|
|
1687
|
+
this.maxVisibilityRatio = 0;
|
|
1688
|
+
this.firstViewableTime = null;
|
|
1689
|
+
this.isViewableAchieved = false;
|
|
1690
|
+
this.element = element;
|
|
1691
|
+
this.config = VIEWABILITY_STANDARDS[adType] || VIEWABILITY_STANDARDS.BANNER;
|
|
1692
|
+
this.onViewableCallback = onViewable;
|
|
1693
|
+
this.startTime = performance.now();
|
|
1694
|
+
this.initIntersectionObserver();
|
|
1695
|
+
this.initMaxMeasureTimer();
|
|
1696
|
+
}
|
|
1697
|
+
initIntersectionObserver() {
|
|
1698
|
+
// IntersectionObserver 지원 확인
|
|
1699
|
+
if (!('IntersectionObserver' in window)) {
|
|
1700
|
+
console.warn('IntersectionObserver not supported, viewability tracking disabled');
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
this.observer = new IntersectionObserver((entries) => this.handleIntersection(entries), {
|
|
1704
|
+
threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0],
|
|
1705
|
+
rootMargin: '0px'
|
|
1706
|
+
});
|
|
1707
|
+
this.observer.observe(this.element);
|
|
1708
|
+
}
|
|
1709
|
+
handleIntersection(entries) {
|
|
1710
|
+
entries.forEach(entry => {
|
|
1711
|
+
const visibilityRatio = entry.intersectionRatio;
|
|
1712
|
+
const isVisible = this.isDocumentVisible();
|
|
1713
|
+
// 최대 가시성 비율 추적
|
|
1714
|
+
this.maxVisibilityRatio = Math.max(this.maxVisibilityRatio, visibilityRatio);
|
|
1715
|
+
// Viewable 조건 확인 (50% 이상 + 문서 가시성)
|
|
1716
|
+
if (visibilityRatio >= this.config.threshold && isVisible) {
|
|
1717
|
+
this.startViewabilityTimer();
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
this.stopViewabilityTimer();
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
isDocumentVisible() {
|
|
1725
|
+
// 단순한 문서 가시성 확인
|
|
1726
|
+
return !document.hidden && document.visibilityState === 'visible';
|
|
1727
|
+
}
|
|
1728
|
+
startViewabilityTimer() {
|
|
1729
|
+
if (this.viewabilityTimer || this.isViewableAchieved)
|
|
1730
|
+
return;
|
|
1731
|
+
if (this.firstViewableTime === null) {
|
|
1732
|
+
this.firstViewableTime = performance.now();
|
|
1733
|
+
}
|
|
1734
|
+
this.viewabilityTimer = setTimeout(() => {
|
|
1735
|
+
this.onViewabilityAchieved();
|
|
1736
|
+
}, this.config.minDuration);
|
|
1737
|
+
}
|
|
1738
|
+
stopViewabilityTimer() {
|
|
1739
|
+
if (this.viewabilityTimer) {
|
|
1740
|
+
clearTimeout(this.viewabilityTimer);
|
|
1741
|
+
this.viewabilityTimer = null;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
initMaxMeasureTimer() {
|
|
1745
|
+
// 최대 측정 시간 후 자동 종료
|
|
1746
|
+
this.maxVisibilityTimer = setTimeout(() => {
|
|
1747
|
+
this.destroy();
|
|
1748
|
+
}, this.config.maxMeasureTime);
|
|
1749
|
+
}
|
|
1750
|
+
onViewabilityAchieved() {
|
|
1751
|
+
if (this.isViewableAchieved)
|
|
1752
|
+
return;
|
|
1753
|
+
this.isViewableAchieved = true;
|
|
1754
|
+
const metrics = this.calculateMetrics();
|
|
1755
|
+
if (this.onViewableCallback) {
|
|
1756
|
+
this.onViewableCallback(metrics);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
calculateMetrics() {
|
|
1760
|
+
const currentTime = performance.now();
|
|
1761
|
+
const exposureTime = this.firstViewableTime
|
|
1762
|
+
? currentTime - this.firstViewableTime
|
|
1763
|
+
: 0;
|
|
1764
|
+
return {
|
|
1765
|
+
isViewable: this.isViewableAchieved,
|
|
1766
|
+
exposureTime,
|
|
1767
|
+
maxVisibilityRatio: this.maxVisibilityRatio,
|
|
1768
|
+
firstViewableTime: this.firstViewableTime,
|
|
1769
|
+
measureStartTime: this.startTime,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
getMetrics() {
|
|
1773
|
+
return this.calculateMetrics();
|
|
1774
|
+
}
|
|
1775
|
+
destroy() {
|
|
1776
|
+
this.stopViewabilityTimer();
|
|
1777
|
+
if (this.maxVisibilityTimer) {
|
|
1778
|
+
clearTimeout(this.maxVisibilityTimer);
|
|
1779
|
+
this.maxVisibilityTimer = null;
|
|
1780
|
+
}
|
|
1781
|
+
if (this.observer) {
|
|
1782
|
+
this.observer.disconnect();
|
|
1783
|
+
this.observer = null;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* BasicFraudDetector - 현실적 구현 버전
|
|
1790
|
+
* 기본적인 봇 탐지 및 간단한 행동 패턴 분석
|
|
1791
|
+
*/
|
|
1792
|
+
class BasicFraudDetector {
|
|
1793
|
+
constructor() {
|
|
1794
|
+
this.mouseEvents = 0;
|
|
1795
|
+
this.keyboardEvents = 0;
|
|
1796
|
+
this.scrollEvents = 0;
|
|
1797
|
+
this.startTime = Date.now();
|
|
1798
|
+
this.initBasicTracking();
|
|
1799
|
+
}
|
|
1800
|
+
initBasicTracking() {
|
|
1801
|
+
// 기본적인 사용자 상호작용 추적
|
|
1802
|
+
document.addEventListener('mousemove', () => this.mouseEvents++, { passive: true });
|
|
1803
|
+
document.addEventListener('keydown', () => this.keyboardEvents++, { passive: true });
|
|
1804
|
+
document.addEventListener('scroll', () => this.scrollEvents++, { passive: true });
|
|
1805
|
+
}
|
|
1806
|
+
calculateFraudScore() {
|
|
1807
|
+
let score = 0;
|
|
1808
|
+
const reasons = [];
|
|
1809
|
+
// 1. 웹드라이버 탐지 (기본)
|
|
1810
|
+
if (this.detectWebDriver()) {
|
|
1811
|
+
score += 50;
|
|
1812
|
+
reasons.push('WebDriver detected');
|
|
1813
|
+
}
|
|
1814
|
+
// 2. 헤드리스 브라우저 기본 탐지
|
|
1815
|
+
if (this.detectBasicHeadless()) {
|
|
1816
|
+
score += 40;
|
|
1817
|
+
reasons.push('Headless browser signatures');
|
|
1818
|
+
}
|
|
1819
|
+
// 3. 사용자 상호작용 부족
|
|
1820
|
+
const sessionTime = Date.now() - this.startTime;
|
|
1821
|
+
if (sessionTime > 5000) { // 5초 이상 경과
|
|
1822
|
+
if (this.mouseEvents === 0) {
|
|
1823
|
+
score += 20;
|
|
1824
|
+
reasons.push('No mouse interaction');
|
|
1825
|
+
}
|
|
1826
|
+
if (this.scrollEvents === 0 && sessionTime > 10000) {
|
|
1827
|
+
score += 15;
|
|
1828
|
+
reasons.push('No scroll activity');
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// 4. 브라우저 환경 이상 징후
|
|
1832
|
+
const browserCheck = this.checkBrowserEnvironment();
|
|
1833
|
+
score += browserCheck.score;
|
|
1834
|
+
reasons.push(...browserCheck.reasons);
|
|
1835
|
+
// 5. 시간 패턴 이상 (너무 빠른 페이지 로드 후 즉시 클릭)
|
|
1836
|
+
if (sessionTime < 1000) {
|
|
1837
|
+
score += 25;
|
|
1838
|
+
reasons.push('Suspiciously fast interaction');
|
|
1839
|
+
}
|
|
1840
|
+
const finalScore = Math.min(score, 100);
|
|
1841
|
+
return {
|
|
1842
|
+
score: finalScore,
|
|
1843
|
+
riskLevel: this.getRiskLevel(finalScore),
|
|
1844
|
+
reasons: reasons
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
detectWebDriver() {
|
|
1848
|
+
// 기본적인 웹드라이버 탐지
|
|
1849
|
+
return !!(window.webdriver ||
|
|
1850
|
+
navigator.webdriver ||
|
|
1851
|
+
window.__webdriver_evaluate ||
|
|
1852
|
+
window.__selenium_evaluate ||
|
|
1853
|
+
window.__webdriver_script_function ||
|
|
1854
|
+
window.__webdriver_script_func ||
|
|
1855
|
+
window.__webdriver_script_fn ||
|
|
1856
|
+
window.__fxdriver_evaluate ||
|
|
1857
|
+
window.__driver_unwrapped ||
|
|
1858
|
+
window.__webdriver_unwrapped ||
|
|
1859
|
+
window.__driver_evaluate ||
|
|
1860
|
+
window.__selenium_unwrapped ||
|
|
1861
|
+
window.__fxdriver_unwrapped);
|
|
1862
|
+
}
|
|
1863
|
+
detectBasicHeadless() {
|
|
1864
|
+
const signatures = [];
|
|
1865
|
+
// PhantomJS 탐지
|
|
1866
|
+
if (window._phantom || window.phantom) {
|
|
1867
|
+
signatures.push('PhantomJS');
|
|
1868
|
+
}
|
|
1869
|
+
// Chrome headless 기본 탐지
|
|
1870
|
+
if (navigator.userAgent.includes('HeadlessChrome')) {
|
|
1871
|
+
signatures.push('Chrome Headless');
|
|
1872
|
+
}
|
|
1873
|
+
// 플러그인 없음 (일반적이지 않음)
|
|
1874
|
+
if (navigator.plugins.length === 0) {
|
|
1875
|
+
signatures.push('No plugins');
|
|
1876
|
+
}
|
|
1877
|
+
return signatures.length > 0;
|
|
1878
|
+
}
|
|
1879
|
+
checkBrowserEnvironment() {
|
|
1880
|
+
let score = 0;
|
|
1881
|
+
const reasons = [];
|
|
1882
|
+
// 언어 설정 이상
|
|
1883
|
+
if (!navigator.language || navigator.language === 'C') {
|
|
1884
|
+
score += 10;
|
|
1885
|
+
reasons.push('Unusual language setting');
|
|
1886
|
+
}
|
|
1887
|
+
// 쿠키 비활성화
|
|
1888
|
+
if (!navigator.cookieEnabled) {
|
|
1889
|
+
score += 15;
|
|
1890
|
+
reasons.push('Cookies disabled');
|
|
1891
|
+
}
|
|
1892
|
+
// 화면 해상도 이상
|
|
1893
|
+
if (screen.width === 0 || screen.height === 0) {
|
|
1894
|
+
score += 20;
|
|
1895
|
+
reasons.push('Invalid screen resolution');
|
|
1896
|
+
}
|
|
1897
|
+
// 시간대 정보 없음
|
|
1898
|
+
try {
|
|
1899
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1900
|
+
if (!timezone || timezone === 'UTC') {
|
|
1901
|
+
score += 5;
|
|
1902
|
+
reasons.push('No timezone info');
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
catch (e) {
|
|
1906
|
+
score += 10;
|
|
1907
|
+
reasons.push('Timezone detection failed');
|
|
1908
|
+
}
|
|
1909
|
+
return { score, reasons };
|
|
1910
|
+
}
|
|
1911
|
+
getRiskLevel(score) {
|
|
1912
|
+
if (score >= 70)
|
|
1913
|
+
return 'CRITICAL';
|
|
1914
|
+
if (score >= 50)
|
|
1915
|
+
return 'HIGH';
|
|
1916
|
+
if (score >= 30)
|
|
1917
|
+
return 'MEDIUM';
|
|
1918
|
+
return 'LOW';
|
|
1919
|
+
}
|
|
1920
|
+
getBrowserInfo() {
|
|
1921
|
+
return {
|
|
1922
|
+
userAgent: navigator.userAgent,
|
|
1923
|
+
language: navigator.language,
|
|
1924
|
+
platform: navigator.platform,
|
|
1925
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
1926
|
+
doNotTrack: navigator.doNotTrack,
|
|
1927
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1928
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
1929
|
+
viewportSize: `${window.innerWidth}x${window.innerHeight}`
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
destroy() {
|
|
1933
|
+
// 이벤트 리스너 정리는 생략 (메모리 누수 방지를 위해 필요시 구현)
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1651
1937
|
/**
|
|
1652
1938
|
* AdStage SDK 엔드포인트 상수 관리
|
|
1653
1939
|
* 모든 API URL을 중앙에서 관리
|
|
@@ -1732,15 +2018,16 @@ class AdsModule {
|
|
|
1732
2018
|
this._isReady = false;
|
|
1733
2019
|
this._config = null;
|
|
1734
2020
|
this.slots = new Map();
|
|
1735
|
-
|
|
2021
|
+
// Advertisement 이벤트 추적 관련
|
|
2022
|
+
this.advertisementEventTracker = null;
|
|
1736
2023
|
}
|
|
1737
2024
|
/**
|
|
1738
2025
|
* Ads 모듈 초기화 (동기)
|
|
1739
2026
|
*/
|
|
1740
2027
|
init(config) {
|
|
1741
2028
|
this._config = config;
|
|
1742
|
-
//
|
|
1743
|
-
this.
|
|
2029
|
+
// AdvertisementEventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
|
|
2030
|
+
this.advertisementEventTracker = new AdvertisementEventTracker(endpoints.getBaseUrl(), config.apiKey, config.debug || false, this.slots);
|
|
1744
2031
|
this._isReady = true;
|
|
1745
2032
|
if (config.debug) {
|
|
1746
2033
|
console.log('🎯 Ads module initialized (sync mode)');
|
|
@@ -1768,7 +2055,11 @@ class AdsModule {
|
|
|
1768
2055
|
height: options?.height || 250,
|
|
1769
2056
|
autoSlide: options?.autoSlide || false,
|
|
1770
2057
|
slideInterval: options?.slideInterval || 5000,
|
|
1771
|
-
onClick: options?.onClick
|
|
2058
|
+
onClick: options?.onClick,
|
|
2059
|
+
// 필터링 옵션들 전달
|
|
2060
|
+
language: options?.language,
|
|
2061
|
+
deviceType: options?.deviceType,
|
|
2062
|
+
country: options?.country
|
|
1772
2063
|
};
|
|
1773
2064
|
return this.createAd(containerId, AdType.BANNER, adstageOptions);
|
|
1774
2065
|
}
|
|
@@ -1780,7 +2071,11 @@ class AdsModule {
|
|
|
1780
2071
|
const adstageOptions = {
|
|
1781
2072
|
maxLines: options?.maxLines || 3,
|
|
1782
2073
|
style: options?.style || 'default',
|
|
1783
|
-
onClick: options?.onClick
|
|
2074
|
+
onClick: options?.onClick,
|
|
2075
|
+
// 필터링 옵션들 전달
|
|
2076
|
+
language: options?.language,
|
|
2077
|
+
deviceType: options?.deviceType,
|
|
2078
|
+
country: options?.country
|
|
1784
2079
|
};
|
|
1785
2080
|
return this.createAd(containerId, AdType.TEXT, adstageOptions);
|
|
1786
2081
|
}
|
|
@@ -1836,6 +2131,14 @@ class AdsModule {
|
|
|
1836
2131
|
if (!slot) {
|
|
1837
2132
|
throw new Error(`Ad slot not found: ${slotId}`);
|
|
1838
2133
|
}
|
|
2134
|
+
// ViewabilityTracker 정리
|
|
2135
|
+
if (slot.viewabilityTracker) {
|
|
2136
|
+
slot.viewabilityTracker.destroy();
|
|
2137
|
+
}
|
|
2138
|
+
// BasicFraudDetector 정리
|
|
2139
|
+
if (slot.fraudDetector) {
|
|
2140
|
+
slot.fraudDetector.destroy();
|
|
2141
|
+
}
|
|
1839
2142
|
// DOM에서 제거
|
|
1840
2143
|
const container = document.getElementById(slot.containerId);
|
|
1841
2144
|
if (container) {
|
|
@@ -1900,8 +2203,8 @@ class AdsModule {
|
|
|
1900
2203
|
// 백그라운드에서 광고 로드
|
|
1901
2204
|
this.loadAdContentInBackground(slot);
|
|
1902
2205
|
// 이벤트 추적 준비
|
|
1903
|
-
if (this.
|
|
1904
|
-
console.log(`📊
|
|
2206
|
+
if (this.advertisementEventTracker && this._config?.debug) {
|
|
2207
|
+
console.log(`📊 Advertisement event tracking enabled for slot: ${slotId}`);
|
|
1905
2208
|
}
|
|
1906
2209
|
return slotId;
|
|
1907
2210
|
}
|
|
@@ -1912,6 +2215,10 @@ class AdsModule {
|
|
|
1912
2215
|
const adElement = document.createElement('div');
|
|
1913
2216
|
adElement.id = slotId;
|
|
1914
2217
|
adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
|
|
2218
|
+
// 확실한 컨테이너 식별을 위한 데이터 속성 추가
|
|
2219
|
+
adElement.setAttribute('data-adstage-container', 'true');
|
|
2220
|
+
adElement.setAttribute('data-adstage-type', type);
|
|
2221
|
+
adElement.setAttribute('data-adstage-slot', slotId);
|
|
1915
2222
|
adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
|
|
1916
2223
|
adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
|
|
1917
2224
|
adElement.style.border = '1px dashed #ccc';
|
|
@@ -1945,6 +2252,8 @@ class AdsModule {
|
|
|
1945
2252
|
// 광고가 1개면 일반 렌더링
|
|
1946
2253
|
slot.advertisement = adstageData[0];
|
|
1947
2254
|
await this.renderAdElement(slot, adstageData[0]);
|
|
2255
|
+
// ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
|
|
2256
|
+
this.startBasicViewabilityTracking(slot, adstageData[0]);
|
|
1948
2257
|
}
|
|
1949
2258
|
slot.isLoaded = true;
|
|
1950
2259
|
if (this._config?.debug) {
|
|
@@ -1957,15 +2266,158 @@ class AdsModule {
|
|
|
1957
2266
|
}
|
|
1958
2267
|
}
|
|
1959
2268
|
/**
|
|
1960
|
-
*
|
|
2269
|
+
* 기본 viewability 추적 시작
|
|
2270
|
+
*/
|
|
2271
|
+
startBasicViewabilityTracking(slot, ad) {
|
|
2272
|
+
const element = document.getElementById(slot.id);
|
|
2273
|
+
if (!element)
|
|
2274
|
+
return;
|
|
2275
|
+
// 기본 fraud 검사
|
|
2276
|
+
const fraudDetector = new BasicFraudDetector();
|
|
2277
|
+
// viewability 추적
|
|
2278
|
+
const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
|
|
2279
|
+
this.handleViewableEvent(ad, slot, metrics, fraudDetector);
|
|
2280
|
+
});
|
|
2281
|
+
// 정리를 위해 저장
|
|
2282
|
+
slot.viewabilityTracker = tracker;
|
|
2283
|
+
slot.fraudDetector = fraudDetector;
|
|
2284
|
+
if (this._config?.debug) {
|
|
2285
|
+
console.log(`🎯 Viewability tracking started for slot: ${slot.id}`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Viewable 이벤트 처리
|
|
2290
|
+
*/
|
|
2291
|
+
async handleViewableEvent(ad, slot, metrics, fraudDetector) {
|
|
2292
|
+
try {
|
|
2293
|
+
const fraudScore = fraudDetector.calculateFraudScore();
|
|
2294
|
+
// 높은 위험도면 차단
|
|
2295
|
+
if (fraudScore.riskLevel === 'CRITICAL') {
|
|
2296
|
+
if (this._config?.debug) {
|
|
2297
|
+
console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
|
|
2298
|
+
}
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
// VIEWABLE 이벤트 전송
|
|
2302
|
+
if (this.advertisementEventTracker) {
|
|
2303
|
+
await this.advertisementEventTracker.trackAdvertisementEvent(ad._id, slot.id, AdEventType.VIEWABLE, {
|
|
2304
|
+
viewabilityMetrics: metrics,
|
|
2305
|
+
fraudScore: fraudScore.score,
|
|
2306
|
+
fraudReasons: fraudScore.reasons,
|
|
2307
|
+
riskLevel: fraudScore.riskLevel
|
|
2308
|
+
});
|
|
2309
|
+
if (this._config?.debug) {
|
|
2310
|
+
console.log(`✅ Viewable impression tracked for ad ${ad._id} (fraud score: ${fraudScore.score})`);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
catch (error) {
|
|
2315
|
+
console.error(`❌ Failed to track viewable impression:`, error);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
|
|
1961
2320
|
*/
|
|
1962
2321
|
renderFallback(slot) {
|
|
1963
2322
|
const element = document.getElementById(slot.id);
|
|
1964
2323
|
if (element) {
|
|
1965
|
-
|
|
1966
|
-
|
|
2324
|
+
// 1순위: AdStage가 생성한 확실한 컨테이너들 (데이터 속성 기반)
|
|
2325
|
+
const adstageContainers = [
|
|
2326
|
+
element.querySelector('[data-adstage-container="true"]'), // 내부 AdStage 컨테이너
|
|
2327
|
+
element.closest('[data-adstage-container="true"]'), // 상위 AdStage 컨테이너
|
|
2328
|
+
element, // 자기 자신이 AdStage 컨테이너인 경우
|
|
2329
|
+
].filter(el => el && el.hasAttribute('data-adstage-container'));
|
|
2330
|
+
// 2순위: AdStage 클래스 기반 컨테이너들
|
|
2331
|
+
const classBasedContainers = [
|
|
2332
|
+
element.closest('.adstage-slot'),
|
|
2333
|
+
element.closest('.adstage-banner'),
|
|
2334
|
+
element.closest('.adstage-text'),
|
|
2335
|
+
element.closest('.adstage-video'),
|
|
2336
|
+
element.closest('.adstage-native'),
|
|
2337
|
+
element.closest('.adstage-interstitial'),
|
|
2338
|
+
].filter(Boolean);
|
|
2339
|
+
// 3순위: 일반적인 광고 컨테이너 패턴들 (fallback)
|
|
2340
|
+
const generalContainers = [
|
|
2341
|
+
element.closest('[class*="ad"]'),
|
|
2342
|
+
element.closest('[class*="banner"]'),
|
|
2343
|
+
element.closest('[class*="container"]'),
|
|
2344
|
+
element.closest('div[style*="height"]'),
|
|
2345
|
+
element.closest('div[style*="min-height"]'),
|
|
2346
|
+
element.parentElement
|
|
2347
|
+
].filter(Boolean);
|
|
2348
|
+
// 우선순위에 따라 컨테이너 선택
|
|
2349
|
+
const possibleContainers = [
|
|
2350
|
+
...adstageContainers,
|
|
2351
|
+
...classBasedContainers,
|
|
2352
|
+
...generalContainers
|
|
2353
|
+
];
|
|
2354
|
+
// 가장 적절한 컨테이너 선택
|
|
2355
|
+
const targetContainer = possibleContainers[0];
|
|
2356
|
+
if (targetContainer) {
|
|
2357
|
+
// 컨테이너 타입 로깅
|
|
2358
|
+
let containerType = 'unknown';
|
|
2359
|
+
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
2360
|
+
containerType = 'adstage-official';
|
|
2361
|
+
}
|
|
2362
|
+
else if (targetContainer.classList.contains('adstage-slot')) {
|
|
2363
|
+
containerType = 'adstage-class';
|
|
2364
|
+
}
|
|
2365
|
+
else {
|
|
2366
|
+
containerType = 'generic';
|
|
2367
|
+
}
|
|
2368
|
+
targetContainer.style.cssText += `
|
|
2369
|
+
height: 0px !important;
|
|
2370
|
+
min-height: 0px !important;
|
|
2371
|
+
padding: 0px !important;
|
|
2372
|
+
margin: 0px !important;
|
|
2373
|
+
border: none !important;
|
|
2374
|
+
overflow: hidden !important;
|
|
2375
|
+
display: block !important;
|
|
2376
|
+
`;
|
|
2377
|
+
// 내부 모든 요소 제거
|
|
2378
|
+
targetContainer.innerHTML = '';
|
|
2379
|
+
// 빈 상태임을 표시하는 속성 추가
|
|
2380
|
+
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
2381
|
+
if (this._config?.debug) {
|
|
2382
|
+
console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
else {
|
|
2386
|
+
// 컨테이너를 찾지 못한 경우 새로운 빈 컨테이너 생성
|
|
2387
|
+
this.createEmptyContainer(slot);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
// 슬롯 상태 업데이트 (제거하지 않고 빈 상태로 마킹)
|
|
2391
|
+
slot.advertisement = undefined;
|
|
2392
|
+
slot.isEmpty = true;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
2396
|
+
*/
|
|
2397
|
+
createEmptyContainer(slot) {
|
|
2398
|
+
const originalContainer = document.getElementById(slot.containerId);
|
|
2399
|
+
if (originalContainer) {
|
|
2400
|
+
// 기존 내용 제거
|
|
2401
|
+
originalContainer.innerHTML = '';
|
|
2402
|
+
// 빈 AdStage 컨테이너 생성
|
|
2403
|
+
const emptyElement = document.createElement('div');
|
|
2404
|
+
emptyElement.id = slot.id;
|
|
2405
|
+
emptyElement.className = 'adstage-slot adstage-empty';
|
|
2406
|
+
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
2407
|
+
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
2408
|
+
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
2409
|
+
emptyElement.style.cssText = `
|
|
2410
|
+
height: 0px !important;
|
|
2411
|
+
min-height: 0px !important;
|
|
2412
|
+
padding: 0px !important;
|
|
2413
|
+
margin: 0px !important;
|
|
2414
|
+
border: none !important;
|
|
2415
|
+
overflow: hidden !important;
|
|
2416
|
+
display: block !important;
|
|
2417
|
+
`;
|
|
2418
|
+
originalContainer.appendChild(emptyElement);
|
|
1967
2419
|
if (this._config?.debug) {
|
|
1968
|
-
console.warn(`⚠️
|
|
2420
|
+
console.warn(`⚠️ Created empty AdStage container: ${slot.id}`);
|
|
1969
2421
|
}
|
|
1970
2422
|
}
|
|
1971
2423
|
}
|
|
@@ -1979,8 +2431,16 @@ class AdsModule {
|
|
|
1979
2431
|
// GET 요청용 query parameters 구성
|
|
1980
2432
|
const params = new URLSearchParams();
|
|
1981
2433
|
params.append('adType', type);
|
|
1982
|
-
//
|
|
1983
|
-
|
|
2434
|
+
// 백엔드 API에서 실제 지원하는 필터링 옵션들만 추가
|
|
2435
|
+
if (options.language) {
|
|
2436
|
+
params.append('language', options.language);
|
|
2437
|
+
}
|
|
2438
|
+
if (options.deviceType) {
|
|
2439
|
+
params.append('deviceType', options.deviceType);
|
|
2440
|
+
}
|
|
2441
|
+
if (options.country) {
|
|
2442
|
+
params.append('country', options.country);
|
|
2443
|
+
}
|
|
1984
2444
|
const url = `${endpoints.advertisements.list()}?${params.toString()}`;
|
|
1985
2445
|
const response = await fetch(url, {
|
|
1986
2446
|
method: 'GET',
|
|
@@ -1990,7 +2450,15 @@ class AdsModule {
|
|
|
1990
2450
|
throw new Error(`Failed to fetch ad data: ${response.status}`);
|
|
1991
2451
|
}
|
|
1992
2452
|
const result = await response.json();
|
|
1993
|
-
|
|
2453
|
+
const advertisements = result.advertisements || [];
|
|
2454
|
+
if (this._config?.debug) {
|
|
2455
|
+
console.log(`📊 Fetched ${advertisements.length} ads for type: ${type}, filters:`, {
|
|
2456
|
+
language: options.language,
|
|
2457
|
+
deviceType: options.deviceType,
|
|
2458
|
+
country: options.country
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
return advertisements;
|
|
1994
2462
|
}
|
|
1995
2463
|
/**
|
|
1996
2464
|
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
@@ -2003,19 +2471,19 @@ class AdsModule {
|
|
|
2003
2471
|
// 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
|
|
2004
2472
|
const trackEventCallback = (adId, slotId, eventType) => {
|
|
2005
2473
|
// 노출 이벤트인 경우 중복 확인
|
|
2006
|
-
if (eventType === AdEventType.
|
|
2007
|
-
if (
|
|
2474
|
+
if (eventType === AdEventType.VIEWABLE) {
|
|
2475
|
+
if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this._config?.debug)) {
|
|
2008
2476
|
if (this._config?.debug) {
|
|
2009
|
-
console.log(`🚫 Duplicate
|
|
2477
|
+
console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
2010
2478
|
}
|
|
2011
2479
|
return; // 중복 노출이면 추적하지 않음
|
|
2012
2480
|
}
|
|
2013
2481
|
if (this._config?.debug) {
|
|
2014
|
-
console.log(`✅ New
|
|
2482
|
+
console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
|
|
2015
2483
|
}
|
|
2016
2484
|
}
|
|
2017
|
-
if (this.
|
|
2018
|
-
console.log(`📊
|
|
2485
|
+
if (this.advertisementEventTracker && this._config?.debug) {
|
|
2486
|
+
console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
2019
2487
|
}
|
|
2020
2488
|
};
|
|
2021
2489
|
let sliderElement;
|
|
@@ -2120,8 +2588,8 @@ class AdsModule {
|
|
|
2120
2588
|
slot.advertisement = newAdData[0]; // 첫 번째 광고로 업데이트
|
|
2121
2589
|
await this.renderAd(slot);
|
|
2122
2590
|
// 새로운 노출 추적
|
|
2123
|
-
if (this.
|
|
2124
|
-
console.log('New
|
|
2591
|
+
if (this.advertisementEventTracker) {
|
|
2592
|
+
console.log('New advertisement viewable tracked for slot:', slot.id);
|
|
2125
2593
|
}
|
|
2126
2594
|
}
|
|
2127
2595
|
}
|