@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
|
@@ -1,2331 +0,0 @@
|
|
|
1
|
-
// 광고 타입 정의
|
|
2
|
-
var AdType;
|
|
3
|
-
(function (AdType) {
|
|
4
|
-
AdType["BANNER"] = "BANNER";
|
|
5
|
-
AdType["POPUP"] = "POPUP";
|
|
6
|
-
AdType["INTERSTITIAL"] = "INTERSTITIAL";
|
|
7
|
-
AdType["NATIVE"] = "NATIVE";
|
|
8
|
-
AdType["VIDEO"] = "VIDEO";
|
|
9
|
-
AdType["TEXT"] = "TEXT";
|
|
10
|
-
})(AdType || (AdType = {}));
|
|
11
|
-
// 플랫폼 정의
|
|
12
|
-
var Platform;
|
|
13
|
-
(function (Platform) {
|
|
14
|
-
Platform["WEB"] = "WEB";
|
|
15
|
-
Platform["MOBILE"] = "MOBILE";
|
|
16
|
-
})(Platform || (Platform = {}));
|
|
17
|
-
// 광고 이벤트 타입
|
|
18
|
-
var AdEventType;
|
|
19
|
-
(function (AdEventType) {
|
|
20
|
-
AdEventType["IMPRESSION"] = "IMPRESSION";
|
|
21
|
-
AdEventType["CLICK"] = "CLICK";
|
|
22
|
-
AdEventType["HOVER"] = "HOVER";
|
|
23
|
-
AdEventType["VIEWABLE"] = "VIEWABLE";
|
|
24
|
-
AdEventType["VIEWABLE_IMPRESSION"] = "VIEWABLE_IMPRESSION";
|
|
25
|
-
AdEventType["COMPLETED"] = "COMPLETED";
|
|
26
|
-
AdEventType["VIDEO_START"] = "VIDEO_START";
|
|
27
|
-
AdEventType["VIDEO_COMPLETE"] = "VIDEO_COMPLETE";
|
|
28
|
-
AdEventType["ERROR"] = "ERROR";
|
|
29
|
-
})(AdEventType || (AdEventType = {}));
|
|
30
|
-
// 디바이스 타입
|
|
31
|
-
var DeviceType;
|
|
32
|
-
(function (DeviceType) {
|
|
33
|
-
DeviceType["DESKTOP"] = "DESKTOP";
|
|
34
|
-
DeviceType["MOBILE"] = "MOBILE";
|
|
35
|
-
DeviceType["TABLET"] = "TABLET";
|
|
36
|
-
})(DeviceType || (DeviceType = {}));
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* SSR 안전한 DOM API 래퍼 클래스
|
|
40
|
-
* 서버사이드 렌더링 환경에서 DOM API 접근 시 오류를 방지합니다.
|
|
41
|
-
*/
|
|
42
|
-
class DOMUtils {
|
|
43
|
-
/**
|
|
44
|
-
* 브라우저 환경 여부 체크
|
|
45
|
-
*/
|
|
46
|
-
static isBrowser() {
|
|
47
|
-
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* SSR 환경 여부 체크
|
|
51
|
-
*/
|
|
52
|
-
static isSSR() {
|
|
53
|
-
return !this.isBrowser();
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* DOM 사용 가능 여부 체크
|
|
57
|
-
*/
|
|
58
|
-
static canUseDOM() {
|
|
59
|
-
return this.isBrowser() && document.readyState !== undefined;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* 안전한 getElementById
|
|
63
|
-
*/
|
|
64
|
-
static safeGetElementById(id) {
|
|
65
|
-
if (!this.canUseDOM())
|
|
66
|
-
return null;
|
|
67
|
-
return document.getElementById(id);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 안전한 querySelector
|
|
71
|
-
*/
|
|
72
|
-
static safeQuerySelector(selector) {
|
|
73
|
-
if (!this.canUseDOM())
|
|
74
|
-
return null;
|
|
75
|
-
return document.querySelector(selector);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* 안전한 querySelectorAll
|
|
79
|
-
*/
|
|
80
|
-
static safeQuerySelectorAll(selector) {
|
|
81
|
-
if (!this.canUseDOM())
|
|
82
|
-
return [];
|
|
83
|
-
return Array.from(document.querySelectorAll(selector));
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 안전한 createElement
|
|
87
|
-
*/
|
|
88
|
-
static safeCreateElement(tagName) {
|
|
89
|
-
if (!this.canUseDOM())
|
|
90
|
-
return null;
|
|
91
|
-
return document.createElement(tagName);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* 안전한 addEventListener
|
|
95
|
-
*/
|
|
96
|
-
static safeAddEventListener(element, event, handler, options) {
|
|
97
|
-
if (!this.canUseDOM() || !element)
|
|
98
|
-
return;
|
|
99
|
-
element.addEventListener(event, handler, options);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* 안전한 removeEventListener
|
|
103
|
-
*/
|
|
104
|
-
static safeRemoveEventListener(element, event, handler, options) {
|
|
105
|
-
if (!this.canUseDOM() || !element)
|
|
106
|
-
return;
|
|
107
|
-
element.removeEventListener(event, handler, options);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* 안전한 window 속성 접근
|
|
111
|
-
*/
|
|
112
|
-
static getWindowProperty(property, defaultValue) {
|
|
113
|
-
if (!this.isBrowser())
|
|
114
|
-
return defaultValue;
|
|
115
|
-
return window[property] ?? defaultValue;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 안전한 document 속성 접근
|
|
119
|
-
*/
|
|
120
|
-
static getDocumentProperty(property, defaultValue) {
|
|
121
|
-
if (!this.canUseDOM())
|
|
122
|
-
return defaultValue;
|
|
123
|
-
return document[property] ?? defaultValue;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* 안전한 window.open
|
|
127
|
-
*/
|
|
128
|
-
static safeWindowOpen(url, target, features) {
|
|
129
|
-
if (!this.isBrowser())
|
|
130
|
-
return null;
|
|
131
|
-
return window.open(url, target, features);
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* 안전한 getComputedStyle
|
|
135
|
-
*/
|
|
136
|
-
static safeGetComputedStyle(element) {
|
|
137
|
-
if (!this.isBrowser() || !element)
|
|
138
|
-
return null;
|
|
139
|
-
return window.getComputedStyle(element);
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* DOM Ready 상태 체크
|
|
143
|
-
*/
|
|
144
|
-
static isDOMReady() {
|
|
145
|
-
if (!this.canUseDOM())
|
|
146
|
-
return false;
|
|
147
|
-
return document.readyState !== 'loading';
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* DOM Ready 대기 (SSR 안전)
|
|
151
|
-
*/
|
|
152
|
-
static waitForDOM() {
|
|
153
|
-
return new Promise((resolve) => {
|
|
154
|
-
if (!this.canUseDOM()) {
|
|
155
|
-
resolve(); // SSR 환경에서는 즉시 resolve
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (this.isDOMReady()) {
|
|
159
|
-
resolve();
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
this.safeAddEventListener(document, 'DOMContentLoaded', () => resolve());
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* 안전한 스타일 적용
|
|
168
|
-
*/
|
|
169
|
-
static safeApplyStyles(element, styles) {
|
|
170
|
-
if (!this.canUseDOM() || !element)
|
|
171
|
-
return;
|
|
172
|
-
Object.entries(styles).forEach(([property, value]) => {
|
|
173
|
-
element.style.setProperty(property, value);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* 안전한 클래스 추가
|
|
178
|
-
*/
|
|
179
|
-
static safeAddClass(element, className) {
|
|
180
|
-
if (!this.canUseDOM() || !element)
|
|
181
|
-
return;
|
|
182
|
-
element.classList.add(className);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* 안전한 클래스 제거
|
|
186
|
-
*/
|
|
187
|
-
static safeRemoveClass(element, className) {
|
|
188
|
-
if (!this.canUseDOM() || !element)
|
|
189
|
-
return;
|
|
190
|
-
element.classList.remove(className);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* 안전한 텍스트 콘텐츠 설정
|
|
194
|
-
*/
|
|
195
|
-
static safeSetTextContent(element, text) {
|
|
196
|
-
if (!this.canUseDOM() || !element)
|
|
197
|
-
return;
|
|
198
|
-
element.textContent = text;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* 안전한 HTML 콘텐츠 설정
|
|
202
|
-
*/
|
|
203
|
-
static safeSetInnerHTML(element, html) {
|
|
204
|
-
if (!this.canUseDOM() || !element)
|
|
205
|
-
return;
|
|
206
|
-
element.innerHTML = html;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* 안전한 자식 요소 추가
|
|
210
|
-
*/
|
|
211
|
-
static safeAppendChild(parent, child) {
|
|
212
|
-
if (!this.canUseDOM() || !parent || !child)
|
|
213
|
-
return;
|
|
214
|
-
parent.appendChild(child);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* 안전한 자식 요소 제거
|
|
218
|
-
*/
|
|
219
|
-
static safeRemoveChild(parent, child) {
|
|
220
|
-
if (!this.canUseDOM() || !parent || !child)
|
|
221
|
-
return;
|
|
222
|
-
parent.removeChild(child);
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* 현재 페이지 정보 가져오기 (SSR 안전)
|
|
226
|
-
*/
|
|
227
|
-
static getPageInfo() {
|
|
228
|
-
return {
|
|
229
|
-
url: this.getWindowProperty('location', { href: '' }).href,
|
|
230
|
-
title: this.getDocumentProperty('title', ''),
|
|
231
|
-
referrer: this.getDocumentProperty('referrer', ''),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* 뷰포트 정보 가져오기 (SSR 안전)
|
|
236
|
-
*/
|
|
237
|
-
static getViewportInfo() {
|
|
238
|
-
return {
|
|
239
|
-
width: this.getWindowProperty('innerWidth', 0),
|
|
240
|
-
height: this.getWindowProperty('innerHeight', 0),
|
|
241
|
-
pixelRatio: this.getWindowProperty('devicePixelRatio', 1),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* 스크롤 정보 가져오기 (SSR 안전)
|
|
246
|
-
*/
|
|
247
|
-
static getScrollInfo() {
|
|
248
|
-
const scrollTop = this.canUseDOM()
|
|
249
|
-
? (window.pageYOffset || document.documentElement.scrollTop)
|
|
250
|
-
: 0;
|
|
251
|
-
return {
|
|
252
|
-
scrollTop,
|
|
253
|
-
scrollLeft: this.getWindowProperty('pageXOffset', 0),
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 기본 광고 렌더러 추상 클래스
|
|
260
|
-
*/
|
|
261
|
-
class BaseAdRenderer {
|
|
262
|
-
constructor(trackEvent) {
|
|
263
|
-
this.trackEvent = trackEvent;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* 공통 클릭 이벤트 핸들러 (SSR 안전)
|
|
267
|
-
*/
|
|
268
|
-
addClickHandler(element, ad, slot) {
|
|
269
|
-
DOMUtils.safeAddEventListener(element, 'click', () => {
|
|
270
|
-
this.trackEvent?.(ad._id, slot.id, 'CLICK');
|
|
271
|
-
if (ad.linkUrl) {
|
|
272
|
-
DOMUtils.safeWindowOpen(ad.linkUrl, '_blank');
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* 공통 스타일 적용 유틸리티 (SSR 안전)
|
|
278
|
-
*/
|
|
279
|
-
applyStyles(element, styles) {
|
|
280
|
-
DOMUtils.safeApplyStyles(element, styles);
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* 크기 값 파싱 유틸리티 (px, %, number 지원)
|
|
284
|
-
*/
|
|
285
|
-
parseSizeValue(value) {
|
|
286
|
-
if (!value)
|
|
287
|
-
return undefined;
|
|
288
|
-
if (typeof value === 'number') {
|
|
289
|
-
return value > 0 ? `${value}px` : undefined;
|
|
290
|
-
}
|
|
291
|
-
if (typeof value === 'string') {
|
|
292
|
-
const trimmed = value.trim();
|
|
293
|
-
if (!trimmed)
|
|
294
|
-
return undefined;
|
|
295
|
-
// 퍼센트 값 처리
|
|
296
|
-
if (trimmed.endsWith('%')) {
|
|
297
|
-
const percent = parseFloat(trimmed);
|
|
298
|
-
return !isNaN(percent) && percent > 0 ? trimmed : undefined;
|
|
299
|
-
}
|
|
300
|
-
// px 값 처리 (px 단위 포함/미포함 모두 지원)
|
|
301
|
-
const numValue = trimmed.endsWith('px')
|
|
302
|
-
? parseFloat(trimmed.slice(0, -2))
|
|
303
|
-
: parseFloat(trimmed);
|
|
304
|
-
return !isNaN(numValue) && numValue > 0 ? `${numValue}px` : undefined;
|
|
305
|
-
}
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* 기본 컨테이너 스타일 (사용자 지정 크기만 적용)
|
|
310
|
-
*/
|
|
311
|
-
getBaseContainerStyles(slot) {
|
|
312
|
-
const styles = {
|
|
313
|
-
cursor: 'pointer',
|
|
314
|
-
position: 'relative',
|
|
315
|
-
overflow: 'hidden',
|
|
316
|
-
};
|
|
317
|
-
// 사용자가 지정한 크기가 있을 때만 적용
|
|
318
|
-
const parsedWidth = this.parseSizeValue(slot.width);
|
|
319
|
-
const parsedHeight = this.parseSizeValue(slot.height);
|
|
320
|
-
if (parsedWidth) {
|
|
321
|
-
styles.width = parsedWidth;
|
|
322
|
-
}
|
|
323
|
-
if (parsedHeight) {
|
|
324
|
-
styles.height = parsedHeight;
|
|
325
|
-
}
|
|
326
|
-
return styles;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* 이미지 스타일 (고유 사이즈 유지, 사용자 지정 크기 우선)
|
|
330
|
-
*/
|
|
331
|
-
getImageStyles(slot) {
|
|
332
|
-
const styles = {
|
|
333
|
-
display: 'block',
|
|
334
|
-
'max-width': '100%',
|
|
335
|
-
height: 'auto',
|
|
336
|
-
};
|
|
337
|
-
// 사용자가 컨테이너 크기를 지정한 경우에만 크기 제한
|
|
338
|
-
const parsedWidth = this.parseSizeValue(slot?.width);
|
|
339
|
-
const parsedHeight = this.parseSizeValue(slot?.height);
|
|
340
|
-
if (parsedWidth && parsedHeight) {
|
|
341
|
-
styles.width = '100%';
|
|
342
|
-
styles.height = '100%';
|
|
343
|
-
styles['object-fit'] = 'cover';
|
|
344
|
-
}
|
|
345
|
-
return styles;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* 기본 폰트 스타일
|
|
349
|
-
*/
|
|
350
|
-
getBaseFontStyles() {
|
|
351
|
-
return {
|
|
352
|
-
'font-family': 'Arial, sans-serif',
|
|
353
|
-
'line-height': '1.4',
|
|
354
|
-
'word-break': 'keep-all',
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* 이미지 요소 생성 (SSR 안전)
|
|
359
|
-
*/
|
|
360
|
-
createImageElement(imageUrl, alt = '', slot) {
|
|
361
|
-
const img = DOMUtils.safeCreateElement('img');
|
|
362
|
-
if (!img)
|
|
363
|
-
return null;
|
|
364
|
-
img.src = imageUrl;
|
|
365
|
-
img.alt = alt;
|
|
366
|
-
this.applyStyles(img, this.getImageStyles(slot));
|
|
367
|
-
return img;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* 텍스트 요소 생성 (SSR 안전)
|
|
371
|
-
*/
|
|
372
|
-
createTextElement(text, tag = 'div', additionalStyles = {}) {
|
|
373
|
-
const element = DOMUtils.safeCreateElement(tag);
|
|
374
|
-
if (!element)
|
|
375
|
-
return null;
|
|
376
|
-
DOMUtils.safeSetTextContent(element, text);
|
|
377
|
-
this.applyStyles(element, {
|
|
378
|
-
...this.getBaseFontStyles(),
|
|
379
|
-
...additionalStyles,
|
|
380
|
-
});
|
|
381
|
-
return element;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* 플레이스홀더 요소 생성
|
|
385
|
-
*/
|
|
386
|
-
createPlaceholder(slot, text = '광고') {
|
|
387
|
-
let placeholder = DOMUtils.safeCreateElement('div');
|
|
388
|
-
// SSR 환경에서 DOM을 사용할 수 없는 경우, 런타임에 생성되도록 함
|
|
389
|
-
if (!placeholder) {
|
|
390
|
-
// SSR에서는 빈 div를 반환하되, 브라우저에서는 제대로 작동하도록 함
|
|
391
|
-
if (typeof document !== 'undefined') {
|
|
392
|
-
placeholder = document.createElement('div');
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
// SSR 환경에서는 더미 객체 반환 (타입 단언 사용)
|
|
396
|
-
placeholder = {};
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// DOM이 사용 가능한 경우에만 스타일 적용
|
|
400
|
-
if (DOMUtils.canUseDOM() && placeholder) {
|
|
401
|
-
this.applyStyles(placeholder, {
|
|
402
|
-
...this.getBaseContainerStyles(slot),
|
|
403
|
-
background: '#f8f9fa',
|
|
404
|
-
display: 'flex',
|
|
405
|
-
'align-items': 'center',
|
|
406
|
-
'justify-content': 'center',
|
|
407
|
-
color: '#6c757d',
|
|
408
|
-
...this.getBaseFontStyles(),
|
|
409
|
-
// 플레이스홀더는 최소 크기 보장
|
|
410
|
-
'min-width': '100px',
|
|
411
|
-
'min-height': '100px',
|
|
412
|
-
});
|
|
413
|
-
DOMUtils.safeSetTextContent(placeholder, text);
|
|
414
|
-
}
|
|
415
|
-
return placeholder;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* 배너 광고 렌더러 - 이미지만 표시
|
|
421
|
-
*/
|
|
422
|
-
class BannerAdRenderer extends BaseAdRenderer {
|
|
423
|
-
render(ad, slot) {
|
|
424
|
-
const adElement = DOMUtils.safeCreateElement('div');
|
|
425
|
-
if (!adElement) {
|
|
426
|
-
// SSR 환경에서는 기본 div 반환
|
|
427
|
-
return document.createElement('div');
|
|
428
|
-
}
|
|
429
|
-
// 기본 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
430
|
-
this.applyStyles(adElement, this.getBaseContainerStyles(slot));
|
|
431
|
-
// 배너 광고는 이미지만 표시
|
|
432
|
-
if (!ad.imageUrl) {
|
|
433
|
-
// 이미지가 없는 경우 플레이스홀더 반환
|
|
434
|
-
const placeholder = this.createPlaceholder(slot, '배너 광고');
|
|
435
|
-
return placeholder || adElement;
|
|
436
|
-
}
|
|
437
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
438
|
-
if (img) {
|
|
439
|
-
DOMUtils.safeAppendChild(adElement, img);
|
|
440
|
-
// 클릭 이벤트 추가
|
|
441
|
-
this.addClickHandler(adElement, ad, slot);
|
|
442
|
-
}
|
|
443
|
-
return adElement;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* 텍스트 광고 렌더러 - textContent만 표시
|
|
449
|
-
*/
|
|
450
|
-
class TextAdRenderer extends BaseAdRenderer {
|
|
451
|
-
render(ad, slot) {
|
|
452
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
453
|
-
if (!adElement) {
|
|
454
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
455
|
-
}
|
|
456
|
-
// 기본 컨테이너 스타일
|
|
457
|
-
const containerStyles = {
|
|
458
|
-
...this.getBaseContainerStyles(slot),
|
|
459
|
-
padding: '20px',
|
|
460
|
-
background: 'transparent',
|
|
461
|
-
display: 'flex',
|
|
462
|
-
'align-items': 'center',
|
|
463
|
-
'justify-content': 'center',
|
|
464
|
-
// text-align은 사용자가 설정할 수 있도록 기본값에서 제외
|
|
465
|
-
...this.getBaseFontStyles(),
|
|
466
|
-
};
|
|
467
|
-
// 사용자가 크기를 지정하지 않은 경우 컨텐츠에 맞춤
|
|
468
|
-
if (!slot.width || slot.width === 0) {
|
|
469
|
-
containerStyles.display = 'inline-flex';
|
|
470
|
-
containerStyles['white-space'] = 'nowrap';
|
|
471
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
472
|
-
}
|
|
473
|
-
// height만 자동인 경우 줄바꿈 허용
|
|
474
|
-
if ((slot.width && slot.width !== 0) && (!slot.height || slot.height === 0)) {
|
|
475
|
-
containerStyles['white-space'] = 'normal';
|
|
476
|
-
containerStyles['min-height'] = 'auto';
|
|
477
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
478
|
-
}
|
|
479
|
-
this.applyStyles(adElement, containerStyles);
|
|
480
|
-
// 텍스트 광고는 textContent만 표시
|
|
481
|
-
if (!ad.textContent) {
|
|
482
|
-
// 텍스트가 없는 경우 플레이스홀더 반환
|
|
483
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
484
|
-
}
|
|
485
|
-
// 텍스트 콘텐츠 생성
|
|
486
|
-
const textContent = this.createTextElement(ad.textContent, 'div', {
|
|
487
|
-
'font-size': '16px',
|
|
488
|
-
'font-weight': '500',
|
|
489
|
-
color: '#212529',
|
|
490
|
-
width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
|
|
491
|
-
});
|
|
492
|
-
if (textContent) {
|
|
493
|
-
adElement.appendChild(textContent);
|
|
494
|
-
}
|
|
495
|
-
// 사용자가 text-align을 지정했는지 확인하고 레이아웃 조정
|
|
496
|
-
setTimeout(() => {
|
|
497
|
-
if (!adElement || typeof window === 'undefined')
|
|
498
|
-
return;
|
|
499
|
-
const computedStyle = window.getComputedStyle(adElement);
|
|
500
|
-
const textAlign = computedStyle.textAlign;
|
|
501
|
-
// 사용자가 text-align을 설정했고, width가 없는 경우
|
|
502
|
-
if (textAlign && textAlign !== 'start' && textAlign !== 'left' && (!slot.width || slot.width === 0)) {
|
|
503
|
-
// 블록 레벨로 변경하여 text-align이 제대로 작동하도록 함
|
|
504
|
-
adElement.style.display = 'block';
|
|
505
|
-
adElement.style.whiteSpace = 'normal';
|
|
506
|
-
// 최소 너비 설정 (텍스트가 한 줄일 때를 위해)
|
|
507
|
-
if (textContent) {
|
|
508
|
-
const textRect = textContent.getBoundingClientRect();
|
|
509
|
-
if (textRect.width > 0) {
|
|
510
|
-
adElement.style.minWidth = `${textRect.width}px`;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}, 0);
|
|
515
|
-
// 클릭 이벤트 추가
|
|
516
|
-
this.addClickHandler(adElement, ad, slot);
|
|
517
|
-
return adElement;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* 네이티브 광고 렌더러 - 이미지 + textContent 표시
|
|
523
|
-
*/
|
|
524
|
-
class NativeAdRenderer extends BaseAdRenderer {
|
|
525
|
-
render(ad, slot) {
|
|
526
|
-
const adElement = DOMUtils.safeCreateElement('div');
|
|
527
|
-
if (!adElement) {
|
|
528
|
-
return document.createElement('div');
|
|
529
|
-
}
|
|
530
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
531
|
-
this.applyStyles(adElement, this.getBaseContainerStyles(slot));
|
|
532
|
-
// 네이티브 광고는 이미지만 표시
|
|
533
|
-
if (!ad.imageUrl) {
|
|
534
|
-
// 이미지가 없는 경우 플레이스홀더 반환
|
|
535
|
-
const placeholder = this.createPlaceholder(slot, '네이티브 광고');
|
|
536
|
-
return placeholder || adElement;
|
|
537
|
-
}
|
|
538
|
-
// 이미지 생성 (고유 사이즈 또는 사용자 지정 크기)
|
|
539
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
540
|
-
if (img) {
|
|
541
|
-
DOMUtils.safeAppendChild(adElement, img);
|
|
542
|
-
// 클릭 이벤트 추가
|
|
543
|
-
this.addClickHandler(adElement, ad, slot);
|
|
544
|
-
}
|
|
545
|
-
return adElement;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* 비디오 광고 렌더러 - 비디오 또는 이미지 표시
|
|
551
|
-
*/
|
|
552
|
-
class VideoAdRenderer extends BaseAdRenderer {
|
|
553
|
-
render(ad, slot) {
|
|
554
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
555
|
-
if (!adElement) {
|
|
556
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
557
|
-
}
|
|
558
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
559
|
-
this.applyStyles(adElement, {
|
|
560
|
-
...this.getBaseContainerStyles(slot),
|
|
561
|
-
background: '#000',
|
|
562
|
-
});
|
|
563
|
-
// 비디오 광고는 비디오만 표시
|
|
564
|
-
if (!ad.videoUrl) {
|
|
565
|
-
// 비디오가 없는 경우 플레이스홀더 반환
|
|
566
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
567
|
-
}
|
|
568
|
-
const video = this.createVideoElement(ad.videoUrl, ad, slot);
|
|
569
|
-
if (video) {
|
|
570
|
-
adElement.appendChild(video);
|
|
571
|
-
}
|
|
572
|
-
// 클릭 이벤트 추가
|
|
573
|
-
this.addClickHandler(adElement, ad, slot);
|
|
574
|
-
return adElement;
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* 비디오 요소 생성
|
|
578
|
-
*/
|
|
579
|
-
createVideoElement(videoUrl, ad, slot) {
|
|
580
|
-
const video = DOMUtils.safeCreateElement('video');
|
|
581
|
-
if (!video)
|
|
582
|
-
return null;
|
|
583
|
-
video.src = videoUrl;
|
|
584
|
-
video.controls = true;
|
|
585
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
586
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
587
|
-
// 비디오 이벤트 추적
|
|
588
|
-
video.addEventListener('play', () => {
|
|
589
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START');
|
|
590
|
-
});
|
|
591
|
-
video.addEventListener('ended', () => {
|
|
592
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE');
|
|
593
|
-
});
|
|
594
|
-
return video;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* 전면/팝업 광고 렌더러 - 핵심 콘텐츠만 표시
|
|
600
|
-
*/
|
|
601
|
-
class InterstitialAdRenderer extends BaseAdRenderer {
|
|
602
|
-
render(ad, slot) {
|
|
603
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
604
|
-
if (!adElement) {
|
|
605
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
606
|
-
}
|
|
607
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
608
|
-
this.applyStyles(adElement, {
|
|
609
|
-
...this.getBaseContainerStyles(slot),
|
|
610
|
-
display: 'flex',
|
|
611
|
-
'flex-direction': 'column',
|
|
612
|
-
});
|
|
613
|
-
// 우선순위: 1. 이미지, 2. 비디오, 3. 텍스트
|
|
614
|
-
if (ad.imageUrl) {
|
|
615
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
616
|
-
if (img) {
|
|
617
|
-
adElement.appendChild(img);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
else if (ad.videoUrl) {
|
|
621
|
-
// 이미지가 없고 비디오가 있는 경우
|
|
622
|
-
const video = this.createVideoElement(ad.videoUrl, ad, slot);
|
|
623
|
-
if (video) {
|
|
624
|
-
adElement.appendChild(video);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
else {
|
|
628
|
-
// 모든 콘텐츠가 없는 경우
|
|
629
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
630
|
-
}
|
|
631
|
-
// 클릭 이벤트 추가
|
|
632
|
-
this.addClickHandler(adElement, ad, slot);
|
|
633
|
-
return adElement;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* 비디오 요소 생성
|
|
637
|
-
*/
|
|
638
|
-
createVideoElement(videoUrl, ad, slot) {
|
|
639
|
-
const video = DOMUtils.safeCreateElement('video');
|
|
640
|
-
if (!video)
|
|
641
|
-
return null;
|
|
642
|
-
video.src = videoUrl;
|
|
643
|
-
video.controls = true;
|
|
644
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
645
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
646
|
-
// 비디오 이벤트 추적
|
|
647
|
-
video.addEventListener('play', () => {
|
|
648
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START');
|
|
649
|
-
});
|
|
650
|
-
video.addEventListener('ended', () => {
|
|
651
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE');
|
|
652
|
-
});
|
|
653
|
-
return video;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
var _a;
|
|
658
|
-
/**
|
|
659
|
-
* 광고 렌더러 팩토리
|
|
660
|
-
* - 광고 타입에 따라 적절한 렌더러 인스턴스를 반환
|
|
661
|
-
*/
|
|
662
|
-
class AdRendererFactory {
|
|
663
|
-
/**
|
|
664
|
-
* 광고 타입에 맞는 렌더러 생성
|
|
665
|
-
*/
|
|
666
|
-
static createRenderer(adType, trackEvent) {
|
|
667
|
-
const RendererClass = this.renderers.get(adType);
|
|
668
|
-
if (!RendererClass) {
|
|
669
|
-
console.warn(`No renderer found for ad type: ${adType}, falling back to Banner renderer`);
|
|
670
|
-
return new BannerAdRenderer(trackEvent);
|
|
671
|
-
}
|
|
672
|
-
return new RendererClass(trackEvent);
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* 광고 렌더링 (편의 메서드)
|
|
676
|
-
*/
|
|
677
|
-
static render(ad, slot, trackEvent) {
|
|
678
|
-
const renderer = this.createRenderer(slot.adType, trackEvent);
|
|
679
|
-
return renderer.render(ad, slot);
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* 사용 가능한 렌더러 타입 목록
|
|
683
|
-
*/
|
|
684
|
-
static getSupportedAdTypes() {
|
|
685
|
-
return Array.from(this.renderers.keys());
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* 커스텀 렌더러 등록
|
|
689
|
-
*/
|
|
690
|
-
static registerRenderer(adType, RendererClass) {
|
|
691
|
-
this.renderers.set(adType, RendererClass);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
_a = AdRendererFactory;
|
|
695
|
-
AdRendererFactory.renderers = new Map();
|
|
696
|
-
(() => {
|
|
697
|
-
// 렌더러 등록
|
|
698
|
-
_a.renderers.set(AdType.BANNER, BannerAdRenderer);
|
|
699
|
-
_a.renderers.set(AdType.TEXT, TextAdRenderer);
|
|
700
|
-
_a.renderers.set(AdType.NATIVE, NativeAdRenderer);
|
|
701
|
-
_a.renderers.set(AdType.VIDEO, VideoAdRenderer);
|
|
702
|
-
_a.renderers.set(AdType.INTERSTITIAL, InterstitialAdRenderer);
|
|
703
|
-
_a.renderers.set(AdType.POPUP, InterstitialAdRenderer); // POPUP은 INTERSTITIAL과 동일
|
|
704
|
-
})();
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* 슬라이더 관리 클래스
|
|
708
|
-
* - 다중 광고 슬라이더 생성 및 관리
|
|
709
|
-
* - 무한 루프 슬라이더 지원
|
|
710
|
-
* - 터치 제스처 및 자동 슬라이드 기능
|
|
711
|
-
*/
|
|
712
|
-
class SliderManager {
|
|
713
|
-
/**
|
|
714
|
-
* 슬라이더 컨테이너 생성
|
|
715
|
-
*/
|
|
716
|
-
static createSliderContainer(slot, advertisements, options, trackEventCallback) {
|
|
717
|
-
const sliderWrapper = document.createElement('div');
|
|
718
|
-
sliderWrapper.className = 'adstage-slider-wrapper';
|
|
719
|
-
// 사용자 지정 크기가 있으면 적용, 없으면 콘텐츠 크기에 맞춤
|
|
720
|
-
const containerStyles = {
|
|
721
|
-
position: 'relative',
|
|
722
|
-
overflow: 'hidden',
|
|
723
|
-
};
|
|
724
|
-
// 사용자가 크기를 지정한 경우
|
|
725
|
-
if (slot.width && slot.width !== 0) {
|
|
726
|
-
let width;
|
|
727
|
-
if (typeof slot.width === 'string') {
|
|
728
|
-
// 문자열인 경우 px 단위가 있는지 확인
|
|
729
|
-
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
// 숫자인 경우 px 단위 추가
|
|
733
|
-
width = `${slot.width}px`;
|
|
734
|
-
}
|
|
735
|
-
containerStyles.width = width;
|
|
736
|
-
containerStyles.display = 'inline-block'; // 지정된 크기에 맞춤 (좌측 정렬)
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
// 컨텐츠 크기에 맞춤
|
|
740
|
-
containerStyles.display = 'inline-block';
|
|
741
|
-
}
|
|
742
|
-
if (slot.height && slot.height !== 0) {
|
|
743
|
-
const height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
|
|
744
|
-
containerStyles.height = height;
|
|
745
|
-
}
|
|
746
|
-
// 스타일 적용
|
|
747
|
-
Object.entries(containerStyles).forEach(([key, value]) => {
|
|
748
|
-
sliderWrapper.style.setProperty(key, value);
|
|
749
|
-
});
|
|
750
|
-
// 크기 측정 (width나 height가 설정되지 않은 경우)
|
|
751
|
-
const needsWidthMeasurement = !slot.width || slot.width === 0;
|
|
752
|
-
const needsHeightMeasurement = !slot.height || slot.height === 0;
|
|
753
|
-
if (needsWidthMeasurement || needsHeightMeasurement) {
|
|
754
|
-
const measureContainer = document.createElement('div');
|
|
755
|
-
measureContainer.style.cssText = `
|
|
756
|
-
position: absolute;
|
|
757
|
-
visibility: hidden;
|
|
758
|
-
white-space: nowrap;
|
|
759
|
-
top: -9999px;
|
|
760
|
-
left: -9999px;
|
|
761
|
-
`;
|
|
762
|
-
// width가 설정되어 있으면 측정 컨테이너에도 적용
|
|
763
|
-
if (!needsWidthMeasurement && slot.width) {
|
|
764
|
-
let width;
|
|
765
|
-
if (typeof slot.width === 'string') {
|
|
766
|
-
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
width = `${slot.width}px`;
|
|
770
|
-
}
|
|
771
|
-
measureContainer.style.width = width;
|
|
772
|
-
measureContainer.style.whiteSpace = 'normal'; // width가 있으면 줄바꿈 허용
|
|
773
|
-
}
|
|
774
|
-
document.body.appendChild(measureContainer);
|
|
775
|
-
let maxWidth = 0;
|
|
776
|
-
let maxHeight = 0;
|
|
777
|
-
// 모든 광고의 크기를 측정하여 최대 크기 찾기
|
|
778
|
-
advertisements.forEach(ad => {
|
|
779
|
-
const measureAdElement = AdRendererFactory.render(ad, slot, trackEventCallback);
|
|
780
|
-
measureContainer.appendChild(measureAdElement);
|
|
781
|
-
const rect = measureAdElement.getBoundingClientRect();
|
|
782
|
-
if (rect.width > maxWidth)
|
|
783
|
-
maxWidth = rect.width;
|
|
784
|
-
if (rect.height > maxHeight)
|
|
785
|
-
maxHeight = rect.height;
|
|
786
|
-
// 측정 후 요소 제거
|
|
787
|
-
measureContainer.removeChild(measureAdElement);
|
|
788
|
-
});
|
|
789
|
-
// 측정된 최대 크기로 래퍼 크기 설정
|
|
790
|
-
if (needsWidthMeasurement && maxWidth > 0) {
|
|
791
|
-
sliderWrapper.style.width = `${maxWidth}px`;
|
|
792
|
-
containerStyles.width = `${maxWidth}px`;
|
|
793
|
-
}
|
|
794
|
-
if (needsHeightMeasurement && maxHeight > 0) {
|
|
795
|
-
sliderWrapper.style.height = `${maxHeight}px`;
|
|
796
|
-
containerStyles.height = `${maxHeight}px`;
|
|
797
|
-
}
|
|
798
|
-
// 측정 컨테이너 제거
|
|
799
|
-
document.body.removeChild(measureContainer);
|
|
800
|
-
}
|
|
801
|
-
// 무한 루프를 위해 첫 번째 슬라이드를 마지막에 복사
|
|
802
|
-
const extendedAds = [...advertisements, advertisements[0]];
|
|
803
|
-
// 슬라이드 컨테이너
|
|
804
|
-
const slideContainer = document.createElement('div');
|
|
805
|
-
slideContainer.className = 'adstage-slide-container';
|
|
806
|
-
// 슬라이드 컨테이너 스타일 - 항상 기본 설정 적용
|
|
807
|
-
const slideContainerStyles = {
|
|
808
|
-
display: 'flex',
|
|
809
|
-
transition: 'transform 0.4s ease-out',
|
|
810
|
-
width: `${extendedAds.length * 100}%`,
|
|
811
|
-
};
|
|
812
|
-
if (slot.height && slot.height !== 0) {
|
|
813
|
-
slideContainerStyles.height = '100%';
|
|
814
|
-
}
|
|
815
|
-
Object.entries(slideContainerStyles).forEach(([key, value]) => {
|
|
816
|
-
slideContainer.style.setProperty(key, value);
|
|
817
|
-
});
|
|
818
|
-
// 각 광고를 슬라이드로 생성 (복사된 첫 번째 포함)
|
|
819
|
-
extendedAds.forEach((ad, index) => {
|
|
820
|
-
const slideElement = document.createElement('div');
|
|
821
|
-
slideElement.className = 'adstage-slide';
|
|
822
|
-
// 슬라이드 스타일 설정 - 항상 균등 분할
|
|
823
|
-
const slideStyles = {
|
|
824
|
-
width: `${100 / extendedAds.length}%`,
|
|
825
|
-
'flex-shrink': '0',
|
|
826
|
-
display: 'flex',
|
|
827
|
-
'align-items': 'center',
|
|
828
|
-
'justify-content': 'center'
|
|
829
|
-
};
|
|
830
|
-
if (slot.height && slot.height !== 0) {
|
|
831
|
-
slideStyles.height = '100%';
|
|
832
|
-
}
|
|
833
|
-
Object.entries(slideStyles).forEach(([key, value]) => {
|
|
834
|
-
slideElement.style.setProperty(key, value);
|
|
835
|
-
});
|
|
836
|
-
// 광고 렌더링
|
|
837
|
-
const adElement = AdRendererFactory.render(ad, slot, trackEventCallback);
|
|
838
|
-
slideElement.appendChild(adElement);
|
|
839
|
-
slideContainer.appendChild(slideElement);
|
|
840
|
-
});
|
|
841
|
-
// 텍스트 광고인지 확인 (모든 광고가 텍스트 타입인 경우)
|
|
842
|
-
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
843
|
-
// 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
|
|
844
|
-
const dotContainer = isAllTextAds ? null : SliderManager.createMinimalDotIndicator(advertisements.length);
|
|
845
|
-
// 슬라이더 상태 관리
|
|
846
|
-
let currentSlide = 0;
|
|
847
|
-
const totalSlides = advertisements.length;
|
|
848
|
-
const autoSlideInterval = (options?.autoSlideInterval || 3) * 1000; // 기본 3초
|
|
849
|
-
// 슬라이드 이동 함수 (무한 루프 지원)
|
|
850
|
-
const moveToSlide = (index, instant = false) => {
|
|
851
|
-
currentSlide = index;
|
|
852
|
-
// 애니메이션 임시 비활성화 (무한 루프용)
|
|
853
|
-
if (instant) {
|
|
854
|
-
slideContainer.style.transition = 'none';
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
slideContainer.style.transition = 'transform 0.4s ease-out';
|
|
858
|
-
}
|
|
859
|
-
// 항상 퍼센트 기반으로 이동
|
|
860
|
-
slideContainer.style.transform = `translateX(-${(100 / extendedAds.length) * currentSlide}%)`;
|
|
861
|
-
// 도트 업데이트 (무채색 스타일) - 실제 광고 인덱스 기준, 텍스트 광고가 아닐 때만
|
|
862
|
-
const actualIndex = currentSlide === totalSlides ? 0 : currentSlide;
|
|
863
|
-
if (dotContainer) {
|
|
864
|
-
const dots = dotContainer.querySelectorAll('.adstage-dot');
|
|
865
|
-
dots.forEach((dot, i) => {
|
|
866
|
-
const dotElement = dot;
|
|
867
|
-
if (i === actualIndex) {
|
|
868
|
-
dotElement.classList.add('active');
|
|
869
|
-
dotElement.style.background = '#666666';
|
|
870
|
-
dotElement.style.borderColor = '#666666';
|
|
871
|
-
dotElement.style.opacity = '1';
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
dotElement.classList.remove('active');
|
|
875
|
-
dotElement.style.background = 'transparent';
|
|
876
|
-
dotElement.style.borderColor = '#cccccc';
|
|
877
|
-
dotElement.style.opacity = '0.7';
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
882
|
-
if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
883
|
-
trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.IMPRESSION);
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
// 무한 루프 처리 함수
|
|
887
|
-
const handleInfiniteLoop = () => {
|
|
888
|
-
if (currentSlide === totalSlides) {
|
|
889
|
-
// 복사된 첫 번째 슬라이드에 도달하면 즉시 원본 첫 번째로 이동
|
|
890
|
-
setTimeout(() => {
|
|
891
|
-
moveToSlide(0, true); // 애니메이션 없이 즉시 이동
|
|
892
|
-
}, 400); // transition 시간과 맞춤
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
// 도트 클릭 이벤트 (텍스트 광고가 아닐 때만)
|
|
896
|
-
if (dotContainer) {
|
|
897
|
-
const dots = dotContainer.querySelectorAll('.adstage-dot');
|
|
898
|
-
dots.forEach((dot, index) => {
|
|
899
|
-
dot.addEventListener('click', () => moveToSlide(index));
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
// 자동 슬라이드 (한 방향으로만 무한 진행)
|
|
903
|
-
let autoSlideTimer = setInterval(() => {
|
|
904
|
-
const nextIndex = currentSlide + 1;
|
|
905
|
-
moveToSlide(nextIndex);
|
|
906
|
-
handleInfiniteLoop();
|
|
907
|
-
}, autoSlideInterval);
|
|
908
|
-
// 마우스 호버 시 자동 슬라이드 일시정지
|
|
909
|
-
sliderWrapper.addEventListener('mouseenter', () => {
|
|
910
|
-
clearInterval(autoSlideTimer);
|
|
911
|
-
});
|
|
912
|
-
sliderWrapper.addEventListener('mouseleave', () => {
|
|
913
|
-
autoSlideTimer = setInterval(() => {
|
|
914
|
-
const nextIndex = currentSlide + 1;
|
|
915
|
-
moveToSlide(nextIndex);
|
|
916
|
-
handleInfiniteLoop();
|
|
917
|
-
}, autoSlideInterval);
|
|
918
|
-
});
|
|
919
|
-
// 터치 제스처 지원 수정 (무한 루프 지원)
|
|
920
|
-
SliderManager.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
|
|
921
|
-
// 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
|
|
922
|
-
sliderWrapper.appendChild(slideContainer);
|
|
923
|
-
if (dotContainer) {
|
|
924
|
-
sliderWrapper.appendChild(dotContainer);
|
|
925
|
-
}
|
|
926
|
-
// 첫 번째 도트 활성화
|
|
927
|
-
moveToSlide(0);
|
|
928
|
-
// 사용자가 크기를 지정하지 않은 경우, 첫 번째 슬라이드 크기에 맞춰 래퍼 크기 동적 조정
|
|
929
|
-
if (!slot.width || slot.width === 0) {
|
|
930
|
-
// DOM 렌더링 후 크기 측정
|
|
931
|
-
setTimeout(() => {
|
|
932
|
-
const firstSlide = slideContainer.children[0];
|
|
933
|
-
if (firstSlide) {
|
|
934
|
-
const firstAdElement = firstSlide.children[0];
|
|
935
|
-
if (firstAdElement) {
|
|
936
|
-
const rect = firstAdElement.getBoundingClientRect();
|
|
937
|
-
sliderWrapper.style.width = `${rect.width}px`;
|
|
938
|
-
if (!slot.height || slot.height === 0) {
|
|
939
|
-
sliderWrapper.style.height = `${rect.height}px`;
|
|
940
|
-
}
|
|
941
|
-
// 크기 조정 후 overflow hidden 재적용
|
|
942
|
-
sliderWrapper.style.overflow = 'hidden';
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}, 10);
|
|
946
|
-
}
|
|
947
|
-
return sliderWrapper;
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* 무채색 미니멀 도트 인디케이터 생성
|
|
951
|
-
*/
|
|
952
|
-
static createMinimalDotIndicator(count) {
|
|
953
|
-
const dotContainer = document.createElement('div');
|
|
954
|
-
dotContainer.className = 'adstage-dots';
|
|
955
|
-
dotContainer.style.cssText = `
|
|
956
|
-
position: absolute;
|
|
957
|
-
bottom: 15px;
|
|
958
|
-
left: 50%;
|
|
959
|
-
transform: translateX(-50%);
|
|
960
|
-
display: flex;
|
|
961
|
-
gap: 12px;
|
|
962
|
-
z-index: 3;
|
|
963
|
-
padding: 8px 16px;
|
|
964
|
-
border-radius: 20px;
|
|
965
|
-
background: rgba(255, 255, 255, 0.1);
|
|
966
|
-
backdrop-filter: blur(10px);
|
|
967
|
-
`;
|
|
968
|
-
for (let i = 0; i < count; i++) {
|
|
969
|
-
const dot = document.createElement('button');
|
|
970
|
-
dot.className = 'adstage-dot';
|
|
971
|
-
dot.style.cssText = `
|
|
972
|
-
width: 8px;
|
|
973
|
-
height: 8px;
|
|
974
|
-
border-radius: 50%;
|
|
975
|
-
border: 1px solid #cccccc;
|
|
976
|
-
background: transparent;
|
|
977
|
-
cursor: pointer;
|
|
978
|
-
transition: all 0.3s ease;
|
|
979
|
-
outline: none;
|
|
980
|
-
opacity: 0.7;
|
|
981
|
-
padding: 0;
|
|
982
|
-
margin: 0;
|
|
983
|
-
flex-shrink: 0;
|
|
984
|
-
`;
|
|
985
|
-
// 호버 효과
|
|
986
|
-
dot.addEventListener('mouseenter', () => {
|
|
987
|
-
if (!dot.classList.contains('active')) {
|
|
988
|
-
dot.style.borderColor = '#999999';
|
|
989
|
-
dot.style.opacity = '0.9';
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
dot.addEventListener('mouseleave', () => {
|
|
993
|
-
if (!dot.classList.contains('active')) {
|
|
994
|
-
dot.style.borderColor = '#cccccc';
|
|
995
|
-
dot.style.opacity = '0.7';
|
|
996
|
-
}
|
|
997
|
-
});
|
|
998
|
-
dotContainer.appendChild(dot);
|
|
999
|
-
}
|
|
1000
|
-
return dotContainer;
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* 터치 제스처 지원 추가
|
|
1004
|
-
*/
|
|
1005
|
-
static addTouchSupport(container, moveToSlide, getCurrentSlide, totalSlides, handleInfiniteLoop) {
|
|
1006
|
-
let startX = 0;
|
|
1007
|
-
let isDragging = false;
|
|
1008
|
-
container.addEventListener('touchstart', (e) => {
|
|
1009
|
-
startX = e.touches[0].clientX;
|
|
1010
|
-
isDragging = true;
|
|
1011
|
-
});
|
|
1012
|
-
container.addEventListener('touchmove', (e) => {
|
|
1013
|
-
if (!isDragging)
|
|
1014
|
-
return;
|
|
1015
|
-
e.preventDefault();
|
|
1016
|
-
});
|
|
1017
|
-
container.addEventListener('touchend', (e) => {
|
|
1018
|
-
if (!isDragging)
|
|
1019
|
-
return;
|
|
1020
|
-
isDragging = false;
|
|
1021
|
-
const endX = e.changedTouches[0].clientX;
|
|
1022
|
-
const diff = startX - endX;
|
|
1023
|
-
if (Math.abs(diff) > 50) { // 50px 이상 스와이프 시
|
|
1024
|
-
const currentSlide = getCurrentSlide();
|
|
1025
|
-
if (diff > 0) {
|
|
1026
|
-
// 왼쪽으로 스와이프 (다음 슬라이드)
|
|
1027
|
-
const nextIndex = currentSlide + 1;
|
|
1028
|
-
moveToSlide(nextIndex);
|
|
1029
|
-
if (handleInfiniteLoop) {
|
|
1030
|
-
handleInfiniteLoop();
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
else {
|
|
1034
|
-
// 오른쪽으로 스와이프 (이전 슬라이드)
|
|
1035
|
-
const prevIndex = currentSlide > 0 ? currentSlide - 1 : totalSlides - 1;
|
|
1036
|
-
moveToSlide(prevIndex);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* 페이드 슬라이더 관리 클래스
|
|
1045
|
-
* - 텍스트 광고 전용 페이드 인/아웃 슬라이더
|
|
1046
|
-
* - 상하 교차 효과 (위에서 아래로, 아래서 위로)
|
|
1047
|
-
* - 무한 루프 지원
|
|
1048
|
-
*/
|
|
1049
|
-
class FadeSliderManager {
|
|
1050
|
-
/**
|
|
1051
|
-
* 페이드 슬라이더 컨테이너 생성
|
|
1052
|
-
*/
|
|
1053
|
-
static createFadeSliderContainer(slot, advertisements, options, trackEventCallback) {
|
|
1054
|
-
const sliderWrapper = document.createElement('div');
|
|
1055
|
-
sliderWrapper.className = 'adstage-fade-slider-wrapper';
|
|
1056
|
-
// 래퍼 스타일 설정
|
|
1057
|
-
const containerStyles = {
|
|
1058
|
-
position: 'relative',
|
|
1059
|
-
overflow: 'hidden',
|
|
1060
|
-
display: 'inline-block',
|
|
1061
|
-
};
|
|
1062
|
-
// 사용자가 크기를 지정한 경우
|
|
1063
|
-
if (slot.width && slot.width !== 0) {
|
|
1064
|
-
let width;
|
|
1065
|
-
if (typeof slot.width === 'string') {
|
|
1066
|
-
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
width = `${slot.width}px`;
|
|
1070
|
-
}
|
|
1071
|
-
containerStyles.width = width;
|
|
1072
|
-
}
|
|
1073
|
-
if (slot.height && slot.height !== 0) {
|
|
1074
|
-
let height;
|
|
1075
|
-
if (typeof slot.height === 'string') {
|
|
1076
|
-
height = slot.height.includes('px') || slot.height.includes('%') ? slot.height : `${slot.height}px`;
|
|
1077
|
-
}
|
|
1078
|
-
else {
|
|
1079
|
-
height = `${slot.height}px`;
|
|
1080
|
-
}
|
|
1081
|
-
containerStyles.height = height;
|
|
1082
|
-
}
|
|
1083
|
-
// 스타일 적용
|
|
1084
|
-
Object.entries(containerStyles).forEach(([key, value]) => {
|
|
1085
|
-
sliderWrapper.style.setProperty(key, value);
|
|
1086
|
-
});
|
|
1087
|
-
// 슬라이드 컨테이너
|
|
1088
|
-
const slideContainer = document.createElement('div');
|
|
1089
|
-
slideContainer.className = 'adstage-fade-slide-container';
|
|
1090
|
-
slideContainer.style.cssText = `
|
|
1091
|
-
position: relative;
|
|
1092
|
-
width: 100%;
|
|
1093
|
-
height: 100%;
|
|
1094
|
-
`;
|
|
1095
|
-
// 크기 측정을 위한 임시 컨테이너 (자동 크기 계산이 필요한 경우)
|
|
1096
|
-
let measureContainer = null;
|
|
1097
|
-
const needsWidthMeasurement = !slot.width || slot.width === 0;
|
|
1098
|
-
const needsHeightMeasurement = !slot.height || slot.height === 0;
|
|
1099
|
-
if (needsWidthMeasurement || needsHeightMeasurement) {
|
|
1100
|
-
measureContainer = document.createElement('div');
|
|
1101
|
-
measureContainer.style.cssText = `
|
|
1102
|
-
position: absolute;
|
|
1103
|
-
visibility: hidden;
|
|
1104
|
-
white-space: nowrap;
|
|
1105
|
-
top: -9999px;
|
|
1106
|
-
left: -9999px;
|
|
1107
|
-
`;
|
|
1108
|
-
// width가 설정되어 있으면 측정 컨테이너에도 적용
|
|
1109
|
-
if (!needsWidthMeasurement && slot.width) {
|
|
1110
|
-
let width;
|
|
1111
|
-
if (typeof slot.width === 'string') {
|
|
1112
|
-
width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
|
|
1113
|
-
}
|
|
1114
|
-
else {
|
|
1115
|
-
width = `${slot.width}px`;
|
|
1116
|
-
}
|
|
1117
|
-
measureContainer.style.width = width;
|
|
1118
|
-
measureContainer.style.whiteSpace = 'normal'; // width가 있으면 줄바꿈 허용
|
|
1119
|
-
}
|
|
1120
|
-
document.body.appendChild(measureContainer);
|
|
1121
|
-
let maxWidth = 0;
|
|
1122
|
-
let maxHeight = 0;
|
|
1123
|
-
// 모든 광고의 크기를 측정하여 최대 크기 찾기
|
|
1124
|
-
advertisements.forEach(ad => {
|
|
1125
|
-
const measureAdElement = AdRendererFactory.render(ad, slot, trackEventCallback);
|
|
1126
|
-
measureContainer.appendChild(measureAdElement);
|
|
1127
|
-
const rect = measureAdElement.getBoundingClientRect();
|
|
1128
|
-
if (rect.width > maxWidth)
|
|
1129
|
-
maxWidth = rect.width;
|
|
1130
|
-
if (rect.height > maxHeight)
|
|
1131
|
-
maxHeight = rect.height;
|
|
1132
|
-
// 측정 후 요소 제거
|
|
1133
|
-
measureContainer.removeChild(measureAdElement);
|
|
1134
|
-
});
|
|
1135
|
-
// 측정된 최대 크기로 래퍼 크기 설정
|
|
1136
|
-
if (needsWidthMeasurement && maxWidth > 0) {
|
|
1137
|
-
sliderWrapper.style.width = `${maxWidth}px`;
|
|
1138
|
-
}
|
|
1139
|
-
if (needsHeightMeasurement && maxHeight > 0) {
|
|
1140
|
-
sliderWrapper.style.height = `${maxHeight}px`;
|
|
1141
|
-
}
|
|
1142
|
-
// 측정 컨테이너 제거
|
|
1143
|
-
document.body.removeChild(measureContainer);
|
|
1144
|
-
}
|
|
1145
|
-
// 각 광고를 슬라이드로 생성
|
|
1146
|
-
const slideElements = [];
|
|
1147
|
-
advertisements.forEach((ad, index) => {
|
|
1148
|
-
const slideElement = document.createElement('div');
|
|
1149
|
-
slideElement.className = 'adstage-fade-slide';
|
|
1150
|
-
slideElement.style.cssText = `
|
|
1151
|
-
position: absolute;
|
|
1152
|
-
top: 0;
|
|
1153
|
-
left: 0;
|
|
1154
|
-
width: 100%;
|
|
1155
|
-
height: 100%;
|
|
1156
|
-
display: flex;
|
|
1157
|
-
align-items: center;
|
|
1158
|
-
justify-content: center;
|
|
1159
|
-
opacity: ${index === 0 ? '1' : '0'};
|
|
1160
|
-
transform: translateY(${index === 0 ? '0' : '20px'});
|
|
1161
|
-
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1162
|
-
z-index: ${index === 0 ? '2' : '1'};
|
|
1163
|
-
`;
|
|
1164
|
-
// 광고 렌더링
|
|
1165
|
-
const adElement = AdRendererFactory.render(ad, slot, trackEventCallback);
|
|
1166
|
-
slideElement.appendChild(adElement);
|
|
1167
|
-
slideContainer.appendChild(slideElement);
|
|
1168
|
-
slideElements.push(slideElement);
|
|
1169
|
-
});
|
|
1170
|
-
// 슬라이더 상태 관리
|
|
1171
|
-
let currentSlide = 0;
|
|
1172
|
-
const totalSlides = advertisements.length;
|
|
1173
|
-
const autoSlideInterval = (options?.autoSlideInterval || 4) * 1000; // 기본 4초 (페이드는 조금 더 길게)
|
|
1174
|
-
// 슬라이드 이동 함수 (페이드 효과)
|
|
1175
|
-
const moveToSlide = (index) => {
|
|
1176
|
-
if (index >= totalSlides) {
|
|
1177
|
-
index = 0; // 무한 루프
|
|
1178
|
-
}
|
|
1179
|
-
else if (index < 0) {
|
|
1180
|
-
index = totalSlides - 1;
|
|
1181
|
-
}
|
|
1182
|
-
const previousSlide = slideElements[currentSlide];
|
|
1183
|
-
const nextSlide = slideElements[index];
|
|
1184
|
-
// 이전 슬라이드 페이드 아웃 (아래로)
|
|
1185
|
-
previousSlide.style.opacity = '0';
|
|
1186
|
-
previousSlide.style.transform = 'translateY(-20px)';
|
|
1187
|
-
previousSlide.style.zIndex = '1';
|
|
1188
|
-
// 다음 슬라이드 페이드 인 (위에서)
|
|
1189
|
-
nextSlide.style.opacity = '1';
|
|
1190
|
-
nextSlide.style.transform = 'translateY(0)';
|
|
1191
|
-
nextSlide.style.zIndex = '2';
|
|
1192
|
-
// 다른 슬라이드들은 숨김
|
|
1193
|
-
slideElements.forEach((slide, i) => {
|
|
1194
|
-
if (i !== index && i !== currentSlide) {
|
|
1195
|
-
slide.style.opacity = '0';
|
|
1196
|
-
slide.style.transform = 'translateY(20px)';
|
|
1197
|
-
slide.style.zIndex = '1';
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
currentSlide = index;
|
|
1201
|
-
// 현재 슬라이드의 광고에 대해 노출 이벤트 추적
|
|
1202
|
-
if (currentSlide > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
|
|
1203
|
-
trackEventCallback(advertisements[currentSlide]._id, slot.id, AdEventType.IMPRESSION);
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
// 자동 슬라이드
|
|
1207
|
-
let autoSlideTimer = setInterval(() => {
|
|
1208
|
-
const nextIndex = currentSlide + 1;
|
|
1209
|
-
moveToSlide(nextIndex);
|
|
1210
|
-
}, autoSlideInterval);
|
|
1211
|
-
// 마우스 호버 시 자동 슬라이드 일시정지
|
|
1212
|
-
sliderWrapper.addEventListener('mouseenter', () => {
|
|
1213
|
-
clearInterval(autoSlideTimer);
|
|
1214
|
-
});
|
|
1215
|
-
sliderWrapper.addEventListener('mouseleave', () => {
|
|
1216
|
-
autoSlideTimer = setInterval(() => {
|
|
1217
|
-
const nextIndex = currentSlide + 1;
|
|
1218
|
-
moveToSlide(nextIndex);
|
|
1219
|
-
}, autoSlideInterval);
|
|
1220
|
-
});
|
|
1221
|
-
// 터치 제스처 지원
|
|
1222
|
-
FadeSliderManager.addTouchSupport(sliderWrapper, moveToSlide, () => currentSlide, totalSlides);
|
|
1223
|
-
// 요소들 조립
|
|
1224
|
-
sliderWrapper.appendChild(slideContainer);
|
|
1225
|
-
return sliderWrapper;
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* 터치 제스처 지원 추가
|
|
1229
|
-
*/
|
|
1230
|
-
static addTouchSupport(container, moveToSlide, getCurrentSlide, totalSlides) {
|
|
1231
|
-
let startX = 0;
|
|
1232
|
-
let startY = 0;
|
|
1233
|
-
let isDragging = false;
|
|
1234
|
-
container.addEventListener('touchstart', (e) => {
|
|
1235
|
-
startX = e.touches[0].clientX;
|
|
1236
|
-
startY = e.touches[0].clientY;
|
|
1237
|
-
isDragging = true;
|
|
1238
|
-
});
|
|
1239
|
-
container.addEventListener('touchmove', (e) => {
|
|
1240
|
-
if (!isDragging)
|
|
1241
|
-
return;
|
|
1242
|
-
e.preventDefault();
|
|
1243
|
-
});
|
|
1244
|
-
container.addEventListener('touchend', (e) => {
|
|
1245
|
-
if (!isDragging)
|
|
1246
|
-
return;
|
|
1247
|
-
isDragging = false;
|
|
1248
|
-
const endX = e.changedTouches[0].clientX;
|
|
1249
|
-
const endY = e.changedTouches[0].clientY;
|
|
1250
|
-
const diffX = startX - endX;
|
|
1251
|
-
const diffY = startY - endY;
|
|
1252
|
-
// 가로 스와이프가 세로 스와이프보다 클 때만 처리
|
|
1253
|
-
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
|
1254
|
-
const currentSlide = getCurrentSlide();
|
|
1255
|
-
if (diffX > 0) {
|
|
1256
|
-
// 왼쪽으로 스와이프 (다음 슬라이드)
|
|
1257
|
-
moveToSlide(currentSlide + 1);
|
|
1258
|
-
}
|
|
1259
|
-
else {
|
|
1260
|
-
// 오른쪽으로 스와이프 (이전 슬라이드)
|
|
1261
|
-
moveToSlide(currentSlide - 1);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/**
|
|
1269
|
-
* 노출 추적 및 중복 방지 관리 클래스
|
|
1270
|
-
* - 메모리 기반 중복 확인
|
|
1271
|
-
* - 세션 스토리지 기반 영구 추적
|
|
1272
|
-
* - 자동 정리 기능
|
|
1273
|
-
*/
|
|
1274
|
-
class ImpressionTracker {
|
|
1275
|
-
/**
|
|
1276
|
-
* 중복 노출 여부 확인
|
|
1277
|
-
*/
|
|
1278
|
-
static isDuplicateImpression(adId, slotId, debug = false) {
|
|
1279
|
-
const key = `${adId}_${slotId}`;
|
|
1280
|
-
const now = Date.now();
|
|
1281
|
-
// 메모리 기반 중복 확인 (새로고침 시 초기화됨)
|
|
1282
|
-
const lastImpression = ImpressionTracker.impressionTracker.get(key);
|
|
1283
|
-
if (lastImpression && (now - lastImpression) < ImpressionTracker.IMPRESSION_COOLDOWN) {
|
|
1284
|
-
if (debug) {
|
|
1285
|
-
console.log(`Duplicate impression blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ImpressionTracker.IMPRESSION_COOLDOWN - (now - lastImpression)) / 1000)}s remaining`);
|
|
1286
|
-
}
|
|
1287
|
-
return true;
|
|
1288
|
-
}
|
|
1289
|
-
// 세션 스토리지 기반 중복 확인 (새로고침 시에도 유지)
|
|
1290
|
-
const sessionKey = `adstage_impression_${key}`;
|
|
1291
|
-
const sessionImpression = sessionStorage.getItem(sessionKey);
|
|
1292
|
-
if (sessionImpression) {
|
|
1293
|
-
const sessionTime = parseInt(sessionImpression, 10);
|
|
1294
|
-
if (!isNaN(sessionTime) && (now - sessionTime) < ImpressionTracker.IMPRESSION_COOLDOWN) {
|
|
1295
|
-
if (debug) {
|
|
1296
|
-
console.log(`Session-based duplicate impression blocked for ad ${adId} in slot ${slotId}. Cooldown: ${Math.round((ImpressionTracker.IMPRESSION_COOLDOWN - (now - sessionTime)) / 1000)}s remaining`);
|
|
1297
|
-
}
|
|
1298
|
-
// 메모리에도 기록하여 이후 요청 최적화
|
|
1299
|
-
ImpressionTracker.impressionTracker.set(key, sessionTime);
|
|
1300
|
-
return true;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
// 노출 시점 기록 (메모리 + 세션 스토리지)
|
|
1304
|
-
ImpressionTracker.impressionTracker.set(key, now);
|
|
1305
|
-
sessionStorage.setItem(sessionKey, now.toString());
|
|
1306
|
-
// 오래된 세션 스토리지 데이터 정리 (선택적)
|
|
1307
|
-
ImpressionTracker.cleanupOldImpressions();
|
|
1308
|
-
return false;
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* 오래된 노출 추적 데이터 정리
|
|
1312
|
-
*/
|
|
1313
|
-
static cleanupOldImpressions() {
|
|
1314
|
-
const now = Date.now();
|
|
1315
|
-
const cleanupThreshold = ImpressionTracker.IMPRESSION_COOLDOWN * 2; // 쿨다운의 2배 시간이 지난 데이터 정리
|
|
1316
|
-
// 세션 스토리지 정리
|
|
1317
|
-
for (let i = 0; i < sessionStorage.length; i++) {
|
|
1318
|
-
const key = sessionStorage.key(i);
|
|
1319
|
-
if (key && key.startsWith('adstage_impression_')) {
|
|
1320
|
-
const timestamp = sessionStorage.getItem(key);
|
|
1321
|
-
if (timestamp) {
|
|
1322
|
-
const time = parseInt(timestamp, 10);
|
|
1323
|
-
if (!isNaN(time) && (now - time) > cleanupThreshold) {
|
|
1324
|
-
sessionStorage.removeItem(key);
|
|
1325
|
-
i--; // 인덱스 조정
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
// 메모리 정리
|
|
1331
|
-
for (const [key, timestamp] of ImpressionTracker.impressionTracker.entries()) {
|
|
1332
|
-
if ((now - timestamp) > cleanupThreshold) {
|
|
1333
|
-
ImpressionTracker.impressionTracker.delete(key);
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* 모든 추적 데이터 정리
|
|
1339
|
-
*/
|
|
1340
|
-
static clear() {
|
|
1341
|
-
ImpressionTracker.impressionTracker.clear();
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
ImpressionTracker.impressionTracker = new Map();
|
|
1345
|
-
ImpressionTracker.IMPRESSION_COOLDOWN = 300000; // 5분 쿨다운
|
|
1346
|
-
|
|
1347
|
-
/**
|
|
1348
|
-
* 디바이스 정보 수집 클래스
|
|
1349
|
-
* - 브라우저 환경 정보 수집
|
|
1350
|
-
* - 디바이스 ID 생성 및 관리
|
|
1351
|
-
* - 세션 ID 생성 및 관리
|
|
1352
|
-
*/
|
|
1353
|
-
class DeviceInfoCollector {
|
|
1354
|
-
/**
|
|
1355
|
-
* 디바이스 ID 생성 및 반환 (SSR 안전)
|
|
1356
|
-
*/
|
|
1357
|
-
static generateDeviceId() {
|
|
1358
|
-
if (!DOMUtils.isBrowser())
|
|
1359
|
-
return 'ssr_device_' + Date.now();
|
|
1360
|
-
const stored = localStorage.getItem('adstage_device_id');
|
|
1361
|
-
if (stored)
|
|
1362
|
-
return stored;
|
|
1363
|
-
const deviceId = 'device_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
1364
|
-
localStorage.setItem('adstage_device_id', deviceId);
|
|
1365
|
-
return deviceId;
|
|
1366
|
-
}
|
|
1367
|
-
/**
|
|
1368
|
-
* 세션 ID 생성 및 반환 (SSR 안전)
|
|
1369
|
-
*/
|
|
1370
|
-
static generateSessionId() {
|
|
1371
|
-
if (!DOMUtils.isBrowser())
|
|
1372
|
-
return 'ssr_session_' + Date.now();
|
|
1373
|
-
const stored = sessionStorage.getItem('adstage_session_id');
|
|
1374
|
-
if (stored)
|
|
1375
|
-
return stored;
|
|
1376
|
-
const sessionId = 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
1377
|
-
sessionStorage.setItem('adstage_session_id', sessionId);
|
|
1378
|
-
return sessionId;
|
|
1379
|
-
}
|
|
1380
|
-
/**
|
|
1381
|
-
* 모바일 디바이스 여부 확인
|
|
1382
|
-
*/
|
|
1383
|
-
static isMobile() {
|
|
1384
|
-
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
1385
|
-
}
|
|
1386
|
-
/**
|
|
1387
|
-
* 플랫폼 타입 반환 (서버 enum에 맞춤)
|
|
1388
|
-
*/
|
|
1389
|
-
static getPlatform() {
|
|
1390
|
-
const userAgent = navigator.userAgent.toLowerCase();
|
|
1391
|
-
if (/iphone|ipad|ipod/.test(userAgent)) {
|
|
1392
|
-
return 'ios';
|
|
1393
|
-
}
|
|
1394
|
-
if (/android/.test(userAgent)) {
|
|
1395
|
-
return 'android';
|
|
1396
|
-
}
|
|
1397
|
-
if (DeviceInfoCollector.isMobile()) {
|
|
1398
|
-
return 'web'; // 모바일 웹
|
|
1399
|
-
}
|
|
1400
|
-
return 'desktop'; // 데스크톱 웹
|
|
1401
|
-
}
|
|
1402
|
-
/**
|
|
1403
|
-
* 완전한 디바이스 정보 수집
|
|
1404
|
-
*/
|
|
1405
|
-
static collectDeviceInfo() {
|
|
1406
|
-
const viewportInfo = DOMUtils.getViewportInfo();
|
|
1407
|
-
return {
|
|
1408
|
-
deviceId: DeviceInfoCollector.generateDeviceId(),
|
|
1409
|
-
sessionId: DeviceInfoCollector.generateSessionId(),
|
|
1410
|
-
osVersion: DOMUtils.isBrowser() ? navigator.platform : 'SSR',
|
|
1411
|
-
deviceModel: DOMUtils.isBrowser() ? navigator.platform : 'SSR',
|
|
1412
|
-
appVersion: '1.0.0',
|
|
1413
|
-
sdkVersion: '1.0.0',
|
|
1414
|
-
language: DOMUtils.isBrowser() ? (navigator.language || 'ko') : 'ko',
|
|
1415
|
-
country: 'KR', // 기본값
|
|
1416
|
-
ipAddress: '', // 서버에서 자동으로 설정됨
|
|
1417
|
-
userAgent: DOMUtils.isBrowser() ? navigator.userAgent : 'SSR',
|
|
1418
|
-
timezone: DOMUtils.isBrowser() ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'UTC',
|
|
1419
|
-
viewportWidth: viewportInfo.width,
|
|
1420
|
-
viewportHeight: viewportInfo.height,
|
|
1421
|
-
screenWidth: DOMUtils.isBrowser() ? screen.width : 0,
|
|
1422
|
-
screenHeight: DOMUtils.isBrowser() ? screen.height : 0,
|
|
1423
|
-
colorDepth: DOMUtils.isBrowser() ? screen.colorDepth : 24,
|
|
1424
|
-
pixelRatio: viewportInfo.pixelRatio,
|
|
1425
|
-
connectionType: DOMUtils.isBrowser() ? (navigator.connection?.effectiveType || 'unknown') : 'unknown',
|
|
1426
|
-
platform: DeviceInfoCollector.getPlatform(),
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
/**
|
|
1430
|
-
* 슬롯 위치 정보 가져오기 (SSR 안전)
|
|
1431
|
-
*/
|
|
1432
|
-
static getSlotPosition(containerId) {
|
|
1433
|
-
const element = DOMUtils.safeGetElementById(containerId);
|
|
1434
|
-
if (!element)
|
|
1435
|
-
return 'unknown';
|
|
1436
|
-
const rect = element.getBoundingClientRect();
|
|
1437
|
-
const scrollInfo = DOMUtils.getScrollInfo();
|
|
1438
|
-
return `x:${Math.round(rect.left)},y:${Math.round(rect.top + scrollInfo.scrollTop)}`;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* 이벤트 추적 관리 클래스
|
|
1444
|
-
* - 광고 이벤트 추적 및 전송
|
|
1445
|
-
* - 중복 노출 방지 통합
|
|
1446
|
-
* - 서버 API 통신
|
|
1447
|
-
*/
|
|
1448
|
-
class EventTracker {
|
|
1449
|
-
constructor(baseUrl, apiKey, debug, slots) {
|
|
1450
|
-
this.baseUrl = baseUrl;
|
|
1451
|
-
this.apiKey = apiKey;
|
|
1452
|
-
this.debug = debug;
|
|
1453
|
-
this.slots = slots;
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* 이벤트 추적
|
|
1457
|
-
*/
|
|
1458
|
-
async trackEvent(adId, slotId, eventType) {
|
|
1459
|
-
try {
|
|
1460
|
-
// 노출 이벤트의 경우 중복 확인
|
|
1461
|
-
if (eventType === AdEventType.IMPRESSION) {
|
|
1462
|
-
if (ImpressionTracker.isDuplicateImpression(adId, slotId, this.debug)) {
|
|
1463
|
-
return; // 중복 노출이므로 추적하지 않음
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
// 현재 슬롯 정보 가져오기
|
|
1467
|
-
const slot = this.slots.get(slotId);
|
|
1468
|
-
// 디바이스 정보 수집
|
|
1469
|
-
const deviceInfo = DeviceInfoCollector.collectDeviceInfo();
|
|
1470
|
-
// 이벤트 데이터 구성 (MongoDB 스키마에 맞춤)
|
|
1471
|
-
const eventData = {
|
|
1472
|
-
// 서버에서 자동 설정: orgId, advertisementId, action
|
|
1473
|
-
// 하지만 DTO 검증을 위해 임시값 제공
|
|
1474
|
-
orgId: 'temp', // 서버에서 API 키로부터 덮어씀
|
|
1475
|
-
advertisementId: adId, // 서버에서 URL 파라미터로부터 덮어씀
|
|
1476
|
-
action: eventType, // 서버에서 URL 파라미터로부터 덮어씀
|
|
1477
|
-
// 필수 필드들
|
|
1478
|
-
adType: slot?.adType || 'BANNER',
|
|
1479
|
-
platform: deviceInfo.platform,
|
|
1480
|
-
// 디바이스 정보를 최상위로 플래튼
|
|
1481
|
-
deviceId: deviceInfo.deviceId,
|
|
1482
|
-
osVersion: deviceInfo.osVersion,
|
|
1483
|
-
deviceModel: deviceInfo.deviceModel,
|
|
1484
|
-
appVersion: deviceInfo.appVersion,
|
|
1485
|
-
sdkVersion: deviceInfo.sdkVersion,
|
|
1486
|
-
language: deviceInfo.language,
|
|
1487
|
-
country: deviceInfo.country,
|
|
1488
|
-
ipAddress: deviceInfo.ipAddress,
|
|
1489
|
-
userAgent: deviceInfo.userAgent,
|
|
1490
|
-
timezone: deviceInfo.timezone,
|
|
1491
|
-
viewportWidth: deviceInfo.viewportWidth,
|
|
1492
|
-
viewportHeight: deviceInfo.viewportHeight,
|
|
1493
|
-
screenWidth: deviceInfo.screenWidth,
|
|
1494
|
-
screenHeight: deviceInfo.screenHeight,
|
|
1495
|
-
connectionType: deviceInfo.connectionType,
|
|
1496
|
-
// 페이지 및 슬롯 정보
|
|
1497
|
-
pageUrl: DOMUtils.getPageInfo().url,
|
|
1498
|
-
pageTitle: DOMUtils.getPageInfo().title,
|
|
1499
|
-
referrer: DOMUtils.getPageInfo().referrer,
|
|
1500
|
-
slotId,
|
|
1501
|
-
slotPosition: DeviceInfoCollector.getSlotPosition(slot?.containerId || ''),
|
|
1502
|
-
slotWidth: EventTracker.parseNumericValue(slot?.width),
|
|
1503
|
-
slotHeight: EventTracker.parseNumericValue(slot?.height),
|
|
1504
|
-
sessionId: deviceInfo.sessionId,
|
|
1505
|
-
// 성능 메트릭
|
|
1506
|
-
pageLoadTime: performance.now(),
|
|
1507
|
-
timestamp: new Date().toISOString(),
|
|
1508
|
-
// 추가 메타데이터
|
|
1509
|
-
metadata: {
|
|
1510
|
-
eventType,
|
|
1511
|
-
sdkVersion: '1.0.0',
|
|
1512
|
-
timestamp: Date.now(),
|
|
1513
|
-
},
|
|
1514
|
-
};
|
|
1515
|
-
await fetch(`${this.baseUrl}/advertisements/events/${adId}/${eventType}`, {
|
|
1516
|
-
method: 'POST',
|
|
1517
|
-
headers: {
|
|
1518
|
-
'x-api-key': this.apiKey,
|
|
1519
|
-
'Content-Type': 'application/json',
|
|
1520
|
-
},
|
|
1521
|
-
body: JSON.stringify(eventData),
|
|
1522
|
-
});
|
|
1523
|
-
if (this.debug) {
|
|
1524
|
-
console.log(`Tracked event: ${eventType} for ad ${adId}`, eventData);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
catch (error) {
|
|
1528
|
-
console.error('Failed to track event:', error);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
/**
|
|
1532
|
-
* 크기 값을 숫자로 변환 (서버 API용)
|
|
1533
|
-
*/
|
|
1534
|
-
static parseNumericValue(value) {
|
|
1535
|
-
if (typeof value === 'number') {
|
|
1536
|
-
return value;
|
|
1537
|
-
}
|
|
1538
|
-
if (typeof value === 'string') {
|
|
1539
|
-
// px 단위 제거하고 숫자만 추출
|
|
1540
|
-
const numericValue = parseFloat(value.replace(/px$/, ''));
|
|
1541
|
-
return isNaN(numericValue) ? 0 : numericValue;
|
|
1542
|
-
}
|
|
1543
|
-
return 0; // 기본값
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
/**
|
|
1548
|
-
* SDK 유틸리티 함수 모음
|
|
1549
|
-
* - DOM 요소 속성 처리
|
|
1550
|
-
* - 자동 슬롯 검색
|
|
1551
|
-
* - 기타 헬퍼 함수들
|
|
1552
|
-
*/
|
|
1553
|
-
class SDKUtils {
|
|
1554
|
-
/**
|
|
1555
|
-
* HTML 요소에서 속성값 가져오기 (data- 프리픽스 선택적)
|
|
1556
|
-
*/
|
|
1557
|
-
static getElementAttribute(element, attrName) {
|
|
1558
|
-
// 1. data- 프리픽스가 있는 경우 우선
|
|
1559
|
-
const dataAttr = element.dataset[attrName];
|
|
1560
|
-
if (dataAttr)
|
|
1561
|
-
return dataAttr;
|
|
1562
|
-
// 2. 직접 속성 확인
|
|
1563
|
-
const directAttr = element.getAttribute(attrName);
|
|
1564
|
-
if (directAttr)
|
|
1565
|
-
return directAttr;
|
|
1566
|
-
// 3. 케밥 케이스로 확인 (ad-type -> adType)
|
|
1567
|
-
const kebabAttr = element.getAttribute(attrName.replace(/[A-Z]/g, '-$&').toLowerCase());
|
|
1568
|
-
if (kebabAttr)
|
|
1569
|
-
return kebabAttr;
|
|
1570
|
-
return undefined;
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* DOM에서 자동 슬롯 요소 찾기 (SSR 안전)
|
|
1574
|
-
*/
|
|
1575
|
-
static findAutoSlotElements() {
|
|
1576
|
-
if (typeof document === 'undefined')
|
|
1577
|
-
return [];
|
|
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
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* AdType enum 값으로 변환
|
|
1614
|
-
*/
|
|
1615
|
-
static parseAdType(adTypeStr, AdType) {
|
|
1616
|
-
return AdType[adTypeStr] || AdType.BANNER;
|
|
1617
|
-
}
|
|
1618
|
-
/**
|
|
1619
|
-
* 브라우저 환경 체크
|
|
1620
|
-
*/
|
|
1621
|
-
static isBrowser() {
|
|
1622
|
-
return typeof window !== 'undefined';
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* SSR 환경 체크
|
|
1626
|
-
*/
|
|
1627
|
-
static isSSR() {
|
|
1628
|
-
return typeof window === 'undefined';
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* DOM 사용 가능 체크
|
|
1632
|
-
*/
|
|
1633
|
-
static canUseDOM() {
|
|
1634
|
-
return !this.isSSR() && typeof document !== 'undefined';
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* DOM 로드 완료 체크 (SSR 안전)
|
|
1638
|
-
*/
|
|
1639
|
-
static isDOMReady() {
|
|
1640
|
-
if (!this.canUseDOM())
|
|
1641
|
-
return false;
|
|
1642
|
-
return document.readyState !== 'loading';
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* DOM 로드 완료 대기 (SSR 안전)
|
|
1646
|
-
*/
|
|
1647
|
-
static waitForDOM() {
|
|
1648
|
-
return new Promise((resolve) => {
|
|
1649
|
-
if (!this.canUseDOM()) {
|
|
1650
|
-
resolve(); // SSR 환경에서는 즉시 resolve
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
if (SDKUtils.isDOMReady()) {
|
|
1654
|
-
resolve();
|
|
1655
|
-
}
|
|
1656
|
-
else {
|
|
1657
|
-
document.addEventListener('DOMContentLoaded', () => resolve());
|
|
1658
|
-
}
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
var jsxRuntime = {exports: {}};
|
|
1664
|
-
|
|
1665
|
-
var reactJsxRuntime_production_min = {};
|
|
1666
|
-
|
|
1667
|
-
var react = {exports: {}};
|
|
1668
|
-
|
|
1669
|
-
var react_production_min = {};
|
|
1670
|
-
|
|
1671
|
-
/**
|
|
1672
|
-
* @license React
|
|
1673
|
-
* react.production.min.js
|
|
1674
|
-
*
|
|
1675
|
-
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
1676
|
-
*
|
|
1677
|
-
* This source code is licensed under the MIT license found in the
|
|
1678
|
-
* LICENSE file in the root directory of this source tree.
|
|
1679
|
-
*/
|
|
1680
|
-
|
|
1681
|
-
var hasRequiredReact_production_min;
|
|
1682
|
-
|
|
1683
|
-
function requireReact_production_min () {
|
|
1684
|
-
if (hasRequiredReact_production_min) return react_production_min;
|
|
1685
|
-
hasRequiredReact_production_min = 1;
|
|
1686
|
-
var l=Symbol.for("react.element"),n=Symbol.for("react.portal"),p=Symbol.for("react.fragment"),q=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),t=Symbol.for("react.provider"),u=Symbol.for("react.context"),v=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),x=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),z=Symbol.iterator;function A(a){if(null===a||"object"!==typeof a)return null;a=z&&a[z]||a["@@iterator"];return "function"===typeof a?a:null}
|
|
1687
|
-
var B={isMounted:function(){return !1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,D={};function E(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B;}E.prototype.isReactComponent={};
|
|
1688
|
-
E.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState");};E.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate");};function F(){}F.prototype=E.prototype;function G(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B;}var H=G.prototype=new F;
|
|
1689
|
-
H.constructor=G;C(H,E.prototype);H.isPureReactComponent=!0;var I=Array.isArray,J=Object.prototype.hasOwnProperty,K={current:null},L={key:!0,ref:!0,__self:!0,__source:!0};
|
|
1690
|
-
function M(a,b,e){var d,c={},k=null,h=null;if(null!=b)for(d in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(k=""+b.key),b)J.call(b,d)&&!L.hasOwnProperty(d)&&(c[d]=b[d]);var g=arguments.length-2;if(1===g)c.children=e;else if(1<g){for(var f=Array(g),m=0;m<g;m++)f[m]=arguments[m+2];c.children=f;}if(a&&a.defaultProps)for(d in g=a.defaultProps,g)void 0===c[d]&&(c[d]=g[d]);return {$$typeof:l,type:a,key:k,ref:h,props:c,_owner:K.current}}
|
|
1691
|
-
function N(a,b){return {$$typeof:l,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function O(a){return "object"===typeof a&&null!==a&&a.$$typeof===l}function escape(a){var b={"=":"=0",":":"=2"};return "$"+a.replace(/[=:]/g,function(a){return b[a]})}var P=/\/+/g;function Q(a,b){return "object"===typeof a&&null!==a&&null!=a.key?escape(""+a.key):b.toString(36)}
|
|
1692
|
-
function R(a,b,e,d,c){var k=typeof a;if("undefined"===k||"boolean"===k)a=null;var h=!1;if(null===a)h=!0;else switch(k){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case l:case n:h=!0;}}if(h)return h=a,c=c(h),a=""===d?"."+Q(h,0):d,I(c)?(e="",null!=a&&(e=a.replace(P,"$&/")+"/"),R(c,b,e,"",function(a){return a})):null!=c&&(O(c)&&(c=N(c,e+(!c.key||h&&h.key===c.key?"":(""+c.key).replace(P,"$&/")+"/")+a)),b.push(c)),1;h=0;d=""===d?".":d+":";if(I(a))for(var g=0;g<a.length;g++){k=
|
|
1693
|
-
a[g];var f=d+Q(k,g);h+=R(k,b,e,f,c);}else if(f=A(a),"function"===typeof f)for(a=f.call(a),g=0;!(k=a.next()).done;)k=k.value,f=d+Q(k,g++),h+=R(k,b,e,f,c);else if("object"===k)throw b=String(a),Error("Objects are not valid as a React child (found: "+("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}
|
|
1694
|
-
function S(a,b,e){if(null==a)return a;var d=[],c=0;R(a,d,"","",function(a){return b.call(e,a,c++)});return d}function T(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b;},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b;});-1===a._status&&(a._status=0,a._result=b);}if(1===a._status)return a._result.default;throw a._result;}
|
|
1695
|
-
var U={current:null},V={transition:null},W={ReactCurrentDispatcher:U,ReactCurrentBatchConfig:V,ReactCurrentOwner:K};function X(){throw Error("act(...) is not supported in production builds of React.");}
|
|
1696
|
-
react_production_min.Children={map:S,forEach:function(a,b,e){S(a,function(){b.apply(this,arguments);},e);},count:function(a){var b=0;S(a,function(){b++;});return b},toArray:function(a){return S(a,function(a){return a})||[]},only:function(a){if(!O(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};react_production_min.Component=E;react_production_min.Fragment=p;react_production_min.Profiler=r;react_production_min.PureComponent=G;react_production_min.StrictMode=q;react_production_min.Suspense=w;
|
|
1697
|
-
react_production_min.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=W;react_production_min.act=X;
|
|
1698
|
-
react_production_min.cloneElement=function(a,b,e){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var d=C({},a.props),c=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=K.current);void 0!==b.key&&(c=""+b.key);if(a.type&&a.type.defaultProps)var g=a.type.defaultProps;for(f in b)J.call(b,f)&&!L.hasOwnProperty(f)&&(d[f]=void 0===b[f]&&void 0!==g?g[f]:b[f]);}var f=arguments.length-2;if(1===f)d.children=e;else if(1<f){g=Array(f);
|
|
1699
|
-
for(var m=0;m<f;m++)g[m]=arguments[m+2];d.children=g;}return {$$typeof:l,type:a.type,key:c,ref:k,props:d,_owner:h}};react_production_min.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};react_production_min.createElement=M;react_production_min.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};react_production_min.createRef=function(){return {current:null}};
|
|
1700
|
-
react_production_min.forwardRef=function(a){return {$$typeof:v,render:a}};react_production_min.isValidElement=O;react_production_min.lazy=function(a){return {$$typeof:y,_payload:{_status:-1,_result:a},_init:T}};react_production_min.memo=function(a,b){return {$$typeof:x,type:a,compare:void 0===b?null:b}};react_production_min.startTransition=function(a){var b=V.transition;V.transition={};try{a();}finally{V.transition=b;}};react_production_min.unstable_act=X;react_production_min.useCallback=function(a,b){return U.current.useCallback(a,b)};react_production_min.useContext=function(a){return U.current.useContext(a)};
|
|
1701
|
-
react_production_min.useDebugValue=function(){};react_production_min.useDeferredValue=function(a){return U.current.useDeferredValue(a)};react_production_min.useEffect=function(a,b){return U.current.useEffect(a,b)};react_production_min.useId=function(){return U.current.useId()};react_production_min.useImperativeHandle=function(a,b,e){return U.current.useImperativeHandle(a,b,e)};react_production_min.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};react_production_min.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};
|
|
1702
|
-
react_production_min.useMemo=function(a,b){return U.current.useMemo(a,b)};react_production_min.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};react_production_min.useRef=function(a){return U.current.useRef(a)};react_production_min.useState=function(a){return U.current.useState(a)};react_production_min.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)};react_production_min.useTransition=function(){return U.current.useTransition()};react_production_min.version="18.3.1";
|
|
1703
|
-
return react_production_min;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
{
|
|
1707
|
-
react.exports = requireReact_production_min();
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
var reactExports = react.exports;
|
|
1711
|
-
|
|
1712
|
-
/**
|
|
1713
|
-
* @license React
|
|
1714
|
-
* react-jsx-runtime.production.min.js
|
|
1715
|
-
*
|
|
1716
|
-
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
1717
|
-
*
|
|
1718
|
-
* This source code is licensed under the MIT license found in the
|
|
1719
|
-
* LICENSE file in the root directory of this source tree.
|
|
1720
|
-
*/
|
|
1721
|
-
|
|
1722
|
-
var hasRequiredReactJsxRuntime_production_min;
|
|
1723
|
-
|
|
1724
|
-
function requireReactJsxRuntime_production_min () {
|
|
1725
|
-
if (hasRequiredReactJsxRuntime_production_min) return reactJsxRuntime_production_min;
|
|
1726
|
-
hasRequiredReactJsxRuntime_production_min = 1;
|
|
1727
|
-
var f=reactExports,k=Symbol.for("react.element"),l=Symbol.for("react.fragment"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};
|
|
1728
|
-
function q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=""+g);void 0!==a.key&&(e=""+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return {$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}reactJsxRuntime_production_min.Fragment=l;reactJsxRuntime_production_min.jsx=q;reactJsxRuntime_production_min.jsxs=q;
|
|
1729
|
-
return reactJsxRuntime_production_min;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
{
|
|
1733
|
-
jsxRuntime.exports = requireReactJsxRuntime_production_min();
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
var jsxRuntimeExports = jsxRuntime.exports;
|
|
1737
|
-
|
|
1738
|
-
const AdStageContext = reactExports.createContext({
|
|
1739
|
-
sdk: null,
|
|
1740
|
-
isLoading: true,
|
|
1741
|
-
error: null,
|
|
1742
|
-
isInitialized: false,
|
|
1743
|
-
});
|
|
1744
|
-
const AdStageProvider = ({ config, children, autoInit = true, }) => {
|
|
1745
|
-
const [sdk, setSdk] = reactExports.useState(null);
|
|
1746
|
-
const [isLoading, setIsLoading] = reactExports.useState(true);
|
|
1747
|
-
const [error, setError] = reactExports.useState(null);
|
|
1748
|
-
const [isInitialized, setIsInitialized] = reactExports.useState(false);
|
|
1749
|
-
reactExports.useEffect(() => {
|
|
1750
|
-
const initializeSDK = async () => {
|
|
1751
|
-
try {
|
|
1752
|
-
setIsLoading(true);
|
|
1753
|
-
setError(null);
|
|
1754
|
-
// Dynamic import를 사용해서 circular dependency 방지
|
|
1755
|
-
const { AdStageSDK } = await Promise.resolve().then(function () { return index; });
|
|
1756
|
-
// SDK 인스턴스 생성
|
|
1757
|
-
const sdkInstance = AdStageSDK.init(config);
|
|
1758
|
-
setSdk(sdkInstance);
|
|
1759
|
-
setIsInitialized(true);
|
|
1760
|
-
setIsLoading(false);
|
|
1761
|
-
}
|
|
1762
|
-
catch (err) {
|
|
1763
|
-
const errorMessage = err instanceof Error ? err.message : 'SDK 초기화 중 오류가 발생했습니다.';
|
|
1764
|
-
setError(errorMessage);
|
|
1765
|
-
setIsLoading(false);
|
|
1766
|
-
console.error('AdStage SDK initialization failed:', err);
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
initializeSDK();
|
|
1770
|
-
}, [config.apiKey, config.debug, autoInit]);
|
|
1771
|
-
const value = {
|
|
1772
|
-
sdk,
|
|
1773
|
-
isLoading,
|
|
1774
|
-
error,
|
|
1775
|
-
isInitialized,
|
|
1776
|
-
};
|
|
1777
|
-
return (jsxRuntimeExports.jsx(AdStageContext.Provider, { value: value, children: children }));
|
|
1778
|
-
};
|
|
1779
|
-
|
|
1780
|
-
/**
|
|
1781
|
-
* AdStage SDK 인스턴스에 접근하기 위한 Hook
|
|
1782
|
-
* AdStageProvider 내부에서만 사용 가능
|
|
1783
|
-
*/
|
|
1784
|
-
const useAdStage = () => {
|
|
1785
|
-
const context = reactExports.useContext(AdStageContext);
|
|
1786
|
-
if (!context) {
|
|
1787
|
-
throw new Error('useAdStage must be used within an AdStageProvider');
|
|
1788
|
-
}
|
|
1789
|
-
return context;
|
|
1790
|
-
};
|
|
1791
|
-
|
|
1792
|
-
/**
|
|
1793
|
-
* 범용 광고 슬롯 컴포넌트
|
|
1794
|
-
* 모든 광고 타입을 지원하며 SSR 환경에서도 안전하게 동작
|
|
1795
|
-
*/
|
|
1796
|
-
const AdSlot = ({ slotId, adType, width, height, className, style, autoSlideInterval = 3, sliderEffect = 'slide', language, deviceType, country, }) => {
|
|
1797
|
-
const containerRef = reactExports.useRef(null);
|
|
1798
|
-
const { sdk, isLoading, error } = useAdStage();
|
|
1799
|
-
const containerId = reactExports.useMemo(() => `adstage-${slotId}`, [slotId]);
|
|
1800
|
-
reactExports.useEffect(() => {
|
|
1801
|
-
if (!sdk || !containerRef.current || isLoading || error) {
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
// 컨테이너에 ID 설정
|
|
1805
|
-
containerRef.current.id = containerId;
|
|
1806
|
-
// 광고 슬롯 생성
|
|
1807
|
-
const createSlot = async () => {
|
|
1808
|
-
try {
|
|
1809
|
-
await sdk.createSlot(slotId, containerId, adType, {
|
|
1810
|
-
width,
|
|
1811
|
-
height,
|
|
1812
|
-
language,
|
|
1813
|
-
deviceType,
|
|
1814
|
-
country,
|
|
1815
|
-
autoSlideInterval,
|
|
1816
|
-
sliderEffect,
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
catch (err) {
|
|
1820
|
-
console.error(`Failed to create ad slot ${slotId}:`, err);
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
createSlot();
|
|
1824
|
-
// 클린업 함수
|
|
1825
|
-
return () => {
|
|
1826
|
-
// SDK에 removeSlot 메서드가 없으므로 DOM 정리만 수행
|
|
1827
|
-
if (containerRef.current) {
|
|
1828
|
-
containerRef.current.innerHTML = '';
|
|
1829
|
-
}
|
|
1830
|
-
};
|
|
1831
|
-
}, [
|
|
1832
|
-
sdk,
|
|
1833
|
-
slotId,
|
|
1834
|
-
containerId,
|
|
1835
|
-
adType,
|
|
1836
|
-
width,
|
|
1837
|
-
height,
|
|
1838
|
-
language,
|
|
1839
|
-
deviceType,
|
|
1840
|
-
country,
|
|
1841
|
-
autoSlideInterval,
|
|
1842
|
-
sliderEffect,
|
|
1843
|
-
isLoading,
|
|
1844
|
-
error,
|
|
1845
|
-
]);
|
|
1846
|
-
const containerStyle = {
|
|
1847
|
-
width: typeof width === 'number' ? `${width}px` : width,
|
|
1848
|
-
height: typeof height === 'number' ? `${height}px` : height,
|
|
1849
|
-
...style,
|
|
1850
|
-
};
|
|
1851
|
-
// 로딩 상태 표시
|
|
1852
|
-
if (isLoading) {
|
|
1853
|
-
return (jsxRuntimeExports.jsx("div", { className: className, style: {
|
|
1854
|
-
...containerStyle,
|
|
1855
|
-
display: 'flex',
|
|
1856
|
-
alignItems: 'center',
|
|
1857
|
-
justifyContent: 'center',
|
|
1858
|
-
backgroundColor: '#f5f5f5',
|
|
1859
|
-
border: '1px dashed #ccc',
|
|
1860
|
-
color: '#999',
|
|
1861
|
-
}, children: "Loading Ad..." }));
|
|
1862
|
-
}
|
|
1863
|
-
// 에러 상태 표시
|
|
1864
|
-
if (error) {
|
|
1865
|
-
return (jsxRuntimeExports.jsx("div", { className: className, style: {
|
|
1866
|
-
...containerStyle,
|
|
1867
|
-
display: 'flex',
|
|
1868
|
-
alignItems: 'center',
|
|
1869
|
-
justifyContent: 'center',
|
|
1870
|
-
backgroundColor: '#fee',
|
|
1871
|
-
border: '1px solid #fcc',
|
|
1872
|
-
color: '#c00',
|
|
1873
|
-
}, children: "Ad Load Error" }));
|
|
1874
|
-
}
|
|
1875
|
-
return (jsxRuntimeExports.jsx("div", { ref: containerRef, className: className, style: containerStyle }));
|
|
1876
|
-
};
|
|
1877
|
-
|
|
1878
|
-
/**
|
|
1879
|
-
* 배너 광고 전용 컴포넌트
|
|
1880
|
-
* AdSlot의 래퍼로 adType이 BANNER로 고정됨
|
|
1881
|
-
*/
|
|
1882
|
-
const BannerAd = (props) => {
|
|
1883
|
-
return jsxRuntimeExports.jsx(AdSlot, { ...props, adType: AdType.BANNER });
|
|
1884
|
-
};
|
|
1885
|
-
|
|
1886
|
-
/**
|
|
1887
|
-
* 텍스트 광고 전용 컴포넌트
|
|
1888
|
-
* AdSlot의 래퍼로 adType이 TEXT로 고정됨
|
|
1889
|
-
*/
|
|
1890
|
-
const TextAd = (props) => {
|
|
1891
|
-
return jsxRuntimeExports.jsx(AdSlot, { ...props, adType: AdType.TEXT });
|
|
1892
|
-
};
|
|
1893
|
-
|
|
1894
|
-
/**
|
|
1895
|
-
* 네이티브 광고 전용 컴포넌트
|
|
1896
|
-
* AdSlot의 래퍼로 adType이 NATIVE로 고정됨
|
|
1897
|
-
*/
|
|
1898
|
-
const NativeAd = (props) => {
|
|
1899
|
-
return jsxRuntimeExports.jsx(AdSlot, { ...props, adType: AdType.NATIVE });
|
|
1900
|
-
};
|
|
1901
|
-
|
|
1902
|
-
/**
|
|
1903
|
-
* 비디오 광고 전용 컴포넌트
|
|
1904
|
-
* AdSlot의 래퍼로 adType이 VIDEO로 고정됨
|
|
1905
|
-
*/
|
|
1906
|
-
const VideoAd = (props) => {
|
|
1907
|
-
return jsxRuntimeExports.jsx(AdSlot, { ...props, adType: AdType.VIDEO });
|
|
1908
|
-
};
|
|
1909
|
-
|
|
1910
|
-
/**
|
|
1911
|
-
* 인터스티셜 광고 전용 컴포넌트
|
|
1912
|
-
* AdSlot의 래퍼로 adType이 INTERSTITIAL로 고정됨
|
|
1913
|
-
*/
|
|
1914
|
-
const InterstitialAd = (props) => {
|
|
1915
|
-
return jsxRuntimeExports.jsx(AdSlot, { ...props, adType: AdType.INTERSTITIAL });
|
|
1916
|
-
};
|
|
1917
|
-
|
|
1918
|
-
/**
|
|
1919
|
-
* 광고 컴포넌트에서 발생하는 오류를 포착하는 Error Boundary
|
|
1920
|
-
* 광고 로딩 실패 시 fallback UI를 표시하고 앱 전체가 크래시되는 것을 방지
|
|
1921
|
-
*/
|
|
1922
|
-
class AdErrorBoundary extends reactExports.Component {
|
|
1923
|
-
constructor(props) {
|
|
1924
|
-
super(props);
|
|
1925
|
-
this.state = { hasError: false, error: null };
|
|
1926
|
-
}
|
|
1927
|
-
static getDerivedStateFromError(error) {
|
|
1928
|
-
return { hasError: true, error };
|
|
1929
|
-
}
|
|
1930
|
-
componentDidCatch(error, errorInfo) {
|
|
1931
|
-
console.error('AdStage Error Boundary caught an error:', error, errorInfo);
|
|
1932
|
-
// 사용자 정의 에러 핸들러 호출
|
|
1933
|
-
if (this.props.onError) {
|
|
1934
|
-
this.props.onError(error, errorInfo);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
render() {
|
|
1938
|
-
if (this.state.hasError) {
|
|
1939
|
-
// 사용자 정의 fallback이 있으면 사용, 없으면 기본 fallback 표시
|
|
1940
|
-
if (this.props.fallback) {
|
|
1941
|
-
return this.props.fallback;
|
|
1942
|
-
}
|
|
1943
|
-
return (jsxRuntimeExports.jsxs("div", { style: {
|
|
1944
|
-
padding: '20px',
|
|
1945
|
-
textAlign: 'center',
|
|
1946
|
-
backgroundColor: '#fee',
|
|
1947
|
-
border: '1px solid #fcc',
|
|
1948
|
-
borderRadius: '4px',
|
|
1949
|
-
color: '#c00',
|
|
1950
|
-
}, children: [jsxRuntimeExports.jsx("h3", { children: "\uAD11\uACE0 \uB85C\uB529 \uC624\uB958" }), jsxRuntimeExports.jsx("p", { children: "\uAD11\uACE0\uB97C \uBD88\uB7EC\uC624\uB294 \uC911 \uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4." }), jsxRuntimeExports.jsx("button", { onClick: () => this.setState({ hasError: false, error: null }), style: {
|
|
1951
|
-
padding: '8px 16px',
|
|
1952
|
-
backgroundColor: '#fff',
|
|
1953
|
-
border: '1px solid #ccc',
|
|
1954
|
-
borderRadius: '4px',
|
|
1955
|
-
cursor: 'pointer',
|
|
1956
|
-
}, children: "\uB2E4\uC2DC \uC2DC\uB3C4" })] }));
|
|
1957
|
-
}
|
|
1958
|
-
return this.props.children;
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
/**
|
|
1963
|
-
* 광고 슬롯 생성 및 관리를 위한 Hook
|
|
1964
|
-
* 컴포넌트에서 직접 슬롯을 제어하고 싶을 때 사용
|
|
1965
|
-
*/
|
|
1966
|
-
const useAdSlot = (options) => {
|
|
1967
|
-
const { sdk } = useAdStage();
|
|
1968
|
-
const [isLoading, setIsLoading] = reactExports.useState(false);
|
|
1969
|
-
const [error, setError] = reactExports.useState(null);
|
|
1970
|
-
const [isCreated, setIsCreated] = reactExports.useState(false);
|
|
1971
|
-
const createSlot = async () => {
|
|
1972
|
-
if (!sdk) {
|
|
1973
|
-
setError('SDK not initialized');
|
|
1974
|
-
return;
|
|
1975
|
-
}
|
|
1976
|
-
setIsLoading(true);
|
|
1977
|
-
setError(null);
|
|
1978
|
-
try {
|
|
1979
|
-
await sdk.createSlot(options.slotId, options.containerId, options.adType, {
|
|
1980
|
-
width: options.width,
|
|
1981
|
-
height: options.height,
|
|
1982
|
-
language: options.language,
|
|
1983
|
-
deviceType: options.deviceType,
|
|
1984
|
-
country: options.country,
|
|
1985
|
-
autoSlideInterval: options.autoSlideInterval,
|
|
1986
|
-
sliderEffect: options.sliderEffect,
|
|
1987
|
-
});
|
|
1988
|
-
setIsCreated(true);
|
|
1989
|
-
}
|
|
1990
|
-
catch (err) {
|
|
1991
|
-
const errorMessage = err instanceof Error ? err.message : '슬롯 생성 중 오류가 발생했습니다.';
|
|
1992
|
-
setError(errorMessage);
|
|
1993
|
-
setIsCreated(false);
|
|
1994
|
-
}
|
|
1995
|
-
finally {
|
|
1996
|
-
setIsLoading(false);
|
|
1997
|
-
}
|
|
1998
|
-
};
|
|
1999
|
-
const resetSlot = () => {
|
|
2000
|
-
setIsLoading(false);
|
|
2001
|
-
setError(null);
|
|
2002
|
-
setIsCreated(false);
|
|
2003
|
-
};
|
|
2004
|
-
// SDK가 변경되면 상태 초기화
|
|
2005
|
-
reactExports.useEffect(() => {
|
|
2006
|
-
resetSlot();
|
|
2007
|
-
}, [sdk]);
|
|
2008
|
-
return {
|
|
2009
|
-
isLoading,
|
|
2010
|
-
error,
|
|
2011
|
-
isCreated,
|
|
2012
|
-
createSlot,
|
|
2013
|
-
resetSlot,
|
|
2014
|
-
};
|
|
2015
|
-
};
|
|
2016
|
-
|
|
2017
|
-
/**
|
|
2018
|
-
* 광고 이벤트 추적을 위한 Hook
|
|
2019
|
-
* 커스텀 이벤트 추적이 필요할 때 사용
|
|
2020
|
-
*/
|
|
2021
|
-
const useAdTracking = () => {
|
|
2022
|
-
const { sdk } = useAdStage();
|
|
2023
|
-
const trackEvent = reactExports.useCallback((adId, slotId, eventType) => {
|
|
2024
|
-
if (!sdk) {
|
|
2025
|
-
console.warn('SDK not initialized - cannot track event');
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
try {
|
|
2029
|
-
// SDK의 eventTracker에 직접 접근할 수 없으므로
|
|
2030
|
-
// 여기서는 console.log로 대체하거나 SDK에 public 메서드가 있다면 사용
|
|
2031
|
-
console.log('Ad Event Tracked:', { adId, slotId, eventType });
|
|
2032
|
-
// 실제 구현에서는 SDK에 trackEvent 메서드가 있다면 사용
|
|
2033
|
-
// sdk.trackEvent(adId, slotId, eventType);
|
|
2034
|
-
}
|
|
2035
|
-
catch (err) {
|
|
2036
|
-
console.error('Failed to track event:', err);
|
|
2037
|
-
}
|
|
2038
|
-
}, [sdk]);
|
|
2039
|
-
const trackClick = reactExports.useCallback((adId, slotId) => {
|
|
2040
|
-
trackEvent(adId, slotId, AdEventType.CLICK);
|
|
2041
|
-
}, [trackEvent]);
|
|
2042
|
-
const trackImpression = reactExports.useCallback((adId, slotId) => {
|
|
2043
|
-
trackEvent(adId, slotId, AdEventType.IMPRESSION);
|
|
2044
|
-
}, [trackEvent]);
|
|
2045
|
-
const trackView = reactExports.useCallback((adId, slotId) => {
|
|
2046
|
-
trackEvent(adId, slotId, AdEventType.VIEWABLE);
|
|
2047
|
-
}, [trackEvent]);
|
|
2048
|
-
const trackClose = reactExports.useCallback((adId, slotId) => {
|
|
2049
|
-
trackEvent(adId, slotId, AdEventType.COMPLETED);
|
|
2050
|
-
}, [trackEvent]);
|
|
2051
|
-
return {
|
|
2052
|
-
trackEvent,
|
|
2053
|
-
trackClick,
|
|
2054
|
-
trackImpression,
|
|
2055
|
-
trackView,
|
|
2056
|
-
trackClose,
|
|
2057
|
-
};
|
|
2058
|
-
};
|
|
2059
|
-
|
|
2060
|
-
/**
|
|
2061
|
-
* AdStage SDK 메인 클래스
|
|
2062
|
-
* - 간단한 API Key 기반 초기화
|
|
2063
|
-
* - 광고 슬롯 자동 관리
|
|
2064
|
-
* - 이벤트 자동 추적
|
|
2065
|
-
*/
|
|
2066
|
-
class AdStageSDK {
|
|
2067
|
-
constructor(config) {
|
|
2068
|
-
this.baseUrl = 'https://beta-api.adstage.app';
|
|
2069
|
-
this.slots = new Map();
|
|
2070
|
-
this.initialized = false;
|
|
2071
|
-
this.config = {
|
|
2072
|
-
debug: false,
|
|
2073
|
-
...config,
|
|
2074
|
-
};
|
|
2075
|
-
this.eventTracker = new EventTracker(this.baseUrl, this.config.apiKey, this.config.debug || false, this.slots);
|
|
2076
|
-
}
|
|
2077
|
-
/**
|
|
2078
|
-
* SDK 초기화 및 인스턴스 반환
|
|
2079
|
-
*/
|
|
2080
|
-
static init(config) {
|
|
2081
|
-
if (!AdStageSDK.instance) {
|
|
2082
|
-
AdStageSDK.instance = new AdStageSDK(config);
|
|
2083
|
-
}
|
|
2084
|
-
return AdStageSDK.instance;
|
|
2085
|
-
}
|
|
2086
|
-
/**
|
|
2087
|
-
* SDK 인스턴스 반환 (이미 초기화된 경우)
|
|
2088
|
-
*/
|
|
2089
|
-
static getInstance() {
|
|
2090
|
-
if (!AdStageSDK.instance) {
|
|
2091
|
-
throw new Error('AdStageSDK must be initialized first. Call AdStageSDK.init(config)');
|
|
2092
|
-
}
|
|
2093
|
-
return AdStageSDK.instance;
|
|
2094
|
-
}
|
|
2095
|
-
/**
|
|
2096
|
-
* 광고 슬롯 생성 및 로드
|
|
2097
|
-
*/
|
|
2098
|
-
async createSlot(id, containerId, adType = AdType.BANNER, options) {
|
|
2099
|
-
const container = DOMUtils.safeGetElementById(containerId);
|
|
2100
|
-
if (!container) {
|
|
2101
|
-
if (DOMUtils.canUseDOM()) {
|
|
2102
|
-
console.error(`Container with ID "${containerId}" not found`);
|
|
2103
|
-
}
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
|
-
const slot = {
|
|
2107
|
-
id,
|
|
2108
|
-
containerId,
|
|
2109
|
-
adType,
|
|
2110
|
-
width: options?.width || 0, // 문자열도 지원
|
|
2111
|
-
height: options?.height || 0, // 문자열도 지원
|
|
2112
|
-
isLoaded: false,
|
|
2113
|
-
isVisible: false,
|
|
2114
|
-
refreshRate: 0,
|
|
2115
|
-
lazyLoad: false,
|
|
2116
|
-
targeting: {},
|
|
2117
|
-
load: async () => { await this.loadSlot(slot, options); return null; },
|
|
2118
|
-
render: (ad) => this.renderSlot(slot, ad),
|
|
2119
|
-
refresh: () => this.refreshSlot(slot.id),
|
|
2120
|
-
destroy: () => this.destroySlot(slot.id),
|
|
2121
|
-
};
|
|
2122
|
-
this.slots.set(id, slot);
|
|
2123
|
-
await this.loadSlot(slot, options);
|
|
2124
|
-
}
|
|
2125
|
-
/**
|
|
2126
|
-
* 광고 슬롯 로드
|
|
2127
|
-
*/
|
|
2128
|
-
async loadSlot(slot, options) {
|
|
2129
|
-
try {
|
|
2130
|
-
const queryParams = new URLSearchParams({
|
|
2131
|
-
adType: slot.adType,
|
|
2132
|
-
status: 'ACTIVE', // ACTIVE 상태인 광고만 조회
|
|
2133
|
-
...(options?.language && { language: options.language }),
|
|
2134
|
-
...(options?.deviceType && { deviceType: options.deviceType }),
|
|
2135
|
-
...(options?.country && { country: options.country }),
|
|
2136
|
-
});
|
|
2137
|
-
const response = await fetch(`${this.baseUrl}/advertisements/list?${queryParams}`, {
|
|
2138
|
-
headers: {
|
|
2139
|
-
'x-api-key': this.config.apiKey,
|
|
2140
|
-
'Content-Type': 'application/json',
|
|
2141
|
-
},
|
|
2142
|
-
});
|
|
2143
|
-
if (!response.ok) {
|
|
2144
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
2145
|
-
}
|
|
2146
|
-
const data = await response.json();
|
|
2147
|
-
const advertisements = data.advertisements || [];
|
|
2148
|
-
if (advertisements.length > 0) {
|
|
2149
|
-
// 여러 광고가 있을 경우 슬라이드로 렌더링
|
|
2150
|
-
this.renderSlotWithSlider(slot, advertisements, options);
|
|
2151
|
-
// 첫 번째 광고에 대해서만 노출 이벤트 추적
|
|
2152
|
-
await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, AdEventType.IMPRESSION);
|
|
2153
|
-
}
|
|
2154
|
-
else {
|
|
2155
|
-
console.warn(`No advertisements available for slot ${slot.id}`);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
catch (error) {
|
|
2159
|
-
console.error(`Failed to load slot ${slot.id}:`, error);
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* 광고 슬롯 렌더링 (슬라이더 포함)
|
|
2164
|
-
*/
|
|
2165
|
-
renderSlotWithSlider(slot, advertisements, options) {
|
|
2166
|
-
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
2167
|
-
if (!container)
|
|
2168
|
-
return;
|
|
2169
|
-
if (advertisements.length === 1) {
|
|
2170
|
-
// 광고가 하나뿐이면 기본 렌더링
|
|
2171
|
-
this.renderSlot(slot, advertisements[0]);
|
|
2172
|
-
return;
|
|
2173
|
-
}
|
|
2174
|
-
// 슬라이더 효과 결정 (옵션으로 지정하거나 텍스트 광고인 경우 fade 기본값)
|
|
2175
|
-
const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
|
|
2176
|
-
const useFadeEffect = options?.sliderEffect === 'fade' ||
|
|
2177
|
-
(options?.sliderEffect !== 'slide' && isAllTextAds);
|
|
2178
|
-
let sliderContainer;
|
|
2179
|
-
if (useFadeEffect) {
|
|
2180
|
-
// 페이드 슬라이더 사용
|
|
2181
|
-
sliderContainer = FadeSliderManager.createFadeSliderContainer(slot, advertisements, options, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
|
|
2182
|
-
}
|
|
2183
|
-
else {
|
|
2184
|
-
// 기본 슬라이더 사용
|
|
2185
|
-
sliderContainer = SliderManager.createSliderContainer(slot, advertisements, options, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
|
|
2186
|
-
}
|
|
2187
|
-
container.innerHTML = '';
|
|
2188
|
-
container.appendChild(sliderContainer);
|
|
2189
|
-
slot.isLoaded = true;
|
|
2190
|
-
if (this.config.debug) {
|
|
2191
|
-
const sliderType = useFadeEffect ? 'fade slider' : 'slider';
|
|
2192
|
-
console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
/**
|
|
2196
|
-
* 광고 슬롯 렌더링 (단일 광고용)
|
|
2197
|
-
*/
|
|
2198
|
-
renderSlot(slot, ad) {
|
|
2199
|
-
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
2200
|
-
if (!container)
|
|
2201
|
-
return;
|
|
2202
|
-
// 팩토리를 사용해서 적절한 렌더러로 광고 생성
|
|
2203
|
-
const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
|
|
2204
|
-
container.innerHTML = '';
|
|
2205
|
-
container.appendChild(adElement);
|
|
2206
|
-
slot.isLoaded = true;
|
|
2207
|
-
if (this.config.debug) {
|
|
2208
|
-
console.log(`Rendered ad for slot ${slot.id}:`, ad);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
/**
|
|
2212
|
-
* 광고 슬롯 새로고침
|
|
2213
|
-
*/
|
|
2214
|
-
async refreshSlot(slotId) {
|
|
2215
|
-
const slot = this.slots.get(slotId);
|
|
2216
|
-
if (slot) {
|
|
2217
|
-
await this.loadSlot(slot);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
/**
|
|
2221
|
-
* 광고 슬롯 제거
|
|
2222
|
-
*/
|
|
2223
|
-
destroySlot(slotId) {
|
|
2224
|
-
const slot = this.slots.get(slotId);
|
|
2225
|
-
if (slot) {
|
|
2226
|
-
const container = DOMUtils.safeGetElementById(slot.containerId);
|
|
2227
|
-
if (container) {
|
|
2228
|
-
DOMUtils.safeSetInnerHTML(container, '');
|
|
2229
|
-
}
|
|
2230
|
-
this.slots.delete(slotId);
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
/**
|
|
2234
|
-
* 자동 슬롯 검색 및 로드 (분리된 SDKUtils 사용)
|
|
2235
|
-
*/
|
|
2236
|
-
async autoLoadSlots() {
|
|
2237
|
-
const elements = SDKUtils.findAutoSlotElements();
|
|
2238
|
-
for (const element of elements) {
|
|
2239
|
-
const slotInfo = SDKUtils.extractSlotInfo(element);
|
|
2240
|
-
if (!slotInfo.slotId || this.slots.has(slotInfo.slotId))
|
|
2241
|
-
continue;
|
|
2242
|
-
const adType = SDKUtils.parseAdType(slotInfo.adType, AdType);
|
|
2243
|
-
await this.createSlot(slotInfo.slotId, element.id || slotInfo.slotId, adType, {
|
|
2244
|
-
width: slotInfo.width,
|
|
2245
|
-
height: slotInfo.height,
|
|
2246
|
-
language: slotInfo.language,
|
|
2247
|
-
deviceType: slotInfo.deviceType,
|
|
2248
|
-
country: slotInfo.country,
|
|
2249
|
-
sliderEffect: slotInfo.sliderEffect,
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
/**
|
|
2254
|
-
* SDK 정리
|
|
2255
|
-
*/
|
|
2256
|
-
destroy() {
|
|
2257
|
-
this.slots.clear();
|
|
2258
|
-
ImpressionTracker.clear(); // 노출 추적 데이터도 정리
|
|
2259
|
-
this.initialized = false;
|
|
2260
|
-
AdStageSDK.instance = null;
|
|
2261
|
-
}
|
|
2262
|
-
/**
|
|
2263
|
-
* 디바이스 ID 가져오기
|
|
2264
|
-
*/
|
|
2265
|
-
getDeviceId() {
|
|
2266
|
-
return DeviceInfoCollector.generateDeviceId();
|
|
2267
|
-
}
|
|
2268
|
-
/**
|
|
2269
|
-
* 세션 ID 가져오기
|
|
2270
|
-
*/
|
|
2271
|
-
getSessionId() {
|
|
2272
|
-
return DeviceInfoCollector.generateSessionId();
|
|
2273
|
-
}
|
|
2274
|
-
/**
|
|
2275
|
-
* 현재 로드된 슬롯 수 가져오기
|
|
2276
|
-
*/
|
|
2277
|
-
getLoadedSlotCount() {
|
|
2278
|
-
return this.slots.size;
|
|
2279
|
-
}
|
|
2280
|
-
/**
|
|
2281
|
-
* 모든 슬롯 정보 가져오기
|
|
2282
|
-
*/
|
|
2283
|
-
getAllSlots() {
|
|
2284
|
-
return new Map(this.slots);
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
AdStageSDK.instance = null;
|
|
2288
|
-
async function autoInit() {
|
|
2289
|
-
if (DOMUtils.isBrowser() && window.adstageConfig) {
|
|
2290
|
-
try {
|
|
2291
|
-
const sdk = AdStageSDK.init(window.adstageConfig);
|
|
2292
|
-
await sdk.autoLoadSlots();
|
|
2293
|
-
}
|
|
2294
|
-
catch (error) {
|
|
2295
|
-
console.error('Failed to auto-initialize AdStageSDK:', error);
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
// 브라우저 환경에서 자동 초기화
|
|
2300
|
-
if (DOMUtils.isBrowser()) {
|
|
2301
|
-
// 타입 단언을 사용하여 window 객체에 할당
|
|
2302
|
-
window.AdStageSDK = AdStageSDK;
|
|
2303
|
-
// DOM 로드 후 자동 초기화
|
|
2304
|
-
if (DOMUtils.isDOMReady()) {
|
|
2305
|
-
autoInit();
|
|
2306
|
-
}
|
|
2307
|
-
else {
|
|
2308
|
-
DOMUtils.waitForDOM().then(autoInit);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
var index = /*#__PURE__*/Object.freeze({
|
|
2313
|
-
__proto__: null,
|
|
2314
|
-
AdErrorBoundary: AdErrorBoundary,
|
|
2315
|
-
get AdEventType () { return AdEventType; },
|
|
2316
|
-
AdSlot: AdSlot,
|
|
2317
|
-
AdStageProvider: AdStageProvider,
|
|
2318
|
-
AdStageSDK: AdStageSDK,
|
|
2319
|
-
get AdType () { return AdType; },
|
|
2320
|
-
BannerAd: BannerAd,
|
|
2321
|
-
InterstitialAd: InterstitialAd,
|
|
2322
|
-
NativeAd: NativeAd,
|
|
2323
|
-
TextAd: TextAd,
|
|
2324
|
-
VideoAd: VideoAd,
|
|
2325
|
-
default: AdStageSDK,
|
|
2326
|
-
useAdSlot: useAdSlot,
|
|
2327
|
-
useAdStage: useAdStage,
|
|
2328
|
-
useAdTracking: useAdTracking
|
|
2329
|
-
});
|
|
2330
|
-
|
|
2331
|
-
export { AdErrorBoundary, AdEventType, AdSlot, AdStageProvider, AdStageSDK, AdType, BannerAd, InterstitialAd, NativeAd, TextAd, VideoAd, AdStageSDK as default, useAdSlot, useAdStage, useAdTracking };
|