@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
|
@@ -3,6 +3,7 @@ import type { AdSlot } from '../types/advertisement';
|
|
|
3
3
|
import { ImpressionTracker } from './impression-tracker';
|
|
4
4
|
import { DeviceInfoCollector } from './device-info-collector';
|
|
5
5
|
import { DOMUtils } from '../utils/dom-utils';
|
|
6
|
+
import { ApiHeaders } from '../utils/api-headers';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 이벤트 추적 관리 클래스
|
|
@@ -96,10 +97,7 @@ export class EventTracker {
|
|
|
96
97
|
`${this.baseUrl}/advertisements/events/${adId}/${eventType}`,
|
|
97
98
|
{
|
|
98
99
|
method: 'POST',
|
|
99
|
-
headers:
|
|
100
|
-
'x-api-key': this.apiKey,
|
|
101
|
-
'Content-Type': 'application/json',
|
|
102
|
-
},
|
|
100
|
+
headers: ApiHeaders.createForEvents(this.apiKey, eventData),
|
|
103
101
|
body: JSON.stringify(eventData),
|
|
104
102
|
}
|
|
105
103
|
);
|
|
@@ -3,16 +3,16 @@ import type { AdSlot, Advertisement } from '../types/advertisement';
|
|
|
3
3
|
import { AdRendererFactory } from '../renderers';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* - 텍스트 광고 전용 페이드 인/아웃
|
|
8
|
-
* -
|
|
6
|
+
* 텍스트 전환 효과 관리 클래스
|
|
7
|
+
* - 텍스트 광고 전용 페이드 인/아웃 + 상하 움직임 효과
|
|
8
|
+
* - 부드러운 전환 애니메이션 (vertical transition)
|
|
9
9
|
* - 무한 루프 지원
|
|
10
10
|
*/
|
|
11
|
-
export class
|
|
11
|
+
export class TextTransitionManager {
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 텍스트 전환 슬라이더 컨테이너 생성
|
|
14
14
|
*/
|
|
15
|
-
static
|
|
15
|
+
static createTextTransitionContainer(
|
|
16
16
|
slot: AdSlot,
|
|
17
17
|
advertisements: Advertisement[],
|
|
18
18
|
options: any,
|
|
@@ -219,7 +219,7 @@ export class FadeSliderManager {
|
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
// 터치 제스처 지원
|
|
222
|
-
|
|
222
|
+
TextTransitionManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
|
|
223
223
|
|
|
224
224
|
// 요소들 조립
|
|
225
225
|
sliderWrapper.appendChild(slideContainer);
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdStage SDK - Ads 모듈
|
|
3
|
+
* 광고 관리 및 렌더링 기능
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AdStageConfig, BaseModule } from '../../types/config';
|
|
7
|
+
import { AdType, AdEventType } from '../../types/advertisement';
|
|
8
|
+
import type { AdSlot, Advertisement } from '../../types/advertisement';
|
|
9
|
+
import { CarouselSliderManager } from '../../managers/carousel-slider-manager';
|
|
10
|
+
import { TextTransitionManager } from '../../managers/text-transition-manager';
|
|
11
|
+
import { ImpressionTracker } from '../../managers/impression-tracker';
|
|
12
|
+
import { EventTracker } from '../../managers/event-tracker';
|
|
13
|
+
import { endpoints } from '../../constants/endpoints';
|
|
14
|
+
import { ApiHeaders } from '../../utils/api-headers';
|
|
15
|
+
|
|
16
|
+
export interface AdOptions {
|
|
17
|
+
width?: string | number;
|
|
18
|
+
height?: number;
|
|
19
|
+
autoSlide?: boolean;
|
|
20
|
+
slideInterval?: number;
|
|
21
|
+
maxLines?: number;
|
|
22
|
+
style?: string;
|
|
23
|
+
autoplay?: boolean;
|
|
24
|
+
muted?: boolean;
|
|
25
|
+
onClick?: (adData: any) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class AdsModule implements BaseModule {
|
|
29
|
+
private _isReady = false;
|
|
30
|
+
private _config: AdStageConfig | null = null;
|
|
31
|
+
private slots = new Map<string, AdSlot>();
|
|
32
|
+
private eventTracker: EventTracker | null = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ads 모듈 초기화 (동기)
|
|
36
|
+
*/
|
|
37
|
+
init(config: AdStageConfig): void {
|
|
38
|
+
this._config = config;
|
|
39
|
+
|
|
40
|
+
// EventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
|
|
41
|
+
this.eventTracker = new EventTracker(
|
|
42
|
+
endpoints.getBaseUrl(),
|
|
43
|
+
config.apiKey,
|
|
44
|
+
config.debug || false,
|
|
45
|
+
this.slots
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this._isReady = true;
|
|
49
|
+
|
|
50
|
+
if (config.debug) {
|
|
51
|
+
console.log('🎯 Ads module initialized (sync mode)');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 모듈 준비 상태 확인
|
|
57
|
+
*/
|
|
58
|
+
isReady(): boolean {
|
|
59
|
+
return this._isReady;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 모듈 설정 반환
|
|
64
|
+
*/
|
|
65
|
+
getConfig(): AdStageConfig | null {
|
|
66
|
+
return this._config;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 배너 광고 생성 (동기)
|
|
71
|
+
*/
|
|
72
|
+
banner(containerId: string, options?: AdOptions): string {
|
|
73
|
+
this.ensureReady();
|
|
74
|
+
|
|
75
|
+
const adstageOptions = {
|
|
76
|
+
width: options?.width || '100%',
|
|
77
|
+
height: options?.height || 250,
|
|
78
|
+
autoSlide: options?.autoSlide || false,
|
|
79
|
+
slideInterval: options?.slideInterval || 5000,
|
|
80
|
+
onClick: options?.onClick
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return this.createAd(containerId, AdType.BANNER, adstageOptions);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 텍스트 광고 생성 (동기)
|
|
88
|
+
*/
|
|
89
|
+
text(containerId: string, options?: AdOptions): string {
|
|
90
|
+
this.ensureReady();
|
|
91
|
+
|
|
92
|
+
const adstageOptions = {
|
|
93
|
+
maxLines: options?.maxLines || 3,
|
|
94
|
+
style: options?.style || 'default',
|
|
95
|
+
onClick: options?.onClick
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return this.createAd(containerId, AdType.TEXT, adstageOptions);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 비디오 광고 생성 (동기)
|
|
103
|
+
*/
|
|
104
|
+
video(containerId: string, options?: AdOptions): string {
|
|
105
|
+
this.ensureReady();
|
|
106
|
+
|
|
107
|
+
const adstageOptions = {
|
|
108
|
+
width: options?.width || 640,
|
|
109
|
+
height: options?.height || 360,
|
|
110
|
+
autoplay: options?.autoplay || false,
|
|
111
|
+
muted: options?.muted || true,
|
|
112
|
+
onClick: options?.onClick
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return this.createAd(containerId, AdType.VIDEO, adstageOptions);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 네이티브 광고 생성 (동기)
|
|
120
|
+
*/
|
|
121
|
+
native(containerId: string, options?: AdOptions): string {
|
|
122
|
+
this.ensureReady();
|
|
123
|
+
|
|
124
|
+
return this.createAd(containerId, AdType.NATIVE, options || {});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 전면 광고 생성 (동기)
|
|
129
|
+
*/
|
|
130
|
+
interstitial(containerId: string, options?: AdOptions): string {
|
|
131
|
+
this.ensureReady();
|
|
132
|
+
|
|
133
|
+
return this.createAd(containerId, AdType.INTERSTITIAL, options || {});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 광고 새로고침
|
|
138
|
+
*/
|
|
139
|
+
refresh(slotId: string): void {
|
|
140
|
+
this.ensureReady();
|
|
141
|
+
|
|
142
|
+
const slot = this.slots.get(slotId);
|
|
143
|
+
if (!slot) {
|
|
144
|
+
throw new Error(`Ad slot not found: ${slotId}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 광고 새로고침 로직
|
|
148
|
+
this.refreshAdSlot(slot);
|
|
149
|
+
|
|
150
|
+
if (this._config?.debug) {
|
|
151
|
+
console.log(`🔄 Ad slot refreshed: ${slotId}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 광고 제거
|
|
157
|
+
*/
|
|
158
|
+
destroy(slotId: string): void {
|
|
159
|
+
this.ensureReady();
|
|
160
|
+
|
|
161
|
+
const slot = this.slots.get(slotId);
|
|
162
|
+
if (!slot) {
|
|
163
|
+
throw new Error(`Ad slot not found: ${slotId}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// DOM에서 제거
|
|
167
|
+
const container = document.getElementById(slot.containerId);
|
|
168
|
+
if (container) {
|
|
169
|
+
container.innerHTML = '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 슬롯 제거
|
|
173
|
+
this.slots.delete(slotId);
|
|
174
|
+
|
|
175
|
+
if (this._config?.debug) {
|
|
176
|
+
console.log(`🗑️ Ad slot destroyed: ${slotId}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 모든 광고 슬롯 반환
|
|
182
|
+
*/
|
|
183
|
+
getAllSlots(): AdSlot[] {
|
|
184
|
+
this.ensureReady();
|
|
185
|
+
return Array.from(this.slots.values());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 특정 광고 슬롯 반환
|
|
190
|
+
*/
|
|
191
|
+
getSlotById(slotId: string): AdSlot | null {
|
|
192
|
+
this.ensureReady();
|
|
193
|
+
return this.slots.get(slotId) || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 광고 생성 내부 메소드 (동기 + Lazy 로딩)
|
|
198
|
+
*/
|
|
199
|
+
private createAd(containerId: string, type: AdType, options: any): string {
|
|
200
|
+
if (!this._config?.apiKey) {
|
|
201
|
+
throw new Error('API key not configured');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const container = document.getElementById(containerId);
|
|
205
|
+
if (!container) {
|
|
206
|
+
throw new Error(`Container not found: ${containerId}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 고유한 슬롯 ID 생성
|
|
210
|
+
const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
211
|
+
|
|
212
|
+
// 즉시 placeholder 생성
|
|
213
|
+
this.createAdSlot(container, slotId, type, options);
|
|
214
|
+
|
|
215
|
+
// 광고 슬롯 정보 저장
|
|
216
|
+
const slot: AdSlot = {
|
|
217
|
+
id: slotId,
|
|
218
|
+
containerId,
|
|
219
|
+
adType: type,
|
|
220
|
+
width: options.width || '100%',
|
|
221
|
+
height: options.height || 250,
|
|
222
|
+
isLoaded: false,
|
|
223
|
+
isVisible: false,
|
|
224
|
+
refreshRate: 0,
|
|
225
|
+
lazyLoad: false,
|
|
226
|
+
targeting: {},
|
|
227
|
+
advertisement: undefined, // 나중에 로드
|
|
228
|
+
config: { type, ...options },
|
|
229
|
+
load: async () => this.fetchAdData(type, options).then(ads => ads[0] || null),
|
|
230
|
+
render: (ad: Advertisement) => this.renderAdElement(slot, ad),
|
|
231
|
+
refresh: async () => this.refreshAdSlot(slot),
|
|
232
|
+
destroy: () => this.destroy(slotId)
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// 슬롯 저장
|
|
236
|
+
this.slots.set(slotId, slot);
|
|
237
|
+
|
|
238
|
+
// 백그라운드에서 광고 로드
|
|
239
|
+
this.loadAdContentInBackground(slot);
|
|
240
|
+
|
|
241
|
+
// 이벤트 추적 준비
|
|
242
|
+
if (this.eventTracker && this._config?.debug) {
|
|
243
|
+
console.log(`📊 Event tracking enabled for slot: ${slotId}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return slotId;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 즉시 광고 슬롯 생성 (placeholder)
|
|
251
|
+
*/
|
|
252
|
+
private createAdSlot(container: HTMLElement, slotId: string, type: AdType, options: any): void {
|
|
253
|
+
const adElement = document.createElement('div');
|
|
254
|
+
adElement.id = slotId;
|
|
255
|
+
adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
|
|
256
|
+
adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
|
|
257
|
+
adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
|
|
258
|
+
adElement.style.border = '1px dashed #ccc';
|
|
259
|
+
adElement.style.display = 'flex';
|
|
260
|
+
adElement.style.alignItems = 'center';
|
|
261
|
+
adElement.style.justifyContent = 'center';
|
|
262
|
+
adElement.style.backgroundColor = '#f9f9f9';
|
|
263
|
+
adElement.style.color = '#666';
|
|
264
|
+
adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
|
|
265
|
+
|
|
266
|
+
container.appendChild(adElement);
|
|
267
|
+
|
|
268
|
+
if (this._config?.debug) {
|
|
269
|
+
console.log(`📦 Placeholder created for slot: ${slotId}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 백그라운드에서 광고 콘텐츠 로드
|
|
275
|
+
*/
|
|
276
|
+
private async loadAdContentInBackground(slot: AdSlot): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
// 광고 데이터 가져오기 - 여러 개 로드
|
|
279
|
+
const adstageData = await this.fetchAdData(slot.adType, slot.config);
|
|
280
|
+
|
|
281
|
+
if (!adstageData || adstageData.length === 0) {
|
|
282
|
+
this.renderFallback(slot);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
|
|
287
|
+
if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
|
|
288
|
+
await this.renderAdSlider(slot, adstageData);
|
|
289
|
+
} else {
|
|
290
|
+
// 광고가 1개면 일반 렌더링
|
|
291
|
+
slot.advertisement = adstageData[0];
|
|
292
|
+
await this.renderAdElement(slot, adstageData[0]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
slot.isLoaded = true;
|
|
296
|
+
|
|
297
|
+
if (this._config?.debug) {
|
|
298
|
+
console.log(`✅ Ad loaded for slot: ${slot.id} (${adstageData.length} ads)`);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
|
|
302
|
+
this.renderFallback(slot);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Fallback 광고 렌더링
|
|
308
|
+
*/
|
|
309
|
+
private renderFallback(slot: AdSlot): void {
|
|
310
|
+
const element = document.getElementById(slot.id);
|
|
311
|
+
if (element) {
|
|
312
|
+
element.innerHTML = `<span>Ad not available</span>`;
|
|
313
|
+
element.style.color = '#999';
|
|
314
|
+
|
|
315
|
+
if (this._config?.debug) {
|
|
316
|
+
console.warn(`⚠️ Fallback rendered for slot: ${slot.id}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 광고 데이터 가져오기
|
|
323
|
+
*/
|
|
324
|
+
private async fetchAdData(type: AdType, options: any): Promise<Advertisement[]> {
|
|
325
|
+
if (!this._config?.apiKey) {
|
|
326
|
+
throw new Error('API key not configured');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// GET 요청용 query parameters 구성
|
|
330
|
+
const params = new URLSearchParams();
|
|
331
|
+
params.append('adType', type);
|
|
332
|
+
|
|
333
|
+
// userAgent와 url은 header나 자동으로 처리되므로 query에서 제외
|
|
334
|
+
// 기타 옵션들을 필요시 query parameter로 추가 가능
|
|
335
|
+
|
|
336
|
+
const url = `${endpoints.advertisements.list()}?${params.toString()}`;
|
|
337
|
+
|
|
338
|
+
const response = await fetch(url, {
|
|
339
|
+
method: 'GET',
|
|
340
|
+
headers: ApiHeaders.create(this._config.apiKey)
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
throw new Error(`Failed to fetch ad data: ${response.status}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = await response.json();
|
|
348
|
+
return result.advertisements || [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
|
|
353
|
+
*/
|
|
354
|
+
private async renderAdSlider(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
|
|
355
|
+
const container = document.getElementById(slot.containerId);
|
|
356
|
+
if (!container) {
|
|
357
|
+
throw new Error(`Container not found: ${slot.containerId}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
|
|
361
|
+
const trackEventCallback = (adId: string, slotId: string, eventType: AdEventType) => {
|
|
362
|
+
// 노출 이벤트인 경우 중복 확인
|
|
363
|
+
if (eventType === AdEventType.IMPRESSION) {
|
|
364
|
+
if (ImpressionTracker.isDuplicateImpression(adId, slotId, this._config?.debug)) {
|
|
365
|
+
if (this._config?.debug) {
|
|
366
|
+
console.log(`🚫 Duplicate impression blocked for ad ${adId} in slot ${slotId}`);
|
|
367
|
+
}
|
|
368
|
+
return; // 중복 노출이면 추적하지 않음
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (this._config?.debug) {
|
|
372
|
+
console.log(`✅ New impression recorded for ad ${adId} in slot ${slotId}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (this.eventTracker && this._config?.debug) {
|
|
377
|
+
console.log(`📊 Event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
let sliderElement: HTMLElement;
|
|
382
|
+
|
|
383
|
+
// 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
|
|
384
|
+
if (slot.adType === AdType.TEXT) {
|
|
385
|
+
sliderElement = TextTransitionManager.createTextTransitionContainer(
|
|
386
|
+
slot,
|
|
387
|
+
advertisements,
|
|
388
|
+
{
|
|
389
|
+
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
390
|
+
...slot.config
|
|
391
|
+
},
|
|
392
|
+
trackEventCallback
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (this._config?.debug) {
|
|
396
|
+
console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
sliderElement = CarouselSliderManager.createSliderContainer(
|
|
400
|
+
slot,
|
|
401
|
+
advertisements,
|
|
402
|
+
{
|
|
403
|
+
autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
|
|
404
|
+
...slot.config
|
|
405
|
+
},
|
|
406
|
+
trackEventCallback
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (this._config?.debug) {
|
|
410
|
+
console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 기존 내용 제거하고 슬라이더 추가
|
|
415
|
+
container.innerHTML = '';
|
|
416
|
+
container.appendChild(sliderElement);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 광고 렌더링 (단일 광고용)
|
|
421
|
+
*/
|
|
422
|
+
private async renderAd(slot: AdSlot): Promise<void> {
|
|
423
|
+
if (!slot.advertisement) {
|
|
424
|
+
throw new Error('No advertisement to render');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
await this.renderAdElement(slot, slot.advertisement);
|
|
428
|
+
slot.isLoaded = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 광고 요소 렌더링 (기본 구현)
|
|
433
|
+
*/
|
|
434
|
+
private async renderAdElement(slot: AdSlot, ad: Advertisement): Promise<void> {
|
|
435
|
+
const container = document.getElementById(slot.containerId);
|
|
436
|
+
if (!container) return;
|
|
437
|
+
|
|
438
|
+
// 기본 HTML 구조 생성
|
|
439
|
+
const adElement = document.createElement('div');
|
|
440
|
+
adElement.className = 'adstage-ad';
|
|
441
|
+
adElement.style.width = typeof slot.width === 'string' ? slot.width : `${slot.width}px`;
|
|
442
|
+
adElement.style.height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
|
|
443
|
+
|
|
444
|
+
// 광고 타입별 렌더링
|
|
445
|
+
switch (slot.adType) {
|
|
446
|
+
case AdType.BANNER:
|
|
447
|
+
if (ad.imageUrl) {
|
|
448
|
+
const img = document.createElement('img');
|
|
449
|
+
img.src = ad.imageUrl;
|
|
450
|
+
img.alt = ad.title;
|
|
451
|
+
img.style.width = '100%';
|
|
452
|
+
img.style.height = '100%';
|
|
453
|
+
img.style.objectFit = 'cover';
|
|
454
|
+
adElement.appendChild(img);
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case AdType.TEXT:
|
|
459
|
+
const textDiv = document.createElement('div');
|
|
460
|
+
textDiv.innerHTML = `
|
|
461
|
+
<h3>${ad.title}</h3>
|
|
462
|
+
${ad.description ? `<p>${ad.description}</p>` : ''}
|
|
463
|
+
${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
|
|
464
|
+
`;
|
|
465
|
+
adElement.appendChild(textDiv);
|
|
466
|
+
break;
|
|
467
|
+
|
|
468
|
+
case AdType.VIDEO:
|
|
469
|
+
if (ad.videoUrl) {
|
|
470
|
+
const video = document.createElement('video');
|
|
471
|
+
video.src = ad.videoUrl;
|
|
472
|
+
video.controls = true;
|
|
473
|
+
video.style.width = '100%';
|
|
474
|
+
video.style.height = '100%';
|
|
475
|
+
adElement.appendChild(video);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
default:
|
|
480
|
+
adElement.innerHTML = `<div>${ad.title}</div>`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 클릭 이벤트 추가
|
|
484
|
+
if (ad.linkUrl) {
|
|
485
|
+
adElement.style.cursor = 'pointer';
|
|
486
|
+
adElement.addEventListener('click', () => {
|
|
487
|
+
window.open(ad.linkUrl, '_blank');
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
container.innerHTML = '';
|
|
492
|
+
container.appendChild(adElement);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 광고 슬롯 새로고침
|
|
497
|
+
*/
|
|
498
|
+
private async refreshAdSlot(slot: AdSlot): Promise<void> {
|
|
499
|
+
try {
|
|
500
|
+
// 새로운 광고 데이터 가져오기 (config에서 타입과 옵션 정보 사용)
|
|
501
|
+
const newAdData = await this.fetchAdData(slot.adType, slot.config || {});
|
|
502
|
+
|
|
503
|
+
if (newAdData && newAdData.length > 0) {
|
|
504
|
+
slot.advertisement = newAdData[0]; // 첫 번째 광고로 업데이트
|
|
505
|
+
await this.renderAd(slot);
|
|
506
|
+
|
|
507
|
+
// 새로운 노출 추적
|
|
508
|
+
if (this.eventTracker) {
|
|
509
|
+
console.log('New impression tracked for slot:', slot.id);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error(`Failed to refresh ad slot: ${slot.id}`, error);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 모듈 준비 상태 확인
|
|
519
|
+
*/
|
|
520
|
+
private ensureReady(): void {
|
|
521
|
+
if (!this._isReady) {
|
|
522
|
+
throw new Error('Ads module not initialized. Call AdStage.init() first.');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdStage SDK - Config 모듈
|
|
3
|
+
* 설정 관리 및 API 키 검증
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AdStageConfig, BaseModule, ApiResponse, OrganizationInfo } from '../../types/config';
|
|
7
|
+
import { endpoints } from '../../constants/endpoints';
|
|
8
|
+
import { ApiHeaders } from '../../utils/api-headers';
|
|
9
|
+
|
|
10
|
+
export class ConfigModule implements BaseModule {
|
|
11
|
+
private _isReady = false;
|
|
12
|
+
private _config: AdStageConfig | null = null;
|
|
13
|
+
private _organizationInfo: OrganizationInfo | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Config 모듈 초기화 (동기)
|
|
17
|
+
*/
|
|
18
|
+
init(config: AdStageConfig): void {
|
|
19
|
+
// 설정만 저장 (서버 검증 없음)
|
|
20
|
+
this._config = {
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
debug: false,
|
|
23
|
+
modules: ['ads', 'events', 'config'],
|
|
24
|
+
validateOnInit: false,
|
|
25
|
+
fallbackMode: true,
|
|
26
|
+
offlineMode: false,
|
|
27
|
+
productionMode: false,
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 사용자가 baseUrl을 제공한 경우 endpoints에 설정
|
|
32
|
+
if (config.baseUrl) {
|
|
33
|
+
endpoints.setBaseUrl(config.baseUrl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this._isReady = true;
|
|
37
|
+
|
|
38
|
+
if (config.debug) {
|
|
39
|
+
console.log('✅ Config module initialized (sync mode)', {
|
|
40
|
+
modules: this._config.modules,
|
|
41
|
+
endpoint: endpoints.getBaseUrl(),
|
|
42
|
+
mode: config.productionMode ? 'production' : 'development'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 모듈 준비 상태 확인
|
|
49
|
+
*/
|
|
50
|
+
isReady(): boolean {
|
|
51
|
+
return this._isReady;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 현재 설정 반환
|
|
56
|
+
*/
|
|
57
|
+
getConfig(): AdStageConfig | null {
|
|
58
|
+
return this._config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 조직 정보 반환
|
|
63
|
+
*/
|
|
64
|
+
getOrganizationInfo(): OrganizationInfo | null {
|
|
65
|
+
return this._organizationInfo;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* API 엔드포인트 반환
|
|
70
|
+
*/
|
|
71
|
+
getApiEndpoint(): string {
|
|
72
|
+
return endpoints.getBaseUrl();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 디버그 모드 여부 확인
|
|
77
|
+
*/
|
|
78
|
+
isDebugMode(): boolean {
|
|
79
|
+
return this._config?.debug || false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 활성화된 모듈 목록 반환
|
|
84
|
+
*/
|
|
85
|
+
getEnabledModules(): string[] {
|
|
86
|
+
return this._config?.modules || [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 특정 모듈이 활성화되어 있는지 확인
|
|
91
|
+
*/
|
|
92
|
+
isModuleEnabled(moduleName: string): boolean {
|
|
93
|
+
return this.getEnabledModules().includes(moduleName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 설정 업데이트 (런타임)
|
|
98
|
+
*/
|
|
99
|
+
updateConfig(updates: Partial<AdStageConfig>): void {
|
|
100
|
+
if (!this._config) {
|
|
101
|
+
throw new Error('Config module not initialized');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this._config = {
|
|
105
|
+
...this._config,
|
|
106
|
+
...updates
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (this.isDebugMode()) {
|
|
110
|
+
console.log('🔄 Config updated', updates);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* API 헤더 생성 (공통 유틸리티 사용)
|
|
116
|
+
*/
|
|
117
|
+
getApiHeaders(): Record<string, string> {
|
|
118
|
+
if (!this._config?.apiKey) {
|
|
119
|
+
throw new Error('API key not available');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return ApiHeaders.create(this._config.apiKey);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
File without changes
|