@adstage/web-sdk 1.3.4 → 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.
- package/README.md +178 -35
- package/dist/index.cjs.js +753 -509
- package/dist/index.d.ts +286 -97
- package/dist/index.esm.js +737 -485
- package/dist/index.standalone.js +737 -485
- package/package.json +1 -1
- package/src/constants/endpoints.ts +93 -0
- package/src/core/AdStage.ts +128 -0
- package/src/index.ts +14 -432
- package/src/managers/{slider-manager.ts → carousel-slider-manager.ts} +9 -8
- package/src/managers/event-tracker.ts +2 -4
- package/src/managers/{fade-slider-manager.ts → text-transition-manager.ts} +7 -7
- package/src/modules/ads/AdsModule.ts +525 -0
- package/src/modules/config/ConfigModule.ts +124 -0
- package/src/modules/deeplinks/DeeplinksModule.ts +0 -0
- package/src/modules/events/EventsModule.ts +106 -0
- package/src/types/config.ts +74 -3
- package/src/types/index.ts +2 -1
- package/src/utils/api-headers.ts +52 -0
- package/src/utils/dom-utils.ts +1 -1
- package/examples/README.md +0 -33
- package/examples/banner-ads.html +0 -512
- package/examples/index.html +0 -338
- package/examples/native-ads.html +0 -634
- package/examples/react-app/README.md +0 -70
- package/examples/react-app/index.html +0 -13
- package/examples/react-app/package-lock.json +0 -3042
- package/examples/react-app/package.json +0 -26
- package/examples/react-app/pnpm-lock.yaml +0 -1857
- package/examples/react-app/public/index.standalone.js +0 -2331
- package/examples/react-app/src/App.tsx +0 -226
- package/examples/react-app/src/index.css +0 -37
- package/examples/react-app/src/main.tsx +0 -10
- package/examples/react-app/tsconfig.json +0 -25
- package/examples/react-app/tsconfig.node.json +0 -10
- package/examples/react-app/vite.config.ts +0 -15
- package/examples/react-nextjs/app/globals.css +0 -200
- package/examples/react-nextjs/app/layout.tsx +0 -27
- package/examples/react-nextjs/app/page.tsx +0 -258
- package/examples/react-nextjs/next.config.js +0 -9
- package/examples/react-nextjs/package.json +0 -22
- package/examples/react-nextjs/pnpm-lock.yaml +0 -343
- package/examples/react-nextjs/tsconfig.json +0 -34
- package/examples/text-ads.html +0 -597
- package/examples/video-ads.html +0 -739
- package/src/react/components/AdErrorBoundary.tsx +0 -75
- package/src/react/components/AdSlot.tsx +0 -144
- package/src/react/components/BannerAd.tsx +0 -24
- package/src/react/components/InterstitialAd.tsx +0 -24
- package/src/react/components/NativeAd.tsx +0 -24
- package/src/react/components/TextAd.tsx +0 -24
- package/src/react/components/VideoAd.tsx +0 -24
- package/src/react/components/index.ts +0 -8
- package/src/react/hooks/index.ts +0 -4
- package/src/react/hooks/useAdSlot.ts +0 -83
- package/src/react/hooks/useAdStage.ts +0 -14
- package/src/react/hooks/useAdTracking.ts +0 -61
- package/src/react/index.ts +0 -4
- package/src/react/providers/AdStageProvider.tsx +0 -86
- package/src/react/providers/index.ts +0 -2
- 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
|
-
|
|
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
|
-
})(
|
|
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
|
-
|
|
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
|
-
})(
|
|
31
|
+
})(AdEventType || (AdEventType = {}));
|
|
34
32
|
// 디바이스 타입
|
|
35
33
|
var DeviceType;
|
|
36
34
|
(function (DeviceType) {
|
|
@@ -294,7 +292,7 @@ class DOMUtils {
|
|
|
294
292
|
|
|
295
293
|
다음을 확인해보세요:
|
|
296
294
|
1. HTML에 id="${id}" 요소가 있는지 확인
|
|
297
|
-
2. React
|
|
295
|
+
2. React 등에서 컴포넌트가 렌더링된 후 SDK 호출
|
|
298
296
|
3. 철자가 정확한지 확인
|
|
299
297
|
4. 중복된 ID가 없는지 확인
|
|
300
298
|
|
|
@@ -765,23 +763,24 @@ _a = AdRendererFactory;
|
|
|
765
763
|
AdRendererFactory.renderers = new Map();
|
|
766
764
|
(() => {
|
|
767
765
|
// 렌더러 등록
|
|
768
|
-
_a.renderers.set(
|
|
769
|
-
_a.renderers.set(
|
|
770
|
-
_a.renderers.set(
|
|
771
|
-
_a.renderers.set(
|
|
772
|
-
_a.renderers.set(
|
|
773
|
-
_a.renderers.set(
|
|
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과 동일
|
|
774
772
|
})();
|
|
775
773
|
|
|
776
774
|
/**
|
|
777
|
-
* 슬라이더 관리 클래스
|
|
778
|
-
* -
|
|
779
|
-
* - 무한 루프
|
|
775
|
+
* 캐러셀 슬라이더 관리 클래스
|
|
776
|
+
* - 배너/비디오 광고용 가로 슬라이드 (횡 스크롤)
|
|
777
|
+
* - 무한 루프 캐러셀 지원
|
|
780
778
|
* - 터치 제스처 및 자동 슬라이드 기능
|
|
779
|
+
* - 도트 인디케이터 포함
|
|
781
780
|
*/
|
|
782
|
-
class
|
|
781
|
+
class CarouselSliderManager {
|
|
783
782
|
/**
|
|
784
|
-
*
|
|
783
|
+
* Create carousel slider container with dot indicators and navigation
|
|
785
784
|
*/
|
|
786
785
|
static createSliderContainer(slot, advertisements, options, trackEventCallback) {
|
|
787
786
|
const sliderWrapper = document.createElement('div');
|
|
@@ -909,9 +908,9 @@ class SliderManager {
|
|
|
909
908
|
slideContainer.appendChild(slideElement);
|
|
910
909
|
});
|
|
911
910
|
// 텍스트 광고인지 확인 (모든 광고가 텍스트 타입인 경우)
|
|
912
|
-
const isAllTextAds = advertisements.every(ad => ad.adType ===
|
|
911
|
+
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
913
912
|
// 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
|
|
914
|
-
const dotContainer = isAllTextAds ? null :
|
|
913
|
+
const dotContainer = isAllTextAds ? null : this.createMinimalDotIndicator(advertisements.length);
|
|
915
914
|
// 슬라이더 상태 관리
|
|
916
915
|
let currentSlide = 0;
|
|
917
916
|
const totalSlides = advertisements.length;
|
|
@@ -950,7 +949,7 @@ class SliderManager {
|
|
|
950
949
|
}
|
|
951
950
|
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
952
951
|
if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
953
|
-
trackEventCallback(advertisements[actualIndex]._id, slot.id,
|
|
952
|
+
trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.IMPRESSION);
|
|
954
953
|
}
|
|
955
954
|
};
|
|
956
955
|
// 무한 루프 처리 함수
|
|
@@ -987,7 +986,7 @@ class SliderManager {
|
|
|
987
986
|
}, autoSlideInterval);
|
|
988
987
|
});
|
|
989
988
|
// 터치 제스처 지원 수정 (무한 루프 지원)
|
|
990
|
-
|
|
989
|
+
this.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
|
|
991
990
|
// 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
|
|
992
991
|
sliderWrapper.appendChild(slideContainer);
|
|
993
992
|
if (dotContainer) {
|
|
@@ -1111,16 +1110,16 @@ class SliderManager {
|
|
|
1111
1110
|
}
|
|
1112
1111
|
|
|
1113
1112
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
* - 텍스트 광고 전용 페이드 인/아웃
|
|
1116
|
-
* -
|
|
1113
|
+
* 텍스트 전환 효과 관리 클래스
|
|
1114
|
+
* - 텍스트 광고 전용 페이드 인/아웃 + 상하 움직임 효과
|
|
1115
|
+
* - 부드러운 전환 애니메이션 (vertical transition)
|
|
1117
1116
|
* - 무한 루프 지원
|
|
1118
1117
|
*/
|
|
1119
|
-
class
|
|
1118
|
+
class TextTransitionManager {
|
|
1120
1119
|
/**
|
|
1121
|
-
*
|
|
1120
|
+
* 텍스트 전환 슬라이더 컨테이너 생성
|
|
1122
1121
|
*/
|
|
1123
|
-
static
|
|
1122
|
+
static createTextTransitionContainer(slot, advertisements, options, trackEventCallback) {
|
|
1124
1123
|
const sliderWrapper = document.createElement('div');
|
|
1125
1124
|
sliderWrapper.className = 'adstage-fade-slider-wrapper';
|
|
1126
1125
|
// 래퍼 스타일 설정
|
|
@@ -1270,7 +1269,7 @@ class FadeSliderManager {
|
|
|
1270
1269
|
currentSlide = index;
|
|
1271
1270
|
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
1272
1271
|
if (currentSlide > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
1273
|
-
trackEventCallback(advertisements[currentSlide]._id, slot.id,
|
|
1272
|
+
trackEventCallback(advertisements[currentSlide]._id, slot.id, AdEventType.IMPRESSION);
|
|
1274
1273
|
}
|
|
1275
1274
|
};
|
|
1276
1275
|
// 자동 슬라이드
|
|
@@ -1289,7 +1288,7 @@ class FadeSliderManager {
|
|
|
1289
1288
|
}, autoSlideInterval);
|
|
1290
1289
|
});
|
|
1291
1290
|
// 터치 제스처 지원
|
|
1292
|
-
|
|
1291
|
+
TextTransitionManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
|
|
1293
1292
|
// 요소들 조립
|
|
1294
1293
|
sliderWrapper.appendChild(slideContainer);
|
|
1295
1294
|
return sliderWrapper;
|
|
@@ -1509,6 +1508,46 @@ class DeviceInfoCollector {
|
|
|
1509
1508
|
}
|
|
1510
1509
|
}
|
|
1511
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
|
+
|
|
1512
1551
|
/**
|
|
1513
1552
|
* 이벤트 추적 관리 클래스
|
|
1514
1553
|
* - 광고 이벤트 추적 및 전송
|
|
@@ -1528,7 +1567,7 @@ class EventTracker {
|
|
|
1528
1567
|
async trackEvent(adId, slotId, eventType) {
|
|
1529
1568
|
try {
|
|
1530
1569
|
// 노출 이벤트의 경우 중복 확인
|
|
1531
|
-
if (eventType ===
|
|
1570
|
+
if (eventType === AdEventType.IMPRESSION) {
|
|
1532
1571
|
if (ImpressionTracker.isDuplicateImpression(adId, slotId, this.debug)) {
|
|
1533
1572
|
return; // 중복 노출이므로 추적하지 않음
|
|
1534
1573
|
}
|
|
@@ -1584,10 +1623,7 @@ class EventTracker {
|
|
|
1584
1623
|
};
|
|
1585
1624
|
await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
|
|
1586
1625
|
method: 'POST',
|
|
1587
|
-
headers:
|
|
1588
|
-
'x-api-key': this.apiKey,
|
|
1589
|
-
'Content-Type': 'application/json',
|
|
1590
|
-
},
|
|
1626
|
+
headers: ApiHeaders.createForEvents(this.apiKey, eventData),
|
|
1591
1627
|
body: JSON.stringify(eventData),
|
|
1592
1628
|
});
|
|
1593
1629
|
if (this.debug) {
|
|
@@ -1615,585 +1651,793 @@ class EventTracker {
|
|
|
1615
1651
|
}
|
|
1616
1652
|
|
|
1617
1653
|
/**
|
|
1618
|
-
* SDK
|
|
1619
|
-
*
|
|
1620
|
-
* - 자동 슬롯 검색
|
|
1621
|
-
* - 기타 헬퍼 함수들
|
|
1654
|
+
* AdStage SDK 엔드포인트 상수 관리
|
|
1655
|
+
* 모든 API URL을 중앙에서 관리
|
|
1622
1656
|
*/
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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;
|
|
1641
1702
|
}
|
|
1642
1703
|
/**
|
|
1643
|
-
*
|
|
1704
|
+
* 기본 URL 변경
|
|
1644
1705
|
*/
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
const elements = document.querySelectorAll('[data-adstage], [adstage]');
|
|
1649
|
-
return Array.from(elements);
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* 요소에서 슬롯 정보 추출
|
|
1653
|
-
*/
|
|
1654
|
-
static extractSlotInfo(element) {
|
|
1655
|
-
// 슬롯 ID 가져오기
|
|
1656
|
-
const slotId = SDKUtils.getElementAttribute(element, 'adstage');
|
|
1657
|
-
// 광고 타입 가져오기
|
|
1658
|
-
const adType = SDKUtils.getElementAttribute(element, 'adType') ||
|
|
1659
|
-
SDKUtils.getElementAttribute(element, 'ad-type') ||
|
|
1660
|
-
'BANNER';
|
|
1661
|
-
// 크기 정보 가져오기 (다양한 형태 지원)
|
|
1662
|
-
const width = SDKUtils.getElementAttribute(element, 'width');
|
|
1663
|
-
const height = SDKUtils.getElementAttribute(element, 'height');
|
|
1664
|
-
// 기타 옵션들
|
|
1665
|
-
const language = SDKUtils.getElementAttribute(element, 'language');
|
|
1666
|
-
const deviceType = SDKUtils.getElementAttribute(element, 'deviceType') ||
|
|
1667
|
-
SDKUtils.getElementAttribute(element, 'device-type');
|
|
1668
|
-
const country = SDKUtils.getElementAttribute(element, 'country');
|
|
1669
|
-
const sliderEffect = SDKUtils.getElementAttribute(element, 'sliderEffect') ||
|
|
1670
|
-
SDKUtils.getElementAttribute(element, 'slider-effect');
|
|
1671
|
-
return {
|
|
1672
|
-
slotId: slotId || null,
|
|
1673
|
-
adType,
|
|
1674
|
-
width,
|
|
1675
|
-
height,
|
|
1676
|
-
language,
|
|
1677
|
-
deviceType,
|
|
1678
|
-
country,
|
|
1679
|
-
sliderEffect,
|
|
1680
|
-
};
|
|
1706
|
+
setBaseUrl(url) {
|
|
1707
|
+
this.baseUrl = url;
|
|
1708
|
+
console.log('🔄 API endpoint changed:', url);
|
|
1681
1709
|
}
|
|
1682
1710
|
/**
|
|
1683
|
-
*
|
|
1711
|
+
* 기본 URL 반환
|
|
1684
1712
|
*/
|
|
1685
|
-
|
|
1686
|
-
return
|
|
1713
|
+
getBaseUrl() {
|
|
1714
|
+
return this.baseUrl;
|
|
1687
1715
|
}
|
|
1688
1716
|
/**
|
|
1689
|
-
*
|
|
1717
|
+
* 커스텀 경로 생성
|
|
1690
1718
|
*/
|
|
1691
|
-
|
|
1692
|
-
return
|
|
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;
|
|
1693
1738
|
}
|
|
1694
1739
|
/**
|
|
1695
|
-
*
|
|
1740
|
+
* Ads 모듈 초기화 (동기)
|
|
1696
1741
|
*/
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
+
}
|
|
1699
1750
|
}
|
|
1700
1751
|
/**
|
|
1701
|
-
*
|
|
1752
|
+
* 모듈 준비 상태 확인
|
|
1702
1753
|
*/
|
|
1703
|
-
|
|
1704
|
-
return
|
|
1754
|
+
isReady() {
|
|
1755
|
+
return this._isReady;
|
|
1705
1756
|
}
|
|
1706
1757
|
/**
|
|
1707
|
-
*
|
|
1758
|
+
* 모듈 설정 반환
|
|
1708
1759
|
*/
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
return false;
|
|
1712
|
-
return document.readyState !== 'loading';
|
|
1760
|
+
getConfig() {
|
|
1761
|
+
return this._config;
|
|
1713
1762
|
}
|
|
1714
1763
|
/**
|
|
1715
|
-
*
|
|
1764
|
+
* 배너 광고 생성 (동기)
|
|
1716
1765
|
*/
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
document.addEventListener('DOMContentLoaded', () => resolve());
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
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);
|
|
1730
1776
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
async function initAdStage(config) {
|
|
1743
|
-
if (globalSDKInstance) {
|
|
1744
|
-
console.warn('AdStage SDK가 이미 초기화되었습니다.');
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
if (isInitializing) {
|
|
1748
|
-
console.warn('AdStage SDK 초기화가 진행 중입니다.');
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
isInitializing = true;
|
|
1752
|
-
try {
|
|
1753
|
-
// 동적 import로 circular dependency 방지
|
|
1754
|
-
const { AdStageSDK } = await Promise.resolve().then(function () { return index; });
|
|
1755
|
-
globalSDKInstance = AdStageSDK.init({
|
|
1756
|
-
apiKey: config.apiKey,
|
|
1757
|
-
debug: config.debug || false
|
|
1758
|
-
});
|
|
1759
|
-
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);
|
|
1760
1788
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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);
|
|
1764
1802
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1803
|
+
/**
|
|
1804
|
+
* 네이티브 광고 생성 (동기)
|
|
1805
|
+
*/
|
|
1806
|
+
native(containerId, options) {
|
|
1807
|
+
this.ensureReady();
|
|
1808
|
+
return this.createAd(containerId, AdType.NATIVE, options || {});
|
|
1767
1809
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다. initAdStage()를 먼저 호출하세요.');
|
|
1775
|
-
}
|
|
1776
|
-
const slotId = `banner-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1777
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'BANNER', {
|
|
1778
|
-
width: options?.width || '100%',
|
|
1779
|
-
height: options?.height || 120,
|
|
1780
|
-
autoSlideInterval: options?.autoSlide ? (options.slideInterval || 5) : 0,
|
|
1781
|
-
sliderEffect: 'fade'
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* 텍스트 광고 생성
|
|
1786
|
-
*/
|
|
1787
|
-
async function createTextAd(containerId, options) {
|
|
1788
|
-
if (!globalSDKInstance) {
|
|
1789
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다.');
|
|
1790
|
-
}
|
|
1791
|
-
const slotId = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1792
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'TEXT', {
|
|
1793
|
-
maxLines: options?.maxLines || 3,
|
|
1794
|
-
style: options?.style || 'card'
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* 비디오 광고 생성
|
|
1799
|
-
*/
|
|
1800
|
-
async function createVideoAd(containerId, options) {
|
|
1801
|
-
if (!globalSDKInstance) {
|
|
1802
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다.');
|
|
1803
|
-
}
|
|
1804
|
-
const slotId = `video-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1805
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'VIDEO', {
|
|
1806
|
-
width: options?.width || '100%',
|
|
1807
|
-
height: options?.height || 300,
|
|
1808
|
-
autoplay: options?.autoplay || false,
|
|
1809
|
-
muted: options?.muted || true
|
|
1810
|
-
});
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* SDK 상태 확인
|
|
1814
|
-
*/
|
|
1815
|
-
function isAdStageReady() {
|
|
1816
|
-
return globalSDKInstance !== null && !isInitializing;
|
|
1817
|
-
}
|
|
1818
|
-
/**
|
|
1819
|
-
* SDK 인스턴스 가져오기 (고급 사용자용)
|
|
1820
|
-
*/
|
|
1821
|
-
function getAdStageInstance() {
|
|
1822
|
-
if (!globalSDKInstance) {
|
|
1823
|
-
console.warn('AdStage SDK가 초기화되지 않았습니다.');
|
|
1824
|
-
return null;
|
|
1810
|
+
/**
|
|
1811
|
+
* 전면 광고 생성 (동기)
|
|
1812
|
+
*/
|
|
1813
|
+
interstitial(containerId, options) {
|
|
1814
|
+
this.ensureReady();
|
|
1815
|
+
return this.createAd(containerId, AdType.INTERSTITIAL, options || {});
|
|
1825
1816
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
// SDK cleanup logic here
|
|
1835
|
-
globalSDKInstance = null;
|
|
1836
|
-
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}`);
|
|
1837
1825
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1826
|
+
// 광고 새로고침 로직
|
|
1827
|
+
this.refreshAdSlot(slot);
|
|
1828
|
+
if (this._config?.debug) {
|
|
1829
|
+
console.log(`🔄 Ad slot refreshed: ${slotId}`);
|
|
1840
1830
|
}
|
|
1841
1831
|
}
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
/**
|
|
1845
|
-
* AdStage SDK 메인 클래스
|
|
1846
|
-
* - 간단한 API Key 기반 초기화
|
|
1847
|
-
* - 광고 슬롯 자동 관리
|
|
1848
|
-
* - 이벤트 자동 추적
|
|
1849
|
-
*/
|
|
1850
|
-
class AdStageSDK {
|
|
1851
|
-
constructor(config) {
|
|
1852
|
-
this.baseUrl = 'https://beta-api.adstage.app';
|
|
1853
|
-
this.slots = new Map();
|
|
1854
|
-
this.initialized = false;
|
|
1855
|
-
this.config = {
|
|
1856
|
-
debug: false,
|
|
1857
|
-
...config,
|
|
1858
|
-
};
|
|
1859
|
-
this.eventTracker = new EventTracker(this.baseUrl, this.config.apiKey, this.config.debug || false, this.slots);
|
|
1860
|
-
}
|
|
1861
1832
|
/**
|
|
1862
|
-
*
|
|
1833
|
+
* 광고 제거
|
|
1863
1834
|
*/
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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}`);
|
|
1867
1850
|
}
|
|
1868
|
-
return AdStageSDK.instance;
|
|
1869
1851
|
}
|
|
1870
1852
|
/**
|
|
1871
|
-
*
|
|
1853
|
+
* 모든 광고 슬롯 반환
|
|
1872
1854
|
*/
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
return AdStageSDK.instance;
|
|
1855
|
+
getAllSlots() {
|
|
1856
|
+
this.ensureReady();
|
|
1857
|
+
return Array.from(this.slots.values());
|
|
1878
1858
|
}
|
|
1879
1859
|
/**
|
|
1880
|
-
* 광고 슬롯
|
|
1860
|
+
* 특정 광고 슬롯 반환
|
|
1881
1861
|
*/
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
});
|
|
1893
|
-
if (this.config.debug) {
|
|
1894
|
-
console.log(`✅ 컨테이너 확인됨: ${containerId}`, container);
|
|
1895
|
-
}
|
|
1896
|
-
const slot = {
|
|
1897
|
-
id,
|
|
1898
|
-
containerId,
|
|
1899
|
-
adType,
|
|
1900
|
-
width: options?.width || 0, // 문자열도 지원
|
|
1901
|
-
height: options?.height || 0, // 문자열도 지원
|
|
1902
|
-
isLoaded: false,
|
|
1903
|
-
isVisible: false,
|
|
1904
|
-
refreshRate: 0,
|
|
1905
|
-
lazyLoad: false,
|
|
1906
|
-
targeting: {},
|
|
1907
|
-
load: async () => { await this.loadSlot(slot, options); return null; },
|
|
1908
|
-
render: (ad) => this.renderSlot(slot, ad),
|
|
1909
|
-
refresh: () => this.refreshSlot(slot.id),
|
|
1910
|
-
destroy: () => this.destroySlot(slot.id),
|
|
1911
|
-
};
|
|
1912
|
-
this.slots.set(id, slot);
|
|
1913
|
-
await this.loadSlot(slot, options);
|
|
1862
|
+
getSlotById(slotId) {
|
|
1863
|
+
this.ensureReady();
|
|
1864
|
+
return this.slots.get(slotId) || null;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* 광고 생성 내부 메소드 (동기 + Lazy 로딩)
|
|
1868
|
+
*/
|
|
1869
|
+
createAd(containerId, type, options) {
|
|
1870
|
+
if (!this._config?.apiKey) {
|
|
1871
|
+
throw new Error('API key not configured');
|
|
1914
1872
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1873
|
+
const container = document.getElementById(containerId);
|
|
1874
|
+
if (!container) {
|
|
1875
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
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
|
+
// 광고 슬롯 정보 저장
|
|
1882
|
+
const slot = {
|
|
1883
|
+
id: slotId,
|
|
1884
|
+
containerId,
|
|
1885
|
+
adType: type,
|
|
1886
|
+
width: options.width || '100%',
|
|
1887
|
+
height: options.height || 250,
|
|
1888
|
+
isLoaded: false,
|
|
1889
|
+
isVisible: false,
|
|
1890
|
+
refreshRate: 0,
|
|
1891
|
+
lazyLoad: false,
|
|
1892
|
+
targeting: {},
|
|
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)
|
|
1899
|
+
};
|
|
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}`);
|
|
1925
1929
|
}
|
|
1926
1930
|
}
|
|
1927
1931
|
/**
|
|
1928
|
-
* 광고
|
|
1932
|
+
* 백그라운드에서 광고 콘텐츠 로드
|
|
1929
1933
|
*/
|
|
1930
|
-
async
|
|
1934
|
+
async loadAdContentInBackground(slot) {
|
|
1931
1935
|
try {
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
...(options?.country && { country: options.country }),
|
|
1938
|
-
});
|
|
1939
|
-
const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
|
|
1940
|
-
if (this.config.debug) {
|
|
1941
|
-
console.log(`🌐 광고 API 요청 시작:`, {
|
|
1942
|
-
url: requestUrl,
|
|
1943
|
-
apiKey: this.config.apiKey.substring(0, 10) + '...',
|
|
1944
|
-
slot: slot.id
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
const response = await fetch(requestUrl, {
|
|
1948
|
-
headers: {
|
|
1949
|
-
'x-api-key': this.config.apiKey,
|
|
1950
|
-
'Content-Type': 'application/json',
|
|
1951
|
-
},
|
|
1952
|
-
});
|
|
1953
|
-
if (this.config.debug) {
|
|
1954
|
-
console.log(`📡 API 응답 상태:`, {
|
|
1955
|
-
status: response.status,
|
|
1956
|
-
statusText: response.statusText,
|
|
1957
|
-
ok: response.ok
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
if (!response.ok) {
|
|
1961
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1962
|
-
}
|
|
1963
|
-
const data = await response.json();
|
|
1964
|
-
if (this.config.debug) {
|
|
1965
|
-
console.log(`📊 API 응답 데이터:`, {
|
|
1966
|
-
data,
|
|
1967
|
-
advertisementsCount: data.advertisements ? data.advertisements.length : 0
|
|
1968
|
-
});
|
|
1936
|
+
// 광고 데이터 가져오기 - 여러 개 로드
|
|
1937
|
+
const adstageData = await this.fetchAdData(slot.adType, slot.config);
|
|
1938
|
+
if (!adstageData || adstageData.length === 0) {
|
|
1939
|
+
this.renderFallback(slot);
|
|
1940
|
+
return;
|
|
1969
1941
|
}
|
|
1970
|
-
|
|
1971
|
-
if (
|
|
1972
|
-
|
|
1973
|
-
console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
|
|
1974
|
-
}
|
|
1975
|
-
// 여러 광고가 있을 경우 슬라이드로 렌더링
|
|
1976
|
-
this.renderSlotWithSlider(slot, advertisements, options);
|
|
1977
|
-
// 첫 번째 광고에 대해서만 노출 이벤트 추적
|
|
1978
|
-
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);
|
|
1979
1945
|
}
|
|
1980
1946
|
else {
|
|
1981
|
-
|
|
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)`);
|
|
1982
1954
|
}
|
|
1983
1955
|
}
|
|
1984
1956
|
catch (error) {
|
|
1985
|
-
console.error(`❌
|
|
1957
|
+
console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
|
|
1958
|
+
this.renderFallback(slot);
|
|
1986
1959
|
}
|
|
1987
1960
|
}
|
|
1988
1961
|
/**
|
|
1989
|
-
* 광고
|
|
1962
|
+
* Fallback 광고 렌더링
|
|
1990
1963
|
*/
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
+
}
|
|
1997
1972
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* 광고 데이터 가져오기
|
|
1976
|
+
*/
|
|
1977
|
+
async fetchAdData(type, options) {
|
|
1978
|
+
if (!this._config?.apiKey) {
|
|
1979
|
+
throw new Error('API key not configured');
|
|
2005
1980
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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}`);
|
|
2010
1993
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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
|
+
}
|
|
2019
2033
|
}
|
|
2020
2034
|
else {
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
+
}
|
|
2023
2042
|
}
|
|
2043
|
+
// 기존 내용 제거하고 슬라이더 추가
|
|
2024
2044
|
container.innerHTML = '';
|
|
2025
|
-
container.appendChild(
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
});
|
|
2045
|
+
container.appendChild(sliderElement);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* 광고 렌더링 (단일 광고용)
|
|
2049
|
+
*/
|
|
2050
|
+
async renderAd(slot) {
|
|
2051
|
+
if (!slot.advertisement) {
|
|
2052
|
+
throw new Error('No advertisement to render');
|
|
2034
2053
|
}
|
|
2054
|
+
await this.renderAdElement(slot, slot.advertisement);
|
|
2055
|
+
slot.isLoaded = true;
|
|
2035
2056
|
}
|
|
2036
2057
|
/**
|
|
2037
|
-
* 광고
|
|
2058
|
+
* 광고 요소 렌더링 (기본 구현)
|
|
2038
2059
|
*/
|
|
2039
|
-
|
|
2040
|
-
const container =
|
|
2041
|
-
if (!container)
|
|
2042
|
-
console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
|
|
2060
|
+
async renderAdElement(slot, ad) {
|
|
2061
|
+
const container = document.getElementById(slot.containerId);
|
|
2062
|
+
if (!container)
|
|
2043
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>`;
|
|
2044
2103
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
ad
|
|
2050
|
-
container: container
|
|
2051
|
-
});
|
|
2052
|
-
}
|
|
2053
|
-
// 팩토리를 사용해서 적절한 렌더러로 광고 생성
|
|
2054
|
-
const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
|
|
2055
|
-
if (this.config.debug) {
|
|
2056
|
-
console.log(`🔧 광고 요소 생성됨:`, {
|
|
2057
|
-
adElement: adElement,
|
|
2058
|
-
tagName: adElement.tagName,
|
|
2059
|
-
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');
|
|
2060
2109
|
});
|
|
2061
2110
|
}
|
|
2062
2111
|
container.innerHTML = '';
|
|
2063
2112
|
container.appendChild(adElement);
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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'
|
|
2070
2179
|
});
|
|
2071
2180
|
}
|
|
2072
2181
|
}
|
|
2073
2182
|
/**
|
|
2074
|
-
*
|
|
2183
|
+
* 모듈 준비 상태 확인
|
|
2075
2184
|
*/
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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);
|
|
2080
2237
|
}
|
|
2081
2238
|
}
|
|
2082
2239
|
/**
|
|
2083
|
-
*
|
|
2240
|
+
* API 헤더 생성 (공통 유틸리티 사용)
|
|
2084
2241
|
*/
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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');
|
|
2091
2268
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
language: slotInfo.language,
|
|
2109
|
-
deviceType: slotInfo.deviceType,
|
|
2110
|
-
country: slotInfo.country,
|
|
2111
|
-
sliderEffect: slotInfo.sliderEffect,
|
|
2112
|
-
});
|
|
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)');
|
|
2113
2285
|
}
|
|
2114
2286
|
}
|
|
2115
2287
|
/**
|
|
2116
|
-
*
|
|
2288
|
+
* 모듈 준비 상태 확인
|
|
2117
2289
|
*/
|
|
2118
|
-
|
|
2119
|
-
this.
|
|
2120
|
-
ImpressionTracker.clear(); // 노출 추적 데이터도 정리
|
|
2121
|
-
this.initialized = false;
|
|
2122
|
-
AdStageSDK.instance = null;
|
|
2290
|
+
isReady() {
|
|
2291
|
+
return this._isReady;
|
|
2123
2292
|
}
|
|
2124
2293
|
/**
|
|
2125
|
-
*
|
|
2294
|
+
* 모듈 설정 반환
|
|
2126
2295
|
*/
|
|
2127
|
-
|
|
2128
|
-
return
|
|
2296
|
+
getConfig() {
|
|
2297
|
+
return this._config;
|
|
2129
2298
|
}
|
|
2299
|
+
// === 향후 구현 예정 메소드들 ===
|
|
2130
2300
|
/**
|
|
2131
|
-
*
|
|
2301
|
+
* 커스텀 이벤트 추적
|
|
2302
|
+
* @example AdStage.events.track('page_view', { page: '/products' })
|
|
2132
2303
|
*/
|
|
2133
|
-
|
|
2134
|
-
|
|
2304
|
+
async track(eventName, properties) {
|
|
2305
|
+
console.log('🚧 [TODO] Event tracking:', { eventName, properties });
|
|
2306
|
+
// TODO: Q1 2025 구현 예정
|
|
2135
2307
|
}
|
|
2136
2308
|
/**
|
|
2137
|
-
*
|
|
2309
|
+
* 페이지 뷰 이벤트
|
|
2310
|
+
* @example AdStage.events.pageView({ page: '/home', title: 'Homepage' })
|
|
2138
2311
|
*/
|
|
2139
|
-
|
|
2140
|
-
|
|
2312
|
+
async pageView(pageData) {
|
|
2313
|
+
console.log('🚧 [TODO] Page view tracking:', pageData);
|
|
2314
|
+
// TODO: Q1 2025 구현 예정
|
|
2141
2315
|
}
|
|
2142
2316
|
/**
|
|
2143
|
-
*
|
|
2317
|
+
* 사용자 액션 이벤트
|
|
2318
|
+
* @example AdStage.events.userAction('button_click', { button_id: 'cta' })
|
|
2144
2319
|
*/
|
|
2145
|
-
|
|
2146
|
-
|
|
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 구현 예정
|
|
2147
2331
|
}
|
|
2148
2332
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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();
|
|
2155
2353
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
+
});
|
|
2158
2386
|
}
|
|
2159
2387
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2388
|
+
/**
|
|
2389
|
+
* SDK 초기화 상태 확인
|
|
2390
|
+
*/
|
|
2391
|
+
static isReady() {
|
|
2392
|
+
return AdStage.instance?._isInitialized || false;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* 현재 설정 반환
|
|
2396
|
+
*/
|
|
2397
|
+
static getConfig() {
|
|
2398
|
+
return AdStage.instance?._config || null;
|
|
2168
2399
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
+
}
|
|
2171
2429
|
}
|
|
2172
2430
|
}
|
|
2173
|
-
// React exports (React가 있을 때만 사용 - 레거시)
|
|
2174
|
-
// export * from './react';
|
|
2175
2431
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
default: AdStageSDK,
|
|
2185
|
-
destroyAdStage: destroyAdStage,
|
|
2186
|
-
getAdStageInstance: getAdStageInstance,
|
|
2187
|
-
initAdStage: initAdStage,
|
|
2188
|
-
isAdStageReady: isAdStageReady
|
|
2189
|
-
});
|
|
2432
|
+
/**
|
|
2433
|
+
* AdStage Web SDK
|
|
2434
|
+
* 네임스페이스 아키텍처 기반 SDK
|
|
2435
|
+
*/
|
|
2436
|
+
// 메인 네임스페이스 클래스
|
|
2437
|
+
// 버전 정보
|
|
2438
|
+
const SDK_VERSION = '2.0.0';
|
|
2439
|
+
const SUPPORTED_MODULES = ['ads', 'events', 'config'];
|
|
2190
2440
|
|
|
2191
|
-
exports.
|
|
2192
|
-
exports.
|
|
2193
|
-
exports.
|
|
2194
|
-
exports.createVideoAd = createVideoAd;
|
|
2195
|
-
exports.default = AdStageSDK;
|
|
2196
|
-
exports.destroyAdStage = destroyAdStage;
|
|
2197
|
-
exports.getAdStageInstance = getAdStageInstance;
|
|
2198
|
-
exports.initAdStage = initAdStage;
|
|
2199
|
-
exports.isAdStageReady = isAdStageReady;
|
|
2441
|
+
exports.AdStage = AdStage;
|
|
2442
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
2443
|
+
exports.SUPPORTED_MODULES = SUPPORTED_MODULES;
|