@adstage/web-sdk 2.4.12 → 2.5.1

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.
@@ -28,6 +28,47 @@ var DeviceType;
28
28
  DeviceType["TABLET"] = "TABLET";
29
29
  })(DeviceType || (DeviceType = {}));
30
30
 
31
+ /**
32
+ * 단순한 VIEWABLE 이벤트 중복 방지 관리 클래스
33
+ * - 세션당 동일 광고 1회만 VIEWABLE 이벤트 허용
34
+ * - 메모리 기반 추적으로 단순화
35
+ */
36
+ class ViewableEventTracker {
37
+ /**
38
+ * 중복 viewable 이벤트 여부 확인
39
+ */
40
+ static isDuplicateViewable(adId, slotId, debug = false) {
41
+ const key = `${adId}_${slotId}`;
42
+ // 이미 VIEWABLE 이벤트가 발생한 광고인지 확인
43
+ if (ViewableEventTracker.viewableTracker.has(key)) {
44
+ if (debug) {
45
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
46
+ }
47
+ return true;
48
+ }
49
+ // 새로운 VIEWABLE 이벤트 기록
50
+ ViewableEventTracker.viewableTracker.add(key);
51
+ if (debug) {
52
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * 모든 추적 데이터 정리 (디버그용)
58
+ */
59
+ static clear() {
60
+ ViewableEventTracker.viewableTracker.clear();
61
+ }
62
+ /**
63
+ * 특정 광고의 viewable 추적 초기화 (디버그용)
64
+ */
65
+ static clearAdViewable(adId, slotId) {
66
+ const key = `${adId}_${slotId}`;
67
+ ViewableEventTracker.viewableTracker.delete(key);
68
+ }
69
+ }
70
+ ViewableEventTracker.viewableTracker = new Set();
71
+
31
72
  /**
32
73
  * SSR 안전한 DOM API 래퍼 클래스
33
74
  * 서버사이드 렌더링 환경에서 DOM API 접근 시 오류를 방지합니다.
@@ -449,6 +490,18 @@ class ApiHeaders {
449
490
  }
450
491
  }
451
492
 
493
+ /**
494
+ * AdStage SDK - 버전 정보 유틸리티
495
+ */
496
+ // package.json에서 버전 정보 가져오기 (빌드 시 자동으로 교체됨)
497
+ const SDK_VERSION$1 = '"2.5.1"';
498
+ /**
499
+ * SDK 버전 정보 반환
500
+ */
501
+ function getSDKVersion() {
502
+ return SDK_VERSION$1;
503
+ }
504
+
452
505
  /**
453
506
  * 광고 이벤트 추적 관리 클래스
454
507
  * - 광고 전용 이벤트 추적 및 전송
@@ -463,24 +516,32 @@ class AdvertisementEventTracker {
463
516
  this.slots = slots;
464
517
  }
465
518
  /**
466
- * 광고 이벤트 추적 - viewability 데이터 지원
519
+ * 광고 이벤트 추적 - 단순화된 viewable 처리
467
520
  */
468
- async trackAdvertisementEvent(adId, slotId, eventType, additionalData) {
521
+ async trackAdvertisementEvent(adId, slotId, eventType) {
469
522
  try {
470
523
  if (this.debug) {
471
524
  console.log(`🚀 AdvertisementEventTracker: Processing ${eventType} event for ad ${adId} in slot ${slotId}`);
472
525
  }
526
+ // VIEWABLE 이벤트 중복 확인
527
+ if (eventType === AdEventType.VIEWABLE) {
528
+ if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
529
+ if (this.debug) {
530
+ console.log(`⏭️ Skipping duplicate viewable event for ad ${adId} in slot ${slotId}`);
531
+ }
532
+ return;
533
+ }
534
+ }
473
535
  // 현재 슬롯 정보 가져오기
474
536
  const slot = this.slots.get(slotId);
475
537
  // 디바이스 정보 수집
476
538
  const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
477
- // 광고 이벤트 데이터 구성 (DTO 구조에 맞춤)
478
- // 서버에서 자동 설정: orgId (API 키로부터), advertisementId, action (URL 파라미터로부터)
539
+ // 광고 이벤트 데이터 구성 (단순화됨)
479
540
  const eventData = {
480
541
  // 필수 필드들 (DTO 검증용)
481
542
  adType: slot?.adType || 'BANNER',
482
543
  platform: deviceInfo.platform,
483
- deviceId: deviceInfo.deviceId, // DTO 검증을 위해 deviceId 직접 전송
544
+ deviceId: deviceInfo.deviceId,
484
545
  // 디바이스 정보는 deviceInfo 객체로 래핑
485
546
  deviceInfo: deviceInfo,
486
547
  // 페이지 및 슬롯 정보
@@ -497,23 +558,13 @@ class AdvertisementEventTracker {
497
558
  // 추가 메타데이터
498
559
  metadata: {
499
560
  eventType,
500
- sdkVersion: '1.0.0',
561
+ sdkVersion: getSDKVersion(),
501
562
  timestamp: Date.now(),
502
563
  },
503
- // viewable 관련 추가 데이터 (DTO 필드명과 매칭)
504
- ...(additionalData?.viewabilityMetrics && {
505
- isViewable: additionalData.viewabilityMetrics.isViewable,
506
- exposureTime: additionalData.viewabilityMetrics.exposureTime,
507
- maxVisibilityRatio: additionalData.viewabilityMetrics.maxVisibilityRatio,
508
- firstViewableTime: additionalData.viewabilityMetrics.firstViewableTime,
509
- // IAB 표준 준수 여부
510
- iabCompliant: additionalData.viewabilityMetrics.isViewable,
511
- }),
512
- // fraud 관련 데이터 (DTO 필드명과 매칭)
513
- ...(additionalData?.fraudScore !== undefined && {
514
- fraudScore: additionalData.fraudScore,
515
- fraudReasons: additionalData.fraudReasons,
516
- riskLevel: additionalData.riskLevel,
564
+ // VIEWABLE 이벤트의 경우 단순한 플래그만 설정
565
+ ...(eventType === AdEventType.VIEWABLE && {
566
+ isViewable: true,
567
+ iabCompliant: true, // 50% 노출 기준으로 단순 판정
517
568
  }),
518
569
  };
519
570
  const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
@@ -565,56 +616,15 @@ class AdvertisementEventTracker {
565
616
  }
566
617
 
567
618
  /**
568
- * IAB 표준 준수 viewable impression 측정
619
+ * 단순한 광고 노출 추적 (50% 노출시 즉시 VIEWABLE 이벤트)
569
620
  */
570
- // 광고 타입별 IAB 표준 설정
571
- const VIEWABILITY_STANDARDS = {
572
- BANNER: {
573
- threshold: 0.5,
574
- minDuration: 1000,
575
- maxMeasureTime: 30000
576
- },
577
- VIDEO: {
578
- threshold: 0.5,
579
- minDuration: 2000,
580
- maxMeasureTime: 60000
581
- },
582
- NATIVE: {
583
- threshold: 0.5,
584
- minDuration: 1000,
585
- maxMeasureTime: 30000
586
- },
587
- INTERSTITIAL: {
588
- threshold: 0.5,
589
- minDuration: 1000,
590
- maxMeasureTime: 10000
591
- },
592
- TEXT: {
593
- threshold: 0.5,
594
- minDuration: 1000,
595
- maxMeasureTime: 30000
596
- },
597
- POPUP: {
598
- threshold: 0.5,
599
- minDuration: 1000,
600
- maxMeasureTime: 10000
601
- }
602
- };
603
- class ViewabilityTracker {
604
- constructor(element, adType, onViewable) {
621
+ class SimpleViewabilityTracker {
622
+ constructor(element, onViewable) {
605
623
  this.observer = null;
606
- this.viewabilityTimer = null;
607
- this.maxVisibilityTimer = null;
608
- this.startTime = 0;
609
- this.maxVisibilityRatio = 0;
610
- this.firstViewableTime = null;
611
- this.isViewableAchieved = false;
624
+ this.isViewableTriggered = false;
612
625
  this.element = element;
613
- this.config = VIEWABILITY_STANDARDS[adType] || VIEWABILITY_STANDARDS.BANNER;
614
626
  this.onViewableCallback = onViewable;
615
- this.startTime = performance.now();
616
627
  this.initIntersectionObserver();
617
- this.initMaxMeasureTimer();
618
628
  }
619
629
  initIntersectionObserver() {
620
630
  // IntersectionObserver 지원 확인
@@ -623,83 +633,28 @@ class ViewabilityTracker {
623
633
  return;
624
634
  }
625
635
  this.observer = new IntersectionObserver((entries) => this.handleIntersection(entries), {
626
- threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0],
636
+ threshold: 0.5, // 50% 노출
627
637
  rootMargin: '0px'
628
638
  });
629
639
  this.observer.observe(this.element);
630
640
  }
631
641
  handleIntersection(entries) {
632
642
  entries.forEach(entry => {
633
- const visibilityRatio = entry.intersectionRatio;
634
- const isVisible = this.isDocumentVisible();
635
- // 최대 가시성 비율 추적
636
- this.maxVisibilityRatio = Math.max(this.maxVisibilityRatio, visibilityRatio);
637
- // Viewable 조건 확인 (50% 이상 + 문서 가시성)
638
- if (visibilityRatio >= this.config.threshold && isVisible) {
639
- this.startViewabilityTimer();
640
- }
641
- else {
642
- this.stopViewabilityTimer();
643
+ // 50% 이상 노출되고 문서가 가시상태이며 아직 트리거되지 않은 경우
644
+ if (entry.intersectionRatio >= 0.5 &&
645
+ this.isDocumentVisible() &&
646
+ !this.isViewableTriggered) {
647
+ this.isViewableTriggered = true;
648
+ if (this.onViewableCallback) {
649
+ this.onViewableCallback();
650
+ }
643
651
  }
644
652
  });
645
653
  }
646
654
  isDocumentVisible() {
647
- // 단순한 문서 가시성 확인
648
655
  return !document.hidden && document.visibilityState === 'visible';
649
656
  }
650
- startViewabilityTimer() {
651
- if (this.viewabilityTimer || this.isViewableAchieved)
652
- return;
653
- if (this.firstViewableTime === null) {
654
- this.firstViewableTime = performance.now();
655
- }
656
- this.viewabilityTimer = setTimeout(() => {
657
- this.onViewabilityAchieved();
658
- }, this.config.minDuration);
659
- }
660
- stopViewabilityTimer() {
661
- if (this.viewabilityTimer) {
662
- clearTimeout(this.viewabilityTimer);
663
- this.viewabilityTimer = null;
664
- }
665
- }
666
- initMaxMeasureTimer() {
667
- // 최대 측정 시간 후 자동 종료
668
- this.maxVisibilityTimer = setTimeout(() => {
669
- this.destroy();
670
- }, this.config.maxMeasureTime);
671
- }
672
- onViewabilityAchieved() {
673
- if (this.isViewableAchieved)
674
- return;
675
- this.isViewableAchieved = true;
676
- const metrics = this.calculateMetrics();
677
- if (this.onViewableCallback) {
678
- this.onViewableCallback(metrics);
679
- }
680
- }
681
- calculateMetrics() {
682
- const currentTime = performance.now();
683
- const exposureTime = this.firstViewableTime
684
- ? currentTime - this.firstViewableTime
685
- : 0;
686
- return {
687
- isViewable: this.isViewableAchieved,
688
- exposureTime,
689
- maxVisibilityRatio: this.maxVisibilityRatio,
690
- firstViewableTime: this.firstViewableTime,
691
- measureStartTime: this.startTime,
692
- };
693
- }
694
- getMetrics() {
695
- return this.calculateMetrics();
696
- }
697
657
  destroy() {
698
- this.stopViewabilityTimer();
699
- if (this.maxVisibilityTimer) {
700
- clearTimeout(this.maxVisibilityTimer);
701
- this.maxVisibilityTimer = null;
702
- }
703
658
  if (this.observer) {
704
659
  this.observer.disconnect();
705
660
  this.observer = null;
@@ -707,155 +662,6 @@ class ViewabilityTracker {
707
662
  }
708
663
  }
709
664
 
710
- /**
711
- * BasicFraudDetector - 현실적 구현 버전
712
- * 기본적인 봇 탐지 및 간단한 행동 패턴 분석
713
- */
714
- class BasicFraudDetector {
715
- constructor() {
716
- this.mouseEvents = 0;
717
- this.keyboardEvents = 0;
718
- this.scrollEvents = 0;
719
- this.startTime = Date.now();
720
- this.initBasicTracking();
721
- }
722
- initBasicTracking() {
723
- // 기본적인 사용자 상호작용 추적
724
- document.addEventListener('mousemove', () => this.mouseEvents++, { passive: true });
725
- document.addEventListener('keydown', () => this.keyboardEvents++, { passive: true });
726
- document.addEventListener('scroll', () => this.scrollEvents++, { passive: true });
727
- }
728
- calculateFraudScore() {
729
- let score = 0;
730
- const reasons = [];
731
- // 1. 웹드라이버 탐지 (기본)
732
- if (this.detectWebDriver()) {
733
- score += 50;
734
- reasons.push('WebDriver detected');
735
- }
736
- // 2. 헤드리스 브라우저 기본 탐지
737
- if (this.detectBasicHeadless()) {
738
- score += 40;
739
- reasons.push('Headless browser signatures');
740
- }
741
- // 3. 사용자 상호작용 부족
742
- const sessionTime = Date.now() - this.startTime;
743
- if (sessionTime > 5000) { // 5초 이상 경과
744
- if (this.mouseEvents === 0) {
745
- score += 20;
746
- reasons.push('No mouse interaction');
747
- }
748
- if (this.scrollEvents === 0 && sessionTime > 10000) {
749
- score += 15;
750
- reasons.push('No scroll activity');
751
- }
752
- }
753
- // 4. 브라우저 환경 이상 징후
754
- const browserCheck = this.checkBrowserEnvironment();
755
- score += browserCheck.score;
756
- reasons.push(...browserCheck.reasons);
757
- // 5. 시간 패턴 이상 (너무 빠른 페이지 로드 후 즉시 클릭)
758
- if (sessionTime < 1000) {
759
- score += 25;
760
- reasons.push('Suspiciously fast interaction');
761
- }
762
- const finalScore = Math.min(score, 100);
763
- return {
764
- score: finalScore,
765
- riskLevel: this.getRiskLevel(finalScore),
766
- reasons: reasons
767
- };
768
- }
769
- detectWebDriver() {
770
- // 기본적인 웹드라이버 탐지
771
- return !!(window.webdriver ||
772
- navigator.webdriver ||
773
- window.__webdriver_evaluate ||
774
- window.__selenium_evaluate ||
775
- window.__webdriver_script_function ||
776
- window.__webdriver_script_func ||
777
- window.__webdriver_script_fn ||
778
- window.__fxdriver_evaluate ||
779
- window.__driver_unwrapped ||
780
- window.__webdriver_unwrapped ||
781
- window.__driver_evaluate ||
782
- window.__selenium_unwrapped ||
783
- window.__fxdriver_unwrapped);
784
- }
785
- detectBasicHeadless() {
786
- const signatures = [];
787
- // PhantomJS 탐지
788
- if (window._phantom || window.phantom) {
789
- signatures.push('PhantomJS');
790
- }
791
- // Chrome headless 기본 탐지
792
- if (navigator.userAgent.includes('HeadlessChrome')) {
793
- signatures.push('Chrome Headless');
794
- }
795
- // 플러그인 없음 (일반적이지 않음)
796
- if (navigator.plugins.length === 0) {
797
- signatures.push('No plugins');
798
- }
799
- return signatures.length > 0;
800
- }
801
- checkBrowserEnvironment() {
802
- let score = 0;
803
- const reasons = [];
804
- // 언어 설정 이상
805
- if (!navigator.language || navigator.language === 'C') {
806
- score += 10;
807
- reasons.push('Unusual language setting');
808
- }
809
- // 쿠키 비활성화
810
- if (!navigator.cookieEnabled) {
811
- score += 15;
812
- reasons.push('Cookies disabled');
813
- }
814
- // 화면 해상도 이상
815
- if (screen.width === 0 || screen.height === 0) {
816
- score += 20;
817
- reasons.push('Invalid screen resolution');
818
- }
819
- // 시간대 정보 없음
820
- try {
821
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
822
- if (!timezone || timezone === 'UTC') {
823
- score += 5;
824
- reasons.push('No timezone info');
825
- }
826
- }
827
- catch (e) {
828
- score += 10;
829
- reasons.push('Timezone detection failed');
830
- }
831
- return { score, reasons };
832
- }
833
- getRiskLevel(score) {
834
- if (score >= 70)
835
- return 'CRITICAL';
836
- if (score >= 50)
837
- return 'HIGH';
838
- if (score >= 30)
839
- return 'MEDIUM';
840
- return 'LOW';
841
- }
842
- getBrowserInfo() {
843
- return {
844
- userAgent: navigator.userAgent,
845
- language: navigator.language,
846
- platform: navigator.platform,
847
- cookieEnabled: navigator.cookieEnabled,
848
- doNotTrack: navigator.doNotTrack,
849
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
850
- screenResolution: `${screen.width}x${screen.height}`,
851
- viewportSize: `${window.innerWidth}x${window.innerHeight}`
852
- };
853
- }
854
- destroy() {
855
- // 이벤트 리스너 정리는 생략 (메모리 누수 방지를 위해 필요시 구현)
856
- }
857
- }
858
-
859
665
  /**
860
666
  * AdStage SDK 엔드포인트 상수 관리
861
667
  * 모든 API URL을 중앙에서 관리
@@ -1559,10 +1365,8 @@ class CarouselSliderManager {
1559
1365
  }
1560
1366
  });
1561
1367
  }
1562
- // 현재 슬라이드의 광고에 대해 노출 이벤트 추적
1563
- if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
1564
- trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.VIEWABLE);
1565
- }
1368
+ // 현재 슬라이드의 광고에 대해 노출 이벤트 추적 (모든 슬라이드 포함)
1369
+ trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.VIEWABLE);
1566
1370
  };
1567
1371
  // 무한 루프 처리 함수
1568
1372
  const handleInfiniteLoop = () => {
@@ -1604,8 +1408,12 @@ class CarouselSliderManager {
1604
1408
  if (dotContainer) {
1605
1409
  sliderWrapper.appendChild(dotContainer);
1606
1410
  }
1607
- // 첫 번째 도트 활성화
1411
+ // 첫 번째 도트 활성화 및 초기 VIEWABLE 이벤트 발생
1608
1412
  moveToSlide(0);
1413
+ // 슬라이더 DOM 추가 후 첫 번째 광고 VIEWABLE 이벤트 트리거 (500ms 후 실제 노출 확인)
1414
+ setTimeout(() => {
1415
+ trackEventCallback(advertisements[0]._id, slot.id, AdEventType.VIEWABLE);
1416
+ }, 500);
1609
1417
  // 사용자가 크기를 지정하지 않은 경우, 첫 번째 슬라이드 크기에 맞춰 래퍼 크기 동적 조정
1610
1418
  if (!slot.width || slot.width === 0) {
1611
1419
  // DOM 렌더링 후 크기 측정
@@ -1946,113 +1754,6 @@ class TextTransitionManager {
1946
1754
  }
1947
1755
  }
1948
1756
 
1949
- /**
1950
- * VIEWABLE 이벤트 추적 및 중복 방지 관리 클래스
1951
- * - 메모리 기반 중복 확인
1952
- * - 세션 스토리지 기반 영구 추적
1953
- * - 자동 정리 기능
1954
- * - ViewabilityTracker와 함께 사용하여 중복 viewable 이벤트 방지
1955
- */
1956
- class ViewableEventTracker {
1957
- /**
1958
- * 중복 viewable 이벤트 여부 확인
1959
- */
1960
- static isDuplicateViewable(adId, slotId, debug = false) {
1961
- const key = `${adId}_${slotId}`;
1962
- const now = Date.now();
1963
- // 디버그 모드에 따른 쿨다운 시간 결정
1964
- const cooldownTime = debug ?
1965
- ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
1966
- ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
1967
- // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
1968
- const lastViewable = ViewableEventTracker.viewableTracker.get(key);
1969
- if (lastViewable && (now - lastViewable) < cooldownTime) {
1970
- if (debug) {
1971
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
1972
- }
1973
- return true;
1974
- }
1975
- // 세션 스토리지 기반 중복 확인 (새로고침 시에도 유지)
1976
- const sessionKey = `adstage_viewable_${key}`;
1977
- const sessionViewable = sessionStorage.getItem(sessionKey);
1978
- if (sessionViewable) {
1979
- const sessionTime = parseInt(sessionViewable, 10);
1980
- if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
1981
- if (debug) {
1982
- 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)`);
1983
- }
1984
- // 메모리에도 기록하여 이후 요청 최적화
1985
- ViewableEventTracker.viewableTracker.set(key, sessionTime);
1986
- return true;
1987
- }
1988
- }
1989
- // viewable 이벤트 시점 기록 (메모리 + 세션 스토리지)
1990
- ViewableEventTracker.viewableTracker.set(key, now);
1991
- sessionStorage.setItem(sessionKey, now.toString());
1992
- // 오래된 세션 스토리지 데이터 정리 (선택적)
1993
- ViewableEventTracker.cleanupOldViewables();
1994
- if (debug) {
1995
- console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
1996
- }
1997
- return false;
1998
- }
1999
- /**
2000
- * 오래된 viewable 추적 데이터 정리
2001
- */
2002
- static cleanupOldViewables() {
2003
- const now = Date.now();
2004
- // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
2005
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
2006
- // 세션 스토리지 정리
2007
- for (let i = 0; i < sessionStorage.length; i++) {
2008
- const key = sessionStorage.key(i);
2009
- if (key && key.startsWith('adstage_viewable_')) {
2010
- const timestamp = sessionStorage.getItem(key);
2011
- if (timestamp) {
2012
- const time = parseInt(timestamp, 10);
2013
- if (!isNaN(time) && (now - time) > cleanupThreshold) {
2014
- sessionStorage.removeItem(key);
2015
- i--; // 인덱스 조정
2016
- }
2017
- }
2018
- }
2019
- }
2020
- // 메모리 정리
2021
- for (const [key, timestamp] of ViewableEventTracker.viewableTracker.entries()) {
2022
- if ((now - timestamp) > cleanupThreshold) {
2023
- ViewableEventTracker.viewableTracker.delete(key);
2024
- }
2025
- }
2026
- }
2027
- /**
2028
- * 모든 추적 데이터 정리 (디버그용)
2029
- */
2030
- static clear() {
2031
- ViewableEventTracker.viewableTracker.clear();
2032
- // 세션 스토리지에서도 adstage_viewable_ 키들 제거
2033
- const keysToRemove = [];
2034
- for (let i = 0; i < sessionStorage.length; i++) {
2035
- const key = sessionStorage.key(i);
2036
- if (key && key.startsWith('adstage_viewable_')) {
2037
- keysToRemove.push(key);
2038
- }
2039
- }
2040
- keysToRemove.forEach(key => sessionStorage.removeItem(key));
2041
- }
2042
- /**
2043
- * 특정 광고의 viewable 추적 초기화 (디버그용)
2044
- */
2045
- static clearAdViewable(adId, slotId) {
2046
- const key = `${adId}_${slotId}`;
2047
- ViewableEventTracker.viewableTracker.delete(key);
2048
- const sessionKey = `adstage_viewable_${key}`;
2049
- sessionStorage.removeItem(sessionKey);
2050
- }
2051
- }
2052
- ViewableEventTracker.viewableTracker = new Map();
2053
- ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
2054
- ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
2055
-
2056
1757
  /**
2057
1758
  * AdRenderer - 광고 렌더링 전용 클래스
2058
1759
  * AdsModule에서 렌더링 관련 기능을 분리
@@ -2248,7 +1949,7 @@ class AdRenderer {
2248
1949
  if (this.debug) {
2249
1950
  console.log(`🔄 Starting advertisement event tracking: ${eventType} for ad ${adId} in slot ${slotId}`);
2250
1951
  }
2251
- await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType, {});
1952
+ await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType);
2252
1953
  if (this.debug) {
2253
1954
  console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
2254
1955
  }
@@ -2804,14 +2505,10 @@ class AdsModule {
2804
2505
  if (!slot) {
2805
2506
  throw new Error(`Ad slot not found: ${slotId}`);
2806
2507
  }
2807
- // ViewabilityTracker 정리
2508
+ // SimpleViewabilityTracker 정리
2808
2509
  if (slot.viewabilityTracker) {
2809
2510
  slot.viewabilityTracker.destroy();
2810
2511
  }
2811
- // BasicFraudDetector 정리
2812
- if (slot.fraudDetector) {
2813
- slot.fraudDetector.destroy();
2814
- }
2815
2512
  // DOM에서 제거
2816
2513
  const container = document.getElementById(slot.containerId);
2817
2514
  if (container) {
@@ -2881,19 +2578,6 @@ class AdsModule {
2881
2578
  }
2882
2579
  return slotId;
2883
2580
  }
2884
- // createAdSlot 제거: AdRenderer.createPlaceholder 사용
2885
- /**
2886
- * 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
2887
- */
2888
- // calculateOptimalContainerSize 제거: AdRenderer.calculateOptimalContainerSize 사용
2889
- /**
2890
- * 최적 크기 조정 전략 선택
2891
- */
2892
- // selectOptimalSizeStrategy 제거: AdRenderer 내부 구현 사용
2893
- /**
2894
- * 전략에 따른 최적 높이 계산
2895
- */
2896
- // calculateOptimalHeight 제거: AdRenderer 내부 구현 사용
2897
2581
  /**
2898
2582
  * 백그라운드에서 광고 콘텐츠 로드
2899
2583
  */
@@ -2912,11 +2596,9 @@ class AdsModule {
2912
2596
  // 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
2913
2597
  if (adstageData.length > 1 || slot.config?.autoSlide) {
2914
2598
  await this.adRenderer?.renderAdSlider(slot, adstageData);
2915
- // 🔧 슬라이더의 번째 광고도 자동 viewability 추적 시작
2916
- if (adstageData.length > 0) {
2917
- setTimeout(() => {
2918
- this.startBasicViewabilityTracking(slot, adstageData[0]);
2919
- }, 100); // 슬라이더 렌더링 완료 후 추적 시작
2599
+ // 🔧 슬라이더는 CarouselSliderManager에서 자체적으로 모든 광고의 VIEWABLE 이벤트를 처리
2600
+ if (this._config?.debug) {
2601
+ console.log(`🎠 Slider will handle VIEWABLE events for ${adstageData.length} ads automatically`);
2920
2602
  }
2921
2603
  }
2922
2604
  else {
@@ -2924,7 +2606,7 @@ class AdsModule {
2924
2606
  slot.advertisement = adstageData[0];
2925
2607
  await this.adRenderer?.renderAdElement(slot, adstageData[0]);
2926
2608
  // ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
2927
- this.startBasicViewabilityTracking(slot, adstageData[0]);
2609
+ this.startSimpleViewabilityTracking(slot, adstageData[0]);
2928
2610
  }
2929
2611
  slot.isLoaded = true;
2930
2612
  if (this._config?.debug) {
@@ -2941,9 +2623,9 @@ class AdsModule {
2941
2623
  */
2942
2624
  // optimizeContainerForBannerAds 제거: AdRenderer.optimizeContainerForBannerAds 사용
2943
2625
  /**
2944
- * 기본 viewability 추적 시작 (재시도 로직 포함)
2626
+ * 단순한 노출 추적 시작 (재시도 로직 포함)
2945
2627
  */
2946
- startBasicViewabilityTracking(slot, ad) {
2628
+ startSimpleViewabilityTracking(slot, ad) {
2947
2629
  const tryStartTracking = (retryCount = 0) => {
2948
2630
  const element = document.getElementById(slot.id);
2949
2631
  if (!element) {
@@ -2959,44 +2641,28 @@ class AdsModule {
2959
2641
  }
2960
2642
  return;
2961
2643
  }
2962
- // 기본 fraud 검사
2963
- const fraudDetector = new BasicFraudDetector();
2964
- // viewability 추적
2965
- const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
2966
- await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2644
+ // 단순한 노출 추적
2645
+ const tracker = new SimpleViewabilityTracker(element, async () => {
2646
+ await this.handleViewableEvent(ad, slot);
2967
2647
  });
2968
2648
  // 정리를 위해 저장
2969
2649
  slot.viewabilityTracker = tracker;
2970
- slot.fraudDetector = fraudDetector;
2971
2650
  if (this._config?.debug) {
2972
- console.log(`🎯 Viewability tracking started for slot: ${slot.id} (element found)`);
2651
+ console.log(`🎯 Simple viewability tracking started for slot: ${slot.id} (element found)`);
2973
2652
  }
2974
2653
  };
2975
2654
  tryStartTracking();
2976
2655
  }
2977
2656
  /**
2978
- * Viewable 이벤트 처리
2657
+ * Viewable 이벤트 처리 (단순화됨)
2979
2658
  */
2980
- async handleViewableEvent(ad, slot, metrics, fraudDetector) {
2659
+ async handleViewableEvent(ad, slot) {
2981
2660
  try {
2982
- const fraudScore = fraudDetector.calculateFraudScore();
2983
- // 높은 위험도면 차단
2984
- if (fraudScore.riskLevel === 'CRITICAL') {
2985
- if (this._config?.debug) {
2986
- console.warn(`🚫 Viewable blocked due to fraud risk: ${fraudScore.score}`, fraudScore.reasons);
2987
- }
2988
- return;
2989
- }
2990
2661
  // VIEWABLE 이벤트 전송
2991
2662
  if (this.advertisementEventTracker) {
2992
- await this.advertisementEventTracker.trackAdvertisementEvent(ad._id, slot.id, AdEventType.VIEWABLE, {
2993
- viewabilityMetrics: metrics,
2994
- fraudScore: fraudScore.score,
2995
- fraudReasons: fraudScore.reasons,
2996
- riskLevel: fraudScore.riskLevel
2997
- });
2663
+ await this.advertisementEventTracker.trackAdvertisementEvent(ad._id, slot.id, AdEventType.VIEWABLE);
2998
2664
  if (this._config?.debug) {
2999
- console.log(`✅ Viewable impression tracked for ad ${ad._id} (fraud score: ${fraudScore.score})`);
2665
+ console.log(`✅ Simple viewable impression tracked for ad ${ad._id}`);
3000
2666
  }
3001
2667
  }
3002
2668
  }
@@ -3004,14 +2670,6 @@ class AdsModule {
3004
2670
  console.error(`❌ Failed to track viewable impression:`, error);
3005
2671
  }
3006
2672
  }
3007
- /**
3008
- * Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
3009
- */
3010
- // renderFallback 제거: AdRenderer.renderFallback 사용
3011
- /**
3012
- * 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
3013
- */
3014
- // createEmptyContainer 제거: AdRenderer 내부 구현 사용
3015
2673
  /**
3016
2674
  * 광고 데이터 가져오기
3017
2675
  */
@@ -3071,18 +2729,6 @@ class AdsModule {
3071
2729
  }
3072
2730
  return advertisements;
3073
2731
  }
3074
- /**
3075
- * 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
3076
- */
3077
- // renderAdSlider 제거: AdRenderer.renderAdSlider 사용
3078
- /**
3079
- * 광고 렌더링 (단일 광고용)
3080
- */
3081
- // renderAd 제거: AdRenderer.renderAd 사용
3082
- /**
3083
- * 광고 요소 렌더링 (기본 구현)
3084
- */
3085
- // renderAdElement 제거: AdRenderer.renderAdElement 사용
3086
2732
  /**
3087
2733
  * 광고 슬롯 새로고침
3088
2734
  */