@adstage/web-sdk 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -35
- package/dist/index.cjs.js +810 -481
- package/dist/index.d.ts +286 -97
- package/dist/index.esm.js +794 -457
- package/dist/index.standalone.js +794 -457
- package/package.json +2 -2
- package/src/constants/endpoints.ts +93 -0
- package/src/core/AdStage.ts +128 -0
- package/src/index.ts +14 -413
- 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 +93 -0
- 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.esm.js
CHANGED
|
@@ -253,6 +253,72 @@ class DOMUtils {
|
|
|
253
253
|
scrollLeft: this.getWindowProperty('pageXOffset', 0),
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
|
|
258
|
+
*/
|
|
259
|
+
static async waitForElement(id, options = {}) {
|
|
260
|
+
const { timeout = 3000, retryInterval = 100, debug = false } = options;
|
|
261
|
+
if (!this.canUseDOM()) {
|
|
262
|
+
throw new Error('DOM을 사용할 수 없는 환경입니다.');
|
|
263
|
+
}
|
|
264
|
+
// 즉시 찾을 수 있으면 바로 반환
|
|
265
|
+
const immediateElement = document.getElementById(id);
|
|
266
|
+
if (immediateElement) {
|
|
267
|
+
if (debug) {
|
|
268
|
+
console.log(`✅ 컨테이너 즉시 발견: ${id}`);
|
|
269
|
+
}
|
|
270
|
+
return immediateElement;
|
|
271
|
+
}
|
|
272
|
+
if (debug) {
|
|
273
|
+
console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
|
|
274
|
+
}
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
let attempts = 0;
|
|
277
|
+
const maxAttempts = Math.ceil(timeout / retryInterval);
|
|
278
|
+
const checkElement = () => {
|
|
279
|
+
attempts++;
|
|
280
|
+
const element = document.getElementById(id);
|
|
281
|
+
if (element) {
|
|
282
|
+
if (debug) {
|
|
283
|
+
console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
|
|
284
|
+
}
|
|
285
|
+
resolve(element);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (attempts >= maxAttempts) {
|
|
289
|
+
const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
|
|
290
|
+
|
|
291
|
+
다음을 확인해보세요:
|
|
292
|
+
1. HTML에 id="${id}" 요소가 있는지 확인
|
|
293
|
+
2. React 등에서 컴포넌트가 렌더링된 후 SDK 호출
|
|
294
|
+
3. 철자가 정확한지 확인
|
|
295
|
+
4. 중복된 ID가 없는지 확인
|
|
296
|
+
|
|
297
|
+
대기 시간: ${timeout}ms (${attempts}번 시도)`;
|
|
298
|
+
if (debug) {
|
|
299
|
+
console.error(errorMessage);
|
|
300
|
+
}
|
|
301
|
+
reject(new Error(errorMessage));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (debug && attempts % 10 === 0) {
|
|
305
|
+
console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
|
|
306
|
+
}
|
|
307
|
+
// Exponential backoff: 처음엔 빠르게, 나중엔 느리게
|
|
308
|
+
const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
|
|
309
|
+
setTimeout(checkElement, nextInterval);
|
|
310
|
+
};
|
|
311
|
+
// 첫 번째 체크는 즉시 실행
|
|
312
|
+
setTimeout(checkElement, retryInterval);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 여러 DOM 요소를 동시에 기다리기
|
|
317
|
+
*/
|
|
318
|
+
static async waitForElements(ids, options = {}) {
|
|
319
|
+
const promises = ids.map(id => this.waitForElement(id, options));
|
|
320
|
+
return Promise.all(promises);
|
|
321
|
+
}
|
|
256
322
|
}
|
|
257
323
|
|
|
258
324
|
/**
|
|
@@ -704,14 +770,15 @@ AdRendererFactory.renderers = new Map();
|
|
|
704
770
|
})();
|
|
705
771
|
|
|
706
772
|
/**
|
|
707
|
-
* 슬라이더 관리 클래스
|
|
708
|
-
* -
|
|
709
|
-
* - 무한 루프
|
|
773
|
+
* 캐러셀 슬라이더 관리 클래스
|
|
774
|
+
* - 배너/비디오 광고용 가로 슬라이드 (횡 스크롤)
|
|
775
|
+
* - 무한 루프 캐러셀 지원
|
|
710
776
|
* - 터치 제스처 및 자동 슬라이드 기능
|
|
777
|
+
* - 도트 인디케이터 포함
|
|
711
778
|
*/
|
|
712
|
-
class
|
|
779
|
+
class CarouselSliderManager {
|
|
713
780
|
/**
|
|
714
|
-
*
|
|
781
|
+
* Create carousel slider container with dot indicators and navigation
|
|
715
782
|
*/
|
|
716
783
|
static createSliderContainer(slot, advertisements, options, trackEventCallback) {
|
|
717
784
|
const sliderWrapper = document.createElement('div');
|
|
@@ -841,7 +908,7 @@ class SliderManager {
|
|
|
841
908
|
// 텍스트 광고인지 확인 (모든 광고가 텍스트 타입인 경우)
|
|
842
909
|
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
843
910
|
// 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
|
|
844
|
-
const dotContainer = isAllTextAds ? null :
|
|
911
|
+
const dotContainer = isAllTextAds ? null : this.createMinimalDotIndicator(advertisements.length);
|
|
845
912
|
// 슬라이더 상태 관리
|
|
846
913
|
let currentSlide = 0;
|
|
847
914
|
const totalSlides = advertisements.length;
|
|
@@ -917,7 +984,7 @@ class SliderManager {
|
|
|
917
984
|
}, autoSlideInterval);
|
|
918
985
|
});
|
|
919
986
|
// 터치 제스처 지원 수정 (무한 루프 지원)
|
|
920
|
-
|
|
987
|
+
this.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
|
|
921
988
|
// 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
|
|
922
989
|
sliderWrapper.appendChild(slideContainer);
|
|
923
990
|
if (dotContainer) {
|
|
@@ -1041,16 +1108,16 @@ class SliderManager {
|
|
|
1041
1108
|
}
|
|
1042
1109
|
|
|
1043
1110
|
/**
|
|
1044
|
-
*
|
|
1045
|
-
* - 텍스트 광고 전용 페이드 인/아웃
|
|
1046
|
-
* -
|
|
1111
|
+
* 텍스트 전환 효과 관리 클래스
|
|
1112
|
+
* - 텍스트 광고 전용 페이드 인/아웃 + 상하 움직임 효과
|
|
1113
|
+
* - 부드러운 전환 애니메이션 (vertical transition)
|
|
1047
1114
|
* - 무한 루프 지원
|
|
1048
1115
|
*/
|
|
1049
|
-
class
|
|
1116
|
+
class TextTransitionManager {
|
|
1050
1117
|
/**
|
|
1051
|
-
*
|
|
1118
|
+
* 텍스트 전환 슬라이더 컨테이너 생성
|
|
1052
1119
|
*/
|
|
1053
|
-
static
|
|
1120
|
+
static createTextTransitionContainer(slot, advertisements, options, trackEventCallback) {
|
|
1054
1121
|
const sliderWrapper = document.createElement('div');
|
|
1055
1122
|
sliderWrapper.className = 'adstage-fade-slider-wrapper';
|
|
1056
1123
|
// 래퍼 스타일 설정
|
|
@@ -1219,7 +1286,7 @@ class FadeSliderManager {
|
|
|
1219
1286
|
}, autoSlideInterval);
|
|
1220
1287
|
});
|
|
1221
1288
|
// 터치 제스처 지원
|
|
1222
|
-
|
|
1289
|
+
TextTransitionManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
|
|
1223
1290
|
// 요소들 조립
|
|
1224
1291
|
sliderWrapper.appendChild(slideContainer);
|
|
1225
1292
|
return sliderWrapper;
|
|
@@ -1439,6 +1506,46 @@ class DeviceInfoCollector {
|
|
|
1439
1506
|
}
|
|
1440
1507
|
}
|
|
1441
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
|
+
|
|
1442
1549
|
/**
|
|
1443
1550
|
* 이벤트 추적 관리 클래스
|
|
1444
1551
|
* - 광고 이벤트 추적 및 전송
|
|
@@ -1514,10 +1621,7 @@ class EventTracker {
|
|
|
1514
1621
|
};
|
|
1515
1622
|
await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
|
|
1516
1623
|
method: 'POST',
|
|
1517
|
-
headers:
|
|
1518
|
-
'x-api-key': this.apiKey,
|
|
1519
|
-
'Content-Type': 'application/json',
|
|
1520
|
-
},
|
|
1624
|
+
headers: ApiHeaders.createForEvents(this.apiKey, eventData),
|
|
1521
1625
|
body: JSON.stringify(eventData),
|
|
1522
1626
|
});
|
|
1523
1627
|
if (this.debug) {
|
|
@@ -1545,558 +1649,791 @@ class EventTracker {
|
|
|
1545
1649
|
}
|
|
1546
1650
|
|
|
1547
1651
|
/**
|
|
1548
|
-
* SDK
|
|
1549
|
-
*
|
|
1550
|
-
* - 자동 슬롯 검색
|
|
1551
|
-
* - 기타 헬퍼 함수들
|
|
1652
|
+
* AdStage SDK 엔드포인트 상수 관리
|
|
1653
|
+
* 모든 API URL을 중앙에서 관리
|
|
1552
1654
|
*/
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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;
|
|
1571
1700
|
}
|
|
1572
1701
|
/**
|
|
1573
|
-
*
|
|
1702
|
+
* 기본 URL 변경
|
|
1574
1703
|
*/
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const elements = document.querySelectorAll('[data-adstage], [adstage]');
|
|
1579
|
-
return Array.from(elements);
|
|
1580
|
-
}
|
|
1581
|
-
/**
|
|
1582
|
-
* 요소에서 슬롯 정보 추출
|
|
1583
|
-
*/
|
|
1584
|
-
static extractSlotInfo(element) {
|
|
1585
|
-
// 슬롯 ID 가져오기
|
|
1586
|
-
const slotId = SDKUtils.getElementAttribute(element, 'adstage');
|
|
1587
|
-
// 광고 타입 가져오기
|
|
1588
|
-
const adType = SDKUtils.getElementAttribute(element, 'adType') ||
|
|
1589
|
-
SDKUtils.getElementAttribute(element, 'ad-type') ||
|
|
1590
|
-
'BANNER';
|
|
1591
|
-
// 크기 정보 가져오기 (다양한 형태 지원)
|
|
1592
|
-
const width = SDKUtils.getElementAttribute(element, 'width');
|
|
1593
|
-
const height = SDKUtils.getElementAttribute(element, 'height');
|
|
1594
|
-
// 기타 옵션들
|
|
1595
|
-
const language = SDKUtils.getElementAttribute(element, 'language');
|
|
1596
|
-
const deviceType = SDKUtils.getElementAttribute(element, 'deviceType') ||
|
|
1597
|
-
SDKUtils.getElementAttribute(element, 'device-type');
|
|
1598
|
-
const country = SDKUtils.getElementAttribute(element, 'country');
|
|
1599
|
-
const sliderEffect = SDKUtils.getElementAttribute(element, 'sliderEffect') ||
|
|
1600
|
-
SDKUtils.getElementAttribute(element, 'slider-effect');
|
|
1601
|
-
return {
|
|
1602
|
-
slotId: slotId || null,
|
|
1603
|
-
adType,
|
|
1604
|
-
width,
|
|
1605
|
-
height,
|
|
1606
|
-
language,
|
|
1607
|
-
deviceType,
|
|
1608
|
-
country,
|
|
1609
|
-
sliderEffect,
|
|
1610
|
-
};
|
|
1704
|
+
setBaseUrl(url) {
|
|
1705
|
+
this.baseUrl = url;
|
|
1706
|
+
console.log('🔄 API endpoint changed:', url);
|
|
1611
1707
|
}
|
|
1612
1708
|
/**
|
|
1613
|
-
*
|
|
1709
|
+
* 기본 URL 반환
|
|
1614
1710
|
*/
|
|
1615
|
-
|
|
1616
|
-
return
|
|
1711
|
+
getBaseUrl() {
|
|
1712
|
+
return this.baseUrl;
|
|
1617
1713
|
}
|
|
1618
1714
|
/**
|
|
1619
|
-
*
|
|
1715
|
+
* 커스텀 경로 생성
|
|
1620
1716
|
*/
|
|
1621
|
-
|
|
1622
|
-
return
|
|
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;
|
|
1623
1736
|
}
|
|
1624
1737
|
/**
|
|
1625
|
-
*
|
|
1738
|
+
* Ads 모듈 초기화 (동기)
|
|
1626
1739
|
*/
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
+
}
|
|
1629
1748
|
}
|
|
1630
1749
|
/**
|
|
1631
|
-
*
|
|
1750
|
+
* 모듈 준비 상태 확인
|
|
1632
1751
|
*/
|
|
1633
|
-
|
|
1634
|
-
return
|
|
1752
|
+
isReady() {
|
|
1753
|
+
return this._isReady;
|
|
1635
1754
|
}
|
|
1636
1755
|
/**
|
|
1637
|
-
*
|
|
1756
|
+
* 모듈 설정 반환
|
|
1638
1757
|
*/
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
return false;
|
|
1642
|
-
return document.readyState !== 'loading';
|
|
1758
|
+
getConfig() {
|
|
1759
|
+
return this._config;
|
|
1643
1760
|
}
|
|
1644
1761
|
/**
|
|
1645
|
-
*
|
|
1762
|
+
* 배너 광고 생성 (동기)
|
|
1646
1763
|
*/
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
document.addEventListener('DOMContentLoaded', () => resolve());
|
|
1658
|
-
}
|
|
1659
|
-
});
|
|
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);
|
|
1660
1774
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
async function initAdStage(config) {
|
|
1673
|
-
if (globalSDKInstance) {
|
|
1674
|
-
console.warn('AdStage SDK가 이미 초기화되었습니다.');
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
if (isInitializing) {
|
|
1678
|
-
console.warn('AdStage SDK 초기화가 진행 중입니다.');
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
isInitializing = true;
|
|
1682
|
-
try {
|
|
1683
|
-
// 동적 import로 circular dependency 방지
|
|
1684
|
-
const { AdStageSDK } = await Promise.resolve().then(function () { return index; });
|
|
1685
|
-
globalSDKInstance = AdStageSDK.init({
|
|
1686
|
-
apiKey: config.apiKey,
|
|
1687
|
-
debug: config.debug || false
|
|
1688
|
-
});
|
|
1689
|
-
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);
|
|
1690
1786
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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);
|
|
1694
1800
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1801
|
+
/**
|
|
1802
|
+
* 네이티브 광고 생성 (동기)
|
|
1803
|
+
*/
|
|
1804
|
+
native(containerId, options) {
|
|
1805
|
+
this.ensureReady();
|
|
1806
|
+
return this.createAd(containerId, AdType.NATIVE, options || {});
|
|
1697
1807
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다. initAdStage()를 먼저 호출하세요.');
|
|
1705
|
-
}
|
|
1706
|
-
const slotId = `banner-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1707
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'BANNER', {
|
|
1708
|
-
width: options?.width || '100%',
|
|
1709
|
-
height: options?.height || 120,
|
|
1710
|
-
autoSlideInterval: options?.autoSlide ? (options.slideInterval || 5) : 0,
|
|
1711
|
-
sliderEffect: 'fade'
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
/**
|
|
1715
|
-
* 텍스트 광고 생성
|
|
1716
|
-
*/
|
|
1717
|
-
async function createTextAd(containerId, options) {
|
|
1718
|
-
if (!globalSDKInstance) {
|
|
1719
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다.');
|
|
1720
|
-
}
|
|
1721
|
-
const slotId = `text-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1722
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'TEXT', {
|
|
1723
|
-
maxLines: options?.maxLines || 3,
|
|
1724
|
-
style: options?.style || 'card'
|
|
1725
|
-
});
|
|
1726
|
-
}
|
|
1727
|
-
/**
|
|
1728
|
-
* 비디오 광고 생성
|
|
1729
|
-
*/
|
|
1730
|
-
async function createVideoAd(containerId, options) {
|
|
1731
|
-
if (!globalSDKInstance) {
|
|
1732
|
-
throw new Error('AdStage SDK가 초기화되지 않았습니다.');
|
|
1733
|
-
}
|
|
1734
|
-
const slotId = `video-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1735
|
-
return globalSDKInstance.createSlot(slotId, containerId, 'VIDEO', {
|
|
1736
|
-
width: options?.width || '100%',
|
|
1737
|
-
height: options?.height || 300,
|
|
1738
|
-
autoplay: options?.autoplay || false,
|
|
1739
|
-
muted: options?.muted || true
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
/**
|
|
1743
|
-
* SDK 상태 확인
|
|
1744
|
-
*/
|
|
1745
|
-
function isAdStageReady() {
|
|
1746
|
-
return globalSDKInstance !== null && !isInitializing;
|
|
1747
|
-
}
|
|
1748
|
-
/**
|
|
1749
|
-
* SDK 인스턴스 가져오기 (고급 사용자용)
|
|
1750
|
-
*/
|
|
1751
|
-
function getAdStageInstance() {
|
|
1752
|
-
if (!globalSDKInstance) {
|
|
1753
|
-
console.warn('AdStage SDK가 초기화되지 않았습니다.');
|
|
1754
|
-
return null;
|
|
1808
|
+
/**
|
|
1809
|
+
* 전면 광고 생성 (동기)
|
|
1810
|
+
*/
|
|
1811
|
+
interstitial(containerId, options) {
|
|
1812
|
+
this.ensureReady();
|
|
1813
|
+
return this.createAd(containerId, AdType.INTERSTITIAL, options || {});
|
|
1755
1814
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
// SDK cleanup logic here
|
|
1765
|
-
globalSDKInstance = null;
|
|
1766
|
-
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}`);
|
|
1767
1823
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1824
|
+
// 광고 새로고침 로직
|
|
1825
|
+
this.refreshAdSlot(slot);
|
|
1826
|
+
if (this._config?.debug) {
|
|
1827
|
+
console.log(`🔄 Ad slot refreshed: ${slotId}`);
|
|
1770
1828
|
}
|
|
1771
1829
|
}
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
/**
|
|
1775
|
-
* AdStage SDK 메인 클래스
|
|
1776
|
-
* - 간단한 API Key 기반 초기화
|
|
1777
|
-
* - 광고 슬롯 자동 관리
|
|
1778
|
-
* - 이벤트 자동 추적
|
|
1779
|
-
*/
|
|
1780
|
-
class AdStageSDK {
|
|
1781
|
-
constructor(config) {
|
|
1782
|
-
this.baseUrl = 'https://beta-api.adstage.app';
|
|
1783
|
-
this.slots = new Map();
|
|
1784
|
-
this.initialized = false;
|
|
1785
|
-
this.config = {
|
|
1786
|
-
debug: false,
|
|
1787
|
-
...config,
|
|
1788
|
-
};
|
|
1789
|
-
this.eventTracker = new EventTracker(this.baseUrl, this.config.apiKey, this.config.debug || false, this.slots);
|
|
1790
|
-
}
|
|
1791
1830
|
/**
|
|
1792
|
-
*
|
|
1831
|
+
* 광고 제거
|
|
1793
1832
|
*/
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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}`);
|
|
1797
1848
|
}
|
|
1798
|
-
return AdStageSDK.instance;
|
|
1799
1849
|
}
|
|
1800
1850
|
/**
|
|
1801
|
-
*
|
|
1851
|
+
* 모든 광고 슬롯 반환
|
|
1802
1852
|
*/
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
}
|
|
1807
|
-
return AdStageSDK.instance;
|
|
1853
|
+
getAllSlots() {
|
|
1854
|
+
this.ensureReady();
|
|
1855
|
+
return Array.from(this.slots.values());
|
|
1808
1856
|
}
|
|
1809
1857
|
/**
|
|
1810
|
-
* 광고 슬롯
|
|
1858
|
+
* 특정 광고 슬롯 반환
|
|
1811
1859
|
*/
|
|
1812
|
-
|
|
1813
|
-
|
|
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');
|
|
1870
|
+
}
|
|
1871
|
+
const container = document.getElementById(containerId);
|
|
1814
1872
|
if (!container) {
|
|
1815
|
-
|
|
1816
|
-
console.error(`Container with ID "${containerId}" not found`);
|
|
1817
|
-
}
|
|
1818
|
-
return;
|
|
1873
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
1819
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
|
+
// 광고 슬롯 정보 저장
|
|
1820
1880
|
const slot = {
|
|
1821
|
-
id,
|
|
1881
|
+
id: slotId,
|
|
1822
1882
|
containerId,
|
|
1823
|
-
adType,
|
|
1824
|
-
width: options
|
|
1825
|
-
height: options
|
|
1883
|
+
adType: type,
|
|
1884
|
+
width: options.width || '100%',
|
|
1885
|
+
height: options.height || 250,
|
|
1826
1886
|
isLoaded: false,
|
|
1827
1887
|
isVisible: false,
|
|
1828
1888
|
refreshRate: 0,
|
|
1829
1889
|
lazyLoad: false,
|
|
1830
1890
|
targeting: {},
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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)
|
|
1835
1897
|
};
|
|
1836
|
-
|
|
1837
|
-
|
|
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}`);
|
|
1927
|
+
}
|
|
1838
1928
|
}
|
|
1839
1929
|
/**
|
|
1840
|
-
* 광고
|
|
1930
|
+
* 백그라운드에서 광고 콘텐츠 로드
|
|
1841
1931
|
*/
|
|
1842
|
-
async
|
|
1932
|
+
async loadAdContentInBackground(slot) {
|
|
1843
1933
|
try {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
...(options?.country && { country: options.country }),
|
|
1850
|
-
});
|
|
1851
|
-
const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
|
|
1852
|
-
if (this.config.debug) {
|
|
1853
|
-
console.log(`🌐 광고 API 요청 시작:`, {
|
|
1854
|
-
url: requestUrl,
|
|
1855
|
-
apiKey: this.config.apiKey.substring(0, 10) + '...',
|
|
1856
|
-
slot: slot.id
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
const response = await fetch(requestUrl, {
|
|
1860
|
-
headers: {
|
|
1861
|
-
'x-api-key': this.config.apiKey,
|
|
1862
|
-
'Content-Type': 'application/json',
|
|
1863
|
-
},
|
|
1864
|
-
});
|
|
1865
|
-
if (this.config.debug) {
|
|
1866
|
-
console.log(`📡 API 응답 상태:`, {
|
|
1867
|
-
status: response.status,
|
|
1868
|
-
statusText: response.statusText,
|
|
1869
|
-
ok: response.ok
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
if (!response.ok) {
|
|
1873
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1874
|
-
}
|
|
1875
|
-
const data = await response.json();
|
|
1876
|
-
if (this.config.debug) {
|
|
1877
|
-
console.log(`📊 API 응답 데이터:`, {
|
|
1878
|
-
data,
|
|
1879
|
-
advertisementsCount: data.advertisements ? data.advertisements.length : 0
|
|
1880
|
-
});
|
|
1934
|
+
// 광고 데이터 가져오기 - 여러 개 로드
|
|
1935
|
+
const adstageData = await this.fetchAdData(slot.adType, slot.config);
|
|
1936
|
+
if (!adstageData || adstageData.length === 0) {
|
|
1937
|
+
this.renderFallback(slot);
|
|
1938
|
+
return;
|
|
1881
1939
|
}
|
|
1882
|
-
|
|
1883
|
-
if (
|
|
1884
|
-
|
|
1885
|
-
console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
|
|
1886
|
-
}
|
|
1887
|
-
// 여러 광고가 있을 경우 슬라이드로 렌더링
|
|
1888
|
-
this.renderSlotWithSlider(slot, advertisements, options);
|
|
1889
|
-
// 첫 번째 광고에 대해서만 노출 이벤트 추적
|
|
1890
|
-
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);
|
|
1891
1943
|
}
|
|
1892
1944
|
else {
|
|
1893
|
-
|
|
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)`);
|
|
1894
1952
|
}
|
|
1895
1953
|
}
|
|
1896
1954
|
catch (error) {
|
|
1897
|
-
console.error(`❌
|
|
1955
|
+
console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
|
|
1956
|
+
this.renderFallback(slot);
|
|
1898
1957
|
}
|
|
1899
1958
|
}
|
|
1900
1959
|
/**
|
|
1901
|
-
* 광고
|
|
1960
|
+
* Fallback 광고 렌더링
|
|
1902
1961
|
*/
|
|
1903
|
-
|
|
1904
|
-
const
|
|
1905
|
-
if (
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
+
}
|
|
1908
1970
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* 광고 데이터 가져오기
|
|
1974
|
+
*/
|
|
1975
|
+
async fetchAdData(type, options) {
|
|
1976
|
+
if (!this._config?.apiKey) {
|
|
1977
|
+
throw new Error('API key not configured');
|
|
1916
1978
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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}`);
|
|
1921
1991
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
+
}
|
|
1930
2031
|
}
|
|
1931
2032
|
else {
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
+
}
|
|
1934
2040
|
}
|
|
2041
|
+
// 기존 내용 제거하고 슬라이더 추가
|
|
1935
2042
|
container.innerHTML = '';
|
|
1936
|
-
container.appendChild(
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
});
|
|
2043
|
+
container.appendChild(sliderElement);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* 광고 렌더링 (단일 광고용)
|
|
2047
|
+
*/
|
|
2048
|
+
async renderAd(slot) {
|
|
2049
|
+
if (!slot.advertisement) {
|
|
2050
|
+
throw new Error('No advertisement to render');
|
|
1945
2051
|
}
|
|
2052
|
+
await this.renderAdElement(slot, slot.advertisement);
|
|
2053
|
+
slot.isLoaded = true;
|
|
1946
2054
|
}
|
|
1947
2055
|
/**
|
|
1948
|
-
* 광고
|
|
2056
|
+
* 광고 요소 렌더링 (기본 구현)
|
|
1949
2057
|
*/
|
|
1950
|
-
|
|
1951
|
-
const container =
|
|
1952
|
-
if (!container)
|
|
1953
|
-
console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
|
|
2058
|
+
async renderAdElement(slot, ad) {
|
|
2059
|
+
const container = document.getElementById(slot.containerId);
|
|
2060
|
+
if (!container)
|
|
1954
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>`;
|
|
1955
2101
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
ad
|
|
1961
|
-
container: container
|
|
1962
|
-
});
|
|
1963
|
-
}
|
|
1964
|
-
// 팩토리를 사용해서 적절한 렌더러로 광고 생성
|
|
1965
|
-
const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
|
|
1966
|
-
if (this.config.debug) {
|
|
1967
|
-
console.log(`🔧 광고 요소 생성됨:`, {
|
|
1968
|
-
adElement: adElement,
|
|
1969
|
-
tagName: adElement.tagName,
|
|
1970
|
-
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');
|
|
1971
2107
|
});
|
|
1972
2108
|
}
|
|
1973
2109
|
container.innerHTML = '';
|
|
1974
2110
|
container.appendChild(adElement);
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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'
|
|
1981
2177
|
});
|
|
1982
2178
|
}
|
|
1983
2179
|
}
|
|
1984
2180
|
/**
|
|
1985
|
-
*
|
|
2181
|
+
* 모듈 준비 상태 확인
|
|
1986
2182
|
*/
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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);
|
|
1991
2235
|
}
|
|
1992
2236
|
}
|
|
1993
2237
|
/**
|
|
1994
|
-
*
|
|
2238
|
+
* API 헤더 생성 (공통 유틸리티 사용)
|
|
1995
2239
|
*/
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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');
|
|
2002
2266
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
language: slotInfo.language,
|
|
2020
|
-
deviceType: slotInfo.deviceType,
|
|
2021
|
-
country: slotInfo.country,
|
|
2022
|
-
sliderEffect: slotInfo.sliderEffect,
|
|
2023
|
-
});
|
|
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)');
|
|
2024
2283
|
}
|
|
2025
2284
|
}
|
|
2026
2285
|
/**
|
|
2027
|
-
*
|
|
2286
|
+
* 모듈 준비 상태 확인
|
|
2028
2287
|
*/
|
|
2029
|
-
|
|
2030
|
-
this.
|
|
2031
|
-
ImpressionTracker.clear(); // 노출 추적 데이터도 정리
|
|
2032
|
-
this.initialized = false;
|
|
2033
|
-
AdStageSDK.instance = null;
|
|
2288
|
+
isReady() {
|
|
2289
|
+
return this._isReady;
|
|
2034
2290
|
}
|
|
2035
2291
|
/**
|
|
2036
|
-
*
|
|
2292
|
+
* 모듈 설정 반환
|
|
2037
2293
|
*/
|
|
2038
|
-
|
|
2039
|
-
return
|
|
2294
|
+
getConfig() {
|
|
2295
|
+
return this._config;
|
|
2040
2296
|
}
|
|
2297
|
+
// === 향후 구현 예정 메소드들 ===
|
|
2041
2298
|
/**
|
|
2042
|
-
*
|
|
2299
|
+
* 커스텀 이벤트 추적
|
|
2300
|
+
* @example AdStage.events.track('page_view', { page: '/products' })
|
|
2043
2301
|
*/
|
|
2044
|
-
|
|
2045
|
-
|
|
2302
|
+
async track(eventName, properties) {
|
|
2303
|
+
console.log('🚧 [TODO] Event tracking:', { eventName, properties });
|
|
2304
|
+
// TODO: Q1 2025 구현 예정
|
|
2046
2305
|
}
|
|
2047
2306
|
/**
|
|
2048
|
-
*
|
|
2307
|
+
* 페이지 뷰 이벤트
|
|
2308
|
+
* @example AdStage.events.pageView({ page: '/home', title: 'Homepage' })
|
|
2049
2309
|
*/
|
|
2050
|
-
|
|
2051
|
-
|
|
2310
|
+
async pageView(pageData) {
|
|
2311
|
+
console.log('🚧 [TODO] Page view tracking:', pageData);
|
|
2312
|
+
// TODO: Q1 2025 구현 예정
|
|
2052
2313
|
}
|
|
2053
2314
|
/**
|
|
2054
|
-
*
|
|
2315
|
+
* 사용자 액션 이벤트
|
|
2316
|
+
* @example AdStage.events.userAction('button_click', { button_id: 'cta' })
|
|
2055
2317
|
*/
|
|
2056
|
-
|
|
2057
|
-
|
|
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 구현 예정
|
|
2058
2329
|
}
|
|
2059
2330
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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();
|
|
2066
2351
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
+
});
|
|
2069
2384
|
}
|
|
2070
2385
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
// DOM 로드 후 자동 초기화
|
|
2077
|
-
if (DOMUtils.isDOMReady()) {
|
|
2078
|
-
autoInit();
|
|
2386
|
+
/**
|
|
2387
|
+
* SDK 초기화 상태 확인
|
|
2388
|
+
*/
|
|
2389
|
+
static isReady() {
|
|
2390
|
+
return AdStage.instance?._isInitialized || false;
|
|
2079
2391
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2392
|
+
/**
|
|
2393
|
+
* 현재 설정 반환
|
|
2394
|
+
*/
|
|
2395
|
+
static getConfig() {
|
|
2396
|
+
return AdStage.instance?._config || null;
|
|
2397
|
+
}
|
|
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
|
+
}
|
|
2082
2427
|
}
|
|
2083
2428
|
}
|
|
2084
|
-
// React exports (React가 있을 때만 사용 - 레거시)
|
|
2085
|
-
// export * from './react';
|
|
2086
2429
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
default: AdStageSDK,
|
|
2096
|
-
destroyAdStage: destroyAdStage,
|
|
2097
|
-
getAdStageInstance: getAdStageInstance,
|
|
2098
|
-
initAdStage: initAdStage,
|
|
2099
|
-
isAdStageReady: isAdStageReady
|
|
2100
|
-
});
|
|
2430
|
+
/**
|
|
2431
|
+
* AdStage Web SDK
|
|
2432
|
+
* 네임스페이스 아키텍처 기반 SDK
|
|
2433
|
+
*/
|
|
2434
|
+
// 메인 네임스페이스 클래스
|
|
2435
|
+
// 버전 정보
|
|
2436
|
+
const SDK_VERSION = '2.0.0';
|
|
2437
|
+
const SUPPORTED_MODULES = ['ads', 'events', 'config'];
|
|
2101
2438
|
|
|
2102
|
-
export {
|
|
2439
|
+
export { AdStage, SDK_VERSION, SUPPORTED_MODULES };
|