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