@adstage/web-sdk 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,340 @@
1
+ /**
2
+ * 베이스 광고 렌더러 - 공통 기능 구현
3
+ */
4
+
5
+ import { AdSlot, Advertisement, AdType, AdEventType } from '../../../types/advertisement';
6
+ import { AdvertisementEventTracker } from '../../../managers/ads/advertisement-event-tracker';
7
+ import { ViewableEventTracker } from '../../../managers/ads/viewable-event-tracker';
8
+ import { IAdRenderer, AdRenderOptions } from '../interfaces/i-ad-renderer';
9
+
10
+ export abstract class BaseAdRenderer implements IAdRenderer {
11
+ public readonly debug: boolean;
12
+ protected advertisementEventTracker: AdvertisementEventTracker | null;
13
+
14
+ constructor(
15
+ public readonly adType: AdType,
16
+ debug: boolean = false,
17
+ advertisementEventTracker?: AdvertisementEventTracker | null
18
+ ) {
19
+ this.debug = debug;
20
+ this.advertisementEventTracker = advertisementEventTracker || null;
21
+ }
22
+
23
+ /**
24
+ * Placeholder(슬롯 컨테이너) 생성 - 공통 구현
25
+ */
26
+ createPlaceholder(
27
+ container: HTMLElement,
28
+ slotId: string,
29
+ options: AdRenderOptions,
30
+ config?: any
31
+ ): void {
32
+ const adElement = document.createElement('div');
33
+ adElement.id = slotId;
34
+ adElement.className = `adstage-slot adstage-${String(this.adType).toLowerCase()}`;
35
+ adElement.setAttribute('data-adstage-container', 'true');
36
+ adElement.setAttribute('data-adstage-type', String(this.adType));
37
+ adElement.setAttribute('data-adstage-slot', slotId);
38
+
39
+ const { width, height } = this.calculateAdSize(container, options, config) || {
40
+ width: '100%',
41
+ height: this.getDefaultHeight()
42
+ };
43
+
44
+ adElement.style.width = width;
45
+ adElement.style.height = height;
46
+
47
+ // 플레이스홀더 스타일 모드 결정
48
+ const placeholderMode = config?.placeholderMode || options.placeholderMode || 'invisible';
49
+
50
+ this.applyPlaceholderStyle(adElement, placeholderMode);
51
+
52
+ container.appendChild(adElement);
53
+
54
+ if (this.debug) {
55
+ console.log(`📦 Placeholder created for ${this.adType} slot: ${slotId} (${width} x ${height}) - Mode: ${placeholderMode}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 플레이스홀더 스타일 적용
61
+ */
62
+ private applyPlaceholderStyle(element: HTMLElement, mode: string): void {
63
+ switch (mode) {
64
+ case 'invisible':
65
+ // 완전히 투명한 플레이스홀더
66
+ element.style.backgroundColor = 'transparent';
67
+ element.style.border = 'none';
68
+ element.style.opacity = '0';
69
+ element.innerHTML = '';
70
+ break;
71
+
72
+ case 'transparent':
73
+ // 투명하지만 공간은 차지
74
+ element.style.backgroundColor = 'transparent';
75
+ element.style.border = 'none';
76
+ element.style.display = 'block';
77
+ element.innerHTML = '';
78
+ break;
79
+
80
+ case 'subtle':
81
+ // 매우 은은한 표시
82
+ element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
83
+ element.style.border = 'none';
84
+ element.style.borderRadius = '4px';
85
+ element.style.display = 'flex';
86
+ element.style.alignItems = 'center';
87
+ element.style.justifyContent = 'center';
88
+ element.innerHTML = '<span style="color: rgba(0, 0, 0, 0.3); font-size: 11px; font-family: sans-serif;">•••</span>';
89
+ break;
90
+
91
+ case 'minimal':
92
+ // 최소한의 표시 (기본값)
93
+ element.style.backgroundColor = 'rgba(248, 249, 250, 0.5)';
94
+ element.style.border = '1px solid rgba(0, 0, 0, 0.08)';
95
+ element.style.borderRadius = '6px';
96
+ element.style.display = 'flex';
97
+ element.style.alignItems = 'center';
98
+ element.style.justifyContent = 'center';
99
+ element.innerHTML = '<span style="color: rgba(0, 0, 0, 0.4); font-size: 12px; font-family: -apple-system, sans-serif;">•••</span>';
100
+ break;
101
+
102
+ case 'debug':
103
+ // 개발/디버그용 명확한 표시
104
+ element.style.border = '2px dashed #e74c3c';
105
+ element.style.display = 'flex';
106
+ element.style.alignItems = 'center';
107
+ element.style.justifyContent = 'center';
108
+ element.style.backgroundColor = 'rgba(231, 76, 60, 0.1)';
109
+ element.style.color = '#e74c3c';
110
+ element.style.fontFamily = 'monospace';
111
+ element.style.fontSize = '11px';
112
+ element.innerHTML = `<span>Loading ${this.adType} ad...</span>`;
113
+ break;
114
+
115
+ default:
116
+ // 기존 스타일 (legacy)
117
+ element.style.border = '1px dashed #ccc';
118
+ element.style.display = 'flex';
119
+ element.style.alignItems = 'center';
120
+ element.style.justifyContent = 'center';
121
+ element.style.backgroundColor = '#f9f9f9';
122
+ element.style.color = '#666';
123
+ element.innerHTML = `<span>Loading ${this.adType} ad...</span>`;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 광고 크기 계산 - 공통 구현
129
+ */
130
+ calculateAdSize(container: HTMLElement, options: AdRenderOptions, config: any): { width: string; height: string } {
131
+ // 사용자가 명시적으로 크기를 지정한 경우
132
+ const explicitWidth = options.width;
133
+ const explicitHeight = options.height;
134
+
135
+ // 너비 처리
136
+ let width: string;
137
+ if (typeof explicitWidth === 'number') {
138
+ width = `${explicitWidth}px`;
139
+ } else if (typeof explicitWidth === 'string') {
140
+ width = explicitWidth;
141
+ } else {
142
+ width = '100%'; // 기본값은 100%
143
+ }
144
+
145
+ // 높이 처리 - 핵심 로직
146
+ let height: string;
147
+ if (typeof explicitHeight === 'number') {
148
+ height = `${explicitHeight}px`;
149
+ } else if (typeof explicitHeight === 'string' && explicitHeight !== '100%' && explicitHeight !== 'auto') {
150
+ // 명시적인 크기 문자열 (예: '200px', '50vh' 등)
151
+ height = explicitHeight;
152
+ } else {
153
+ // 100%, auto이거나 높이가 지정되지 않은 경우 스마트 계산
154
+ const containerHeight = this.getContainerHeight(container);
155
+
156
+ if (containerHeight > 0) {
157
+ // 컨테이너에 높이가 있으면 100% 사용
158
+ height = '100%';
159
+ if (config?.debug || this.debug) {
160
+ console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
161
+ }
162
+ } else {
163
+ // 컨테이너에 높이가 없으면 타입별 기본값 사용
164
+ height = this.getDefaultHeight();
165
+ if (config?.debug || this.debug) {
166
+ console.log(`📏 Using default height ${height} for ${this.adType}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ return { width, height };
172
+ }
173
+
174
+ /**
175
+ * 컨테이너의 실제 높이 계산 - 공통 구현
176
+ */
177
+ protected getContainerHeight(container: HTMLElement): number {
178
+ const computedStyle = window.getComputedStyle(container);
179
+ const height = parseFloat(computedStyle.height);
180
+
181
+ if (!height || height === 0) {
182
+ const minHeight = parseFloat(computedStyle.minHeight);
183
+ if (minHeight > 0) return minHeight;
184
+
185
+ if (container.style.height && container.style.height !== 'auto') {
186
+ const styleHeight = parseFloat(container.style.height);
187
+ if (styleHeight > 0) return styleHeight;
188
+ }
189
+
190
+ const heightAttr = container.getAttribute('height');
191
+ if (heightAttr) {
192
+ const attrHeight = parseFloat(heightAttr);
193
+ if (attrHeight > 0) return attrHeight;
194
+ }
195
+ }
196
+
197
+ return height || 0;
198
+ }
199
+
200
+ /**
201
+ * 이벤트 트래킹 콜백 생성 - 공통 구현
202
+ */
203
+ protected createEventTrackingCallback() {
204
+ return async (adId: string, slotId: string, eventType: AdEventType) => {
205
+ if (eventType === AdEventType.VIEWABLE) {
206
+ if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
207
+ if (this.debug) {
208
+ console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
209
+ }
210
+ return;
211
+ }
212
+ if (this.debug) {
213
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
214
+ }
215
+ }
216
+
217
+ if (this.advertisementEventTracker) {
218
+ try {
219
+ if (this.debug) {
220
+ console.log(`🔄 Starting advertisement event tracking: ${eventType} for ad ${adId} in slot ${slotId}`);
221
+ }
222
+ await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType);
223
+ if (this.debug) {
224
+ console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
225
+ }
226
+ } catch (error) {
227
+ if (this.debug) {
228
+ console.error(`❌ Failed to track ${eventType} event for ad ${adId}:`, error);
229
+ }
230
+ }
231
+ } else {
232
+ if (this.debug) {
233
+ console.warn(`⚠️ AdvertisementEventTracker not available for ${eventType} event`);
234
+ }
235
+ }
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Fallback 광고 렌더링 - 공통 구현
241
+ */
242
+ renderFallback(slot: AdSlot): void {
243
+ const element = document.getElementById(slot.id);
244
+ if (element) {
245
+ const adstageContainers = [
246
+ element.querySelector('[data-adstage-container="true"]'),
247
+ element.closest('[data-adstage-container="true"]'),
248
+ element
249
+ ].filter(el => el && (el as HTMLElement).hasAttribute('data-adstage-container')) as HTMLElement[];
250
+
251
+ const classBasedContainers = [
252
+ element.closest('.adstage-slot'),
253
+ element.closest(`.adstage-${String(this.adType).toLowerCase()}`),
254
+ element.closest('[class*="ad"]'),
255
+ element.closest('[class*="banner"]'),
256
+ element.closest('[class*="container"]'),
257
+ element.closest('div[style*="height"]'),
258
+ element.closest('div[style*="min-height"]'),
259
+ element.parentElement
260
+ ].filter(Boolean) as HTMLElement[];
261
+
262
+ const possibleContainers = [...adstageContainers, ...classBasedContainers];
263
+ const targetContainer = possibleContainers[0];
264
+
265
+ if (targetContainer) {
266
+ let containerType = 'unknown';
267
+ if (targetContainer.hasAttribute('data-adstage-container')) {
268
+ containerType = 'adstage-official';
269
+ } else if (targetContainer.classList.contains('adstage-slot')) {
270
+ containerType = 'adstage-class';
271
+ } else {
272
+ containerType = 'generic';
273
+ }
274
+
275
+ targetContainer.style.cssText += `
276
+ height: 0px !important;
277
+ min-height: 0px !important;
278
+ padding: 0px !important;
279
+ margin: 0px !important;
280
+ border: none !important;
281
+ overflow: hidden !important;
282
+ display: block !important;
283
+ `;
284
+ targetContainer.innerHTML = '';
285
+ targetContainer.setAttribute('data-adstage-empty', 'true');
286
+
287
+ if (this.debug) {
288
+ console.warn(`⚠️ ${this.adType} container collapsed (${containerType}): ${slot.id}`, targetContainer);
289
+ }
290
+ } else {
291
+ this.createEmptyContainer(slot);
292
+ }
293
+ }
294
+ slot.advertisement = undefined;
295
+ (slot as any).isEmpty = true;
296
+ }
297
+
298
+ /**
299
+ * 빈 컨테이너 생성 - 공통 구현
300
+ */
301
+ private createEmptyContainer(slot: AdSlot): void {
302
+ const originalContainer = document.getElementById(slot.containerId);
303
+ if (originalContainer) {
304
+ originalContainer.innerHTML = '';
305
+ const emptyElement = document.createElement('div');
306
+ emptyElement.id = slot.id;
307
+ emptyElement.className = `adstage-slot adstage-empty adstage-${String(this.adType).toLowerCase()}`;
308
+ emptyElement.setAttribute('data-adstage-container', 'true');
309
+ emptyElement.setAttribute('data-adstage-empty', 'true');
310
+ emptyElement.setAttribute('data-adstage-slot', slot.id);
311
+ emptyElement.style.cssText = `
312
+ height: 0px !important;
313
+ min-height: 0px !important;
314
+ padding: 0px !important;
315
+ margin: 0px !important;
316
+ border: none !important;
317
+ overflow: hidden !important;
318
+ display: block !important;
319
+ `;
320
+ originalContainer.appendChild(emptyElement);
321
+ if (this.debug) {
322
+ console.warn(`⚠️ Created empty ${this.adType} container: ${slot.id}`);
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * 디버그 로그 출력 - 공통 구현
329
+ */
330
+ protected log(message: string, ...args: any[]): void {
331
+ if (this.debug) {
332
+ console.log(`[${this.adType}] ${message}`, ...args);
333
+ }
334
+ }
335
+
336
+ // 추상 메서드들 - 각 렌더러에서 구현해야 함
337
+ abstract renderAdElement(slot: AdSlot, advertisement: Advertisement): Promise<void>;
338
+ abstract renderMultipleAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void>;
339
+ abstract getDefaultHeight(): string;
340
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * 전면광고 전용 렌더러
3
+ */
4
+
5
+ import { AdSlot, Advertisement, AdType } from '../../../types/advertisement';
6
+ import { AdvertisementEventTracker } from '../../../managers/ads/advertisement-event-tracker';
7
+ import { BaseAdRenderer } from './base-ad-renderer';
8
+
9
+ export class InterstitialAdRenderer extends BaseAdRenderer {
10
+ constructor(debug: boolean = false, advertisementEventTracker?: AdvertisementEventTracker | null) {
11
+ super(AdType.INTERSTITIAL, debug, advertisementEventTracker);
12
+ }
13
+
14
+ /**
15
+ * 전면광고 기본 높이 (크게 설정)
16
+ */
17
+ getDefaultHeight(): string {
18
+ return '400px';
19
+ }
20
+
21
+ /**
22
+ * 단일 전면광고 렌더링
23
+ */
24
+ async renderAdElement(slot: AdSlot, advertisement: Advertisement): Promise<void> {
25
+ const container = document.getElementById(slot.containerId);
26
+ if (!container) return;
27
+
28
+ // 전면광고 오버레이 생성
29
+ const overlay = document.createElement('div');
30
+ overlay.className = 'adstage-interstitial-overlay';
31
+ overlay.style.cssText = `
32
+ position: fixed;
33
+ top: 0;
34
+ left: 0;
35
+ width: 100vw;
36
+ height: 100vh;
37
+ background: rgba(0, 0, 0, 0.8);
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ z-index: 10000;
42
+ animation: fadeIn 0.3s ease-out;
43
+ `;
44
+
45
+ // 전면광고 콘텐츠
46
+ const adContent = document.createElement('div');
47
+ adContent.className = 'adstage-interstitial-content';
48
+ adContent.style.cssText = `
49
+ position: relative;
50
+ max-width: 90vw;
51
+ max-height: 90vh;
52
+ background: white;
53
+ border-radius: 12px;
54
+ padding: 24px;
55
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
56
+ animation: slideUp 0.3s ease-out;
57
+ overflow: auto;
58
+ `;
59
+
60
+ // 닫기 버튼
61
+ const closeButton = document.createElement('button');
62
+ closeButton.innerHTML = '×';
63
+ closeButton.style.cssText = `
64
+ position: absolute;
65
+ top: 12px;
66
+ right: 12px;
67
+ width: 32px;
68
+ height: 32px;
69
+ border: none;
70
+ background: #f3f4f6;
71
+ color: #6b7280;
72
+ font-size: 20px;
73
+ font-weight: bold;
74
+ border-radius: 50%;
75
+ cursor: pointer;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ transition: all 0.2s;
80
+ `;
81
+
82
+ closeButton.addEventListener('click', () => {
83
+ this.closeInterstitial(overlay);
84
+ });
85
+
86
+ closeButton.addEventListener('mouseenter', () => {
87
+ closeButton.style.backgroundColor = '#e5e7eb';
88
+ closeButton.style.color = '#374151';
89
+ });
90
+
91
+ closeButton.addEventListener('mouseleave', () => {
92
+ closeButton.style.backgroundColor = '#f3f4f6';
93
+ closeButton.style.color = '#6b7280';
94
+ });
95
+
96
+ // 이미지 (있는 경우)
97
+ if (advertisement.imageUrl) {
98
+ const img = document.createElement('img');
99
+ img.src = advertisement.imageUrl;
100
+ img.alt = advertisement.title || 'Interstitial Advertisement';
101
+ img.style.cssText = `
102
+ width: 100%;
103
+ max-height: 300px;
104
+ object-fit: cover;
105
+ border-radius: 8px;
106
+ margin-bottom: 16px;
107
+ `;
108
+ adContent.appendChild(img);
109
+ }
110
+
111
+ // 제목
112
+ if (advertisement.title) {
113
+ const title = document.createElement('h2');
114
+ title.textContent = advertisement.title;
115
+ title.style.cssText = `
116
+ margin: 0 0 12px 0;
117
+ font-size: 24px;
118
+ font-weight: 700;
119
+ color: #111827;
120
+ line-height: 1.3;
121
+ `;
122
+ adContent.appendChild(title);
123
+ }
124
+
125
+ // 설명
126
+ if (advertisement.textContent) {
127
+ const description = document.createElement('p');
128
+ description.textContent = advertisement.textContent;
129
+ description.style.cssText = `
130
+ margin: 0 0 20px 0;
131
+ font-size: 16px;
132
+ color: #6b7280;
133
+ line-height: 1.5;
134
+ `;
135
+ adContent.appendChild(description);
136
+ }
137
+
138
+ // CTA 버튼 (링크가 있는 경우)
139
+ if (advertisement.linkUrl) {
140
+ const ctaButton = document.createElement('button');
141
+ ctaButton.textContent = 'Get Started';
142
+ ctaButton.style.cssText = `
143
+ width: 100%;
144
+ padding: 12px 24px;
145
+ background: #3b82f6;
146
+ color: white;
147
+ border: none;
148
+ border-radius: 8px;
149
+ font-size: 16px;
150
+ font-weight: 600;
151
+ cursor: pointer;
152
+ transition: background-color 0.2s;
153
+ `;
154
+
155
+ ctaButton.addEventListener('click', () => {
156
+ if (this.advertisementEventTracker) {
157
+ console.log(`Interstitial click tracked for ad: ${advertisement._id}`);
158
+ }
159
+ window.open(advertisement.linkUrl!, '_blank');
160
+ this.closeInterstitial(overlay);
161
+ });
162
+
163
+ ctaButton.addEventListener('mouseenter', () => {
164
+ ctaButton.style.backgroundColor = '#2563eb';
165
+ });
166
+
167
+ ctaButton.addEventListener('mouseleave', () => {
168
+ ctaButton.style.backgroundColor = '#3b82f6';
169
+ });
170
+
171
+ adContent.appendChild(ctaButton);
172
+ }
173
+
174
+ // ESC 키로 닫기
175
+ const handleEscape = (event: KeyboardEvent) => {
176
+ if (event.key === 'Escape') {
177
+ this.closeInterstitial(overlay);
178
+ document.removeEventListener('keydown', handleEscape);
179
+ }
180
+ };
181
+ document.addEventListener('keydown', handleEscape);
182
+
183
+ // 오버레이 클릭으로 닫기
184
+ overlay.addEventListener('click', (event) => {
185
+ if (event.target === overlay) {
186
+ this.closeInterstitial(overlay);
187
+ }
188
+ });
189
+
190
+ // CSS 애니메이션 추가
191
+ if (!document.getElementById('adstage-interstitial-styles')) {
192
+ const styles = document.createElement('style');
193
+ styles.id = 'adstage-interstitial-styles';
194
+ styles.textContent = `
195
+ @keyframes fadeIn {
196
+ from { opacity: 0; }
197
+ to { opacity: 1; }
198
+ }
199
+ @keyframes slideUp {
200
+ from {
201
+ opacity: 0;
202
+ transform: translateY(20px);
203
+ }
204
+ to {
205
+ opacity: 1;
206
+ transform: translateY(0);
207
+ }
208
+ }
209
+ `;
210
+ document.head.appendChild(styles);
211
+ }
212
+
213
+ adContent.appendChild(closeButton);
214
+ overlay.appendChild(adContent);
215
+ document.body.appendChild(overlay);
216
+
217
+ if (this.debug) {
218
+ console.log(`🖼️ Interstitial ad rendered: ${advertisement._id}`);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 다중 전면광고 렌더링 - 현재는 단일 렌더링만 지원
224
+ */
225
+ async renderMultipleAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
226
+ if (advertisements.length > 0) {
227
+ await this.renderAdElement(slot, advertisements[0]);
228
+
229
+ if (this.debug) {
230
+ console.log(`🖼️ Interstitial ad rendered (first of ${advertisements.length}): ${slot.id}`);
231
+ }
232
+ } else {
233
+ if (this.debug) {
234
+ console.warn(`⚠️ No interstitial advertisements available for slot: ${slot.id}`);
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * 전면광고 닫기
241
+ */
242
+ private closeInterstitial(overlay: HTMLElement): void {
243
+ overlay.style.animation = 'fadeOut 0.3s ease-out forwards';
244
+
245
+ // CSS 애니메이션이 없으면 즉시 제거
246
+ setTimeout(() => {
247
+ if (overlay.parentNode) {
248
+ overlay.parentNode.removeChild(overlay);
249
+ }
250
+ }, 300);
251
+
252
+ if (this.debug) {
253
+ console.log('🖼️ Interstitial ad closed');
254
+ }
255
+ }
256
+ }