@adstage/web-sdk 2.5.3 → 2.6.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/README.md +59 -0
- package/dist/index.cjs.js +2073 -1045
- package/dist/index.d.ts +55 -12
- package/dist/index.esm.js +2073 -1045
- package/dist/index.standalone.js +2073 -1045
- package/package.json +1 -1
- package/src/constants/endpoints.ts +0 -1
- package/src/core/{AdStage.ts → adstage.ts} +36 -8
- package/src/index.ts +9 -3
- package/src/managers/ads/advertisement-event-tracker.ts +15 -11
- package/src/managers/ads/carousel-slider-manager.ts +90 -12
- package/src/managers/ads/slider-event-tracker.ts +57 -0
- package/src/managers/ads/text-transition-manager.ts +91 -26
- package/src/modules/ads/ad-renderer.ts +259 -0
- package/src/modules/ads/{AdsModule.ts → ads-module.ts} +202 -21
- package/src/modules/ads/interfaces/i-ad-renderer.ts +77 -0
- package/src/modules/ads/renderers/banner-ad-renderer.ts +414 -0
- package/src/modules/ads/renderers/base-ad-renderer.ts +340 -0
- package/src/modules/ads/renderers/interstitial-ad-renderer.ts +256 -0
- package/src/modules/ads/renderers/native-ad-renderer.ts +154 -0
- package/src/modules/ads/renderers/text-ad-renderer.ts +120 -0
- package/src/modules/ads/renderers/video-ad-renderer.ts +433 -0
- package/src/modules/config/{ConfigModule.ts → config-module.ts} +1 -5
- package/src/react/{AdStageProvider.tsx → ad-stage-provider.tsx} +1 -1
- package/src/react/index.ts +2 -2
- package/src/types/config.ts +2 -184
- package/src/utils/ad-click-handler.ts +155 -0
- package/src/utils/text-ad-utils.ts +37 -0
- package/src/dummy/ads_dummy.json +0 -84
- package/src/modules/ads/AdRenderer.ts +0 -735
- package/src/renderers/banner-renderer.ts +0 -35
- package/src/renderers/base-renderer.ts +0 -209
- package/src/renderers/index.ts +0 -71
- package/src/renderers/interstitial-renderer.ts +0 -70
- package/src/renderers/native-renderer.ts +0 -35
- package/src/renderers/text-renderer.ts +0 -94
- package/src/renderers/video-renderer.ts +0 -63
- /package/src/modules/events/{EventsModule.ts → events-module.ts} +0 -0
package/dist/index.standalone.js
CHANGED
|
@@ -28,47 +28,6 @@ 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
|
-
|
|
72
31
|
/**
|
|
73
32
|
* SSR 안전한 DOM API 래퍼 클래스
|
|
74
33
|
* 서버사이드 렌더링 환경에서 DOM API 접근 시 오류를 방지합니다.
|
|
@@ -494,7 +453,7 @@ class ApiHeaders {
|
|
|
494
453
|
* AdStage SDK - 버전 정보 유틸리티
|
|
495
454
|
*/
|
|
496
455
|
// package.json에서 버전 정보 가져오기 (빌드 시 자동으로 교체됨)
|
|
497
|
-
const SDK_VERSION$1 = '"2.
|
|
456
|
+
const SDK_VERSION$1 = '"2.6.1"';
|
|
498
457
|
/**
|
|
499
458
|
* SDK 버전 정보 반환
|
|
500
459
|
*/
|
|
@@ -523,15 +482,6 @@ class AdvertisementEventTracker {
|
|
|
523
482
|
if (this.debug) {
|
|
524
483
|
console.log(`🚀 AdvertisementEventTracker: Processing ${eventType} event for ad ${adId} in slot ${slotId}`);
|
|
525
484
|
}
|
|
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
|
-
}
|
|
535
485
|
// 현재 슬롯 정보 가져오기
|
|
536
486
|
const slot = this.slots.get(slotId);
|
|
537
487
|
// 디바이스 정보 수집
|
|
@@ -575,6 +525,12 @@ class AdvertisementEventTracker {
|
|
|
575
525
|
headers,
|
|
576
526
|
eventData
|
|
577
527
|
});
|
|
528
|
+
console.log(`🌐 Full API call details:`, {
|
|
529
|
+
method: 'POST',
|
|
530
|
+
url,
|
|
531
|
+
hasApiKey: !!this.apiKey,
|
|
532
|
+
bodySize: JSON.stringify(eventData).length
|
|
533
|
+
});
|
|
578
534
|
}
|
|
579
535
|
const response = await fetch(url, {
|
|
580
536
|
method: 'POST',
|
|
@@ -596,7 +552,15 @@ class AdvertisementEventTracker {
|
|
|
596
552
|
}
|
|
597
553
|
}
|
|
598
554
|
catch (error) {
|
|
599
|
-
console.error('Failed to track advertisement event:', error);
|
|
555
|
+
console.error('❌ Failed to track advertisement event:', error);
|
|
556
|
+
console.error('🔍 Debug info:', {
|
|
557
|
+
baseUrl: this.baseUrl,
|
|
558
|
+
apiKey: this.apiKey ? `${this.apiKey.substring(0, 8)}...` : 'NOT_SET',
|
|
559
|
+
url: `${this.baseUrl}/advertisements/events/${adId}/${eventType}`,
|
|
560
|
+
eventType,
|
|
561
|
+
adId,
|
|
562
|
+
slotId
|
|
563
|
+
});
|
|
600
564
|
}
|
|
601
565
|
}
|
|
602
566
|
/**
|
|
@@ -719,7 +683,6 @@ class EndpointBuilder {
|
|
|
719
683
|
*/
|
|
720
684
|
setBaseUrl(url) {
|
|
721
685
|
this.baseUrl = url;
|
|
722
|
-
console.log('🔄 API endpoint changed:', url);
|
|
723
686
|
}
|
|
724
687
|
/**
|
|
725
688
|
* 기본 URL 반환
|
|
@@ -740,454 +703,152 @@ class EndpointBuilder {
|
|
|
740
703
|
const endpoints = new EndpointBuilder();
|
|
741
704
|
|
|
742
705
|
/**
|
|
743
|
-
*
|
|
706
|
+
* 슬라이더 이벤트 추적 공통 유틸리티
|
|
707
|
+
* - 모든 슬라이더 타입에서 일관된 VIEWABLE 이벤트 추적
|
|
708
|
+
* - 중복 방지는 상위 레벨에서 처리
|
|
744
709
|
*/
|
|
745
|
-
class
|
|
746
|
-
constructor(trackEvent) {
|
|
747
|
-
this.trackEvent = trackEvent;
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* 공통 클릭 이벤트 핸들러 (SSR 안전)
|
|
751
|
-
*/
|
|
752
|
-
addClickHandler(element, ad, slot) {
|
|
753
|
-
DOMUtils.safeAddEventListener(element, 'click', () => {
|
|
754
|
-
this.trackEvent?.(ad._id, slot.id, 'CLICK');
|
|
755
|
-
if (ad.linkUrl) {
|
|
756
|
-
DOMUtils.safeWindowOpen(ad.linkUrl, '_blank');
|
|
757
|
-
}
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* 공통 스타일 적용 유틸리티 (SSR 안전)
|
|
762
|
-
*/
|
|
763
|
-
applyStyles(element, styles) {
|
|
764
|
-
DOMUtils.safeApplyStyles(element, styles);
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* 크기 값 파싱 유틸리티 (px, %, number 지원)
|
|
768
|
-
*/
|
|
769
|
-
parseSizeValue(value) {
|
|
770
|
-
if (!value)
|
|
771
|
-
return undefined;
|
|
772
|
-
if (typeof value === 'number') {
|
|
773
|
-
return value > 0 ? `${value}px` : undefined;
|
|
774
|
-
}
|
|
775
|
-
if (typeof value === 'string') {
|
|
776
|
-
const trimmed = value.trim();
|
|
777
|
-
if (!trimmed)
|
|
778
|
-
return undefined;
|
|
779
|
-
// 퍼센트 값 처리
|
|
780
|
-
if (trimmed.endsWith('%')) {
|
|
781
|
-
const percent = parseFloat(trimmed);
|
|
782
|
-
return !isNaN(percent) && percent > 0 ? trimmed : undefined;
|
|
783
|
-
}
|
|
784
|
-
// px 값 처리 (px 단위 포함/미포함 모두 지원)
|
|
785
|
-
const numValue = trimmed.endsWith('px')
|
|
786
|
-
? parseFloat(trimmed.slice(0, -2))
|
|
787
|
-
: parseFloat(trimmed);
|
|
788
|
-
return !isNaN(numValue) && numValue > 0 ? `${numValue}px` : undefined;
|
|
789
|
-
}
|
|
790
|
-
return undefined;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* 기본 컨테이너 스타일 (사용자 지정 크기만 적용)
|
|
794
|
-
*/
|
|
795
|
-
getBaseContainerStyles(slot) {
|
|
796
|
-
const styles = {
|
|
797
|
-
cursor: 'pointer',
|
|
798
|
-
position: 'relative',
|
|
799
|
-
overflow: 'hidden',
|
|
800
|
-
};
|
|
801
|
-
// 사용자가 지정한 크기가 있을 때만 적용
|
|
802
|
-
const parsedWidth = this.parseSizeValue(slot.width);
|
|
803
|
-
const parsedHeight = this.parseSizeValue(slot.height);
|
|
804
|
-
if (parsedWidth) {
|
|
805
|
-
styles.width = parsedWidth;
|
|
806
|
-
}
|
|
807
|
-
if (parsedHeight) {
|
|
808
|
-
styles.height = parsedHeight;
|
|
809
|
-
}
|
|
810
|
-
return styles;
|
|
811
|
-
}
|
|
710
|
+
class SliderEventTracker {
|
|
812
711
|
/**
|
|
813
|
-
*
|
|
712
|
+
* 슬라이드 변경 시 VIEWABLE 이벤트 추적
|
|
713
|
+
* @param advertisement 현재 슬라이드의 광고
|
|
714
|
+
* @param slot 광고 슬롯
|
|
715
|
+
* @param slideIndex 현재 슬라이드 인덱스
|
|
716
|
+
* @param trackEventCallback 이벤트 추적 콜백
|
|
717
|
+
* @param debug 디버그 모드
|
|
814
718
|
*/
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
'max-width': '100%',
|
|
819
|
-
height: 'auto',
|
|
820
|
-
'object-position': 'center', // 🎯 이미지 항상 중앙 정렬
|
|
821
|
-
};
|
|
822
|
-
// 사용자가 컨테이너 크기를 지정한 경우에만 크기 제한
|
|
823
|
-
const parsedWidth = this.parseSizeValue(slot?.width);
|
|
824
|
-
const parsedHeight = this.parseSizeValue(slot?.height);
|
|
825
|
-
if (parsedWidth && parsedHeight) {
|
|
826
|
-
styles.width = '100%';
|
|
827
|
-
styles.height = '100%';
|
|
828
|
-
styles['object-fit'] = 'cover';
|
|
829
|
-
styles['object-position'] = 'center'; // 🎯 크기 조정 시에도 중앙 정렬
|
|
719
|
+
static trackSlideViewable(advertisement, slot, slideIndex, trackEventCallback, debug = false) {
|
|
720
|
+
if (debug) {
|
|
721
|
+
console.log(`🎯 Triggering VIEWABLE event for slide change: ad ${advertisement._id} (index: ${slideIndex}) in slot: ${slot.id}`);
|
|
830
722
|
}
|
|
831
|
-
|
|
723
|
+
// 모든 슬라이드에 대해 VIEWABLE 이벤트 추적 (첫 번째 포함)
|
|
724
|
+
trackEventCallback(advertisement._id, slot.id, AdEventType.VIEWABLE);
|
|
832
725
|
}
|
|
833
726
|
/**
|
|
834
|
-
*
|
|
727
|
+
* 초기 슬라이드 로딩 시 VIEWABLE 이벤트 추적
|
|
728
|
+
* @param advertisement 첫 번째 슬라이드의 광고
|
|
729
|
+
* @param slot 광고 슬롯
|
|
730
|
+
* @param trackEventCallback 이벤트 추적 콜백
|
|
731
|
+
* @param debug 디버그 모드
|
|
835
732
|
*/
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
'line-height': '1.4',
|
|
840
|
-
'word-break': 'keep-all',
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* 이미지 요소 생성 (SSR 안전)
|
|
845
|
-
*/
|
|
846
|
-
createImageElement(imageUrl, alt = '', slot) {
|
|
847
|
-
const img = DOMUtils.safeCreateElement('img');
|
|
848
|
-
if (!img)
|
|
849
|
-
return null;
|
|
850
|
-
img.src = imageUrl;
|
|
851
|
-
img.alt = alt;
|
|
852
|
-
this.applyStyles(img, this.getImageStyles(slot));
|
|
853
|
-
return img;
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* 텍스트 요소 생성 (SSR 안전)
|
|
857
|
-
*/
|
|
858
|
-
createTextElement(text, tag = 'div', additionalStyles = {}) {
|
|
859
|
-
const element = DOMUtils.safeCreateElement(tag);
|
|
860
|
-
if (!element)
|
|
861
|
-
return null;
|
|
862
|
-
DOMUtils.safeSetTextContent(element, text);
|
|
863
|
-
this.applyStyles(element, {
|
|
864
|
-
...this.getBaseFontStyles(),
|
|
865
|
-
...additionalStyles,
|
|
866
|
-
});
|
|
867
|
-
return element;
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* 플레이스홀더 요소 생성
|
|
871
|
-
*/
|
|
872
|
-
createPlaceholder(slot, text = '광고') {
|
|
873
|
-
let placeholder = DOMUtils.safeCreateElement('div');
|
|
874
|
-
// SSR 환경에서 DOM을 사용할 수 없는 경우, 런타임에 생성되도록 함
|
|
875
|
-
if (!placeholder) {
|
|
876
|
-
// SSR에서는 빈 div를 반환하되, 브라우저에서는 제대로 작동하도록 함
|
|
877
|
-
if (typeof document !== 'undefined') {
|
|
878
|
-
placeholder = document.createElement('div');
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
// SSR 환경에서는 더미 객체 반환 (타입 단언 사용)
|
|
882
|
-
placeholder = {};
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
// DOM이 사용 가능한 경우에만 스타일 적용
|
|
886
|
-
if (DOMUtils.canUseDOM() && placeholder) {
|
|
887
|
-
this.applyStyles(placeholder, {
|
|
888
|
-
...this.getBaseContainerStyles(slot),
|
|
889
|
-
background: '#f8f9fa',
|
|
890
|
-
display: 'flex',
|
|
891
|
-
'align-items': 'center',
|
|
892
|
-
'justify-content': 'center',
|
|
893
|
-
color: '#6c757d',
|
|
894
|
-
...this.getBaseFontStyles(),
|
|
895
|
-
// 플레이스홀더는 최소 크기 보장
|
|
896
|
-
'min-width': '100px',
|
|
897
|
-
'min-height': '100px',
|
|
898
|
-
});
|
|
899
|
-
DOMUtils.safeSetTextContent(placeholder, text);
|
|
900
|
-
}
|
|
901
|
-
return placeholder;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* 배너 광고 렌더러 - 이미지만 표시
|
|
907
|
-
*/
|
|
908
|
-
class BannerAdRenderer extends BaseAdRenderer {
|
|
909
|
-
render(ad, slot) {
|
|
910
|
-
const adElement = DOMUtils.safeCreateElement('div');
|
|
911
|
-
if (!adElement) {
|
|
912
|
-
// SSR 환경에서는 기본 div 반환
|
|
913
|
-
return document.createElement('div');
|
|
914
|
-
}
|
|
915
|
-
// 기본 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
916
|
-
this.applyStyles(adElement, this.getBaseContainerStyles(slot));
|
|
917
|
-
// 배너 광고는 이미지만 표시
|
|
918
|
-
if (!ad.imageUrl) {
|
|
919
|
-
// 이미지가 없는 경우 플레이스홀더 반환
|
|
920
|
-
const placeholder = this.createPlaceholder(slot, '배너 광고');
|
|
921
|
-
return placeholder || adElement;
|
|
922
|
-
}
|
|
923
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
924
|
-
if (img) {
|
|
925
|
-
DOMUtils.safeAppendChild(adElement, img);
|
|
926
|
-
// 클릭 이벤트 추가
|
|
927
|
-
this.addClickHandler(adElement, ad, slot);
|
|
928
|
-
}
|
|
929
|
-
return adElement;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* 텍스트 광고 렌더러 - textContent만 표시
|
|
935
|
-
*/
|
|
936
|
-
class TextAdRenderer extends BaseAdRenderer {
|
|
937
|
-
render(ad, slot) {
|
|
938
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
939
|
-
if (!adElement) {
|
|
940
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
941
|
-
}
|
|
942
|
-
// 기본 컨테이너 스타일
|
|
943
|
-
const containerStyles = {
|
|
944
|
-
...this.getBaseContainerStyles(slot),
|
|
945
|
-
padding: '4px',
|
|
946
|
-
background: 'transparent',
|
|
947
|
-
display: 'flex',
|
|
948
|
-
'align-items': 'center',
|
|
949
|
-
'justify-content': 'center',
|
|
950
|
-
// text-align은 사용자가 설정할 수 있도록 기본값에서 제외
|
|
951
|
-
...this.getBaseFontStyles(),
|
|
952
|
-
};
|
|
953
|
-
// 사용자가 크기를 지정하지 않은 경우 컨텐츠에 맞춤
|
|
954
|
-
if (!slot.width || slot.width === 0) {
|
|
955
|
-
containerStyles.display = 'inline-flex';
|
|
956
|
-
containerStyles['white-space'] = 'nowrap';
|
|
957
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
958
|
-
}
|
|
959
|
-
// height만 자동인 경우 줄바꿈 허용
|
|
960
|
-
if ((slot.width && slot.width !== 0) && (!slot.height || slot.height === 0)) {
|
|
961
|
-
containerStyles['white-space'] = 'normal';
|
|
962
|
-
containerStyles['min-height'] = 'auto';
|
|
963
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
964
|
-
}
|
|
965
|
-
this.applyStyles(adElement, containerStyles);
|
|
966
|
-
// 텍스트 광고는 textContent만 표시
|
|
967
|
-
if (!ad.textContent) {
|
|
968
|
-
// 텍스트가 없는 경우 플레이스홀더 반환
|
|
969
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
970
|
-
}
|
|
971
|
-
// 텍스트 콘텐츠 생성
|
|
972
|
-
const textContent = this.createTextElement(ad.textContent, 'div', {
|
|
973
|
-
'font-size': '14px',
|
|
974
|
-
'font-weight': '500',
|
|
975
|
-
color: '#212529',
|
|
976
|
-
width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
|
|
977
|
-
});
|
|
978
|
-
if (textContent) {
|
|
979
|
-
adElement.appendChild(textContent);
|
|
980
|
-
}
|
|
981
|
-
// 사용자가 text-align을 지정했는지 확인하고 레이아웃 조정
|
|
982
|
-
setTimeout(() => {
|
|
983
|
-
if (!adElement || typeof window === 'undefined')
|
|
984
|
-
return;
|
|
985
|
-
const computedStyle = window.getComputedStyle(adElement);
|
|
986
|
-
const textAlign = computedStyle.textAlign;
|
|
987
|
-
// 사용자가 text-align을 설정했고, width가 없는 경우
|
|
988
|
-
if (textAlign && textAlign !== 'start' && textAlign !== 'left' && (!slot.width || slot.width === 0)) {
|
|
989
|
-
// 블록 레벨로 변경하여 text-align이 제대로 작동하도록 함
|
|
990
|
-
adElement.style.display = 'block';
|
|
991
|
-
adElement.style.whiteSpace = 'normal';
|
|
992
|
-
// 최소 너비 설정 (텍스트가 한 줄일 때를 위해)
|
|
993
|
-
if (textContent) {
|
|
994
|
-
const textRect = textContent.getBoundingClientRect();
|
|
995
|
-
if (textRect.width > 0) {
|
|
996
|
-
adElement.style.minWidth = `${textRect.width}px`;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}, 0);
|
|
1001
|
-
// 클릭 이벤트 추가
|
|
1002
|
-
this.addClickHandler(adElement, ad, slot);
|
|
1003
|
-
return adElement;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* 네이티브 광고 렌더러 - 이미지 + textContent 표시
|
|
1009
|
-
*/
|
|
1010
|
-
class NativeAdRenderer extends BaseAdRenderer {
|
|
1011
|
-
render(ad, slot) {
|
|
1012
|
-
const adElement = DOMUtils.safeCreateElement('div');
|
|
1013
|
-
if (!adElement) {
|
|
1014
|
-
return document.createElement('div');
|
|
1015
|
-
}
|
|
1016
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
1017
|
-
this.applyStyles(adElement, this.getBaseContainerStyles(slot));
|
|
1018
|
-
// 네이티브 광고는 이미지만 표시
|
|
1019
|
-
if (!ad.imageUrl) {
|
|
1020
|
-
// 이미지가 없는 경우 플레이스홀더 반환
|
|
1021
|
-
const placeholder = this.createPlaceholder(slot, '네이티브 광고');
|
|
1022
|
-
return placeholder || adElement;
|
|
1023
|
-
}
|
|
1024
|
-
// 이미지 생성 (고유 사이즈 또는 사용자 지정 크기)
|
|
1025
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
1026
|
-
if (img) {
|
|
1027
|
-
DOMUtils.safeAppendChild(adElement, img);
|
|
1028
|
-
// 클릭 이벤트 추가
|
|
1029
|
-
this.addClickHandler(adElement, ad, slot);
|
|
733
|
+
static trackInitialSlideViewable(advertisement, slot, trackEventCallback, debug = false) {
|
|
734
|
+
if (debug) {
|
|
735
|
+
console.log(`🎯 Triggering initial VIEWABLE event: ad ${advertisement._id} (index: 0) in slot: ${slot.id}`);
|
|
1030
736
|
}
|
|
1031
|
-
|
|
737
|
+
// 첫 번째 슬라이드도 동일하게 추적
|
|
738
|
+
trackEventCallback(advertisement._id, slot.id, AdEventType.VIEWABLE);
|
|
1032
739
|
}
|
|
1033
740
|
}
|
|
1034
741
|
|
|
1035
742
|
/**
|
|
1036
|
-
*
|
|
743
|
+
* 광고 클릭 이벤트 핸들러 유틸리티
|
|
744
|
+
* 모든 광고 타입에서 일관된 클릭 이벤트 처리를 위한 공통 컴포넌트
|
|
1037
745
|
*/
|
|
1038
|
-
class
|
|
1039
|
-
render(ad, slot) {
|
|
1040
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
1041
|
-
if (!adElement) {
|
|
1042
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
1043
|
-
}
|
|
1044
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
1045
|
-
this.applyStyles(adElement, {
|
|
1046
|
-
...this.getBaseContainerStyles(slot),
|
|
1047
|
-
background: '#000',
|
|
1048
|
-
});
|
|
1049
|
-
// 비디오 광고는 비디오만 표시
|
|
1050
|
-
if (!ad.videoUrl) {
|
|
1051
|
-
// 비디오가 없는 경우 플레이스홀더 반환
|
|
1052
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
1053
|
-
}
|
|
1054
|
-
const video = this.createVideoElement(ad.videoUrl, ad, slot);
|
|
1055
|
-
if (video) {
|
|
1056
|
-
adElement.appendChild(video);
|
|
1057
|
-
}
|
|
1058
|
-
// 클릭 이벤트 추가
|
|
1059
|
-
this.addClickHandler(adElement, ad, slot);
|
|
1060
|
-
return adElement;
|
|
1061
|
-
}
|
|
746
|
+
class AdClickHandler {
|
|
1062
747
|
/**
|
|
1063
|
-
*
|
|
748
|
+
* 광고 요소에 클릭 이벤트를 추가하는 공통 함수
|
|
749
|
+
* @param element - 클릭 이벤트를 추가할 DOM 요소
|
|
750
|
+
* @param advertisement - 광고 데이터
|
|
751
|
+
* @param slot - 광고 슬롯 정보
|
|
752
|
+
* @param trackEventCallback - 이벤트 추적 콜백 함수
|
|
753
|
+
* @param debug - 디버그 모드
|
|
754
|
+
* @param adTypeLabel - 광고 타입 라벨 (로그용, 선택사항)
|
|
1064
755
|
*/
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (!
|
|
1068
|
-
return
|
|
1069
|
-
video.src = videoUrl;
|
|
1070
|
-
video.controls = true;
|
|
1071
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
1072
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
1073
|
-
// 비디오 이벤트 추적
|
|
1074
|
-
video.addEventListener('play', () => {
|
|
1075
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START');
|
|
1076
|
-
});
|
|
1077
|
-
video.addEventListener('ended', () => {
|
|
1078
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE');
|
|
1079
|
-
});
|
|
1080
|
-
return video;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
/**
|
|
1085
|
-
* 전면/팝업 광고 렌더러 - 핵심 콘텐츠만 표시
|
|
1086
|
-
*/
|
|
1087
|
-
class InterstitialAdRenderer extends BaseAdRenderer {
|
|
1088
|
-
render(ad, slot) {
|
|
1089
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
1090
|
-
if (!adElement) {
|
|
1091
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
1092
|
-
}
|
|
1093
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
1094
|
-
this.applyStyles(adElement, {
|
|
1095
|
-
...this.getBaseContainerStyles(slot),
|
|
1096
|
-
display: 'flex',
|
|
1097
|
-
'flex-direction': 'column',
|
|
1098
|
-
});
|
|
1099
|
-
// 우선순위: 1. 이미지, 2. 비디오, 3. 텍스트
|
|
1100
|
-
if (ad.imageUrl) {
|
|
1101
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
1102
|
-
if (img) {
|
|
1103
|
-
adElement.appendChild(img);
|
|
1104
|
-
}
|
|
756
|
+
static addClickEvent(element, advertisement, slot, trackEventCallback, debug = false, adTypeLabel) {
|
|
757
|
+
// linkUrl이 없으면 클릭 이벤트 추가하지 않음
|
|
758
|
+
if (!advertisement.linkUrl) {
|
|
759
|
+
return;
|
|
1105
760
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
761
|
+
// 커서 스타일 설정
|
|
762
|
+
element.style.cursor = 'pointer';
|
|
763
|
+
// 클릭 이벤트 리스너 추가
|
|
764
|
+
element.addEventListener('click', (e) => {
|
|
765
|
+
e.preventDefault();
|
|
766
|
+
e.stopPropagation();
|
|
767
|
+
// 이벤트 추적
|
|
768
|
+
if (trackEventCallback) {
|
|
769
|
+
trackEventCallback(advertisement._id, slot.id, AdEventType.CLICK);
|
|
770
|
+
}
|
|
771
|
+
// 링크 이동
|
|
772
|
+
window.open(advertisement.linkUrl, '_blank');
|
|
773
|
+
// 디버그 로그
|
|
774
|
+
if (debug) {
|
|
775
|
+
const typeLabel = adTypeLabel || String(slot.adType).toLowerCase();
|
|
776
|
+
console.log(`🔗 ${typeLabel} ad clicked: ${advertisement._id} -> ${advertisement.linkUrl}`);
|
|
1111
777
|
}
|
|
1112
|
-
}
|
|
1113
|
-
else {
|
|
1114
|
-
// 모든 콘텐츠가 없는 경우
|
|
1115
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
1116
|
-
}
|
|
1117
|
-
// 클릭 이벤트 추가
|
|
1118
|
-
this.addClickHandler(adElement, ad, slot);
|
|
1119
|
-
return adElement;
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* 비디오 요소 생성
|
|
1123
|
-
*/
|
|
1124
|
-
createVideoElement(videoUrl, ad, slot) {
|
|
1125
|
-
const video = DOMUtils.safeCreateElement('video');
|
|
1126
|
-
if (!video)
|
|
1127
|
-
return null;
|
|
1128
|
-
video.src = videoUrl;
|
|
1129
|
-
video.controls = true;
|
|
1130
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
1131
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
1132
|
-
// 비디오 이벤트 추적
|
|
1133
|
-
video.addEventListener('play', () => {
|
|
1134
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START');
|
|
1135
|
-
});
|
|
1136
|
-
video.addEventListener('ended', () => {
|
|
1137
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE');
|
|
1138
778
|
});
|
|
1139
|
-
return video;
|
|
1140
779
|
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
var _a;
|
|
1144
|
-
/**
|
|
1145
|
-
* 광고 렌더러 팩토리
|
|
1146
|
-
* - 광고 타입에 따라 적절한 렌더러 인스턴스를 반환
|
|
1147
|
-
*/
|
|
1148
|
-
class AdRendererFactory {
|
|
1149
780
|
/**
|
|
1150
|
-
*
|
|
781
|
+
* 렌더러에서 사용할 수 있는 간편한 클릭 이벤트 추가 함수
|
|
782
|
+
* BaseAdRenderer를 상속받은 클래스에서 사용
|
|
783
|
+
* @param element - 클릭 이벤트를 추가할 DOM 요소
|
|
784
|
+
* @param advertisement - 광고 데이터
|
|
785
|
+
* @param slot - 광고 슬롯 정보
|
|
786
|
+
* @param createEventTrackingCallback - 이벤트 추적 콜백 생성 함수
|
|
787
|
+
* @param debug - 디버그 모드
|
|
788
|
+
* @param adTypeLabel - 광고 타입 라벨 (로그용, 선택사항)
|
|
1151
789
|
*/
|
|
1152
|
-
static
|
|
1153
|
-
|
|
1154
|
-
if (!
|
|
1155
|
-
|
|
1156
|
-
return new BannerAdRenderer(trackEvent);
|
|
790
|
+
static addClickEventForRenderer(element, advertisement, slot, createEventTrackingCallback, debug = false, adTypeLabel) {
|
|
791
|
+
// linkUrl이 없으면 클릭 이벤트 추가하지 않음
|
|
792
|
+
if (!advertisement.linkUrl) {
|
|
793
|
+
return;
|
|
1157
794
|
}
|
|
1158
|
-
|
|
795
|
+
// 커서 스타일 설정
|
|
796
|
+
element.style.cursor = 'pointer';
|
|
797
|
+
// 클릭 이벤트 리스너 추가
|
|
798
|
+
element.addEventListener('click', (e) => {
|
|
799
|
+
e.preventDefault();
|
|
800
|
+
e.stopPropagation();
|
|
801
|
+
// 이벤트 추적 콜백 생성
|
|
802
|
+
const trackEventCallback = createEventTrackingCallback();
|
|
803
|
+
trackEventCallback(advertisement._id, slot.id, AdEventType.CLICK);
|
|
804
|
+
// 링크 이동
|
|
805
|
+
window.open(advertisement.linkUrl, '_blank');
|
|
806
|
+
// 디버그 로그
|
|
807
|
+
if (debug) {
|
|
808
|
+
const typeLabel = adTypeLabel || String(slot.adType).toLowerCase();
|
|
809
|
+
console.log(`🔗 ${typeLabel} ad clicked: ${advertisement._id} -> ${advertisement.linkUrl}`);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
1159
812
|
}
|
|
1160
813
|
/**
|
|
1161
|
-
*
|
|
814
|
+
* 슬라이더/매니저에서 사용할 수 있는 클릭 이벤트 추가 함수
|
|
815
|
+
* 이미 trackEventCallback이 준비된 상황에서 사용
|
|
816
|
+
* @param element - 클릭 이벤트를 추가할 DOM 요소
|
|
817
|
+
* @param advertisement - 광고 데이터
|
|
818
|
+
* @param slot - 광고 슬롯 정보
|
|
819
|
+
* @param trackEventCallback - 준비된 이벤트 추적 콜백
|
|
820
|
+
* @param debug - 디버그 모드
|
|
821
|
+
* @param adTypeLabel - 광고 타입 라벨 (로그용, 선택사항)
|
|
1162
822
|
*/
|
|
1163
|
-
static
|
|
1164
|
-
|
|
1165
|
-
return renderer.render(ad, slot);
|
|
823
|
+
static addClickEventForSlider(element, advertisement, slot, trackEventCallback, debug = false, adTypeLabel) {
|
|
824
|
+
this.addClickEvent(element, advertisement, slot, trackEventCallback, debug, adTypeLabel);
|
|
1166
825
|
}
|
|
1167
826
|
/**
|
|
1168
|
-
*
|
|
827
|
+
* 클릭 가능한 광고인지 확인하는 헬퍼 함수
|
|
828
|
+
* @param advertisement - 광고 데이터
|
|
829
|
+
* @returns linkUrl이 있으면 true, 없으면 false
|
|
1169
830
|
*/
|
|
1170
|
-
static
|
|
1171
|
-
return
|
|
831
|
+
static isClickable(advertisement) {
|
|
832
|
+
return Boolean(advertisement.linkUrl);
|
|
1172
833
|
}
|
|
1173
834
|
/**
|
|
1174
|
-
*
|
|
835
|
+
* 여러 요소에 대해 일괄적으로 클릭 이벤트를 추가하는 함수
|
|
836
|
+
* @param elements - 클릭 이벤트를 추가할 DOM 요소들
|
|
837
|
+
* @param advertisements - 광고 데이터 배열 (elements와 같은 순서)
|
|
838
|
+
* @param slot - 광고 슬롯 정보
|
|
839
|
+
* @param trackEventCallback - 이벤트 추적 콜백
|
|
840
|
+
* @param debug - 디버그 모드
|
|
841
|
+
* @param adTypeLabel - 광고 타입 라벨 (로그용, 선택사항)
|
|
1175
842
|
*/
|
|
1176
|
-
static
|
|
1177
|
-
|
|
843
|
+
static addClickEventsBatch(elements, advertisements, slot, trackEventCallback, debug = false, adTypeLabel) {
|
|
844
|
+
elements.forEach((element, index) => {
|
|
845
|
+
const advertisement = advertisements[index];
|
|
846
|
+
if (advertisement) {
|
|
847
|
+
this.addClickEvent(element, advertisement, slot, trackEventCallback, debug, adTypeLabel);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
1178
850
|
}
|
|
1179
851
|
}
|
|
1180
|
-
_a = AdRendererFactory;
|
|
1181
|
-
AdRendererFactory.renderers = new Map();
|
|
1182
|
-
(() => {
|
|
1183
|
-
// 렌더러 등록
|
|
1184
|
-
_a.renderers.set(AdType.BANNER, BannerAdRenderer);
|
|
1185
|
-
_a.renderers.set(AdType.TEXT, TextAdRenderer);
|
|
1186
|
-
_a.renderers.set(AdType.NATIVE, NativeAdRenderer);
|
|
1187
|
-
_a.renderers.set(AdType.VIDEO, VideoAdRenderer);
|
|
1188
|
-
_a.renderers.set(AdType.INTERSTITIAL, InterstitialAdRenderer);
|
|
1189
|
-
_a.renderers.set(AdType.POPUP, InterstitialAdRenderer); // POPUP은 INTERSTITIAL과 동일
|
|
1190
|
-
})();
|
|
1191
852
|
|
|
1192
853
|
/**
|
|
1193
854
|
* 캐러셀 슬라이더 관리 클래스
|
|
@@ -1197,10 +858,78 @@ AdRendererFactory.renderers = new Map();
|
|
|
1197
858
|
* - 도트 인디케이터 포함
|
|
1198
859
|
*/
|
|
1199
860
|
class CarouselSliderManager {
|
|
861
|
+
/**
|
|
862
|
+
* 간단한 광고 요소 생성 (크기 측정용)
|
|
863
|
+
*/
|
|
864
|
+
static createSimpleAdElement(slot, advertisement) {
|
|
865
|
+
const adElement = document.createElement('div');
|
|
866
|
+
adElement.className = `adstage-ad adstage-${String(slot.adType).toLowerCase()}`;
|
|
867
|
+
adElement.setAttribute('data-adstage-ad-id', advertisement._id);
|
|
868
|
+
adElement.setAttribute('data-adstage-slot-id', slot.id);
|
|
869
|
+
// 기본 스타일 설정
|
|
870
|
+
adElement.style.display = 'block';
|
|
871
|
+
adElement.style.width = '100%';
|
|
872
|
+
adElement.style.height = 'auto';
|
|
873
|
+
// 광고 타입별 기본 컨테이너 설정
|
|
874
|
+
switch (slot.adType) {
|
|
875
|
+
case AdType.BANNER:
|
|
876
|
+
if (advertisement.imageUrl) {
|
|
877
|
+
const img = document.createElement('img');
|
|
878
|
+
img.src = advertisement.imageUrl;
|
|
879
|
+
img.style.width = '100%';
|
|
880
|
+
img.style.height = 'auto';
|
|
881
|
+
img.style.objectFit = 'cover';
|
|
882
|
+
adElement.appendChild(img);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
adElement.style.height = '100px';
|
|
886
|
+
adElement.style.backgroundColor = '#f0f0f0';
|
|
887
|
+
adElement.style.border = '1px dashed #ccc';
|
|
888
|
+
adElement.textContent = 'Banner Ad';
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
case AdType.VIDEO:
|
|
892
|
+
if (advertisement.videoUrl) {
|
|
893
|
+
const video = document.createElement('video');
|
|
894
|
+
video.src = advertisement.videoUrl;
|
|
895
|
+
video.style.width = '100%';
|
|
896
|
+
video.style.height = 'auto';
|
|
897
|
+
adElement.appendChild(video);
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
adElement.style.height = '200px';
|
|
901
|
+
adElement.style.backgroundColor = '#000';
|
|
902
|
+
adElement.style.border = '1px solid #666';
|
|
903
|
+
adElement.textContent = 'Video Ad';
|
|
904
|
+
adElement.style.color = 'white';
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
case AdType.TEXT:
|
|
908
|
+
if (advertisement.textContent) {
|
|
909
|
+
const textDiv = document.createElement('div');
|
|
910
|
+
textDiv.textContent = advertisement.textContent || '';
|
|
911
|
+
textDiv.style.padding = '8px';
|
|
912
|
+
textDiv.style.fontSize = '14px';
|
|
913
|
+
adElement.appendChild(textDiv);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
adElement.style.height = '50px';
|
|
917
|
+
adElement.style.padding = '8px';
|
|
918
|
+
adElement.textContent = 'Text Ad';
|
|
919
|
+
}
|
|
920
|
+
break;
|
|
921
|
+
default:
|
|
922
|
+
adElement.style.height = '100px';
|
|
923
|
+
adElement.style.border = '1px dashed #ccc';
|
|
924
|
+
adElement.style.backgroundColor = '#f9f9f9';
|
|
925
|
+
adElement.textContent = `${slot.adType} Ad`;
|
|
926
|
+
}
|
|
927
|
+
return adElement;
|
|
928
|
+
}
|
|
1200
929
|
/**
|
|
1201
930
|
* Create carousel slider container with dot indicators and navigation
|
|
1202
931
|
*/
|
|
1203
|
-
static createSliderContainer(slot, advertisements, options, trackEventCallback) {
|
|
932
|
+
static createSliderContainer(slot, advertisements, options, trackEventCallback, debug = false) {
|
|
1204
933
|
const sliderWrapper = document.createElement('div');
|
|
1205
934
|
sliderWrapper.className = 'adstage-slider-wrapper';
|
|
1206
935
|
// 사용자 지정 크기가 있으면 적용, 없으면 콘텐츠 크기에 맞춤
|
|
@@ -1263,7 +992,7 @@ class CarouselSliderManager {
|
|
|
1263
992
|
let maxHeight = 0;
|
|
1264
993
|
// 모든 광고의 크기를 측정하여 최대 크기 찾기
|
|
1265
994
|
advertisements.forEach(ad => {
|
|
1266
|
-
const measureAdElement =
|
|
995
|
+
const measureAdElement = this.createSimpleAdElement(slot, ad);
|
|
1267
996
|
measureContainer.appendChild(measureAdElement);
|
|
1268
997
|
const rect = measureAdElement.getBoundingClientRect();
|
|
1269
998
|
if (rect.width > maxWidth)
|
|
@@ -1321,7 +1050,9 @@ class CarouselSliderManager {
|
|
|
1321
1050
|
slideElement.style.setProperty(key, value);
|
|
1322
1051
|
});
|
|
1323
1052
|
// 광고 렌더링
|
|
1324
|
-
const adElement =
|
|
1053
|
+
const adElement = this.createSimpleAdElement(slot, ad);
|
|
1054
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
1055
|
+
AdClickHandler.addClickEventForSlider(adElement, ad, slot, trackEventCallback, debug, String(slot.adType).toLowerCase());
|
|
1325
1056
|
slideElement.appendChild(adElement);
|
|
1326
1057
|
slideContainer.appendChild(slideElement);
|
|
1327
1058
|
});
|
|
@@ -1365,9 +1096,9 @@ class CarouselSliderManager {
|
|
|
1365
1096
|
}
|
|
1366
1097
|
});
|
|
1367
1098
|
}
|
|
1368
|
-
//
|
|
1369
|
-
|
|
1370
|
-
|
|
1099
|
+
// 🎯 공통 슬라이더 이벤트 추적 적용 (모든 슬라이드 포함)
|
|
1100
|
+
SliderEventTracker.trackSlideViewable(advertisements[actualIndex], slot, actualIndex, trackEventCallback, debug // debug 모드
|
|
1101
|
+
);
|
|
1371
1102
|
};
|
|
1372
1103
|
// 무한 루프 처리 함수
|
|
1373
1104
|
const handleInfiniteLoop = () => {
|
|
@@ -1527,48 +1258,750 @@ class CarouselSliderManager {
|
|
|
1527
1258
|
}
|
|
1528
1259
|
|
|
1529
1260
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
* -
|
|
1532
|
-
* -
|
|
1533
|
-
* - 무한 루프 지원
|
|
1261
|
+
* 단순한 VIEWABLE 이벤트 중복 방지 관리 클래스
|
|
1262
|
+
* - 세션당 동일 광고 1회만 VIEWABLE 이벤트 허용
|
|
1263
|
+
* - 메모리 기반 추적으로 단순화
|
|
1534
1264
|
*/
|
|
1535
|
-
class
|
|
1265
|
+
class ViewableEventTracker {
|
|
1536
1266
|
/**
|
|
1537
|
-
*
|
|
1267
|
+
* 중복 viewable 이벤트 여부 확인
|
|
1538
1268
|
*/
|
|
1539
|
-
static
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
overflow: 'hidden',
|
|
1546
|
-
display: 'inline-block',
|
|
1547
|
-
};
|
|
1548
|
-
// 사용자가 크기를 지정한 경우
|
|
1549
|
-
if (slot.width && slot.width !== 0) {
|
|
1550
|
-
let width;
|
|
1551
|
-
if (typeof slot.width === 'string') {
|
|
1552
|
-
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
1553
|
-
}
|
|
1554
|
-
else {
|
|
1555
|
-
width = `${slot.width}px`;
|
|
1269
|
+
static isDuplicateViewable(adId, slotId, debug = false) {
|
|
1270
|
+
const key = `${adId}_${slotId}`;
|
|
1271
|
+
// 이미 VIEWABLE 이벤트가 발생한 광고인지 확인
|
|
1272
|
+
if (ViewableEventTracker.viewableTracker.has(key)) {
|
|
1273
|
+
if (debug) {
|
|
1274
|
+
console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
1556
1275
|
}
|
|
1557
|
-
|
|
1276
|
+
return true;
|
|
1558
1277
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
else {
|
|
1565
|
-
height = `${slot.height}px`;
|
|
1566
|
-
}
|
|
1567
|
-
containerStyles.height = height;
|
|
1278
|
+
// 새로운 VIEWABLE 이벤트 기록
|
|
1279
|
+
ViewableEventTracker.viewableTracker.add(key);
|
|
1280
|
+
if (debug) {
|
|
1281
|
+
console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
|
|
1568
1282
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* 모든 추적 데이터 정리 (디버그용)
|
|
1287
|
+
*/
|
|
1288
|
+
static clear() {
|
|
1289
|
+
ViewableEventTracker.viewableTracker.clear();
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* 특정 광고의 viewable 추적 초기화 (디버그용)
|
|
1293
|
+
*/
|
|
1294
|
+
static clearAdViewable(adId, slotId) {
|
|
1295
|
+
const key = `${adId}_${slotId}`;
|
|
1296
|
+
ViewableEventTracker.viewableTracker.delete(key);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
ViewableEventTracker.viewableTracker = new Set();
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* 베이스 광고 렌더러 - 공통 기능 구현
|
|
1303
|
+
*/
|
|
1304
|
+
class BaseAdRenderer {
|
|
1305
|
+
constructor(adType, debug = false, advertisementEventTracker) {
|
|
1306
|
+
this.adType = adType;
|
|
1307
|
+
this.debug = debug;
|
|
1308
|
+
this.advertisementEventTracker = advertisementEventTracker || null;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Placeholder(슬롯 컨테이너) 생성 - 공통 구현
|
|
1312
|
+
*/
|
|
1313
|
+
createPlaceholder(container, slotId, options, config) {
|
|
1314
|
+
const adElement = document.createElement('div');
|
|
1315
|
+
adElement.id = slotId;
|
|
1316
|
+
adElement.className = `adstage-slot adstage-${String(this.adType).toLowerCase()}`;
|
|
1317
|
+
adElement.setAttribute('data-adstage-container', 'true');
|
|
1318
|
+
adElement.setAttribute('data-adstage-type', String(this.adType));
|
|
1319
|
+
adElement.setAttribute('data-adstage-slot', slotId);
|
|
1320
|
+
const { width, height } = this.calculateAdSize(container, options, config) || {
|
|
1321
|
+
width: '100%',
|
|
1322
|
+
height: this.getDefaultHeight()
|
|
1323
|
+
};
|
|
1324
|
+
adElement.style.width = width;
|
|
1325
|
+
adElement.style.height = height;
|
|
1326
|
+
// 플레이스홀더 스타일 모드 결정
|
|
1327
|
+
const placeholderMode = config?.placeholderMode || options.placeholderMode || 'invisible';
|
|
1328
|
+
this.applyPlaceholderStyle(adElement, placeholderMode);
|
|
1329
|
+
container.appendChild(adElement);
|
|
1330
|
+
if (this.debug) {
|
|
1331
|
+
console.log(`📦 Placeholder created for ${this.adType} slot: ${slotId} (${width} x ${height}) - Mode: ${placeholderMode}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* 플레이스홀더 스타일 적용
|
|
1336
|
+
*/
|
|
1337
|
+
applyPlaceholderStyle(element, mode) {
|
|
1338
|
+
switch (mode) {
|
|
1339
|
+
case 'invisible':
|
|
1340
|
+
// 완전히 투명한 플레이스홀더
|
|
1341
|
+
element.style.backgroundColor = 'transparent';
|
|
1342
|
+
element.style.border = 'none';
|
|
1343
|
+
element.style.opacity = '0';
|
|
1344
|
+
element.innerHTML = '';
|
|
1345
|
+
break;
|
|
1346
|
+
case 'transparent':
|
|
1347
|
+
// 투명하지만 공간은 차지
|
|
1348
|
+
element.style.backgroundColor = 'transparent';
|
|
1349
|
+
element.style.border = 'none';
|
|
1350
|
+
element.style.display = 'block';
|
|
1351
|
+
element.innerHTML = '';
|
|
1352
|
+
break;
|
|
1353
|
+
case 'subtle':
|
|
1354
|
+
// 매우 은은한 표시
|
|
1355
|
+
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
|
|
1356
|
+
element.style.border = 'none';
|
|
1357
|
+
element.style.borderRadius = '4px';
|
|
1358
|
+
element.style.display = 'flex';
|
|
1359
|
+
element.style.alignItems = 'center';
|
|
1360
|
+
element.style.justifyContent = 'center';
|
|
1361
|
+
element.innerHTML = '<span style="color: rgba(0, 0, 0, 0.3); font-size: 11px; font-family: sans-serif;">•••</span>';
|
|
1362
|
+
break;
|
|
1363
|
+
case 'minimal':
|
|
1364
|
+
// 최소한의 표시 (기본값)
|
|
1365
|
+
element.style.backgroundColor = 'rgba(248, 249, 250, 0.5)';
|
|
1366
|
+
element.style.border = '1px solid rgba(0, 0, 0, 0.08)';
|
|
1367
|
+
element.style.borderRadius = '6px';
|
|
1368
|
+
element.style.display = 'flex';
|
|
1369
|
+
element.style.alignItems = 'center';
|
|
1370
|
+
element.style.justifyContent = 'center';
|
|
1371
|
+
element.innerHTML = '<span style="color: rgba(0, 0, 0, 0.4); font-size: 12px; font-family: -apple-system, sans-serif;">•••</span>';
|
|
1372
|
+
break;
|
|
1373
|
+
case 'debug':
|
|
1374
|
+
// 개발/디버그용 명확한 표시
|
|
1375
|
+
element.style.border = '2px dashed #e74c3c';
|
|
1376
|
+
element.style.display = 'flex';
|
|
1377
|
+
element.style.alignItems = 'center';
|
|
1378
|
+
element.style.justifyContent = 'center';
|
|
1379
|
+
element.style.backgroundColor = 'rgba(231, 76, 60, 0.1)';
|
|
1380
|
+
element.style.color = '#e74c3c';
|
|
1381
|
+
element.style.fontFamily = 'monospace';
|
|
1382
|
+
element.style.fontSize = '11px';
|
|
1383
|
+
element.innerHTML = `<span>Loading ${this.adType} ad...</span>`;
|
|
1384
|
+
break;
|
|
1385
|
+
default:
|
|
1386
|
+
// 기존 스타일 (legacy)
|
|
1387
|
+
element.style.border = '1px dashed #ccc';
|
|
1388
|
+
element.style.display = 'flex';
|
|
1389
|
+
element.style.alignItems = 'center';
|
|
1390
|
+
element.style.justifyContent = 'center';
|
|
1391
|
+
element.style.backgroundColor = '#f9f9f9';
|
|
1392
|
+
element.style.color = '#666';
|
|
1393
|
+
element.innerHTML = `<span>Loading ${this.adType} ad...</span>`;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* 광고 크기 계산 - 공통 구현
|
|
1398
|
+
*/
|
|
1399
|
+
calculateAdSize(container, options, config) {
|
|
1400
|
+
// 사용자가 명시적으로 크기를 지정한 경우
|
|
1401
|
+
const explicitWidth = options.width;
|
|
1402
|
+
const explicitHeight = options.height;
|
|
1403
|
+
// 너비 처리
|
|
1404
|
+
let width;
|
|
1405
|
+
if (typeof explicitWidth === 'number') {
|
|
1406
|
+
width = `${explicitWidth}px`;
|
|
1407
|
+
}
|
|
1408
|
+
else if (typeof explicitWidth === 'string') {
|
|
1409
|
+
width = explicitWidth;
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
width = '100%'; // 기본값은 100%
|
|
1413
|
+
}
|
|
1414
|
+
// 높이 처리 - 핵심 로직
|
|
1415
|
+
let height;
|
|
1416
|
+
if (typeof explicitHeight === 'number') {
|
|
1417
|
+
height = `${explicitHeight}px`;
|
|
1418
|
+
}
|
|
1419
|
+
else if (typeof explicitHeight === 'string' && explicitHeight !== '100%' && explicitHeight !== 'auto') {
|
|
1420
|
+
// 명시적인 크기 문자열 (예: '200px', '50vh' 등)
|
|
1421
|
+
height = explicitHeight;
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
// 100%, auto이거나 높이가 지정되지 않은 경우 스마트 계산
|
|
1425
|
+
const containerHeight = this.getContainerHeight(container);
|
|
1426
|
+
if (containerHeight > 0) {
|
|
1427
|
+
// 컨테이너에 높이가 있으면 100% 사용
|
|
1428
|
+
height = '100%';
|
|
1429
|
+
if (config?.debug || this.debug) {
|
|
1430
|
+
console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
// 컨테이너에 높이가 없으면 타입별 기본값 사용
|
|
1435
|
+
height = this.getDefaultHeight();
|
|
1436
|
+
if (config?.debug || this.debug) {
|
|
1437
|
+
console.log(`📏 Using default height ${height} for ${this.adType}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return { width, height };
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* 컨테이너의 실제 높이 계산 - 공통 구현
|
|
1445
|
+
*/
|
|
1446
|
+
getContainerHeight(container) {
|
|
1447
|
+
const computedStyle = window.getComputedStyle(container);
|
|
1448
|
+
const height = parseFloat(computedStyle.height);
|
|
1449
|
+
if (!height || height === 0) {
|
|
1450
|
+
const minHeight = parseFloat(computedStyle.minHeight);
|
|
1451
|
+
if (minHeight > 0)
|
|
1452
|
+
return minHeight;
|
|
1453
|
+
if (container.style.height && container.style.height !== 'auto') {
|
|
1454
|
+
const styleHeight = parseFloat(container.style.height);
|
|
1455
|
+
if (styleHeight > 0)
|
|
1456
|
+
return styleHeight;
|
|
1457
|
+
}
|
|
1458
|
+
const heightAttr = container.getAttribute('height');
|
|
1459
|
+
if (heightAttr) {
|
|
1460
|
+
const attrHeight = parseFloat(heightAttr);
|
|
1461
|
+
if (attrHeight > 0)
|
|
1462
|
+
return attrHeight;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return height || 0;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* 이벤트 트래킹 콜백 생성 - 공통 구현
|
|
1469
|
+
*/
|
|
1470
|
+
createEventTrackingCallback() {
|
|
1471
|
+
return async (adId, slotId, eventType) => {
|
|
1472
|
+
if (eventType === AdEventType.VIEWABLE) {
|
|
1473
|
+
if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
|
|
1474
|
+
if (this.debug) {
|
|
1475
|
+
console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
1476
|
+
}
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (this.debug) {
|
|
1480
|
+
console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if (this.advertisementEventTracker) {
|
|
1484
|
+
try {
|
|
1485
|
+
if (this.debug) {
|
|
1486
|
+
console.log(`🔄 Starting advertisement event tracking: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
1487
|
+
}
|
|
1488
|
+
await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType);
|
|
1489
|
+
if (this.debug) {
|
|
1490
|
+
console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
if (this.debug) {
|
|
1495
|
+
console.error(`❌ Failed to track ${eventType} event for ad ${adId}:`, error);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
else {
|
|
1500
|
+
if (this.debug) {
|
|
1501
|
+
console.warn(`⚠️ AdvertisementEventTracker not available for ${eventType} event`);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Fallback 광고 렌더링 - 공통 구현
|
|
1508
|
+
*/
|
|
1509
|
+
renderFallback(slot) {
|
|
1510
|
+
const element = document.getElementById(slot.id);
|
|
1511
|
+
if (element) {
|
|
1512
|
+
const adstageContainers = [
|
|
1513
|
+
element.querySelector('[data-adstage-container="true"]'),
|
|
1514
|
+
element.closest('[data-adstage-container="true"]'),
|
|
1515
|
+
element
|
|
1516
|
+
].filter(el => el && el.hasAttribute('data-adstage-container'));
|
|
1517
|
+
const classBasedContainers = [
|
|
1518
|
+
element.closest('.adstage-slot'),
|
|
1519
|
+
element.closest(`.adstage-${String(this.adType).toLowerCase()}`),
|
|
1520
|
+
element.closest('[class*="ad"]'),
|
|
1521
|
+
element.closest('[class*="banner"]'),
|
|
1522
|
+
element.closest('[class*="container"]'),
|
|
1523
|
+
element.closest('div[style*="height"]'),
|
|
1524
|
+
element.closest('div[style*="min-height"]'),
|
|
1525
|
+
element.parentElement
|
|
1526
|
+
].filter(Boolean);
|
|
1527
|
+
const possibleContainers = [...adstageContainers, ...classBasedContainers];
|
|
1528
|
+
const targetContainer = possibleContainers[0];
|
|
1529
|
+
if (targetContainer) {
|
|
1530
|
+
let containerType = 'unknown';
|
|
1531
|
+
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
1532
|
+
containerType = 'adstage-official';
|
|
1533
|
+
}
|
|
1534
|
+
else if (targetContainer.classList.contains('adstage-slot')) {
|
|
1535
|
+
containerType = 'adstage-class';
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
containerType = 'generic';
|
|
1539
|
+
}
|
|
1540
|
+
targetContainer.style.cssText += `
|
|
1541
|
+
height: 0px !important;
|
|
1542
|
+
min-height: 0px !important;
|
|
1543
|
+
padding: 0px !important;
|
|
1544
|
+
margin: 0px !important;
|
|
1545
|
+
border: none !important;
|
|
1546
|
+
overflow: hidden !important;
|
|
1547
|
+
display: block !important;
|
|
1548
|
+
`;
|
|
1549
|
+
targetContainer.innerHTML = '';
|
|
1550
|
+
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
1551
|
+
if (this.debug) {
|
|
1552
|
+
console.warn(`⚠️ ${this.adType} container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
this.createEmptyContainer(slot);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
slot.advertisement = undefined;
|
|
1560
|
+
slot.isEmpty = true;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* 빈 컨테이너 생성 - 공통 구현
|
|
1564
|
+
*/
|
|
1565
|
+
createEmptyContainer(slot) {
|
|
1566
|
+
const originalContainer = document.getElementById(slot.containerId);
|
|
1567
|
+
if (originalContainer) {
|
|
1568
|
+
originalContainer.innerHTML = '';
|
|
1569
|
+
const emptyElement = document.createElement('div');
|
|
1570
|
+
emptyElement.id = slot.id;
|
|
1571
|
+
emptyElement.className = `adstage-slot adstage-empty adstage-${String(this.adType).toLowerCase()}`;
|
|
1572
|
+
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
1573
|
+
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
1574
|
+
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
1575
|
+
emptyElement.style.cssText = `
|
|
1576
|
+
height: 0px !important;
|
|
1577
|
+
min-height: 0px !important;
|
|
1578
|
+
padding: 0px !important;
|
|
1579
|
+
margin: 0px !important;
|
|
1580
|
+
border: none !important;
|
|
1581
|
+
overflow: hidden !important;
|
|
1582
|
+
display: block !important;
|
|
1583
|
+
`;
|
|
1584
|
+
originalContainer.appendChild(emptyElement);
|
|
1585
|
+
if (this.debug) {
|
|
1586
|
+
console.warn(`⚠️ Created empty ${this.adType} container: ${slot.id}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* 디버그 로그 출력 - 공통 구현
|
|
1592
|
+
*/
|
|
1593
|
+
log(message, ...args) {
|
|
1594
|
+
if (this.debug) {
|
|
1595
|
+
console.log(`[${this.adType}] ${message}`, ...args);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
import { AdSlot, Advertisement, AdType } from '../../../types/advertisement';
|
|
1602
|
+
import { AdvertisementEventTracker } from '../../../managers/ads/advertisement-event-tracker';
|
|
1603
|
+
import { CarouselSliderManager } from '../../../managers/ads/carousel-slider-manager';
|
|
1604
|
+
import { BaseAdRenderer } from './base-ad-renderer';
|
|
1605
|
+
import { AdRenderOptions } from '../interfaces/i-ad-renderer'; 광고 전용 렌더러
|
|
1606
|
+
*/
|
|
1607
|
+
class BannerAdRenderer extends BaseAdRenderer {
|
|
1608
|
+
constructor(debug = false, advertisementEventTracker) {
|
|
1609
|
+
super(AdType.BANNER, debug, advertisementEventTracker);
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* 배너 광고 기본 높이
|
|
1613
|
+
*/
|
|
1614
|
+
getDefaultHeight() {
|
|
1615
|
+
return '250px';
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* 단일 배너 광고 렌더링
|
|
1619
|
+
*/
|
|
1620
|
+
async renderAdElement(slot, advertisement) {
|
|
1621
|
+
const container = document.getElementById(slot.containerId);
|
|
1622
|
+
if (!container)
|
|
1623
|
+
return;
|
|
1624
|
+
const adElement = document.createElement('div');
|
|
1625
|
+
adElement.className = 'adstage-ad adstage-banner-ad';
|
|
1626
|
+
const optimizedHeight = slot.optimizedHeight;
|
|
1627
|
+
const containerElement = container.parentElement || container;
|
|
1628
|
+
if (optimizedHeight) {
|
|
1629
|
+
adElement.style.width = '100%';
|
|
1630
|
+
adElement.style.height = String(optimizedHeight);
|
|
1631
|
+
}
|
|
1632
|
+
else {
|
|
1633
|
+
const config = slot.config;
|
|
1634
|
+
const options = {
|
|
1635
|
+
width: config?.width,
|
|
1636
|
+
height: config?.height
|
|
1637
|
+
};
|
|
1638
|
+
const { width, height } = this.calculateAdSize(containerElement, options, { debug: this.debug });
|
|
1639
|
+
adElement.style.width = width;
|
|
1640
|
+
adElement.style.height = height;
|
|
1641
|
+
}
|
|
1642
|
+
if (advertisement.imageUrl) {
|
|
1643
|
+
await this.renderOptimizedBannerImage(adElement, advertisement, slot);
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
adElement.innerHTML = `<div>${advertisement.title || 'Banner Ad'}</div>`;
|
|
1647
|
+
}
|
|
1648
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
1649
|
+
AdClickHandler.addClickEventForRenderer(adElement, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Banner');
|
|
1650
|
+
container.innerHTML = '';
|
|
1651
|
+
container.appendChild(adElement);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* 다중 배너 광고 렌더링 (슬라이더)
|
|
1655
|
+
*/
|
|
1656
|
+
async renderMultipleAds(slot, advertisements) {
|
|
1657
|
+
const container = document.getElementById(slot.containerId);
|
|
1658
|
+
if (!container) {
|
|
1659
|
+
throw new Error(`Container not found: ${slot.containerId}`);
|
|
1660
|
+
}
|
|
1661
|
+
// 배너 광고를 위한 컨테이너 최적화
|
|
1662
|
+
await this.optimizeContainerForBannerAds(slot, advertisements);
|
|
1663
|
+
const trackEventCallback = this.createEventTrackingCallback();
|
|
1664
|
+
const optimizedSliderOptions = {
|
|
1665
|
+
autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
|
|
1666
|
+
...slot.config,
|
|
1667
|
+
optimizedHeight: slot.optimizedHeight,
|
|
1668
|
+
aspectRatio: slot.aspectRatio
|
|
1669
|
+
};
|
|
1670
|
+
const sliderElement = CarouselSliderManager.createSliderContainer(slot, advertisements, optimizedSliderOptions, trackEventCallback, this.debug);
|
|
1671
|
+
if (sliderElement) {
|
|
1672
|
+
container.innerHTML = '';
|
|
1673
|
+
container.appendChild(sliderElement);
|
|
1674
|
+
if (this.debug) {
|
|
1675
|
+
console.log(`🎠 Banner carousel created for slot: ${slot.id} with ${advertisements.length} ads (optimized: ${slot.optimizedHeight || 'default'})`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* 여러 광고의 최적 컨테이너 크기 계산
|
|
1681
|
+
*/
|
|
1682
|
+
async calculateOptimalContainerSize(advertisements, containerWidth) {
|
|
1683
|
+
if (!advertisements.length) {
|
|
1684
|
+
return {
|
|
1685
|
+
width: '100%',
|
|
1686
|
+
height: this.getDefaultHeight(),
|
|
1687
|
+
aspectRatio: 16 / 9
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
const imageDimensions = await Promise.allSettled(advertisements
|
|
1692
|
+
.filter(ad => ad.imageUrl)
|
|
1693
|
+
.map(ad => this.loadImageDimensions(ad.imageUrl)));
|
|
1694
|
+
const validDimensions = imageDimensions
|
|
1695
|
+
.filter((result) => result.status === 'fulfilled')
|
|
1696
|
+
.map(result => result.value);
|
|
1697
|
+
if (validDimensions.length === 0) {
|
|
1698
|
+
return {
|
|
1699
|
+
width: '100%',
|
|
1700
|
+
height: this.getDefaultHeight(),
|
|
1701
|
+
aspectRatio: 16 / 9
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
const strategy = this.selectOptimalSizeStrategy(validDimensions);
|
|
1705
|
+
const optimalHeight = this.calculateOptimalHeight(validDimensions, containerWidth, strategy);
|
|
1706
|
+
if (this.debug) {
|
|
1707
|
+
console.log(`📐 Optimal banner container calculated: ${containerWidth}x${optimalHeight} (strategy: ${strategy})`);
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
width: '100%',
|
|
1711
|
+
height: `${optimalHeight}px`,
|
|
1712
|
+
aspectRatio: containerWidth / optimalHeight
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
console.warn('Failed to calculate optimal banner size, using defaults:', error);
|
|
1717
|
+
return {
|
|
1718
|
+
width: '100%',
|
|
1719
|
+
height: this.getDefaultHeight(),
|
|
1720
|
+
aspectRatio: 16 / 9
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* 배너 광고를 위한 컨테이너 최적화
|
|
1726
|
+
*/
|
|
1727
|
+
async optimizeContainerForBannerAds(slot, advertisements) {
|
|
1728
|
+
try {
|
|
1729
|
+
const container = document.getElementById(slot.containerId);
|
|
1730
|
+
const adElement = document.getElementById(slot.id);
|
|
1731
|
+
if (!container || !adElement)
|
|
1732
|
+
return;
|
|
1733
|
+
const containerWidth = container.getBoundingClientRect().width || 300;
|
|
1734
|
+
const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth);
|
|
1735
|
+
adElement.style.height = optimalSize.height;
|
|
1736
|
+
slot.optimizedHeight = optimalSize.height;
|
|
1737
|
+
slot.aspectRatio = optimalSize.aspectRatio;
|
|
1738
|
+
if (this.debug) {
|
|
1739
|
+
console.log(`🔧 Banner container optimized for ${advertisements.length} ads: ${optimalSize.height}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
console.warn('Banner container optimization failed, using default size:', error);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* 최적 크기 조정 전략 선택
|
|
1748
|
+
*/
|
|
1749
|
+
selectOptimalSizeStrategy(dimensions) {
|
|
1750
|
+
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
1751
|
+
const ratioGroups = new Map();
|
|
1752
|
+
aspectRatios.forEach(ratio => {
|
|
1753
|
+
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
1754
|
+
const key = roundedRatio.toString();
|
|
1755
|
+
ratioGroups.set(key, (ratioGroups.get(key) || 0) + 1);
|
|
1756
|
+
});
|
|
1757
|
+
const maxGroup = Math.max(...ratioGroups.values());
|
|
1758
|
+
const totalImages = dimensions.length;
|
|
1759
|
+
if (maxGroup / totalImages >= 0.7) {
|
|
1760
|
+
return 'dominant';
|
|
1761
|
+
}
|
|
1762
|
+
const standardRatios = [16 / 9, 4 / 3, 1 / 1, 3 / 2];
|
|
1763
|
+
const standardCount = aspectRatios.filter(ratio => standardRatios.some(standard => Math.abs(ratio - standard) < 0.1)).length;
|
|
1764
|
+
if (standardCount / totalImages >= 0.5) {
|
|
1765
|
+
return 'common';
|
|
1766
|
+
}
|
|
1767
|
+
return 'average';
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* 전략에 따른 최적 높이 계산
|
|
1771
|
+
*/
|
|
1772
|
+
calculateOptimalHeight(dimensions, containerWidth, strategy) {
|
|
1773
|
+
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
1774
|
+
switch (strategy) {
|
|
1775
|
+
case 'dominant': {
|
|
1776
|
+
const ratioGroups = new Map();
|
|
1777
|
+
aspectRatios.forEach(ratio => {
|
|
1778
|
+
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
1779
|
+
const key = roundedRatio.toString();
|
|
1780
|
+
const existing = ratioGroups.get(key);
|
|
1781
|
+
if (existing) {
|
|
1782
|
+
existing.count++;
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
const dominantGroup = Array.from(ratioGroups.values()).reduce((max, current) => current.count > max.count ? current : max);
|
|
1789
|
+
return Math.round(containerWidth / dominantGroup.ratio);
|
|
1790
|
+
}
|
|
1791
|
+
case 'common': {
|
|
1792
|
+
const standardRatios = [
|
|
1793
|
+
{ ratio: 16 / 9, name: '16:9' },
|
|
1794
|
+
{ ratio: 4 / 3, name: '4:3' },
|
|
1795
|
+
{ ratio: 1 / 1, name: '1:1' },
|
|
1796
|
+
{ ratio: 3 / 2, name: '3:2' }
|
|
1797
|
+
];
|
|
1798
|
+
const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
1799
|
+
const bestStandard = standardRatios.reduce((best, current) => Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best);
|
|
1800
|
+
if (this.debug) {
|
|
1801
|
+
console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
|
|
1802
|
+
}
|
|
1803
|
+
return Math.round(containerWidth / bestStandard.ratio);
|
|
1804
|
+
}
|
|
1805
|
+
case 'average':
|
|
1806
|
+
default: {
|
|
1807
|
+
const averageRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
1808
|
+
return Math.round(containerWidth / averageRatio);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* 이미지 로드 및 실제 크기 획득
|
|
1814
|
+
*/
|
|
1815
|
+
loadImageDimensions(imageUrl) {
|
|
1816
|
+
return new Promise((resolve, reject) => {
|
|
1817
|
+
const img = new Image();
|
|
1818
|
+
img.onload = () => {
|
|
1819
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
1820
|
+
};
|
|
1821
|
+
img.onerror = () => {
|
|
1822
|
+
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
1823
|
+
};
|
|
1824
|
+
img.src = imageUrl;
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* 이미지와 컨테이너 비율을 고려한 최적화 스타일 적용
|
|
1829
|
+
*/
|
|
1830
|
+
applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio) {
|
|
1831
|
+
const ratio = imageAspectRatio / containerAspectRatio;
|
|
1832
|
+
if (Math.abs(ratio - 1) < 0.1) {
|
|
1833
|
+
img.style.objectFit = 'cover';
|
|
1834
|
+
img.style.objectPosition = 'center';
|
|
1835
|
+
}
|
|
1836
|
+
else if (ratio > 1.3) {
|
|
1837
|
+
img.style.objectFit = 'contain';
|
|
1838
|
+
img.style.objectPosition = 'center';
|
|
1839
|
+
img.style.backgroundColor = '#f0f0f0';
|
|
1840
|
+
}
|
|
1841
|
+
else if (ratio < 0.7) {
|
|
1842
|
+
img.style.objectFit = 'cover';
|
|
1843
|
+
img.style.objectPosition = 'center';
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
img.style.objectFit = 'cover';
|
|
1847
|
+
img.style.objectPosition = 'center';
|
|
1848
|
+
}
|
|
1849
|
+
if (this.debug) {
|
|
1850
|
+
console.log(`🎨 Banner image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* 배너 이미지 최적화 렌더링 - public으로 변경
|
|
1855
|
+
*/
|
|
1856
|
+
async renderOptimizedBannerImage(container, advertisement, slot) {
|
|
1857
|
+
const img = document.createElement('img');
|
|
1858
|
+
img.style.width = '100%';
|
|
1859
|
+
img.style.height = '100%';
|
|
1860
|
+
img.style.display = 'block';
|
|
1861
|
+
img.style.borderRadius = '8px';
|
|
1862
|
+
img.alt = advertisement.title || 'Banner Advertisement';
|
|
1863
|
+
container.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">Loading...</div>';
|
|
1864
|
+
try {
|
|
1865
|
+
if (!advertisement.imageUrl) {
|
|
1866
|
+
throw new Error('Image URL is not provided');
|
|
1867
|
+
}
|
|
1868
|
+
const imageDimensions = await this.loadImageDimensions(advertisement.imageUrl);
|
|
1869
|
+
const containerRect = container.getBoundingClientRect();
|
|
1870
|
+
const containerWidth = containerRect.width;
|
|
1871
|
+
const containerHeight = containerRect.height;
|
|
1872
|
+
if (this.debug) {
|
|
1873
|
+
console.log(`📸 Banner image dimensions: ${imageDimensions.width}x${imageDimensions.height}`);
|
|
1874
|
+
console.log(`📦 Banner container dimensions: ${containerWidth}x${containerHeight}`);
|
|
1875
|
+
}
|
|
1876
|
+
const imageAspectRatio = imageDimensions.width / imageDimensions.height;
|
|
1877
|
+
const containerAspectRatio = containerWidth / containerHeight;
|
|
1878
|
+
this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
|
|
1879
|
+
img.src = advertisement.imageUrl;
|
|
1880
|
+
container.innerHTML = '';
|
|
1881
|
+
container.appendChild(img);
|
|
1882
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
1883
|
+
AdClickHandler.addClickEventForRenderer(img, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Banner');
|
|
1884
|
+
if (this.debug) {
|
|
1885
|
+
console.log(`✅ Optimized banner image rendered for ad: ${advertisement._id}`);
|
|
1886
|
+
}
|
|
1887
|
+
return img;
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
console.error('❌ Failed to load optimized banner image:', error);
|
|
1891
|
+
if (advertisement.imageUrl) {
|
|
1892
|
+
img.src = advertisement.imageUrl;
|
|
1893
|
+
img.style.objectFit = 'cover';
|
|
1894
|
+
img.style.objectPosition = 'center';
|
|
1895
|
+
container.innerHTML = '';
|
|
1896
|
+
container.appendChild(img);
|
|
1897
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
1898
|
+
AdClickHandler.addClickEventForRenderer(img, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Banner');
|
|
1899
|
+
}
|
|
1900
|
+
return img;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* 텍스트 광고 공통 유틸리티 함수들
|
|
1907
|
+
*/
|
|
1908
|
+
class TextAdUtils {
|
|
1909
|
+
/**
|
|
1910
|
+
* 텍스트 광고의 기본 스타일을 생성
|
|
1911
|
+
* @param isSimple 간단한 스타일 여부
|
|
1912
|
+
* @returns CSS 스타일 문자열
|
|
1913
|
+
*/
|
|
1914
|
+
static createTextAdStyles(isSimple = false) {
|
|
1915
|
+
const baseStyles = `
|
|
1916
|
+
font-family: 'Arial', sans-serif;
|
|
1917
|
+
text-decoration: none;
|
|
1918
|
+
display: block;
|
|
1919
|
+
text-align: left;
|
|
1920
|
+
transition: color 0.3s ease;
|
|
1921
|
+
width: 100%;
|
|
1922
|
+
box-sizing: border-box;
|
|
1923
|
+
overflow: visible;
|
|
1924
|
+
padding-right: 2px;
|
|
1925
|
+
padding-left: 2px;
|
|
1926
|
+
white-space: nowrap;
|
|
1927
|
+
`;
|
|
1928
|
+
return baseStyles;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* 텍스트 광고의 콘텐츠를 설정 (textContent 우선, 없으면 title)
|
|
1932
|
+
* @param element 설정할 HTML 요소
|
|
1933
|
+
* @param adData 광고 데이터
|
|
1934
|
+
*/
|
|
1935
|
+
static setTextAdContent(element, adData) {
|
|
1936
|
+
const content = adData.textContent || adData.title || '';
|
|
1937
|
+
element.textContent = content;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* 텍스트 전환 효과 관리 클래스
|
|
1943
|
+
* - 텍스트 광고 전용 페이드 인/아웃 + 상하 움직임 효과
|
|
1944
|
+
* - 부드러운 전환 애니메이션 (vertical transition)
|
|
1945
|
+
* - 무한 루프 지원
|
|
1946
|
+
*/
|
|
1947
|
+
class TextTransitionManager {
|
|
1948
|
+
/**
|
|
1949
|
+
* 간단한 광고 요소 생성 (크기 측정용)
|
|
1950
|
+
*/
|
|
1951
|
+
static createSimpleAdElement(slot, advertisement) {
|
|
1952
|
+
const adElement = document.createElement('div');
|
|
1953
|
+
adElement.className = `adstage-ad adstage-${String(slot.adType).toLowerCase()}`;
|
|
1954
|
+
adElement.setAttribute('data-adstage-ad-id', advertisement._id);
|
|
1955
|
+
adElement.setAttribute('data-adstage-slot-id', slot.id);
|
|
1956
|
+
// 🎯 공통 스타일 및 콘텐츠 적용 (중복 제거)
|
|
1957
|
+
adElement.style.cssText = TextAdUtils.createTextAdStyles(true);
|
|
1958
|
+
TextAdUtils.setTextAdContent(adElement, advertisement);
|
|
1959
|
+
return adElement;
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* 텍스트 전환 컨테이너 생성
|
|
1963
|
+
*/
|
|
1964
|
+
static createTextTransitionContainer(slot, advertisements, options, trackEventCallback, debug = false) {
|
|
1965
|
+
// 사용자가 높이를 지정했는지 미리 확인 ('auto'나 undefined면 자동 높이)
|
|
1966
|
+
const hasUserDefinedHeight = slot.height && slot.height !== 0 && slot.height !== 'auto';
|
|
1967
|
+
const sliderWrapper = document.createElement('div');
|
|
1968
|
+
sliderWrapper.className = 'adstage-fade-slider-wrapper';
|
|
1969
|
+
// 래퍼 스타일 설정
|
|
1970
|
+
const containerStyles = {
|
|
1971
|
+
position: 'relative',
|
|
1972
|
+
overflow: 'hidden',
|
|
1973
|
+
display: 'block',
|
|
1974
|
+
};
|
|
1975
|
+
// 높이가 지정되지 않은 경우 자동 높이 사용
|
|
1976
|
+
if (!hasUserDefinedHeight) {
|
|
1977
|
+
containerStyles.height = 'auto';
|
|
1978
|
+
containerStyles.minHeight = 'fit-content';
|
|
1979
|
+
}
|
|
1980
|
+
// 사용자가 크기를 지정한 경우
|
|
1981
|
+
if (slot.width && slot.width !== 0) {
|
|
1982
|
+
let width;
|
|
1983
|
+
if (typeof slot.width === 'string') {
|
|
1984
|
+
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
width = `${slot.width}px`;
|
|
1988
|
+
}
|
|
1989
|
+
containerStyles.width = width;
|
|
1990
|
+
}
|
|
1991
|
+
// 사용자가 높이를 명시적으로 지정한 경우에만 적용 ('auto'는 제외)
|
|
1992
|
+
if (slot.height && slot.height !== 0 && slot.height !== 'auto' && hasUserDefinedHeight) {
|
|
1993
|
+
let height;
|
|
1994
|
+
if (typeof slot.height === 'string') {
|
|
1995
|
+
height = slot.height.includes('px') || slot.height.includes('%') ? slot.height : `${slot.height}px`;
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
height = `${slot.height}px`;
|
|
1999
|
+
}
|
|
2000
|
+
containerStyles.height = height;
|
|
2001
|
+
}
|
|
2002
|
+
// 스타일 적용
|
|
2003
|
+
Object.entries(containerStyles).forEach(([key, value]) => {
|
|
2004
|
+
sliderWrapper.style.setProperty(key, value);
|
|
1572
2005
|
});
|
|
1573
2006
|
// 슬라이드 컨테이너
|
|
1574
2007
|
const slideContainer = document.createElement('div');
|
|
@@ -1576,13 +2009,14 @@ class TextTransitionManager {
|
|
|
1576
2009
|
slideContainer.style.cssText = `
|
|
1577
2010
|
position: relative;
|
|
1578
2011
|
width: 100%;
|
|
1579
|
-
height: 100%;
|
|
2012
|
+
${hasUserDefinedHeight ? 'height: 100%;' : 'height: auto; min-height: fit-content;'}
|
|
1580
2013
|
`;
|
|
1581
2014
|
// 크기 측정을 위한 임시 컨테이너 (자동 크기 계산이 필요한 경우)
|
|
1582
2015
|
let measureContainer = null;
|
|
1583
2016
|
const needsWidthMeasurement = !slot.width || slot.width === 0;
|
|
1584
|
-
const needsHeightMeasurement = !slot.height || slot.height === 0;
|
|
1585
|
-
|
|
2017
|
+
const needsHeightMeasurement = !slot.height || slot.height === 0 || slot.height === undefined || slot.height === 'auto';
|
|
2018
|
+
// 너비 측정만 필요한 경우 (높이는 자동으로 유지)
|
|
2019
|
+
if (needsWidthMeasurement || (needsHeightMeasurement && hasUserDefinedHeight)) {
|
|
1586
2020
|
measureContainer = document.createElement('div');
|
|
1587
2021
|
measureContainer.style.cssText = `
|
|
1588
2022
|
position: absolute;
|
|
@@ -1608,7 +2042,7 @@ class TextTransitionManager {
|
|
|
1608
2042
|
let maxHeight = 0;
|
|
1609
2043
|
// 모든 광고의 크기를 측정하여 최대 크기 찾기
|
|
1610
2044
|
advertisements.forEach(ad => {
|
|
1611
|
-
const measureAdElement =
|
|
2045
|
+
const measureAdElement = this.createSimpleAdElement(slot, ad);
|
|
1612
2046
|
measureContainer.appendChild(measureAdElement);
|
|
1613
2047
|
const rect = measureAdElement.getBoundingClientRect();
|
|
1614
2048
|
if (rect.width > maxWidth)
|
|
@@ -1622,7 +2056,8 @@ class TextTransitionManager {
|
|
|
1622
2056
|
if (needsWidthMeasurement && maxWidth > 0) {
|
|
1623
2057
|
sliderWrapper.style.width = `${maxWidth}px`;
|
|
1624
2058
|
}
|
|
1625
|
-
|
|
2059
|
+
// 사용자가 높이를 지정하지 않은 경우 auto 높이 유지 (측정된 높이 적용하지 않음)
|
|
2060
|
+
if (needsHeightMeasurement && maxHeight > 0 && hasUserDefinedHeight) {
|
|
1626
2061
|
sliderWrapper.style.height = `${maxHeight}px`;
|
|
1627
2062
|
}
|
|
1628
2063
|
// 측정 컨테이너 제거
|
|
@@ -1633,22 +2068,27 @@ class TextTransitionManager {
|
|
|
1633
2068
|
advertisements.forEach((ad, index) => {
|
|
1634
2069
|
const slideElement = document.createElement('div');
|
|
1635
2070
|
slideElement.className = 'adstage-fade-slide';
|
|
2071
|
+
// 자동 높이일 때는 첫 번째 슬라이드만 relative로 높이 확보
|
|
2072
|
+
const isFirstSlide = index === 0;
|
|
2073
|
+
const positionStyle = hasUserDefinedHeight ? 'absolute' : (isFirstSlide ? 'relative' : 'absolute');
|
|
1636
2074
|
slideElement.style.cssText = `
|
|
1637
|
-
position:
|
|
1638
|
-
top: 0;
|
|
1639
|
-
left: 0;
|
|
2075
|
+
position: ${positionStyle};
|
|
2076
|
+
top: ${positionStyle === 'absolute' ? '0' : 'auto'};
|
|
2077
|
+
left: ${positionStyle === 'absolute' ? '0' : 'auto'};
|
|
1640
2078
|
width: 100%;
|
|
1641
|
-
height: 100%;
|
|
2079
|
+
${hasUserDefinedHeight ? 'height: 100%;' : 'height: auto;'}
|
|
1642
2080
|
display: flex;
|
|
1643
2081
|
align-items: center;
|
|
1644
|
-
justify-content:
|
|
2082
|
+
justify-content: flex-start;
|
|
1645
2083
|
opacity: ${index === 0 ? '1' : '0'};
|
|
1646
2084
|
transform: translateY(${index === 0 ? '0' : '20px'});
|
|
1647
2085
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1648
2086
|
z-index: ${index === 0 ? '2' : '1'};
|
|
1649
2087
|
`;
|
|
1650
|
-
// 광고 렌더링
|
|
1651
|
-
const adElement =
|
|
2088
|
+
// 광고 렌더링 - 공통 함수 사용으로 일관성 유지
|
|
2089
|
+
const adElement = this.createSimpleAdElement(slot, ad);
|
|
2090
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
2091
|
+
AdClickHandler.addClickEventForSlider(adElement, ad, slot, trackEventCallback, debug, 'Text');
|
|
1652
2092
|
slideElement.appendChild(adElement);
|
|
1653
2093
|
slideContainer.appendChild(slideElement);
|
|
1654
2094
|
slideElements.push(slideElement);
|
|
@@ -1667,6 +2107,19 @@ class TextTransitionManager {
|
|
|
1667
2107
|
}
|
|
1668
2108
|
const previousSlide = slideElements[currentSlide];
|
|
1669
2109
|
const nextSlide = slideElements[index];
|
|
2110
|
+
// 자동 높이 모드일 때는 위치 변경
|
|
2111
|
+
if (!hasUserDefinedHeight) {
|
|
2112
|
+
// 이전 슬라이드를 absolute로 변경
|
|
2113
|
+
if (previousSlide.style.position === 'relative') {
|
|
2114
|
+
previousSlide.style.position = 'absolute';
|
|
2115
|
+
previousSlide.style.top = '0';
|
|
2116
|
+
previousSlide.style.left = '0';
|
|
2117
|
+
}
|
|
2118
|
+
// 다음 슬라이드를 relative로 변경하여 높이 확보
|
|
2119
|
+
nextSlide.style.position = 'relative';
|
|
2120
|
+
nextSlide.style.top = 'auto';
|
|
2121
|
+
nextSlide.style.left = 'auto';
|
|
2122
|
+
}
|
|
1670
2123
|
// 이전 슬라이드 페이드 아웃 (아래로)
|
|
1671
2124
|
previousSlide.style.opacity = '0';
|
|
1672
2125
|
previousSlide.style.transform = 'translateY(-20px)';
|
|
@@ -1681,13 +2134,18 @@ class TextTransitionManager {
|
|
|
1681
2134
|
slide.style.opacity = '0';
|
|
1682
2135
|
slide.style.transform = 'translateY(20px)';
|
|
1683
2136
|
slide.style.zIndex = '1';
|
|
2137
|
+
// 자동 높이 모드일 때는 다른 슬라이드들을 absolute로
|
|
2138
|
+
if (!hasUserDefinedHeight && slide.style.position === 'relative') {
|
|
2139
|
+
slide.style.position = 'absolute';
|
|
2140
|
+
slide.style.top = '0';
|
|
2141
|
+
slide.style.left = '0';
|
|
2142
|
+
}
|
|
1684
2143
|
}
|
|
1685
2144
|
});
|
|
1686
2145
|
currentSlide = index;
|
|
1687
|
-
//
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
}
|
|
2146
|
+
// 🎯 공통 슬라이더 이벤트 추적 적용 (모든 슬라이드 포함)
|
|
2147
|
+
SliderEventTracker.trackSlideViewable(advertisements[currentSlide], slot, currentSlide, trackEventCallback, debug // debug 모드 상속
|
|
2148
|
+
);
|
|
1691
2149
|
};
|
|
1692
2150
|
// 자동 슬라이드
|
|
1693
2151
|
let autoSlideTimer = setInterval(() => {
|
|
@@ -1752,496 +2210,975 @@ class TextTransitionManager {
|
|
|
1752
2210
|
}
|
|
1753
2211
|
|
|
1754
2212
|
/**
|
|
1755
|
-
*
|
|
1756
|
-
* AdsModule에서 렌더링 관련 기능을 분리
|
|
2213
|
+
* 텍스트 광고 전용 렌더러
|
|
1757
2214
|
*/
|
|
1758
|
-
class
|
|
2215
|
+
class TextAdRenderer extends BaseAdRenderer {
|
|
1759
2216
|
constructor(debug = false, advertisementEventTracker) {
|
|
1760
|
-
|
|
1761
|
-
this.advertisementEventTracker = advertisementEventTracker || null;
|
|
2217
|
+
super(AdType.TEXT, debug, advertisementEventTracker);
|
|
1762
2218
|
}
|
|
1763
2219
|
/**
|
|
1764
|
-
*
|
|
2220
|
+
* 텍스트 광고 기본 높이
|
|
2221
|
+
*/
|
|
2222
|
+
getDefaultHeight() {
|
|
2223
|
+
return '60px';
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* 단일 텍스트 광고 렌더링
|
|
1765
2227
|
*/
|
|
1766
|
-
|
|
2228
|
+
async renderAdElement(slot, advertisement) {
|
|
2229
|
+
const container = document.getElementById(slot.containerId);
|
|
2230
|
+
if (!container)
|
|
2231
|
+
return;
|
|
1767
2232
|
const adElement = document.createElement('div');
|
|
1768
|
-
adElement.
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2233
|
+
adElement.className = 'adstage-ad adstage-text-ad';
|
|
2234
|
+
const optimizedHeight = slot.optimizedHeight;
|
|
2235
|
+
const containerElement = container.parentElement || container;
|
|
2236
|
+
if (optimizedHeight) {
|
|
2237
|
+
adElement.style.width = '100%';
|
|
2238
|
+
adElement.style.height = String(optimizedHeight);
|
|
2239
|
+
}
|
|
2240
|
+
else {
|
|
2241
|
+
const config = slot.config;
|
|
2242
|
+
const options = {
|
|
2243
|
+
width: config?.width,
|
|
2244
|
+
height: config?.height
|
|
2245
|
+
};
|
|
2246
|
+
const { width, height } = this.calculateAdSize(containerElement, options, { debug: this.debug });
|
|
2247
|
+
adElement.style.width = width;
|
|
2248
|
+
adElement.style.height = height;
|
|
2249
|
+
}
|
|
2250
|
+
// 텍스트 콘텐츠 렌더링
|
|
2251
|
+
const textDiv = document.createElement('div');
|
|
2252
|
+
textDiv.className = 'adstage-text-content';
|
|
2253
|
+
textDiv.style.cssText = TextAdUtils.createTextAdStyles(false);
|
|
2254
|
+
TextAdUtils.setTextAdContent(textDiv, advertisement); // 최대 라인 수 제한
|
|
2255
|
+
const maxLines = slot.config?.maxLines;
|
|
2256
|
+
if (maxLines && typeof maxLines === 'number') {
|
|
2257
|
+
textDiv.style.display = '-webkit-box';
|
|
2258
|
+
textDiv.style.webkitLineClamp = String(maxLines);
|
|
2259
|
+
textDiv.style.webkitBoxOrient = 'vertical';
|
|
2260
|
+
textDiv.style.overflow = 'hidden';
|
|
2261
|
+
}
|
|
2262
|
+
adElement.appendChild(textDiv);
|
|
2263
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
2264
|
+
AdClickHandler.addClickEventForRenderer(adElement, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Text');
|
|
2265
|
+
container.innerHTML = '';
|
|
1786
2266
|
container.appendChild(adElement);
|
|
1787
2267
|
if (this.debug) {
|
|
1788
|
-
console.log(
|
|
2268
|
+
console.log(`✨ Single text ad rendered: ${advertisement._id}`);
|
|
1789
2269
|
}
|
|
1790
2270
|
}
|
|
1791
2271
|
/**
|
|
1792
|
-
*
|
|
2272
|
+
* 다중 텍스트 광고 렌더링 (전환 효과)
|
|
1793
2273
|
*/
|
|
1794
|
-
async
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
height: this.getDefaultHeightForAdType(adType) || '250px',
|
|
1799
|
-
aspectRatio: 16 / 9
|
|
1800
|
-
};
|
|
2274
|
+
async renderMultipleAds(slot, advertisements) {
|
|
2275
|
+
const container = document.getElementById(slot.containerId);
|
|
2276
|
+
if (!container) {
|
|
2277
|
+
throw new Error(`Container not found: ${slot.containerId}`);
|
|
1801
2278
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
};
|
|
2279
|
+
const trackEventCallback = this.createEventTrackingCallback();
|
|
2280
|
+
const optimizedSliderOptions = {
|
|
2281
|
+
autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
|
|
2282
|
+
...slot.config,
|
|
2283
|
+
optimizedHeight: slot.optimizedHeight,
|
|
2284
|
+
aspectRatio: slot.aspectRatio
|
|
2285
|
+
};
|
|
2286
|
+
const sliderElement = TextTransitionManager.createTextTransitionContainer(slot, advertisements, optimizedSliderOptions, trackEventCallback, this.debug);
|
|
2287
|
+
if (sliderElement) {
|
|
2288
|
+
container.innerHTML = '';
|
|
2289
|
+
container.appendChild(sliderElement);
|
|
2290
|
+
if (this.debug) {
|
|
2291
|
+
console.log(`✨ Text transition created for slot: ${slot.id} with ${advertisements.length} ads`);
|
|
1815
2292
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* 비디오 광고 전용 렌더러
|
|
2299
|
+
*/
|
|
2300
|
+
class VideoAdRenderer extends BaseAdRenderer {
|
|
2301
|
+
constructor(debug = false, advertisementEventTracker) {
|
|
2302
|
+
super(AdType.VIDEO, debug, advertisementEventTracker);
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* 비디오 광고 기본 높이 (16:9 비율 고려)
|
|
2306
|
+
*/
|
|
2307
|
+
getDefaultHeight() {
|
|
2308
|
+
return '360px';
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* 단일 비디오 광고 렌더링
|
|
2312
|
+
*/
|
|
2313
|
+
async renderAdElement(slot, advertisement) {
|
|
2314
|
+
const container = document.getElementById(slot.containerId);
|
|
2315
|
+
if (!container)
|
|
2316
|
+
return;
|
|
2317
|
+
// 비디오 전용 컨테이너 생성
|
|
2318
|
+
const videoContainer = document.createElement('div');
|
|
2319
|
+
videoContainer.className = 'adstage-video-container';
|
|
2320
|
+
videoContainer.style.cssText = `
|
|
2321
|
+
width: 100%;
|
|
2322
|
+
height: 100%;
|
|
2323
|
+
display: flex;
|
|
2324
|
+
align-items: center;
|
|
2325
|
+
justify-content: center;
|
|
2326
|
+
background: #000;
|
|
2327
|
+
border-radius: 8px;
|
|
2328
|
+
overflow: hidden;
|
|
2329
|
+
`;
|
|
2330
|
+
// 비디오 요소 렌더링
|
|
2331
|
+
this.renderVideoElementDirect(videoContainer, advertisement, slot);
|
|
2332
|
+
container.innerHTML = '';
|
|
2333
|
+
container.appendChild(videoContainer);
|
|
2334
|
+
if (this.debug) {
|
|
2335
|
+
console.log(`🎬 Single video ad rendered: ${advertisement._id}`);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* 다중 비디오 광고 렌더링 - 비디오는 단일 렌더링만 지원
|
|
2340
|
+
*/
|
|
2341
|
+
async renderMultipleAds(slot, advertisements) {
|
|
2342
|
+
const container = document.getElementById(slot.containerId);
|
|
2343
|
+
if (!container) {
|
|
2344
|
+
throw new Error(`Container not found: ${slot.containerId}`);
|
|
2345
|
+
}
|
|
2346
|
+
if (advertisements.length > 0) {
|
|
2347
|
+
const videoAd = advertisements[0]; // 첫 번째 비디오만 사용
|
|
2348
|
+
// 비디오 전용 컨테이너 생성
|
|
2349
|
+
const videoContainer = document.createElement('div');
|
|
2350
|
+
videoContainer.className = 'adstage-video-container';
|
|
2351
|
+
videoContainer.style.cssText = `
|
|
2352
|
+
width: 100%;
|
|
2353
|
+
height: 100%;
|
|
2354
|
+
display: flex;
|
|
2355
|
+
align-items: center;
|
|
2356
|
+
justify-content: center;
|
|
2357
|
+
background: #000;
|
|
2358
|
+
border-radius: 8px;
|
|
2359
|
+
overflow: hidden;
|
|
2360
|
+
`;
|
|
2361
|
+
// 비디오 요소 렌더링
|
|
2362
|
+
this.renderVideoElementDirect(videoContainer, videoAd, slot);
|
|
2363
|
+
// 컨테이너에 추가
|
|
2364
|
+
container.innerHTML = '';
|
|
2365
|
+
container.appendChild(videoContainer);
|
|
2366
|
+
// VIEWABLE 이벤트 추적
|
|
2367
|
+
const trackEventCallback = this.createEventTrackingCallback();
|
|
2368
|
+
setTimeout(() => {
|
|
2369
|
+
trackEventCallback(videoAd._id, slot.id, AdEventType.VIEWABLE);
|
|
2370
|
+
}, 100);
|
|
1818
2371
|
if (this.debug) {
|
|
1819
|
-
console.log(
|
|
2372
|
+
console.log(`🎬 Single video rendered for VIDEO slot: ${slot.id} (${videoAd._id})`);
|
|
1820
2373
|
}
|
|
1821
|
-
return {
|
|
1822
|
-
width: '100%',
|
|
1823
|
-
height: `${optimalHeight}px`,
|
|
1824
|
-
aspectRatio: containerWidth / optimalHeight
|
|
1825
|
-
};
|
|
1826
2374
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
height: this.getDefaultHeightForAdType(adType) || '250px',
|
|
1832
|
-
aspectRatio: 16 / 9
|
|
1833
|
-
};
|
|
2375
|
+
else {
|
|
2376
|
+
if (this.debug) {
|
|
2377
|
+
console.warn(`⚠️ No video advertisements available for slot: ${slot.id}`);
|
|
2378
|
+
}
|
|
1834
2379
|
}
|
|
1835
2380
|
}
|
|
1836
2381
|
/**
|
|
1837
|
-
*
|
|
2382
|
+
* 비디오 요소 직접 렌더링
|
|
1838
2383
|
*/
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2384
|
+
renderVideoElementDirect(container, advertisement, slot) {
|
|
2385
|
+
// 비디오 컨테이너를 relative로 설정
|
|
2386
|
+
container.style.position = 'relative';
|
|
2387
|
+
const video = document.createElement('video');
|
|
2388
|
+
// 비디오 기본 설정
|
|
2389
|
+
video.style.width = '100%';
|
|
2390
|
+
video.style.height = '100%';
|
|
2391
|
+
video.style.objectFit = 'contain';
|
|
2392
|
+
video.preload = 'metadata';
|
|
2393
|
+
// 슬롯 설정에서 비디오 옵션들 확인 (기본값 적용)
|
|
2394
|
+
const config = slot.config;
|
|
2395
|
+
// 디버깅: config 내용 확인
|
|
2396
|
+
if (this.debug) {
|
|
2397
|
+
console.log('🎬 Video config received:', config);
|
|
2398
|
+
}
|
|
2399
|
+
// 기본 설정: 모든 컨트롤 숨김 (사용자 요구사항 - config에 상관없이 기본값 적용)
|
|
2400
|
+
video.controls = false;
|
|
2401
|
+
// 기본 설정: 자동 재생 true (config에 상관없이 기본값 적용, 단 사용자가 명시적으로 false 설정시 존중)
|
|
2402
|
+
video.autoplay = config?.autoplay === false ? false : true;
|
|
2403
|
+
// 기본 설정: 음소거 true (기본값 강제 적용)
|
|
2404
|
+
video.muted = true;
|
|
2405
|
+
// 기본 설정: 반복 재생 true (config에 상관없이 기본값 적용, 단 사용자가 명시적으로 false 설정시 존중)
|
|
2406
|
+
video.loop = config?.loop === false ? false : true;
|
|
2407
|
+
// 디버깅: 최종 비디오 설정 확인
|
|
2408
|
+
if (this.debug) {
|
|
2409
|
+
console.log('🎬 Final video settings:', {
|
|
2410
|
+
autoplay: video.autoplay,
|
|
2411
|
+
muted: video.muted,
|
|
2412
|
+
loop: video.loop,
|
|
2413
|
+
controls: video.controls
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
// playsinline 설정 (모바일에서 전체화면 방지)
|
|
2417
|
+
if (config?.playsinline !== false) {
|
|
2418
|
+
video.setAttribute('playsinline', '');
|
|
2419
|
+
}
|
|
2420
|
+
// 사용자가 명시적으로 controls=true를 설정한 경우에만 오버라이드 (첫 번째 비디오는 기본값 유지)
|
|
2421
|
+
if (config?.controls === true) {
|
|
2422
|
+
video.controls = true;
|
|
2423
|
+
if (this.debug) {
|
|
2424
|
+
console.log('🎬 User explicitly enabled controls, overriding default');
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
// 특별한 컨트롤 설정 처리
|
|
2428
|
+
if (config?.hideControls) {
|
|
2429
|
+
// 완전히 모든 컨트롤 숨김 (pointer-events까지 차단)
|
|
2430
|
+
video.controls = false;
|
|
2431
|
+
video.style.cssText += `
|
|
2432
|
+
pointer-events: none;
|
|
2433
|
+
`;
|
|
2434
|
+
}
|
|
2435
|
+
else if (config?.customControls) {
|
|
2436
|
+
// controls가 true일 때만 특정 컨트롤 숨기기 (CSS로 처리)
|
|
2437
|
+
if (video.controls) {
|
|
2438
|
+
const customControlsStyle = document.createElement('style');
|
|
2439
|
+
customControlsStyle.textContent = `
|
|
2440
|
+
video::-webkit-media-controls-play-button {
|
|
2441
|
+
display: ${config.customControls.hidePlayButton ? 'none' : 'block'} !important;
|
|
2442
|
+
}
|
|
2443
|
+
video::-webkit-media-controls-timeline {
|
|
2444
|
+
display: ${config.customControls.hideProgressBar ? 'none' : 'block'} !important;
|
|
2445
|
+
}
|
|
2446
|
+
video::-webkit-media-controls-current-time-display {
|
|
2447
|
+
display: ${config.customControls.hideCurrentTime ? 'none' : 'block'} !important;
|
|
2448
|
+
}
|
|
2449
|
+
video::-webkit-media-controls-time-remaining-display {
|
|
2450
|
+
display: ${config.customControls.hideRemainingTime ? 'none' : 'block'} !important;
|
|
2451
|
+
}
|
|
2452
|
+
video::-webkit-media-controls-volume-slider {
|
|
2453
|
+
display: ${config.customControls.hideVolumeSlider ? 'none' : 'block'} !important;
|
|
2454
|
+
}
|
|
2455
|
+
video::-webkit-media-controls-mute-button {
|
|
2456
|
+
display: ${config.customControls.hideMuteButton ? 'none' : 'block'} !important;
|
|
2457
|
+
}
|
|
2458
|
+
video::-webkit-media-controls-fullscreen-button {
|
|
2459
|
+
display: ${config.customControls.hideFullscreenButton ? 'none' : 'block'} !important;
|
|
2460
|
+
}
|
|
2461
|
+
`;
|
|
2462
|
+
document.head.appendChild(customControlsStyle);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
else if (video.controls) {
|
|
2466
|
+
// controls=true이지만 특별한 설정이 없을 때, 기본 스타일 적용 (시간만 표시)
|
|
2467
|
+
const defaultControlsStyle = document.createElement('style');
|
|
2468
|
+
defaultControlsStyle.id = 'adstage-video-default-controls';
|
|
2469
|
+
defaultControlsStyle.textContent = `
|
|
2470
|
+
.adstage-video-container video::-webkit-media-controls-mute-button {
|
|
2471
|
+
display: none !important;
|
|
2472
|
+
}
|
|
2473
|
+
.adstage-video-container video::-webkit-media-controls-fullscreen-button {
|
|
2474
|
+
display: none !important;
|
|
2475
|
+
}
|
|
2476
|
+
.adstage-video-container video::-webkit-media-controls-toggle-closed-captions-button {
|
|
2477
|
+
display: none !important;
|
|
2478
|
+
}
|
|
2479
|
+
.adstage-video-container video::-webkit-media-controls-volume-slider {
|
|
2480
|
+
display: none !important;
|
|
2481
|
+
}
|
|
2482
|
+
.adstage-video-container video::-webkit-media-controls-overflow-button {
|
|
2483
|
+
display: none !important;
|
|
2484
|
+
}
|
|
2485
|
+
.adstage-video-container video::-webkit-media-controls-picture-in-picture-button {
|
|
2486
|
+
display: none !important;
|
|
2487
|
+
}
|
|
2488
|
+
video::-webkit-media-controls-mute-button {
|
|
2489
|
+
display: none !important;
|
|
2490
|
+
}
|
|
2491
|
+
video::-webkit-media-controls-fullscreen-button {
|
|
2492
|
+
display: none !important;
|
|
2493
|
+
}
|
|
2494
|
+
video::-webkit-media-controls-toggle-closed-captions-button {
|
|
2495
|
+
display: none !important;
|
|
2496
|
+
}
|
|
2497
|
+
video::-webkit-media-controls-volume-slider {
|
|
2498
|
+
display: none !important;
|
|
2499
|
+
}
|
|
2500
|
+
video::-webkit-media-controls-overflow-button {
|
|
2501
|
+
display: none !important;
|
|
2502
|
+
}
|
|
2503
|
+
video::-webkit-media-controls-picture-in-picture-button {
|
|
2504
|
+
display: none !important;
|
|
2505
|
+
}
|
|
2506
|
+
`;
|
|
2507
|
+
// 스타일이 이미 존재하지 않으면 추가
|
|
2508
|
+
if (!document.getElementById('adstage-video-default-controls')) {
|
|
2509
|
+
document.head.appendChild(defaultControlsStyle);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
// 기본값이 controls=false이므로 아무것도 표시하지 않음
|
|
2513
|
+
if (this.debug) {
|
|
2514
|
+
console.log('🎬 Video controls setting:', video.controls ? 'enabled' : 'disabled (default)');
|
|
2515
|
+
}
|
|
2516
|
+
// 커스텀 음소거 토글 버튼 생성 (기본적으로 항상 표시)
|
|
2517
|
+
const muteButton = document.createElement('button');
|
|
2518
|
+
muteButton.className = 'adstage-video-mute-button';
|
|
2519
|
+
muteButton.style.cssText = `
|
|
2520
|
+
position: absolute;
|
|
2521
|
+
top: 12px;
|
|
2522
|
+
left: 12px;
|
|
2523
|
+
width: 40px;
|
|
2524
|
+
height: 40px;
|
|
2525
|
+
border: none;
|
|
2526
|
+
border-radius: 50%;
|
|
2527
|
+
background: rgba(0, 0, 0, 0.7);
|
|
2528
|
+
color: white;
|
|
2529
|
+
font-size: 16px;
|
|
2530
|
+
cursor: pointer;
|
|
2531
|
+
display: flex;
|
|
2532
|
+
align-items: center;
|
|
2533
|
+
justify-content: center;
|
|
2534
|
+
z-index: 10;
|
|
2535
|
+
transition: all 0.3s ease;
|
|
2536
|
+
backdrop-filter: blur(4px);
|
|
2537
|
+
`;
|
|
2538
|
+
// hideControls가 true면 음소거 버튼도 숨김
|
|
2539
|
+
if (config?.hideControls) {
|
|
2540
|
+
muteButton.style.display = 'none';
|
|
2541
|
+
}
|
|
2542
|
+
// 음소거 상태에 따른 아이콘 업데이트
|
|
2543
|
+
const updateMuteButtonIcon = () => {
|
|
2544
|
+
const mutedIcon = `
|
|
2545
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
|
2546
|
+
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
|
2547
|
+
</svg>
|
|
2548
|
+
`;
|
|
2549
|
+
const unmutedIcon = `
|
|
2550
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
|
2551
|
+
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
|
2552
|
+
</svg>
|
|
2553
|
+
`;
|
|
2554
|
+
muteButton.innerHTML = video.muted ? mutedIcon : unmutedIcon;
|
|
2555
|
+
muteButton.title = video.muted ? 'Click to unmute' : 'Click to mute';
|
|
2556
|
+
};
|
|
2557
|
+
// 초기 아이콘 설정
|
|
2558
|
+
updateMuteButtonIcon();
|
|
2559
|
+
// 음소거 버튼 클릭 이벤트
|
|
2560
|
+
muteButton.addEventListener('click', (e) => {
|
|
2561
|
+
e.stopPropagation(); // 비디오 클릭 이벤트 방지
|
|
2562
|
+
video.muted = !video.muted;
|
|
2563
|
+
updateMuteButtonIcon();
|
|
2564
|
+
if (this.debug) {
|
|
2565
|
+
console.log(`🔊 Video mute toggled: ${video.muted ? 'muted' : 'unmuted'}`);
|
|
2566
|
+
}
|
|
1846
2567
|
});
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2568
|
+
// 마우스 호버 효과
|
|
2569
|
+
muteButton.addEventListener('mouseenter', () => {
|
|
2570
|
+
muteButton.style.background = 'rgba(0, 0, 0, 0.9)';
|
|
2571
|
+
muteButton.style.transform = 'scale(1.1)';
|
|
2572
|
+
});
|
|
2573
|
+
muteButton.addEventListener('mouseleave', () => {
|
|
2574
|
+
muteButton.style.background = 'rgba(0, 0, 0, 0.7)';
|
|
2575
|
+
muteButton.style.transform = 'scale(1)';
|
|
2576
|
+
});
|
|
2577
|
+
// 비디오 URL 설정
|
|
2578
|
+
if (advertisement.videoUrl) {
|
|
2579
|
+
video.src = advertisement.videoUrl;
|
|
2580
|
+
// 자동 재생을 위한 다단계 시도
|
|
2581
|
+
const attemptAutoplay = () => {
|
|
2582
|
+
if (video.autoplay && video.muted && video.paused) {
|
|
2583
|
+
video.play().catch(error => {
|
|
2584
|
+
if (this.debug) {
|
|
2585
|
+
console.warn('🎬 Auto-play was prevented:', error);
|
|
2586
|
+
console.warn('🎬 Trying muted autoplay fallback...');
|
|
2587
|
+
}
|
|
2588
|
+
// 음소거 상태에서 다시 시도
|
|
2589
|
+
video.muted = true;
|
|
2590
|
+
video.play().catch(fallbackError => {
|
|
2591
|
+
if (this.debug) {
|
|
2592
|
+
console.error('🎬 Autoplay completely failed:', fallbackError);
|
|
2593
|
+
}
|
|
2594
|
+
});
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
// 다양한 이벤트에서 자동 재생 시도
|
|
2599
|
+
video.addEventListener('loadedmetadata', () => {
|
|
2600
|
+
if (this.debug) {
|
|
2601
|
+
console.log('🎬 Video metadata loaded, attempting autoplay...');
|
|
2602
|
+
}
|
|
2603
|
+
attemptAutoplay();
|
|
2604
|
+
});
|
|
2605
|
+
video.addEventListener('canplay', () => {
|
|
2606
|
+
if (this.debug) {
|
|
2607
|
+
console.log('🎬 Video can play, attempting autoplay...');
|
|
2608
|
+
}
|
|
2609
|
+
attemptAutoplay();
|
|
2610
|
+
});
|
|
2611
|
+
video.addEventListener('loadeddata', () => {
|
|
2612
|
+
if (this.debug) {
|
|
2613
|
+
console.log('🎬 Video data loaded, attempting autoplay...');
|
|
2614
|
+
}
|
|
2615
|
+
attemptAutoplay();
|
|
2616
|
+
});
|
|
2617
|
+
// 비디오 로드 시작 즉시 한 번 시도
|
|
2618
|
+
setTimeout(() => {
|
|
2619
|
+
if (this.debug) {
|
|
2620
|
+
console.log('🎬 Initial autoplay attempt after timeout...');
|
|
2621
|
+
}
|
|
2622
|
+
attemptAutoplay();
|
|
2623
|
+
}, 100);
|
|
2624
|
+
}
|
|
2625
|
+
else if (advertisement.imageUrl) {
|
|
2626
|
+
// 비디오 URL이 없으면 이미지를 대체 표시
|
|
2627
|
+
const img = document.createElement('img');
|
|
2628
|
+
img.src = advertisement.imageUrl;
|
|
2629
|
+
img.style.width = '100%';
|
|
2630
|
+
img.style.height = '100%';
|
|
2631
|
+
img.style.objectFit = 'contain';
|
|
2632
|
+
img.alt = advertisement.title || 'Video thumbnail';
|
|
2633
|
+
container.appendChild(img);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
2637
|
+
AdClickHandler.addClickEventForRenderer(video, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Video');
|
|
2638
|
+
// 비디오 로드 에러 처리
|
|
2639
|
+
video.addEventListener('error', (e) => {
|
|
2640
|
+
console.error('Video load failed:', e);
|
|
2641
|
+
// 대체 이미지 표시
|
|
2642
|
+
if (advertisement.imageUrl) {
|
|
2643
|
+
const img = document.createElement('img');
|
|
2644
|
+
img.src = advertisement.imageUrl;
|
|
2645
|
+
img.style.width = '100%';
|
|
2646
|
+
img.style.height = '100%';
|
|
2647
|
+
img.style.objectFit = 'contain';
|
|
2648
|
+
img.alt = advertisement.title || 'Video thumbnail';
|
|
2649
|
+
// 클릭 이벤트 추가 (공통 컴포넌트 사용)
|
|
2650
|
+
AdClickHandler.addClickEventForRenderer(img, advertisement, slot, () => this.createEventTrackingCallback(), this.debug, 'Video fallback');
|
|
2651
|
+
container.innerHTML = '';
|
|
2652
|
+
container.appendChild(img);
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
// 비디오와 음소거 버튼을 컨테이너에 추가
|
|
2656
|
+
container.appendChild(video);
|
|
2657
|
+
container.appendChild(muteButton);
|
|
2658
|
+
if (this.debug) {
|
|
2659
|
+
console.log(`🎬 Video element created for ad: ${advertisement._id} (autoplay: ${video.autoplay}, muted: ${video.muted}, loop: ${video.loop})`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/**
|
|
2665
|
+
* 네이티브 광고 전용 렌더러
|
|
2666
|
+
*/
|
|
2667
|
+
class NativeAdRenderer extends BaseAdRenderer {
|
|
2668
|
+
constructor(debug = false, advertisementEventTracker) {
|
|
2669
|
+
super(AdType.NATIVE, debug, advertisementEventTracker);
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* 네이티브 광고 기본 높이
|
|
2673
|
+
*/
|
|
2674
|
+
getDefaultHeight() {
|
|
2675
|
+
return '200px';
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* 단일 네이티브 광고 렌더링
|
|
2679
|
+
*/
|
|
2680
|
+
async renderAdElement(slot, advertisement) {
|
|
2681
|
+
const container = document.getElementById(slot.containerId);
|
|
2682
|
+
if (!container)
|
|
2683
|
+
return;
|
|
2684
|
+
const adElement = document.createElement('div');
|
|
2685
|
+
adElement.className = 'adstage-ad adstage-native-ad';
|
|
2686
|
+
adElement.style.cssText = `
|
|
2687
|
+
width: 100%;
|
|
2688
|
+
height: 100%;
|
|
2689
|
+
display: flex;
|
|
2690
|
+
flex-direction: column;
|
|
2691
|
+
padding: 16px;
|
|
2692
|
+
background: #fff;
|
|
2693
|
+
border: 1px solid #e5e7eb;
|
|
2694
|
+
border-radius: 8px;
|
|
2695
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
2696
|
+
`;
|
|
2697
|
+
// 제목
|
|
2698
|
+
if (advertisement.title) {
|
|
2699
|
+
const title = document.createElement('h3');
|
|
2700
|
+
title.textContent = advertisement.title;
|
|
2701
|
+
title.style.cssText = `
|
|
2702
|
+
margin: 0 0 8px 0;
|
|
2703
|
+
font-size: 16px;
|
|
2704
|
+
font-weight: 600;
|
|
2705
|
+
color: #111827;
|
|
2706
|
+
line-height: 1.3;
|
|
2707
|
+
`;
|
|
2708
|
+
adElement.appendChild(title);
|
|
2709
|
+
}
|
|
2710
|
+
// 설명
|
|
2711
|
+
if (advertisement.textContent) {
|
|
2712
|
+
const description = document.createElement('p');
|
|
2713
|
+
description.textContent = advertisement.textContent;
|
|
2714
|
+
description.style.cssText = `
|
|
2715
|
+
margin: 0 0 12px 0;
|
|
2716
|
+
font-size: 14px;
|
|
2717
|
+
color: #6b7280;
|
|
2718
|
+
line-height: 1.4;
|
|
2719
|
+
flex: 1;
|
|
2720
|
+
`;
|
|
2721
|
+
adElement.appendChild(description);
|
|
2722
|
+
}
|
|
2723
|
+
// 이미지 (있는 경우)
|
|
2724
|
+
if (advertisement.imageUrl) {
|
|
2725
|
+
const imageContainer = document.createElement('div');
|
|
2726
|
+
imageContainer.style.cssText = `
|
|
2727
|
+
width: 100%;
|
|
2728
|
+
height: 120px;
|
|
2729
|
+
margin-bottom: 12px;
|
|
2730
|
+
border-radius: 6px;
|
|
2731
|
+
overflow: hidden;
|
|
2732
|
+
background: #f3f4f6;
|
|
2733
|
+
`;
|
|
2734
|
+
const img = document.createElement('img');
|
|
2735
|
+
img.src = advertisement.imageUrl;
|
|
2736
|
+
img.alt = advertisement.title || 'Native Advertisement';
|
|
2737
|
+
img.style.cssText = `
|
|
2738
|
+
width: 100%;
|
|
2739
|
+
height: 100%;
|
|
2740
|
+
object-fit: cover;
|
|
2741
|
+
`;
|
|
2742
|
+
imageContainer.appendChild(img);
|
|
2743
|
+
adElement.appendChild(imageContainer);
|
|
2744
|
+
}
|
|
2745
|
+
// CTA 버튼 (링크가 있는 경우)
|
|
2746
|
+
if (advertisement.linkUrl) {
|
|
2747
|
+
const ctaButton = document.createElement('button');
|
|
2748
|
+
ctaButton.textContent = 'Learn More';
|
|
2749
|
+
ctaButton.style.cssText = `
|
|
2750
|
+
padding: 8px 16px;
|
|
2751
|
+
background: #3b82f6;
|
|
2752
|
+
color: white;
|
|
2753
|
+
border: none;
|
|
2754
|
+
border-radius: 6px;
|
|
2755
|
+
font-size: 14px;
|
|
2756
|
+
font-weight: 500;
|
|
2757
|
+
cursor: pointer;
|
|
2758
|
+
align-self: flex-start;
|
|
2759
|
+
transition: background-color 0.2s;
|
|
2760
|
+
`;
|
|
2761
|
+
ctaButton.addEventListener('click', () => {
|
|
2762
|
+
if (this.advertisementEventTracker) {
|
|
2763
|
+
console.log(`Native click tracked for ad: ${advertisement._id}`);
|
|
2764
|
+
}
|
|
2765
|
+
window.open(advertisement.linkUrl, '_blank');
|
|
2766
|
+
});
|
|
2767
|
+
ctaButton.addEventListener('mouseenter', () => {
|
|
2768
|
+
ctaButton.style.backgroundColor = '#2563eb';
|
|
2769
|
+
});
|
|
2770
|
+
ctaButton.addEventListener('mouseleave', () => {
|
|
2771
|
+
ctaButton.style.backgroundColor = '#3b82f6';
|
|
2772
|
+
});
|
|
2773
|
+
adElement.appendChild(ctaButton);
|
|
1851
2774
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
if (
|
|
1855
|
-
|
|
2775
|
+
container.innerHTML = '';
|
|
2776
|
+
container.appendChild(adElement);
|
|
2777
|
+
if (this.debug) {
|
|
2778
|
+
console.log(`🏠 Single native ad rendered: ${advertisement._id}`);
|
|
1856
2779
|
}
|
|
1857
|
-
return 'average';
|
|
1858
2780
|
}
|
|
1859
2781
|
/**
|
|
1860
|
-
*
|
|
2782
|
+
* 다중 네이티브 광고 렌더링 - 현재는 단일 렌더링만 지원
|
|
1861
2783
|
*/
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
aspectRatios.forEach(ratio => {
|
|
1868
|
-
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
1869
|
-
const key = roundedRatio.toString();
|
|
1870
|
-
const existing = ratioGroups.get(key);
|
|
1871
|
-
if (existing) {
|
|
1872
|
-
existing.count++;
|
|
1873
|
-
}
|
|
1874
|
-
else {
|
|
1875
|
-
ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1878
|
-
const dominantGroup = Array.from(ratioGroups.values()).reduce((max, current) => current.count > max.count ? current : max);
|
|
1879
|
-
return Math.round(containerWidth / dominantGroup.ratio);
|
|
1880
|
-
}
|
|
1881
|
-
case 'common': {
|
|
1882
|
-
const standardRatios = [
|
|
1883
|
-
{ ratio: 16 / 9, name: '16:9' },
|
|
1884
|
-
{ ratio: 4 / 3, name: '4:3' },
|
|
1885
|
-
{ ratio: 1 / 1, name: '1:1' },
|
|
1886
|
-
{ ratio: 3 / 2, name: '3:2' }
|
|
1887
|
-
];
|
|
1888
|
-
const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
1889
|
-
const bestStandard = standardRatios.reduce((best, current) => Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best);
|
|
1890
|
-
if (this.debug) {
|
|
1891
|
-
console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
|
|
1892
|
-
}
|
|
1893
|
-
return Math.round(containerWidth / bestStandard.ratio);
|
|
2784
|
+
async renderMultipleAds(slot, advertisements) {
|
|
2785
|
+
if (advertisements.length > 0) {
|
|
2786
|
+
await this.renderAdElement(slot, advertisements[0]);
|
|
2787
|
+
if (this.debug) {
|
|
2788
|
+
console.log(`🏠 Native ad rendered (first of ${advertisements.length}): ${slot.id}`);
|
|
1894
2789
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2790
|
+
}
|
|
2791
|
+
else {
|
|
2792
|
+
if (this.debug) {
|
|
2793
|
+
console.warn(`⚠️ No native advertisements available for slot: ${slot.id}`);
|
|
1899
2794
|
}
|
|
1900
2795
|
}
|
|
1901
2796
|
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* 전면광고 전용 렌더러
|
|
2801
|
+
*/
|
|
2802
|
+
class InterstitialAdRenderer extends BaseAdRenderer {
|
|
2803
|
+
constructor(debug = false, advertisementEventTracker) {
|
|
2804
|
+
super(AdType.INTERSTITIAL, debug, advertisementEventTracker);
|
|
2805
|
+
}
|
|
1902
2806
|
/**
|
|
1903
|
-
*
|
|
2807
|
+
* 전면광고 기본 높이 (크게 설정)
|
|
1904
2808
|
*/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
const container = document.getElementById(slot.containerId);
|
|
1908
|
-
const adElement = document.getElementById(slot.id);
|
|
1909
|
-
if (!container || !adElement)
|
|
1910
|
-
return;
|
|
1911
|
-
const containerWidth = container.getBoundingClientRect().width || 300;
|
|
1912
|
-
const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth, slot.adType);
|
|
1913
|
-
adElement.style.height = optimalSize.height;
|
|
1914
|
-
slot.optimizedHeight = optimalSize.height;
|
|
1915
|
-
slot.aspectRatio = optimalSize.aspectRatio;
|
|
1916
|
-
if (this.debug) {
|
|
1917
|
-
console.log(`🔧 Container optimized for ${advertisements.length} banner ads: ${optimalSize.height}`);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
catch (error) {
|
|
1921
|
-
console.warn('Container optimization failed, using default size:', error);
|
|
1922
|
-
}
|
|
2809
|
+
getDefaultHeight() {
|
|
2810
|
+
return '400px';
|
|
1923
2811
|
}
|
|
1924
2812
|
/**
|
|
1925
|
-
*
|
|
2813
|
+
* 단일 전면광고 렌더링
|
|
1926
2814
|
*/
|
|
1927
|
-
async
|
|
2815
|
+
async renderAdElement(slot, advertisement) {
|
|
1928
2816
|
const container = document.getElementById(slot.containerId);
|
|
1929
|
-
if (!container)
|
|
1930
|
-
|
|
2817
|
+
if (!container)
|
|
2818
|
+
return;
|
|
2819
|
+
// 전면광고 오버레이 생성
|
|
2820
|
+
const overlay = document.createElement('div');
|
|
2821
|
+
overlay.className = 'adstage-interstitial-overlay';
|
|
2822
|
+
overlay.style.cssText = `
|
|
2823
|
+
position: fixed;
|
|
2824
|
+
top: 0;
|
|
2825
|
+
left: 0;
|
|
2826
|
+
width: 100vw;
|
|
2827
|
+
height: 100vh;
|
|
2828
|
+
background: rgba(0, 0, 0, 0.8);
|
|
2829
|
+
display: flex;
|
|
2830
|
+
align-items: center;
|
|
2831
|
+
justify-content: center;
|
|
2832
|
+
z-index: 10000;
|
|
2833
|
+
animation: fadeIn 0.3s ease-out;
|
|
2834
|
+
`;
|
|
2835
|
+
// 전면광고 콘텐츠
|
|
2836
|
+
const adContent = document.createElement('div');
|
|
2837
|
+
adContent.className = 'adstage-interstitial-content';
|
|
2838
|
+
adContent.style.cssText = `
|
|
2839
|
+
position: relative;
|
|
2840
|
+
max-width: 90vw;
|
|
2841
|
+
max-height: 90vh;
|
|
2842
|
+
background: white;
|
|
2843
|
+
border-radius: 12px;
|
|
2844
|
+
padding: 24px;
|
|
2845
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
|
|
2846
|
+
animation: slideUp 0.3s ease-out;
|
|
2847
|
+
overflow: auto;
|
|
2848
|
+
`;
|
|
2849
|
+
// 닫기 버튼
|
|
2850
|
+
const closeButton = document.createElement('button');
|
|
2851
|
+
closeButton.innerHTML = '×';
|
|
2852
|
+
closeButton.style.cssText = `
|
|
2853
|
+
position: absolute;
|
|
2854
|
+
top: 12px;
|
|
2855
|
+
right: 12px;
|
|
2856
|
+
width: 32px;
|
|
2857
|
+
height: 32px;
|
|
2858
|
+
border: none;
|
|
2859
|
+
background: #f3f4f6;
|
|
2860
|
+
color: #6b7280;
|
|
2861
|
+
font-size: 20px;
|
|
2862
|
+
font-weight: bold;
|
|
2863
|
+
border-radius: 50%;
|
|
2864
|
+
cursor: pointer;
|
|
2865
|
+
display: flex;
|
|
2866
|
+
align-items: center;
|
|
2867
|
+
justify-content: center;
|
|
2868
|
+
transition: all 0.2s;
|
|
2869
|
+
`;
|
|
2870
|
+
closeButton.addEventListener('click', () => {
|
|
2871
|
+
this.closeInterstitial(overlay);
|
|
2872
|
+
});
|
|
2873
|
+
closeButton.addEventListener('mouseenter', () => {
|
|
2874
|
+
closeButton.style.backgroundColor = '#e5e7eb';
|
|
2875
|
+
closeButton.style.color = '#374151';
|
|
2876
|
+
});
|
|
2877
|
+
closeButton.addEventListener('mouseleave', () => {
|
|
2878
|
+
closeButton.style.backgroundColor = '#f3f4f6';
|
|
2879
|
+
closeButton.style.color = '#6b7280';
|
|
2880
|
+
});
|
|
2881
|
+
// 이미지 (있는 경우)
|
|
2882
|
+
if (advertisement.imageUrl) {
|
|
2883
|
+
const img = document.createElement('img');
|
|
2884
|
+
img.src = advertisement.imageUrl;
|
|
2885
|
+
img.alt = advertisement.title || 'Interstitial Advertisement';
|
|
2886
|
+
img.style.cssText = `
|
|
2887
|
+
width: 100%;
|
|
2888
|
+
max-height: 300px;
|
|
2889
|
+
object-fit: cover;
|
|
2890
|
+
border-radius: 8px;
|
|
2891
|
+
margin-bottom: 16px;
|
|
2892
|
+
`;
|
|
2893
|
+
adContent.appendChild(img);
|
|
2894
|
+
}
|
|
2895
|
+
// 제목
|
|
2896
|
+
if (advertisement.title) {
|
|
2897
|
+
const title = document.createElement('h2');
|
|
2898
|
+
title.textContent = advertisement.title;
|
|
2899
|
+
title.style.cssText = `
|
|
2900
|
+
margin: 0 0 12px 0;
|
|
2901
|
+
font-size: 24px;
|
|
2902
|
+
font-weight: 700;
|
|
2903
|
+
color: #111827;
|
|
2904
|
+
line-height: 1.3;
|
|
2905
|
+
`;
|
|
2906
|
+
adContent.appendChild(title);
|
|
2907
|
+
}
|
|
2908
|
+
// 설명
|
|
2909
|
+
if (advertisement.textContent) {
|
|
2910
|
+
const description = document.createElement('p');
|
|
2911
|
+
description.textContent = advertisement.textContent;
|
|
2912
|
+
description.style.cssText = `
|
|
2913
|
+
margin: 0 0 20px 0;
|
|
2914
|
+
font-size: 16px;
|
|
2915
|
+
color: #6b7280;
|
|
2916
|
+
line-height: 1.5;
|
|
2917
|
+
`;
|
|
2918
|
+
adContent.appendChild(description);
|
|
1931
2919
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
catch (error) {
|
|
1955
|
-
if (this.debug) {
|
|
1956
|
-
console.error(`❌ Failed to track ${eventType} event for ad ${adId}:`, error);
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
else {
|
|
1961
|
-
if (this.debug) {
|
|
1962
|
-
console.warn(`⚠️ AdvertisementEventTracker not available for ${eventType} event`);
|
|
2920
|
+
// CTA 버튼 (링크가 있는 경우)
|
|
2921
|
+
if (advertisement.linkUrl) {
|
|
2922
|
+
const ctaButton = document.createElement('button');
|
|
2923
|
+
ctaButton.textContent = 'Get Started';
|
|
2924
|
+
ctaButton.style.cssText = `
|
|
2925
|
+
width: 100%;
|
|
2926
|
+
padding: 12px 24px;
|
|
2927
|
+
background: #3b82f6;
|
|
2928
|
+
color: white;
|
|
2929
|
+
border: none;
|
|
2930
|
+
border-radius: 8px;
|
|
2931
|
+
font-size: 16px;
|
|
2932
|
+
font-weight: 600;
|
|
2933
|
+
cursor: pointer;
|
|
2934
|
+
transition: background-color 0.2s;
|
|
2935
|
+
`;
|
|
2936
|
+
ctaButton.addEventListener('click', () => {
|
|
2937
|
+
if (this.advertisementEventTracker) {
|
|
2938
|
+
console.log(`Interstitial click tracked for ad: ${advertisement._id}`);
|
|
1963
2939
|
}
|
|
2940
|
+
window.open(advertisement.linkUrl, '_blank');
|
|
2941
|
+
this.closeInterstitial(overlay);
|
|
2942
|
+
});
|
|
2943
|
+
ctaButton.addEventListener('mouseenter', () => {
|
|
2944
|
+
ctaButton.style.backgroundColor = '#2563eb';
|
|
2945
|
+
});
|
|
2946
|
+
ctaButton.addEventListener('mouseleave', () => {
|
|
2947
|
+
ctaButton.style.backgroundColor = '#3b82f6';
|
|
2948
|
+
});
|
|
2949
|
+
adContent.appendChild(ctaButton);
|
|
2950
|
+
}
|
|
2951
|
+
// ESC 키로 닫기
|
|
2952
|
+
const handleEscape = (event) => {
|
|
2953
|
+
if (event.key === 'Escape') {
|
|
2954
|
+
this.closeInterstitial(overlay);
|
|
2955
|
+
document.removeEventListener('keydown', handleEscape);
|
|
1964
2956
|
}
|
|
1965
2957
|
};
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
};
|
|
1973
|
-
|
|
1974
|
-
|
|
2958
|
+
document.addEventListener('keydown', handleEscape);
|
|
2959
|
+
// 오버레이 클릭으로 닫기
|
|
2960
|
+
overlay.addEventListener('click', (event) => {
|
|
2961
|
+
if (event.target === overlay) {
|
|
2962
|
+
this.closeInterstitial(overlay);
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
// CSS 애니메이션 추가
|
|
2966
|
+
if (!document.getElementById('adstage-interstitial-styles')) {
|
|
2967
|
+
const styles = document.createElement('style');
|
|
2968
|
+
styles.id = 'adstage-interstitial-styles';
|
|
2969
|
+
styles.textContent = `
|
|
2970
|
+
@keyframes fadeIn {
|
|
2971
|
+
from { opacity: 0; }
|
|
2972
|
+
to { opacity: 1; }
|
|
2973
|
+
}
|
|
2974
|
+
@keyframes slideUp {
|
|
2975
|
+
from {
|
|
2976
|
+
opacity: 0;
|
|
2977
|
+
transform: translateY(20px);
|
|
2978
|
+
}
|
|
2979
|
+
to {
|
|
2980
|
+
opacity: 1;
|
|
2981
|
+
transform: translateY(0);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
`;
|
|
2985
|
+
document.head.appendChild(styles);
|
|
2986
|
+
}
|
|
2987
|
+
adContent.appendChild(closeButton);
|
|
2988
|
+
overlay.appendChild(adContent);
|
|
2989
|
+
document.body.appendChild(overlay);
|
|
2990
|
+
if (this.debug) {
|
|
2991
|
+
console.log(`🖼️ Interstitial ad rendered: ${advertisement._id}`);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* 다중 전면광고 렌더링 - 현재는 단일 렌더링만 지원
|
|
2996
|
+
*/
|
|
2997
|
+
async renderMultipleAds(slot, advertisements) {
|
|
2998
|
+
if (advertisements.length > 0) {
|
|
2999
|
+
await this.renderAdElement(slot, advertisements[0]);
|
|
1975
3000
|
if (this.debug) {
|
|
1976
|
-
console.log(
|
|
3001
|
+
console.log(`🖼️ Interstitial ad rendered (first of ${advertisements.length}): ${slot.id}`);
|
|
1977
3002
|
}
|
|
1978
3003
|
}
|
|
1979
3004
|
else {
|
|
1980
|
-
sliderElement = CarouselSliderManager.createSliderContainer(slot, advertisements, optimizedSliderOptions, trackEventCallback);
|
|
1981
3005
|
if (this.debug) {
|
|
1982
|
-
console.
|
|
3006
|
+
console.warn(`⚠️ No interstitial advertisements available for slot: ${slot.id}`);
|
|
1983
3007
|
}
|
|
1984
3008
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* 전면광고 닫기
|
|
3012
|
+
*/
|
|
3013
|
+
closeInterstitial(overlay) {
|
|
3014
|
+
overlay.style.animation = 'fadeOut 0.3s ease-out forwards';
|
|
3015
|
+
// CSS 애니메이션이 없으면 즉시 제거
|
|
3016
|
+
setTimeout(() => {
|
|
3017
|
+
if (overlay.parentNode) {
|
|
3018
|
+
overlay.parentNode.removeChild(overlay);
|
|
3019
|
+
}
|
|
3020
|
+
}, 300);
|
|
1987
3021
|
if (this.debug) {
|
|
1988
|
-
console.log(
|
|
3022
|
+
console.log('🖼️ Interstitial ad closed');
|
|
1989
3023
|
}
|
|
1990
3024
|
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
/**
|
|
3028
|
+
* AdRenderer - 광고 렌더링 팩토리 클래스
|
|
3029
|
+
* 광고 타입별로 적절한 렌더러를 생성하고 관리
|
|
3030
|
+
*/
|
|
3031
|
+
class AdRenderer {
|
|
3032
|
+
constructor(debug = false, advertisementEventTracker) {
|
|
3033
|
+
this.renderers = new Map();
|
|
3034
|
+
this.debug = debug;
|
|
3035
|
+
this.advertisementEventTracker = advertisementEventTracker || null;
|
|
3036
|
+
// 각 광고 타입별 렌더러 초기화
|
|
3037
|
+
this.initializeRenderers();
|
|
3038
|
+
}
|
|
1991
3039
|
/**
|
|
1992
|
-
* 광고
|
|
3040
|
+
* 광고 타입별 렌더러 초기화
|
|
1993
3041
|
*/
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
3042
|
+
initializeRenderers() {
|
|
3043
|
+
this.renderers.set(AdType.BANNER, new BannerAdRenderer(this.debug, this.advertisementEventTracker));
|
|
3044
|
+
this.renderers.set(AdType.TEXT, new TextAdRenderer(this.debug, this.advertisementEventTracker));
|
|
3045
|
+
this.renderers.set(AdType.VIDEO, new VideoAdRenderer(this.debug, this.advertisementEventTracker));
|
|
3046
|
+
this.renderers.set(AdType.NATIVE, new NativeAdRenderer(this.debug, this.advertisementEventTracker));
|
|
3047
|
+
this.renderers.set(AdType.INTERSTITIAL, new InterstitialAdRenderer(this.debug, this.advertisementEventTracker));
|
|
3048
|
+
if (this.debug) {
|
|
3049
|
+
console.log(`🏭 AdRenderer factory initialized with ${this.renderers.size} renderers`);
|
|
1997
3050
|
}
|
|
1998
|
-
await this.renderAdElement(slot, slot.advertisement);
|
|
1999
|
-
slot.isLoaded = true;
|
|
2000
3051
|
}
|
|
2001
3052
|
/**
|
|
2002
|
-
* 광고
|
|
3053
|
+
* 광고 요소를 동기적으로 생성해서 반환 (크기 측정 등을 위한 helper)
|
|
2003
3054
|
*/
|
|
2004
|
-
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
return;
|
|
3055
|
+
createAdElement(slot, advertisement) {
|
|
3056
|
+
const renderer = this.getRenderer(slot.adType);
|
|
3057
|
+
// 기본 광고 요소 생성
|
|
2008
3058
|
const adElement = document.createElement('div');
|
|
2009
|
-
adElement.className =
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
{ width: '100%', height: '250px' };
|
|
2019
|
-
adElement.style.width = width;
|
|
2020
|
-
adElement.style.height = height;
|
|
2021
|
-
}
|
|
3059
|
+
adElement.className = `adstage-ad adstage-${String(slot.adType).toLowerCase()}`;
|
|
3060
|
+
adElement.setAttribute('data-adstage-ad-id', advertisement._id);
|
|
3061
|
+
adElement.setAttribute('data-adstage-slot-id', slot.id);
|
|
3062
|
+
// 광고 타입별 기본 컨테이너 설정
|
|
3063
|
+
const { width, height } = renderer.calculateAdSize(adElement, slot.config || {}, advertisement);
|
|
3064
|
+
adElement.style.width = width;
|
|
3065
|
+
adElement.style.height = height;
|
|
3066
|
+
adElement.style.display = 'block';
|
|
3067
|
+
// 간단한 내용 설정 (크기 측정용)
|
|
2022
3068
|
switch (slot.adType) {
|
|
2023
3069
|
case AdType.BANNER:
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
3070
|
+
if (advertisement.imageUrl) {
|
|
3071
|
+
const img = document.createElement('img');
|
|
3072
|
+
img.src = advertisement.imageUrl;
|
|
3073
|
+
img.style.width = '100%';
|
|
3074
|
+
img.style.height = '100%';
|
|
3075
|
+
img.style.objectFit = 'cover';
|
|
3076
|
+
adElement.appendChild(img);
|
|
2026
3077
|
}
|
|
2027
3078
|
break;
|
|
2028
|
-
case AdType.TEXT: {
|
|
2029
|
-
const textDiv = document.createElement('div');
|
|
2030
|
-
textDiv.innerHTML = `
|
|
2031
|
-
${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
|
|
2032
|
-
`;
|
|
2033
|
-
adElement.appendChild(textDiv);
|
|
2034
|
-
break;
|
|
2035
|
-
}
|
|
2036
3079
|
case AdType.VIDEO:
|
|
2037
|
-
if (
|
|
3080
|
+
if (advertisement.videoUrl) {
|
|
2038
3081
|
const video = document.createElement('video');
|
|
2039
|
-
video.src =
|
|
2040
|
-
video.controls = true;
|
|
3082
|
+
video.src = advertisement.videoUrl;
|
|
2041
3083
|
video.style.width = '100%';
|
|
2042
3084
|
video.style.height = '100%';
|
|
2043
3085
|
adElement.appendChild(video);
|
|
2044
3086
|
}
|
|
2045
3087
|
break;
|
|
3088
|
+
case AdType.TEXT:
|
|
3089
|
+
if (advertisement.textContent) {
|
|
3090
|
+
const textDiv = document.createElement('div');
|
|
3091
|
+
textDiv.textContent = advertisement.textContent || '';
|
|
3092
|
+
textDiv.style.padding = '8px';
|
|
3093
|
+
adElement.appendChild(textDiv);
|
|
3094
|
+
}
|
|
3095
|
+
break;
|
|
2046
3096
|
default:
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
adElement.addEventListener('click', () => {
|
|
2052
|
-
window.open(ad.linkUrl, '_blank');
|
|
2053
|
-
});
|
|
3097
|
+
// 기본 placeholder
|
|
3098
|
+
adElement.style.border = '1px dashed #ccc';
|
|
3099
|
+
adElement.style.backgroundColor = '#f9f9f9';
|
|
3100
|
+
adElement.textContent = `${slot.adType} Ad`;
|
|
2054
3101
|
}
|
|
2055
|
-
|
|
2056
|
-
container.appendChild(adElement);
|
|
3102
|
+
return adElement;
|
|
2057
3103
|
}
|
|
2058
3104
|
/**
|
|
2059
|
-
*
|
|
3105
|
+
* 광고 타입에 따른 렌더러 획득
|
|
2060
3106
|
*/
|
|
2061
|
-
|
|
2062
|
-
const
|
|
2063
|
-
if (
|
|
2064
|
-
|
|
2065
|
-
element.querySelector('[data-adstage-container="true"]'),
|
|
2066
|
-
element.closest('[data-adstage-container="true"]'),
|
|
2067
|
-
element
|
|
2068
|
-
].filter(el => el && el.hasAttribute('data-adstage-container'));
|
|
2069
|
-
const classBasedContainers = [
|
|
2070
|
-
element.closest('.adstage-slot'),
|
|
2071
|
-
element.closest('.adstage-banner'),
|
|
2072
|
-
element.closest('.adstage-text'),
|
|
2073
|
-
element.closest('.adstage-video'),
|
|
2074
|
-
element.closest('.adstage-native'),
|
|
2075
|
-
element.closest('.adstage-interstitial')
|
|
2076
|
-
].filter(Boolean);
|
|
2077
|
-
const generalContainers = [
|
|
2078
|
-
element.closest('[class*="ad"]'),
|
|
2079
|
-
element.closest('[class*="banner"]'),
|
|
2080
|
-
element.closest('[class*="container"]'),
|
|
2081
|
-
element.closest('div[style*="height"]'),
|
|
2082
|
-
element.closest('div[style*="min-height"]'),
|
|
2083
|
-
element.parentElement
|
|
2084
|
-
].filter(Boolean);
|
|
2085
|
-
const possibleContainers = [...adstageContainers, ...classBasedContainers, ...generalContainers];
|
|
2086
|
-
const targetContainer = possibleContainers[0];
|
|
2087
|
-
if (targetContainer) {
|
|
2088
|
-
let containerType = 'unknown';
|
|
2089
|
-
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
2090
|
-
containerType = 'adstage-official';
|
|
2091
|
-
}
|
|
2092
|
-
else if (targetContainer.classList.contains('adstage-slot')) {
|
|
2093
|
-
containerType = 'adstage-class';
|
|
2094
|
-
}
|
|
2095
|
-
else {
|
|
2096
|
-
containerType = 'generic';
|
|
2097
|
-
}
|
|
2098
|
-
targetContainer.style.cssText += `
|
|
2099
|
-
height: 0px !important;
|
|
2100
|
-
min-height: 0px !important;
|
|
2101
|
-
padding: 0px !important;
|
|
2102
|
-
margin: 0px !important;
|
|
2103
|
-
border: none !important;
|
|
2104
|
-
overflow: hidden !important;
|
|
2105
|
-
display: block !important;
|
|
2106
|
-
`;
|
|
2107
|
-
targetContainer.innerHTML = '';
|
|
2108
|
-
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
2109
|
-
if (this.debug) {
|
|
2110
|
-
console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
else {
|
|
2114
|
-
this.createEmptyContainer(slot);
|
|
2115
|
-
}
|
|
3107
|
+
getRenderer(adType) {
|
|
3108
|
+
const renderer = this.renderers.get(adType);
|
|
3109
|
+
if (!renderer) {
|
|
3110
|
+
throw new Error(`No renderer found for ad type: ${adType}`);
|
|
2116
3111
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
3112
|
+
return renderer;
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Placeholder(슬롯 컨테이너) 생성
|
|
3116
|
+
*/
|
|
3117
|
+
createPlaceholder(container, slotId, type, options, config) {
|
|
3118
|
+
const renderer = this.getRenderer(type);
|
|
3119
|
+
renderer.createPlaceholder(container, slotId, options, config);
|
|
2119
3120
|
}
|
|
2120
3121
|
/**
|
|
2121
|
-
*
|
|
3122
|
+
* 배너 광고를 위한 컨테이너 최적화 (배너 전용)
|
|
2122
3123
|
*/
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
if (originalContainer) {
|
|
2126
|
-
originalContainer.innerHTML = '';
|
|
2127
|
-
const emptyElement = document.createElement('div');
|
|
2128
|
-
emptyElement.id = slot.id;
|
|
2129
|
-
emptyElement.className = 'adstage-slot adstage-empty';
|
|
2130
|
-
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
2131
|
-
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
2132
|
-
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
2133
|
-
emptyElement.style.cssText = `
|
|
2134
|
-
height: 0px !important;
|
|
2135
|
-
min-height: 0px !important;
|
|
2136
|
-
padding: 0px !important;
|
|
2137
|
-
margin: 0px !important;
|
|
2138
|
-
border: none !important;
|
|
2139
|
-
overflow: hidden !important;
|
|
2140
|
-
display: block !important;
|
|
2141
|
-
`;
|
|
2142
|
-
originalContainer.appendChild(emptyElement);
|
|
3124
|
+
async optimizeContainerForBannerAds(slot, advertisements) {
|
|
3125
|
+
if (slot.adType !== AdType.BANNER) {
|
|
2143
3126
|
if (this.debug) {
|
|
2144
|
-
console.warn(`⚠️
|
|
3127
|
+
console.warn(`⚠️ Container optimization is only supported for BANNER ads, got: ${slot.adType}`);
|
|
2145
3128
|
}
|
|
3129
|
+
return;
|
|
2146
3130
|
}
|
|
3131
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER);
|
|
3132
|
+
await bannerRenderer.optimizeContainerForBannerAds(slot, advertisements);
|
|
2147
3133
|
}
|
|
2148
3134
|
/**
|
|
2149
|
-
* 광고
|
|
3135
|
+
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
2150
3136
|
*/
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return '250px'; // 일반 배너
|
|
2155
|
-
case AdType.TEXT:
|
|
2156
|
-
return '60px'; // 텍스트는 좀 더 작게
|
|
2157
|
-
case AdType.VIDEO:
|
|
2158
|
-
return '360px'; // 비디오는 16:9 비율 고려
|
|
2159
|
-
case AdType.NATIVE:
|
|
2160
|
-
return '200px'; // 네이티브는 중간 크기
|
|
2161
|
-
case AdType.INTERSTITIAL:
|
|
2162
|
-
return '400px'; // 전면광고는 크게
|
|
2163
|
-
default:
|
|
2164
|
-
return '250px';
|
|
2165
|
-
}
|
|
3137
|
+
async renderAdSlider(slot, advertisements) {
|
|
3138
|
+
const renderer = this.getRenderer(slot.adType);
|
|
3139
|
+
await renderer.renderMultipleAds(slot, advertisements);
|
|
2166
3140
|
}
|
|
2167
3141
|
/**
|
|
2168
|
-
*
|
|
3142
|
+
* 광고 렌더링 (단일 광고용)
|
|
2169
3143
|
*/
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
const explicitHeight = options.height;
|
|
2174
|
-
// 너비 처리
|
|
2175
|
-
let width;
|
|
2176
|
-
if (typeof explicitWidth === 'number') {
|
|
2177
|
-
width = `${explicitWidth}px`;
|
|
2178
|
-
}
|
|
2179
|
-
else if (typeof explicitWidth === 'string') {
|
|
2180
|
-
width = explicitWidth;
|
|
2181
|
-
}
|
|
2182
|
-
else {
|
|
2183
|
-
width = '100%'; // 기본값은 100%
|
|
2184
|
-
}
|
|
2185
|
-
// 높이 처리 - 핵심 로직
|
|
2186
|
-
let height;
|
|
2187
|
-
if (typeof explicitHeight === 'number') {
|
|
2188
|
-
height = `${explicitHeight}px`;
|
|
2189
|
-
}
|
|
2190
|
-
else if (typeof explicitHeight === 'string' && explicitHeight !== '100%' && explicitHeight !== 'auto') {
|
|
2191
|
-
// 명시적인 크기 문자열 (예: '200px', '50vh' 등)
|
|
2192
|
-
height = explicitHeight;
|
|
2193
|
-
}
|
|
2194
|
-
else {
|
|
2195
|
-
// 100%, auto이거나 높이가 지정되지 않은 경우 스마트 계산
|
|
2196
|
-
const containerHeight = this.getContainerHeight(container);
|
|
2197
|
-
if (containerHeight > 0) {
|
|
2198
|
-
// 컨테이너에 높이가 있으면 100% 사용
|
|
2199
|
-
height = '100%';
|
|
2200
|
-
if (config?.debug) {
|
|
2201
|
-
console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
else {
|
|
2205
|
-
// 컨테이너에 높이가 없으면 타입별 기본값 사용 (나중에 동적 조정됨)
|
|
2206
|
-
height = this.getDefaultHeightForAdType(type);
|
|
2207
|
-
if (config?.debug) {
|
|
2208
|
-
console.log(`📏 Using default height ${height} (will be optimized for ${type})`);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
3144
|
+
async renderAd(slot) {
|
|
3145
|
+
if (!slot.advertisement) {
|
|
3146
|
+
throw new Error('No advertisement to render');
|
|
2211
3147
|
}
|
|
2212
|
-
|
|
3148
|
+
const renderer = this.getRenderer(slot.adType);
|
|
3149
|
+
await renderer.renderAdElement(slot, slot.advertisement);
|
|
3150
|
+
slot.isLoaded = true;
|
|
2213
3151
|
}
|
|
2214
3152
|
/**
|
|
2215
|
-
*
|
|
3153
|
+
* 광고 요소 렌더링 (기본 구현) - 호환성을 위해 유지
|
|
2216
3154
|
*/
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return height || 0;
|
|
3155
|
+
async renderAdElement(slot, ad) {
|
|
3156
|
+
const renderer = this.getRenderer(slot.adType);
|
|
3157
|
+
await renderer.renderAdElement(slot, ad);
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Fallback 광고 렌더링 - 컨테이너 접기/생성
|
|
3161
|
+
*/
|
|
3162
|
+
renderFallback(slot) {
|
|
3163
|
+
const renderer = this.getRenderer(slot.adType);
|
|
3164
|
+
renderer.renderFallback(slot);
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* 광고 타입별 기본 높이 반환
|
|
3168
|
+
*/
|
|
3169
|
+
getDefaultHeightForAdType(type) {
|
|
3170
|
+
const renderer = this.getRenderer(type);
|
|
3171
|
+
return renderer.getDefaultHeight();
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* 컨테이너와 광고 타입에 따른 스마트한 크기 계산
|
|
3175
|
+
*/
|
|
3176
|
+
calculateAdSize(container, type, options, config) {
|
|
3177
|
+
const renderer = this.getRenderer(type);
|
|
3178
|
+
return renderer.calculateAdSize(container, options, config);
|
|
2242
3179
|
}
|
|
2243
3180
|
/**
|
|
2244
|
-
* 이미지 로드 및 실제 크기 획득 (
|
|
3181
|
+
* 이미지 로드 및 실제 크기 획득 (배너 전용)
|
|
2245
3182
|
*/
|
|
2246
3183
|
loadImageDimensions(imageUrl) {
|
|
2247
3184
|
return new Promise((resolve, reject) => {
|
|
@@ -2256,118 +3193,36 @@ class AdRenderer {
|
|
|
2256
3193
|
});
|
|
2257
3194
|
}
|
|
2258
3195
|
/**
|
|
2259
|
-
*
|
|
3196
|
+
* 배너 이미지 최적화 렌더링 (배너 전용)
|
|
2260
3197
|
*/
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
// 비율이 거의 같으면 cover 사용
|
|
2265
|
-
img.style.objectFit = 'cover';
|
|
2266
|
-
img.style.objectPosition = 'center';
|
|
2267
|
-
}
|
|
2268
|
-
else if (ratio > 1.3) {
|
|
2269
|
-
// 이미지가 훨씬 가로형이면 contain으로 전체 보이기
|
|
2270
|
-
img.style.objectFit = 'contain';
|
|
2271
|
-
img.style.objectPosition = 'center';
|
|
2272
|
-
img.style.backgroundColor = '#f0f0f0'; // 빈 공간 배경색
|
|
2273
|
-
}
|
|
2274
|
-
else if (ratio < 0.7) {
|
|
2275
|
-
// 이미지가 훨씬 세로형이면 cover로 채우기
|
|
2276
|
-
img.style.objectFit = 'cover';
|
|
2277
|
-
img.style.objectPosition = 'center';
|
|
2278
|
-
}
|
|
2279
|
-
else {
|
|
2280
|
-
// 적당한 차이면 스마트 cover
|
|
2281
|
-
img.style.objectFit = 'cover';
|
|
2282
|
-
img.style.objectPosition = 'center';
|
|
2283
|
-
}
|
|
2284
|
-
if (this.debug) {
|
|
2285
|
-
console.log(`🎨 Image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
|
|
3198
|
+
async renderOptimizedBannerImage(container, advertisement, slot) {
|
|
3199
|
+
if (slot.adType !== AdType.BANNER) {
|
|
3200
|
+
throw new Error('renderOptimizedBannerImage is only supported for BANNER ads');
|
|
2286
3201
|
}
|
|
3202
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER);
|
|
3203
|
+
return await bannerRenderer.renderOptimizedBannerImage(container, advertisement, slot);
|
|
2287
3204
|
}
|
|
2288
3205
|
/**
|
|
2289
|
-
*
|
|
2290
|
-
* 이미지 실제 크기를 로드한 후 컨테이너와 비율을 맞춤
|
|
3206
|
+
* 여러 광고의 최적 컨테이너 크기 계산 (배너 전용)
|
|
2291
3207
|
*/
|
|
2292
|
-
async
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
// 이미지 로딩 상태 표시
|
|
2301
|
-
container.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">Loading...</div>';
|
|
2302
|
-
try {
|
|
2303
|
-
// 이미지 URL 체크
|
|
2304
|
-
if (!advertisement.imageUrl) {
|
|
2305
|
-
throw new Error('Image URL is not provided');
|
|
2306
|
-
}
|
|
2307
|
-
// 🆕 이미지 실제 크기 로드
|
|
2308
|
-
const imageDimensions = await this.loadImageDimensions(advertisement.imageUrl);
|
|
2309
|
-
// 컨테이너 크기 정보
|
|
2310
|
-
const containerRect = container.getBoundingClientRect();
|
|
2311
|
-
const containerWidth = containerRect.width;
|
|
2312
|
-
const containerHeight = containerRect.height;
|
|
2313
|
-
if (this.debug) {
|
|
2314
|
-
console.log(`📸 Image dimensions: ${imageDimensions.width}x${imageDimensions.height}`);
|
|
2315
|
-
console.log(`📦 Container dimensions: ${containerWidth}x${containerHeight}`);
|
|
2316
|
-
}
|
|
2317
|
-
// 비율 계산
|
|
2318
|
-
const imageAspectRatio = imageDimensions.width / imageDimensions.height;
|
|
2319
|
-
const containerAspectRatio = containerWidth / containerHeight;
|
|
2320
|
-
// 🆕 스마트 스타일 적용
|
|
2321
|
-
this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
|
|
2322
|
-
// 이미지 설정 및 추가
|
|
2323
|
-
img.src = advertisement.imageUrl;
|
|
2324
|
-
// 컨테이너 클리어 후 이미지 추가
|
|
2325
|
-
container.innerHTML = '';
|
|
2326
|
-
container.appendChild(img);
|
|
2327
|
-
// 클릭 이벤트 등록
|
|
2328
|
-
if (advertisement.linkUrl) {
|
|
2329
|
-
img.style.cursor = 'pointer';
|
|
2330
|
-
img.addEventListener('click', () => {
|
|
2331
|
-
if (this.advertisementEventTracker) {
|
|
2332
|
-
// AdvertisementEventTracker의 실제 메소드명을 확인해야 함
|
|
2333
|
-
console.log(`Click tracked for ad: ${advertisement._id}`);
|
|
2334
|
-
}
|
|
2335
|
-
window.open(advertisement.linkUrl, '_blank');
|
|
2336
|
-
});
|
|
2337
|
-
}
|
|
2338
|
-
if (this.debug) {
|
|
2339
|
-
console.log(`✅ Optimized banner image rendered for ad: ${advertisement._id}`);
|
|
2340
|
-
}
|
|
2341
|
-
return img;
|
|
2342
|
-
}
|
|
2343
|
-
catch (error) {
|
|
2344
|
-
console.error('❌ Failed to load optimized banner image:', error);
|
|
2345
|
-
// 폴백: 일반 이미지 렌더링
|
|
2346
|
-
if (advertisement.imageUrl) {
|
|
2347
|
-
img.src = advertisement.imageUrl;
|
|
2348
|
-
img.style.objectFit = 'cover';
|
|
2349
|
-
img.style.objectPosition = 'center';
|
|
2350
|
-
container.innerHTML = '';
|
|
2351
|
-
container.appendChild(img);
|
|
2352
|
-
if (advertisement.linkUrl) {
|
|
2353
|
-
img.style.cursor = 'pointer';
|
|
2354
|
-
img.addEventListener('click', () => {
|
|
2355
|
-
if (this.advertisementEventTracker) {
|
|
2356
|
-
console.log(`Click tracked for ad: ${advertisement._id}`);
|
|
2357
|
-
}
|
|
2358
|
-
window.open(advertisement.linkUrl, '_blank');
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
return img;
|
|
3208
|
+
async calculateOptimalContainerSize(advertisements, containerWidth, adType) {
|
|
3209
|
+
if (adType !== AdType.BANNER) {
|
|
3210
|
+
const renderer = this.getRenderer(adType);
|
|
3211
|
+
return {
|
|
3212
|
+
width: '100%',
|
|
3213
|
+
height: renderer.getDefaultHeight(),
|
|
3214
|
+
aspectRatio: 16 / 9
|
|
3215
|
+
};
|
|
2363
3216
|
}
|
|
3217
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER);
|
|
3218
|
+
return await bannerRenderer.calculateOptimalContainerSize(advertisements, containerWidth);
|
|
2364
3219
|
}
|
|
2365
3220
|
/**
|
|
2366
3221
|
* 디버그 로그 출력
|
|
2367
3222
|
*/
|
|
2368
3223
|
log(message, ...args) {
|
|
2369
3224
|
if (this.debug) {
|
|
2370
|
-
console.log(message
|
|
3225
|
+
console.log(`[AdRenderer] ${message}`, ...args);
|
|
2371
3226
|
}
|
|
2372
3227
|
}
|
|
2373
3228
|
}
|
|
@@ -2385,6 +3240,8 @@ class AdsModule {
|
|
|
2385
3240
|
this.advertisementEventTracker = null;
|
|
2386
3241
|
// 렌더링 관련
|
|
2387
3242
|
this.adRenderer = null;
|
|
3243
|
+
// DOM 변화 감지를 위한 MutationObserver
|
|
3244
|
+
this.mutationObserver = null;
|
|
2388
3245
|
}
|
|
2389
3246
|
/**
|
|
2390
3247
|
* Ads 모듈 초기화 (동기)
|
|
@@ -2395,9 +3252,11 @@ class AdsModule {
|
|
|
2395
3252
|
this.advertisementEventTracker = new AdvertisementEventTracker(endpoints.getBaseUrl(), config.apiKey, config.debug || false, this.slots);
|
|
2396
3253
|
// AdRenderer 초기화
|
|
2397
3254
|
this.adRenderer = new AdRenderer(config.debug || false, this.advertisementEventTracker);
|
|
3255
|
+
// DOM 변화 감지를 위한 MutationObserver 설정
|
|
3256
|
+
this.setupAutoCleanup();
|
|
2398
3257
|
this._isReady = true;
|
|
2399
3258
|
if (config.debug) {
|
|
2400
|
-
console.log('🎯 Ads module initialized (sync mode)');
|
|
3259
|
+
console.log('🎯 Ads module initialized (sync mode) with auto-cleanup');
|
|
2401
3260
|
}
|
|
2402
3261
|
}
|
|
2403
3262
|
/**
|
|
@@ -2451,16 +3310,24 @@ class AdsModule {
|
|
|
2451
3310
|
return this.createAd(containerId, AdType.TEXT, adstageOptions);
|
|
2452
3311
|
}
|
|
2453
3312
|
/**
|
|
2454
|
-
* 비디오 광고 생성 (동기)
|
|
3313
|
+
* 비디오 광고 생성 (동기) - 단일 비디오만 지원
|
|
2455
3314
|
*/
|
|
2456
3315
|
video(containerId, options) {
|
|
2457
3316
|
this.ensureReady();
|
|
2458
3317
|
const adstageOptions = {
|
|
2459
3318
|
width: options?.width || 640,
|
|
2460
3319
|
height: options?.height || 360,
|
|
2461
|
-
autoplay: options?.autoplay
|
|
2462
|
-
muted: options?.muted
|
|
2463
|
-
|
|
3320
|
+
autoplay: options?.autoplay !== undefined ? options.autoplay : true, // 기본값 true (사용자 요구사항)
|
|
3321
|
+
muted: options?.muted !== undefined ? options.muted : true, // 기본값 true
|
|
3322
|
+
loop: options?.loop !== undefined ? options.loop : true, // 기본값 true (사용자 요구사항)
|
|
3323
|
+
playsinline: options?.playsinline !== false, // 기본값 true
|
|
3324
|
+
controls: options?.controls !== undefined ? options.controls : false, // 기본값 false (사용자 요구사항)
|
|
3325
|
+
hideControls: options?.hideControls || false,
|
|
3326
|
+
customControls: options?.customControls,
|
|
3327
|
+
autoSlide: false, // 비디오는 슬라이드 비활성화
|
|
3328
|
+
maxAds: 1, // 하나의 비디오만 가져오기
|
|
3329
|
+
onClick: options?.onClick,
|
|
3330
|
+
...(options?.adId && { adId: options.adId }) // 특정 비디오 ID가 있으면 사용
|
|
2464
3331
|
};
|
|
2465
3332
|
return this.createAd(containerId, AdType.VIDEO, adstageOptions);
|
|
2466
3333
|
}
|
|
@@ -2538,12 +3405,58 @@ class AdsModule {
|
|
|
2538
3405
|
if (!this._config?.apiKey) {
|
|
2539
3406
|
throw new Error('API key not configured');
|
|
2540
3407
|
}
|
|
2541
|
-
|
|
2542
|
-
if (!container) {
|
|
2543
|
-
throw new Error(`Container not found: ${containerId}`);
|
|
2544
|
-
}
|
|
2545
|
-
// 고유한 슬롯 ID 생성
|
|
3408
|
+
// 즉시 슬롯 ID 생성 (개발자에게 바로 반환)
|
|
2546
3409
|
const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3410
|
+
// 비동기로 광고 생성 처리 (디바운싱 및 재시도 로직 포함)
|
|
3411
|
+
this.createAdWithRetry(containerId, type, options, slotId, 0);
|
|
3412
|
+
return slotId;
|
|
3413
|
+
}
|
|
3414
|
+
async createAdWithRetry(containerId, type, options, slotId, attempt) {
|
|
3415
|
+
const maxAttempts = 5;
|
|
3416
|
+
const delays = [0, 50, 100, 200, 500]; // 점진적 지연
|
|
3417
|
+
let container = null;
|
|
3418
|
+
let containerIdString;
|
|
3419
|
+
// HTMLElement인지 string인지 구분
|
|
3420
|
+
if (typeof containerId === 'string') {
|
|
3421
|
+
// string인 경우 DOM에서 찾기
|
|
3422
|
+
containerIdString = containerId;
|
|
3423
|
+
container = document.getElementById(containerId);
|
|
3424
|
+
if (!container) {
|
|
3425
|
+
if (attempt < maxAttempts - 1) {
|
|
3426
|
+
if (this._config?.debug) {
|
|
3427
|
+
console.warn(`Container not found: ${containerId}. Retrying in ${delays[attempt + 1]}ms... (attempt ${attempt + 1}/${maxAttempts})`);
|
|
3428
|
+
}
|
|
3429
|
+
setTimeout(() => {
|
|
3430
|
+
this.createAdWithRetry(containerId, type, options, slotId, attempt + 1);
|
|
3431
|
+
}, delays[attempt + 1]);
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
else {
|
|
3435
|
+
console.error(`Container not found after ${maxAttempts} attempts: ${containerId}`);
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
else {
|
|
3441
|
+
// HTMLElement인 경우 직접 사용
|
|
3442
|
+
container = containerId;
|
|
3443
|
+
containerIdString = container.id || `auto-${slotId}`;
|
|
3444
|
+
// ID가 없으면 자동 생성
|
|
3445
|
+
if (!container.id) {
|
|
3446
|
+
container.id = containerIdString;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
// 컨테이너를 찾았으면 광고 생성
|
|
3450
|
+
try {
|
|
3451
|
+
this.createAdInternal(containerIdString, type, options, slotId, container);
|
|
3452
|
+
}
|
|
3453
|
+
catch (error) {
|
|
3454
|
+
console.error('광고 생성 중 오류 발생:', error);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
createAdInternal(containerId, type, options, slotId, container) {
|
|
3458
|
+
// 컨테이너에 슬롯 ID 속성 추가 (MutationObserver가 감지할 수 있도록)
|
|
3459
|
+
container.setAttribute('data-adstage-slot-id', slotId);
|
|
2547
3460
|
// 즉시 placeholder 생성 (AdRenderer 위임)
|
|
2548
3461
|
this.adRenderer?.createPlaceholder(container, slotId, type, options, this._config);
|
|
2549
3462
|
// 광고 슬롯 정보 저장
|
|
@@ -2552,7 +3465,7 @@ class AdsModule {
|
|
|
2552
3465
|
containerId,
|
|
2553
3466
|
adType: type,
|
|
2554
3467
|
width: options.width || '100%',
|
|
2555
|
-
height: options.height || 250,
|
|
3468
|
+
height: options.height || (type === AdType.TEXT ? 'auto' : 250), // 텍스트 광고는 콘텐츠 높이에 맞춤
|
|
2556
3469
|
isLoaded: false,
|
|
2557
3470
|
isVisible: false,
|
|
2558
3471
|
refreshRate: 0,
|
|
@@ -2573,7 +3486,6 @@ class AdsModule {
|
|
|
2573
3486
|
if (this.advertisementEventTracker && this._config?.debug) {
|
|
2574
3487
|
console.log(`📊 Advertisement event tracking enabled for slot: ${slotId}`);
|
|
2575
3488
|
}
|
|
2576
|
-
return slotId;
|
|
2577
3489
|
}
|
|
2578
3490
|
/**
|
|
2579
3491
|
* 백그라운드에서 광고 콘텐츠 로드
|
|
@@ -2754,6 +3666,98 @@ class AdsModule {
|
|
|
2754
3666
|
throw new Error('Ads module not initialized. Call AdStage.init() first.');
|
|
2755
3667
|
}
|
|
2756
3668
|
}
|
|
3669
|
+
/**
|
|
3670
|
+
* DOM 변화 감지를 통한 자동 정리 설정
|
|
3671
|
+
*/
|
|
3672
|
+
setupAutoCleanup() {
|
|
3673
|
+
// 브라우저 환경에서만 실행
|
|
3674
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
// 기존 observer가 있으면 해제
|
|
3678
|
+
if (this.mutationObserver) {
|
|
3679
|
+
this.mutationObserver.disconnect();
|
|
3680
|
+
}
|
|
3681
|
+
// 새로운 MutationObserver 생성
|
|
3682
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
3683
|
+
mutations.forEach((mutation) => {
|
|
3684
|
+
// 제거된 노드들 확인
|
|
3685
|
+
mutation.removedNodes.forEach((node) => {
|
|
3686
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
3687
|
+
this.handleRemovedElement(node);
|
|
3688
|
+
}
|
|
3689
|
+
});
|
|
3690
|
+
});
|
|
3691
|
+
});
|
|
3692
|
+
// document 전체를 관찰 (childList와 subtree 옵션 사용)
|
|
3693
|
+
this.mutationObserver.observe(document.body, {
|
|
3694
|
+
childList: true,
|
|
3695
|
+
subtree: true
|
|
3696
|
+
});
|
|
3697
|
+
if (this._config?.debug) {
|
|
3698
|
+
console.log('🔍 Auto-cleanup MutationObserver enabled');
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* 제거된 요소에서 광고 슬롯 정리
|
|
3703
|
+
*/
|
|
3704
|
+
handleRemovedElement(element) {
|
|
3705
|
+
// 제거된 요소가 광고 컨테이너인지 확인
|
|
3706
|
+
const slotId = element.getAttribute('data-adstage-slot-id');
|
|
3707
|
+
if (slotId) {
|
|
3708
|
+
this.autoDestroy(slotId);
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
// 제거된 요소의 하위에 광고 컨테이너가 있는지 확인
|
|
3712
|
+
const adContainers = element.querySelectorAll('[data-adstage-slot-id]');
|
|
3713
|
+
adContainers.forEach((container) => {
|
|
3714
|
+
const containerSlotId = container.getAttribute('data-adstage-slot-id');
|
|
3715
|
+
if (containerSlotId) {
|
|
3716
|
+
this.autoDestroy(containerSlotId);
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* 자동 정리 (로그 없이 조용히 정리)
|
|
3722
|
+
*/
|
|
3723
|
+
autoDestroy(slotId) {
|
|
3724
|
+
const slot = this.slots.get(slotId);
|
|
3725
|
+
if (slot) {
|
|
3726
|
+
try {
|
|
3727
|
+
// 슬롯 정리 (로그 출력 최소화)
|
|
3728
|
+
this.slots.delete(slotId);
|
|
3729
|
+
if (this._config?.debug) {
|
|
3730
|
+
console.log(`🧹 Auto-cleanup: slot ${slotId} removed`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
catch (error) {
|
|
3734
|
+
if (this._config?.debug) {
|
|
3735
|
+
console.warn(`Auto-cleanup failed for slot ${slotId}:`, error);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
/**
|
|
3741
|
+
* 모듈 종료 시 정리
|
|
3742
|
+
*/
|
|
3743
|
+
destroyModule() {
|
|
3744
|
+
const debugMode = this._config?.debug;
|
|
3745
|
+
// MutationObserver 해제
|
|
3746
|
+
if (this.mutationObserver) {
|
|
3747
|
+
this.mutationObserver.disconnect();
|
|
3748
|
+
this.mutationObserver = null;
|
|
3749
|
+
}
|
|
3750
|
+
// 모든 슬롯 정리
|
|
3751
|
+
this.slots.clear();
|
|
3752
|
+
// 다른 리소스들 정리
|
|
3753
|
+
this.advertisementEventTracker = null;
|
|
3754
|
+
this.adRenderer = null;
|
|
3755
|
+
this._isReady = false;
|
|
3756
|
+
this._config = null;
|
|
3757
|
+
if (debugMode) {
|
|
3758
|
+
console.log('🗑️ Ads module destroyed');
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
2757
3761
|
}
|
|
2758
3762
|
|
|
2759
3763
|
/**
|
|
@@ -2775,10 +3779,6 @@ class ConfigModule {
|
|
|
2775
3779
|
timeout: 30000,
|
|
2776
3780
|
debug: false,
|
|
2777
3781
|
modules: ['ads', 'events', 'config'],
|
|
2778
|
-
validateOnInit: false,
|
|
2779
|
-
fallbackMode: true,
|
|
2780
|
-
offlineMode: false,
|
|
2781
|
-
productionMode: false,
|
|
2782
3782
|
...config
|
|
2783
3783
|
};
|
|
2784
3784
|
// 사용자가 baseUrl을 제공한 경우 endpoints에 설정
|
|
@@ -2790,7 +3790,7 @@ class ConfigModule {
|
|
|
2790
3790
|
console.log('✅ Config module initialized (sync mode)', {
|
|
2791
3791
|
modules: this._config.modules,
|
|
2792
3792
|
endpoint: endpoints.getBaseUrl(),
|
|
2793
|
-
mode:
|
|
3793
|
+
mode: 'development'
|
|
2794
3794
|
});
|
|
2795
3795
|
}
|
|
2796
3796
|
}
|
|
@@ -2976,10 +3976,6 @@ class AdStage {
|
|
|
2976
3976
|
timeout: 30000,
|
|
2977
3977
|
debug: false,
|
|
2978
3978
|
modules: ['ads', 'events', 'config'],
|
|
2979
|
-
validateOnInit: false,
|
|
2980
|
-
fallbackMode: true,
|
|
2981
|
-
offlineMode: false,
|
|
2982
|
-
productionMode: false,
|
|
2983
3979
|
...config
|
|
2984
3980
|
};
|
|
2985
3981
|
// 🔧 baseUrl이 설정된 경우 전역 endpoints 객체 업데이트
|
|
@@ -3003,7 +3999,7 @@ class AdStage {
|
|
|
3003
3999
|
version: '2.0.0',
|
|
3004
4000
|
modules: enabledModules,
|
|
3005
4001
|
apiKey: config.apiKey.substring(0, 8) + '...',
|
|
3006
|
-
mode:
|
|
4002
|
+
mode: 'development'
|
|
3007
4003
|
});
|
|
3008
4004
|
}
|
|
3009
4005
|
}
|
|
@@ -3050,6 +4046,34 @@ class AdStage {
|
|
|
3050
4046
|
}
|
|
3051
4047
|
}
|
|
3052
4048
|
}
|
|
4049
|
+
/**
|
|
4050
|
+
* 디버그용 메서드들
|
|
4051
|
+
*/
|
|
4052
|
+
AdStage.debug = {
|
|
4053
|
+
/**
|
|
4054
|
+
* 모든 viewable 추적 데이터 초기화
|
|
4055
|
+
*/
|
|
4056
|
+
clearAllViewable: () => {
|
|
4057
|
+
ViewableEventTracker.clear();
|
|
4058
|
+
console.log('✅ AdStage Debug: 모든 viewable 추적 데이터 초기화됨');
|
|
4059
|
+
},
|
|
4060
|
+
/**
|
|
4061
|
+
* 특정 광고의 viewable 추적 초기화
|
|
4062
|
+
*/
|
|
4063
|
+
clearAdViewable: (adId, slotId) => {
|
|
4064
|
+
ViewableEventTracker.clearAdViewable(adId, slotId);
|
|
4065
|
+
console.log(`✅ AdStage Debug: 광고 ${adId}(${slotId})의 viewable 추적 초기화됨`);
|
|
4066
|
+
},
|
|
4067
|
+
/**
|
|
4068
|
+
* 현재 추적 중인 viewable 상태 확인
|
|
4069
|
+
*/
|
|
4070
|
+
getViewableStatus: () => {
|
|
4071
|
+
console.log('📊 AdStage Debug: 현재 viewable 추적 상태', {
|
|
4072
|
+
trackedCount: ViewableEventTracker.viewableTracker.size,
|
|
4073
|
+
trackedItems: Array.from(ViewableEventTracker.viewableTracker)
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
3053
4077
|
|
|
3054
4078
|
var jsxRuntime = {exports: {}};
|
|
3055
4079
|
|
|
@@ -3211,5 +4235,9 @@ function useAdStageInstance() {
|
|
|
3211
4235
|
// 버전 정보
|
|
3212
4236
|
const SDK_VERSION = '2.0.0';
|
|
3213
4237
|
const SUPPORTED_MODULES = ['ads', 'events', 'config'];
|
|
4238
|
+
// 브라우저 환경에서 전역 객체로 노출 (디버깅용)
|
|
4239
|
+
if (typeof window !== 'undefined') {
|
|
4240
|
+
window.AdStage = AdStage;
|
|
4241
|
+
}
|
|
3214
4242
|
|
|
3215
4243
|
export { AdStage, AdStageProvider, SDK_VERSION, SUPPORTED_MODULES, useAdStageContext, useAdStageInstance };
|