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