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