@adstage/web-sdk 2.1.2 โ†’ 2.1.4

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.
@@ -82,7 +82,7 @@ export class AdsModule implements BaseModule {
82
82
 
83
83
  const adstageOptions = {
84
84
  width: options?.width || '100%',
85
- height: options?.height || 250,
85
+ height: options?.height || 'auto', // ๐Ÿ”ง ๋™์  ํฌ๊ธฐ ์กฐ์ • ํ™œ์šฉ
86
86
  autoSlide: options?.autoSlide || false,
87
87
  slideInterval: options?.slideInterval || 5000,
88
88
  onClick: options?.onClick,
@@ -284,8 +284,10 @@ export class AdsModule implements BaseModule {
284
284
  adElement.setAttribute('data-adstage-type', type);
285
285
  adElement.setAttribute('data-adstage-slot', slotId);
286
286
 
287
- adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
288
- adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
287
+ // ์Šค๋งˆํŠธํ•œ ํฌ๊ธฐ ์„ค์ •
288
+ const { width, height } = this.calculateAdSize(container, type, options);
289
+ adElement.style.width = width;
290
+ adElement.style.height = height;
289
291
  adElement.style.border = '1px dashed #ccc';
290
292
  adElement.style.display = 'flex';
291
293
  adElement.style.alignItems = 'center';
@@ -297,7 +299,313 @@ export class AdsModule implements BaseModule {
297
299
  container.appendChild(adElement);
298
300
 
299
301
  if (this._config?.debug) {
300
- 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%' && 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
+ }
419
+
420
+ /**
421
+ * ์—ฌ๋Ÿฌ ๊ด‘๊ณ ์˜ ์ตœ์  ์ปจํ…Œ์ด๋„ˆ ํฌ๊ธฐ ๊ณ„์‚ฐ (๋™์  ํฌ๊ธฐ ์กฐ์ •)
422
+ */
423
+ private async calculateOptimalContainerSize(
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
+ }
483
+
484
+ /**
485
+ * ์ตœ์  ํฌ๊ธฐ ์กฐ์ • ์ „๋žต ์„ ํƒ
486
+ */
487
+ private selectOptimalSizeStrategy(dimensions: { width: number; height: number }[]): 'average' | 'common' | 'dominant' {
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
+ }
519
+
520
+ /**
521
+ * ์ „๋žต์— ๋”ฐ๋ฅธ ์ตœ์  ๋†’์ด ๊ณ„์‚ฐ
522
+ */
523
+ private calculateOptimalHeight(
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)}`);
301
609
  }
302
610
  }
303
611
 
@@ -314,6 +622,11 @@ export class AdsModule implements BaseModule {
314
622
  return;
315
623
  }
316
624
 
625
+ // ๐Ÿ†• ๋™์  ํฌ๊ธฐ ์กฐ์ •: ๋ฐฐ๋„ˆ ๊ด‘๊ณ ์˜ ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๊ธฐ๋ฐ˜์œผ๋กœ ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”
626
+ if (slot.adType === AdType.BANNER && adstageData.length > 0) {
627
+ await this.optimizeContainerForBannerAds(slot, adstageData);
628
+ }
629
+
317
630
  // ๊ด‘๊ณ ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ด๊ฑฐ๋‚˜ autoSlide ์˜ต์…˜์ด ์žˆ์œผ๋ฉด ์Šฌ๋ผ์ด๋”๋กœ ๋ Œ๋”๋ง
318
631
  if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
319
632
  await this.renderAdSlider(slot, adstageData);
@@ -337,6 +650,38 @@ export class AdsModule implements BaseModule {
337
650
  }
338
651
  }
339
652
 
653
+ /**
654
+ * ๋ฐฐ๋„ˆ ๊ด‘๊ณ ๋ฅผ ์œ„ํ•œ ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”
655
+ */
656
+ private async optimizeContainerForBannerAds(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
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
+ }
684
+
340
685
  /**
341
686
  * ๊ธฐ๋ณธ viewability ์ถ”์  ์‹œ์ž‘
342
687
  */
@@ -606,15 +951,21 @@ export class AdsModule implements BaseModule {
606
951
 
607
952
  let sliderElement: HTMLElement;
608
953
 
954
+ // ์ตœ์ ํ™”๋œ ์Šฌ๋ผ์ด๋” ์˜ต์…˜ ์ค€๋น„
955
+ const optimizedSliderOptions = {
956
+ autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
957
+ ...slot.config,
958
+ // ๐Ÿ†• ๋™์  ํฌ๊ธฐ ์ •๋ณด ์ „๋‹ฌ
959
+ optimizedHeight: (slot as any).optimizedHeight,
960
+ aspectRatio: (slot as any).aspectRatio
961
+ };
962
+
609
963
  // ํ…์ŠคํŠธ ๊ด‘๊ณ ๋Š” TextTransitionManager ์‚ฌ์šฉ, ๊ทธ ์™ธ๋Š” CarouselSliderManager ์‚ฌ์šฉ
610
964
  if (slot.adType === AdType.TEXT) {
611
965
  sliderElement = TextTransitionManager.createTextTransitionContainer(
612
966
  slot,
613
967
  advertisements,
614
- {
615
- autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
616
- ...slot.config
617
- },
968
+ optimizedSliderOptions,
618
969
  trackEventCallback
619
970
  );
620
971
 
@@ -625,15 +976,12 @@ export class AdsModule implements BaseModule {
625
976
  sliderElement = CarouselSliderManager.createSliderContainer(
626
977
  slot,
627
978
  advertisements,
628
- {
629
- autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
630
- ...slot.config
631
- },
979
+ optimizedSliderOptions,
632
980
  trackEventCallback
633
981
  );
634
982
 
635
983
  if (this._config?.debug) {
636
- console.log(`๐ŸŽ  Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
984
+ console.log(`๐ŸŽ  Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads (optimized: ${(slot as any).optimizedHeight || 'default'})`);
637
985
  }
638
986
  }
639
987
 
@@ -664,20 +1012,25 @@ export class AdsModule implements BaseModule {
664
1012
  // ๊ธฐ๋ณธ HTML ๊ตฌ์กฐ ์ƒ์„ฑ
665
1013
  const adElement = document.createElement('div');
666
1014
  adElement.className = 'adstage-ad';
667
- adElement.style.width = typeof slot.width === 'string' ? slot.width : `${slot.width}px`;
668
- adElement.style.height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
1015
+
1016
+ // ์Šค๋งˆํŠธํ•œ ํฌ๊ธฐ ์„ค์ • - ์ตœ์ ํ™”๋œ ํฌ๊ธฐ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ
1017
+ const optimizedHeight = (slot as any).optimizedHeight;
1018
+ const containerElement = container.parentElement || container;
1019
+
1020
+ if (optimizedHeight) {
1021
+ adElement.style.width = '100%';
1022
+ adElement.style.height = optimizedHeight;
1023
+ } else {
1024
+ const { width, height } = this.calculateAdSize(containerElement, slot.adType, slot.config || {});
1025
+ adElement.style.width = width;
1026
+ adElement.style.height = height;
1027
+ }
669
1028
 
670
1029
  // ๊ด‘๊ณ  ํƒ€์ž…๋ณ„ ๋ Œ๋”๋ง
671
1030
  switch (slot.adType) {
672
1031
  case AdType.BANNER:
673
1032
  if (ad.imageUrl) {
674
- const img = document.createElement('img');
675
- img.src = ad.imageUrl;
676
- img.alt = ad.title;
677
- img.style.width = '100%';
678
- img.style.height = '100%';
679
- img.style.objectFit = 'cover';
680
- adElement.appendChild(img);
1033
+ await this.renderOptimizedBannerImage(adElement, ad, slot);
681
1034
  }
682
1035
  break;
683
1036
 
@@ -718,6 +1071,58 @@ export class AdsModule implements BaseModule {
718
1071
  container.appendChild(adElement);
719
1072
  }
720
1073
 
1074
+ /**
1075
+ * ์ตœ์ ํ™”๋œ ๋ฐฐ๋„ˆ ์ด๋ฏธ์ง€ ๋ Œ๋”๋ง
1076
+ */
1077
+ private async renderOptimizedBannerImage(adElement: HTMLElement, ad: Advertisement, slot: AdSlot): Promise<void> {
1078
+ try {
1079
+ // ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ •๋ณด ๋กœ๋“œ
1080
+ const imageDimensions = await this.loadImageDimensions(ad.imageUrl!);
1081
+ const imageAspectRatio = imageDimensions.width / imageDimensions.height;
1082
+
1083
+ // ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ๊ณ„์‚ฐ
1084
+ const containerAspectRatio = (slot as any).aspectRatio || 16/9;
1085
+
1086
+ // ์ด๋ฏธ์ง€ ์š”์†Œ ์ƒ์„ฑ
1087
+ const img = document.createElement('img');
1088
+ img.src = ad.imageUrl!;
1089
+ img.alt = ad.title;
1090
+ img.style.width = '100%';
1091
+ img.style.height = '100%';
1092
+
1093
+ // ๐ŸŽจ ์ตœ์ ํ™”๋œ ์Šคํƒ€์ผ ์ ์šฉ
1094
+ this.applyOptimizedImageStyle(img, imageAspectRatio, containerAspectRatio);
1095
+
1096
+ // ์ด๋ฏธ์ง€ ๋กœ๋“œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
1097
+ img.onload = () => {
1098
+ if (this._config?.debug) {
1099
+ console.log(`๐Ÿ–ผ๏ธ Optimized banner image loaded: ${imageDimensions.width}x${imageDimensions.height} (ratio: ${imageAspectRatio.toFixed(2)})`);
1100
+ }
1101
+ };
1102
+
1103
+ // ์—๋Ÿฌ ์ฒ˜๋ฆฌ
1104
+ img.onerror = () => {
1105
+ console.warn(`Failed to load banner image: ${ad.imageUrl}`);
1106
+ // ํด๋ฐฑ ํ…์ŠคํŠธ ํ‘œ์‹œ
1107
+ adElement.innerHTML = `<div style="display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #666;">${ad.title}</div>`;
1108
+ };
1109
+
1110
+ adElement.appendChild(img);
1111
+
1112
+ } catch (error) {
1113
+ console.warn('Failed to optimize banner image, using fallback:', error);
1114
+
1115
+ // ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋ Œ๋”๋ง (ํด๋ฐฑ)
1116
+ const img = document.createElement('img');
1117
+ img.src = ad.imageUrl!;
1118
+ img.alt = ad.title;
1119
+ img.style.width = '100%';
1120
+ img.style.height = '100%';
1121
+ img.style.objectFit = 'cover';
1122
+ adElement.appendChild(img);
1123
+ }
1124
+ }
1125
+
721
1126
  /**
722
1127
  * ๊ด‘๊ณ  ์Šฌ๋กฏ ์ƒˆ๋กœ๊ณ ์นจ
723
1128
  */