@adstage/web-sdk 2.1.0 → 2.1.3
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/dist/index.cjs.js +462 -29
- package/dist/index.d.ts +45 -1
- package/dist/index.esm.js +462 -29
- package/dist/index.standalone.js +462 -29
- package/package.json +1 -1
- package/src/modules/ads/AdsModule.ts +536 -29
|
@@ -279,8 +279,15 @@ export class AdsModule implements BaseModule {
|
|
|
279
279
|
const adElement = document.createElement('div');
|
|
280
280
|
adElement.id = slotId;
|
|
281
281
|
adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
|
|
282
|
-
|
|
283
|
-
adElement.
|
|
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;
|
|
284
291
|
adElement.style.border = '1px dashed #ccc';
|
|
285
292
|
adElement.style.display = 'flex';
|
|
286
293
|
adElement.style.alignItems = 'center';
|
|
@@ -292,7 +299,312 @@ export class AdsModule implements BaseModule {
|
|
|
292
299
|
container.appendChild(adElement);
|
|
293
300
|
|
|
294
301
|
if (this._config?.debug) {
|
|
295
|
-
console.log(`📦 Placeholder created for slot: ${slotId}`);
|
|
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%') {
|
|
329
|
+
height = explicitHeight;
|
|
330
|
+
} else {
|
|
331
|
+
// 100%이거나 높이가 지정되지 않은 경우 스마트 계산
|
|
332
|
+
const containerHeight = this.getContainerHeight(container);
|
|
333
|
+
|
|
334
|
+
if (containerHeight > 0) {
|
|
335
|
+
// 컨테이너에 높이가 있으면 100% 사용
|
|
336
|
+
height = '100%';
|
|
337
|
+
if (this._config?.debug) {
|
|
338
|
+
console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// 컨테이너에 높이가 없으면 타입별 기본값 사용
|
|
342
|
+
height = this.getDefaultHeightForAdType(type);
|
|
343
|
+
if (this._config?.debug) {
|
|
344
|
+
console.log(`📏 Using default height ${height} (no container height)`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { width, height };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 컨테이너의 실제 높이 계산
|
|
354
|
+
*/
|
|
355
|
+
private getContainerHeight(container: HTMLElement): number {
|
|
356
|
+
// 현재 계산된 스타일에서 높이 확인
|
|
357
|
+
const computedStyle = window.getComputedStyle(container);
|
|
358
|
+
const height = parseFloat(computedStyle.height);
|
|
359
|
+
|
|
360
|
+
// height가 auto이거나 0이면 다른 방법들 시도
|
|
361
|
+
if (!height || height === 0) {
|
|
362
|
+
// min-height 확인
|
|
363
|
+
const minHeight = parseFloat(computedStyle.minHeight);
|
|
364
|
+
if (minHeight > 0) return minHeight;
|
|
365
|
+
|
|
366
|
+
// CSS로 설정된 고정 높이 확인
|
|
367
|
+
if (container.style.height && container.style.height !== 'auto') {
|
|
368
|
+
const styleHeight = parseFloat(container.style.height);
|
|
369
|
+
if (styleHeight > 0) return styleHeight;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 속성으로 설정된 높이 확인
|
|
373
|
+
const heightAttr = container.getAttribute('height');
|
|
374
|
+
if (heightAttr) {
|
|
375
|
+
const attrHeight = parseFloat(heightAttr);
|
|
376
|
+
if (attrHeight > 0) return attrHeight;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return height || 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 광고 타입별 기본 높이 반환
|
|
385
|
+
*/
|
|
386
|
+
private getDefaultHeightForAdType(type: AdType): string {
|
|
387
|
+
switch (type) {
|
|
388
|
+
case AdType.BANNER:
|
|
389
|
+
return '250px'; // 일반 배너
|
|
390
|
+
case AdType.TEXT:
|
|
391
|
+
return '120px'; // 텍스트는 좀 더 작게
|
|
392
|
+
case AdType.VIDEO:
|
|
393
|
+
return '360px'; // 비디오는 16:9 비율 고려
|
|
394
|
+
case AdType.NATIVE:
|
|
395
|
+
return '200px'; // 네이티브는 중간 크기
|
|
396
|
+
case AdType.INTERSTITIAL:
|
|
397
|
+
return '400px'; // 전면광고는 크게
|
|
398
|
+
default:
|
|
399
|
+
return '250px';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 이미지 크기 정보 로드 (프리로딩)
|
|
405
|
+
*/
|
|
406
|
+
private async loadImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const img = new Image();
|
|
409
|
+
img.onload = () => {
|
|
410
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
411
|
+
};
|
|
412
|
+
img.onerror = () => {
|
|
413
|
+
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
414
|
+
};
|
|
415
|
+
img.src = imageUrl;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
|
|
421
|
+
*/
|
|
422
|
+
private async calculateOptimalContainerSize(
|
|
423
|
+
advertisements: Advertisement[],
|
|
424
|
+
containerWidth: number,
|
|
425
|
+
adType: AdType
|
|
426
|
+
): Promise<{ width: string; height: string; aspectRatio: number }> {
|
|
427
|
+
if (!advertisements.length || adType !== AdType.BANNER) {
|
|
428
|
+
// 배너가 아니거나 광고가 없으면 기본값
|
|
429
|
+
return {
|
|
430
|
+
width: '100%',
|
|
431
|
+
height: this.getDefaultHeightForAdType(adType),
|
|
432
|
+
aspectRatio: 16/9
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
// 모든 배너 이미지의 크기 정보 로드
|
|
438
|
+
const imageDimensions = await Promise.allSettled(
|
|
439
|
+
advertisements
|
|
440
|
+
.filter(ad => ad.imageUrl)
|
|
441
|
+
.map(ad => this.loadImageDimensions(ad.imageUrl!))
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const validDimensions = imageDimensions
|
|
445
|
+
.filter((result): result is PromiseFulfilledResult<{ width: number; height: number }> =>
|
|
446
|
+
result.status === 'fulfilled'
|
|
447
|
+
)
|
|
448
|
+
.map(result => result.value);
|
|
449
|
+
|
|
450
|
+
if (validDimensions.length === 0) {
|
|
451
|
+
// 이미지 로드 실패시 기본값
|
|
452
|
+
return {
|
|
453
|
+
width: '100%',
|
|
454
|
+
height: this.getDefaultHeightForAdType(adType),
|
|
455
|
+
aspectRatio: 16/9
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 최적 전략 선택
|
|
460
|
+
const strategy = this.selectOptimalSizeStrategy(validDimensions);
|
|
461
|
+
const optimalHeight = this.calculateOptimalHeight(validDimensions, containerWidth, strategy);
|
|
462
|
+
|
|
463
|
+
if (this._config?.debug) {
|
|
464
|
+
console.log(`📐 Optimal container calculated: ${containerWidth}x${optimalHeight} (strategy: ${strategy})`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
width: '100%',
|
|
469
|
+
height: `${optimalHeight}px`,
|
|
470
|
+
aspectRatio: containerWidth / optimalHeight
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.warn('Failed to calculate optimal size, using defaults:', error);
|
|
475
|
+
return {
|
|
476
|
+
width: '100%',
|
|
477
|
+
height: this.getDefaultHeightForAdType(adType),
|
|
478
|
+
aspectRatio: 16/9
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 최적 크기 조정 전략 선택
|
|
485
|
+
*/
|
|
486
|
+
private selectOptimalSizeStrategy(dimensions: { width: number; height: number }[]): 'average' | 'common' | 'dominant' {
|
|
487
|
+
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
488
|
+
|
|
489
|
+
// 1. 공통 비율이 있는지 확인 (±0.1 허용)
|
|
490
|
+
const ratioGroups = new Map<string, number>();
|
|
491
|
+
aspectRatios.forEach(ratio => {
|
|
492
|
+
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
493
|
+
const key = roundedRatio.toString();
|
|
494
|
+
ratioGroups.set(key, (ratioGroups.get(key) || 0) + 1);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const maxGroup = Math.max(...ratioGroups.values());
|
|
498
|
+
const totalImages = dimensions.length;
|
|
499
|
+
|
|
500
|
+
// 70% 이상이 비슷한 비율이면 dominant 전략
|
|
501
|
+
if (maxGroup / totalImages >= 0.7) {
|
|
502
|
+
return 'dominant';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 표준 비율들이 많으면 common 전략
|
|
506
|
+
const standardRatios = [16/9, 4/3, 1/1, 3/2];
|
|
507
|
+
const standardCount = aspectRatios.filter(ratio =>
|
|
508
|
+
standardRatios.some(standard => Math.abs(ratio - standard) < 0.1)
|
|
509
|
+
).length;
|
|
510
|
+
|
|
511
|
+
if (standardCount / totalImages >= 0.5) {
|
|
512
|
+
return 'common';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 기본은 평균 전략
|
|
516
|
+
return 'average';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 전략에 따른 최적 높이 계산
|
|
521
|
+
*/
|
|
522
|
+
private calculateOptimalHeight(
|
|
523
|
+
dimensions: { width: number; height: number }[],
|
|
524
|
+
containerWidth: number,
|
|
525
|
+
strategy: 'average' | 'common' | 'dominant'
|
|
526
|
+
): number {
|
|
527
|
+
const aspectRatios = dimensions.map(d => d.width / d.height);
|
|
528
|
+
|
|
529
|
+
switch (strategy) {
|
|
530
|
+
case 'dominant':
|
|
531
|
+
// 가장 많은 비율을 기준으로
|
|
532
|
+
const ratioGroups = new Map<string, { ratio: number; count: number }>();
|
|
533
|
+
aspectRatios.forEach(ratio => {
|
|
534
|
+
const roundedRatio = Math.round(ratio * 10) / 10;
|
|
535
|
+
const key = roundedRatio.toString();
|
|
536
|
+
const existing = ratioGroups.get(key);
|
|
537
|
+
if (existing) {
|
|
538
|
+
existing.count++;
|
|
539
|
+
} else {
|
|
540
|
+
ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const dominantGroup = Array.from(ratioGroups.values())
|
|
545
|
+
.reduce((max, current) => current.count > max.count ? current : max);
|
|
546
|
+
|
|
547
|
+
return Math.round(containerWidth / dominantGroup.ratio);
|
|
548
|
+
|
|
549
|
+
case 'common':
|
|
550
|
+
// 표준 비율 중 가장 적합한 것 선택
|
|
551
|
+
const standardRatios = [
|
|
552
|
+
{ ratio: 16/9, name: '16:9' },
|
|
553
|
+
{ ratio: 4/3, name: '4:3' },
|
|
554
|
+
{ ratio: 1/1, name: '1:1' },
|
|
555
|
+
{ ratio: 3/2, name: '3:2' }
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
559
|
+
const bestStandard = standardRatios.reduce((best, current) =>
|
|
560
|
+
Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (this._config?.debug) {
|
|
564
|
+
console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return Math.round(containerWidth / bestStandard.ratio);
|
|
568
|
+
|
|
569
|
+
case 'average':
|
|
570
|
+
default:
|
|
571
|
+
// 평균 비율 사용
|
|
572
|
+
const averageRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
|
|
573
|
+
return Math.round(containerWidth / averageRatio);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* 개별 이미지에 최적화된 렌더링 스타일 적용
|
|
579
|
+
*/
|
|
580
|
+
private applyOptimizedImageStyle(
|
|
581
|
+
img: HTMLImageElement,
|
|
582
|
+
imageAspectRatio: number,
|
|
583
|
+
containerAspectRatio: number
|
|
584
|
+
): void {
|
|
585
|
+
const ratio = imageAspectRatio / containerAspectRatio;
|
|
586
|
+
|
|
587
|
+
if (Math.abs(ratio - 1) < 0.1) {
|
|
588
|
+
// 비율이 거의 같으면 cover 사용
|
|
589
|
+
img.style.objectFit = 'cover';
|
|
590
|
+
img.style.objectPosition = 'center';
|
|
591
|
+
} else if (ratio > 1.3) {
|
|
592
|
+
// 이미지가 훨씬 가로형이면 contain으로 전체 보이기
|
|
593
|
+
img.style.objectFit = 'contain';
|
|
594
|
+
img.style.objectPosition = 'center';
|
|
595
|
+
img.style.backgroundColor = '#f0f0f0'; // 빈 공간 배경색
|
|
596
|
+
} else if (ratio < 0.7) {
|
|
597
|
+
// 이미지가 훨씬 세로형이면 cover로 채우기
|
|
598
|
+
img.style.objectFit = 'cover';
|
|
599
|
+
img.style.objectPosition = 'center';
|
|
600
|
+
} else {
|
|
601
|
+
// 적당한 차이면 스마트 cover
|
|
602
|
+
img.style.objectFit = 'cover';
|
|
603
|
+
img.style.objectPosition = 'center';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (this._config?.debug) {
|
|
607
|
+
console.log(`🎨 Image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
|
|
296
608
|
}
|
|
297
609
|
}
|
|
298
610
|
|
|
@@ -309,6 +621,11 @@ export class AdsModule implements BaseModule {
|
|
|
309
621
|
return;
|
|
310
622
|
}
|
|
311
623
|
|
|
624
|
+
// 🆕 동적 크기 조정: 배너 광고의 경우 이미지 크기 기반으로 컨테이너 최적화
|
|
625
|
+
if (slot.adType === AdType.BANNER && adstageData.length > 0) {
|
|
626
|
+
await this.optimizeContainerForBannerAds(slot, adstageData);
|
|
627
|
+
}
|
|
628
|
+
|
|
312
629
|
// 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
|
|
313
630
|
if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
|
|
314
631
|
await this.renderAdSlider(slot, adstageData);
|
|
@@ -332,6 +649,38 @@ export class AdsModule implements BaseModule {
|
|
|
332
649
|
}
|
|
333
650
|
}
|
|
334
651
|
|
|
652
|
+
/**
|
|
653
|
+
* 배너 광고를 위한 컨테이너 최적화
|
|
654
|
+
*/
|
|
655
|
+
private async optimizeContainerForBannerAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
656
|
+
try {
|
|
657
|
+
const container = document.getElementById(slot.containerId);
|
|
658
|
+
const adElement = document.getElementById(slot.id);
|
|
659
|
+
|
|
660
|
+
if (!container || !adElement) return;
|
|
661
|
+
|
|
662
|
+
// 현재 컨테이너 너비 확인
|
|
663
|
+
const containerWidth = container.getBoundingClientRect().width || 300;
|
|
664
|
+
|
|
665
|
+
// 최적 크기 계산
|
|
666
|
+
const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth, slot.adType);
|
|
667
|
+
|
|
668
|
+
// 컨테이너 크기 동적 조정
|
|
669
|
+
adElement.style.height = optimalSize.height;
|
|
670
|
+
|
|
671
|
+
// 슬롯 정보 업데이트
|
|
672
|
+
(slot as any).optimizedHeight = optimalSize.height;
|
|
673
|
+
(slot as any).aspectRatio = optimalSize.aspectRatio;
|
|
674
|
+
|
|
675
|
+
if (this._config?.debug) {
|
|
676
|
+
console.log(`🔧 Container optimized for ${advertisements.length} banner ads: ${optimalSize.height}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.warn('Container optimization failed, using default size:', error);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
335
684
|
/**
|
|
336
685
|
* 기본 viewability 추적 시작
|
|
337
686
|
*/
|
|
@@ -401,24 +750,122 @@ export class AdsModule implements BaseModule {
|
|
|
401
750
|
}
|
|
402
751
|
|
|
403
752
|
/**
|
|
404
|
-
* Fallback 광고 렌더링 -
|
|
753
|
+
* Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
|
|
405
754
|
*/
|
|
406
755
|
private renderFallback(slot: AdSlot): void {
|
|
407
756
|
const element = document.getElementById(slot.id);
|
|
408
757
|
if (element) {
|
|
409
|
-
//
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
758
|
+
// 1순위: AdStage가 생성한 확실한 컨테이너들 (데이터 속성 기반)
|
|
759
|
+
const adstageContainers = [
|
|
760
|
+
element.querySelector('[data-adstage-container="true"]'), // 내부 AdStage 컨테이너
|
|
761
|
+
element.closest('[data-adstage-container="true"]'), // 상위 AdStage 컨테이너
|
|
762
|
+
element, // 자기 자신이 AdStage 컨테이너인 경우
|
|
763
|
+
].filter(el => el && el.hasAttribute('data-adstage-container'));
|
|
764
|
+
|
|
765
|
+
// 2순위: AdStage 클래스 기반 컨테이너들
|
|
766
|
+
const classBasedContainers = [
|
|
767
|
+
element.closest('.adstage-slot'),
|
|
768
|
+
element.closest('.adstage-banner'),
|
|
769
|
+
element.closest('.adstage-text'),
|
|
770
|
+
element.closest('.adstage-video'),
|
|
771
|
+
element.closest('.adstage-native'),
|
|
772
|
+
element.closest('.adstage-interstitial'),
|
|
773
|
+
].filter(Boolean);
|
|
774
|
+
|
|
775
|
+
// 3순위: 일반적인 광고 컨테이너 패턴들 (fallback)
|
|
776
|
+
const generalContainers = [
|
|
777
|
+
element.closest('[class*="ad"]'),
|
|
778
|
+
element.closest('[class*="banner"]'),
|
|
779
|
+
element.closest('[class*="container"]'),
|
|
780
|
+
element.closest('div[style*="height"]'),
|
|
781
|
+
element.closest('div[style*="min-height"]'),
|
|
782
|
+
element.parentElement
|
|
783
|
+
].filter(Boolean);
|
|
784
|
+
|
|
785
|
+
// 우선순위에 따라 컨테이너 선택
|
|
786
|
+
const possibleContainers = [
|
|
787
|
+
...adstageContainers,
|
|
788
|
+
...classBasedContainers,
|
|
789
|
+
...generalContainers
|
|
790
|
+
];
|
|
791
|
+
|
|
792
|
+
// 가장 적절한 컨테이너 선택
|
|
793
|
+
const targetContainer = possibleContainers[0] as HTMLElement;
|
|
794
|
+
|
|
795
|
+
if (targetContainer) {
|
|
796
|
+
// 컨테이너 타입 로깅
|
|
797
|
+
let containerType = 'unknown';
|
|
798
|
+
if (targetContainer.hasAttribute('data-adstage-container')) {
|
|
799
|
+
containerType = 'adstage-official';
|
|
800
|
+
} else if (targetContainer.classList.contains('adstage-slot')) {
|
|
801
|
+
containerType = 'adstage-class';
|
|
802
|
+
} else {
|
|
803
|
+
containerType = 'generic';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
targetContainer.style.cssText += `
|
|
807
|
+
height: 0px !important;
|
|
808
|
+
min-height: 0px !important;
|
|
809
|
+
padding: 0px !important;
|
|
810
|
+
margin: 0px !important;
|
|
811
|
+
border: none !important;
|
|
812
|
+
overflow: hidden !important;
|
|
813
|
+
display: block !important;
|
|
814
|
+
`;
|
|
815
|
+
|
|
816
|
+
// 내부 모든 요소 제거
|
|
817
|
+
targetContainer.innerHTML = '';
|
|
818
|
+
|
|
819
|
+
// 빈 상태임을 표시하는 속성 추가
|
|
820
|
+
targetContainer.setAttribute('data-adstage-empty', 'true');
|
|
413
821
|
|
|
414
822
|
if (this._config?.debug) {
|
|
415
|
-
console.warn(`⚠️ Ad
|
|
823
|
+
console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
|
|
416
824
|
}
|
|
825
|
+
} else {
|
|
826
|
+
// 컨테이너를 찾지 못한 경우 새로운 빈 컨테이너 생성
|
|
827
|
+
this.createEmptyContainer(slot);
|
|
417
828
|
}
|
|
418
829
|
}
|
|
419
830
|
|
|
420
|
-
// 슬롯
|
|
421
|
-
|
|
831
|
+
// 슬롯 상태 업데이트 (제거하지 않고 빈 상태로 마킹)
|
|
832
|
+
slot.advertisement = undefined;
|
|
833
|
+
(slot as any).isEmpty = true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
|
|
838
|
+
*/
|
|
839
|
+
private createEmptyContainer(slot: AdSlot): void {
|
|
840
|
+
const originalContainer = document.getElementById(slot.containerId);
|
|
841
|
+
if (originalContainer) {
|
|
842
|
+
// 기존 내용 제거
|
|
843
|
+
originalContainer.innerHTML = '';
|
|
844
|
+
|
|
845
|
+
// 빈 AdStage 컨테이너 생성
|
|
846
|
+
const emptyElement = document.createElement('div');
|
|
847
|
+
emptyElement.id = slot.id;
|
|
848
|
+
emptyElement.className = 'adstage-slot adstage-empty';
|
|
849
|
+
emptyElement.setAttribute('data-adstage-container', 'true');
|
|
850
|
+
emptyElement.setAttribute('data-adstage-empty', 'true');
|
|
851
|
+
emptyElement.setAttribute('data-adstage-slot', slot.id);
|
|
852
|
+
|
|
853
|
+
emptyElement.style.cssText = `
|
|
854
|
+
height: 0px !important;
|
|
855
|
+
min-height: 0px !important;
|
|
856
|
+
padding: 0px !important;
|
|
857
|
+
margin: 0px !important;
|
|
858
|
+
border: none !important;
|
|
859
|
+
overflow: hidden !important;
|
|
860
|
+
display: block !important;
|
|
861
|
+
`;
|
|
862
|
+
|
|
863
|
+
originalContainer.appendChild(emptyElement);
|
|
864
|
+
|
|
865
|
+
if (this._config?.debug) {
|
|
866
|
+
console.warn(`⚠️ Created empty AdStage container: ${slot.id}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
422
869
|
}
|
|
423
870
|
|
|
424
871
|
/**
|
|
@@ -503,15 +950,21 @@ export class AdsModule implements BaseModule {
|
|
|
503
950
|
|
|
504
951
|
let sliderElement: HTMLElement;
|
|
505
952
|
|
|
953
|
+
// 최적화된 슬라이더 옵션 준비
|
|
954
|
+
const optimizedSliderOptions = {
|
|
955
|
+
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
956
|
+
...slot.config,
|
|
957
|
+
// 🆕 동적 크기 정보 전달
|
|
958
|
+
optimizedHeight: (slot as any).optimizedHeight,
|
|
959
|
+
aspectRatio: (slot as any).aspectRatio
|
|
960
|
+
};
|
|
961
|
+
|
|
506
962
|
// 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
|
|
507
963
|
if (slot.adType === AdType.TEXT) {
|
|
508
964
|
sliderElement = TextTransitionManager.createTextTransitionContainer(
|
|
509
965
|
slot,
|
|
510
966
|
advertisements,
|
|
511
|
-
|
|
512
|
-
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
513
|
-
...slot.config
|
|
514
|
-
},
|
|
967
|
+
optimizedSliderOptions,
|
|
515
968
|
trackEventCallback
|
|
516
969
|
);
|
|
517
970
|
|
|
@@ -522,15 +975,12 @@ export class AdsModule implements BaseModule {
|
|
|
522
975
|
sliderElement = CarouselSliderManager.createSliderContainer(
|
|
523
976
|
slot,
|
|
524
977
|
advertisements,
|
|
525
|
-
|
|
526
|
-
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
527
|
-
...slot.config
|
|
528
|
-
},
|
|
978
|
+
optimizedSliderOptions,
|
|
529
979
|
trackEventCallback
|
|
530
980
|
);
|
|
531
981
|
|
|
532
982
|
if (this._config?.debug) {
|
|
533
|
-
console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
|
|
983
|
+
console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads (optimized: ${(slot as any).optimizedHeight || 'default'})`);
|
|
534
984
|
}
|
|
535
985
|
}
|
|
536
986
|
|
|
@@ -561,20 +1011,25 @@ export class AdsModule implements BaseModule {
|
|
|
561
1011
|
// 기본 HTML 구조 생성
|
|
562
1012
|
const adElement = document.createElement('div');
|
|
563
1013
|
adElement.className = 'adstage-ad';
|
|
564
|
-
|
|
565
|
-
|
|
1014
|
+
|
|
1015
|
+
// 스마트한 크기 설정 - 최적화된 크기가 있으면 사용
|
|
1016
|
+
const optimizedHeight = (slot as any).optimizedHeight;
|
|
1017
|
+
const containerElement = container.parentElement || container;
|
|
1018
|
+
|
|
1019
|
+
if (optimizedHeight) {
|
|
1020
|
+
adElement.style.width = '100%';
|
|
1021
|
+
adElement.style.height = optimizedHeight;
|
|
1022
|
+
} else {
|
|
1023
|
+
const { width, height } = this.calculateAdSize(containerElement, slot.adType, slot.config || {});
|
|
1024
|
+
adElement.style.width = width;
|
|
1025
|
+
adElement.style.height = height;
|
|
1026
|
+
}
|
|
566
1027
|
|
|
567
1028
|
// 광고 타입별 렌더링
|
|
568
1029
|
switch (slot.adType) {
|
|
569
1030
|
case AdType.BANNER:
|
|
570
1031
|
if (ad.imageUrl) {
|
|
571
|
-
|
|
572
|
-
img.src = ad.imageUrl;
|
|
573
|
-
img.alt = ad.title;
|
|
574
|
-
img.style.width = '100%';
|
|
575
|
-
img.style.height = '100%';
|
|
576
|
-
img.style.objectFit = 'cover';
|
|
577
|
-
adElement.appendChild(img);
|
|
1032
|
+
await this.renderOptimizedBannerImage(adElement, ad, slot);
|
|
578
1033
|
}
|
|
579
1034
|
break;
|
|
580
1035
|
|
|
@@ -615,6 +1070,58 @@ export class AdsModule implements BaseModule {
|
|
|
615
1070
|
container.appendChild(adElement);
|
|
616
1071
|
}
|
|
617
1072
|
|
|
1073
|
+
/**
|
|
1074
|
+
* 최적화된 배너 이미지 렌더링
|
|
1075
|
+
*/
|
|
1076
|
+
private async renderOptimizedBannerImage(adElement: HTMLElement, ad: Advertisement, slot: AdSlot): Promise<void> {
|
|
1077
|
+
try {
|
|
1078
|
+
// 이미지 크기 정보 로드
|
|
1079
|
+
const imageDimensions = await this.loadImageDimensions(ad.imageUrl!);
|
|
1080
|
+
const imageAspectRatio = imageDimensions.width / imageDimensions.height;
|
|
1081
|
+
|
|
1082
|
+
// 컨테이너 비율 계산
|
|
1083
|
+
const containerAspectRatio = (slot as any).aspectRatio || 16/9;
|
|
1084
|
+
|
|
1085
|
+
// 이미지 요소 생성
|
|
1086
|
+
const img = document.createElement('img');
|
|
1087
|
+
img.src = ad.imageUrl!;
|
|
1088
|
+
img.alt = ad.title;
|
|
1089
|
+
img.style.width = '100%';
|
|
1090
|
+
img.style.height = '100%';
|
|
1091
|
+
|
|
1092
|
+
// 🎨 최적화된 스타일 적용
|
|
1093
|
+
this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
|
|
1094
|
+
|
|
1095
|
+
// 이미지 로드 완료 처리
|
|
1096
|
+
img.onload = () => {
|
|
1097
|
+
if (this._config?.debug) {
|
|
1098
|
+
console.log(`🖼️ Optimized banner image loaded: ${imageDimensions.width}x${imageDimensions.height} (ratio: ${imageAspectRatio.toFixed(2)})`);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// 에러 처리
|
|
1103
|
+
img.onerror = () => {
|
|
1104
|
+
console.warn(`Failed to load banner image: ${ad.imageUrl}`);
|
|
1105
|
+
// 폴백 텍스트 표시
|
|
1106
|
+
adElement.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #666;">${ad.title}</div>`;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
adElement.appendChild(img);
|
|
1110
|
+
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
console.warn('Failed to optimize banner image, using fallback:', error);
|
|
1113
|
+
|
|
1114
|
+
// 기본 이미지 렌더링 (폴백)
|
|
1115
|
+
const img = document.createElement('img');
|
|
1116
|
+
img.src = ad.imageUrl!;
|
|
1117
|
+
img.alt = ad.title;
|
|
1118
|
+
img.style.width = '100%';
|
|
1119
|
+
img.style.height = '100%';
|
|
1120
|
+
img.style.objectFit = 'cover';
|
|
1121
|
+
adElement.appendChild(img);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
618
1125
|
/**
|
|
619
1126
|
* 광고 슬롯 새로고침
|
|
620
1127
|
*/
|