@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.
Files changed (38) hide show
  1. package/README.md +59 -0
  2. package/dist/index.cjs.js +2073 -1045
  3. package/dist/index.d.ts +55 -12
  4. package/dist/index.esm.js +2073 -1045
  5. package/dist/index.standalone.js +2073 -1045
  6. package/package.json +1 -1
  7. package/src/constants/endpoints.ts +0 -1
  8. package/src/core/{AdStage.ts → adstage.ts} +36 -8
  9. package/src/index.ts +9 -3
  10. package/src/managers/ads/advertisement-event-tracker.ts +15 -11
  11. package/src/managers/ads/carousel-slider-manager.ts +90 -12
  12. package/src/managers/ads/slider-event-tracker.ts +57 -0
  13. package/src/managers/ads/text-transition-manager.ts +91 -26
  14. package/src/modules/ads/ad-renderer.ts +259 -0
  15. package/src/modules/ads/{AdsModule.ts → ads-module.ts} +202 -21
  16. package/src/modules/ads/interfaces/i-ad-renderer.ts +77 -0
  17. package/src/modules/ads/renderers/banner-ad-renderer.ts +414 -0
  18. package/src/modules/ads/renderers/base-ad-renderer.ts +340 -0
  19. package/src/modules/ads/renderers/interstitial-ad-renderer.ts +256 -0
  20. package/src/modules/ads/renderers/native-ad-renderer.ts +154 -0
  21. package/src/modules/ads/renderers/text-ad-renderer.ts +120 -0
  22. package/src/modules/ads/renderers/video-ad-renderer.ts +433 -0
  23. package/src/modules/config/{ConfigModule.ts → config-module.ts} +1 -5
  24. package/src/react/{AdStageProvider.tsx → ad-stage-provider.tsx} +1 -1
  25. package/src/react/index.ts +2 -2
  26. package/src/types/config.ts +2 -184
  27. package/src/utils/ad-click-handler.ts +155 -0
  28. package/src/utils/text-ad-utils.ts +37 -0
  29. package/src/dummy/ads_dummy.json +0 -84
  30. package/src/modules/ads/AdRenderer.ts +0 -735
  31. package/src/renderers/banner-renderer.ts +0 -35
  32. package/src/renderers/base-renderer.ts +0 -209
  33. package/src/renderers/index.ts +0 -71
  34. package/src/renderers/interstitial-renderer.ts +0 -70
  35. package/src/renderers/native-renderer.ts +0 -35
  36. package/src/renderers/text-renderer.ts +0 -94
  37. package/src/renderers/video-renderer.ts +0 -63
  38. /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 './AdRenderer';
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 || false,
136
- muted: options?.muted || true,
137
- onClick: options?.onClick
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
- const container = document.getElementById(containerId);
235
- if (!container) {
236
- throw new Error(`Container not found: ${containerId}`);
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
- // 고유한 슬롯 ID 생성
240
- const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
308
+ // 컨테이너를 찾았으면 광고 생성
309
+ try {
310
+ this.createAdInternal(containerIdString, type, options, slotId, container);
311
+ } catch (error) {
312
+ console.error('광고 생성 중 오류 발생:', error);
313
+ }
314
+ }
241
315
 
242
- // 즉시 placeholder 생성 (AdRenderer 위임)
243
- this.adRenderer?.createPlaceholder(container, slotId, type, options, this._config);
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
- render: (ad: Advertisement) => this.adRenderer?.renderAdElement(slot, ad),
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
+ }