@adstage/web-sdk 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/index.cjs.js +2304 -0
  4. package/dist/index.d.ts +416 -0
  5. package/dist/index.esm.js +2288 -0
  6. package/dist/index.standalone.js +2331 -0
  7. package/examples/README.md +33 -0
  8. package/examples/banner-ads.html +512 -0
  9. package/examples/index.html +338 -0
  10. package/examples/native-ads.html +634 -0
  11. package/examples/react-app/README.md +70 -0
  12. package/examples/react-app/index.html +13 -0
  13. package/examples/react-app/package-lock.json +3042 -0
  14. package/examples/react-app/package.json +26 -0
  15. package/examples/react-app/pnpm-lock.yaml +1857 -0
  16. package/examples/react-app/public/index.standalone.js +2331 -0
  17. package/examples/react-app/src/App.tsx +226 -0
  18. package/examples/react-app/src/index.css +37 -0
  19. package/examples/react-app/src/main.tsx +10 -0
  20. package/examples/react-app/tsconfig.json +25 -0
  21. package/examples/react-app/tsconfig.node.json +10 -0
  22. package/examples/react-app/vite.config.ts +15 -0
  23. package/examples/react-nextjs/app/globals.css +200 -0
  24. package/examples/react-nextjs/app/layout.tsx +27 -0
  25. package/examples/react-nextjs/app/page.tsx +258 -0
  26. package/examples/react-nextjs/next.config.js +9 -0
  27. package/examples/react-nextjs/package.json +22 -0
  28. package/examples/react-nextjs/pnpm-lock.yaml +343 -0
  29. package/examples/react-nextjs/tsconfig.json +34 -0
  30. package/examples/text-ads.html +597 -0
  31. package/examples/video-ads.html +739 -0
  32. package/package.json +83 -0
  33. package/src/global.d.ts +20 -0
  34. package/src/index.ts +350 -0
  35. package/src/managers/device-info-collector.ts +127 -0
  36. package/src/managers/event-tracker.ts +131 -0
  37. package/src/managers/fade-slider-manager.ts +276 -0
  38. package/src/managers/impression-tracker.ts +88 -0
  39. package/src/managers/slider-manager.ts +405 -0
  40. package/src/react/components/AdErrorBoundary.tsx +75 -0
  41. package/src/react/components/AdSlot.tsx +144 -0
  42. package/src/react/components/BannerAd.tsx +24 -0
  43. package/src/react/components/InterstitialAd.tsx +24 -0
  44. package/src/react/components/NativeAd.tsx +24 -0
  45. package/src/react/components/TextAd.tsx +24 -0
  46. package/src/react/components/VideoAd.tsx +24 -0
  47. package/src/react/components/index.ts +8 -0
  48. package/src/react/hooks/index.ts +4 -0
  49. package/src/react/hooks/useAdSlot.ts +83 -0
  50. package/src/react/hooks/useAdStage.ts +14 -0
  51. package/src/react/hooks/useAdTracking.ts +61 -0
  52. package/src/react/index.ts +4 -0
  53. package/src/react/providers/AdStageProvider.tsx +86 -0
  54. package/src/react/providers/index.ts +2 -0
  55. package/src/renderers/banner-renderer.ts +35 -0
  56. package/src/renderers/base-renderer.ts +207 -0
  57. package/src/renderers/index.ts +71 -0
  58. package/src/renderers/interstitial-renderer.ts +70 -0
  59. package/src/renderers/native-renderer.ts +35 -0
  60. package/src/renderers/text-renderer.ts +94 -0
  61. package/src/renderers/video-renderer.ts +63 -0
  62. package/src/types/advertisement.ts +197 -0
  63. package/src/types/api.ts +173 -0
  64. package/src/types/config.ts +174 -0
  65. package/src/types/events.ts +60 -0
  66. package/src/types/index.ts +6 -0
  67. package/src/utils/dom-utils.ts +237 -0
  68. package/src/utils/sdk-utils.ts +134 -0
@@ -0,0 +1,405 @@
1
+ import { AdType, AdEventType } from '../types/advertisement';
2
+ import type { AdSlot, Advertisement } from '../types/advertisement';
3
+ import { AdRendererFactory } from '../renderers';
4
+
5
+ /**
6
+ * 슬라이더 관리 클래스
7
+ * - 다중 광고 슬라이더 생성 및 관리
8
+ * - 무한 루프 슬라이더 지원
9
+ * - 터치 제스처 및 자동 슬라이드 기능
10
+ */
11
+ export class SliderManager {
12
+ /**
13
+ * 슬라이더 컨테이너 생성
14
+ */
15
+ static createSliderContainer(
16
+ slot: AdSlot,
17
+ advertisements: Advertisement[],
18
+ options: any,
19
+ trackEventCallback: (adId: string, slotId: string, eventType: AdEventType) => void
20
+ ): HTMLElement {
21
+ const sliderWrapper = document.createElement('div');
22
+ sliderWrapper.className = 'adstage-slider-wrapper';
23
+
24
+ // 사용자 지정 크기가 있으면 적용, 없으면 콘텐츠 크기에 맞춤
25
+ const containerStyles: Record<string, string> = {
26
+ position: 'relative',
27
+ overflow: 'hidden',
28
+ };
29
+
30
+ // 사용자가 크기를 지정한 경우
31
+ if (slot.width && slot.width !== 0) {
32
+ let width: string;
33
+ if (typeof slot.width === 'string') {
34
+ // 문자열인 경우 px 단위가 있는지 확인
35
+ width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
36
+ } else {
37
+ // 숫자인 경우 px 단위 추가
38
+ width = `${slot.width}px`;
39
+ }
40
+ containerStyles.width = width;
41
+ containerStyles.display = 'inline-block'; // 지정된 크기에 맞춤 (좌측 정렬)
42
+ } else {
43
+ // 컨텐츠 크기에 맞춤
44
+ containerStyles.display = 'inline-block';
45
+ }
46
+
47
+ if (slot.height && slot.height !== 0) {
48
+ const height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
49
+ containerStyles.height = height;
50
+ }
51
+
52
+ // 스타일 적용
53
+ Object.entries(containerStyles).forEach(([key, value]) => {
54
+ sliderWrapper.style.setProperty(key, value);
55
+ });
56
+
57
+ // 크기 측정 (width나 height가 설정되지 않은 경우)
58
+ const needsWidthMeasurement = !slot.width || slot.width === 0;
59
+ const needsHeightMeasurement = !slot.height || slot.height === 0;
60
+
61
+ if (needsWidthMeasurement || needsHeightMeasurement) {
62
+ const measureContainer = document.createElement('div');
63
+ measureContainer.style.cssText = `
64
+ position: absolute;
65
+ visibility: hidden;
66
+ white-space: nowrap;
67
+ top: -9999px;
68
+ left: -9999px;
69
+ `;
70
+
71
+ // width가 설정되어 있으면 측정 컨테이너에도 적용
72
+ if (!needsWidthMeasurement && slot.width) {
73
+ let width: string;
74
+ if (typeof slot.width === 'string') {
75
+ width = slot.width.includes('px') || slot.width.includes('%') ? slot.width : `${slot.width}px`;
76
+ } else {
77
+ width = `${slot.width}px`;
78
+ }
79
+ measureContainer.style.width = width;
80
+ measureContainer.style.whiteSpace = 'normal'; // width가 있으면 줄바꿈 허용
81
+ }
82
+
83
+ document.body.appendChild(measureContainer);
84
+
85
+ let maxWidth = 0;
86
+ let maxHeight = 0;
87
+
88
+ // 모든 광고의 크기를 측정하여 최대 크기 찾기
89
+ advertisements.forEach(ad => {
90
+ const measureAdElement = AdRendererFactory.render(
91
+ ad,
92
+ slot,
93
+ trackEventCallback
94
+ );
95
+ measureContainer.appendChild(measureAdElement);
96
+
97
+ const rect = measureAdElement.getBoundingClientRect();
98
+ if (rect.width > maxWidth) maxWidth = rect.width;
99
+ if (rect.height > maxHeight) maxHeight = rect.height;
100
+
101
+ // 측정 후 요소 제거
102
+ measureContainer.removeChild(measureAdElement);
103
+ });
104
+
105
+ // 측정된 최대 크기로 래퍼 크기 설정
106
+ if (needsWidthMeasurement && maxWidth > 0) {
107
+ sliderWrapper.style.width = `${maxWidth}px`;
108
+ containerStyles.width = `${maxWidth}px`;
109
+ }
110
+
111
+ if (needsHeightMeasurement && maxHeight > 0) {
112
+ sliderWrapper.style.height = `${maxHeight}px`;
113
+ containerStyles.height = `${maxHeight}px`;
114
+ }
115
+
116
+ // 측정 컨테이너 제거
117
+ document.body.removeChild(measureContainer);
118
+ }
119
+ // 무한 루프를 위해 첫 번째 슬라이드를 마지막에 복사
120
+ const extendedAds = [...advertisements, advertisements[0]];
121
+
122
+ // 슬라이드 컨테이너
123
+ const slideContainer = document.createElement('div');
124
+ slideContainer.className = 'adstage-slide-container';
125
+
126
+ // 슬라이드 컨테이너 스타일 - 항상 기본 설정 적용
127
+ const slideContainerStyles: Record<string, string> = {
128
+ display: 'flex',
129
+ transition: 'transform 0.4s ease-out',
130
+ width: `${extendedAds.length * 100}%`,
131
+ };
132
+
133
+ if (slot.height && slot.height !== 0) {
134
+ slideContainerStyles.height = '100%';
135
+ }
136
+
137
+ Object.entries(slideContainerStyles).forEach(([key, value]) => {
138
+ slideContainer.style.setProperty(key, value);
139
+ });
140
+
141
+ // 각 광고를 슬라이드로 생성 (복사된 첫 번째 포함)
142
+ extendedAds.forEach((ad, index) => {
143
+ const slideElement = document.createElement('div');
144
+ slideElement.className = 'adstage-slide';
145
+
146
+ // 슬라이드 스타일 설정 - 항상 균등 분할
147
+ const slideStyles: Record<string, string> = {
148
+ width: `${100 / extendedAds.length}%`,
149
+ 'flex-shrink': '0',
150
+ display: 'flex',
151
+ 'align-items': 'center',
152
+ 'justify-content': 'center'
153
+ };
154
+
155
+ if (slot.height && slot.height !== 0) {
156
+ slideStyles.height = '100%';
157
+ }
158
+
159
+ Object.entries(slideStyles).forEach(([key, value]) => {
160
+ slideElement.style.setProperty(key, value);
161
+ });
162
+
163
+ // 광고 렌더링
164
+ const adElement = AdRendererFactory.render(
165
+ ad,
166
+ slot,
167
+ trackEventCallback
168
+ );
169
+
170
+ slideElement.appendChild(adElement);
171
+ slideContainer.appendChild(slideElement);
172
+ });
173
+
174
+ // 텍스트 광고인지 확인 (모든 광고가 텍스트 타입인 경우)
175
+ const isAllTextAds = advertisements.every(ad => ad.adType === AdType.TEXT);
176
+
177
+ // 무채색 도트 인디케이터 생성 (원본 광고 수만큼) - 텍스트 광고가 아닐 때만
178
+ const dotContainer = isAllTextAds ? null : SliderManager.createMinimalDotIndicator(advertisements.length);
179
+
180
+ // 슬라이더 상태 관리
181
+ let currentSlide = 0;
182
+ const totalSlides = advertisements.length;
183
+ const autoSlideInterval = (options?.autoSlideInterval || 3) * 1000; // 기본 3초
184
+
185
+ // 슬라이드 이동 함수 (무한 루프 지원)
186
+ const moveToSlide = (index: number, instant = false) => {
187
+ currentSlide = index;
188
+
189
+ // 애니메이션 임시 비활성화 (무한 루프용)
190
+ if (instant) {
191
+ slideContainer.style.transition = 'none';
192
+ } else {
193
+ slideContainer.style.transition = 'transform 0.4s ease-out';
194
+ }
195
+
196
+ // 항상 퍼센트 기반으로 이동
197
+ slideContainer.style.transform = `translateX(-${(100 / extendedAds.length) * currentSlide}%)`;
198
+
199
+ // 도트 업데이트 (무채색 스타일) - 실제 광고 인덱스 기준, 텍스트 광고가 아닐 때만
200
+ const actualIndex = currentSlide === totalSlides ? 0 : currentSlide;
201
+ if (dotContainer) {
202
+ const dots = dotContainer.querySelectorAll('.adstage-dot');
203
+ dots.forEach((dot: Element, i: number) => {
204
+ const dotElement = dot as HTMLElement;
205
+ if (i === actualIndex) {
206
+ dotElement.classList.add('active');
207
+ dotElement.style.background = '#666666';
208
+ dotElement.style.borderColor = '#666666';
209
+ dotElement.style.opacity = '1';
210
+ } else {
211
+ dotElement.classList.remove('active');
212
+ dotElement.style.background = 'transparent';
213
+ dotElement.style.borderColor = '#cccccc';
214
+ dotElement.style.opacity = '0.7';
215
+ }
216
+ });
217
+ }
218
+
219
+ // 현재 슬라이드의 광고에 대해 노출 이벤트 추적
220
+ if (actualIndex > 0) { // 첫 번째는 이미 loadSlot에서 추적됨
221
+ trackEventCallback(advertisements[actualIndex]._id, slot.id, AdEventType.IMPRESSION);
222
+ }
223
+ };
224
+
225
+ // 무한 루프 처리 함수
226
+ const handleInfiniteLoop = () => {
227
+ if (currentSlide === totalSlides) {
228
+ // 복사된 첫 번째 슬라이드에 도달하면 즉시 원본 첫 번째로 이동
229
+ setTimeout(() => {
230
+ moveToSlide(0, true); // 애니메이션 없이 즉시 이동
231
+ }, 400); // transition 시간과 맞춤
232
+ }
233
+ };
234
+
235
+ // 도트 클릭 이벤트 (텍스트 광고가 아닐 때만)
236
+ if (dotContainer) {
237
+ const dots = dotContainer.querySelectorAll('.adstage-dot');
238
+ dots.forEach((dot: Element, index: number) => {
239
+ dot.addEventListener('click', () => moveToSlide(index));
240
+ });
241
+ }
242
+
243
+ // 자동 슬라이드 (한 방향으로만 무한 진행)
244
+ let autoSlideTimer = setInterval(() => {
245
+ const nextIndex = currentSlide + 1;
246
+ moveToSlide(nextIndex);
247
+ handleInfiniteLoop();
248
+ }, autoSlideInterval);
249
+
250
+ // 마우스 호버 시 자동 슬라이드 일시정지
251
+ sliderWrapper.addEventListener('mouseenter', () => {
252
+ clearInterval(autoSlideTimer);
253
+ });
254
+
255
+ sliderWrapper.addEventListener('mouseleave', () => {
256
+ autoSlideTimer = setInterval(() => {
257
+ const nextIndex = currentSlide + 1;
258
+ moveToSlide(nextIndex);
259
+ handleInfiniteLoop();
260
+ }, autoSlideInterval);
261
+ });
262
+
263
+ // 터치 제스처 지원 수정 (무한 루프 지원)
264
+ SliderManager.addTouchSupport(slideContainer, moveToSlide, () => currentSlide, totalSlides, handleInfiniteLoop);
265
+
266
+ // 요소들 조립 (화살표 제거, 도트는 텍스트 광고가 아닐 때만 추가)
267
+ sliderWrapper.appendChild(slideContainer);
268
+ if (dotContainer) {
269
+ sliderWrapper.appendChild(dotContainer);
270
+ }
271
+
272
+ // 첫 번째 도트 활성화
273
+ moveToSlide(0);
274
+
275
+ // 사용자가 크기를 지정하지 않은 경우, 첫 번째 슬라이드 크기에 맞춰 래퍼 크기 동적 조정
276
+ if (!slot.width || slot.width === 0) {
277
+ // DOM 렌더링 후 크기 측정
278
+ setTimeout(() => {
279
+ const firstSlide = slideContainer.children[0] as HTMLElement;
280
+ if (firstSlide) {
281
+ const firstAdElement = firstSlide.children[0] as HTMLElement;
282
+ if (firstAdElement) {
283
+ const rect = firstAdElement.getBoundingClientRect();
284
+ sliderWrapper.style.width = `${rect.width}px`;
285
+ if (!slot.height || slot.height === 0) {
286
+ sliderWrapper.style.height = `${rect.height}px`;
287
+ }
288
+
289
+ // 크기 조정 후 overflow hidden 재적용
290
+ sliderWrapper.style.overflow = 'hidden';
291
+ }
292
+ }
293
+ }, 10);
294
+ }
295
+
296
+ return sliderWrapper;
297
+ }
298
+
299
+ /**
300
+ * 무채색 미니멀 도트 인디케이터 생성
301
+ */
302
+ private static createMinimalDotIndicator(count: number): HTMLElement {
303
+ const dotContainer = document.createElement('div');
304
+ dotContainer.className = 'adstage-dots';
305
+ dotContainer.style.cssText = `
306
+ position: absolute;
307
+ bottom: 15px;
308
+ left: 50%;
309
+ transform: translateX(-50%);
310
+ display: flex;
311
+ gap: 12px;
312
+ z-index: 3;
313
+ padding: 8px 16px;
314
+ border-radius: 20px;
315
+ background: rgba(255, 255, 255, 0.1);
316
+ backdrop-filter: blur(10px);
317
+ `;
318
+
319
+ for (let i = 0; i < count; i++) {
320
+ const dot = document.createElement('button');
321
+ dot.className = 'adstage-dot';
322
+ dot.style.cssText = `
323
+ width: 8px;
324
+ height: 8px;
325
+ border-radius: 50%;
326
+ border: 1px solid #cccccc;
327
+ background: transparent;
328
+ cursor: pointer;
329
+ transition: all 0.3s ease;
330
+ outline: none;
331
+ opacity: 0.7;
332
+ padding: 0;
333
+ margin: 0;
334
+ flex-shrink: 0;
335
+ `;
336
+
337
+ // 호버 효과
338
+ dot.addEventListener('mouseenter', () => {
339
+ if (!dot.classList.contains('active')) {
340
+ dot.style.borderColor = '#999999';
341
+ dot.style.opacity = '0.9';
342
+ }
343
+ });
344
+
345
+ dot.addEventListener('mouseleave', () => {
346
+ if (!dot.classList.contains('active')) {
347
+ dot.style.borderColor = '#cccccc';
348
+ dot.style.opacity = '0.7';
349
+ }
350
+ });
351
+
352
+ dotContainer.appendChild(dot);
353
+ }
354
+
355
+ return dotContainer;
356
+ }
357
+
358
+ /**
359
+ * 터치 제스처 지원 추가
360
+ */
361
+ private static addTouchSupport(
362
+ container: HTMLElement,
363
+ moveToSlide: (index: number, instant?: boolean) => void,
364
+ getCurrentSlide: () => number,
365
+ totalSlides: number,
366
+ handleInfiniteLoop?: () => void
367
+ ): void {
368
+ let startX = 0;
369
+ let isDragging = false;
370
+
371
+ container.addEventListener('touchstart', (e) => {
372
+ startX = e.touches[0].clientX;
373
+ isDragging = true;
374
+ });
375
+
376
+ container.addEventListener('touchmove', (e) => {
377
+ if (!isDragging) return;
378
+ e.preventDefault();
379
+ });
380
+
381
+ container.addEventListener('touchend', (e) => {
382
+ if (!isDragging) return;
383
+ isDragging = false;
384
+
385
+ const endX = e.changedTouches[0].clientX;
386
+ const diff = startX - endX;
387
+
388
+ if (Math.abs(diff) > 50) { // 50px 이상 스와이프 시
389
+ const currentSlide = getCurrentSlide();
390
+ if (diff > 0) {
391
+ // 왼쪽으로 스와이프 (다음 슬라이드)
392
+ const nextIndex = currentSlide + 1;
393
+ moveToSlide(nextIndex);
394
+ if (handleInfiniteLoop) {
395
+ handleInfiniteLoop();
396
+ }
397
+ } else {
398
+ // 오른쪽으로 스와이프 (이전 슬라이드)
399
+ const prevIndex = currentSlide > 0 ? currentSlide - 1 : totalSlides - 1;
400
+ moveToSlide(prevIndex);
401
+ }
402
+ }
403
+ });
404
+ }
405
+ }
@@ -0,0 +1,75 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+
3
+ interface AdErrorBoundaryProps {
4
+ children: ReactNode;
5
+ fallback?: ReactNode;
6
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
7
+ }
8
+
9
+ interface AdErrorBoundaryState {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ /**
15
+ * 광고 컴포넌트에서 발생하는 오류를 포착하는 Error Boundary
16
+ * 광고 로딩 실패 시 fallback UI를 표시하고 앱 전체가 크래시되는 것을 방지
17
+ */
18
+ export class AdErrorBoundary extends Component<AdErrorBoundaryProps, AdErrorBoundaryState> {
19
+ constructor(props: AdErrorBoundaryProps) {
20
+ super(props);
21
+ this.state = { hasError: false, error: null };
22
+ }
23
+
24
+ static getDerivedStateFromError(error: Error): AdErrorBoundaryState {
25
+ return { hasError: true, error };
26
+ }
27
+
28
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
29
+ console.error('AdStage Error Boundary caught an error:', error, errorInfo);
30
+
31
+ // 사용자 정의 에러 핸들러 호출
32
+ if (this.props.onError) {
33
+ this.props.onError(error, errorInfo);
34
+ }
35
+ }
36
+
37
+ render() {
38
+ if (this.state.hasError) {
39
+ // 사용자 정의 fallback이 있으면 사용, 없으면 기본 fallback 표시
40
+ if (this.props.fallback) {
41
+ return this.props.fallback;
42
+ }
43
+
44
+ return (
45
+ <div
46
+ style={{
47
+ padding: '20px',
48
+ textAlign: 'center',
49
+ backgroundColor: '#fee',
50
+ border: '1px solid #fcc',
51
+ borderRadius: '4px',
52
+ color: '#c00',
53
+ }}
54
+ >
55
+ <h3>광고 로딩 오류</h3>
56
+ <p>광고를 불러오는 중 문제가 발생했습니다.</p>
57
+ <button
58
+ onClick={() => this.setState({ hasError: false, error: null })}
59
+ style={{
60
+ padding: '8px 16px',
61
+ backgroundColor: '#fff',
62
+ border: '1px solid #ccc',
63
+ borderRadius: '4px',
64
+ cursor: 'pointer',
65
+ }}
66
+ >
67
+ 다시 시도
68
+ </button>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ return this.props.children;
74
+ }
75
+ }
@@ -0,0 +1,144 @@
1
+ import React, { useEffect, useRef, useMemo } from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { useAdStage } from '../hooks/useAdStage';
4
+
5
+ interface AdSlotProps {
6
+ slotId: string;
7
+ adType: AdType;
8
+ width?: string | number;
9
+ height?: string | number;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ autoSlideInterval?: number;
13
+ sliderEffect?: 'slide' | 'fade';
14
+ language?: string;
15
+ deviceType?: string;
16
+ country?: string;
17
+ }
18
+
19
+ /**
20
+ * 범용 광고 슬롯 컴포넌트
21
+ * 모든 광고 타입을 지원하며 SSR 환경에서도 안전하게 동작
22
+ */
23
+ export const AdSlot: React.FC<AdSlotProps> = ({
24
+ slotId,
25
+ adType,
26
+ width,
27
+ height,
28
+ className,
29
+ style,
30
+ autoSlideInterval = 3,
31
+ sliderEffect = 'slide',
32
+ language,
33
+ deviceType,
34
+ country,
35
+ }) => {
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const { sdk, isLoading, error } = useAdStage();
38
+
39
+ const containerId = useMemo(() => `adstage-${slotId}`, [slotId]);
40
+
41
+ useEffect(() => {
42
+ if (!sdk || !containerRef.current || isLoading || error) {
43
+ return;
44
+ }
45
+
46
+ // 컨테이너에 ID 설정
47
+ containerRef.current.id = containerId;
48
+
49
+ // 광고 슬롯 생성
50
+ const createSlot = async () => {
51
+ try {
52
+ await sdk.createSlot(slotId, containerId, adType, {
53
+ width,
54
+ height,
55
+ language,
56
+ deviceType,
57
+ country,
58
+ autoSlideInterval,
59
+ sliderEffect,
60
+ });
61
+ } catch (err) {
62
+ console.error(`Failed to create ad slot ${slotId}:`, err);
63
+ }
64
+ };
65
+
66
+ createSlot();
67
+
68
+ // 클린업 함수
69
+ return () => {
70
+ // SDK에 removeSlot 메서드가 없으므로 DOM 정리만 수행
71
+ if (containerRef.current) {
72
+ containerRef.current.innerHTML = '';
73
+ }
74
+ };
75
+ }, [
76
+ sdk,
77
+ slotId,
78
+ containerId,
79
+ adType,
80
+ width,
81
+ height,
82
+ language,
83
+ deviceType,
84
+ country,
85
+ autoSlideInterval,
86
+ sliderEffect,
87
+ isLoading,
88
+ error,
89
+ ]);
90
+
91
+ const containerStyle: React.CSSProperties = {
92
+ width: typeof width === 'number' ? `${width}px` : width,
93
+ height: typeof height === 'number' ? `${height}px` : height,
94
+ ...style,
95
+ };
96
+
97
+ // 로딩 상태 표시
98
+ if (isLoading) {
99
+ return (
100
+ <div
101
+ className={className}
102
+ style={{
103
+ ...containerStyle,
104
+ display: 'flex',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ backgroundColor: '#f5f5f5',
108
+ border: '1px dashed #ccc',
109
+ color: '#999',
110
+ }}
111
+ >
112
+ Loading Ad...
113
+ </div>
114
+ );
115
+ }
116
+
117
+ // 에러 상태 표시
118
+ if (error) {
119
+ return (
120
+ <div
121
+ className={className}
122
+ style={{
123
+ ...containerStyle,
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ backgroundColor: '#fee',
128
+ border: '1px solid #fcc',
129
+ color: '#c00',
130
+ }}
131
+ >
132
+ Ad Load Error
133
+ </div>
134
+ );
135
+ }
136
+
137
+ return (
138
+ <div
139
+ ref={containerRef}
140
+ className={className}
141
+ style={containerStyle}
142
+ />
143
+ );
144
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { AdSlot } from './AdSlot';
4
+
5
+ interface BannerAdProps {
6
+ slotId: string;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ autoSlideInterval?: number;
12
+ sliderEffect?: 'slide' | 'fade';
13
+ language?: string;
14
+ deviceType?: string;
15
+ country?: string;
16
+ }
17
+
18
+ /**
19
+ * 배너 광고 전용 컴포넌트
20
+ * AdSlot의 래퍼로 adType이 BANNER로 고정됨
21
+ */
22
+ export const BannerAd: React.FC<BannerAdProps> = (props) => {
23
+ return <AdSlot {...props} adType={AdType.BANNER} />;
24
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { AdSlot } from './AdSlot';
4
+
5
+ interface InterstitialAdProps {
6
+ slotId: string;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ autoSlideInterval?: number;
12
+ sliderEffect?: 'slide' | 'fade';
13
+ language?: string;
14
+ deviceType?: string;
15
+ country?: string;
16
+ }
17
+
18
+ /**
19
+ * 인터스티셜 광고 전용 컴포넌트
20
+ * AdSlot의 래퍼로 adType이 INTERSTITIAL로 고정됨
21
+ */
22
+ export const InterstitialAd: React.FC<InterstitialAdProps> = (props) => {
23
+ return <AdSlot {...props} adType={AdType.INTERSTITIAL} />;
24
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { AdSlot } from './AdSlot';
4
+
5
+ interface NativeAdProps {
6
+ slotId: string;
7
+ width?: string | number;
8
+ height?: string | number;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ autoSlideInterval?: number;
12
+ sliderEffect?: 'slide' | 'fade';
13
+ language?: string;
14
+ deviceType?: string;
15
+ country?: string;
16
+ }
17
+
18
+ /**
19
+ * 네이티브 광고 전용 컴포넌트
20
+ * AdSlot의 래퍼로 adType이 NATIVE로 고정됨
21
+ */
22
+ export const NativeAd: React.FC<NativeAdProps> = (props) => {
23
+ return <AdSlot {...props} adType={AdType.NATIVE} />;
24
+ };