@adstage/web-sdk 2.4.4 → 2.4.6

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 CHANGED
@@ -47,11 +47,15 @@ class ViewableEventTracker {
47
47
  static isDuplicateViewable(adId, slotId, debug = false) {
48
48
  const key = `${adId}_${slotId}`;
49
49
  const now = Date.now();
50
+ // 디버그 모드에 따른 쿨다운 시간 결정
51
+ const cooldownTime = debug ?
52
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
53
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
50
54
  // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
51
55
  const lastViewable = ViewableEventTracker.viewableTracker.get(key);
52
- if (lastViewable && (now - lastViewable) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
56
+ if (lastViewable && (now - lastViewable) < cooldownTime) {
53
57
  if (debug) {
54
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - lastViewable)) / 1000)}s remaining`);
58
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
55
59
  }
56
60
  return true;
57
61
  }
@@ -60,9 +64,9 @@ class ViewableEventTracker {
60
64
  const sessionViewable = sessionStorage.getItem(sessionKey);
61
65
  if (sessionViewable) {
62
66
  const sessionTime = parseInt(sessionViewable, 10);
63
- if (!isNaN(sessionTime) && (now - sessionTime) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
67
+ if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
64
68
  if (debug) {
65
- console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
69
+ console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - sessionTime)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
66
70
  }
67
71
  // 메모리에도 기록하여 이후 요청 최적화
68
72
  ViewableEventTracker.viewableTracker.set(key, sessionTime);
@@ -74,6 +78,9 @@ class ViewableEventTracker {
74
78
  sessionStorage.setItem(sessionKey, now.toString());
75
79
  // 오래된 세션 스토리지 데이터 정리 (선택적)
76
80
  ViewableEventTracker.cleanupOldViewables();
81
+ if (debug) {
82
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
83
+ }
77
84
  return false;
78
85
  }
79
86
  /**
@@ -81,7 +88,8 @@ class ViewableEventTracker {
81
88
  */
82
89
  static cleanupOldViewables() {
83
90
  const now = Date.now();
84
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
91
+ // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
92
+ const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
85
93
  // 세션 스토리지 정리
86
94
  for (let i = 0; i < sessionStorage.length; i++) {
87
95
  const key = sessionStorage.key(i);
@@ -104,14 +112,33 @@ class ViewableEventTracker {
104
112
  }
105
113
  }
106
114
  /**
107
- * 모든 추적 데이터 정리
115
+ * 모든 추적 데이터 정리 (디버그용)
108
116
  */
109
117
  static clear() {
110
118
  ViewableEventTracker.viewableTracker.clear();
119
+ // 세션 스토리지에서도 adstage_viewable_ 키들 제거
120
+ const keysToRemove = [];
121
+ for (let i = 0; i < sessionStorage.length; i++) {
122
+ const key = sessionStorage.key(i);
123
+ if (key && key.startsWith('adstage_viewable_')) {
124
+ keysToRemove.push(key);
125
+ }
126
+ }
127
+ keysToRemove.forEach(key => sessionStorage.removeItem(key));
128
+ }
129
+ /**
130
+ * 특정 광고의 viewable 추적 초기화 (디버그용)
131
+ */
132
+ static clearAdViewable(adId, slotId) {
133
+ const key = `${adId}_${slotId}`;
134
+ ViewableEventTracker.viewableTracker.delete(key);
135
+ const sessionKey = `adstage_viewable_${key}`;
136
+ sessionStorage.removeItem(sessionKey);
111
137
  }
112
138
  }
113
139
  ViewableEventTracker.viewableTracker = new Map();
114
- ViewableEventTracker.VIEWABLE_COOLDOWN = 300000; // 5분 쿨다운
140
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
141
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
115
142
 
116
143
  /**
117
144
  * SSR 안전한 DOM API 래퍼 클래스
@@ -607,13 +634,32 @@ class AdvertisementEventTracker {
607
634
  riskLevel: additionalData.riskLevel,
608
635
  }),
609
636
  };
610
- await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
637
+ const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
638
+ const headers = ApiHeaders.createForEvents(this.apiKey, eventData);
639
+ if (this.debug) {
640
+ console.log(`🚀 Sending advertisement event: ${eventType} for ad ${adId}`, {
641
+ url,
642
+ headers,
643
+ eventData
644
+ });
645
+ }
646
+ const response = await fetch(url, {
611
647
  method: 'POST',
612
- headers: ApiHeaders.createForEvents(this.apiKey, eventData),
648
+ headers,
613
649
  body: JSON.stringify(eventData),
614
650
  });
615
651
  if (this.debug) {
616
- console.log(`Tracked advertisement event: ${eventType} for ad ${adId}`, eventData);
652
+ console.log(`📡 API Response Status: ${response.status} ${response.statusText}`, {
653
+ url,
654
+ ok: response.ok
655
+ });
656
+ }
657
+ if (!response.ok) {
658
+ const errorText = await response.text();
659
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
660
+ }
661
+ if (this.debug) {
662
+ console.log(`✅ Successfully tracked advertisement event: ${eventType} for ad ${adId}`);
617
663
  }
618
664
  }
619
665
  catch (error) {
@@ -948,6 +994,7 @@ const API_PATHS = {
948
994
  /** 광고 관련 */
949
995
  advertisements: {
950
996
  list: '/advertisements/list',
997
+ detail: '/advertisements',
951
998
  events: '/advertisements/events'
952
999
  },
953
1000
  /** 이벤트 관련 */
@@ -966,6 +1013,7 @@ class EndpointBuilder {
966
1013
  */
967
1014
  this.advertisements = {
968
1015
  list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
1016
+ detail: (adId) => `${this.baseUrl}${API_PATHS.advertisements.detail}/${adId}`,
969
1017
  events: (adId, eventType) => `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
970
1018
  };
971
1019
  /**
@@ -1206,7 +1254,7 @@ class TextAdRenderer extends BaseAdRenderer {
1206
1254
  // 기본 컨테이너 스타일
1207
1255
  const containerStyles = {
1208
1256
  ...this.getBaseContainerStyles(slot),
1209
- padding: '20px',
1257
+ padding: '4px',
1210
1258
  background: 'transparent',
1211
1259
  display: 'flex',
1212
1260
  'align-items': 'center',
@@ -1234,7 +1282,7 @@ class TextAdRenderer extends BaseAdRenderer {
1234
1282
  }
1235
1283
  // 텍스트 콘텐츠 생성
1236
1284
  const textContent = this.createTextElement(ad.textContent, 'div', {
1237
- 'font-size': '16px',
1285
+ 'font-size': '14px',
1238
1286
  'font-weight': '500',
1239
1287
  color: '#212529',
1240
1288
  width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
@@ -2285,8 +2333,6 @@ class AdRenderer {
2285
2333
  case AdType.TEXT: {
2286
2334
  const textDiv = document.createElement('div');
2287
2335
  textDiv.innerHTML = `
2288
- <h3>${ad.title}</h3>
2289
- ${ad.description ? `<p>${ad.description}</p>` : ''}
2290
2336
  ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
2291
2337
  `;
2292
2338
  adElement.appendChild(textDiv);
@@ -2412,7 +2458,7 @@ class AdRenderer {
2412
2458
  case AdType.BANNER:
2413
2459
  return '250px'; // 일반 배너
2414
2460
  case AdType.TEXT:
2415
- return '120px'; // 텍스트는 좀 더 작게
2461
+ return '60px'; // 텍스트는 좀 더 작게
2416
2462
  case AdType.VIDEO:
2417
2463
  return '360px'; // 비디오는 16:9 비율 고려
2418
2464
  case AdType.NATIVE:
@@ -2682,6 +2728,8 @@ class AdsModule {
2682
2728
  autoSlide: options?.autoSlide || false,
2683
2729
  slideInterval: options?.slideInterval || 5000,
2684
2730
  onClick: options?.onClick,
2731
+ // 특정 광고 ID
2732
+ adId: options?.adId,
2685
2733
  // 필터링 옵션들 전달
2686
2734
  language: options?.language,
2687
2735
  deviceType: options?.deviceType,
@@ -2698,6 +2746,8 @@ class AdsModule {
2698
2746
  maxLines: options?.maxLines || 3,
2699
2747
  style: options?.style || 'default',
2700
2748
  onClick: options?.onClick,
2749
+ // 특정 광고 ID
2750
+ adId: options?.adId,
2701
2751
  // 필터링 옵션들 전달
2702
2752
  language: options?.language,
2703
2753
  deviceType: options?.deviceType,
@@ -2897,8 +2947,8 @@ class AdsModule {
2897
2947
  // 기본 fraud 검사
2898
2948
  const fraudDetector = new BasicFraudDetector();
2899
2949
  // viewability 추적
2900
- const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
2901
- this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2950
+ const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
2951
+ await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2902
2952
  });
2903
2953
  // 정리를 위해 저장
2904
2954
  slot.viewabilityTracker = tracker;
@@ -2952,7 +3002,27 @@ class AdsModule {
2952
3002
  if (!this._config?.apiKey) {
2953
3003
  throw new Error('API key not configured');
2954
3004
  }
2955
- // GET 요청용 query parameters 구성
3005
+ // 특정 광고 ID가 지정된 경우, 해당 광고만 요청
3006
+ if (options.adId) {
3007
+ const url = `${endpoints.advertisements.detail(options.adId)}`;
3008
+ const response = await fetch(url, {
3009
+ method: 'GET',
3010
+ headers: ApiHeaders.create(this._config.apiKey)
3011
+ });
3012
+ if (!response.ok) {
3013
+ if (response.status === 404) {
3014
+ console.warn(`🚫 Advertisement not found with ID: ${options.adId}`);
3015
+ return [];
3016
+ }
3017
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
3018
+ }
3019
+ const advertisement = await response.json();
3020
+ if (this._config?.debug) {
3021
+ console.log(`📊 Fetched specific ad with ID: ${options.adId}`, advertisement);
3022
+ }
3023
+ return [advertisement];
3024
+ }
3025
+ // 일반적인 광고 목록 요청
2956
3026
  const params = new URLSearchParams();
2957
3027
  params.append('adType', type);
2958
3028
  // 백엔드 API에서 실제 지원하는 필터링 옵션들만 추가
package/dist/index.d.ts CHANGED
@@ -220,6 +220,7 @@ interface AdOptions {
220
220
  autoplay?: boolean;
221
221
  muted?: boolean;
222
222
  onClick?: (adData: any) => void;
223
+ adId?: string;
223
224
  language?: 'ko' | 'en' | 'ja' | 'zh';
224
225
  deviceType?: 'MOBILE' | 'DESKTOP';
225
226
  country?: 'KR' | 'US' | 'JP' | 'CN' | 'DE';
package/dist/index.esm.js CHANGED
@@ -45,11 +45,15 @@ class ViewableEventTracker {
45
45
  static isDuplicateViewable(adId, slotId, debug = false) {
46
46
  const key = `${adId}_${slotId}`;
47
47
  const now = Date.now();
48
+ // 디버그 모드에 따른 쿨다운 시간 결정
49
+ const cooldownTime = debug ?
50
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
51
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
48
52
  // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
49
53
  const lastViewable = ViewableEventTracker.viewableTracker.get(key);
50
- if (lastViewable && (now - lastViewable) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
54
+ if (lastViewable && (now - lastViewable) < cooldownTime) {
51
55
  if (debug) {
52
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - lastViewable)) / 1000)}s remaining`);
56
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
53
57
  }
54
58
  return true;
55
59
  }
@@ -58,9 +62,9 @@ class ViewableEventTracker {
58
62
  const sessionViewable = sessionStorage.getItem(sessionKey);
59
63
  if (sessionViewable) {
60
64
  const sessionTime = parseInt(sessionViewable, 10);
61
- if (!isNaN(sessionTime) && (now - sessionTime) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
65
+ if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
62
66
  if (debug) {
63
- console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
67
+ console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - sessionTime)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
64
68
  }
65
69
  // 메모리에도 기록하여 이후 요청 최적화
66
70
  ViewableEventTracker.viewableTracker.set(key, sessionTime);
@@ -72,6 +76,9 @@ class ViewableEventTracker {
72
76
  sessionStorage.setItem(sessionKey, now.toString());
73
77
  // 오래된 세션 스토리지 데이터 정리 (선택적)
74
78
  ViewableEventTracker.cleanupOldViewables();
79
+ if (debug) {
80
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
81
+ }
75
82
  return false;
76
83
  }
77
84
  /**
@@ -79,7 +86,8 @@ class ViewableEventTracker {
79
86
  */
80
87
  static cleanupOldViewables() {
81
88
  const now = Date.now();
82
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
89
+ // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
90
+ const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
83
91
  // 세션 스토리지 정리
84
92
  for (let i = 0; i < sessionStorage.length; i++) {
85
93
  const key = sessionStorage.key(i);
@@ -102,14 +110,33 @@ class ViewableEventTracker {
102
110
  }
103
111
  }
104
112
  /**
105
- * 모든 추적 데이터 정리
113
+ * 모든 추적 데이터 정리 (디버그용)
106
114
  */
107
115
  static clear() {
108
116
  ViewableEventTracker.viewableTracker.clear();
117
+ // 세션 스토리지에서도 adstage_viewable_ 키들 제거
118
+ const keysToRemove = [];
119
+ for (let i = 0; i < sessionStorage.length; i++) {
120
+ const key = sessionStorage.key(i);
121
+ if (key && key.startsWith('adstage_viewable_')) {
122
+ keysToRemove.push(key);
123
+ }
124
+ }
125
+ keysToRemove.forEach(key => sessionStorage.removeItem(key));
126
+ }
127
+ /**
128
+ * 특정 광고의 viewable 추적 초기화 (디버그용)
129
+ */
130
+ static clearAdViewable(adId, slotId) {
131
+ const key = `${adId}_${slotId}`;
132
+ ViewableEventTracker.viewableTracker.delete(key);
133
+ const sessionKey = `adstage_viewable_${key}`;
134
+ sessionStorage.removeItem(sessionKey);
109
135
  }
110
136
  }
111
137
  ViewableEventTracker.viewableTracker = new Map();
112
- ViewableEventTracker.VIEWABLE_COOLDOWN = 300000; // 5분 쿨다운
138
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
139
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
113
140
 
114
141
  /**
115
142
  * SSR 안전한 DOM API 래퍼 클래스
@@ -605,13 +632,32 @@ class AdvertisementEventTracker {
605
632
  riskLevel: additionalData.riskLevel,
606
633
  }),
607
634
  };
608
- await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
635
+ const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
636
+ const headers = ApiHeaders.createForEvents(this.apiKey, eventData);
637
+ if (this.debug) {
638
+ console.log(`🚀 Sending advertisement event: ${eventType} for ad ${adId}`, {
639
+ url,
640
+ headers,
641
+ eventData
642
+ });
643
+ }
644
+ const response = await fetch(url, {
609
645
  method: 'POST',
610
- headers: ApiHeaders.createForEvents(this.apiKey, eventData),
646
+ headers,
611
647
  body: JSON.stringify(eventData),
612
648
  });
613
649
  if (this.debug) {
614
- console.log(`Tracked advertisement event: ${eventType} for ad ${adId}`, eventData);
650
+ console.log(`📡 API Response Status: ${response.status} ${response.statusText}`, {
651
+ url,
652
+ ok: response.ok
653
+ });
654
+ }
655
+ if (!response.ok) {
656
+ const errorText = await response.text();
657
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
658
+ }
659
+ if (this.debug) {
660
+ console.log(`✅ Successfully tracked advertisement event: ${eventType} for ad ${adId}`);
615
661
  }
616
662
  }
617
663
  catch (error) {
@@ -946,6 +992,7 @@ const API_PATHS = {
946
992
  /** 광고 관련 */
947
993
  advertisements: {
948
994
  list: '/advertisements/list',
995
+ detail: '/advertisements',
949
996
  events: '/advertisements/events'
950
997
  },
951
998
  /** 이벤트 관련 */
@@ -964,6 +1011,7 @@ class EndpointBuilder {
964
1011
  */
965
1012
  this.advertisements = {
966
1013
  list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
1014
+ detail: (adId) => `${this.baseUrl}${API_PATHS.advertisements.detail}/${adId}`,
967
1015
  events: (adId, eventType) => `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
968
1016
  };
969
1017
  /**
@@ -1204,7 +1252,7 @@ class TextAdRenderer extends BaseAdRenderer {
1204
1252
  // 기본 컨테이너 스타일
1205
1253
  const containerStyles = {
1206
1254
  ...this.getBaseContainerStyles(slot),
1207
- padding: '20px',
1255
+ padding: '4px',
1208
1256
  background: 'transparent',
1209
1257
  display: 'flex',
1210
1258
  'align-items': 'center',
@@ -1232,7 +1280,7 @@ class TextAdRenderer extends BaseAdRenderer {
1232
1280
  }
1233
1281
  // 텍스트 콘텐츠 생성
1234
1282
  const textContent = this.createTextElement(ad.textContent, 'div', {
1235
- 'font-size': '16px',
1283
+ 'font-size': '14px',
1236
1284
  'font-weight': '500',
1237
1285
  color: '#212529',
1238
1286
  width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
@@ -2283,8 +2331,6 @@ class AdRenderer {
2283
2331
  case AdType.TEXT: {
2284
2332
  const textDiv = document.createElement('div');
2285
2333
  textDiv.innerHTML = `
2286
- <h3>${ad.title}</h3>
2287
- ${ad.description ? `<p>${ad.description}</p>` : ''}
2288
2334
  ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
2289
2335
  `;
2290
2336
  adElement.appendChild(textDiv);
@@ -2410,7 +2456,7 @@ class AdRenderer {
2410
2456
  case AdType.BANNER:
2411
2457
  return '250px'; // 일반 배너
2412
2458
  case AdType.TEXT:
2413
- return '120px'; // 텍스트는 좀 더 작게
2459
+ return '60px'; // 텍스트는 좀 더 작게
2414
2460
  case AdType.VIDEO:
2415
2461
  return '360px'; // 비디오는 16:9 비율 고려
2416
2462
  case AdType.NATIVE:
@@ -2680,6 +2726,8 @@ class AdsModule {
2680
2726
  autoSlide: options?.autoSlide || false,
2681
2727
  slideInterval: options?.slideInterval || 5000,
2682
2728
  onClick: options?.onClick,
2729
+ // 특정 광고 ID
2730
+ adId: options?.adId,
2683
2731
  // 필터링 옵션들 전달
2684
2732
  language: options?.language,
2685
2733
  deviceType: options?.deviceType,
@@ -2696,6 +2744,8 @@ class AdsModule {
2696
2744
  maxLines: options?.maxLines || 3,
2697
2745
  style: options?.style || 'default',
2698
2746
  onClick: options?.onClick,
2747
+ // 특정 광고 ID
2748
+ adId: options?.adId,
2699
2749
  // 필터링 옵션들 전달
2700
2750
  language: options?.language,
2701
2751
  deviceType: options?.deviceType,
@@ -2895,8 +2945,8 @@ class AdsModule {
2895
2945
  // 기본 fraud 검사
2896
2946
  const fraudDetector = new BasicFraudDetector();
2897
2947
  // viewability 추적
2898
- const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
2899
- this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2948
+ const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
2949
+ await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2900
2950
  });
2901
2951
  // 정리를 위해 저장
2902
2952
  slot.viewabilityTracker = tracker;
@@ -2950,7 +3000,27 @@ class AdsModule {
2950
3000
  if (!this._config?.apiKey) {
2951
3001
  throw new Error('API key not configured');
2952
3002
  }
2953
- // GET 요청용 query parameters 구성
3003
+ // 특정 광고 ID가 지정된 경우, 해당 광고만 요청
3004
+ if (options.adId) {
3005
+ const url = `${endpoints.advertisements.detail(options.adId)}`;
3006
+ const response = await fetch(url, {
3007
+ method: 'GET',
3008
+ headers: ApiHeaders.create(this._config.apiKey)
3009
+ });
3010
+ if (!response.ok) {
3011
+ if (response.status === 404) {
3012
+ console.warn(`🚫 Advertisement not found with ID: ${options.adId}`);
3013
+ return [];
3014
+ }
3015
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
3016
+ }
3017
+ const advertisement = await response.json();
3018
+ if (this._config?.debug) {
3019
+ console.log(`📊 Fetched specific ad with ID: ${options.adId}`, advertisement);
3020
+ }
3021
+ return [advertisement];
3022
+ }
3023
+ // 일반적인 광고 목록 요청
2954
3024
  const params = new URLSearchParams();
2955
3025
  params.append('adType', type);
2956
3026
  // 백엔드 API에서 실제 지원하는 필터링 옵션들만 추가
@@ -42,11 +42,15 @@ class ViewableEventTracker {
42
42
  static isDuplicateViewable(adId, slotId, debug = false) {
43
43
  const key = `${adId}_${slotId}`;
44
44
  const now = Date.now();
45
+ // 디버그 모드에 따른 쿨다운 시간 결정
46
+ const cooldownTime = debug ?
47
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
48
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
45
49
  // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
46
50
  const lastViewable = ViewableEventTracker.viewableTracker.get(key);
47
- if (lastViewable && (now - lastViewable) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
51
+ if (lastViewable && (now - lastViewable) < cooldownTime) {
48
52
  if (debug) {
49
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - lastViewable)) / 1000)}s remaining`);
53
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
50
54
  }
51
55
  return true;
52
56
  }
@@ -55,9 +59,9 @@ class ViewableEventTracker {
55
59
  const sessionViewable = sessionStorage.getItem(sessionKey);
56
60
  if (sessionViewable) {
57
61
  const sessionTime = parseInt(sessionViewable, 10);
58
- if (!isNaN(sessionTime) && (now - sessionTime) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
62
+ if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
59
63
  if (debug) {
60
- console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
64
+ console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - sessionTime)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
61
65
  }
62
66
  // 메모리에도 기록하여 이후 요청 최적화
63
67
  ViewableEventTracker.viewableTracker.set(key, sessionTime);
@@ -69,6 +73,9 @@ class ViewableEventTracker {
69
73
  sessionStorage.setItem(sessionKey, now.toString());
70
74
  // 오래된 세션 스토리지 데이터 정리 (선택적)
71
75
  ViewableEventTracker.cleanupOldViewables();
76
+ if (debug) {
77
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
78
+ }
72
79
  return false;
73
80
  }
74
81
  /**
@@ -76,7 +83,8 @@ class ViewableEventTracker {
76
83
  */
77
84
  static cleanupOldViewables() {
78
85
  const now = Date.now();
79
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
86
+ // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
87
+ const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
80
88
  // 세션 스토리지 정리
81
89
  for (let i = 0; i < sessionStorage.length; i++) {
82
90
  const key = sessionStorage.key(i);
@@ -99,14 +107,33 @@ class ViewableEventTracker {
99
107
  }
100
108
  }
101
109
  /**
102
- * 모든 추적 데이터 정리
110
+ * 모든 추적 데이터 정리 (디버그용)
103
111
  */
104
112
  static clear() {
105
113
  ViewableEventTracker.viewableTracker.clear();
114
+ // 세션 스토리지에서도 adstage_viewable_ 키들 제거
115
+ const keysToRemove = [];
116
+ for (let i = 0; i < sessionStorage.length; i++) {
117
+ const key = sessionStorage.key(i);
118
+ if (key && key.startsWith('adstage_viewable_')) {
119
+ keysToRemove.push(key);
120
+ }
121
+ }
122
+ keysToRemove.forEach(key => sessionStorage.removeItem(key));
123
+ }
124
+ /**
125
+ * 특정 광고의 viewable 추적 초기화 (디버그용)
126
+ */
127
+ static clearAdViewable(adId, slotId) {
128
+ const key = `${adId}_${slotId}`;
129
+ ViewableEventTracker.viewableTracker.delete(key);
130
+ const sessionKey = `adstage_viewable_${key}`;
131
+ sessionStorage.removeItem(sessionKey);
106
132
  }
107
133
  }
108
134
  ViewableEventTracker.viewableTracker = new Map();
109
- ViewableEventTracker.VIEWABLE_COOLDOWN = 300000; // 5분 쿨다운
135
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
136
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
110
137
 
111
138
  /**
112
139
  * SSR 안전한 DOM API 래퍼 클래스
@@ -602,13 +629,32 @@ class AdvertisementEventTracker {
602
629
  riskLevel: additionalData.riskLevel,
603
630
  }),
604
631
  };
605
- await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
632
+ const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
633
+ const headers = ApiHeaders.createForEvents(this.apiKey, eventData);
634
+ if (this.debug) {
635
+ console.log(`🚀 Sending advertisement event: ${eventType} for ad ${adId}`, {
636
+ url,
637
+ headers,
638
+ eventData
639
+ });
640
+ }
641
+ const response = await fetch(url, {
606
642
  method: 'POST',
607
- headers: ApiHeaders.createForEvents(this.apiKey, eventData),
643
+ headers,
608
644
  body: JSON.stringify(eventData),
609
645
  });
610
646
  if (this.debug) {
611
- console.log(`Tracked advertisement event: ${eventType} for ad ${adId}`, eventData);
647
+ console.log(`📡 API Response Status: ${response.status} ${response.statusText}`, {
648
+ url,
649
+ ok: response.ok
650
+ });
651
+ }
652
+ if (!response.ok) {
653
+ const errorText = await response.text();
654
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
655
+ }
656
+ if (this.debug) {
657
+ console.log(`✅ Successfully tracked advertisement event: ${eventType} for ad ${adId}`);
612
658
  }
613
659
  }
614
660
  catch (error) {
@@ -943,6 +989,7 @@ const API_PATHS = {
943
989
  /** 광고 관련 */
944
990
  advertisements: {
945
991
  list: '/advertisements/list',
992
+ detail: '/advertisements',
946
993
  events: '/advertisements/events'
947
994
  },
948
995
  /** 이벤트 관련 */
@@ -961,6 +1008,7 @@ class EndpointBuilder {
961
1008
  */
962
1009
  this.advertisements = {
963
1010
  list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
1011
+ detail: (adId) => `${this.baseUrl}${API_PATHS.advertisements.detail}/${adId}`,
964
1012
  events: (adId, eventType) => `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
965
1013
  };
966
1014
  /**
@@ -1201,7 +1249,7 @@ class TextAdRenderer extends BaseAdRenderer {
1201
1249
  // 기본 컨테이너 스타일
1202
1250
  const containerStyles = {
1203
1251
  ...this.getBaseContainerStyles(slot),
1204
- padding: '20px',
1252
+ padding: '4px',
1205
1253
  background: 'transparent',
1206
1254
  display: 'flex',
1207
1255
  'align-items': 'center',
@@ -1229,7 +1277,7 @@ class TextAdRenderer extends BaseAdRenderer {
1229
1277
  }
1230
1278
  // 텍스트 콘텐츠 생성
1231
1279
  const textContent = this.createTextElement(ad.textContent, 'div', {
1232
- 'font-size': '16px',
1280
+ 'font-size': '14px',
1233
1281
  'font-weight': '500',
1234
1282
  color: '#212529',
1235
1283
  width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
@@ -2280,8 +2328,6 @@ class AdRenderer {
2280
2328
  case AdType.TEXT: {
2281
2329
  const textDiv = document.createElement('div');
2282
2330
  textDiv.innerHTML = `
2283
- <h3>${ad.title}</h3>
2284
- ${ad.description ? `<p>${ad.description}</p>` : ''}
2285
2331
  ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
2286
2332
  `;
2287
2333
  adElement.appendChild(textDiv);
@@ -2407,7 +2453,7 @@ class AdRenderer {
2407
2453
  case AdType.BANNER:
2408
2454
  return '250px'; // 일반 배너
2409
2455
  case AdType.TEXT:
2410
- return '120px'; // 텍스트는 좀 더 작게
2456
+ return '60px'; // 텍스트는 좀 더 작게
2411
2457
  case AdType.VIDEO:
2412
2458
  return '360px'; // 비디오는 16:9 비율 고려
2413
2459
  case AdType.NATIVE:
@@ -2677,6 +2723,8 @@ class AdsModule {
2677
2723
  autoSlide: options?.autoSlide || false,
2678
2724
  slideInterval: options?.slideInterval || 5000,
2679
2725
  onClick: options?.onClick,
2726
+ // 특정 광고 ID
2727
+ adId: options?.adId,
2680
2728
  // 필터링 옵션들 전달
2681
2729
  language: options?.language,
2682
2730
  deviceType: options?.deviceType,
@@ -2693,6 +2741,8 @@ class AdsModule {
2693
2741
  maxLines: options?.maxLines || 3,
2694
2742
  style: options?.style || 'default',
2695
2743
  onClick: options?.onClick,
2744
+ // 특정 광고 ID
2745
+ adId: options?.adId,
2696
2746
  // 필터링 옵션들 전달
2697
2747
  language: options?.language,
2698
2748
  deviceType: options?.deviceType,
@@ -2892,8 +2942,8 @@ class AdsModule {
2892
2942
  // 기본 fraud 검사
2893
2943
  const fraudDetector = new BasicFraudDetector();
2894
2944
  // viewability 추적
2895
- const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
2896
- this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2945
+ const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
2946
+ await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
2897
2947
  });
2898
2948
  // 정리를 위해 저장
2899
2949
  slot.viewabilityTracker = tracker;
@@ -2947,7 +2997,27 @@ class AdsModule {
2947
2997
  if (!this._config?.apiKey) {
2948
2998
  throw new Error('API key not configured');
2949
2999
  }
2950
- // GET 요청용 query parameters 구성
3000
+ // 특정 광고 ID가 지정된 경우, 해당 광고만 요청
3001
+ if (options.adId) {
3002
+ const url = `${endpoints.advertisements.detail(options.adId)}`;
3003
+ const response = await fetch(url, {
3004
+ method: 'GET',
3005
+ headers: ApiHeaders.create(this._config.apiKey)
3006
+ });
3007
+ if (!response.ok) {
3008
+ if (response.status === 404) {
3009
+ console.warn(`🚫 Advertisement not found with ID: ${options.adId}`);
3010
+ return [];
3011
+ }
3012
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
3013
+ }
3014
+ const advertisement = await response.json();
3015
+ if (this._config?.debug) {
3016
+ console.log(`📊 Fetched specific ad with ID: ${options.adId}`, advertisement);
3017
+ }
3018
+ return [advertisement];
3019
+ }
3020
+ // 일반적인 광고 목록 요청
2951
3021
  const params = new URLSearchParams();
2952
3022
  params.append('adType', type);
2953
3023
  // 백엔드 API에서 실제 지원하는 필터링 옵션들만 추가
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adstage/web-sdk",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "AdStage Web SDK - Production-ready marketing platform SDK with React Provider support for seamless integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -26,6 +26,7 @@ export const API_PATHS = {
26
26
  /** 광고 관련 */
27
27
  advertisements: {
28
28
  list: '/advertisements/list',
29
+ detail: '/advertisements',
29
30
  events: '/advertisements/events'
30
31
  },
31
32
 
@@ -67,6 +68,7 @@ export class EndpointBuilder {
67
68
  */
68
69
  advertisements = {
69
70
  list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
71
+ detail: (adId: string) => `${this.baseUrl}${API_PATHS.advertisements.detail}/${adId}`,
70
72
  events: (adId: string, eventType: string) =>
71
73
  `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
72
74
  };
@@ -107,17 +107,37 @@ export class AdvertisementEventTracker {
107
107
  };
108
108
 
109
109
 
110
- await fetch(
111
- `${this.baseUrl}/advertisements/events/${adId}/${eventType}`,
112
- {
113
- method: 'POST',
114
- headers: ApiHeaders.createForEvents(this.apiKey, eventData),
115
- body: JSON.stringify(eventData),
116
- }
117
- );
110
+ const url = `${this.baseUrl}/advertisements/events/${adId}/${eventType}`;
111
+ const headers = ApiHeaders.createForEvents(this.apiKey, eventData);
112
+
113
+ if (this.debug) {
114
+ console.log(`🚀 Sending advertisement event: ${eventType} for ad ${adId}`, {
115
+ url,
116
+ headers,
117
+ eventData
118
+ });
119
+ }
120
+
121
+ const response = await fetch(url, {
122
+ method: 'POST',
123
+ headers,
124
+ body: JSON.stringify(eventData),
125
+ });
126
+
127
+ if (this.debug) {
128
+ console.log(`📡 API Response Status: ${response.status} ${response.statusText}`, {
129
+ url,
130
+ ok: response.ok
131
+ });
132
+ }
133
+
134
+ if (!response.ok) {
135
+ const errorText = await response.text();
136
+ throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
137
+ }
118
138
 
119
139
  if (this.debug) {
120
- console.log(`Tracked advertisement event: ${eventType} for ad ${adId}`, eventData);
140
+ console.log(`✅ Successfully tracked advertisement event: ${eventType} for ad ${adId}`);
121
141
  }
122
142
  } catch (error) {
123
143
  console.error('Failed to track advertisement event:', error);
@@ -7,7 +7,8 @@
7
7
  */
8
8
  export class ViewableEventTracker {
9
9
  private static viewableTracker = new Map<string, number>();
10
- private static readonly VIEWABLE_COOLDOWN = 300000; // 5분 쿨다운
10
+ private static readonly VIEWABLE_COOLDOWN_PRODUCTION = 300000; // 5분 쿨다운 (프로덕션)
11
+ private static readonly VIEWABLE_COOLDOWN_DEBUG = 30000; // 30초 쿨다운 (디버그)
11
12
 
12
13
  /**
13
14
  * 중복 viewable 이벤트 여부 확인
@@ -16,11 +17,16 @@ export class ViewableEventTracker {
16
17
  const key = `${adId}_${slotId}`;
17
18
  const now = Date.now();
18
19
 
20
+ // 디버그 모드에 따른 쿨다운 시간 결정
21
+ const cooldownTime = debug ?
22
+ ViewableEventTracker.VIEWABLE_COOLDOWN_DEBUG :
23
+ ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION;
24
+
19
25
  // 메모리 기반 중복 확인 (새로고침 시 초기화됨)
20
26
  const lastViewable = ViewableEventTracker.viewableTracker.get(key);
21
- if (lastViewable && (now - lastViewable) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
27
+ if (lastViewable && (now - lastViewable) < cooldownTime) {
22
28
  if (debug) {
23
- console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - lastViewable)) / 1000)}s remaining`);
29
+ console.log(`Duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - lastViewable)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
24
30
  }
25
31
  return true;
26
32
  }
@@ -30,9 +36,9 @@ export class ViewableEventTracker {
30
36
  const sessionViewable = sessionStorage.getItem(sessionKey);
31
37
  if (sessionViewable) {
32
38
  const sessionTime = parseInt(sessionViewable, 10);
33
- if (!isNaN(sessionTime) && (now - sessionTime) < ViewableEventTracker.VIEWABLE_COOLDOWN) {
39
+ if (!isNaN(sessionTime) && (now - sessionTime) < cooldownTime) {
34
40
  if (debug) {
35
- console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ViewableEventTracker.VIEWABLE_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
41
+ console.log(`Session-based duplicate viewable blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((cooldownTime - (now - sessionTime)) / 1000)}s remaining (${debug ? 'debug' : 'production'} mode)`);
36
42
  }
37
43
  // 메모리에도 기록하여 이후 요청 최적화
38
44
  ViewableEventTracker.viewableTracker.set(key, sessionTime);
@@ -47,6 +53,10 @@ export class ViewableEventTracker {
47
53
  // 오래된 세션 스토리지 데이터 정리 (선택적)
48
54
  ViewableEventTracker.cleanupOldViewables();
49
55
 
56
+ if (debug) {
57
+ console.log(`✅ New viewable recorded for ad ${adId} in slot ${slotId} (${debug ? 'debug' : 'production'} mode)`);
58
+ }
59
+
50
60
  return false;
51
61
  }
52
62
 
@@ -55,7 +65,8 @@ export class ViewableEventTracker {
55
65
  */
56
66
  private static cleanupOldViewables(): void {
57
67
  const now = Date.now();
58
- const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
68
+ // 프로덕션 쿨다운의 2배 시간이 지난 데이터 정리
69
+ const cleanupThreshold = ViewableEventTracker.VIEWABLE_COOLDOWN_PRODUCTION * 2;
59
70
 
60
71
  // 세션 스토리지 정리
61
72
  for (let i = 0; i < sessionStorage.length; i++) {
@@ -81,9 +92,28 @@ export class ViewableEventTracker {
81
92
  }
82
93
 
83
94
  /**
84
- * 모든 추적 데이터 정리
95
+ * 모든 추적 데이터 정리 (디버그용)
85
96
  */
86
97
  static clear(): void {
87
98
  ViewableEventTracker.viewableTracker.clear();
99
+ // 세션 스토리지에서도 adstage_viewable_ 키들 제거
100
+ const keysToRemove: string[] = [];
101
+ for (let i = 0; i < sessionStorage.length; i++) {
102
+ const key = sessionStorage.key(i);
103
+ if (key && key.startsWith('adstage_viewable_')) {
104
+ keysToRemove.push(key);
105
+ }
106
+ }
107
+ keysToRemove.forEach(key => sessionStorage.removeItem(key));
108
+ }
109
+
110
+ /**
111
+ * 특정 광고의 viewable 추적 초기화 (디버그용)
112
+ */
113
+ static clearAdViewable(adId: string, slotId: string): void {
114
+ const key = `${adId}_${slotId}`;
115
+ ViewableEventTracker.viewableTracker.delete(key);
116
+ const sessionKey = `adstage_viewable_${key}`;
117
+ sessionStorage.removeItem(sessionKey);
88
118
  }
89
119
  }
@@ -345,8 +345,6 @@ export class AdRenderer {
345
345
  case AdType.TEXT: {
346
346
  const textDiv = document.createElement('div');
347
347
  textDiv.innerHTML = `
348
- <h3>${ad.title}</h3>
349
- ${ad.description ? `<p>${ad.description}</p>` : ''}
350
348
  ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
351
349
  `;
352
350
  adElement.appendChild(textDiv);
@@ -480,7 +478,7 @@ export class AdRenderer {
480
478
  case AdType.BANNER:
481
479
  return '250px'; // 일반 배너
482
480
  case AdType.TEXT:
483
- return '120px'; // 텍스트는 좀 더 작게
481
+ return '60px'; // 텍스트는 좀 더 작게
484
482
  case AdType.VIDEO:
485
483
  return '360px'; // 비디오는 16:9 비율 고려
486
484
  case AdType.NATIVE:
@@ -26,6 +26,8 @@ export interface AdOptions {
26
26
  autoplay?: boolean;
27
27
  muted?: boolean;
28
28
  onClick?: (adData: any) => void;
29
+ // 특정 광고 ID 지정
30
+ adId?: string;
29
31
  // 광고 필터링 옵션 (백엔드 API에서 실제 지원하는 것들만)
30
32
  language?: 'ko' | 'en' | 'ja' | 'zh';
31
33
  deviceType?: 'MOBILE' | 'DESKTOP';
@@ -91,6 +93,8 @@ export class AdsModule implements BaseModule {
91
93
  autoSlide: options?.autoSlide || false,
92
94
  slideInterval: options?.slideInterval || 5000,
93
95
  onClick: options?.onClick,
96
+ // 특정 광고 ID
97
+ adId: options?.adId,
94
98
  // 필터링 옵션들 전달
95
99
  language: options?.language,
96
100
  deviceType: options?.deviceType,
@@ -110,6 +114,8 @@ export class AdsModule implements BaseModule {
110
114
  maxLines: options?.maxLines || 3,
111
115
  style: options?.style || 'default',
112
116
  onClick: options?.onClick,
117
+ // 특정 광고 ID
118
+ adId: options?.adId,
113
119
  // 필터링 옵션들 전달
114
120
  language: options?.language,
115
121
  deviceType: options?.deviceType,
@@ -351,8 +357,8 @@ export class AdsModule implements BaseModule {
351
357
  const fraudDetector = new BasicFraudDetector();
352
358
 
353
359
  // viewability 추적
354
- const tracker = new ViewabilityTracker(element, slot.adType, (metrics) => {
355
- this.handleViewableEvent(ad, slot, metrics, fraudDetector);
360
+ const tracker = new ViewabilityTracker(element, slot.adType, async (metrics) => {
361
+ await this.handleViewableEvent(ad, slot, metrics, fraudDetector);
356
362
  });
357
363
 
358
364
  // 정리를 위해 저장
@@ -426,7 +432,33 @@ export class AdsModule implements BaseModule {
426
432
  throw new Error('API key not configured');
427
433
  }
428
434
 
429
- // GET 요청용 query parameters 구성
435
+ // 특정 광고 ID가 지정된 경우, 해당 광고만 요청
436
+ if (options.adId) {
437
+ const url = `${endpoints.advertisements.detail(options.adId)}`;
438
+
439
+ const response = await fetch(url, {
440
+ method: 'GET',
441
+ headers: ApiHeaders.create(this._config.apiKey)
442
+ });
443
+
444
+ if (!response.ok) {
445
+ if (response.status === 404) {
446
+ console.warn(`🚫 Advertisement not found with ID: ${options.adId}`);
447
+ return [];
448
+ }
449
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
450
+ }
451
+
452
+ const advertisement = await response.json();
453
+
454
+ if (this._config?.debug) {
455
+ console.log(`📊 Fetched specific ad with ID: ${options.adId}`, advertisement);
456
+ }
457
+
458
+ return [advertisement];
459
+ }
460
+
461
+ // 일반적인 광고 목록 요청
430
462
  const params = new URLSearchParams();
431
463
  params.append('adType', type);
432
464
 
@@ -16,7 +16,7 @@ export class TextAdRenderer extends BaseAdRenderer {
16
16
  // 기본 컨테이너 스타일
17
17
  const containerStyles: Record<string, string> = {
18
18
  ...this.getBaseContainerStyles(slot),
19
- padding: '20px',
19
+ padding: '4px',
20
20
  background: 'transparent',
21
21
  display: 'flex',
22
22
  'align-items': 'center',
@@ -52,7 +52,7 @@ export class TextAdRenderer extends BaseAdRenderer {
52
52
  ad.textContent,
53
53
  'div',
54
54
  {
55
- 'font-size': '16px',
55
+ 'font-size': '14px',
56
56
  'font-weight': '500',
57
57
  color: '#212529',
58
58
  width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함