@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.esm.js CHANGED
@@ -2215,8 +2215,14 @@ class AdsModule {
2215
2215
  const adElement = document.createElement('div');
2216
2216
  adElement.id = slotId;
2217
2217
  adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
2218
- adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
2219
- adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
2218
+ // 확실한 컨테이너 식별을 위한 데이터 속성 추가
2219
+ adElement.setAttribute('data-adstage-container', 'true');
2220
+ adElement.setAttribute('data-adstage-type', type);
2221
+ adElement.setAttribute('data-adstage-slot', slotId);
2222
+ // 스마트한 크기 설정
2223
+ const { width, height } = this.calculateAdSize(container, type, options);
2224
+ adElement.style.width = width;
2225
+ adElement.style.height = height;
2220
2226
  adElement.style.border = '1px dashed #ccc';
2221
2227
  adElement.style.display = 'flex';
2222
2228
  adElement.style.alignItems = 'center';
@@ -2226,7 +2232,266 @@ class AdsModule {
2226
2232
  adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
2227
2233
  container.appendChild(adElement);
2228
2234
  if (this._config?.debug) {
2229
- console.log(`📦 Placeholder created for slot: ${slotId}`);
2235
+ console.log(`📦 Placeholder created for slot: ${slotId} (${width} x ${height})`);
2236
+ }
2237
+ }
2238
+ /**
2239
+ * 컨테이너와 광고 타입에 따른 스마트한 크기 계산
2240
+ */
2241
+ calculateAdSize(container, type, options) {
2242
+ // 사용자가 명시적으로 크기를 지정한 경우
2243
+ const explicitWidth = options.width;
2244
+ const explicitHeight = options.height;
2245
+ // 너비 처리
2246
+ let width;
2247
+ if (typeof explicitWidth === 'number') {
2248
+ width = `${explicitWidth}px`;
2249
+ }
2250
+ else if (typeof explicitWidth === 'string') {
2251
+ width = explicitWidth;
2252
+ }
2253
+ else {
2254
+ width = '100%'; // 기본값은 100%
2255
+ }
2256
+ // 높이 처리 - 핵심 로직
2257
+ let height;
2258
+ if (typeof explicitHeight === 'number') {
2259
+ height = `${explicitHeight}px`;
2260
+ }
2261
+ else if (typeof explicitHeight === 'string' && explicitHeight !== '100%') {
2262
+ height = explicitHeight;
2263
+ }
2264
+ else {
2265
+ // 100%이거나 높이가 지정되지 않은 경우 스마트 계산
2266
+ const containerHeight = this.getContainerHeight(container);
2267
+ if (containerHeight > 0) {
2268
+ // 컨테이너에 높이가 있으면 100% 사용
2269
+ height = '100%';
2270
+ if (this._config?.debug) {
2271
+ console.log(`📏 Using 100% height (container: ${containerHeight}px)`);
2272
+ }
2273
+ }
2274
+ else {
2275
+ // 컨테이너에 높이가 없으면 타입별 기본값 사용
2276
+ height = this.getDefaultHeightForAdType(type);
2277
+ if (this._config?.debug) {
2278
+ console.log(`📏 Using default height ${height} (no container height)`);
2279
+ }
2280
+ }
2281
+ }
2282
+ return { width, height };
2283
+ }
2284
+ /**
2285
+ * 컨테이너의 실제 높이 계산
2286
+ */
2287
+ getContainerHeight(container) {
2288
+ // 현재 계산된 스타일에서 높이 확인
2289
+ const computedStyle = window.getComputedStyle(container);
2290
+ const height = parseFloat(computedStyle.height);
2291
+ // height가 auto이거나 0이면 다른 방법들 시도
2292
+ if (!height || height === 0) {
2293
+ // min-height 확인
2294
+ const minHeight = parseFloat(computedStyle.minHeight);
2295
+ if (minHeight > 0)
2296
+ return minHeight;
2297
+ // CSS로 설정된 고정 높이 확인
2298
+ if (container.style.height && container.style.height !== 'auto') {
2299
+ const styleHeight = parseFloat(container.style.height);
2300
+ if (styleHeight > 0)
2301
+ return styleHeight;
2302
+ }
2303
+ // 속성으로 설정된 높이 확인
2304
+ const heightAttr = container.getAttribute('height');
2305
+ if (heightAttr) {
2306
+ const attrHeight = parseFloat(heightAttr);
2307
+ if (attrHeight > 0)
2308
+ return attrHeight;
2309
+ }
2310
+ }
2311
+ return height || 0;
2312
+ }
2313
+ /**
2314
+ * 광고 타입별 기본 높이 반환
2315
+ */
2316
+ getDefaultHeightForAdType(type) {
2317
+ switch (type) {
2318
+ case AdType.BANNER:
2319
+ return '250px'; // 일반 배너
2320
+ case AdType.TEXT:
2321
+ return '120px'; // 텍스트는 좀 더 작게
2322
+ case AdType.VIDEO:
2323
+ return '360px'; // 비디오는 16:9 비율 고려
2324
+ case AdType.NATIVE:
2325
+ return '200px'; // 네이티브는 중간 크기
2326
+ case AdType.INTERSTITIAL:
2327
+ return '400px'; // 전면광고는 크게
2328
+ default:
2329
+ return '250px';
2330
+ }
2331
+ }
2332
+ /**
2333
+ * 이미지 크기 정보 로드 (프리로딩)
2334
+ */
2335
+ async loadImageDimensions(imageUrl) {
2336
+ return new Promise((resolve, reject) => {
2337
+ const img = new Image();
2338
+ img.onload = () => {
2339
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
2340
+ };
2341
+ img.onerror = () => {
2342
+ reject(new Error(`Failed to load image: ${imageUrl}`));
2343
+ };
2344
+ img.src = imageUrl;
2345
+ });
2346
+ }
2347
+ /**
2348
+ * 여러 광고의 최적 컨테이너 크기 계산 (동적 크기 조정)
2349
+ */
2350
+ async calculateOptimalContainerSize(advertisements, containerWidth, adType) {
2351
+ if (!advertisements.length || adType !== AdType.BANNER) {
2352
+ // 배너가 아니거나 광고가 없으면 기본값
2353
+ return {
2354
+ width: '100%',
2355
+ height: this.getDefaultHeightForAdType(adType),
2356
+ aspectRatio: 16 / 9
2357
+ };
2358
+ }
2359
+ try {
2360
+ // 모든 배너 이미지의 크기 정보 로드
2361
+ const imageDimensions = await Promise.allSettled(advertisements
2362
+ .filter(ad => ad.imageUrl)
2363
+ .map(ad => this.loadImageDimensions(ad.imageUrl)));
2364
+ const validDimensions = imageDimensions
2365
+ .filter((result) => result.status === 'fulfilled')
2366
+ .map(result => result.value);
2367
+ if (validDimensions.length === 0) {
2368
+ // 이미지 로드 실패시 기본값
2369
+ return {
2370
+ width: '100%',
2371
+ height: this.getDefaultHeightForAdType(adType),
2372
+ aspectRatio: 16 / 9
2373
+ };
2374
+ }
2375
+ // 최적 전략 선택
2376
+ const strategy = this.selectOptimalSizeStrategy(validDimensions);
2377
+ const optimalHeight = this.calculateOptimalHeight(validDimensions, containerWidth, strategy);
2378
+ if (this._config?.debug) {
2379
+ console.log(`📐 Optimal container calculated: ${containerWidth}x${optimalHeight} (strategy: ${strategy})`);
2380
+ }
2381
+ return {
2382
+ width: '100%',
2383
+ height: `${optimalHeight}px`,
2384
+ aspectRatio: containerWidth / optimalHeight
2385
+ };
2386
+ }
2387
+ catch (error) {
2388
+ console.warn('Failed to calculate optimal size, using defaults:', error);
2389
+ return {
2390
+ width: '100%',
2391
+ height: this.getDefaultHeightForAdType(adType),
2392
+ aspectRatio: 16 / 9
2393
+ };
2394
+ }
2395
+ }
2396
+ /**
2397
+ * 최적 크기 조정 전략 선택
2398
+ */
2399
+ selectOptimalSizeStrategy(dimensions) {
2400
+ const aspectRatios = dimensions.map(d => d.width / d.height);
2401
+ // 1. 공통 비율이 있는지 확인 (±0.1 허용)
2402
+ const ratioGroups = new Map();
2403
+ aspectRatios.forEach(ratio => {
2404
+ const roundedRatio = Math.round(ratio * 10) / 10;
2405
+ const key = roundedRatio.toString();
2406
+ ratioGroups.set(key, (ratioGroups.get(key) || 0) + 1);
2407
+ });
2408
+ const maxGroup = Math.max(...ratioGroups.values());
2409
+ const totalImages = dimensions.length;
2410
+ // 70% 이상이 비슷한 비율이면 dominant 전략
2411
+ if (maxGroup / totalImages >= 0.7) {
2412
+ return 'dominant';
2413
+ }
2414
+ // 표준 비율들이 많으면 common 전략
2415
+ const standardRatios = [16 / 9, 4 / 3, 1 / 1, 3 / 2];
2416
+ const standardCount = aspectRatios.filter(ratio => standardRatios.some(standard => Math.abs(ratio - standard) < 0.1)).length;
2417
+ if (standardCount / totalImages >= 0.5) {
2418
+ return 'common';
2419
+ }
2420
+ // 기본은 평균 전략
2421
+ return 'average';
2422
+ }
2423
+ /**
2424
+ * 전략에 따른 최적 높이 계산
2425
+ */
2426
+ calculateOptimalHeight(dimensions, containerWidth, strategy) {
2427
+ const aspectRatios = dimensions.map(d => d.width / d.height);
2428
+ switch (strategy) {
2429
+ case 'dominant':
2430
+ // 가장 많은 비율을 기준으로
2431
+ const ratioGroups = new Map();
2432
+ aspectRatios.forEach(ratio => {
2433
+ const roundedRatio = Math.round(ratio * 10) / 10;
2434
+ const key = roundedRatio.toString();
2435
+ const existing = ratioGroups.get(key);
2436
+ if (existing) {
2437
+ existing.count++;
2438
+ }
2439
+ else {
2440
+ ratioGroups.set(key, { ratio: roundedRatio, count: 1 });
2441
+ }
2442
+ });
2443
+ const dominantGroup = Array.from(ratioGroups.values())
2444
+ .reduce((max, current) => current.count > max.count ? current : max);
2445
+ return Math.round(containerWidth / dominantGroup.ratio);
2446
+ case 'common':
2447
+ // 표준 비율 중 가장 적합한 것 선택
2448
+ const standardRatios = [
2449
+ { ratio: 16 / 9, name: '16:9' },
2450
+ { ratio: 4 / 3, name: '4:3' },
2451
+ { ratio: 1 / 1, name: '1:1' },
2452
+ { ratio: 3 / 2, name: '3:2' }
2453
+ ];
2454
+ const avgRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
2455
+ const bestStandard = standardRatios.reduce((best, current) => Math.abs(current.ratio - avgRatio) < Math.abs(best.ratio - avgRatio) ? current : best);
2456
+ if (this._config?.debug) {
2457
+ console.log(`📊 Using standard ratio: ${bestStandard.name} (avg: ${avgRatio.toFixed(2)})`);
2458
+ }
2459
+ return Math.round(containerWidth / bestStandard.ratio);
2460
+ case 'average':
2461
+ default:
2462
+ // 평균 비율 사용
2463
+ const averageRatio = aspectRatios.reduce((sum, ratio) => sum + ratio, 0) / aspectRatios.length;
2464
+ return Math.round(containerWidth / averageRatio);
2465
+ }
2466
+ }
2467
+ /**
2468
+ * 개별 이미지에 최적화된 렌더링 스타일 적용
2469
+ */
2470
+ applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio) {
2471
+ const ratio = imageAspectRatio / containerAspectRatio;
2472
+ if (Math.abs(ratio - 1) < 0.1) {
2473
+ // 비율이 거의 같으면 cover 사용
2474
+ img.style.objectFit = 'cover';
2475
+ img.style.objectPosition = 'center';
2476
+ }
2477
+ else if (ratio > 1.3) {
2478
+ // 이미지가 훨씬 가로형이면 contain으로 전체 보이기
2479
+ img.style.objectFit = 'contain';
2480
+ img.style.objectPosition = 'center';
2481
+ img.style.backgroundColor = '#f0f0f0'; // 빈 공간 배경색
2482
+ }
2483
+ else if (ratio < 0.7) {
2484
+ // 이미지가 훨씬 세로형이면 cover로 채우기
2485
+ img.style.objectFit = 'cover';
2486
+ img.style.objectPosition = 'center';
2487
+ }
2488
+ else {
2489
+ // 적당한 차이면 스마트 cover
2490
+ img.style.objectFit = 'cover';
2491
+ img.style.objectPosition = 'center';
2492
+ }
2493
+ if (this._config?.debug) {
2494
+ console.log(`🎨 Image style applied: objectFit=${img.style.objectFit}, ratio=${ratio.toFixed(2)}`);
2230
2495
  }
2231
2496
  }
2232
2497
  /**
@@ -2240,6 +2505,10 @@ class AdsModule {
2240
2505
  this.renderFallback(slot);
2241
2506
  return;
2242
2507
  }
2508
+ // 🆕 동적 크기 조정: 배너 광고의 경우 이미지 크기 기반으로 컨테이너 최적화
2509
+ if (slot.adType === AdType.BANNER && adstageData.length > 0) {
2510
+ await this.optimizeContainerForBannerAds(slot, adstageData);
2511
+ }
2243
2512
  // 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
2244
2513
  if (adstageData.length > 1 || slot.config?.autoSlide) {
2245
2514
  await this.renderAdSlider(slot, adstageData);
@@ -2261,6 +2530,32 @@ class AdsModule {
2261
2530
  this.renderFallback(slot);
2262
2531
  }
2263
2532
  }
2533
+ /**
2534
+ * 배너 광고를 위한 컨테이너 최적화
2535
+ */
2536
+ async optimizeContainerForBannerAds(slot, advertisements) {
2537
+ try {
2538
+ const container = document.getElementById(slot.containerId);
2539
+ const adElement = document.getElementById(slot.id);
2540
+ if (!container || !adElement)
2541
+ return;
2542
+ // 현재 컨테이너 너비 확인
2543
+ const containerWidth = container.getBoundingClientRect().width || 300;
2544
+ // 최적 크기 계산
2545
+ const optimalSize = await this.calculateOptimalContainerSize(advertisements, containerWidth, slot.adType);
2546
+ // 컨테이너 크기 동적 조정
2547
+ adElement.style.height = optimalSize.height;
2548
+ // 슬롯 정보 업데이트
2549
+ slot.optimizedHeight = optimalSize.height;
2550
+ slot.aspectRatio = optimalSize.aspectRatio;
2551
+ if (this._config?.debug) {
2552
+ console.log(`🔧 Container optimized for ${advertisements.length} banner ads: ${optimalSize.height}`);
2553
+ }
2554
+ }
2555
+ catch (error) {
2556
+ console.warn('Container optimization failed, using default size:', error);
2557
+ }
2558
+ }
2264
2559
  /**
2265
2560
  * 기본 viewability 추적 시작
2266
2561
  */
@@ -2312,22 +2607,110 @@ class AdsModule {
2312
2607
  }
2313
2608
  }
2314
2609
  /**
2315
- * Fallback 광고 렌더링 - DOM에서 완전 제거
2610
+ * Fallback 광고 렌더링 - AdStage 확실한 컨테이너 우선 탐지
2316
2611
  */
2317
2612
  renderFallback(slot) {
2318
2613
  const element = document.getElementById(slot.id);
2319
2614
  if (element) {
2320
- // 부모 컨테이너에서 광고 슬롯을 완전히 제거
2321
- const parentContainer = element.parentNode;
2322
- if (parentContainer) {
2323
- parentContainer.removeChild(element);
2615
+ // 1순위: AdStage가 생성한 확실한 컨테이너들 (데이터 속성 기반)
2616
+ const adstageContainers = [
2617
+ element.querySelector('[data-adstage-container="true"]'), // 내부 AdStage 컨테이너
2618
+ element.closest('[data-adstage-container="true"]'), // 상위 AdStage 컨테이너
2619
+ element, // 자기 자신이 AdStage 컨테이너인 경우
2620
+ ].filter(el => el && el.hasAttribute('data-adstage-container'));
2621
+ // 2순위: AdStage 클래스 기반 컨테이너들
2622
+ const classBasedContainers = [
2623
+ element.closest('.adstage-slot'),
2624
+ element.closest('.adstage-banner'),
2625
+ element.closest('.adstage-text'),
2626
+ element.closest('.adstage-video'),
2627
+ element.closest('.adstage-native'),
2628
+ element.closest('.adstage-interstitial'),
2629
+ ].filter(Boolean);
2630
+ // 3순위: 일반적인 광고 컨테이너 패턴들 (fallback)
2631
+ const generalContainers = [
2632
+ element.closest('[class*="ad"]'),
2633
+ element.closest('[class*="banner"]'),
2634
+ element.closest('[class*="container"]'),
2635
+ element.closest('div[style*="height"]'),
2636
+ element.closest('div[style*="min-height"]'),
2637
+ element.parentElement
2638
+ ].filter(Boolean);
2639
+ // 우선순위에 따라 컨테이너 선택
2640
+ const possibleContainers = [
2641
+ ...adstageContainers,
2642
+ ...classBasedContainers,
2643
+ ...generalContainers
2644
+ ];
2645
+ // 가장 적절한 컨테이너 선택
2646
+ const targetContainer = possibleContainers[0];
2647
+ if (targetContainer) {
2648
+ // 컨테이너 타입 로깅
2649
+ let containerType = 'unknown';
2650
+ if (targetContainer.hasAttribute('data-adstage-container')) {
2651
+ containerType = 'adstage-official';
2652
+ }
2653
+ else if (targetContainer.classList.contains('adstage-slot')) {
2654
+ containerType = 'adstage-class';
2655
+ }
2656
+ else {
2657
+ containerType = 'generic';
2658
+ }
2659
+ targetContainer.style.cssText += `
2660
+ height: 0px !important;
2661
+ min-height: 0px !important;
2662
+ padding: 0px !important;
2663
+ margin: 0px !important;
2664
+ border: none !important;
2665
+ overflow: hidden !important;
2666
+ display: block !important;
2667
+ `;
2668
+ // 내부 모든 요소 제거
2669
+ targetContainer.innerHTML = '';
2670
+ // 빈 상태임을 표시하는 속성 추가
2671
+ targetContainer.setAttribute('data-adstage-empty', 'true');
2324
2672
  if (this._config?.debug) {
2325
- console.warn(`⚠️ Ad slot completely removed from DOM: ${slot.id}`);
2673
+ console.warn(`⚠️ Ad container collapsed (${containerType}): ${slot.id}`, targetContainer);
2326
2674
  }
2327
2675
  }
2676
+ else {
2677
+ // 컨테이너를 찾지 못한 경우 새로운 빈 컨테이너 생성
2678
+ this.createEmptyContainer(slot);
2679
+ }
2680
+ }
2681
+ // 슬롯 상태 업데이트 (제거하지 않고 빈 상태로 마킹)
2682
+ slot.advertisement = undefined;
2683
+ slot.isEmpty = true;
2684
+ }
2685
+ /**
2686
+ * 빈 컨테이너 생성 (컨테이너를 찾지 못한 경우)
2687
+ */
2688
+ createEmptyContainer(slot) {
2689
+ const originalContainer = document.getElementById(slot.containerId);
2690
+ if (originalContainer) {
2691
+ // 기존 내용 제거
2692
+ originalContainer.innerHTML = '';
2693
+ // 빈 AdStage 컨테이너 생성
2694
+ const emptyElement = document.createElement('div');
2695
+ emptyElement.id = slot.id;
2696
+ emptyElement.className = 'adstage-slot adstage-empty';
2697
+ emptyElement.setAttribute('data-adstage-container', 'true');
2698
+ emptyElement.setAttribute('data-adstage-empty', 'true');
2699
+ emptyElement.setAttribute('data-adstage-slot', slot.id);
2700
+ emptyElement.style.cssText = `
2701
+ height: 0px !important;
2702
+ min-height: 0px !important;
2703
+ padding: 0px !important;
2704
+ margin: 0px !important;
2705
+ border: none !important;
2706
+ overflow: hidden !important;
2707
+ display: block !important;
2708
+ `;
2709
+ originalContainer.appendChild(emptyElement);
2710
+ if (this._config?.debug) {
2711
+ console.warn(`⚠️ Created empty AdStage container: ${slot.id}`);
2712
+ }
2328
2713
  }
2329
- // 슬롯 맵에서도 제거
2330
- this.slots.delete(slot.id);
2331
2714
  }
2332
2715
  /**
2333
2716
  * 광고 데이터 가져오기
@@ -2395,23 +2778,25 @@ class AdsModule {
2395
2778
  }
2396
2779
  };
2397
2780
  let sliderElement;
2781
+ // 최적화된 슬라이더 옵션 준비
2782
+ const optimizedSliderOptions = {
2783
+ autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
2784
+ ...slot.config,
2785
+ // 🆕 동적 크기 정보 전달
2786
+ optimizedHeight: slot.optimizedHeight,
2787
+ aspectRatio: slot.aspectRatio
2788
+ };
2398
2789
  // 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
2399
2790
  if (slot.adType === AdType.TEXT) {
2400
- sliderElement = TextTransitionManager.createTextTransitionContainer(slot, advertisements, {
2401
- autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
2402
- ...slot.config
2403
- }, trackEventCallback);
2791
+ sliderElement = TextTransitionManager.createTextTransitionContainer(slot, advertisements, optimizedSliderOptions, trackEventCallback);
2404
2792
  if (this._config?.debug) {
2405
2793
  console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
2406
2794
  }
2407
2795
  }
2408
2796
  else {
2409
- sliderElement = CarouselSliderManager.createSliderContainer(slot, advertisements, {
2410
- autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
2411
- ...slot.config
2412
- }, trackEventCallback);
2797
+ sliderElement = CarouselSliderManager.createSliderContainer(slot, advertisements, optimizedSliderOptions, trackEventCallback);
2413
2798
  if (this._config?.debug) {
2414
- console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
2799
+ console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads (optimized: ${slot.optimizedHeight || 'default'})`);
2415
2800
  }
2416
2801
  }
2417
2802
  // 기존 내용 제거하고 슬라이더 추가
@@ -2438,19 +2823,23 @@ class AdsModule {
2438
2823
  // 기본 HTML 구조 생성
2439
2824
  const adElement = document.createElement('div');
2440
2825
  adElement.className = 'adstage-ad';
2441
- adElement.style.width = typeof slot.width === 'string' ? slot.width : `${slot.width}px`;
2442
- adElement.style.height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
2826
+ // 스마트한 크기 설정 - 최적화된 크기가 있으면 사용
2827
+ const optimizedHeight = slot.optimizedHeight;
2828
+ const containerElement = container.parentElement || container;
2829
+ if (optimizedHeight) {
2830
+ adElement.style.width = '100%';
2831
+ adElement.style.height = optimizedHeight;
2832
+ }
2833
+ else {
2834
+ const { width, height } = this.calculateAdSize(containerElement, slot.adType, slot.config || {});
2835
+ adElement.style.width = width;
2836
+ adElement.style.height = height;
2837
+ }
2443
2838
  // 광고 타입별 렌더링
2444
2839
  switch (slot.adType) {
2445
2840
  case AdType.BANNER:
2446
2841
  if (ad.imageUrl) {
2447
- const img = document.createElement('img');
2448
- img.src = ad.imageUrl;
2449
- img.alt = ad.title;
2450
- img.style.width = '100%';
2451
- img.style.height = '100%';
2452
- img.style.objectFit = 'cover';
2453
- adElement.appendChild(img);
2842
+ await this.renderOptimizedBannerImage(adElement, ad, slot);
2454
2843
  }
2455
2844
  break;
2456
2845
  case AdType.TEXT:
@@ -2485,6 +2874,50 @@ class AdsModule {
2485
2874
  container.innerHTML = '';
2486
2875
  container.appendChild(adElement);
2487
2876
  }
2877
+ /**
2878
+ * 최적화된 배너 이미지 렌더링
2879
+ */
2880
+ async renderOptimizedBannerImage(adElement, ad, slot) {
2881
+ try {
2882
+ // 이미지 크기 정보 로드
2883
+ const imageDimensions = await this.loadImageDimensions(ad.imageUrl);
2884
+ const imageAspectRatio = imageDimensions.width / imageDimensions.height;
2885
+ // 컨테이너 비율 계산
2886
+ const containerAspectRatio = slot.aspectRatio || 16 / 9;
2887
+ // 이미지 요소 생성
2888
+ const img = document.createElement('img');
2889
+ img.src = ad.imageUrl;
2890
+ img.alt = ad.title;
2891
+ img.style.width = '100%';
2892
+ img.style.height = '100%';
2893
+ // 🎨 최적화된 스타일 적용
2894
+ this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
2895
+ // 이미지 로드 완료 처리
2896
+ img.onload = () => {
2897
+ if (this._config?.debug) {
2898
+ console.log(`🖼️ Optimized banner image loaded: ${imageDimensions.width}x${imageDimensions.height} (ratio: ${imageAspectRatio.toFixed(2)})`);
2899
+ }
2900
+ };
2901
+ // 에러 처리
2902
+ img.onerror = () => {
2903
+ console.warn(`Failed to load banner image: ${ad.imageUrl}`);
2904
+ // 폴백 텍스트 표시
2905
+ adElement.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #666;">${ad.title}</div>`;
2906
+ };
2907
+ adElement.appendChild(img);
2908
+ }
2909
+ catch (error) {
2910
+ console.warn('Failed to optimize banner image, using fallback:', error);
2911
+ // 기본 이미지 렌더링 (폴백)
2912
+ const img = document.createElement('img');
2913
+ img.src = ad.imageUrl;
2914
+ img.alt = ad.title;
2915
+ img.style.width = '100%';
2916
+ img.style.height = '100%';
2917
+ img.style.objectFit = 'cover';
2918
+ adElement.appendChild(img);
2919
+ }
2920
+ }
2488
2921
  /**
2489
2922
  * 광고 슬롯 새로고침
2490
2923
  */