@adstage/web-sdk 1.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs.js +2304 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.esm.js +2288 -0
- package/dist/index.standalone.js +2331 -0
- package/examples/README.md +33 -0
- package/examples/banner-ads.html +512 -0
- package/examples/index.html +338 -0
- package/examples/native-ads.html +634 -0
- package/examples/react-app/README.md +70 -0
- package/examples/react-app/index.html +13 -0
- package/examples/react-app/package-lock.json +3042 -0
- package/examples/react-app/package.json +26 -0
- package/examples/react-app/pnpm-lock.yaml +1857 -0
- package/examples/react-app/public/index.standalone.js +2331 -0
- package/examples/react-app/src/App.tsx +226 -0
- package/examples/react-app/src/index.css +37 -0
- package/examples/react-app/src/main.tsx +10 -0
- package/examples/react-app/tsconfig.json +25 -0
- package/examples/react-app/tsconfig.node.json +10 -0
- package/examples/react-app/vite.config.ts +15 -0
- package/examples/react-nextjs/app/globals.css +200 -0
- package/examples/react-nextjs/app/layout.tsx +27 -0
- package/examples/react-nextjs/app/page.tsx +258 -0
- package/examples/react-nextjs/next.config.js +9 -0
- package/examples/react-nextjs/package.json +22 -0
- package/examples/react-nextjs/pnpm-lock.yaml +343 -0
- package/examples/react-nextjs/tsconfig.json +34 -0
- package/examples/text-ads.html +597 -0
- package/examples/video-ads.html +739 -0
- package/package.json +83 -0
- package/src/global.d.ts +20 -0
- package/src/index.ts +350 -0
- package/src/managers/device-info-collector.ts +127 -0
- package/src/managers/event-tracker.ts +131 -0
- package/src/managers/fade-slider-manager.ts +276 -0
- package/src/managers/impression-tracker.ts +88 -0
- package/src/managers/slider-manager.ts +405 -0
- package/src/react/components/AdErrorBoundary.tsx +75 -0
- package/src/react/components/AdSlot.tsx +144 -0
- package/src/react/components/BannerAd.tsx +24 -0
- package/src/react/components/InterstitialAd.tsx +24 -0
- package/src/react/components/NativeAd.tsx +24 -0
- package/src/react/components/TextAd.tsx +24 -0
- package/src/react/components/VideoAd.tsx +24 -0
- package/src/react/components/index.ts +8 -0
- package/src/react/hooks/index.ts +4 -0
- package/src/react/hooks/useAdSlot.ts +83 -0
- package/src/react/hooks/useAdStage.ts +14 -0
- package/src/react/hooks/useAdTracking.ts +61 -0
- package/src/react/index.ts +4 -0
- package/src/react/providers/AdStageProvider.tsx +86 -0
- package/src/react/providers/index.ts +2 -0
- package/src/renderers/banner-renderer.ts +35 -0
- package/src/renderers/base-renderer.ts +207 -0
- package/src/renderers/index.ts +71 -0
- package/src/renderers/interstitial-renderer.ts +70 -0
- package/src/renderers/native-renderer.ts +35 -0
- package/src/renderers/text-renderer.ts +94 -0
- package/src/renderers/video-renderer.ts +63 -0
- package/src/types/advertisement.ts +197 -0
- package/src/types/api.ts +173 -0
- package/src/types/config.ts +174 -0
- package/src/types/events.ts +60 -0
- package/src/types/index.ts +6 -0
- package/src/utils/dom-utils.ts +237 -0
- package/src/utils/sdk-utils.ts +134 -0
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adstage/web-sdk",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AdStage Web SDK for displaying advertisements",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs.js",
|
|
7
|
+
"module": "dist/index.esm.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.esm.js",
|
|
13
|
+
"require": "./dist/index.cjs.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"examples",
|
|
20
|
+
"README.md",
|
|
21
|
+
"REACT_GUIDE.md",
|
|
22
|
+
"PUBLISH_GUIDE.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build:types": "tsc --project tsconfig.types.json",
|
|
26
|
+
"build": "npm run build:types && rollup -c",
|
|
27
|
+
"build:watch": "rollup -c -w",
|
|
28
|
+
"dev": "rollup -c -w",
|
|
29
|
+
"clean": "rm -rf dist dist-types",
|
|
30
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
31
|
+
"lint": "tsc --noEmit",
|
|
32
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
33
|
+
"publish:npm": "npm publish --access public",
|
|
34
|
+
"version:patch": "npm version patch",
|
|
35
|
+
"version:minor": "npm version minor",
|
|
36
|
+
"version:major": "npm version major"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"adstage",
|
|
40
|
+
"advertisement",
|
|
41
|
+
"ads",
|
|
42
|
+
"banner",
|
|
43
|
+
"sdk",
|
|
44
|
+
"web",
|
|
45
|
+
"react",
|
|
46
|
+
"hooks",
|
|
47
|
+
"component",
|
|
48
|
+
"typescript",
|
|
49
|
+
"text",
|
|
50
|
+
"native",
|
|
51
|
+
"video",
|
|
52
|
+
"marketing",
|
|
53
|
+
"monetization"
|
|
54
|
+
],
|
|
55
|
+
"author": "AdStage",
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@rollup/plugin-commonjs": "^25.0.0",
|
|
59
|
+
"@rollup/plugin-node-resolve": "^15.1.0",
|
|
60
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
61
|
+
"@rollup/plugin-terser": "^0.4.3",
|
|
62
|
+
"@rollup/plugin-typescript": "^11.1.0",
|
|
63
|
+
"@types/node": "^20.0.0",
|
|
64
|
+
"@types/react": "^18.0.0",
|
|
65
|
+
"react": "^18.0.0",
|
|
66
|
+
"rollup": "^3.23.0",
|
|
67
|
+
"rollup-plugin-dts": "^5.3.0",
|
|
68
|
+
"tslib": "^2.8.1",
|
|
69
|
+
"typescript": "^5.0.0"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"react": ">=16.8.0"
|
|
73
|
+
},
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/nbase-io/adstage.git",
|
|
77
|
+
"directory": "sdk"
|
|
78
|
+
},
|
|
79
|
+
"bugs": {
|
|
80
|
+
"url": "https://github.com/nbase-io/adstage/issues"
|
|
81
|
+
},
|
|
82
|
+
"homepage": "https://github.com/nbase-io/adstage#readme"
|
|
83
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdStage SDK 전역 타입 선언
|
|
3
|
+
*
|
|
4
|
+
* 이 파일을 프로젝트에 포함하면 TypeScript에서 window.AdStageSDK를 인식할 수 있습니다.
|
|
5
|
+
*
|
|
6
|
+
* 사용법:
|
|
7
|
+
* 1. 이 파일을 프로젝트의 types/ 폴더에 복사
|
|
8
|
+
* 2. tsconfig.json의 include에 추가
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AdStageSDK, AdStageConfig } from './index';
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
AdStageSDK: typeof AdStageSDK;
|
|
16
|
+
adstageConfig?: AdStageConfig;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { AdType, AdEventType } from './types/advertisement';
|
|
2
|
+
import type { AdSlot, Advertisement } from './types/advertisement';
|
|
3
|
+
import { AdRendererFactory } from './renderers';
|
|
4
|
+
import { SliderManager } from './managers/slider-manager';
|
|
5
|
+
import { FadeSliderManager } from './managers/fade-slider-manager';
|
|
6
|
+
import { ImpressionTracker } from './managers/impression-tracker';
|
|
7
|
+
import { EventTracker } from './managers/event-tracker';
|
|
8
|
+
import { DeviceInfoCollector } from './managers/device-info-collector';
|
|
9
|
+
import { SDKUtils } from './utils/sdk-utils';
|
|
10
|
+
import { DOMUtils } from './utils/dom-utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AdStage SDK 설정
|
|
14
|
+
*/
|
|
15
|
+
export interface AdStageConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
debug?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AdStage SDK 메인 클래스
|
|
22
|
+
* - 간단한 API Key 기반 초기화
|
|
23
|
+
* - 광고 슬롯 자동 관리
|
|
24
|
+
* - 이벤트 자동 추적
|
|
25
|
+
*/
|
|
26
|
+
export class AdStageSDK {
|
|
27
|
+
private static instance: AdStageSDK | null = null;
|
|
28
|
+
|
|
29
|
+
private config: AdStageConfig;
|
|
30
|
+
private baseUrl = 'https://beta-api.adstage.app';
|
|
31
|
+
private slots = new Map<string, AdSlot>();
|
|
32
|
+
private initialized = false;
|
|
33
|
+
private eventTracker: EventTracker;
|
|
34
|
+
|
|
35
|
+
constructor(config: AdStageConfig) {
|
|
36
|
+
this.config = {
|
|
37
|
+
debug: false,
|
|
38
|
+
...config,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.eventTracker = new EventTracker(
|
|
42
|
+
this.baseUrl,
|
|
43
|
+
this.config.apiKey,
|
|
44
|
+
this.config.debug || false,
|
|
45
|
+
this.slots
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* SDK 초기화 및 인스턴스 반환
|
|
51
|
+
*/
|
|
52
|
+
static init(config: AdStageConfig): AdStageSDK {
|
|
53
|
+
if (!AdStageSDK.instance) {
|
|
54
|
+
AdStageSDK.instance = new AdStageSDK(config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return AdStageSDK.instance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* SDK 인스턴스 반환 (이미 초기화된 경우)
|
|
62
|
+
*/
|
|
63
|
+
static getInstance(): AdStageSDK {
|
|
64
|
+
if (!AdStageSDK.instance) {
|
|
65
|
+
throw new Error('AdStageSDK must be initialized first. Call AdStageSDK.init(config)');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return AdStageSDK.instance;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 광고 슬롯 생성 및 로드
|
|
73
|
+
*/
|
|
74
|
+
async createSlot(
|
|
75
|
+
id: string,
|
|
76
|
+
containerId: string,
|
|
77
|
+
adType: AdType = AdType.BANNER,
|
|
78
|
+
options?: {
|
|
79
|
+
width?: number | string; // 숫자(px) 또는 문자열(%, px 등) 지원
|
|
80
|
+
height?: number | string; // 숫자(px) 또는 문자열(%, px 등) 지원
|
|
81
|
+
language?: string;
|
|
82
|
+
deviceType?: string;
|
|
83
|
+
country?: string;
|
|
84
|
+
autoSlideInterval?: number; // 자동 슬라이드 간격 (초), 기본값: 3초
|
|
85
|
+
sliderEffect?: 'slide' | 'fade'; // 슬라이더 효과 선택 (기본값: slide)
|
|
86
|
+
}
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const container = DOMUtils.safeGetElementById(containerId);
|
|
89
|
+
if (!container) {
|
|
90
|
+
if (DOMUtils.canUseDOM()) {
|
|
91
|
+
console.error(`Container with ID "${containerId}" not found`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const slot: AdSlot = {
|
|
97
|
+
id,
|
|
98
|
+
containerId,
|
|
99
|
+
adType,
|
|
100
|
+
width: options?.width || 0, // 문자열도 지원
|
|
101
|
+
height: options?.height || 0, // 문자열도 지원
|
|
102
|
+
isLoaded: false,
|
|
103
|
+
isVisible: false,
|
|
104
|
+
refreshRate: 0,
|
|
105
|
+
lazyLoad: false,
|
|
106
|
+
targeting: {},
|
|
107
|
+
load: async () => { await this.loadSlot(slot, options); return null; },
|
|
108
|
+
render: (ad: Advertisement) => this.renderSlot(slot, ad),
|
|
109
|
+
refresh: () => this.refreshSlot(slot.id),
|
|
110
|
+
destroy: () => this.destroySlot(slot.id),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.slots.set(id, slot);
|
|
114
|
+
await this.loadSlot(slot, options);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 광고 슬롯 로드
|
|
119
|
+
*/
|
|
120
|
+
private async loadSlot(slot: AdSlot, options?: any): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
const queryParams = new URLSearchParams({
|
|
123
|
+
adType: slot.adType,
|
|
124
|
+
status: 'ACTIVE', // ACTIVE 상태인 광고만 조회
|
|
125
|
+
...(options?.language && { language: options.language }),
|
|
126
|
+
...(options?.deviceType && { deviceType: options.deviceType }),
|
|
127
|
+
...(options?.country && { country: options.country }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const response = await fetch(
|
|
131
|
+
`${this.baseUrl}/advertisements/list?${queryParams}`,
|
|
132
|
+
{
|
|
133
|
+
headers: {
|
|
134
|
+
'x-api-key': this.config.apiKey,
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
const advertisements = data.advertisements || [];
|
|
146
|
+
|
|
147
|
+
if (advertisements.length > 0) {
|
|
148
|
+
// 여러 광고가 있을 경우 슬라이드로 렌더링
|
|
149
|
+
this.renderSlotWithSlider(slot, advertisements, options);
|
|
150
|
+
|
|
151
|
+
// 첫 번째 광고에 대해서만 노출 이벤트 추적
|
|
152
|
+
await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, AdEventType.IMPRESSION);
|
|
153
|
+
} else {
|
|
154
|
+
console.warn(`No advertisements available for slot ${slot.id}`);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`Failed to load slot ${slot.id}:`, error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 광고 슬롯 렌더링 (슬라이더 포함)
|
|
163
|
+
*/
|
|
164
|
+
private renderSlotWithSlider(slot: AdSlot, advertisements: Advertisement[], options?: any): void {
|
|
165
|
+
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
166
|
+
if (!container) return;
|
|
167
|
+
|
|
168
|
+
if (advertisements.length === 1) {
|
|
169
|
+
// 광고가 하나뿐이면 기본 렌더링
|
|
170
|
+
this.renderSlot(slot, advertisements[0]);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 슬라이더 효과 결정 (옵션으로 지정하거나 텍스트 광고인 경우 fade 기본값)
|
|
175
|
+
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
176
|
+
const useFadeEffect = options?.sliderEffect === 'fade' ||
|
|
177
|
+
(options?.sliderEffect !== 'slide' && isAllTextAds);
|
|
178
|
+
|
|
179
|
+
let sliderContainer: HTMLElement;
|
|
180
|
+
|
|
181
|
+
if (useFadeEffect) {
|
|
182
|
+
// 페이드 슬라이더 사용
|
|
183
|
+
sliderContainer = FadeSliderManager.createFadeSliderContainer(
|
|
184
|
+
slot,
|
|
185
|
+
advertisements,
|
|
186
|
+
options,
|
|
187
|
+
(adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType)
|
|
188
|
+
);
|
|
189
|
+
} else {
|
|
190
|
+
// 기본 슬라이더 사용
|
|
191
|
+
sliderContainer = SliderManager.createSliderContainer(
|
|
192
|
+
slot,
|
|
193
|
+
advertisements,
|
|
194
|
+
options,
|
|
195
|
+
(adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
container.innerHTML = '';
|
|
200
|
+
container.appendChild(sliderContainer);
|
|
201
|
+
slot.isLoaded = true;
|
|
202
|
+
|
|
203
|
+
if (this.config.debug) {
|
|
204
|
+
const sliderType = useFadeEffect ? 'fade slider' : 'slider';
|
|
205
|
+
console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 광고 슬롯 렌더링 (단일 광고용)
|
|
211
|
+
*/
|
|
212
|
+
private renderSlot(slot: AdSlot, ad: Advertisement): void {
|
|
213
|
+
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
214
|
+
if (!container) return;
|
|
215
|
+
|
|
216
|
+
// 팩토리를 사용해서 적절한 렌더러로 광고 생성
|
|
217
|
+
const adElement = AdRendererFactory.render(
|
|
218
|
+
ad,
|
|
219
|
+
slot,
|
|
220
|
+
(adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
container.innerHTML = '';
|
|
224
|
+
container.appendChild(adElement);
|
|
225
|
+
slot.isLoaded = true;
|
|
226
|
+
|
|
227
|
+
if (this.config.debug) {
|
|
228
|
+
console.log(`Rendered ad for slot ${slot.id}:`, ad);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 광고 슬롯 새로고침
|
|
234
|
+
*/
|
|
235
|
+
async refreshSlot(slotId: string): Promise<void> {
|
|
236
|
+
const slot = this.slots.get(slotId);
|
|
237
|
+
if (slot) {
|
|
238
|
+
await this.loadSlot(slot);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 광고 슬롯 제거
|
|
244
|
+
*/
|
|
245
|
+
destroySlot(slotId: string): void {
|
|
246
|
+
const slot = this.slots.get(slotId);
|
|
247
|
+
if (slot) {
|
|
248
|
+
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
249
|
+
if (container) {
|
|
250
|
+
DOMUtils.safeSetInnerHTML(container, '');
|
|
251
|
+
}
|
|
252
|
+
this.slots.delete(slotId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 자동 슬롯 검색 및 로드 (분리된 SDKUtils 사용)
|
|
258
|
+
*/
|
|
259
|
+
async autoLoadSlots(): Promise<void> {
|
|
260
|
+
const elements = SDKUtils.findAutoSlotElements();
|
|
261
|
+
|
|
262
|
+
for (const element of elements) {
|
|
263
|
+
const slotInfo = SDKUtils.extractSlotInfo(element);
|
|
264
|
+
|
|
265
|
+
if (!slotInfo.slotId || this.slots.has(slotInfo.slotId)) continue;
|
|
266
|
+
|
|
267
|
+
const adType = SDKUtils.parseAdType(slotInfo.adType, AdType);
|
|
268
|
+
|
|
269
|
+
await this.createSlot(slotInfo.slotId, element.id || slotInfo.slotId, adType, {
|
|
270
|
+
width: slotInfo.width,
|
|
271
|
+
height: slotInfo.height,
|
|
272
|
+
language: slotInfo.language,
|
|
273
|
+
deviceType: slotInfo.deviceType,
|
|
274
|
+
country: slotInfo.country,
|
|
275
|
+
sliderEffect: slotInfo.sliderEffect as 'slide' | 'fade' | undefined,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* SDK 정리
|
|
282
|
+
*/
|
|
283
|
+
destroy(): void {
|
|
284
|
+
this.slots.clear();
|
|
285
|
+
ImpressionTracker.clear(); // 노출 추적 데이터도 정리
|
|
286
|
+
this.initialized = false;
|
|
287
|
+
AdStageSDK.instance = null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 디바이스 ID 가져오기
|
|
292
|
+
*/
|
|
293
|
+
getDeviceId(): string {
|
|
294
|
+
return DeviceInfoCollector.generateDeviceId();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 세션 ID 가져오기
|
|
299
|
+
*/
|
|
300
|
+
getSessionId(): string {
|
|
301
|
+
return DeviceInfoCollector.generateSessionId();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 현재 로드된 슬롯 수 가져오기
|
|
306
|
+
*/
|
|
307
|
+
getLoadedSlotCount(): number {
|
|
308
|
+
return this.slots.size;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 모든 슬롯 정보 가져오기
|
|
313
|
+
*/
|
|
314
|
+
getAllSlots(): Map<string, AdSlot> {
|
|
315
|
+
return new Map(this.slots);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function autoInit() {
|
|
320
|
+
if (DOMUtils.isBrowser() && (window as any).adstageConfig) {
|
|
321
|
+
try {
|
|
322
|
+
const sdk = AdStageSDK.init((window as any).adstageConfig);
|
|
323
|
+
await sdk.autoLoadSlots();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('Failed to auto-initialize AdStageSDK:', error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 브라우저 환경에서 자동 초기화
|
|
331
|
+
if (DOMUtils.isBrowser()) {
|
|
332
|
+
// 타입 단언을 사용하여 window 객체에 할당
|
|
333
|
+
(window as any).AdStageSDK = AdStageSDK;
|
|
334
|
+
|
|
335
|
+
// DOM 로드 후 자동 초기화
|
|
336
|
+
if (DOMUtils.isDOMReady()) {
|
|
337
|
+
autoInit();
|
|
338
|
+
} else {
|
|
339
|
+
DOMUtils.waitForDOM().then(autoInit);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default AdStageSDK;
|
|
344
|
+
|
|
345
|
+
// Core SDK exports
|
|
346
|
+
export { AdType, AdEventType };
|
|
347
|
+
export type { AdSlot, Advertisement };
|
|
348
|
+
|
|
349
|
+
// React exports (통합)
|
|
350
|
+
export * from './react';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { DOMUtils } from '../utils/dom-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 디바이스 정보 수집 클래스
|
|
5
|
+
* - 브라우저 환경 정보 수집
|
|
6
|
+
* - 디바이스 ID 생성 및 관리
|
|
7
|
+
* - 세션 ID 생성 및 관리
|
|
8
|
+
*/
|
|
9
|
+
export class DeviceInfoCollector {
|
|
10
|
+
/**
|
|
11
|
+
* 디바이스 ID 생성 및 반환 (SSR 안전)
|
|
12
|
+
*/
|
|
13
|
+
static generateDeviceId(): string {
|
|
14
|
+
if (!DOMUtils.isBrowser()) return 'ssr_device_' + Date.now();
|
|
15
|
+
|
|
16
|
+
const stored = localStorage.getItem('adstage_device_id');
|
|
17
|
+
if (stored) return stored;
|
|
18
|
+
|
|
19
|
+
const deviceId = 'device_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
20
|
+
localStorage.setItem('adstage_device_id', deviceId);
|
|
21
|
+
return deviceId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 세션 ID 생성 및 반환 (SSR 안전)
|
|
26
|
+
*/
|
|
27
|
+
static generateSessionId(): string {
|
|
28
|
+
if (!DOMUtils.isBrowser()) return 'ssr_session_' + Date.now();
|
|
29
|
+
|
|
30
|
+
const stored = sessionStorage.getItem('adstage_session_id');
|
|
31
|
+
if (stored) return stored;
|
|
32
|
+
|
|
33
|
+
const sessionId = 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
34
|
+
sessionStorage.setItem('adstage_session_id', sessionId);
|
|
35
|
+
return sessionId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 모바일 디바이스 여부 확인
|
|
40
|
+
*/
|
|
41
|
+
static isMobile(): boolean {
|
|
42
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 플랫폼 타입 반환 (서버 enum에 맞춤)
|
|
47
|
+
*/
|
|
48
|
+
static getPlatform(): 'ios' | 'android' | 'web' | 'desktop' {
|
|
49
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (/iphone|ipad|ipod/.test(userAgent)) {
|
|
52
|
+
return 'ios';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (/android/.test(userAgent)) {
|
|
56
|
+
return 'android';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (DeviceInfoCollector.isMobile()) {
|
|
60
|
+
return 'web'; // 모바일 웹
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return 'desktop'; // 데스크톱 웹
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 완전한 디바이스 정보 수집
|
|
68
|
+
*/
|
|
69
|
+
static collectDeviceInfo(): {
|
|
70
|
+
deviceId: string;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
osVersion: string;
|
|
73
|
+
deviceModel: string;
|
|
74
|
+
appVersion: string;
|
|
75
|
+
sdkVersion: string;
|
|
76
|
+
language: string;
|
|
77
|
+
country: string;
|
|
78
|
+
ipAddress: string;
|
|
79
|
+
userAgent: string;
|
|
80
|
+
timezone: string;
|
|
81
|
+
viewportWidth: number;
|
|
82
|
+
viewportHeight: number;
|
|
83
|
+
screenWidth: number;
|
|
84
|
+
screenHeight: number;
|
|
85
|
+
colorDepth: number;
|
|
86
|
+
pixelRatio: number;
|
|
87
|
+
connectionType: string;
|
|
88
|
+
platform: 'ios' | 'android' | 'web' | 'desktop';
|
|
89
|
+
} {
|
|
90
|
+
const viewportInfo = DOMUtils.getViewportInfo();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
deviceId: DeviceInfoCollector.generateDeviceId(),
|
|
94
|
+
sessionId: DeviceInfoCollector.generateSessionId(),
|
|
95
|
+
osVersion: DOMUtils.isBrowser() ? navigator.platform : 'SSR',
|
|
96
|
+
deviceModel: DOMUtils.isBrowser() ? navigator.platform : 'SSR',
|
|
97
|
+
appVersion: '1.0.0',
|
|
98
|
+
sdkVersion: '1.0.0',
|
|
99
|
+
language: DOMUtils.isBrowser() ? (navigator.language || 'ko') : 'ko',
|
|
100
|
+
country: 'KR', // 기본값
|
|
101
|
+
ipAddress: '', // 서버에서 자동으로 설정됨
|
|
102
|
+
userAgent: DOMUtils.isBrowser() ? navigator.userAgent : 'SSR',
|
|
103
|
+
timezone: DOMUtils.isBrowser() ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'UTC',
|
|
104
|
+
viewportWidth: viewportInfo.width,
|
|
105
|
+
viewportHeight: viewportInfo.height,
|
|
106
|
+
screenWidth: DOMUtils.isBrowser() ? screen.width : 0,
|
|
107
|
+
screenHeight: DOMUtils.isBrowser() ? screen.height : 0,
|
|
108
|
+
colorDepth: DOMUtils.isBrowser() ? screen.colorDepth : 24,
|
|
109
|
+
pixelRatio: viewportInfo.pixelRatio,
|
|
110
|
+
connectionType: DOMUtils.isBrowser() ? ((navigator as any).connection?.effectiveType || 'unknown') : 'unknown',
|
|
111
|
+
platform: DeviceInfoCollector.getPlatform(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 슬롯 위치 정보 가져오기 (SSR 안전)
|
|
117
|
+
*/
|
|
118
|
+
static getSlotPosition(containerId: string): string {
|
|
119
|
+
const element = DOMUtils.safeGetElementById(containerId);
|
|
120
|
+
if (!element) return 'unknown';
|
|
121
|
+
|
|
122
|
+
const rect = element.getBoundingClientRect();
|
|
123
|
+
const scrollInfo = DOMUtils.getScrollInfo();
|
|
124
|
+
|
|
125
|
+
return `x:${Math.round(rect.left)},y:${Math.round(rect.top + scrollInfo.scrollTop)}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AdEventType } from '../types/advertisement';
|
|
2
|
+
import type { AdSlot } from '../types/advertisement';
|
|
3
|
+
import { ImpressionTracker } from './impression-tracker';
|
|
4
|
+
import { DeviceInfoCollector } from './device-info-collector';
|
|
5
|
+
import { DOMUtils } from '../utils/dom-utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 이벤트 추적 관리 클래스
|
|
9
|
+
* - 광고 이벤트 추적 및 전송
|
|
10
|
+
* - 중복 노출 방지 통합
|
|
11
|
+
* - 서버 API 통신
|
|
12
|
+
*/
|
|
13
|
+
export class EventTracker {
|
|
14
|
+
private baseUrl: string;
|
|
15
|
+
private apiKey: string;
|
|
16
|
+
private debug: boolean;
|
|
17
|
+
private slots: Map<string, AdSlot>;
|
|
18
|
+
|
|
19
|
+
constructor(baseUrl: string, apiKey: string, debug: boolean, slots: Map<string, AdSlot>) {
|
|
20
|
+
this.baseUrl = baseUrl;
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
this.debug = debug;
|
|
23
|
+
this.slots = slots;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 이벤트 추적
|
|
28
|
+
*/
|
|
29
|
+
async trackEvent(adId: string, slotId: string, eventType: AdEventType): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
// 노출 이벤트의 경우 중복 확인
|
|
32
|
+
if (eventType === AdEventType.IMPRESSION) {
|
|
33
|
+
if (ImpressionTracker.isDuplicateImpression(adId, slotId, this.debug)) {
|
|
34
|
+
return; // 중복 노출이므로 추적하지 않음
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 현재 슬롯 정보 가져오기
|
|
39
|
+
const slot = this.slots.get(slotId);
|
|
40
|
+
|
|
41
|
+
// 디바이스 정보 수집
|
|
42
|
+
const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
|
|
43
|
+
|
|
44
|
+
// 이벤트 데이터 구성 (MongoDB 스키마에 맞춤)
|
|
45
|
+
const eventData = {
|
|
46
|
+
// 서버에서 자동 설정: orgId, advertisementId, action
|
|
47
|
+
// 하지만 DTO 검증을 위해 임시값 제공
|
|
48
|
+
orgId: 'temp', // 서버에서 API 키로부터 덮어씀
|
|
49
|
+
advertisementId: adId, // 서버에서 URL 파라미터로부터 덮어씀
|
|
50
|
+
action: eventType, // 서버에서 URL 파라미터로부터 덮어씀
|
|
51
|
+
|
|
52
|
+
// 필수 필드들
|
|
53
|
+
adType: slot?.adType || 'BANNER',
|
|
54
|
+
platform: deviceInfo.platform,
|
|
55
|
+
|
|
56
|
+
// 디바이스 정보를 최상위로 플래튼
|
|
57
|
+
deviceId: deviceInfo.deviceId,
|
|
58
|
+
osVersion: deviceInfo.osVersion,
|
|
59
|
+
deviceModel: deviceInfo.deviceModel,
|
|
60
|
+
appVersion: deviceInfo.appVersion,
|
|
61
|
+
sdkVersion: deviceInfo.sdkVersion,
|
|
62
|
+
language: deviceInfo.language,
|
|
63
|
+
country: deviceInfo.country,
|
|
64
|
+
ipAddress: deviceInfo.ipAddress,
|
|
65
|
+
userAgent: deviceInfo.userAgent,
|
|
66
|
+
timezone: deviceInfo.timezone,
|
|
67
|
+
viewportWidth: deviceInfo.viewportWidth,
|
|
68
|
+
viewportHeight: deviceInfo.viewportHeight,
|
|
69
|
+
screenWidth: deviceInfo.screenWidth,
|
|
70
|
+
screenHeight: deviceInfo.screenHeight,
|
|
71
|
+
connectionType: deviceInfo.connectionType,
|
|
72
|
+
|
|
73
|
+
// 페이지 및 슬롯 정보
|
|
74
|
+
pageUrl: DOMUtils.getPageInfo().url,
|
|
75
|
+
pageTitle: DOMUtils.getPageInfo().title,
|
|
76
|
+
referrer: DOMUtils.getPageInfo().referrer,
|
|
77
|
+
slotId,
|
|
78
|
+
slotPosition: DeviceInfoCollector.getSlotPosition(slot?.containerId || ''),
|
|
79
|
+
slotWidth: EventTracker.parseNumericValue(slot?.width),
|
|
80
|
+
slotHeight: EventTracker.parseNumericValue(slot?.height),
|
|
81
|
+
sessionId: deviceInfo.sessionId,
|
|
82
|
+
|
|
83
|
+
// 성능 메트릭
|
|
84
|
+
pageLoadTime: performance.now(),
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
|
|
87
|
+
// 추가 메타데이터
|
|
88
|
+
metadata: {
|
|
89
|
+
eventType,
|
|
90
|
+
sdkVersion: '1.0.0',
|
|
91
|
+
timestamp: Date.now(),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await fetch(
|
|
96
|
+
`${this.baseUrl}/advertisements/events/${adId}/${eventType}`,
|
|
97
|
+
{
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'x-api-key': this.apiKey,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(eventData),
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (this.debug) {
|
|
108
|
+
console.log(`Tracked event: ${eventType} for ad ${adId}`, eventData);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Failed to track event:', error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 크기 값을 숫자로 변환 (서버 API용)
|
|
117
|
+
*/
|
|
118
|
+
private static parseNumericValue(value: number | string | undefined): number {
|
|
119
|
+
if (typeof value === 'number') {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof value === 'string') {
|
|
124
|
+
// px 단위 제거하고 숫자만 추출
|
|
125
|
+
const numericValue = parseFloat(value.replace(/px$/, ''));
|
|
126
|
+
return isNaN(numericValue) ? 0 : numericValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return 0; // 기본값
|
|
130
|
+
}
|
|
131
|
+
}
|