@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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdRenderer - 광고 렌더링 팩토리 클래스
|
|
3
|
+
* 광고 타입별로 적절한 렌더러를 생성하고 관리
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AdSlot, Advertisement, AdType } from '../../types/advertisement';
|
|
7
|
+
import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
|
|
8
|
+
import { IAdRenderer } from './interfaces/i-ad-renderer';
|
|
9
|
+
import { BannerAdRenderer } from './renderers/banner-ad-renderer';
|
|
10
|
+
import { TextAdRenderer } from './renderers/text-ad-renderer';
|
|
11
|
+
import { VideoAdRenderer } from './renderers/video-ad-renderer';
|
|
12
|
+
import { NativeAdRenderer } from './renderers/native-ad-renderer';
|
|
13
|
+
import { InterstitialAdRenderer } from './renderers/interstitial-ad-renderer';
|
|
14
|
+
|
|
15
|
+
export class AdRenderer {
|
|
16
|
+
private debug: boolean;
|
|
17
|
+
private advertisementEventTracker: AdvertisementEventTracker | null;
|
|
18
|
+
private renderers: Map<AdType, IAdRenderer> = new Map();
|
|
19
|
+
|
|
20
|
+
constructor(debug: boolean = false, advertisementEventTracker?: AdvertisementEventTracker | null) {
|
|
21
|
+
this.debug = debug;
|
|
22
|
+
this.advertisementEventTracker = advertisementEventTracker || null;
|
|
23
|
+
|
|
24
|
+
// 각 광고 타입별 렌더러 초기화
|
|
25
|
+
this.initializeRenderers();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 광고 타입별 렌더러 초기화
|
|
30
|
+
*/
|
|
31
|
+
private initializeRenderers(): void {
|
|
32
|
+
this.renderers.set(AdType.BANNER, new BannerAdRenderer(this.debug, this.advertisementEventTracker));
|
|
33
|
+
this.renderers.set(AdType.TEXT, new TextAdRenderer(this.debug, this.advertisementEventTracker));
|
|
34
|
+
this.renderers.set(AdType.VIDEO, new VideoAdRenderer(this.debug, this.advertisementEventTracker));
|
|
35
|
+
this.renderers.set(AdType.NATIVE, new NativeAdRenderer(this.debug, this.advertisementEventTracker));
|
|
36
|
+
this.renderers.set(AdType.INTERSTITIAL, new InterstitialAdRenderer(this.debug, this.advertisementEventTracker));
|
|
37
|
+
|
|
38
|
+
if (this.debug) {
|
|
39
|
+
console.log(`🏭 AdRenderer factory initialized with ${this.renderers.size} renderers`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 광고 요소를 동기적으로 생성해서 반환 (크기 측정 등을 위한 helper)
|
|
45
|
+
*/
|
|
46
|
+
createAdElement(slot: AdSlot, advertisement: Advertisement): HTMLElement {
|
|
47
|
+
const renderer = this.getRenderer(slot.adType);
|
|
48
|
+
|
|
49
|
+
// 기본 광고 요소 생성
|
|
50
|
+
const adElement = document.createElement('div');
|
|
51
|
+
adElement.className = `adstage-ad adstage-${String(slot.adType).toLowerCase()}`;
|
|
52
|
+
adElement.setAttribute('data-adstage-ad-id', advertisement._id);
|
|
53
|
+
adElement.setAttribute('data-adstage-slot-id', slot.id);
|
|
54
|
+
|
|
55
|
+
// 광고 타입별 기본 컨테이너 설정
|
|
56
|
+
const { width, height } = renderer.calculateAdSize(
|
|
57
|
+
adElement,
|
|
58
|
+
slot.config || {},
|
|
59
|
+
advertisement
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
adElement.style.width = width;
|
|
63
|
+
adElement.style.height = height;
|
|
64
|
+
adElement.style.display = 'block';
|
|
65
|
+
|
|
66
|
+
// 간단한 내용 설정 (크기 측정용)
|
|
67
|
+
switch (slot.adType) {
|
|
68
|
+
case AdType.BANNER:
|
|
69
|
+
if (advertisement.imageUrl) {
|
|
70
|
+
const img = document.createElement('img');
|
|
71
|
+
img.src = advertisement.imageUrl;
|
|
72
|
+
img.style.width = '100%';
|
|
73
|
+
img.style.height = '100%';
|
|
74
|
+
img.style.objectFit = 'cover';
|
|
75
|
+
adElement.appendChild(img);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case AdType.VIDEO:
|
|
79
|
+
if (advertisement.videoUrl) {
|
|
80
|
+
const video = document.createElement('video');
|
|
81
|
+
video.src = advertisement.videoUrl;
|
|
82
|
+
video.style.width = '100%';
|
|
83
|
+
video.style.height = '100%';
|
|
84
|
+
adElement.appendChild(video);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case AdType.TEXT:
|
|
88
|
+
if (advertisement.textContent) {
|
|
89
|
+
const textDiv = document.createElement('div');
|
|
90
|
+
textDiv.textContent = advertisement.textContent || '';
|
|
91
|
+
textDiv.style.padding = '8px';
|
|
92
|
+
adElement.appendChild(textDiv);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
default:
|
|
96
|
+
// 기본 placeholder
|
|
97
|
+
adElement.style.border = '1px dashed #ccc';
|
|
98
|
+
adElement.style.backgroundColor = '#f9f9f9';
|
|
99
|
+
adElement.textContent = `${slot.adType} Ad`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return adElement;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 광고 타입에 따른 렌더러 획득
|
|
107
|
+
*/
|
|
108
|
+
public getRenderer(adType: AdType): IAdRenderer {
|
|
109
|
+
const renderer = this.renderers.get(adType);
|
|
110
|
+
if (!renderer) {
|
|
111
|
+
throw new Error(`No renderer found for ad type: ${adType}`);
|
|
112
|
+
}
|
|
113
|
+
return renderer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Placeholder(슬롯 컨테이너) 생성
|
|
118
|
+
*/
|
|
119
|
+
createPlaceholder(
|
|
120
|
+
container: HTMLElement,
|
|
121
|
+
slotId: string,
|
|
122
|
+
type: AdType,
|
|
123
|
+
options: any,
|
|
124
|
+
config?: any
|
|
125
|
+
): void {
|
|
126
|
+
const renderer = this.getRenderer(type);
|
|
127
|
+
renderer.createPlaceholder(container, slotId, options, config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 배너 광고를 위한 컨테이너 최적화 (배너 전용)
|
|
132
|
+
*/
|
|
133
|
+
async optimizeContainerForBannerAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
134
|
+
if (slot.adType !== AdType.BANNER) {
|
|
135
|
+
if (this.debug) {
|
|
136
|
+
console.warn(`⚠️ Container optimization is only supported for BANNER ads, got: ${slot.adType}`);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER) as BannerAdRenderer;
|
|
142
|
+
await bannerRenderer.optimizeContainerForBannerAds(slot, advertisements);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
147
|
+
*/
|
|
148
|
+
async renderAdSlider(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
149
|
+
const renderer = this.getRenderer(slot.adType);
|
|
150
|
+
await renderer.renderMultipleAds(slot, advertisements);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 광고 렌더링 (단일 광고용)
|
|
155
|
+
*/
|
|
156
|
+
async renderAd(slot: AdSlot): Promise<void> {
|
|
157
|
+
if (!slot.advertisement) {
|
|
158
|
+
throw new Error('No advertisement to render');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const renderer = this.getRenderer(slot.adType);
|
|
162
|
+
await renderer.renderAdElement(slot, slot.advertisement);
|
|
163
|
+
slot.isLoaded = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 광고 요소 렌더링 (기본 구현) - 호환성을 위해 유지
|
|
168
|
+
*/
|
|
169
|
+
async renderAdElement(slot: AdSlot, ad: Advertisement): Promise<void> {
|
|
170
|
+
const renderer = this.getRenderer(slot.adType);
|
|
171
|
+
await renderer.renderAdElement(slot, ad);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fallback 광고 렌더링 - 컨테이너 접기/생성
|
|
176
|
+
*/
|
|
177
|
+
renderFallback(slot: AdSlot): void {
|
|
178
|
+
const renderer = this.getRenderer(slot.adType);
|
|
179
|
+
renderer.renderFallback(slot);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 광고 타입별 기본 높이 반환
|
|
184
|
+
*/
|
|
185
|
+
getDefaultHeightForAdType(type: AdType): string {
|
|
186
|
+
const renderer = this.getRenderer(type);
|
|
187
|
+
return renderer.getDefaultHeight();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 컨테이너와 광고 타입에 따른 스마트한 크기 계산
|
|
192
|
+
*/
|
|
193
|
+
calculateAdSize(container: HTMLElement, type: AdType, options: any, config: any): { width: string; height: string } {
|
|
194
|
+
const renderer = this.getRenderer(type);
|
|
195
|
+
return renderer.calculateAdSize(container, options, config);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 이미지 로드 및 실제 크기 획득 (배너 전용)
|
|
200
|
+
*/
|
|
201
|
+
loadImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const img = new Image();
|
|
204
|
+
img.onload = () => {
|
|
205
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
206
|
+
};
|
|
207
|
+
img.onerror = () => {
|
|
208
|
+
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
209
|
+
};
|
|
210
|
+
img.src = imageUrl;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 배너 이미지 최적화 렌더링 (배너 전용)
|
|
216
|
+
*/
|
|
217
|
+
async renderOptimizedBannerImage(
|
|
218
|
+
container: HTMLElement,
|
|
219
|
+
advertisement: Advertisement,
|
|
220
|
+
slot: AdSlot
|
|
221
|
+
): Promise<HTMLImageElement> {
|
|
222
|
+
if (slot.adType !== AdType.BANNER) {
|
|
223
|
+
throw new Error('renderOptimizedBannerImage is only supported for BANNER ads');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER) as BannerAdRenderer;
|
|
227
|
+
return await bannerRenderer.renderOptimizedBannerImage(container, advertisement, slot);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 여러 광고의 최적 컨테이너 크기 계산 (배너 전용)
|
|
232
|
+
*/
|
|
233
|
+
async calculateOptimalContainerSize(
|
|
234
|
+
advertisements: Advertisement[],
|
|
235
|
+
containerWidth: number,
|
|
236
|
+
adType: AdType
|
|
237
|
+
): Promise<{ width: string; height: string; aspectRatio: number }> {
|
|
238
|
+
if (adType !== AdType.BANNER) {
|
|
239
|
+
const renderer = this.getRenderer(adType);
|
|
240
|
+
return {
|
|
241
|
+
width: '100%',
|
|
242
|
+
height: renderer.getDefaultHeight(),
|
|
243
|
+
aspectRatio: 16 / 9
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const bannerRenderer = this.getRenderer(AdType.BANNER) as BannerAdRenderer;
|
|
248
|
+
return await bannerRenderer.calculateOptimalContainerSize(advertisements, containerWidth);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 디버그 로그 출력
|
|
253
|
+
*/
|
|
254
|
+
private log(message: string, ...args: any[]): void {
|
|
255
|
+
if (this.debug) {
|
|
256
|
+
console.log(`[AdRenderer] ${message}`, ...args);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -12,7 +12,7 @@ import { SimpleViewabilityTracker } from '../../managers/ads/viewability-tracker
|
|
|
12
12
|
import { endpoints } from '../../constants/endpoints';
|
|
13
13
|
import { ApiHeaders } from '../../utils/api-headers';
|
|
14
14
|
// 새로 분리된 클래스들
|
|
15
|
-
import { AdRenderer } from './
|
|
15
|
+
import { AdRenderer } from './ad-renderer';
|
|
16
16
|
|
|
17
17
|
export interface AdOptions {
|
|
18
18
|
width?: string | number;
|
|
@@ -23,6 +23,19 @@ export interface AdOptions {
|
|
|
23
23
|
style?: string;
|
|
24
24
|
autoplay?: boolean;
|
|
25
25
|
muted?: boolean;
|
|
26
|
+
loop?: boolean;
|
|
27
|
+
playsinline?: boolean;
|
|
28
|
+
controls?: boolean; // 비디오 컨트롤 표시 여부
|
|
29
|
+
hideControls?: boolean; // 모든 컨트롤 숨기기
|
|
30
|
+
customControls?: {
|
|
31
|
+
hidePlayButton?: boolean;
|
|
32
|
+
hideProgressBar?: boolean;
|
|
33
|
+
hideCurrentTime?: boolean;
|
|
34
|
+
hideRemainingTime?: boolean;
|
|
35
|
+
hideVolumeSlider?: boolean;
|
|
36
|
+
hideMuteButton?: boolean;
|
|
37
|
+
hideFullscreenButton?: boolean;
|
|
38
|
+
};
|
|
26
39
|
onClick?: (adData: any) => void;
|
|
27
40
|
// 특정 광고 ID 지정
|
|
28
41
|
adId?: string;
|
|
@@ -40,6 +53,8 @@ export class AdsModule implements BaseModule {
|
|
|
40
53
|
private advertisementEventTracker: AdvertisementEventTracker | null = null;
|
|
41
54
|
// 렌더링 관련
|
|
42
55
|
private adRenderer: AdRenderer | null = null;
|
|
56
|
+
// DOM 변화 감지를 위한 MutationObserver
|
|
57
|
+
private mutationObserver: MutationObserver | null = null;
|
|
43
58
|
|
|
44
59
|
/**
|
|
45
60
|
* Ads 모듈 초기화 (동기)
|
|
@@ -58,10 +73,13 @@ export class AdsModule implements BaseModule {
|
|
|
58
73
|
// AdRenderer 초기화
|
|
59
74
|
this.adRenderer = new AdRenderer(config.debug || false, this.advertisementEventTracker);
|
|
60
75
|
|
|
76
|
+
// DOM 변화 감지를 위한 MutationObserver 설정
|
|
77
|
+
this.setupAutoCleanup();
|
|
78
|
+
|
|
61
79
|
this._isReady = true;
|
|
62
80
|
|
|
63
81
|
if (config.debug) {
|
|
64
|
-
console.log('🎯 Ads module initialized (sync mode)');
|
|
82
|
+
console.log('🎯 Ads module initialized (sync mode) with auto-cleanup');
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -82,7 +100,7 @@ export class AdsModule implements BaseModule {
|
|
|
82
100
|
/**
|
|
83
101
|
* 배너 광고 생성 (동기)
|
|
84
102
|
*/
|
|
85
|
-
banner(containerId: string, options?: AdOptions): string {
|
|
103
|
+
banner(containerId: string | HTMLElement, options?: AdOptions): string {
|
|
86
104
|
this.ensureReady();
|
|
87
105
|
|
|
88
106
|
const adstageOptions = {
|
|
@@ -105,7 +123,7 @@ export class AdsModule implements BaseModule {
|
|
|
105
123
|
/**
|
|
106
124
|
* 텍스트 광고 생성 (동기)
|
|
107
125
|
*/
|
|
108
|
-
text(containerId: string, options?: AdOptions): string {
|
|
126
|
+
text(containerId: string | HTMLElement, options?: AdOptions): string {
|
|
109
127
|
this.ensureReady();
|
|
110
128
|
|
|
111
129
|
const adstageOptions = {
|
|
@@ -124,17 +142,25 @@ export class AdsModule implements BaseModule {
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
/**
|
|
127
|
-
* 비디오 광고 생성 (동기)
|
|
145
|
+
* 비디오 광고 생성 (동기) - 단일 비디오만 지원
|
|
128
146
|
*/
|
|
129
|
-
video(containerId: string, options?: AdOptions): string {
|
|
147
|
+
video(containerId: string | HTMLElement, options?: AdOptions): string {
|
|
130
148
|
this.ensureReady();
|
|
131
149
|
|
|
132
150
|
const adstageOptions = {
|
|
133
151
|
width: options?.width || 640,
|
|
134
152
|
height: options?.height || 360,
|
|
135
|
-
autoplay: options?.autoplay
|
|
136
|
-
muted: options?.muted
|
|
137
|
-
|
|
153
|
+
autoplay: options?.autoplay !== undefined ? options.autoplay : true, // 기본값 true (사용자 요구사항)
|
|
154
|
+
muted: options?.muted !== undefined ? options.muted : true, // 기본값 true
|
|
155
|
+
loop: options?.loop !== undefined ? options.loop : true, // 기본값 true (사용자 요구사항)
|
|
156
|
+
playsinline: options?.playsinline !== false, // 기본값 true
|
|
157
|
+
controls: options?.controls !== undefined ? options.controls : false, // 기본값 false (사용자 요구사항)
|
|
158
|
+
hideControls: options?.hideControls || false,
|
|
159
|
+
customControls: options?.customControls,
|
|
160
|
+
autoSlide: false, // 비디오는 슬라이드 비활성화
|
|
161
|
+
maxAds: 1, // 하나의 비디오만 가져오기
|
|
162
|
+
onClick: options?.onClick,
|
|
163
|
+
...(options?.adId && { adId: options.adId }) // 특정 비디오 ID가 있으면 사용
|
|
138
164
|
};
|
|
139
165
|
|
|
140
166
|
return this.createAd(containerId, AdType.VIDEO, adstageOptions);
|
|
@@ -226,21 +252,73 @@ export class AdsModule implements BaseModule {
|
|
|
226
252
|
/**
|
|
227
253
|
* 광고 생성 내부 메소드 (동기 + Lazy 로딩)
|
|
228
254
|
*/
|
|
229
|
-
private createAd(containerId: string, type: AdType, options: any): string {
|
|
255
|
+
private createAd(containerId: string | HTMLElement, type: AdType, options: any): string {
|
|
230
256
|
if (!this._config?.apiKey) {
|
|
231
257
|
throw new Error('API key not configured');
|
|
232
258
|
}
|
|
233
259
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
260
|
+
// 즉시 슬롯 ID 생성 (개발자에게 바로 반환)
|
|
261
|
+
const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
262
|
+
|
|
263
|
+
// 비동기로 광고 생성 처리 (디바운싱 및 재시도 로직 포함)
|
|
264
|
+
this.createAdWithRetry(containerId, type, options, slotId, 0);
|
|
265
|
+
|
|
266
|
+
return slotId;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private async createAdWithRetry(containerId: string | HTMLElement, type: AdType, options: any, slotId: string, attempt: number): Promise<void> {
|
|
270
|
+
const maxAttempts = 5;
|
|
271
|
+
const delays = [0, 50, 100, 200, 500]; // 점진적 지연
|
|
272
|
+
|
|
273
|
+
let container: HTMLElement | null = null;
|
|
274
|
+
let containerIdString: string;
|
|
275
|
+
|
|
276
|
+
// HTMLElement인지 string인지 구분
|
|
277
|
+
if (typeof containerId === 'string') {
|
|
278
|
+
// string인 경우 DOM에서 찾기
|
|
279
|
+
containerIdString = containerId;
|
|
280
|
+
container = document.getElementById(containerId);
|
|
281
|
+
|
|
282
|
+
if (!container) {
|
|
283
|
+
if (attempt < maxAttempts - 1) {
|
|
284
|
+
if (this._config?.debug) {
|
|
285
|
+
console.warn(`Container not found: ${containerId}. Retrying in ${delays[attempt + 1]}ms... (attempt ${attempt + 1}/${maxAttempts})`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
this.createAdWithRetry(containerId, type, options, slotId, attempt + 1);
|
|
290
|
+
}, delays[attempt + 1]);
|
|
291
|
+
return;
|
|
292
|
+
} else {
|
|
293
|
+
console.error(`Container not found after ${maxAttempts} attempts: ${containerId}`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// HTMLElement인 경우 직접 사용
|
|
299
|
+
container = containerId;
|
|
300
|
+
containerIdString = container.id || `auto-${slotId}`;
|
|
301
|
+
|
|
302
|
+
// ID가 없으면 자동 생성
|
|
303
|
+
if (!container.id) {
|
|
304
|
+
container.id = containerIdString;
|
|
305
|
+
}
|
|
237
306
|
}
|
|
238
307
|
|
|
239
|
-
//
|
|
240
|
-
|
|
308
|
+
// 컨테이너를 찾았으면 광고 생성
|
|
309
|
+
try {
|
|
310
|
+
this.createAdInternal(containerIdString, type, options, slotId, container);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('광고 생성 중 오류 발생:', error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
241
315
|
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
private createAdInternal(containerId: string, type: AdType, options: any, slotId: string, container: HTMLElement): void {
|
|
317
|
+
// 컨테이너에 슬롯 ID 속성 추가 (MutationObserver가 감지할 수 있도록)
|
|
318
|
+
container.setAttribute('data-adstage-slot-id', slotId);
|
|
319
|
+
|
|
320
|
+
// 즉시 placeholder 생성 (AdRenderer 위임)
|
|
321
|
+
this.adRenderer?.createPlaceholder(container, slotId, type, options, this._config);
|
|
244
322
|
|
|
245
323
|
// 광고 슬롯 정보 저장
|
|
246
324
|
const slot: AdSlot = {
|
|
@@ -248,7 +326,7 @@ export class AdsModule implements BaseModule {
|
|
|
248
326
|
containerId,
|
|
249
327
|
adType: type,
|
|
250
328
|
width: options.width || '100%',
|
|
251
|
-
height: options.height || 250,
|
|
329
|
+
height: options.height || (type === AdType.TEXT ? 'auto' : 250), // 텍스트 광고는 콘텐츠 높이에 맞춤
|
|
252
330
|
isLoaded: false,
|
|
253
331
|
isVisible: false,
|
|
254
332
|
refreshRate: 0,
|
|
@@ -257,7 +335,7 @@ export class AdsModule implements BaseModule {
|
|
|
257
335
|
advertisement: undefined, // 나중에 로드
|
|
258
336
|
config: { type, ...options },
|
|
259
337
|
load: async () => this.fetchAdData(type, options).then(ads => ads[0] || null),
|
|
260
|
-
|
|
338
|
+
render: (ad: Advertisement) => this.adRenderer?.renderAdElement(slot, ad),
|
|
261
339
|
refresh: async () => this.refreshAdSlot(slot),
|
|
262
340
|
destroy: () => this.destroy(slotId)
|
|
263
341
|
};
|
|
@@ -272,8 +350,6 @@ export class AdsModule implements BaseModule {
|
|
|
272
350
|
if (this.advertisementEventTracker && this._config?.debug) {
|
|
273
351
|
console.log(`📊 Advertisement event tracking enabled for slot: ${slotId}`);
|
|
274
352
|
}
|
|
275
|
-
|
|
276
|
-
return slotId;
|
|
277
353
|
}
|
|
278
354
|
|
|
279
355
|
/**
|
|
@@ -495,4 +571,109 @@ export class AdsModule implements BaseModule {
|
|
|
495
571
|
throw new Error('Ads module not initialized. Call AdStage.init() first.');
|
|
496
572
|
}
|
|
497
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* DOM 변화 감지를 통한 자동 정리 설정
|
|
577
|
+
*/
|
|
578
|
+
private setupAutoCleanup(): void {
|
|
579
|
+
// 브라우저 환경에서만 실행
|
|
580
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 기존 observer가 있으면 해제
|
|
585
|
+
if (this.mutationObserver) {
|
|
586
|
+
this.mutationObserver.disconnect();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 새로운 MutationObserver 생성
|
|
590
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
591
|
+
mutations.forEach((mutation) => {
|
|
592
|
+
// 제거된 노드들 확인
|
|
593
|
+
mutation.removedNodes.forEach((node) => {
|
|
594
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
595
|
+
this.handleRemovedElement(node as Element);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// document 전체를 관찰 (childList와 subtree 옵션 사용)
|
|
602
|
+
this.mutationObserver.observe(document.body, {
|
|
603
|
+
childList: true,
|
|
604
|
+
subtree: true
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (this._config?.debug) {
|
|
608
|
+
console.log('🔍 Auto-cleanup MutationObserver enabled');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* 제거된 요소에서 광고 슬롯 정리
|
|
614
|
+
*/
|
|
615
|
+
private handleRemovedElement(element: Element): void {
|
|
616
|
+
// 제거된 요소가 광고 컨테이너인지 확인
|
|
617
|
+
const slotId = element.getAttribute('data-adstage-slot-id');
|
|
618
|
+
if (slotId) {
|
|
619
|
+
this.autoDestroy(slotId);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 제거된 요소의 하위에 광고 컨테이너가 있는지 확인
|
|
624
|
+
const adContainers = element.querySelectorAll('[data-adstage-slot-id]');
|
|
625
|
+
adContainers.forEach((container) => {
|
|
626
|
+
const containerSlotId = container.getAttribute('data-adstage-slot-id');
|
|
627
|
+
if (containerSlotId) {
|
|
628
|
+
this.autoDestroy(containerSlotId);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 자동 정리 (로그 없이 조용히 정리)
|
|
635
|
+
*/
|
|
636
|
+
private autoDestroy(slotId: string): void {
|
|
637
|
+
const slot = this.slots.get(slotId);
|
|
638
|
+
if (slot) {
|
|
639
|
+
try {
|
|
640
|
+
// 슬롯 정리 (로그 출력 최소화)
|
|
641
|
+
this.slots.delete(slotId);
|
|
642
|
+
|
|
643
|
+
if (this._config?.debug) {
|
|
644
|
+
console.log(`🧹 Auto-cleanup: slot ${slotId} removed`);
|
|
645
|
+
}
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (this._config?.debug) {
|
|
648
|
+
console.warn(`Auto-cleanup failed for slot ${slotId}:`, error);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 모듈 종료 시 정리
|
|
656
|
+
*/
|
|
657
|
+
destroyModule(): void {
|
|
658
|
+
const debugMode = this._config?.debug;
|
|
659
|
+
|
|
660
|
+
// MutationObserver 해제
|
|
661
|
+
if (this.mutationObserver) {
|
|
662
|
+
this.mutationObserver.disconnect();
|
|
663
|
+
this.mutationObserver = null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 모든 슬롯 정리
|
|
667
|
+
this.slots.clear();
|
|
668
|
+
|
|
669
|
+
// 다른 리소스들 정리
|
|
670
|
+
this.advertisementEventTracker = null;
|
|
671
|
+
this.adRenderer = null;
|
|
672
|
+
this._isReady = false;
|
|
673
|
+
this._config = null;
|
|
674
|
+
|
|
675
|
+
if (debugMode) {
|
|
676
|
+
console.log('🗑️ Ads module destroyed');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
498
679
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 광고 렌더러 공통 인터페이스
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AdSlot, Advertisement, AdType } from '../../../types/advertisement';
|
|
6
|
+
|
|
7
|
+
export interface IAdRenderer {
|
|
8
|
+
/**
|
|
9
|
+
* 광고 타입 (이 렌더러가 처리하는 타입)
|
|
10
|
+
*/
|
|
11
|
+
readonly adType: AdType;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 디버그 모드 여부
|
|
15
|
+
*/
|
|
16
|
+
readonly debug: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Placeholder(슬롯 컨테이너) 생성
|
|
20
|
+
*/
|
|
21
|
+
createPlaceholder(
|
|
22
|
+
container: HTMLElement,
|
|
23
|
+
slotId: string,
|
|
24
|
+
options: any,
|
|
25
|
+
config?: any
|
|
26
|
+
): void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 광고 요소 렌더링
|
|
30
|
+
*/
|
|
31
|
+
renderAdElement(slot: AdSlot, advertisement: Advertisement): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 다중 광고 렌더링 (슬라이더/전환 효과 포함)
|
|
35
|
+
*/
|
|
36
|
+
renderMultipleAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fallback 렌더링 (광고가 없을 때)
|
|
40
|
+
*/
|
|
41
|
+
renderFallback(slot: AdSlot): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 광고 크기 계산
|
|
45
|
+
*/
|
|
46
|
+
calculateAdSize(container: HTMLElement, options: any, config: any): { width: string; height: string };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 광고 타입별 기본 높이 반환
|
|
50
|
+
*/
|
|
51
|
+
getDefaultHeight(): string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AdRenderOptions {
|
|
55
|
+
width?: string | number;
|
|
56
|
+
height?: number;
|
|
57
|
+
autoSlide?: boolean;
|
|
58
|
+
slideInterval?: number;
|
|
59
|
+
autoplay?: boolean;
|
|
60
|
+
muted?: boolean;
|
|
61
|
+
loop?: boolean;
|
|
62
|
+
controls?: boolean;
|
|
63
|
+
hideControls?: boolean;
|
|
64
|
+
customControls?: {
|
|
65
|
+
hidePlayButton?: boolean;
|
|
66
|
+
hideProgressBar?: boolean;
|
|
67
|
+
hideCurrentTime?: boolean;
|
|
68
|
+
hideRemainingTime?: boolean;
|
|
69
|
+
hideVolumeSlider?: boolean;
|
|
70
|
+
hideMuteButton?: boolean;
|
|
71
|
+
hideFullscreenButton?: boolean;
|
|
72
|
+
};
|
|
73
|
+
maxLines?: number;
|
|
74
|
+
style?: string;
|
|
75
|
+
onClick?: (adData: any) => void;
|
|
76
|
+
placeholderMode?: 'invisible' | 'transparent' | 'subtle' | 'minimal' | 'debug' | 'legacy';
|
|
77
|
+
}
|