@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
|
@@ -1,735 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AdRenderer - 광고 렌더링 전용 클래스
|
|
3
|
-
* AdsModule에서 렌더링 관련 기능을 분리
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { AdSlot, Advertisement, AdType, AdEventType } from '../../types/advertisement';
|
|
7
|
-
import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
|
|
8
|
-
import { CarouselSliderManager } from '../../managers/ads/carousel-slider-manager';
|
|
9
|
-
import { TextTransitionManager } from '../../managers/ads/text-transition-manager';
|
|
10
|
-
import { ViewableEventTracker } from '../../managers/ads/viewable-event-tracker';
|
|
11
|
-
|
|
12
|
-
export class AdRenderer {
|
|
13
|
-
private debug: boolean;
|
|
14
|
-
private advertisementEventTracker: AdvertisementEventTracker | null;
|
|
15
|
-
|
|
16
|
-
constructor(debug: boolean = false, advertisementEventTracker?: AdvertisementEventTracker | null) {
|
|
17
|
-
this.debug = debug;
|
|
18
|
-
this.advertisementEventTracker = advertisementEventTracker || null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Placeholder(슬롯 컨테이너) 생성
|
|
23
|
-
*/
|
|
24
|
-
createPlaceholder(
|
|
25
|
-
container: HTMLElement,
|
|
26
|
-
slotId: string,
|
|
27
|
-
type: AdType,
|
|
28
|
-
options: any,
|
|
29
|
-
_config?: any
|
|
30
|
-
): void {
|
|
31
|
-
const adElement = document.createElement('div');
|
|
32
|
-
adElement.id = slotId;
|
|
33
|
-
adElement.className = `adstage-slot adstage-${String(type).toLowerCase()}`;
|
|
34
|
-
adElement.setAttribute('data-adstage-container', 'true');
|
|
35
|
-
adElement.setAttribute('data-adstage-type', String(type));
|
|
36
|
-
adElement.setAttribute('data-adstage-slot', slotId);
|
|
37
|
-
|
|
38
|
-
const { width, height } = this.calculateAdSize(container, type, options, _config) || {
|
|
39
|
-
width: '100%',
|
|
40
|
-
height: '250px'
|
|
41
|
-
};
|
|
42
|
-
adElement.style.width = width;
|
|
43
|
-
adElement.style.height = height;
|
|
44
|
-
adElement.style.border = '1px dashed #ccc';
|
|
45
|
-
adElement.style.display = 'flex';
|
|
46
|
-
adElement.style.alignItems = 'center';
|
|
47
|
-
adElement.style.justifyContent = 'center';
|
|
48
|
-
adElement.style.backgroundColor = '#f9f9f9';
|
|
49
|
-
adElement.style.color = '#666';
|
|
50
|
-
adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
|
|
51
|
-
|
|
52
|
-
container.appendChild(adElement);
|
|
53
|
-
|
|
54
|
-
if (this.debug) {
|
|
55
|
-
console.log(`📦 Placeholder created for slot: ${slotId} (${width} x ${height})`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
|
|
61
|
-
*/
|
|
62
|
-
async calculateOptimalContainerSize(
|
|
63
|
-
advertisements: Advertisement[],
|
|
64
|
-
containerWidth: number,
|
|
65
|
-
adType: AdType
|
|
66
|
-
): Promise<{ width: string; height: string; aspectRatio: number }> {
|
|
67
|
-
if (!advertisements.length || adType !== AdType.BANNER) {
|
|
68
|
-
return {
|
|
69
|
-
width: '100%',
|
|
70
|
-
height: this.getDefaultHeightForAdType(adType) || '250px',
|
|
71
|
-
aspectRatio: 16 / 9
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const imageDimensions = await Promise.allSettled(
|
|
77
|
-
advertisements
|
|
78
|
-
.filter(ad => ad.imageUrl)
|
|
79
|
-
.map(ad => this.loadImageDimensions(ad.imageUrl!))
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const validDimensions = imageDimensions
|
|
83
|
-
.filter((result): result is PromiseFulfilledResult<{ width: number; height: number }> =>
|
|
84
|
-
result.status === 'fulfilled'
|
|
85
|
-
)
|
|
86
|
-
.map(result => result.value);
|
|
87
|
-
|
|
88
|
-
if (validDimensions.length === 0) {
|
|
89
|
-
return {
|
|
90
|
-
width: '100%',
|
|
91
|
-
height: this.getDefaultHeightForAdType(adType) || '250px',
|
|
92
|
-
aspectRatio: 16 / 9
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const strategy = this.selectOptimalSizeStrategy(validDimensions);
|
|
97
|
-
const optimalHeight = this.calculateOptimalHeight(validDimensions, containerWidth, strategy);
|
|
98
|
-
|
|
99
|
-
if (this.debug) {
|
|
100
|
-
console.log(`📐 Optimal container calculated: ${containerWidth}x${optimalHeight} (strategy: ${strategy})`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
width: '100%',
|
|
105
|
-
height: `${optimalHeight}px`,
|
|
106
|
-
aspectRatio: containerWidth / optimalHeight
|
|
107
|
-
};
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.warn('Failed to calculate optimal size, using defaults:', error);
|
|
110
|
-
return {
|
|
111
|
-
width: '100%',
|
|
112
|
-
height: this.getDefaultHeightForAdType(adType) || '250px',
|
|
113
|
-
aspectRatio: 16 / 9
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 최적 크기 조정 전략 선택
|
|
120
|
-
*/
|
|
121
|
-
private selectOptimalSizeStrategy(
|
|
122
|
-
dimensions: { width: number; height: number }[]
|
|
123
|
-
): 'average' | 'common' | 'dominant' {
|
|
124
|
-
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
125
|
-
|
|
126
|
-
const ratioGroups = new Map<string, number>();
|
|
127
|
-
aspectRatios.forEach(ratio => {
|
|
128
|
-
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
129
|
-
const key = roundedRatio.toString();
|
|
130
|
-
ratioGroups.set(key, (ratioGroups.get(key) || 0) + 1);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const maxGroup = Math.max(...ratioGroups.values());
|
|
134
|
-
const totalImages = dimensions.length;
|
|
135
|
-
|
|
136
|
-
if (maxGroup / totalImages >= 0.7) {
|
|
137
|
-
return 'dominant';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const standardRatios = [16 / 9, 4 / 3, 1 / 1, 3 / 2];
|
|
141
|
-
const standardCount = aspectRatios.filter(ratio =>
|
|
142
|
-
standardRatios.some(standard => Math.abs(ratio - standard) < 0.1)
|
|
143
|
-
).length;
|
|
144
|
-
|
|
145
|
-
if (standardCount / totalImages >= 0.5) {
|
|
146
|
-
return 'common';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return 'average';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* 전략에 따른 최적 높이 계산
|
|
154
|
-
*/
|
|
155
|
-
private calculateOptimalHeight(
|
|
156
|
-
dimensions: { width: number; height: number }[],
|
|
157
|
-
containerWidth: number,
|
|
158
|
-
strategy: 'average' | 'common' | 'dominant'
|
|
159
|
-
): number {
|
|
160
|
-
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
161
|
-
|
|
162
|
-
switch (strategy) {
|
|
163
|
-
case 'dominant': {
|
|
164
|
-
const ratioGroups = new Map<string, { ratio: number; count: number }>();
|
|
165
|
-
aspectRatios.forEach(ratio => {
|
|
166
|
-
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
167
|
-
const key = roundedRatio.toString();
|
|
168
|
-
const existing = ratioGroups.get(key);
|
|
169
|
-
if (existing) {
|
|
170
|
-
existing.count++;
|
|
171
|
-
} else {
|
|
172
|
-
ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const dominantGroup = Array.from(ratioGroups.values()).reduce((max, current) =>
|
|
177
|
-
current.count > max.count ? current : max
|
|
178
|
-
);
|
|
179
|
-
return Math.round(containerWidth / dominantGroup.ratio);
|
|
180
|
-
}
|
|
181
|
-
case 'common': {
|
|
182
|
-
const standardRatios = [
|
|
183
|
-
{ ratio: 16 / 9, name: '16:9' },
|
|
184
|
-
{ ratio: 4 / 3, name: '4:3' },
|
|
185
|
-
{ ratio: 1 / 1, name: '1:1' },
|
|
186
|
-
{ ratio: 3 / 2, name: '3:2' }
|
|
187
|
-
];
|
|
188
|
-
const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
189
|
-
const bestStandard = standardRatios.reduce((best, current) =>
|
|
190
|
-
Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best
|
|
191
|
-
);
|
|
192
|
-
if (this.debug) {
|
|
193
|
-
console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
|
|
194
|
-
}
|
|
195
|
-
return Math.round(containerWidth / bestStandard.ratio);
|
|
196
|
-
}
|
|
197
|
-
case 'average':
|
|
198
|
-
default: {
|
|
199
|
-
const averageRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
200
|
-
return Math.round(containerWidth / averageRatio);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* 배너 광고를 위한 컨테이너 최적화
|
|
207
|
-
*/
|
|
208
|
-
async optimizeContainerForBannerAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
209
|
-
try {
|
|
210
|
-
const container = document.getElementById(slot.containerId);
|
|
211
|
-
const adElement = document.getElementById(slot.id);
|
|
212
|
-
if (!container || !adElement) return;
|
|
213
|
-
|
|
214
|
-
const containerWidth = container.getBoundingClientRect().width || 300;
|
|
215
|
-
const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth, slot.adType);
|
|
216
|
-
|
|
217
|
-
adElement.style.height = optimalSize.height;
|
|
218
|
-
(slot as any).optimizedHeight = optimalSize.height;
|
|
219
|
-
(slot as any).aspectRatio = optimalSize.aspectRatio;
|
|
220
|
-
|
|
221
|
-
if (this.debug) {
|
|
222
|
-
console.log(`🔧 Container optimized for ${advertisements.length} banner ads: ${optimalSize.height}`);
|
|
223
|
-
}
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.warn('Container optimization failed, using default size:', error);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
231
|
-
*/
|
|
232
|
-
async renderAdSlider(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
233
|
-
const container = document.getElementById(slot.containerId);
|
|
234
|
-
if (!container) {
|
|
235
|
-
throw new Error(`Container not found: ${slot.containerId}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const trackEventCallback = async (adId: string, slotId: string, eventType: AdEventType) => {
|
|
239
|
-
if (eventType === AdEventType.VIEWABLE) {
|
|
240
|
-
if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this.debug)) {
|
|
241
|
-
if (this.debug) {
|
|
242
|
-
console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
243
|
-
}
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (this.debug) {
|
|
247
|
-
console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (this.advertisementEventTracker) {
|
|
252
|
-
try {
|
|
253
|
-
if (this.debug) {
|
|
254
|
-
console.log(`🔄 Starting advertisement event tracking: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
255
|
-
}
|
|
256
|
-
await this.advertisementEventTracker.trackAdvertisementEvent(adId, slotId, eventType);
|
|
257
|
-
if (this.debug) {
|
|
258
|
-
console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
259
|
-
}
|
|
260
|
-
} catch (error) {
|
|
261
|
-
if (this.debug) {
|
|
262
|
-
console.error(`❌ Failed to track ${eventType} event for ad ${adId}:`, error);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
if (this.debug) {
|
|
267
|
-
console.warn(`⚠️ AdvertisementEventTracker not available for ${eventType} event`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
let sliderElement: HTMLElement;
|
|
273
|
-
const optimizedSliderOptions = {
|
|
274
|
-
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
275
|
-
...slot.config,
|
|
276
|
-
optimizedHeight: (slot as any).optimizedHeight,
|
|
277
|
-
aspectRatio: (slot as any).aspectRatio
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
if (slot.adType === AdType.TEXT) {
|
|
281
|
-
sliderElement = TextTransitionManager.createTextTransitionContainer(
|
|
282
|
-
slot,
|
|
283
|
-
advertisements,
|
|
284
|
-
optimizedSliderOptions,
|
|
285
|
-
trackEventCallback
|
|
286
|
-
);
|
|
287
|
-
if (this.debug) {
|
|
288
|
-
console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
sliderElement = CarouselSliderManager.createSliderContainer(
|
|
292
|
-
slot,
|
|
293
|
-
advertisements,
|
|
294
|
-
optimizedSliderOptions,
|
|
295
|
-
trackEventCallback
|
|
296
|
-
);
|
|
297
|
-
if (this.debug) {
|
|
298
|
-
console.log(
|
|
299
|
-
`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads (optimized: ${(slot as any).optimizedHeight || 'default'})`
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
container.innerHTML = '';
|
|
305
|
-
container.appendChild(sliderElement);
|
|
306
|
-
if (this.debug) {
|
|
307
|
-
console.log(`� Slider uses manual VIEWABLE event tracking for ${advertisements.length} ads`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* 광고 렌더링 (단일 광고용)
|
|
313
|
-
*/
|
|
314
|
-
async renderAd(slot: AdSlot): Promise<void> {
|
|
315
|
-
if (!slot.advertisement) {
|
|
316
|
-
throw new Error('No advertisement to render');
|
|
317
|
-
}
|
|
318
|
-
await this.renderAdElement(slot, slot.advertisement);
|
|
319
|
-
slot.isLoaded = true;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* 광고 요소 렌더링 (기본 구현)
|
|
324
|
-
*/
|
|
325
|
-
async renderAdElement(slot: AdSlot, ad: Advertisement): Promise<void> {
|
|
326
|
-
const container = document.getElementById(slot.containerId);
|
|
327
|
-
if (!container) return;
|
|
328
|
-
|
|
329
|
-
const adElement = document.createElement('div');
|
|
330
|
-
adElement.className = 'adstage-ad';
|
|
331
|
-
|
|
332
|
-
const optimizedHeight = (slot as any).optimizedHeight;
|
|
333
|
-
const containerElement = container.parentElement || container;
|
|
334
|
-
|
|
335
|
-
if (optimizedHeight) {
|
|
336
|
-
adElement.style.width = '100%';
|
|
337
|
-
adElement.style.height = String(optimizedHeight);
|
|
338
|
-
} else {
|
|
339
|
-
const { width, height } =
|
|
340
|
-
this.calculateAdSize(containerElement, slot.adType, slot.config || {}, { debug: this.debug }) ||
|
|
341
|
-
{ width: '100%', height: '250px' };
|
|
342
|
-
adElement.style.width = width;
|
|
343
|
-
adElement.style.height = height;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
switch (slot.adType) {
|
|
347
|
-
case AdType.BANNER:
|
|
348
|
-
if (ad.imageUrl) {
|
|
349
|
-
await this.renderOptimizedBannerImage(adElement, ad, slot);
|
|
350
|
-
}
|
|
351
|
-
break;
|
|
352
|
-
case AdType.TEXT: {
|
|
353
|
-
const textDiv = document.createElement('div');
|
|
354
|
-
textDiv.innerHTML = `
|
|
355
|
-
${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
|
|
356
|
-
`;
|
|
357
|
-
adElement.appendChild(textDiv);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
case AdType.VIDEO:
|
|
361
|
-
if (ad.videoUrl) {
|
|
362
|
-
const video = document.createElement('video');
|
|
363
|
-
video.src = ad.videoUrl;
|
|
364
|
-
video.controls = true;
|
|
365
|
-
video.style.width = '100%';
|
|
366
|
-
video.style.height = '100%';
|
|
367
|
-
adElement.appendChild(video);
|
|
368
|
-
}
|
|
369
|
-
break;
|
|
370
|
-
default:
|
|
371
|
-
adElement.innerHTML = `<div>${ad.title}</div>`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (ad.linkUrl) {
|
|
375
|
-
adElement.style.cursor = 'pointer';
|
|
376
|
-
adElement.addEventListener('click', () => {
|
|
377
|
-
window.open(ad.linkUrl!, '_blank');
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
container.innerHTML = '';
|
|
382
|
-
container.appendChild(adElement);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Fallback 광고 렌더링 - 컨테이너 접기/생성
|
|
387
|
-
*/
|
|
388
|
-
renderFallback(slot: AdSlot): void {
|
|
389
|
-
const element = document.getElementById(slot.id);
|
|
390
|
-
if (element) {
|
|
391
|
-
const adstageContainers = [
|
|
392
|
-
element.querySelector('[data-adstage-container="true"]'),
|
|
393
|
-
element.closest('[data-adstage-container="true"]'),
|
|
394
|
-
element
|
|
395
|
-
].filter(el => el && (el as HTMLElement).hasAttribute('data-adstage-container')) as HTMLElement[];
|
|
396
|
-
|
|
397
|
-
const classBasedContainers = [
|
|
398
|
-
element.closest('.adstage-slot'),
|
|
399
|
-
element.closest('.adstage-banner'),
|
|
400
|
-
element.closest('.adstage-text'),
|
|
401
|
-
element.closest('.adstage-video'),
|
|
402
|
-
element.closest('.adstage-native'),
|
|
403
|
-
element.closest('.adstage-interstitial')
|
|
404
|
-
].filter(Boolean) as HTMLElement[];
|
|
405
|
-
|
|
406
|
-
const generalContainers = [
|
|
407
|
-
element.closest('[class*="ad"]'),
|
|
408
|
-
element.closest('[class*="banner"]'),
|
|
409
|
-
element.closest('[class*="container"]'),
|
|
410
|
-
element.closest('div[style*="height"]'),
|
|
411
|
-
element.closest('div[style*="min-height"]'),
|
|
412
|
-
element.parentElement
|
|
413
|
-
].filter(Boolean) as HTMLElement[];
|
|
414
|
-
|
|
415
|
-
const possibleContainers = [...adstageContainers, ...classBasedContainers, ...generalContainers];
|
|
416
|
-
const targetContainer = possibleContainers[0];
|
|
417
|
-
|
|
418
|
-
if (targetContainer) {
|
|
419
|
-
let containerType = 'unknown';
|
|
420
|
-
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
421
|
-
containerType = 'adstage-official';
|
|
422
|
-
} else if (targetContainer.classList.contains('adstage-slot')) {
|
|
423
|
-
containerType = 'adstage-class';
|
|
424
|
-
} else {
|
|
425
|
-
containerType = 'generic';
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
targetContainer.style.cssText += `
|
|
429
|
-
height: 0px !important;
|
|
430
|
-
min-height: 0px !important;
|
|
431
|
-
padding: 0px !important;
|
|
432
|
-
margin: 0px !important;
|
|
433
|
-
border: none !important;
|
|
434
|
-
overflow: hidden !important;
|
|
435
|
-
display: block !important;
|
|
436
|
-
`;
|
|
437
|
-
targetContainer.innerHTML = '';
|
|
438
|
-
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
439
|
-
|
|
440
|
-
if (this.debug) {
|
|
441
|
-
console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
this.createEmptyContainer(slot);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
slot.advertisement = undefined;
|
|
448
|
-
(slot as any).isEmpty = true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
453
|
-
*/
|
|
454
|
-
private createEmptyContainer(slot: AdSlot): void {
|
|
455
|
-
const originalContainer = document.getElementById(slot.containerId);
|
|
456
|
-
if (originalContainer) {
|
|
457
|
-
originalContainer.innerHTML = '';
|
|
458
|
-
const emptyElement = document.createElement('div');
|
|
459
|
-
emptyElement.id = slot.id;
|
|
460
|
-
emptyElement.className = 'adstage-slot adstage-empty';
|
|
461
|
-
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
462
|
-
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
463
|
-
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
464
|
-
emptyElement.style.cssText = `
|
|
465
|
-
height: 0px !important;
|
|
466
|
-
min-height: 0px !important;
|
|
467
|
-
padding: 0px !important;
|
|
468
|
-
margin: 0px !important;
|
|
469
|
-
border: none !important;
|
|
470
|
-
overflow: hidden !important;
|
|
471
|
-
display: block !important;
|
|
472
|
-
`;
|
|
473
|
-
originalContainer.appendChild(emptyElement);
|
|
474
|
-
if (this.debug) {
|
|
475
|
-
console.warn(`⚠️ Created empty AdStage container: ${slot.id}`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* 광고 타입별 기본 높이 반환
|
|
482
|
-
*/
|
|
483
|
-
getDefaultHeightForAdType(type: AdType): string {
|
|
484
|
-
switch (type) {
|
|
485
|
-
case AdType.BANNER:
|
|
486
|
-
return '250px'; // 일반 배너
|
|
487
|
-
case AdType.TEXT:
|
|
488
|
-
return '60px'; // 텍스트는 좀 더 작게
|
|
489
|
-
case AdType.VIDEO:
|
|
490
|
-
return '360px'; // 비디오는 16:9 비율 고려
|
|
491
|
-
case AdType.NATIVE:
|
|
492
|
-
return '200px'; // 네이티브는 중간 크기
|
|
493
|
-
case AdType.INTERSTITIAL:
|
|
494
|
-
return '400px'; // 전면광고는 크게
|
|
495
|
-
default:
|
|
496
|
-
return '250px';
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* 컨테이너와 광고 타입에 따른 스마트한 크기 계산
|
|
502
|
-
*/
|
|
503
|
-
calculateAdSize(container: HTMLElement, type: AdType, options: any, config: any): { width: string; height: string } {
|
|
504
|
-
// 사용자가 명시적으로 크기를 지정한 경우
|
|
505
|
-
const explicitWidth = options.width;
|
|
506
|
-
const explicitHeight = options.height;
|
|
507
|
-
|
|
508
|
-
// 너비 처리
|
|
509
|
-
let width: string;
|
|
510
|
-
if (typeof explicitWidth === 'number') {
|
|
511
|
-
width = `${explicitWidth}px`;
|
|
512
|
-
} else if (typeof explicitWidth === 'string') {
|
|
513
|
-
width = explicitWidth;
|
|
514
|
-
} else {
|
|
515
|
-
width = '100%'; // 기본값은 100%
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// 높이 처리 - 핵심 로직
|
|
519
|
-
let height: string;
|
|
520
|
-
if (typeof explicitHeight === 'number') {
|
|
521
|
-
height = `${explicitHeight}px`;
|
|
522
|
-
} else if (typeof explicitHeight === 'string' && explicitHeight !== '100%' && explicitHeight !== 'auto') {
|
|
523
|
-
// 명시적인 크기 문자열 (예: '200px', '50vh' 등)
|
|
524
|
-
height = explicitHeight;
|
|
525
|
-
} else {
|
|
526
|
-
// 100%, auto이거나 높이가 지정되지 않은 경우 스마트 계산
|
|
527
|
-
const containerHeight = this.getContainerHeight(container);
|
|
528
|
-
|
|
529
|
-
if (containerHeight > 0) {
|
|
530
|
-
// 컨테이너에 높이가 있으면 100% 사용
|
|
531
|
-
height = '100%';
|
|
532
|
-
if (config?.debug) {
|
|
533
|
-
console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
|
|
534
|
-
}
|
|
535
|
-
} else {
|
|
536
|
-
// 컨테이너에 높이가 없으면 타입별 기본값 사용 (나중에 동적 조정됨)
|
|
537
|
-
height = this.getDefaultHeightForAdType(type);
|
|
538
|
-
if (config?.debug) {
|
|
539
|
-
console.log(`📏 Using default height ${height} (will be optimized for ${type})`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return { width, height };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* 컨테이너의 실제 높이 계산
|
|
549
|
-
*/
|
|
550
|
-
private getContainerHeight(container: HTMLElement): number {
|
|
551
|
-
// 현재 계산된 스타일에서 높이 확인
|
|
552
|
-
const computedStyle = window.getComputedStyle(container);
|
|
553
|
-
const height = parseFloat(computedStyle.height);
|
|
554
|
-
|
|
555
|
-
// height가 auto이거나 0이면 다른 방법들 시도
|
|
556
|
-
if (!height || height === 0) {
|
|
557
|
-
// min-height 확인
|
|
558
|
-
const minHeight = parseFloat(computedStyle.minHeight);
|
|
559
|
-
if (minHeight > 0) return minHeight;
|
|
560
|
-
|
|
561
|
-
// CSS로 설정된 고정 높이 확인
|
|
562
|
-
if (container.style.height && container.style.height !== 'auto') {
|
|
563
|
-
const styleHeight = parseFloat(container.style.height);
|
|
564
|
-
if (styleHeight > 0) return styleHeight;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// 속성으로 설정된 높이 확인
|
|
568
|
-
const heightAttr = container.getAttribute('height');
|
|
569
|
-
if (heightAttr) {
|
|
570
|
-
const attrHeight = parseFloat(heightAttr);
|
|
571
|
-
if (attrHeight > 0) return attrHeight;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return height || 0;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* 이미지 로드 및 실제 크기 획득 (Promise 기반)
|
|
580
|
-
*/
|
|
581
|
-
loadImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
|
582
|
-
return new Promise((resolve, reject) => {
|
|
583
|
-
const img = new Image();
|
|
584
|
-
img.onload = () => {
|
|
585
|
-
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
586
|
-
};
|
|
587
|
-
img.onerror = () => {
|
|
588
|
-
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
589
|
-
};
|
|
590
|
-
img.src = imageUrl;
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* 이미지와 컨테이너 비율을 고려한 최적화 스타일 적용
|
|
596
|
-
*/
|
|
597
|
-
private applyOptimizedImageStyle(
|
|
598
|
-
img: HTMLImageElement,
|
|
599
|
-
imageAspectRatio: number,
|
|
600
|
-
containerAspectRatio: number
|
|
601
|
-
): void {
|
|
602
|
-
const ratio = imageAspectRatio / containerAspectRatio;
|
|
603
|
-
|
|
604
|
-
if (Math.abs(ratio - 1) < 0.1) {
|
|
605
|
-
// 비율이 거의 같으면 cover 사용
|
|
606
|
-
img.style.objectFit = 'cover';
|
|
607
|
-
img.style.objectPosition = 'center';
|
|
608
|
-
} else if (ratio > 1.3) {
|
|
609
|
-
// 이미지가 훨씬 가로형이면 contain으로 전체 보이기
|
|
610
|
-
img.style.objectFit = 'contain';
|
|
611
|
-
img.style.objectPosition = 'center';
|
|
612
|
-
img.style.backgroundColor = '#f0f0f0'; // 빈 공간 배경색
|
|
613
|
-
} else if (ratio < 0.7) {
|
|
614
|
-
// 이미지가 훨씬 세로형이면 cover로 채우기
|
|
615
|
-
img.style.objectFit = 'cover';
|
|
616
|
-
img.style.objectPosition = 'center';
|
|
617
|
-
} else {
|
|
618
|
-
// 적당한 차이면 스마트 cover
|
|
619
|
-
img.style.objectFit = 'cover';
|
|
620
|
-
img.style.objectPosition = 'center';
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (this.debug) {
|
|
624
|
-
console.log(`🎨 Image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* 배너 이미지 최적화 렌더링
|
|
630
|
-
* 이미지 실제 크기를 로드한 후 컨테이너와 비율을 맞춤
|
|
631
|
-
*/
|
|
632
|
-
async renderOptimizedBannerImage(
|
|
633
|
-
container: HTMLElement,
|
|
634
|
-
advertisement: Advertisement,
|
|
635
|
-
slot: AdSlot
|
|
636
|
-
): Promise<HTMLImageElement> {
|
|
637
|
-
const img = document.createElement('img');
|
|
638
|
-
|
|
639
|
-
// 기본 스타일 설정
|
|
640
|
-
img.style.width = '100%';
|
|
641
|
-
img.style.height = '100%';
|
|
642
|
-
img.style.display = 'block';
|
|
643
|
-
img.style.borderRadius = '8px';
|
|
644
|
-
img.alt = advertisement.title || 'Advertisement';
|
|
645
|
-
|
|
646
|
-
// 이미지 로딩 상태 표시
|
|
647
|
-
container.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">Loading...</div>';
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
// 이미지 URL 체크
|
|
651
|
-
if (!advertisement.imageUrl) {
|
|
652
|
-
throw new Error('Image URL is not provided');
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// 🆕 이미지 실제 크기 로드
|
|
656
|
-
const imageDimensions = await this.loadImageDimensions(advertisement.imageUrl);
|
|
657
|
-
|
|
658
|
-
// 컨테이너 크기 정보
|
|
659
|
-
const containerRect = container.getBoundingClientRect();
|
|
660
|
-
const containerWidth = containerRect.width;
|
|
661
|
-
const containerHeight = containerRect.height;
|
|
662
|
-
|
|
663
|
-
if (this.debug) {
|
|
664
|
-
console.log(`📸 Image dimensions: ${imageDimensions.width}x${imageDimensions.height}`);
|
|
665
|
-
console.log(`📦 Container dimensions: ${containerWidth}x${containerHeight}`);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// 비율 계산
|
|
669
|
-
const imageAspectRatio = imageDimensions.width / imageDimensions.height;
|
|
670
|
-
const containerAspectRatio = containerWidth / containerHeight;
|
|
671
|
-
|
|
672
|
-
// 🆕 스마트 스타일 적용
|
|
673
|
-
this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
|
|
674
|
-
|
|
675
|
-
// 이미지 설정 및 추가
|
|
676
|
-
img.src = advertisement.imageUrl;
|
|
677
|
-
|
|
678
|
-
// 컨테이너 클리어 후 이미지 추가
|
|
679
|
-
container.innerHTML = '';
|
|
680
|
-
container.appendChild(img);
|
|
681
|
-
|
|
682
|
-
// 클릭 이벤트 등록
|
|
683
|
-
if (advertisement.linkUrl) {
|
|
684
|
-
img.style.cursor = 'pointer';
|
|
685
|
-
img.addEventListener('click', () => {
|
|
686
|
-
if (this.advertisementEventTracker) {
|
|
687
|
-
// AdvertisementEventTracker의 실제 메소드명을 확인해야 함
|
|
688
|
-
console.log(`Click tracked for ad: ${advertisement._id}`);
|
|
689
|
-
}
|
|
690
|
-
window.open(advertisement.linkUrl, '_blank');
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
if (this.debug) {
|
|
695
|
-
console.log(`✅ Optimized banner image rendered for ad: ${advertisement._id}`);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return img;
|
|
699
|
-
|
|
700
|
-
} catch (error) {
|
|
701
|
-
console.error('❌ Failed to load optimized banner image:', error);
|
|
702
|
-
|
|
703
|
-
// 폴백: 일반 이미지 렌더링
|
|
704
|
-
if (advertisement.imageUrl) {
|
|
705
|
-
img.src = advertisement.imageUrl;
|
|
706
|
-
img.style.objectFit = 'cover';
|
|
707
|
-
img.style.objectPosition = 'center';
|
|
708
|
-
|
|
709
|
-
container.innerHTML = '';
|
|
710
|
-
container.appendChild(img);
|
|
711
|
-
|
|
712
|
-
if (advertisement.linkUrl) {
|
|
713
|
-
img.style.cursor = 'pointer';
|
|
714
|
-
img.addEventListener('click', () => {
|
|
715
|
-
if (this.advertisementEventTracker) {
|
|
716
|
-
console.log(`Click tracked for ad: ${advertisement._id}`);
|
|
717
|
-
}
|
|
718
|
-
window.open(advertisement.linkUrl, '_blank');
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return img;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* 디버그 로그 출력
|
|
729
|
-
*/
|
|
730
|
-
private log(message: string, ...args: any[]): void {
|
|
731
|
-
if (this.debug) {
|
|
732
|
-
console.log(message, ...args);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|