@adstage/web-sdk 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -35
- package/dist/index.cjs.js +753 -509
- package/dist/index.d.ts +286 -97
- package/dist/index.esm.js +737 -485
- package/dist/index.standalone.js +737 -485
- package/package.json +1 -1
- package/src/constants/endpoints.ts +93 -0
- package/src/core/AdStage.ts +128 -0
- package/src/index.ts +14 -432
- package/src/managers/{slider-manager.ts → carousel-slider-manager.ts} +9 -8
- package/src/managers/event-tracker.ts +2 -4
- package/src/managers/{fade-slider-manager.ts → text-transition-manager.ts} +7 -7
- package/src/modules/ads/AdsModule.ts +525 -0
- package/src/modules/config/ConfigModule.ts +124 -0
- package/src/modules/deeplinks/DeeplinksModule.ts +0 -0
- package/src/modules/events/EventsModule.ts +106 -0
- package/src/types/config.ts +74 -3
- package/src/types/index.ts +2 -1
- package/src/utils/api-headers.ts +52 -0
- package/src/utils/dom-utils.ts +1 -1
- package/examples/README.md +0 -33
- package/examples/banner-ads.html +0 -512
- package/examples/index.html +0 -338
- package/examples/native-ads.html +0 -634
- package/examples/react-app/README.md +0 -70
- package/examples/react-app/index.html +0 -13
- package/examples/react-app/package-lock.json +0 -3042
- package/examples/react-app/package.json +0 -26
- package/examples/react-app/pnpm-lock.yaml +0 -1857
- package/examples/react-app/public/index.standalone.js +0 -2331
- package/examples/react-app/src/App.tsx +0 -226
- package/examples/react-app/src/index.css +0 -37
- package/examples/react-app/src/main.tsx +0 -10
- package/examples/react-app/tsconfig.json +0 -25
- package/examples/react-app/tsconfig.node.json +0 -10
- package/examples/react-app/vite.config.ts +0 -15
- package/examples/react-nextjs/app/globals.css +0 -200
- package/examples/react-nextjs/app/layout.tsx +0 -27
- package/examples/react-nextjs/app/page.tsx +0 -258
- package/examples/react-nextjs/next.config.js +0 -9
- package/examples/react-nextjs/package.json +0 -22
- package/examples/react-nextjs/pnpm-lock.yaml +0 -343
- package/examples/react-nextjs/tsconfig.json +0 -34
- package/examples/text-ads.html +0 -597
- package/examples/video-ads.html +0 -739
- package/src/react/components/AdErrorBoundary.tsx +0 -75
- package/src/react/components/AdSlot.tsx +0 -144
- package/src/react/components/BannerAd.tsx +0 -24
- package/src/react/components/InterstitialAd.tsx +0 -24
- package/src/react/components/NativeAd.tsx +0 -24
- package/src/react/components/TextAd.tsx +0 -24
- package/src/react/components/VideoAd.tsx +0 -24
- package/src/react/components/index.ts +0 -8
- package/src/react/hooks/index.ts +0 -4
- package/src/react/hooks/useAdSlot.ts +0 -83
- package/src/react/hooks/useAdStage.ts +0 -14
- package/src/react/hooks/useAdTracking.ts +0 -61
- package/src/react/index.ts +0 -4
- package/src/react/providers/AdStageProvider.tsx +0 -86
- package/src/react/providers/index.ts +0 -2
- package/src/utils/sdk-standalone.ts +0 -155
|
@@ -3,18 +3,19 @@ import type { AdSlot, Advertisement } from '../types/advertisement';
|
|
|
3
3
|
import { AdRendererFactory } from '../renderers';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* 슬라이더 관리 클래스
|
|
7
|
-
* -
|
|
8
|
-
* - 무한 루프
|
|
6
|
+
* 캐러셀 슬라이더 관리 클래스
|
|
7
|
+
* - 배너/비디오 광고용 가로 슬라이드 (횡 스크롤)
|
|
8
|
+
* - 무한 루프 캐러셀 지원
|
|
9
9
|
* - 터치 제스처 및 자동 슬라이드 기능
|
|
10
|
+
* - 도트 인디케이터 포함
|
|
10
11
|
*/
|
|
11
|
-
export class
|
|
12
|
+
export class CarouselSliderManager {
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Create carousel slider container with dot indicators and navigation
|
|
14
15
|
*/
|
|
15
16
|
static createSliderContainer(
|
|
16
17
|
slot: AdSlot,
|
|
17
|
-
advertisements:
|
|
18
|
+
advertisements: any[],
|
|
18
19
|
options: any,
|
|
19
20
|
trackEventCallback: (adId: string, slotId: string, eventType: AdEventType) => void
|
|
20
21
|
): HTMLElement {
|
|
@@ -175,7 +176,7 @@ export class SliderManager {
|
|
|
175
176
|
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
176
177
|
|
|
177
178
|
// 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
|
|
178
|
-
const dotContainer = isAllTextAds ? null :
|
|
179
|
+
const dotContainer = isAllTextAds ? null : this.createMinimalDotIndicator(advertisements.length);
|
|
179
180
|
|
|
180
181
|
// 슬라이더 상태 관리
|
|
181
182
|
let currentSlide = 0;
|
|
@@ -261,7 +262,7 @@ export class SliderManager {
|
|
|
261
262
|
});
|
|
262
263
|
|
|
263
264
|
// 터치 제스처 지원 수정 (무한 루프 지원)
|
|
264
|
-
|
|
265
|
+
this.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
|
|
265
266
|
|
|
266
267
|
// 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
|
|
267
268
|
sliderWrapper.appendChild(slideContainer);
|
|
@@ -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
|
+
}
|