@adstage/web-sdk 2.4.12 → 2.5.0

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