@adstage/web-sdk 2.3.7 → 2.4.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.
- package/README.md +1 -1
- package/dist/index.cjs.js +1885 -2001
- package/dist/index.d.ts +12 -71
- package/dist/index.esm.js +1886 -1999
- package/dist/index.standalone.js +2212 -2397
- package/package.json +1 -1
- package/src/core/AdStage.ts +0 -9
- package/src/index.ts +0 -3
- package/src/managers/ads/advertisement-event-tracker.ts +8 -15
- package/src/managers/ads/carousel-slider-manager.ts +4 -35
- package/src/managers/ads/text-transition-manager.ts +1 -2
- package/src/managers/ads/viewable-event-tracker.ts +1 -5
- package/src/modules/ads/AdRenderer.ts +730 -0
- package/src/modules/ads/AdsModule.ts +27 -737
- package/src/react/AdStageProvider.tsx +0 -117
- package/src/react/index.ts +0 -11
- package/src/renderers/base-renderer.ts +5 -11
- package/src/types/advertisement.ts +17 -4
- package/src/types/api.ts +5 -1
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
import { AdStageConfig, BaseModule } from '../../types/config';
|
|
7
7
|
import { AdType, AdEventType } from '../../types/advertisement';
|
|
8
8
|
import type { AdSlot, Advertisement } from '../../types/advertisement';
|
|
9
|
-
|
|
10
|
-
import { TextTransitionManager } from '../../managers/ads/text-transition-manager';
|
|
11
|
-
import { ViewableEventTracker } from '../../managers/ads/viewable-event-tracker';
|
|
9
|
+
// 렌더링 관련 매니저/뷰 트래커는 AdRenderer로 이전됨
|
|
12
10
|
import { AdvertisementEventTracker } from '../../managers/ads/advertisement-event-tracker';
|
|
13
11
|
import { ViewabilityTracker, VIEWABILITY_STANDARDS } from '../../managers/ads/viewability-tracker';
|
|
14
12
|
import { BasicFraudDetector } from '../../managers/ads/basic-fraud-detector';
|
|
15
13
|
import type { ViewabilityMetrics } from '../../managers/ads/viewability-tracker';
|
|
16
14
|
import { endpoints } from '../../constants/endpoints';
|
|
17
15
|
import { ApiHeaders } from '../../utils/api-headers';
|
|
16
|
+
// 새로 분리된 클래스들
|
|
17
|
+
import { AdRenderer } from './AdRenderer';
|
|
18
18
|
|
|
19
19
|
export interface AdOptions {
|
|
20
20
|
width?: string | number;
|
|
@@ -38,6 +38,8 @@ export class AdsModule implements BaseModule {
|
|
|
38
38
|
private slots = new Map<string, AdSlot>();
|
|
39
39
|
// Advertisement 이벤트 추적 관련
|
|
40
40
|
private advertisementEventTracker: AdvertisementEventTracker | null = null;
|
|
41
|
+
// 렌더링 관련
|
|
42
|
+
private adRenderer: AdRenderer | null = null;
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Ads 모듈 초기화 (동기)
|
|
@@ -53,6 +55,9 @@ export class AdsModule implements BaseModule {
|
|
|
53
55
|
this.slots
|
|
54
56
|
);
|
|
55
57
|
|
|
58
|
+
// AdRenderer 초기화
|
|
59
|
+
this.adRenderer = new AdRenderer(config.debug || false, this.advertisementEventTracker);
|
|
60
|
+
|
|
56
61
|
this._isReady = true;
|
|
57
62
|
|
|
58
63
|
if (config.debug) {
|
|
@@ -235,8 +240,8 @@ export class AdsModule implements BaseModule {
|
|
|
235
240
|
// 고유한 슬롯 ID 생성
|
|
236
241
|
const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
237
242
|
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
// 즉시 placeholder 생성 (AdRenderer 위임)
|
|
244
|
+
this.adRenderer?.createPlaceholder(container, slotId, type, options, this._config);
|
|
240
245
|
|
|
241
246
|
// 광고 슬롯 정보 저장
|
|
242
247
|
const slot: AdSlot = {
|
|
@@ -253,7 +258,7 @@ export class AdsModule implements BaseModule {
|
|
|
253
258
|
advertisement: undefined, // 나중에 로드
|
|
254
259
|
config: { type, ...options },
|
|
255
260
|
load: async () => this.fetchAdData(type, options).then(ads => ads[0] || null),
|
|
256
|
-
|
|
261
|
+
render: (ad: Advertisement) => this.adRenderer?.renderAdElement(slot, ad),
|
|
257
262
|
refresh: async () => this.refreshAdSlot(slot),
|
|
258
263
|
destroy: () => this.destroy(slotId)
|
|
259
264
|
};
|
|
@@ -272,342 +277,22 @@ export class AdsModule implements BaseModule {
|
|
|
272
277
|
return slotId;
|
|
273
278
|
}
|
|
274
279
|
|
|
275
|
-
|
|
276
|
-
* 즉시 광고 슬롯 생성 (placeholder)
|
|
277
|
-
*/
|
|
278
|
-
private createAdSlot(container: HTMLElement, slotId: string, type: AdType, options: any): void {
|
|
279
|
-
const adElement = document.createElement('div');
|
|
280
|
-
adElement.id = slotId;
|
|
281
|
-
adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
|
|
282
|
-
// 확실한 컨테이너 식별을 위한 데이터 속성 추가
|
|
283
|
-
adElement.setAttribute('data-adstage-container', 'true');
|
|
284
|
-
adElement.setAttribute('data-adstage-type', type);
|
|
285
|
-
adElement.setAttribute('data-adstage-slot', slotId);
|
|
286
|
-
|
|
287
|
-
// 스마트한 크기 설정
|
|
288
|
-
const { width, height } = this.calculateAdSize(container, type, options);
|
|
289
|
-
adElement.style.width = width;
|
|
290
|
-
adElement.style.height = height;
|
|
291
|
-
adElement.style.border = '1px dashed #ccc';
|
|
292
|
-
adElement.style.display = 'flex';
|
|
293
|
-
adElement.style.alignItems = 'center';
|
|
294
|
-
adElement.style.justifyContent = 'center';
|
|
295
|
-
adElement.style.backgroundColor = '#f9f9f9';
|
|
296
|
-
adElement.style.color = '#666';
|
|
297
|
-
adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
|
|
298
|
-
|
|
299
|
-
container.appendChild(adElement);
|
|
300
|
-
|
|
301
|
-
if (this._config?.debug) {
|
|
302
|
-
console.log(`📦 Placeholder created for slot: ${slotId} (${width} x ${height})`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* 컨테이너와 광고 타입에 따른 스마트한 크기 계산
|
|
308
|
-
*/
|
|
309
|
-
private calculateAdSize(container: HTMLElement, type: AdType, options: any): { width: string; height: string } {
|
|
310
|
-
// 사용자가 명시적으로 크기를 지정한 경우
|
|
311
|
-
const explicitWidth = options.width;
|
|
312
|
-
const explicitHeight = options.height;
|
|
313
|
-
|
|
314
|
-
// 너비 처리
|
|
315
|
-
let width: string;
|
|
316
|
-
if (typeof explicitWidth === 'number') {
|
|
317
|
-
width = `${explicitWidth}px`;
|
|
318
|
-
} else if (typeof explicitWidth === 'string') {
|
|
319
|
-
width = explicitWidth;
|
|
320
|
-
} else {
|
|
321
|
-
width = '100%'; // 기본값은 100%
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// 높이 처리 - 핵심 로직
|
|
325
|
-
let height: string;
|
|
326
|
-
if (typeof explicitHeight === 'number') {
|
|
327
|
-
height = `${explicitHeight}px`;
|
|
328
|
-
} else if (typeof explicitHeight === 'string' && explicitHeight !== '100%' && explicitHeight !== 'auto') {
|
|
329
|
-
// 명시적인 크기 문자열 (예: '200px', '50vh' 등)
|
|
330
|
-
height = explicitHeight;
|
|
331
|
-
} else {
|
|
332
|
-
// 100%, auto이거나 높이가 지정되지 않은 경우 스마트 계산
|
|
333
|
-
const containerHeight = this.getContainerHeight(container);
|
|
334
|
-
|
|
335
|
-
if (containerHeight > 0) {
|
|
336
|
-
// 컨테이너에 높이가 있으면 100% 사용
|
|
337
|
-
height = '100%';
|
|
338
|
-
if (this._config?.debug) {
|
|
339
|
-
console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
// 컨테이너에 높이가 없으면 타입별 기본값 사용 (나중에 동적 조정됨)
|
|
343
|
-
height = this.getDefaultHeightForAdType(type);
|
|
344
|
-
if (this._config?.debug) {
|
|
345
|
-
console.log(`📏 Using default height ${height} (will be optimized for ${type})`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return { width, height };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* 컨테이너의 실제 높이 계산
|
|
355
|
-
*/
|
|
356
|
-
private getContainerHeight(container: HTMLElement): number {
|
|
357
|
-
// 현재 계산된 스타일에서 높이 확인
|
|
358
|
-
const computedStyle = window.getComputedStyle(container);
|
|
359
|
-
const height = parseFloat(computedStyle.height);
|
|
360
|
-
|
|
361
|
-
// height가 auto이거나 0이면 다른 방법들 시도
|
|
362
|
-
if (!height || height === 0) {
|
|
363
|
-
// min-height 확인
|
|
364
|
-
const minHeight = parseFloat(computedStyle.minHeight);
|
|
365
|
-
if (minHeight > 0) return minHeight;
|
|
366
|
-
|
|
367
|
-
// CSS로 설정된 고정 높이 확인
|
|
368
|
-
if (container.style.height && container.style.height !== 'auto') {
|
|
369
|
-
const styleHeight = parseFloat(container.style.height);
|
|
370
|
-
if (styleHeight > 0) return styleHeight;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 속성으로 설정된 높이 확인
|
|
374
|
-
const heightAttr = container.getAttribute('height');
|
|
375
|
-
if (heightAttr) {
|
|
376
|
-
const attrHeight = parseFloat(heightAttr);
|
|
377
|
-
if (attrHeight > 0) return attrHeight;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return height || 0;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* 광고 타입별 기본 높이 반환
|
|
386
|
-
*/
|
|
387
|
-
private getDefaultHeightForAdType(type: AdType): string {
|
|
388
|
-
switch (type) {
|
|
389
|
-
case AdType.BANNER:
|
|
390
|
-
return '250px'; // 일반 배너
|
|
391
|
-
case AdType.TEXT:
|
|
392
|
-
return '120px'; // 텍스트는 좀 더 작게
|
|
393
|
-
case AdType.VIDEO:
|
|
394
|
-
return '360px'; // 비디오는 16:9 비율 고려
|
|
395
|
-
case AdType.NATIVE:
|
|
396
|
-
return '200px'; // 네이티브는 중간 크기
|
|
397
|
-
case AdType.INTERSTITIAL:
|
|
398
|
-
return '400px'; // 전면광고는 크게
|
|
399
|
-
default:
|
|
400
|
-
return '250px';
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* 이미지 크기 정보 로드 (프리로딩)
|
|
406
|
-
*/
|
|
407
|
-
private async loadImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
|
408
|
-
return new Promise((resolve, reject) => {
|
|
409
|
-
const img = new Image();
|
|
410
|
-
img.onload = () => {
|
|
411
|
-
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
412
|
-
};
|
|
413
|
-
img.onerror = () => {
|
|
414
|
-
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
415
|
-
};
|
|
416
|
-
img.src = imageUrl;
|
|
417
|
-
});
|
|
418
|
-
}
|
|
280
|
+
// createAdSlot 제거: AdRenderer.createPlaceholder 사용
|
|
419
281
|
|
|
420
282
|
/**
|
|
421
283
|
* 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
|
|
422
284
|
*/
|
|
423
|
-
|
|
424
|
-
advertisements: Advertisement[],
|
|
425
|
-
containerWidth: number,
|
|
426
|
-
adType: AdType
|
|
427
|
-
): Promise<{ width: string; height: string; aspectRatio: number }> {
|
|
428
|
-
if (!advertisements.length || adType !== AdType.BANNER) {
|
|
429
|
-
// 배너가 아니거나 광고가 없으면 기본값
|
|
430
|
-
return {
|
|
431
|
-
width: '100%',
|
|
432
|
-
height: this.getDefaultHeightForAdType(adType),
|
|
433
|
-
aspectRatio: 16/9
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
// 모든 배너 이미지의 크기 정보 로드
|
|
439
|
-
const imageDimensions = await Promise.allSettled(
|
|
440
|
-
advertisements
|
|
441
|
-
.filter(ad => ad.imageUrl)
|
|
442
|
-
.map(ad => this.loadImageDimensions(ad.imageUrl!))
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
const validDimensions = imageDimensions
|
|
446
|
-
.filter((result): result is PromiseFulfilledResult<{ width: number; height: number }> =>
|
|
447
|
-
result.status === 'fulfilled'
|
|
448
|
-
)
|
|
449
|
-
.map(result => result.value);
|
|
450
|
-
|
|
451
|
-
if (validDimensions.length === 0) {
|
|
452
|
-
// 이미지 로드 실패시 기본값
|
|
453
|
-
return {
|
|
454
|
-
width: '100%',
|
|
455
|
-
height: this.getDefaultHeightForAdType(adType),
|
|
456
|
-
aspectRatio: 16/9
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// 최적 전략 선택
|
|
461
|
-
const strategy = this.selectOptimalSizeStrategy(validDimensions);
|
|
462
|
-
const optimalHeight = this.calculateOptimalHeight(validDimensions, containerWidth, strategy);
|
|
463
|
-
|
|
464
|
-
if (this._config?.debug) {
|
|
465
|
-
console.log(`📐 Optimal container calculated: ${containerWidth}x${optimalHeight} (strategy: ${strategy})`);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
width: '100%',
|
|
470
|
-
height: `${optimalHeight}px`,
|
|
471
|
-
aspectRatio: containerWidth / optimalHeight
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
} catch (error) {
|
|
475
|
-
console.warn('Failed to calculate optimal size, using defaults:', error);
|
|
476
|
-
return {
|
|
477
|
-
width: '100%',
|
|
478
|
-
height: this.getDefaultHeightForAdType(adType),
|
|
479
|
-
aspectRatio: 16/9
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
}
|
|
285
|
+
// calculateOptimalContainerSize 제거: AdRenderer.calculateOptimalContainerSize 사용
|
|
483
286
|
|
|
484
287
|
/**
|
|
485
288
|
* 최적 크기 조정 전략 선택
|
|
486
289
|
*/
|
|
487
|
-
|
|
488
|
-
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
489
|
-
|
|
490
|
-
// 1. 공통 비율이 있는지 확인 (±0.1 허용)
|
|
491
|
-
const ratioGroups = new Map<string, number>();
|
|
492
|
-
aspectRatios.forEach(ratio => {
|
|
493
|
-
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
494
|
-
const key = roundedRatio.toString();
|
|
495
|
-
ratioGroups.set(key, (ratioGroups.get(key) || 0) + 1);
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
const maxGroup = Math.max(...ratioGroups.values());
|
|
499
|
-
const totalImages = dimensions.length;
|
|
500
|
-
|
|
501
|
-
// 70% 이상이 비슷한 비율이면 dominant 전략
|
|
502
|
-
if (maxGroup / totalImages >= 0.7) {
|
|
503
|
-
return 'dominant';
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// 표준 비율들이 많으면 common 전략
|
|
507
|
-
const standardRatios = [16/9, 4/3, 1/1, 3/2];
|
|
508
|
-
const standardCount = aspectRatios.filter(ratio =>
|
|
509
|
-
standardRatios.some(standard => Math.abs(ratio - standard) < 0.1)
|
|
510
|
-
).length;
|
|
511
|
-
|
|
512
|
-
if (standardCount / totalImages >= 0.5) {
|
|
513
|
-
return 'common';
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// 기본은 평균 전략
|
|
517
|
-
return 'average';
|
|
518
|
-
}
|
|
290
|
+
// selectOptimalSizeStrategy 제거: AdRenderer 내부 구현 사용
|
|
519
291
|
|
|
520
292
|
/**
|
|
521
293
|
* 전략에 따른 최적 높이 계산
|
|
522
294
|
*/
|
|
523
|
-
|
|
524
|
-
dimensions: { width: number; height: number }[],
|
|
525
|
-
containerWidth: number,
|
|
526
|
-
strategy: 'average' | 'common' | 'dominant'
|
|
527
|
-
): number {
|
|
528
|
-
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
529
|
-
|
|
530
|
-
switch (strategy) {
|
|
531
|
-
case 'dominant':
|
|
532
|
-
// 가장 많은 비율을 기준으로
|
|
533
|
-
const ratioGroups = new Map<string, { ratio: number; count: number }>();
|
|
534
|
-
aspectRatios.forEach(ratio => {
|
|
535
|
-
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
536
|
-
const key = roundedRatio.toString();
|
|
537
|
-
const existing = ratioGroups.get(key);
|
|
538
|
-
if (existing) {
|
|
539
|
-
existing.count++;
|
|
540
|
-
} else {
|
|
541
|
-
ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const dominantGroup = Array.from(ratioGroups.values())
|
|
546
|
-
.reduce((max, current) => current.count > max.count ? current : max);
|
|
547
|
-
|
|
548
|
-
return Math.round(containerWidth / dominantGroup.ratio);
|
|
549
|
-
|
|
550
|
-
case 'common':
|
|
551
|
-
// 표준 비율 중 가장 적합한 것 선택
|
|
552
|
-
const standardRatios = [
|
|
553
|
-
{ ratio: 16/9, name: '16:9' },
|
|
554
|
-
{ ratio: 4/3, name: '4:3' },
|
|
555
|
-
{ ratio: 1/1, name: '1:1' },
|
|
556
|
-
{ ratio: 3/2, name: '3:2' }
|
|
557
|
-
];
|
|
558
|
-
|
|
559
|
-
const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
560
|
-
const bestStandard = standardRatios.reduce((best, current) =>
|
|
561
|
-
Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
if (this._config?.debug) {
|
|
565
|
-
console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return Math.round(containerWidth / bestStandard.ratio);
|
|
569
|
-
|
|
570
|
-
case 'average':
|
|
571
|
-
default:
|
|
572
|
-
// 평균 비율 사용
|
|
573
|
-
const averageRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
574
|
-
return Math.round(containerWidth / averageRatio);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* 개별 이미지에 최적화된 렌더링 스타일 적용
|
|
580
|
-
*/
|
|
581
|
-
private applyOptimizedImageStyle(
|
|
582
|
-
img: HTMLImageElement,
|
|
583
|
-
imageAspectRatio: number,
|
|
584
|
-
containerAspectRatio: number
|
|
585
|
-
): void {
|
|
586
|
-
const ratio = imageAspectRatio / containerAspectRatio;
|
|
587
|
-
|
|
588
|
-
if (Math.abs(ratio - 1) < 0.1) {
|
|
589
|
-
// 비율이 거의 같으면 cover 사용
|
|
590
|
-
img.style.objectFit = 'cover';
|
|
591
|
-
img.style.objectPosition = 'center';
|
|
592
|
-
} else if (ratio > 1.3) {
|
|
593
|
-
// 이미지가 훨씬 가로형이면 contain으로 전체 보이기
|
|
594
|
-
img.style.objectFit = 'contain';
|
|
595
|
-
img.style.objectPosition = 'center';
|
|
596
|
-
img.style.backgroundColor = '#f0f0f0'; // 빈 공간 배경색
|
|
597
|
-
} else if (ratio < 0.7) {
|
|
598
|
-
// 이미지가 훨씬 세로형이면 cover로 채우기
|
|
599
|
-
img.style.objectFit = 'cover';
|
|
600
|
-
img.style.objectPosition = 'center';
|
|
601
|
-
} else {
|
|
602
|
-
// 적당한 차이면 스마트 cover
|
|
603
|
-
img.style.objectFit = 'cover';
|
|
604
|
-
img.style.objectPosition = 'center';
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (this._config?.debug) {
|
|
608
|
-
console.log(`🎨 Image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
295
|
+
// calculateOptimalHeight 제거: AdRenderer 내부 구현 사용
|
|
611
296
|
|
|
612
297
|
/**
|
|
613
298
|
* 백그라운드에서 광고 콘텐츠 로드
|
|
@@ -618,22 +303,22 @@ export class AdsModule implements BaseModule {
|
|
|
618
303
|
const adstageData = await this.fetchAdData(slot.adType, slot.config);
|
|
619
304
|
|
|
620
305
|
if (!adstageData || adstageData.length === 0) {
|
|
621
|
-
|
|
306
|
+
this.adRenderer?.renderFallback(slot);
|
|
622
307
|
return;
|
|
623
308
|
}
|
|
624
309
|
|
|
625
310
|
// 🆕 동적 크기 조정: 배너 광고의 경우 이미지 크기 기반으로 컨테이너 최적화
|
|
626
311
|
if (slot.adType === AdType.BANNER && adstageData.length > 0) {
|
|
627
|
-
await this.optimizeContainerForBannerAds(slot, adstageData);
|
|
312
|
+
await this.adRenderer?.optimizeContainerForBannerAds(slot, adstageData);
|
|
628
313
|
}
|
|
629
314
|
|
|
630
315
|
// 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
|
|
631
316
|
if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
|
|
632
|
-
await this.renderAdSlider(slot, adstageData);
|
|
317
|
+
await this.adRenderer?.renderAdSlider(slot, adstageData);
|
|
633
318
|
} else {
|
|
634
319
|
// 광고가 1개면 일반 렌더링
|
|
635
320
|
slot.advertisement = adstageData[0];
|
|
636
|
-
|
|
321
|
+
await this.adRenderer?.renderAdElement(slot, adstageData[0]);
|
|
637
322
|
|
|
638
323
|
// ✅ 신규: Viewable impression 추적 시작 (기존 즉시 추적 대신)
|
|
639
324
|
this.startBasicViewabilityTracking(slot, adstageData[0]);
|
|
@@ -646,41 +331,14 @@ export class AdsModule implements BaseModule {
|
|
|
646
331
|
}
|
|
647
332
|
} catch (error) {
|
|
648
333
|
console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
|
|
649
|
-
this.renderFallback(slot);
|
|
334
|
+
this.adRenderer?.renderFallback(slot);
|
|
650
335
|
}
|
|
651
336
|
}
|
|
652
337
|
|
|
653
338
|
/**
|
|
654
339
|
* 배너 광고를 위한 컨테이너 최적화
|
|
655
340
|
*/
|
|
656
|
-
|
|
657
|
-
try {
|
|
658
|
-
const container = document.getElementById(slot.containerId);
|
|
659
|
-
const adElement = document.getElementById(slot.id);
|
|
660
|
-
|
|
661
|
-
if (!container || !adElement) return;
|
|
662
|
-
|
|
663
|
-
// 현재 컨테이너 너비 확인
|
|
664
|
-
const containerWidth = container.getBoundingClientRect().width || 300;
|
|
665
|
-
|
|
666
|
-
// 최적 크기 계산
|
|
667
|
-
const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth, slot.adType);
|
|
668
|
-
|
|
669
|
-
// 컨테이너 크기 동적 조정
|
|
670
|
-
adElement.style.height = optimalSize.height;
|
|
671
|
-
|
|
672
|
-
// 슬롯 정보 업데이트
|
|
673
|
-
(slot as any).optimizedHeight = optimalSize.height;
|
|
674
|
-
(slot as any).aspectRatio = optimalSize.aspectRatio;
|
|
675
|
-
|
|
676
|
-
if (this._config?.debug) {
|
|
677
|
-
console.log(`🔧 Container optimized for ${advertisements.length} banner ads: ${optimalSize.height}`);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
} catch (error) {
|
|
681
|
-
console.warn('Container optimization failed, using default size:', error);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
341
|
+
// optimizeContainerForBannerAds 제거: AdRenderer.optimizeContainerForBannerAds 사용
|
|
684
342
|
|
|
685
343
|
/**
|
|
686
344
|
* 기본 viewability 추적 시작
|
|
@@ -753,121 +411,12 @@ export class AdsModule implements BaseModule {
|
|
|
753
411
|
/**
|
|
754
412
|
* Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
|
|
755
413
|
*/
|
|
756
|
-
|
|
757
|
-
const element = document.getElementById(slot.id);
|
|
758
|
-
if (element) {
|
|
759
|
-
// 1순위: AdStage가 생성한 확실한 컨테이너들 (데이터 속성 기반)
|
|
760
|
-
const adstageContainers = [
|
|
761
|
-
element.querySelector('[data-adstage-container="true"]'), // 내부 AdStage 컨테이너
|
|
762
|
-
element.closest('[data-adstage-container="true"]'), // 상위 AdStage 컨테이너
|
|
763
|
-
element, // 자기 자신이 AdStage 컨테이너인 경우
|
|
764
|
-
].filter(el => el && el.hasAttribute('data-adstage-container'));
|
|
765
|
-
|
|
766
|
-
// 2순위: AdStage 클래스 기반 컨테이너들
|
|
767
|
-
const classBasedContainers = [
|
|
768
|
-
element.closest('.adstage-slot'),
|
|
769
|
-
element.closest('.adstage-banner'),
|
|
770
|
-
element.closest('.adstage-text'),
|
|
771
|
-
element.closest('.adstage-video'),
|
|
772
|
-
element.closest('.adstage-native'),
|
|
773
|
-
element.closest('.adstage-interstitial'),
|
|
774
|
-
].filter(Boolean);
|
|
775
|
-
|
|
776
|
-
// 3순위: 일반적인 광고 컨테이너 패턴들 (fallback)
|
|
777
|
-
const generalContainers = [
|
|
778
|
-
element.closest('[class*="ad"]'),
|
|
779
|
-
element.closest('[class*="banner"]'),
|
|
780
|
-
element.closest('[class*="container"]'),
|
|
781
|
-
element.closest('div[style*="height"]'),
|
|
782
|
-
element.closest('div[style*="min-height"]'),
|
|
783
|
-
element.parentElement
|
|
784
|
-
].filter(Boolean);
|
|
785
|
-
|
|
786
|
-
// 우선순위에 따라 컨테이너 선택
|
|
787
|
-
const possibleContainers = [
|
|
788
|
-
...adstageContainers,
|
|
789
|
-
...classBasedContainers,
|
|
790
|
-
...generalContainers
|
|
791
|
-
];
|
|
792
|
-
|
|
793
|
-
// 가장 적절한 컨테이너 선택
|
|
794
|
-
const targetContainer = possibleContainers[0] as HTMLElement;
|
|
795
|
-
|
|
796
|
-
if (targetContainer) {
|
|
797
|
-
// 컨테이너 타입 로깅
|
|
798
|
-
let containerType = 'unknown';
|
|
799
|
-
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
800
|
-
containerType = 'adstage-official';
|
|
801
|
-
} else if (targetContainer.classList.contains('adstage-slot')) {
|
|
802
|
-
containerType = 'adstage-class';
|
|
803
|
-
} else {
|
|
804
|
-
containerType = 'generic';
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
targetContainer.style.cssText += `
|
|
808
|
-
height: 0px !important;
|
|
809
|
-
min-height: 0px !important;
|
|
810
|
-
padding: 0px !important;
|
|
811
|
-
margin: 0px !important;
|
|
812
|
-
border: none !important;
|
|
813
|
-
overflow: hidden !important;
|
|
814
|
-
display: block !important;
|
|
815
|
-
`;
|
|
816
|
-
|
|
817
|
-
// 내부 모든 요소 제거
|
|
818
|
-
targetContainer.innerHTML = '';
|
|
819
|
-
|
|
820
|
-
// 빈 상태임을 표시하는 속성 추가
|
|
821
|
-
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
822
|
-
|
|
823
|
-
if (this._config?.debug) {
|
|
824
|
-
console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
825
|
-
}
|
|
826
|
-
} else {
|
|
827
|
-
// 컨테이너를 찾지 못한 경우 새로운 빈 컨테이너 생성
|
|
828
|
-
this.createEmptyContainer(slot);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// 슬롯 상태 업데이트 (제거하지 않고 빈 상태로 마킹)
|
|
833
|
-
slot.advertisement = undefined;
|
|
834
|
-
(slot as any).isEmpty = true;
|
|
835
|
-
}
|
|
414
|
+
// renderFallback 제거: AdRenderer.renderFallback 사용
|
|
836
415
|
|
|
837
416
|
/**
|
|
838
417
|
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
839
418
|
*/
|
|
840
|
-
|
|
841
|
-
const originalContainer = document.getElementById(slot.containerId);
|
|
842
|
-
if (originalContainer) {
|
|
843
|
-
// 기존 내용 제거
|
|
844
|
-
originalContainer.innerHTML = '';
|
|
845
|
-
|
|
846
|
-
// 빈 AdStage 컨테이너 생성
|
|
847
|
-
const emptyElement = document.createElement('div');
|
|
848
|
-
emptyElement.id = slot.id;
|
|
849
|
-
emptyElement.className = 'adstage-slot adstage-empty';
|
|
850
|
-
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
851
|
-
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
852
|
-
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
853
|
-
|
|
854
|
-
emptyElement.style.cssText = `
|
|
855
|
-
height: 0px !important;
|
|
856
|
-
min-height: 0px !important;
|
|
857
|
-
padding: 0px !important;
|
|
858
|
-
margin: 0px !important;
|
|
859
|
-
border: none !important;
|
|
860
|
-
overflow: hidden !important;
|
|
861
|
-
display: block !important;
|
|
862
|
-
`;
|
|
863
|
-
|
|
864
|
-
originalContainer.appendChild(emptyElement);
|
|
865
|
-
|
|
866
|
-
if (this._config?.debug) {
|
|
867
|
-
console.warn(`⚠️ Created empty AdStage container: ${slot.id}`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
}
|
|
419
|
+
// createEmptyContainer 제거: AdRenderer 내부 구현 사용
|
|
871
420
|
|
|
872
421
|
/**
|
|
873
422
|
* 광고 데이터 가져오기
|
|
@@ -922,276 +471,17 @@ export class AdsModule implements BaseModule {
|
|
|
922
471
|
/**
|
|
923
472
|
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
924
473
|
*/
|
|
925
|
-
|
|
926
|
-
const container = document.getElementById(slot.containerId);
|
|
927
|
-
if (!container) {
|
|
928
|
-
throw new Error(`Container not found: ${slot.containerId}`);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
|
|
932
|
-
const trackEventCallback = async (adId: string, slotId: string, eventType: AdEventType) => {
|
|
933
|
-
// 노출 이벤트인 경우 중복 확인
|
|
934
|
-
if (eventType === AdEventType.VIEWABLE) {
|
|
935
|
-
if (ViewableEventTracker.isDuplicateViewable(adId, slotId, this._config?.debug)) {
|
|
936
|
-
if (this._config?.debug) {
|
|
937
|
-
console.log(`🚫 Duplicate viewable blocked for ad ${adId} in slot ${slotId}`);
|
|
938
|
-
}
|
|
939
|
-
return; // 중복 노출이면 추적하지 않음
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (this._config?.debug) {
|
|
943
|
-
console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId}`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// 실제 API 호출로 이벤트 전송
|
|
948
|
-
if (this.advertisementEventTracker) {
|
|
949
|
-
try {
|
|
950
|
-
await this.advertisementEventTracker.trackAdvertisementEvent(
|
|
951
|
-
adId,
|
|
952
|
-
slotId,
|
|
953
|
-
eventType,
|
|
954
|
-
{} // 기본 메타데이터
|
|
955
|
-
);
|
|
956
|
-
|
|
957
|
-
if (this._config?.debug) {
|
|
958
|
-
console.log(`📊 Advertisement event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
959
|
-
}
|
|
960
|
-
} catch (error) {
|
|
961
|
-
if (this._config?.debug) {
|
|
962
|
-
console.error(`❌ Failed to track ${eventType} event for ad ${adId}:`, error);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// 🆕 슬라이더에서 VIEWABLE 이벤트 발생 시 해당 광고로 ViewabilityTracker 교체
|
|
968
|
-
if (eventType === AdEventType.VIEWABLE && advertisements.length > 1) {
|
|
969
|
-
const currentAd = advertisements.find(ad => ad._id === adId);
|
|
970
|
-
if (currentAd) {
|
|
971
|
-
// 기존 ViewabilityTracker 정리
|
|
972
|
-
if ((slot as any).viewabilityTracker) {
|
|
973
|
-
(slot as any).viewabilityTracker.destroy();
|
|
974
|
-
(slot as any).viewabilityTracker = null;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// 현재 광고에 대해 새로운 ViewabilityTracker 시작
|
|
978
|
-
this.startBasicViewabilityTracking(slot, currentAd);
|
|
979
|
-
|
|
980
|
-
if (this._config?.debug) {
|
|
981
|
-
console.log(`🔄 ViewabilityTracker switched to ad ${adId} in slider: ${slot.id}`);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
let sliderElement: HTMLElement;
|
|
988
|
-
|
|
989
|
-
// 최적화된 슬라이더 옵션 준비
|
|
990
|
-
const optimizedSliderOptions = {
|
|
991
|
-
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
992
|
-
...slot.config,
|
|
993
|
-
// 🆕 동적 크기 정보 전달
|
|
994
|
-
optimizedHeight: (slot as any).optimizedHeight,
|
|
995
|
-
aspectRatio: (slot as any).aspectRatio
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
// 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
|
|
999
|
-
if (slot.adType === AdType.TEXT) {
|
|
1000
|
-
sliderElement = TextTransitionManager.createTextTransitionContainer(
|
|
1001
|
-
slot,
|
|
1002
|
-
advertisements,
|
|
1003
|
-
optimizedSliderOptions,
|
|
1004
|
-
trackEventCallback
|
|
1005
|
-
);
|
|
1006
|
-
|
|
1007
|
-
if (this._config?.debug) {
|
|
1008
|
-
console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
|
|
1009
|
-
}
|
|
1010
|
-
} else {
|
|
1011
|
-
sliderElement = CarouselSliderManager.createSliderContainer(
|
|
1012
|
-
slot,
|
|
1013
|
-
advertisements,
|
|
1014
|
-
optimizedSliderOptions,
|
|
1015
|
-
trackEventCallback
|
|
1016
|
-
);
|
|
1017
|
-
|
|
1018
|
-
if (this._config?.debug) {
|
|
1019
|
-
console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads (optimized: ${(slot as any).optimizedHeight || 'default'})`);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// 기존 내용 제거하고 슬라이더 추가
|
|
1024
|
-
container.innerHTML = '';
|
|
1025
|
-
container.appendChild(sliderElement);
|
|
1026
|
-
|
|
1027
|
-
// ✅ 신규: 슬라이더에서도 첫 번째 광고에 대해 ViewabilityTracker 시작
|
|
1028
|
-
if (advertisements.length > 0) {
|
|
1029
|
-
this.startBasicViewabilityTracking(slot, advertisements[0]);
|
|
1030
|
-
|
|
1031
|
-
if (this._config?.debug) {
|
|
1032
|
-
console.log(`🎯 Viewability tracking started for first ad in slider: ${slot.id}`);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
474
|
+
// renderAdSlider 제거: AdRenderer.renderAdSlider 사용
|
|
1036
475
|
|
|
1037
476
|
/**
|
|
1038
477
|
* 광고 렌더링 (단일 광고용)
|
|
1039
478
|
*/
|
|
1040
|
-
|
|
1041
|
-
if (!slot.advertisement) {
|
|
1042
|
-
throw new Error('No advertisement to render');
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
await this.renderAdElement(slot, slot.advertisement);
|
|
1046
|
-
slot.isLoaded = true;
|
|
1047
|
-
}
|
|
479
|
+
// renderAd 제거: AdRenderer.renderAd 사용
|
|
1048
480
|
|
|
1049
481
|
/**
|
|
1050
482
|
* 광고 요소 렌더링 (기본 구현)
|
|
1051
483
|
*/
|
|
1052
|
-
|
|
1053
|
-
const container = document.getElementById(slot.containerId);
|
|
1054
|
-
if (!container) return;
|
|
1055
|
-
|
|
1056
|
-
// 기본 HTML 구조 생성
|
|
1057
|
-
const adElement = document.createElement('div');
|
|
1058
|
-
adElement.className = 'adstage-ad';
|
|
1059
|
-
|
|
1060
|
-
// 스마트한 크기 설정 - 최적화된 크기가 있으면 사용
|
|
1061
|
-
const optimizedHeight = (slot as any).optimizedHeight;
|
|
1062
|
-
const containerElement = container.parentElement || container;
|
|
1063
|
-
|
|
1064
|
-
if (optimizedHeight) {
|
|
1065
|
-
adElement.style.width = '100%';
|
|
1066
|
-
adElement.style.height = optimizedHeight;
|
|
1067
|
-
} else {
|
|
1068
|
-
const { width, height } = this.calculateAdSize(containerElement, slot.adType, slot.config || {});
|
|
1069
|
-
adElement.style.width = width;
|
|
1070
|
-
adElement.style.height = height;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// 광고 타입별 렌더링
|
|
1074
|
-
switch (slot.adType) {
|
|
1075
|
-
case AdType.BANNER:
|
|
1076
|
-
if (ad.imageUrl) {
|
|
1077
|
-
await this.renderOptimizedBannerImage(adElement, ad, slot);
|
|
1078
|
-
}
|
|
1079
|
-
break;
|
|
1080
|
-
|
|
1081
|
-
case AdType.TEXT:
|
|
1082
|
-
const textDiv = document.createElement('div');
|
|
1083
|
-
textDiv.innerHTML = `
|
|
1084
|
-
<h3>${ad.title}</h3>
|
|
1085
|
-
${ad.description ? `<p>${ad.description}</p>` : ''}
|
|
1086
|
-
${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
|
|
1087
|
-
`;
|
|
1088
|
-
adElement.appendChild(textDiv);
|
|
1089
|
-
break;
|
|
1090
|
-
|
|
1091
|
-
case AdType.VIDEO:
|
|
1092
|
-
if (ad.videoUrl) {
|
|
1093
|
-
const video = document.createElement('video');
|
|
1094
|
-
video.src = ad.videoUrl;
|
|
1095
|
-
video.controls = true;
|
|
1096
|
-
video.style.width = '100%';
|
|
1097
|
-
video.style.height = '100%';
|
|
1098
|
-
adElement.appendChild(video);
|
|
1099
|
-
}
|
|
1100
|
-
break;
|
|
1101
|
-
|
|
1102
|
-
default:
|
|
1103
|
-
adElement.innerHTML = `<div>${ad.title}</div>`;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// 클릭 이벤트 추가
|
|
1107
|
-
if (ad.linkUrl) {
|
|
1108
|
-
adElement.style.cursor = 'pointer';
|
|
1109
|
-
adElement.addEventListener('click', () => {
|
|
1110
|
-
window.open(ad.linkUrl, '_blank');
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
container.innerHTML = '';
|
|
1115
|
-
container.appendChild(adElement);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
/**
|
|
1119
|
-
* 최적화된 배너 이미지 렌더링
|
|
1120
|
-
*/
|
|
1121
|
-
private async renderOptimizedBannerImage(adElement: HTMLElement, ad: Advertisement, slot: AdSlot): Promise<void> {
|
|
1122
|
-
try {
|
|
1123
|
-
// 사용자가 크기를 지정했는지 확인
|
|
1124
|
-
const configWidth = slot.config?.width;
|
|
1125
|
-
const configHeight = slot.config?.height;
|
|
1126
|
-
|
|
1127
|
-
const hasUserDefinedWidth = configWidth &&
|
|
1128
|
-
(typeof configWidth === 'number' || (typeof configWidth === 'string' && configWidth !== '100%'));
|
|
1129
|
-
const hasUserDefinedHeight = configHeight &&
|
|
1130
|
-
(typeof configHeight === 'number' || (typeof configHeight === 'string' && configHeight !== 'auto'));
|
|
1131
|
-
const hasUserDefinedSize = hasUserDefinedWidth || hasUserDefinedHeight;
|
|
1132
|
-
|
|
1133
|
-
// 이미지 요소 생성
|
|
1134
|
-
const img = document.createElement('img');
|
|
1135
|
-
img.src = ad.imageUrl!;
|
|
1136
|
-
img.alt = ad.title;
|
|
1137
|
-
|
|
1138
|
-
if (hasUserDefinedSize) {
|
|
1139
|
-
// 🎯 사용자가 크기를 지정한 경우: 컨테이너에 꽉 차도록 설정
|
|
1140
|
-
img.style.width = '100%';
|
|
1141
|
-
img.style.height = '100%';
|
|
1142
|
-
img.style.objectFit = 'cover'; // 컨테이너에 꽉 찬 상태로 비율 유지
|
|
1143
|
-
img.style.objectPosition = 'center';
|
|
1144
|
-
|
|
1145
|
-
if (this._config?.debug) {
|
|
1146
|
-
console.log(`🎯 User-defined size detected: filling container completely`);
|
|
1147
|
-
}
|
|
1148
|
-
} else {
|
|
1149
|
-
// 사용자가 크기를 지정하지 않은 경우: 동적 최적화 적용
|
|
1150
|
-
const imageDimensions = await this.loadImageDimensions(ad.imageUrl!);
|
|
1151
|
-
const imageAspectRatio = imageDimensions.width / imageDimensions.height;
|
|
1152
|
-
const containerAspectRatio = (slot as any).aspectRatio || 16/9;
|
|
1153
|
-
|
|
1154
|
-
img.style.width = '100%';
|
|
1155
|
-
img.style.height = '100%';
|
|
1156
|
-
|
|
1157
|
-
// 🎨 최적화된 스타일 적용
|
|
1158
|
-
this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
|
|
1159
|
-
|
|
1160
|
-
if (this._config?.debug) {
|
|
1161
|
-
console.log(`🖼️ Optimized banner image loaded: ${imageDimensions.width}x${imageDimensions.height} (ratio: ${imageAspectRatio.toFixed(2)})`);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// 이미지 로드 완료 처리
|
|
1166
|
-
img.onload = () => {
|
|
1167
|
-
if (this._config?.debug) {
|
|
1168
|
-
console.log(`✅ Banner image loaded successfully`);
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
// 에러 처리
|
|
1173
|
-
img.onerror = () => {
|
|
1174
|
-
console.warn(`Failed to load banner image: ${ad.imageUrl}`);
|
|
1175
|
-
// 폴백 텍스트 표시
|
|
1176
|
-
adElement.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #666;">${ad.title}</div>`;
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
adElement.appendChild(img);
|
|
1180
|
-
|
|
1181
|
-
} catch (error) {
|
|
1182
|
-
console.warn('Failed to optimize banner image, using fallback:', error);
|
|
1183
|
-
|
|
1184
|
-
// 기본 이미지 렌더링 (폴백)
|
|
1185
|
-
const img = document.createElement('img');
|
|
1186
|
-
img.src = ad.imageUrl!;
|
|
1187
|
-
img.alt = ad.title;
|
|
1188
|
-
img.style.width = '100%';
|
|
1189
|
-
img.style.height = '100%';
|
|
1190
|
-
img.style.objectFit = 'cover';
|
|
1191
|
-
img.style.objectPosition = 'center'; // 🎯 폴백 이미지도 중앙 정렬
|
|
1192
|
-
adElement.appendChild(img);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
484
|
+
// renderAdElement 제거: AdRenderer.renderAdElement 사용
|
|
1195
485
|
|
|
1196
486
|
/**
|
|
1197
487
|
* 광고 슬롯 새로고침
|
|
@@ -1203,7 +493,7 @@ export class AdsModule implements BaseModule {
|
|
|
1203
493
|
|
|
1204
494
|
if (newAdData && newAdData.length > 0) {
|
|
1205
495
|
slot.advertisement = newAdData[0]; // 첫 번째 광고로 업데이트
|
|
1206
|
-
|
|
496
|
+
await this.adRenderer?.renderAd(slot);
|
|
1207
497
|
|
|
1208
498
|
// 새로운 노출 추적
|
|
1209
499
|
if (this.advertisementEventTracker) {
|