@adstage/web-sdk 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +178 -35
  2. package/dist/index.cjs.js +810 -481
  3. package/dist/index.d.ts +286 -97
  4. package/dist/index.esm.js +794 -457
  5. package/dist/index.standalone.js +794 -457
  6. package/package.json +2 -2
  7. package/src/constants/endpoints.ts +93 -0
  8. package/src/core/AdStage.ts +128 -0
  9. package/src/index.ts +14 -413
  10. package/src/managers/{slider-manager.ts → carousel-slider-manager.ts} +9 -8
  11. package/src/managers/event-tracker.ts +2 -4
  12. package/src/managers/{fade-slider-manager.ts → text-transition-manager.ts} +7 -7
  13. package/src/modules/ads/AdsModule.ts +525 -0
  14. package/src/modules/config/ConfigModule.ts +124 -0
  15. package/src/modules/deeplinks/DeeplinksModule.ts +0 -0
  16. package/src/modules/events/EventsModule.ts +106 -0
  17. package/src/types/config.ts +74 -3
  18. package/src/types/index.ts +2 -1
  19. package/src/utils/api-headers.ts +52 -0
  20. package/src/utils/dom-utils.ts +93 -0
  21. package/examples/README.md +0 -33
  22. package/examples/banner-ads.html +0 -512
  23. package/examples/index.html +0 -338
  24. package/examples/native-ads.html +0 -634
  25. package/examples/react-app/README.md +0 -70
  26. package/examples/react-app/index.html +0 -13
  27. package/examples/react-app/package-lock.json +0 -3042
  28. package/examples/react-app/package.json +0 -26
  29. package/examples/react-app/pnpm-lock.yaml +0 -1857
  30. package/examples/react-app/public/index.standalone.js +0 -2331
  31. package/examples/react-app/src/App.tsx +0 -226
  32. package/examples/react-app/src/index.css +0 -37
  33. package/examples/react-app/src/main.tsx +0 -10
  34. package/examples/react-app/tsconfig.json +0 -25
  35. package/examples/react-app/tsconfig.node.json +0 -10
  36. package/examples/react-app/vite.config.ts +0 -15
  37. package/examples/react-nextjs/app/globals.css +0 -200
  38. package/examples/react-nextjs/app/layout.tsx +0 -27
  39. package/examples/react-nextjs/app/page.tsx +0 -258
  40. package/examples/react-nextjs/next.config.js +0 -9
  41. package/examples/react-nextjs/package.json +0 -22
  42. package/examples/react-nextjs/pnpm-lock.yaml +0 -343
  43. package/examples/react-nextjs/tsconfig.json +0 -34
  44. package/examples/text-ads.html +0 -597
  45. package/examples/video-ads.html +0 -739
  46. package/src/react/components/AdErrorBoundary.tsx +0 -75
  47. package/src/react/components/AdSlot.tsx +0 -144
  48. package/src/react/components/BannerAd.tsx +0 -24
  49. package/src/react/components/InterstitialAd.tsx +0 -24
  50. package/src/react/components/NativeAd.tsx +0 -24
  51. package/src/react/components/TextAd.tsx +0 -24
  52. package/src/react/components/VideoAd.tsx +0 -24
  53. package/src/react/components/index.ts +0 -8
  54. package/src/react/hooks/index.ts +0 -4
  55. package/src/react/hooks/useAdSlot.ts +0 -83
  56. package/src/react/hooks/useAdStage.ts +0 -14
  57. package/src/react/hooks/useAdTracking.ts +0 -61
  58. package/src/react/index.ts +0 -4
  59. package/src/react/providers/AdStageProvider.tsx +0 -86
  60. package/src/react/providers/index.ts +0 -2
  61. package/src/utils/sdk-standalone.ts +0 -155
package/dist/index.cjs.js CHANGED
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  // 광고 타입 정의
6
- exports.AdType = void 0;
4
+ var AdType;
7
5
  (function (AdType) {
8
6
  AdType["BANNER"] = "BANNER";
9
7
  AdType["POPUP"] = "POPUP";
@@ -11,7 +9,7 @@ exports.AdType = void 0;
11
9
  AdType["NATIVE"] = "NATIVE";
12
10
  AdType["VIDEO"] = "VIDEO";
13
11
  AdType["TEXT"] = "TEXT";
14
- })(exports.AdType || (exports.AdType = {}));
12
+ })(AdType || (AdType = {}));
15
13
  // 플랫폼 정의
16
14
  var Platform;
17
15
  (function (Platform) {
@@ -19,7 +17,7 @@ var Platform;
19
17
  Platform["MOBILE"] = "MOBILE";
20
18
  })(Platform || (Platform = {}));
21
19
  // 광고 이벤트 타입
22
- exports.AdEventType = void 0;
20
+ var AdEventType;
23
21
  (function (AdEventType) {
24
22
  AdEventType["IMPRESSION"] = "IMPRESSION";
25
23
  AdEventType["CLICK"] = "CLICK";
@@ -30,7 +28,7 @@ exports.AdEventType = void 0;
30
28
  AdEventType["VIDEO_START"] = "VIDEO_START";
31
29
  AdEventType["VIDEO_COMPLETE"] = "VIDEO_COMPLETE";
32
30
  AdEventType["ERROR"] = "ERROR";
33
- })(exports.AdEventType || (exports.AdEventType = {}));
31
+ })(AdEventType || (AdEventType = {}));
34
32
  // 디바이스 타입
35
33
  var DeviceType;
36
34
  (function (DeviceType) {
@@ -257,6 +255,72 @@ class DOMUtils {
257
255
  scrollLeft: this.getWindowProperty('pageXOffset', 0),
258
256
  };
259
257
  }
258
+ /**
259
+ * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
260
+ */
261
+ static async waitForElement(id, options = {}) {
262
+ const { timeout = 3000, retryInterval = 100, debug = false } = options;
263
+ if (!this.canUseDOM()) {
264
+ throw new Error('DOM을 사용할 수 없는 환경입니다.');
265
+ }
266
+ // 즉시 찾을 수 있으면 바로 반환
267
+ const immediateElement = document.getElementById(id);
268
+ if (immediateElement) {
269
+ if (debug) {
270
+ console.log(`✅ 컨테이너 즉시 발견: ${id}`);
271
+ }
272
+ return immediateElement;
273
+ }
274
+ if (debug) {
275
+ console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
276
+ }
277
+ return new Promise((resolve, reject) => {
278
+ let attempts = 0;
279
+ const maxAttempts = Math.ceil(timeout / retryInterval);
280
+ const checkElement = () => {
281
+ attempts++;
282
+ const element = document.getElementById(id);
283
+ if (element) {
284
+ if (debug) {
285
+ console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
286
+ }
287
+ resolve(element);
288
+ return;
289
+ }
290
+ if (attempts >= maxAttempts) {
291
+ const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
292
+
293
+ 다음을 확인해보세요:
294
+ 1. HTML에 id="${id}" 요소가 있는지 확인
295
+ 2. React 등에서 컴포넌트가 렌더링된 후 SDK 호출
296
+ 3. 철자가 정확한지 확인
297
+ 4. 중복된 ID가 없는지 확인
298
+
299
+ 대기 시간: ${timeout}ms (${attempts}번 시도)`;
300
+ if (debug) {
301
+ console.error(errorMessage);
302
+ }
303
+ reject(new Error(errorMessage));
304
+ return;
305
+ }
306
+ if (debug && attempts % 10 === 0) {
307
+ console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
308
+ }
309
+ // Exponential backoff: 처음엔 빠르게, 나중엔 느리게
310
+ const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
311
+ setTimeout(checkElement, nextInterval);
312
+ };
313
+ // 첫 번째 체크는 즉시 실행
314
+ setTimeout(checkElement, retryInterval);
315
+ });
316
+ }
317
+ /**
318
+ * 여러 DOM 요소를 동시에 기다리기
319
+ */
320
+ static async waitForElements(ids, options = {}) {
321
+ const promises = ids.map(id => this.waitForElement(id, options));
322
+ return Promise.all(promises);
323
+ }
260
324
  }
261
325
 
262
326
  /**
@@ -699,23 +763,24 @@ _a = AdRendererFactory;
699
763
  AdRendererFactory.renderers = new Map();
700
764
  (() => {
701
765
  // 렌더러 등록
702
- _a.renderers.set(exports.AdType.BANNER, BannerAdRenderer);
703
- _a.renderers.set(exports.AdType.TEXT, TextAdRenderer);
704
- _a.renderers.set(exports.AdType.NATIVE, NativeAdRenderer);
705
- _a.renderers.set(exports.AdType.VIDEO, VideoAdRenderer);
706
- _a.renderers.set(exports.AdType.INTERSTITIAL, InterstitialAdRenderer);
707
- _a.renderers.set(exports.AdType.POPUP, InterstitialAdRenderer); // POPUP은 INTERSTITIAL과 동일
766
+ _a.renderers.set(AdType.BANNER, BannerAdRenderer);
767
+ _a.renderers.set(AdType.TEXT, TextAdRenderer);
768
+ _a.renderers.set(AdType.NATIVE, NativeAdRenderer);
769
+ _a.renderers.set(AdType.VIDEO, VideoAdRenderer);
770
+ _a.renderers.set(AdType.INTERSTITIAL, InterstitialAdRenderer);
771
+ _a.renderers.set(AdType.POPUP, InterstitialAdRenderer); // POPUP은 INTERSTITIAL과 동일
708
772
  })();
709
773
 
710
774
  /**
711
- * 슬라이더 관리 클래스
712
- * - 다중 광고 슬라이더 생성 관리
713
- * - 무한 루프 슬라이더 지원
775
+ * 캐러셀 슬라이더 관리 클래스
776
+ * - 배너/비디오 광고용 가로 슬라이드 (횡 스크롤)
777
+ * - 무한 루프 캐러셀 지원
714
778
  * - 터치 제스처 및 자동 슬라이드 기능
779
+ * - 도트 인디케이터 포함
715
780
  */
716
- class SliderManager {
781
+ class CarouselSliderManager {
717
782
  /**
718
- * 슬라이더 컨테이너 생성
783
+ * Create carousel slider container with dot indicators and navigation
719
784
  */
720
785
  static createSliderContainer(slot, advertisements, options, trackEventCallback) {
721
786
  const sliderWrapper = document.createElement('div');
@@ -843,9 +908,9 @@ class SliderManager {
843
908
  slideContainer.appendChild(slideElement);
844
909
  });
845
910
  // 텍스트 광고인지 확인 (모든 광고가 텍스트 타입인 경우)
846
- const isAllTextAds = advertisements.every(ad => ad.adType === exports.AdType.TEXT);
911
+ const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
847
912
  // 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
848
- const dotContainer = isAllTextAds ? null : SliderManager.createMinimalDotIndicator(advertisements.length);
913
+ const dotContainer = isAllTextAds ? null : this.createMinimalDotIndicator(advertisements.length);
849
914
  // 슬라이더 상태 관리
850
915
  let currentSlide = 0;
851
916
  const totalSlides = advertisements.length;
@@ -884,7 +949,7 @@ class SliderManager {
884
949
  }
885
950
  // 현재 슬라이드의 광고에 대해 노출 이벤트 추적
886
951
  if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
887
- trackEventCallback(advertisements[actualIndex]._id, slot.id, exports.AdEventType.IMPRESSION);
952
+ trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.IMPRESSION);
888
953
  }
889
954
  };
890
955
  // 무한 루프 처리 함수
@@ -921,7 +986,7 @@ class SliderManager {
921
986
  }, autoSlideInterval);
922
987
  });
923
988
  // 터치 제스처 지원 수정 (무한 루프 지원)
924
- SliderManager.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
989
+ this.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
925
990
  // 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
926
991
  sliderWrapper.appendChild(slideContainer);
927
992
  if (dotContainer) {
@@ -1045,16 +1110,16 @@ class SliderManager {
1045
1110
  }
1046
1111
 
1047
1112
  /**
1048
- * 페이드 슬라이더 관리 클래스
1049
- * - 텍스트 광고 전용 페이드 인/아웃 슬라이더
1050
- * - 상하 교차 효과 (위에서 아래로, 아래서 위로)
1113
+ * 텍스트 전환 효과 관리 클래스
1114
+ * - 텍스트 광고 전용 페이드 인/아웃 + 상하 움직임 효과
1115
+ * - 부드러운 전환 애니메이션 (vertical transition)
1051
1116
  * - 무한 루프 지원
1052
1117
  */
1053
- class FadeSliderManager {
1118
+ class TextTransitionManager {
1054
1119
  /**
1055
- * 페이드 슬라이더 컨테이너 생성
1120
+ * 텍스트 전환 슬라이더 컨테이너 생성
1056
1121
  */
1057
- static createFadeSliderContainer(slot, advertisements, options, trackEventCallback) {
1122
+ static createTextTransitionContainer(slot, advertisements, options, trackEventCallback) {
1058
1123
  const sliderWrapper = document.createElement('div');
1059
1124
  sliderWrapper.className = 'adstage-fade-slider-wrapper';
1060
1125
  // 래퍼 스타일 설정
@@ -1204,7 +1269,7 @@ class FadeSliderManager {
1204
1269
  currentSlide = index;
1205
1270
  // 현재 슬라이드의 광고에 대해 노출 이벤트 추적
1206
1271
  if (currentSlide > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
1207
- trackEventCallback(advertisements[currentSlide]._id, slot.id, exports.AdEventType.IMPRESSION);
1272
+ trackEventCallback(advertisements[currentSlide]._id, slot.id, AdEventType.IMPRESSION);
1208
1273
  }
1209
1274
  };
1210
1275
  // 자동 슬라이드
@@ -1223,7 +1288,7 @@ class FadeSliderManager {
1223
1288
  }, autoSlideInterval);
1224
1289
  });
1225
1290
  // 터치 제스처 지원
1226
- FadeSliderManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
1291
+ TextTransitionManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
1227
1292
  // 요소들 조립
1228
1293
  sliderWrapper.appendChild(slideContainer);
1229
1294
  return sliderWrapper;
@@ -1443,6 +1508,46 @@ class DeviceInfoCollector {
1443
1508
  }
1444
1509
  }
1445
1510
 
1511
+ /**
1512
+ * AdStage SDK - API 헤더 유틸리티
1513
+ * 공통 헤더 생성 로직
1514
+ */
1515
+ class ApiHeaders {
1516
+ /**
1517
+ * 표준 API 헤더 생성
1518
+ */
1519
+ static create(apiKey, options) {
1520
+ if (!apiKey) {
1521
+ throw new Error('API key is required');
1522
+ }
1523
+ const headers = {
1524
+ 'x-api-key': apiKey,
1525
+ 'Content-Type': options?.contentType || 'application/json'
1526
+ };
1527
+ // User-Agent는 이벤트 추적에서 실제로 사용됨
1528
+ if (typeof navigator !== 'undefined') {
1529
+ headers['User-Agent'] = options?.userAgent || navigator.userAgent;
1530
+ }
1531
+ // X-Current-URL은 현재 서버에서 사용하지 않으므로 제거
1532
+ // 필요시 이벤트 데이터 body에 포함
1533
+ return headers;
1534
+ }
1535
+ /**
1536
+ * 이벤트 추적용 헤더 생성
1537
+ * User-Agent는 서버에서 실제로 사용됨
1538
+ */
1539
+ static createForEvents(apiKey, eventData) {
1540
+ const baseHeaders = ApiHeaders.create(apiKey);
1541
+ // User-Agent 오버라이드 (서버에서 실제 사용)
1542
+ if (eventData?.userAgent) {
1543
+ baseHeaders['User-Agent'] = eventData.userAgent;
1544
+ }
1545
+ // 다른 정보들은 HTTP 헤더가 아닌 이벤트 데이터 body에 포함하는 것이 적절
1546
+ // (currentUrl, referrer 등은 POST body로 전송)
1547
+ return baseHeaders;
1548
+ }
1549
+ }
1550
+
1446
1551
  /**
1447
1552
  * 이벤트 추적 관리 클래스
1448
1553
  * - 광고 이벤트 추적 및 전송
@@ -1462,7 +1567,7 @@ class EventTracker {
1462
1567
  async trackEvent(adId, slotId, eventType) {
1463
1568
  try {
1464
1569
  // 노출 이벤트의 경우 중복 확인
1465
- if (eventType === exports.AdEventType.IMPRESSION) {
1570
+ if (eventType === AdEventType.IMPRESSION) {
1466
1571
  if (ImpressionTracker.isDuplicateImpression(adId, slotId, this.debug)) {
1467
1572
  return; // 중복 노출이므로 추적하지 않음
1468
1573
  }
@@ -1518,10 +1623,7 @@ class EventTracker {
1518
1623
  };
1519
1624
  await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
1520
1625
  method: 'POST',
1521
- headers: {
1522
- 'x-api-key': this.apiKey,
1523
- 'Content-Type': 'application/json',
1524
- },
1626
+ headers: ApiHeaders.createForEvents(this.apiKey, eventData),
1525
1627
  body: JSON.stringify(eventData),
1526
1628
  });
1527
1629
  if (this.debug) {
@@ -1549,566 +1651,793 @@ class EventTracker {
1549
1651
  }
1550
1652
 
1551
1653
  /**
1552
- * SDK 유틸리티 함수 모음
1553
- * - DOM 요소 속성 처리
1554
- * - 자동 슬롯 검색
1555
- * - 기타 헬퍼 함수들
1654
+ * AdStage SDK 엔드포인트 상수 관리
1655
+ * 모든 API URL을 중앙에서 관리
1556
1656
  */
1557
- class SDKUtils {
1558
- /**
1559
- * HTML 요소에서 속성값 가져오기 (data- 프리픽스 선택적)
1560
- */
1561
- static getElementAttribute(element, attrName) {
1562
- // 1. data- 프리픽스가 있는 경우 우선
1563
- const dataAttr = element.dataset[attrName];
1564
- if (dataAttr)
1565
- return dataAttr;
1566
- // 2. 직접 속성 확인
1567
- const directAttr = element.getAttribute(attrName);
1568
- if (directAttr)
1569
- return directAttr;
1570
- // 3. 케밥 케이스로 확인 (ad-type -> adType)
1571
- const kebabAttr = element.getAttribute(attrName.replace(/[A-Z]/g, '-$&').toLowerCase());
1572
- if (kebabAttr)
1573
- return kebabAttr;
1574
- return undefined;
1657
+ /**
1658
+ * 환경별 API 엔드포인트 (읽기 전용)
1659
+ */
1660
+ const API_ENDPOINTS = {
1661
+ /** 프로덕션 환경 */
1662
+ production: 'https://api.adstage.io',
1663
+ /** 베타 환경 (기본값) */
1664
+ beta: 'https://beta-api.adstage.app'
1665
+ };
1666
+ /**
1667
+ * API 경로 상수
1668
+ */
1669
+ const API_PATHS = {
1670
+ /** 광고 관련 */
1671
+ advertisements: {
1672
+ list: '/advertisements/list',
1673
+ events: '/advertisements/events'
1674
+ },
1675
+ /** 이벤트 관련 */
1676
+ events: {
1677
+ track: '/events/track',
1678
+ batch: '/events/batch'
1679
+ }
1680
+ };
1681
+ /**
1682
+ * 완전한 API URL 생성 헬퍼
1683
+ */
1684
+ class EndpointBuilder {
1685
+ constructor(baseUrl) {
1686
+ /**
1687
+ * 광고 엔드포인트
1688
+ */
1689
+ this.advertisements = {
1690
+ list: () => `${this.baseUrl}${API_PATHS.advertisements.list}`,
1691
+ events: (adId, eventType) => `${this.baseUrl}${API_PATHS.advertisements.events}/${adId}/${eventType}`
1692
+ };
1693
+ /**
1694
+ * 이벤트 엔드포인트
1695
+ */
1696
+ this.events = {
1697
+ track: () => `${this.baseUrl}${API_PATHS.events.track}`,
1698
+ batch: () => `${this.baseUrl}${API_PATHS.events.batch}`
1699
+ };
1700
+ // 기본값은 베타 환경 사용
1701
+ this.baseUrl = baseUrl || API_ENDPOINTS.beta;
1575
1702
  }
1576
1703
  /**
1577
- * DOM에서 자동 슬롯 요소 찾기 (SSR 안전)
1704
+ * 기본 URL 변경
1578
1705
  */
1579
- static findAutoSlotElements() {
1580
- if (typeof document === 'undefined')
1581
- return [];
1582
- const elements = document.querySelectorAll('[data-adstage], [adstage]');
1583
- return Array.from(elements);
1584
- }
1585
- /**
1586
- * 요소에서 슬롯 정보 추출
1587
- */
1588
- static extractSlotInfo(element) {
1589
- // 슬롯 ID 가져오기
1590
- const slotId = SDKUtils.getElementAttribute(element, 'adstage');
1591
- // 광고 타입 가져오기
1592
- const adType = SDKUtils.getElementAttribute(element, 'adType') ||
1593
- SDKUtils.getElementAttribute(element, 'ad-type') ||
1594
- 'BANNER';
1595
- // 크기 정보 가져오기 (다양한 형태 지원)
1596
- const width = SDKUtils.getElementAttribute(element, 'width');
1597
- const height = SDKUtils.getElementAttribute(element, 'height');
1598
- // 기타 옵션들
1599
- const language = SDKUtils.getElementAttribute(element, 'language');
1600
- const deviceType = SDKUtils.getElementAttribute(element, 'deviceType') ||
1601
- SDKUtils.getElementAttribute(element, 'device-type');
1602
- const country = SDKUtils.getElementAttribute(element, 'country');
1603
- const sliderEffect = SDKUtils.getElementAttribute(element, 'sliderEffect') ||
1604
- SDKUtils.getElementAttribute(element, 'slider-effect');
1605
- return {
1606
- slotId: slotId || null,
1607
- adType,
1608
- width,
1609
- height,
1610
- language,
1611
- deviceType,
1612
- country,
1613
- sliderEffect,
1614
- };
1706
+ setBaseUrl(url) {
1707
+ this.baseUrl = url;
1708
+ console.log('🔄 API endpoint changed:', url);
1615
1709
  }
1616
1710
  /**
1617
- * AdType enum 값으로 변환
1711
+ * 기본 URL 반환
1618
1712
  */
1619
- static parseAdType(adTypeStr, AdType) {
1620
- return AdType[adTypeStr] || AdType.BANNER;
1713
+ getBaseUrl() {
1714
+ return this.baseUrl;
1621
1715
  }
1622
1716
  /**
1623
- * 브라우저 환경 체크
1717
+ * 커스텀 경로 생성
1624
1718
  */
1625
- static isBrowser() {
1626
- return typeof window !== 'undefined';
1719
+ custom(path) {
1720
+ return `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
1721
+ }
1722
+ }
1723
+ /**
1724
+ * 전역 엔드포인트 빌더 인스턴스 (기본: 베타 환경)
1725
+ */
1726
+ const endpoints = new EndpointBuilder();
1727
+
1728
+ /**
1729
+ * AdStage SDK - Ads 모듈
1730
+ * 광고 관리 및 렌더링 기능
1731
+ */
1732
+ class AdsModule {
1733
+ constructor() {
1734
+ this._isReady = false;
1735
+ this._config = null;
1736
+ this.slots = new Map();
1737
+ this.eventTracker = null;
1627
1738
  }
1628
1739
  /**
1629
- * SSR 환경 체크
1740
+ * Ads 모듈 초기화 (동기)
1630
1741
  */
1631
- static isSSR() {
1632
- return typeof window === 'undefined';
1742
+ init(config) {
1743
+ this._config = config;
1744
+ // EventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
1745
+ this.eventTracker = new EventTracker(endpoints.getBaseUrl(), config.apiKey, config.debug || false, this.slots);
1746
+ this._isReady = true;
1747
+ if (config.debug) {
1748
+ console.log('🎯 Ads module initialized (sync mode)');
1749
+ }
1633
1750
  }
1634
1751
  /**
1635
- * DOM 사용 가능 체크
1752
+ * 모듈 준비 상태 확인
1636
1753
  */
1637
- static canUseDOM() {
1638
- return !this.isSSR() && typeof document !== 'undefined';
1754
+ isReady() {
1755
+ return this._isReady;
1639
1756
  }
1640
1757
  /**
1641
- * DOM 로드 완료 체크 (SSR 안전)
1758
+ * 모듈 설정 반환
1642
1759
  */
1643
- static isDOMReady() {
1644
- if (!this.canUseDOM())
1645
- return false;
1646
- return document.readyState !== 'loading';
1760
+ getConfig() {
1761
+ return this._config;
1647
1762
  }
1648
1763
  /**
1649
- * DOM 로드 완료 대기 (SSR 안전)
1764
+ * 배너 광고 생성 (동기)
1650
1765
  */
1651
- static waitForDOM() {
1652
- return new Promise((resolve) => {
1653
- if (!this.canUseDOM()) {
1654
- resolve(); // SSR 환경에서는 즉시 resolve
1655
- return;
1656
- }
1657
- if (SDKUtils.isDOMReady()) {
1658
- resolve();
1659
- }
1660
- else {
1661
- document.addEventListener('DOMContentLoaded', () => resolve());
1662
- }
1663
- });
1766
+ banner(containerId, options) {
1767
+ this.ensureReady();
1768
+ const adstageOptions = {
1769
+ width: options?.width || '100%',
1770
+ height: options?.height || 250,
1771
+ autoSlide: options?.autoSlide || false,
1772
+ slideInterval: options?.slideInterval || 5000,
1773
+ onClick: options?.onClick
1774
+ };
1775
+ return this.createAd(containerId, AdType.BANNER, adstageOptions);
1664
1776
  }
1665
- }
1666
-
1667
- /**
1668
- * AdStage SDK Standalone API
1669
- * 간단하고 직관적인 사용을 위한 통합 API
1670
- */
1671
- let globalSDKInstance = null;
1672
- let isInitializing = false;
1673
- /**
1674
- * SDK 초기화 (한 번만 호출)
1675
- */
1676
- async function initAdStage(config) {
1677
- if (globalSDKInstance) {
1678
- console.warn('AdStage SDK가 이미 초기화되었습니다.');
1679
- return;
1680
- }
1681
- if (isInitializing) {
1682
- console.warn('AdStage SDK 초기화가 진행 중입니다.');
1683
- return;
1684
- }
1685
- isInitializing = true;
1686
- try {
1687
- // 동적 import로 circular dependency 방지
1688
- const { AdStageSDK } = await Promise.resolve().then(function () { return index; });
1689
- globalSDKInstance = AdStageSDK.init({
1690
- apiKey: config.apiKey,
1691
- debug: config.debug || false
1692
- });
1693
- console.log('✅ AdStage SDK 초기화 완료');
1777
+ /**
1778
+ * 텍스트 광고 생성 (동기)
1779
+ */
1780
+ text(containerId, options) {
1781
+ this.ensureReady();
1782
+ const adstageOptions = {
1783
+ maxLines: options?.maxLines || 3,
1784
+ style: options?.style || 'default',
1785
+ onClick: options?.onClick
1786
+ };
1787
+ return this.createAd(containerId, AdType.TEXT, adstageOptions);
1694
1788
  }
1695
- catch (error) {
1696
- console.error('❌ AdStage SDK 초기화 실패:', error);
1697
- throw error;
1789
+ /**
1790
+ * 비디오 광고 생성 (동기)
1791
+ */
1792
+ video(containerId, options) {
1793
+ this.ensureReady();
1794
+ const adstageOptions = {
1795
+ width: options?.width || 640,
1796
+ height: options?.height || 360,
1797
+ autoplay: options?.autoplay || false,
1798
+ muted: options?.muted || true,
1799
+ onClick: options?.onClick
1800
+ };
1801
+ return this.createAd(containerId, AdType.VIDEO, adstageOptions);
1698
1802
  }
1699
- finally {
1700
- isInitializing = false;
1803
+ /**
1804
+ * 네이티브 광고 생성 (동기)
1805
+ */
1806
+ native(containerId, options) {
1807
+ this.ensureReady();
1808
+ return this.createAd(containerId, AdType.NATIVE, options || {});
1701
1809
  }
1702
- }
1703
- /**
1704
- * 배너 광고 생성 (가장 간단한 API)
1705
- */
1706
- async function createBanner(containerId, options) {
1707
- if (!globalSDKInstance) {
1708
- throw new Error('AdStage SDK가 초기화되지 않았습니다. initAdStage()를 먼저 호출하세요.');
1709
- }
1710
- const slotId = `banner-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1711
- return globalSDKInstance.createSlot(slotId, containerId, 'BANNER', {
1712
- width: options?.width || '100%',
1713
- height: options?.height || 120,
1714
- autoSlideInterval: options?.autoSlide ? (options.slideInterval || 5) : 0,
1715
- sliderEffect: 'fade'
1716
- });
1717
- }
1718
- /**
1719
- * 텍스트 광고 생성
1720
- */
1721
- async function createTextAd(containerId, options) {
1722
- if (!globalSDKInstance) {
1723
- throw new Error('AdStage SDK가 초기화되지 않았습니다.');
1724
- }
1725
- const slotId = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1726
- return globalSDKInstance.createSlot(slotId, containerId, 'TEXT', {
1727
- maxLines: options?.maxLines || 3,
1728
- style: options?.style || 'card'
1729
- });
1730
- }
1731
- /**
1732
- * 비디오 광고 생성
1733
- */
1734
- async function createVideoAd(containerId, options) {
1735
- if (!globalSDKInstance) {
1736
- throw new Error('AdStage SDK가 초기화되지 않았습니다.');
1737
- }
1738
- const slotId = `video-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1739
- return globalSDKInstance.createSlot(slotId, containerId, 'VIDEO', {
1740
- width: options?.width || '100%',
1741
- height: options?.height || 300,
1742
- autoplay: options?.autoplay || false,
1743
- muted: options?.muted || true
1744
- });
1745
- }
1746
- /**
1747
- * SDK 상태 확인
1748
- */
1749
- function isAdStageReady() {
1750
- return globalSDKInstance !== null && !isInitializing;
1751
- }
1752
- /**
1753
- * SDK 인스턴스 가져오기 (고급 사용자용)
1754
- */
1755
- function getAdStageInstance() {
1756
- if (!globalSDKInstance) {
1757
- console.warn('AdStage SDK가 초기화되지 않았습니다.');
1758
- return null;
1810
+ /**
1811
+ * 전면 광고 생성 (동기)
1812
+ */
1813
+ interstitial(containerId, options) {
1814
+ this.ensureReady();
1815
+ return this.createAd(containerId, AdType.INTERSTITIAL, options || {});
1759
1816
  }
1760
- return globalSDKInstance;
1761
- }
1762
- /**
1763
- * SDK 초기화 해제 (필요시)
1764
- */
1765
- function destroyAdStage() {
1766
- if (globalSDKInstance) {
1767
- try {
1768
- // SDK cleanup logic here
1769
- globalSDKInstance = null;
1770
- console.log('🧹 AdStage SDK 정리 완료');
1817
+ /**
1818
+ * 광고 새로고침
1819
+ */
1820
+ refresh(slotId) {
1821
+ this.ensureReady();
1822
+ const slot = this.slots.get(slotId);
1823
+ if (!slot) {
1824
+ throw new Error(`Ad slot not found: ${slotId}`);
1771
1825
  }
1772
- catch (error) {
1773
- console.error('SDK 정리 중 오류:', error);
1826
+ // 광고 새로고침 로직
1827
+ this.refreshAdSlot(slot);
1828
+ if (this._config?.debug) {
1829
+ console.log(`🔄 Ad slot refreshed: ${slotId}`);
1774
1830
  }
1775
1831
  }
1776
- }
1777
-
1778
- /**
1779
- * AdStage SDK 메인 클래스
1780
- * - 간단한 API Key 기반 초기화
1781
- * - 광고 슬롯 자동 관리
1782
- * - 이벤트 자동 추적
1783
- */
1784
- class AdStageSDK {
1785
- constructor(config) {
1786
- this.baseUrl = 'https://beta-api.adstage.app';
1787
- this.slots = new Map();
1788
- this.initialized = false;
1789
- this.config = {
1790
- debug: false,
1791
- ...config,
1792
- };
1793
- this.eventTracker = new EventTracker(this.baseUrl, this.config.apiKey, this.config.debug || false, this.slots);
1794
- }
1795
1832
  /**
1796
- * SDK 초기화 및 인스턴스 반환
1833
+ * 광고 제거
1797
1834
  */
1798
- static init(config) {
1799
- if (!AdStageSDK.instance) {
1800
- AdStageSDK.instance = new AdStageSDK(config);
1835
+ destroy(slotId) {
1836
+ this.ensureReady();
1837
+ const slot = this.slots.get(slotId);
1838
+ if (!slot) {
1839
+ throw new Error(`Ad slot not found: ${slotId}`);
1840
+ }
1841
+ // DOM에서 제거
1842
+ const container = document.getElementById(slot.containerId);
1843
+ if (container) {
1844
+ container.innerHTML = '';
1845
+ }
1846
+ // 슬롯 제거
1847
+ this.slots.delete(slotId);
1848
+ if (this._config?.debug) {
1849
+ console.log(`🗑️ Ad slot destroyed: ${slotId}`);
1801
1850
  }
1802
- return AdStageSDK.instance;
1803
1851
  }
1804
1852
  /**
1805
- * SDK 인스턴스 반환 (이미 초기화된 경우)
1853
+ * 모든 광고 슬롯 반환
1806
1854
  */
1807
- static getInstance() {
1808
- if (!AdStageSDK.instance) {
1809
- throw new Error('AdStageSDK must be initialized first. Call AdStageSDK.init(config)');
1810
- }
1811
- return AdStageSDK.instance;
1855
+ getAllSlots() {
1856
+ this.ensureReady();
1857
+ return Array.from(this.slots.values());
1858
+ }
1859
+ /**
1860
+ * 특정 광고 슬롯 반환
1861
+ */
1862
+ getSlotById(slotId) {
1863
+ this.ensureReady();
1864
+ return this.slots.get(slotId) || null;
1812
1865
  }
1813
1866
  /**
1814
- * 광고 슬롯 생성 로드
1867
+ * 광고 생성 내부 메소드 (동기 + Lazy 로딩)
1815
1868
  */
1816
- async createSlot(id, containerId, adType = exports.AdType.BANNER, options) {
1817
- const container = DOMUtils.safeGetElementById(containerId);
1869
+ createAd(containerId, type, options) {
1870
+ if (!this._config?.apiKey) {
1871
+ throw new Error('API key not configured');
1872
+ }
1873
+ const container = document.getElementById(containerId);
1818
1874
  if (!container) {
1819
- if (DOMUtils.canUseDOM()) {
1820
- console.error(`Container with ID "${containerId}" not found`);
1821
- }
1822
- return;
1875
+ throw new Error(`Container not found: ${containerId}`);
1823
1876
  }
1877
+ // 고유한 슬롯 ID 생성
1878
+ const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1879
+ // 즉시 placeholder 생성
1880
+ this.createAdSlot(container, slotId, type, options);
1881
+ // 광고 슬롯 정보 저장
1824
1882
  const slot = {
1825
- id,
1883
+ id: slotId,
1826
1884
  containerId,
1827
- adType,
1828
- width: options?.width || 0, // 문자열도 지원
1829
- height: options?.height || 0, // 문자열도 지원
1885
+ adType: type,
1886
+ width: options.width || '100%',
1887
+ height: options.height || 250,
1830
1888
  isLoaded: false,
1831
1889
  isVisible: false,
1832
1890
  refreshRate: 0,
1833
1891
  lazyLoad: false,
1834
1892
  targeting: {},
1835
- load: async () => { await this.loadSlot(slot, options); return null; },
1836
- render: (ad) => this.renderSlot(slot, ad),
1837
- refresh: () => this.refreshSlot(slot.id),
1838
- destroy: () => this.destroySlot(slot.id),
1893
+ advertisement: undefined, // 나중에 로드
1894
+ config: { type, ...options },
1895
+ load: async () => this.fetchAdData(type, options).then(ads => ads[0] || null),
1896
+ render: (ad) => this.renderAdElement(slot, ad),
1897
+ refresh: async () => this.refreshAdSlot(slot),
1898
+ destroy: () => this.destroy(slotId)
1839
1899
  };
1840
- this.slots.set(id, slot);
1841
- await this.loadSlot(slot, options);
1900
+ // 슬롯 저장
1901
+ this.slots.set(slotId, slot);
1902
+ // 백그라운드에서 광고 로드
1903
+ this.loadAdContentInBackground(slot);
1904
+ // 이벤트 추적 준비
1905
+ if (this.eventTracker && this._config?.debug) {
1906
+ console.log(`📊 Event tracking enabled for slot: ${slotId}`);
1907
+ }
1908
+ return slotId;
1909
+ }
1910
+ /**
1911
+ * 즉시 광고 슬롯 생성 (placeholder)
1912
+ */
1913
+ createAdSlot(container, slotId, type, options) {
1914
+ const adElement = document.createElement('div');
1915
+ adElement.id = slotId;
1916
+ adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
1917
+ adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
1918
+ adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
1919
+ adElement.style.border = '1px dashed #ccc';
1920
+ adElement.style.display = 'flex';
1921
+ adElement.style.alignItems = 'center';
1922
+ adElement.style.justifyContent = 'center';
1923
+ adElement.style.backgroundColor = '#f9f9f9';
1924
+ adElement.style.color = '#666';
1925
+ adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
1926
+ container.appendChild(adElement);
1927
+ if (this._config?.debug) {
1928
+ console.log(`📦 Placeholder created for slot: ${slotId}`);
1929
+ }
1842
1930
  }
1843
1931
  /**
1844
- * 광고 슬롯 로드
1932
+ * 백그라운드에서 광고 콘텐츠 로드
1845
1933
  */
1846
- async loadSlot(slot, options) {
1934
+ async loadAdContentInBackground(slot) {
1847
1935
  try {
1848
- const queryParams = new URLSearchParams({
1849
- adType: slot.adType,
1850
- status: 'ACTIVE', // ACTIVE 상태인 광고만 조회
1851
- ...(options?.language && { language: options.language }),
1852
- ...(options?.deviceType && { deviceType: options.deviceType }),
1853
- ...(options?.country && { country: options.country }),
1854
- });
1855
- const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
1856
- if (this.config.debug) {
1857
- console.log(`🌐 광고 API 요청 시작:`, {
1858
- url: requestUrl,
1859
- apiKey: this.config.apiKey.substring(0, 10) + '...',
1860
- slot: slot.id
1861
- });
1862
- }
1863
- const response = await fetch(requestUrl, {
1864
- headers: {
1865
- 'x-api-key': this.config.apiKey,
1866
- 'Content-Type': 'application/json',
1867
- },
1868
- });
1869
- if (this.config.debug) {
1870
- console.log(`📡 API 응답 상태:`, {
1871
- status: response.status,
1872
- statusText: response.statusText,
1873
- ok: response.ok
1874
- });
1875
- }
1876
- if (!response.ok) {
1877
- throw new Error(`HTTP error! status: ${response.status}`);
1936
+ // 광고 데이터 가져오기 - 여러 개 로드
1937
+ const adstageData = await this.fetchAdData(slot.adType, slot.config);
1938
+ if (!adstageData || adstageData.length === 0) {
1939
+ this.renderFallback(slot);
1940
+ return;
1878
1941
  }
1879
- const data = await response.json();
1880
- if (this.config.debug) {
1881
- console.log(`📊 API 응답 데이터:`, {
1882
- data,
1883
- advertisementsCount: data.advertisements ? data.advertisements.length : 0
1884
- });
1885
- }
1886
- const advertisements = data.advertisements || [];
1887
- if (advertisements.length > 0) {
1888
- if (this.config.debug) {
1889
- console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
1890
- }
1891
- // 여러 광고가 있을 경우 슬라이드로 렌더링
1892
- this.renderSlotWithSlider(slot, advertisements, options);
1893
- // 첫 번째 광고에 대해서만 노출 이벤트 추적
1894
- await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, exports.AdEventType.IMPRESSION);
1942
+ // 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
1943
+ if (adstageData.length > 1 || slot.config?.autoSlide) {
1944
+ await this.renderAdSlider(slot, adstageData);
1895
1945
  }
1896
1946
  else {
1897
- console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
1947
+ // 광고가 1개면 일반 렌더링
1948
+ slot.advertisement = adstageData[0];
1949
+ await this.renderAdElement(slot, adstageData[0]);
1950
+ }
1951
+ slot.isLoaded = true;
1952
+ if (this._config?.debug) {
1953
+ console.log(`✅ Ad loaded for slot: ${slot.id} (${adstageData.length} ads)`);
1898
1954
  }
1899
1955
  }
1900
1956
  catch (error) {
1901
- console.error(`❌ 슬롯 ${slot.id} 로드 실패:`, error);
1957
+ console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
1958
+ this.renderFallback(slot);
1902
1959
  }
1903
1960
  }
1904
1961
  /**
1905
- * 광고 슬롯 렌더링 (슬라이더 포함)
1962
+ * Fallback 광고 렌더링
1906
1963
  */
1907
- renderSlotWithSlider(slot, advertisements, options) {
1908
- const container = DOMUtils.safeGetElementById(slot.containerId);
1909
- if (!container) {
1910
- console.error(`❌ 컨테이너를 찾을 없습니다: ${slot.containerId}`);
1911
- return;
1964
+ renderFallback(slot) {
1965
+ const element = document.getElementById(slot.id);
1966
+ if (element) {
1967
+ element.innerHTML = `<span>Ad not available</span>`;
1968
+ element.style.color = '#999';
1969
+ if (this._config?.debug) {
1970
+ console.warn(`⚠️ Fallback rendered for slot: ${slot.id}`);
1971
+ }
1912
1972
  }
1913
- if (this.config.debug) {
1914
- console.log(`🎨 광고 렌더링 시작:`, {
1915
- slotId: slot.id,
1916
- containerId: slot.containerId,
1917
- advertisementCount: advertisements.length,
1918
- container: container
1919
- });
1973
+ }
1974
+ /**
1975
+ * 광고 데이터 가져오기
1976
+ */
1977
+ async fetchAdData(type, options) {
1978
+ if (!this._config?.apiKey) {
1979
+ throw new Error('API key not configured');
1920
1980
  }
1921
- if (advertisements.length === 1) {
1922
- // 광고가 하나뿐이면 기본 렌더링
1923
- this.renderSlot(slot, advertisements[0]);
1924
- return;
1981
+ // GET 요청용 query parameters 구성
1982
+ const params = new URLSearchParams();
1983
+ params.append('adType', type);
1984
+ // userAgent와 url은 header나 자동으로 처리되므로 query에서 제외
1985
+ // 기타 옵션들을 필요시 query parameter로 추가 가능
1986
+ const url = `${endpoints.advertisements.list()}?${params.toString()}`;
1987
+ const response = await fetch(url, {
1988
+ method: 'GET',
1989
+ headers: ApiHeaders.create(this._config.apiKey)
1990
+ });
1991
+ if (!response.ok) {
1992
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
1925
1993
  }
1926
- // 슬라이더 효과 결정 (옵션으로 지정하거나 텍스트 광고인 경우 fade 기본값)
1927
- const isAllTextAds = advertisements.every(ad => ad.adType === exports.AdType.TEXT);
1928
- const useFadeEffect = options?.sliderEffect === 'fade' ||
1929
- (options?.sliderEffect !== 'slide' && isAllTextAds);
1930
- let sliderContainer;
1931
- if (useFadeEffect) {
1932
- // 페이드 슬라이더 사용
1933
- sliderContainer = FadeSliderManager.createFadeSliderContainer(slot, advertisements, options, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
1994
+ const result = await response.json();
1995
+ return result.advertisements || [];
1996
+ }
1997
+ /**
1998
+ * 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
1999
+ */
2000
+ async renderAdSlider(slot, advertisements) {
2001
+ const container = document.getElementById(slot.containerId);
2002
+ if (!container) {
2003
+ throw new Error(`Container not found: ${slot.containerId}`);
2004
+ }
2005
+ // 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
2006
+ const trackEventCallback = (adId, slotId, eventType) => {
2007
+ // 노출 이벤트인 경우 중복 확인
2008
+ if (eventType === AdEventType.IMPRESSION) {
2009
+ if (ImpressionTracker.isDuplicateImpression(adId, slotId, this._config?.debug)) {
2010
+ if (this._config?.debug) {
2011
+ console.log(`🚫 Duplicate impression blocked for ad ${adId} in slot ${slotId}`);
2012
+ }
2013
+ return; // 중복 노출이면 추적하지 않음
2014
+ }
2015
+ if (this._config?.debug) {
2016
+ console.log(`✅ New impression recorded for ad ${adId} in slot ${slotId}`);
2017
+ }
2018
+ }
2019
+ if (this.eventTracker && this._config?.debug) {
2020
+ console.log(`📊 Event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
2021
+ }
2022
+ };
2023
+ let sliderElement;
2024
+ // 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
2025
+ if (slot.adType === AdType.TEXT) {
2026
+ sliderElement = TextTransitionManager.createTextTransitionContainer(slot, advertisements, {
2027
+ autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
2028
+ ...slot.config
2029
+ }, trackEventCallback);
2030
+ if (this._config?.debug) {
2031
+ console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
2032
+ }
1934
2033
  }
1935
2034
  else {
1936
- // 기본 슬라이더 사용
1937
- sliderContainer = SliderManager.createSliderContainer(slot, advertisements, options, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
2035
+ sliderElement = CarouselSliderManager.createSliderContainer(slot, advertisements, {
2036
+ autoSlideInterval: (slot.config?.slideInterval || 5000) / 1000,
2037
+ ...slot.config
2038
+ }, trackEventCallback);
2039
+ if (this._config?.debug) {
2040
+ console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
2041
+ }
1938
2042
  }
2043
+ // 기존 내용 제거하고 슬라이더 추가
1939
2044
  container.innerHTML = '';
1940
- container.appendChild(sliderContainer);
1941
- slot.isLoaded = true;
1942
- if (this.config.debug) {
1943
- const sliderType = useFadeEffect ? 'fade slider' : 'slider';
1944
- console.log(`✅ ${advertisements.length}개 광고를 ${sliderType}로 렌더링 완료:`, {
1945
- slotId: slot.id,
1946
- container: container,
1947
- sliderContainer: sliderContainer
1948
- });
2045
+ container.appendChild(sliderElement);
2046
+ }
2047
+ /**
2048
+ * 광고 렌더링 (단일 광고용)
2049
+ */
2050
+ async renderAd(slot) {
2051
+ if (!slot.advertisement) {
2052
+ throw new Error('No advertisement to render');
1949
2053
  }
2054
+ await this.renderAdElement(slot, slot.advertisement);
2055
+ slot.isLoaded = true;
1950
2056
  }
1951
2057
  /**
1952
- * 광고 슬롯 렌더링 (단일 광고용)
2058
+ * 광고 요소 렌더링 (기본 구현)
1953
2059
  */
1954
- renderSlot(slot, ad) {
1955
- const container = DOMUtils.safeGetElementById(slot.containerId);
1956
- if (!container) {
1957
- console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
2060
+ async renderAdElement(slot, ad) {
2061
+ const container = document.getElementById(slot.containerId);
2062
+ if (!container)
1958
2063
  return;
2064
+ // 기본 HTML 구조 생성
2065
+ const adElement = document.createElement('div');
2066
+ adElement.className = 'adstage-ad';
2067
+ adElement.style.width = typeof slot.width === 'string' ? slot.width : `${slot.width}px`;
2068
+ adElement.style.height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
2069
+ // 광고 타입별 렌더링
2070
+ switch (slot.adType) {
2071
+ case AdType.BANNER:
2072
+ if (ad.imageUrl) {
2073
+ const img = document.createElement('img');
2074
+ img.src = ad.imageUrl;
2075
+ img.alt = ad.title;
2076
+ img.style.width = '100%';
2077
+ img.style.height = '100%';
2078
+ img.style.objectFit = 'cover';
2079
+ adElement.appendChild(img);
2080
+ }
2081
+ break;
2082
+ case AdType.TEXT:
2083
+ const textDiv = document.createElement('div');
2084
+ textDiv.innerHTML = `
2085
+ <h3>${ad.title}</h3>
2086
+ ${ad.description ? `<p>${ad.description}</p>` : ''}
2087
+ ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
2088
+ `;
2089
+ adElement.appendChild(textDiv);
2090
+ break;
2091
+ case AdType.VIDEO:
2092
+ if (ad.videoUrl) {
2093
+ const video = document.createElement('video');
2094
+ video.src = ad.videoUrl;
2095
+ video.controls = true;
2096
+ video.style.width = '100%';
2097
+ video.style.height = '100%';
2098
+ adElement.appendChild(video);
2099
+ }
2100
+ break;
2101
+ default:
2102
+ adElement.innerHTML = `<div>${ad.title}</div>`;
1959
2103
  }
1960
- if (this.config.debug) {
1961
- console.log(`🎨 단일 광고 렌더링 시작:`, {
1962
- slotId: slot.id,
1963
- containerId: slot.containerId,
1964
- ad: ad,
1965
- container: container
1966
- });
1967
- }
1968
- // 팩토리를 사용해서 적절한 렌더러로 광고 생성
1969
- const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
1970
- if (this.config.debug) {
1971
- console.log(`🔧 광고 요소 생성됨:`, {
1972
- adElement: adElement,
1973
- tagName: adElement.tagName,
1974
- innerHTML: adElement.innerHTML.substring(0, 200) + '...'
2104
+ // 클릭 이벤트 추가
2105
+ if (ad.linkUrl) {
2106
+ adElement.style.cursor = 'pointer';
2107
+ adElement.addEventListener('click', () => {
2108
+ window.open(ad.linkUrl, '_blank');
1975
2109
  });
1976
2110
  }
1977
2111
  container.innerHTML = '';
1978
2112
  container.appendChild(adElement);
1979
- slot.isLoaded = true;
1980
- if (this.config.debug) {
1981
- console.log(`✅ 단일 광고 렌더링 완료:`, {
1982
- slotId: slot.id,
1983
- ad: ad,
1984
- containerContent: container.innerHTML.substring(0, 200) + '...'
2113
+ }
2114
+ /**
2115
+ * 광고 슬롯 새로고침
2116
+ */
2117
+ async refreshAdSlot(slot) {
2118
+ try {
2119
+ // 새로운 광고 데이터 가져오기 (config에서 타입과 옵션 정보 사용)
2120
+ const newAdData = await this.fetchAdData(slot.adType, slot.config || {});
2121
+ if (newAdData && newAdData.length > 0) {
2122
+ slot.advertisement = newAdData[0]; // 첫 번째 광고로 업데이트
2123
+ await this.renderAd(slot);
2124
+ // 새로운 노출 추적
2125
+ if (this.eventTracker) {
2126
+ console.log('New impression tracked for slot:', slot.id);
2127
+ }
2128
+ }
2129
+ }
2130
+ catch (error) {
2131
+ console.error(`Failed to refresh ad slot: ${slot.id}`, error);
2132
+ }
2133
+ }
2134
+ /**
2135
+ * 모듈 준비 상태 확인
2136
+ */
2137
+ ensureReady() {
2138
+ if (!this._isReady) {
2139
+ throw new Error('Ads module not initialized. Call AdStage.init() first.');
2140
+ }
2141
+ }
2142
+ }
2143
+
2144
+ /**
2145
+ * AdStage SDK - Config 모듈
2146
+ * 설정 관리 및 API 키 검증
2147
+ */
2148
+ class ConfigModule {
2149
+ constructor() {
2150
+ this._isReady = false;
2151
+ this._config = null;
2152
+ this._organizationInfo = null;
2153
+ }
2154
+ /**
2155
+ * Config 모듈 초기화 (동기)
2156
+ */
2157
+ init(config) {
2158
+ // 설정만 저장 (서버 검증 없음)
2159
+ this._config = {
2160
+ timeout: 30000,
2161
+ debug: false,
2162
+ modules: ['ads', 'events', 'config'],
2163
+ validateOnInit: false,
2164
+ fallbackMode: true,
2165
+ offlineMode: false,
2166
+ productionMode: false,
2167
+ ...config
2168
+ };
2169
+ // 사용자가 baseUrl을 제공한 경우 endpoints에 설정
2170
+ if (config.baseUrl) {
2171
+ endpoints.setBaseUrl(config.baseUrl);
2172
+ }
2173
+ this._isReady = true;
2174
+ if (config.debug) {
2175
+ console.log('✅ Config module initialized (sync mode)', {
2176
+ modules: this._config.modules,
2177
+ endpoint: endpoints.getBaseUrl(),
2178
+ mode: config.productionMode ? 'production' : 'development'
1985
2179
  });
1986
2180
  }
1987
2181
  }
1988
2182
  /**
1989
- * 광고 슬롯 새로고침
2183
+ * 모듈 준비 상태 확인
1990
2184
  */
1991
- async refreshSlot(slotId) {
1992
- const slot = this.slots.get(slotId);
1993
- if (slot) {
1994
- await this.loadSlot(slot);
2185
+ isReady() {
2186
+ return this._isReady;
2187
+ }
2188
+ /**
2189
+ * 현재 설정 반환
2190
+ */
2191
+ getConfig() {
2192
+ return this._config;
2193
+ }
2194
+ /**
2195
+ * 조직 정보 반환
2196
+ */
2197
+ getOrganizationInfo() {
2198
+ return this._organizationInfo;
2199
+ }
2200
+ /**
2201
+ * API 엔드포인트 반환
2202
+ */
2203
+ getApiEndpoint() {
2204
+ return endpoints.getBaseUrl();
2205
+ }
2206
+ /**
2207
+ * 디버그 모드 여부 확인
2208
+ */
2209
+ isDebugMode() {
2210
+ return this._config?.debug || false;
2211
+ }
2212
+ /**
2213
+ * 활성화된 모듈 목록 반환
2214
+ */
2215
+ getEnabledModules() {
2216
+ return this._config?.modules || [];
2217
+ }
2218
+ /**
2219
+ * 특정 모듈이 활성화되어 있는지 확인
2220
+ */
2221
+ isModuleEnabled(moduleName) {
2222
+ return this.getEnabledModules().includes(moduleName);
2223
+ }
2224
+ /**
2225
+ * 설정 업데이트 (런타임)
2226
+ */
2227
+ updateConfig(updates) {
2228
+ if (!this._config) {
2229
+ throw new Error('Config module not initialized');
2230
+ }
2231
+ this._config = {
2232
+ ...this._config,
2233
+ ...updates
2234
+ };
2235
+ if (this.isDebugMode()) {
2236
+ console.log('🔄 Config updated', updates);
1995
2237
  }
1996
2238
  }
1997
2239
  /**
1998
- * 광고 슬롯 제거
2240
+ * API 헤더 생성 (공통 유틸리티 사용)
1999
2241
  */
2000
- destroySlot(slotId) {
2001
- const slot = this.slots.get(slotId);
2002
- if (slot) {
2003
- const container = DOMUtils.safeGetElementById(slot.containerId);
2004
- if (container) {
2005
- DOMUtils.safeSetInnerHTML(container, '');
2242
+ getApiHeaders() {
2243
+ if (!this._config?.apiKey) {
2244
+ throw new Error('API key not available');
2245
+ }
2246
+ return ApiHeaders.create(this._config.apiKey);
2247
+ }
2248
+ }
2249
+
2250
+ /**
2251
+ * AdStage SDK - Events 모듈 (기본 구조)
2252
+ * 이벤트 추적 시스템 - Q1 2025 구현 예정
2253
+ */
2254
+ class EventsModule {
2255
+ constructor() {
2256
+ this._isReady = false;
2257
+ this._config = null;
2258
+ // === 배치 처리 (향후 구현) ===
2259
+ this.batch = {
2260
+ start: () => {
2261
+ console.log('🚧 [TODO] Batch events start');
2262
+ },
2263
+ add: (eventName, properties) => {
2264
+ console.log('🚧 [TODO] Batch events add:', { eventName, properties });
2265
+ },
2266
+ flush: async () => {
2267
+ console.log('🚧 [TODO] Batch events flush');
2006
2268
  }
2007
- this.slots.delete(slotId);
2008
- }
2009
- }
2010
- /**
2011
- * 자동 슬롯 검색 로드 (분리된 SDKUtils 사용)
2012
- */
2013
- async autoLoadSlots() {
2014
- const elements = SDKUtils.findAutoSlotElements();
2015
- for (const element of elements) {
2016
- const slotInfo = SDKUtils.extractSlotInfo(element);
2017
- if (!slotInfo.slotId || this.slots.has(slotInfo.slotId))
2018
- continue;
2019
- const adType = SDKUtils.parseAdType(slotInfo.adType, exports.AdType);
2020
- await this.createSlot(slotInfo.slotId, element.id || slotInfo.slotId, adType, {
2021
- width: slotInfo.width,
2022
- height: slotInfo.height,
2023
- language: slotInfo.language,
2024
- deviceType: slotInfo.deviceType,
2025
- country: slotInfo.country,
2026
- sliderEffect: slotInfo.sliderEffect,
2027
- });
2269
+ };
2270
+ // === 실시간 이벤트 (향후 구현) ===
2271
+ this.realtime = {
2272
+ track: async (eventName, properties) => {
2273
+ console.log('🚧 [TODO] Realtime event tracking:', { eventName, properties });
2274
+ }
2275
+ };
2276
+ }
2277
+ /**
2278
+ * Events 모듈 초기화 (동기)
2279
+ */
2280
+ init(config) {
2281
+ this._config = config;
2282
+ this._isReady = true;
2283
+ if (config.debug) {
2284
+ console.log('📊 Events module initialized (sync mode)');
2028
2285
  }
2029
2286
  }
2030
2287
  /**
2031
- * SDK 정리
2288
+ * 모듈 준비 상태 확인
2032
2289
  */
2033
- destroy() {
2034
- this.slots.clear();
2035
- ImpressionTracker.clear(); // 노출 추적 데이터도 정리
2036
- this.initialized = false;
2037
- AdStageSDK.instance = null;
2290
+ isReady() {
2291
+ return this._isReady;
2038
2292
  }
2039
2293
  /**
2040
- * 디바이스 ID 가져오기
2294
+ * 모듈 설정 반환
2041
2295
  */
2042
- getDeviceId() {
2043
- return DeviceInfoCollector.generateDeviceId();
2296
+ getConfig() {
2297
+ return this._config;
2044
2298
  }
2299
+ // === 향후 구현 예정 메소드들 ===
2045
2300
  /**
2046
- * 세션 ID 가져오기
2301
+ * 커스텀 이벤트 추적
2302
+ * @example AdStage.events.track('page_view', { page: '/products' })
2047
2303
  */
2048
- getSessionId() {
2049
- return DeviceInfoCollector.generateSessionId();
2304
+ async track(eventName, properties) {
2305
+ console.log('🚧 [TODO] Event tracking:', { eventName, properties });
2306
+ // TODO: Q1 2025 구현 예정
2050
2307
  }
2051
2308
  /**
2052
- * 현재 로드된 슬롯 수 가져오기
2309
+ * 페이지 이벤트
2310
+ * @example AdStage.events.pageView({ page: '/home', title: 'Homepage' })
2053
2311
  */
2054
- getLoadedSlotCount() {
2055
- return this.slots.size;
2312
+ async pageView(pageData) {
2313
+ console.log('🚧 [TODO] Page view tracking:', pageData);
2314
+ // TODO: Q1 2025 구현 예정
2056
2315
  }
2057
2316
  /**
2058
- * 모든 슬롯 정보 가져오기
2317
+ * 사용자 액션 이벤트
2318
+ * @example AdStage.events.userAction('button_click', { button_id: 'cta' })
2059
2319
  */
2060
- getAllSlots() {
2061
- return new Map(this.slots);
2320
+ async userAction(actionType, metadata) {
2321
+ console.log('🚧 [TODO] User action tracking:', { actionType, metadata });
2322
+ // TODO: Q1 2025 구현 예정
2323
+ }
2324
+ /**
2325
+ * 컨버전 이벤트
2326
+ * @example AdStage.events.conversion({ type: 'purchase', value: 99.99 })
2327
+ */
2328
+ async conversion(conversionData) {
2329
+ console.log('🚧 [TODO] Conversion tracking:', conversionData);
2330
+ // TODO: Q1 2025 구현 예정
2062
2331
  }
2063
2332
  }
2064
- AdStageSDK.instance = null;
2065
- async function autoInit() {
2066
- if (DOMUtils.isBrowser() && window.adstageConfig) {
2067
- try {
2068
- const sdk = AdStageSDK.init(window.adstageConfig);
2069
- await sdk.autoLoadSlots();
2333
+
2334
+ /**
2335
+ * AdStage SDK - 메인 네임스페이스 클래스
2336
+ * v2.0.0 - 확장 가능한 모듈 아키텍처
2337
+ */
2338
+ class AdStage {
2339
+ constructor() {
2340
+ this._isInitialized = false;
2341
+ this._config = null;
2342
+ // 모듈 초기화 (ads, config는 완전 구현, events는 기본 구조)
2343
+ this.config = new ConfigModule();
2344
+ this.ads = new AdsModule();
2345
+ this.events = new EventsModule();
2346
+ }
2347
+ /**
2348
+ * AdStage SDK 초기화 (동기)
2349
+ */
2350
+ static init(config) {
2351
+ if (!AdStage.instance) {
2352
+ AdStage.instance = new AdStage();
2070
2353
  }
2071
- catch (error) {
2072
- console.error('Failed to auto-initialize AdStageSDK:', error);
2354
+ const instance = AdStage.instance;
2355
+ // 설정 검증
2356
+ if (!config.apiKey) {
2357
+ throw new Error('API key is required for AdStage initialization');
2358
+ }
2359
+ // 설정 저장 (서버 검증 없음)
2360
+ instance._config = {
2361
+ timeout: 30000,
2362
+ debug: false,
2363
+ modules: ['ads', 'events', 'config'],
2364
+ validateOnInit: false,
2365
+ fallbackMode: true,
2366
+ offlineMode: false,
2367
+ productionMode: false,
2368
+ ...config
2369
+ };
2370
+ // 모듈 동기 초기화
2371
+ const enabledModules = instance._config.modules || ['ads', 'events', 'config'];
2372
+ for (const moduleName of enabledModules) {
2373
+ const module = instance[moduleName];
2374
+ if (module && typeof module.init === 'function') {
2375
+ module.init(instance._config);
2376
+ }
2377
+ }
2378
+ instance._isInitialized = true;
2379
+ if (config.debug) {
2380
+ console.log('🚀 AdStage SDK initialized (sync mode)', {
2381
+ version: '2.0.0',
2382
+ modules: enabledModules,
2383
+ apiKey: config.apiKey.substring(0, 8) + '...',
2384
+ mode: config.productionMode ? 'production' : 'development'
2385
+ });
2073
2386
  }
2074
2387
  }
2075
- }
2076
- // 브라우저 환경에서 자동 초기화
2077
- if (DOMUtils.isBrowser()) {
2078
- // 타입 단언을 사용하여 window 객체에 할당
2079
- window.AdStageSDK = AdStageSDK;
2080
- // DOM 로드 후 자동 초기화
2081
- if (DOMUtils.isDOMReady()) {
2082
- autoInit();
2388
+ /**
2389
+ * SDK 초기화 상태 확인
2390
+ */
2391
+ static isReady() {
2392
+ return AdStage.instance?._isInitialized || false;
2083
2393
  }
2084
- else {
2085
- DOMUtils.waitForDOM().then(autoInit);
2394
+ /**
2395
+ * 현재 설정 반환
2396
+ */
2397
+ static getConfig() {
2398
+ return AdStage.instance?._config || null;
2399
+ }
2400
+ /**
2401
+ * SDK 인스턴스 반환 (공개 메소드로 변경)
2402
+ */
2403
+ static getInstance() {
2404
+ if (!AdStage.instance) {
2405
+ throw new Error('AdStage not initialized. Call AdStage.init() first.');
2406
+ }
2407
+ return AdStage.instance;
2408
+ }
2409
+ /**
2410
+ * 편의성을 위한 정적 모듈 접근자들
2411
+ */
2412
+ static get ads() {
2413
+ return AdStage.getInstance().ads;
2414
+ }
2415
+ static get events() {
2416
+ return AdStage.getInstance().events;
2417
+ }
2418
+ static get config() {
2419
+ return AdStage.getInstance().config;
2420
+ }
2421
+ /**
2422
+ * SDK 리셋 (테스트용)
2423
+ */
2424
+ static reset() {
2425
+ if (AdStage.instance) {
2426
+ AdStage.instance._isInitialized = false;
2427
+ AdStage.instance._config = null;
2428
+ }
2086
2429
  }
2087
2430
  }
2088
- // React exports (React가 있을 때만 사용 - 레거시)
2089
- // export * from './react';
2090
2431
 
2091
- var index = /*#__PURE__*/Object.freeze({
2092
- __proto__: null,
2093
- get AdEventType () { return exports.AdEventType; },
2094
- AdStageSDK: AdStageSDK,
2095
- get AdType () { return exports.AdType; },
2096
- createBanner: createBanner,
2097
- createTextAd: createTextAd,
2098
- createVideoAd: createVideoAd,
2099
- default: AdStageSDK,
2100
- destroyAdStage: destroyAdStage,
2101
- getAdStageInstance: getAdStageInstance,
2102
- initAdStage: initAdStage,
2103
- isAdStageReady: isAdStageReady
2104
- });
2432
+ /**
2433
+ * AdStage Web SDK
2434
+ * 네임스페이스 아키텍처 기반 SDK
2435
+ */
2436
+ // 메인 네임스페이스 클래스
2437
+ // 버전 정보
2438
+ const SDK_VERSION = '2.0.0';
2439
+ const SUPPORTED_MODULES = ['ads', 'events', 'config'];
2105
2440
 
2106
- exports.AdStageSDK = AdStageSDK;
2107
- exports.createBanner = createBanner;
2108
- exports.createTextAd = createTextAd;
2109
- exports.createVideoAd = createVideoAd;
2110
- exports.default = AdStageSDK;
2111
- exports.destroyAdStage = destroyAdStage;
2112
- exports.getAdStageInstance = getAdStageInstance;
2113
- exports.initAdStage = initAdStage;
2114
- exports.isAdStageReady = isAdStageReady;
2441
+ exports.AdStage = AdStage;
2442
+ exports.SDK_VERSION = SDK_VERSION;
2443
+ exports.SUPPORTED_MODULES = SUPPORTED_MODULES;