@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,24 @@
1
+ import React from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { AdSlot } from './AdSlot';
4
+
5
+ interface TextAdProps {
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이 TEXT로 고정됨
21
+ */
22
+ export const TextAd: React.FC<TextAdProps> = (props) => {
23
+ return <AdSlot {...props} adType={AdType.TEXT} />;
24
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { AdSlot } from './AdSlot';
4
+
5
+ interface VideoAdProps {
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이 VIDEO로 고정됨
21
+ */
22
+ export const VideoAd: React.FC<VideoAdProps> = (props) => {
23
+ return <AdSlot {...props} adType={AdType.VIDEO} />;
24
+ };
@@ -0,0 +1,8 @@
1
+ // React 컴포넌트들
2
+ export { AdSlot } from './AdSlot';
3
+ export { BannerAd } from './BannerAd';
4
+ export { TextAd } from './TextAd';
5
+ export { NativeAd } from './NativeAd';
6
+ export { VideoAd } from './VideoAd';
7
+ export { InterstitialAd } from './InterstitialAd';
8
+ export { AdErrorBoundary } from './AdErrorBoundary';
@@ -0,0 +1,4 @@
1
+ // React Hook들
2
+ export { useAdStage } from './useAdStage';
3
+ export { useAdSlot } from './useAdSlot';
4
+ export { useAdTracking } from './useAdTracking';
@@ -0,0 +1,83 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { AdType } from '../../types/advertisement';
3
+ import { useAdStage } from './useAdStage';
4
+
5
+ interface UseAdSlotOptions {
6
+ slotId: string;
7
+ containerId: string;
8
+ adType: AdType;
9
+ width?: string | number;
10
+ height?: string | number;
11
+ language?: string;
12
+ deviceType?: string;
13
+ country?: string;
14
+ autoSlideInterval?: number;
15
+ sliderEffect?: 'slide' | 'fade';
16
+ }
17
+
18
+ interface UseAdSlotReturn {
19
+ isLoading: boolean;
20
+ error: string | null;
21
+ isCreated: boolean;
22
+ createSlot: () => Promise<void>;
23
+ resetSlot: () => void;
24
+ }
25
+
26
+ /**
27
+ * 광고 슬롯 생성 및 관리를 위한 Hook
28
+ * 컴포넌트에서 직접 슬롯을 제어하고 싶을 때 사용
29
+ */
30
+ export const useAdSlot = (options: UseAdSlotOptions): UseAdSlotReturn => {
31
+ const { sdk } = useAdStage();
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [isCreated, setIsCreated] = useState(false);
35
+
36
+ const createSlot = async () => {
37
+ if (!sdk) {
38
+ setError('SDK not initialized');
39
+ return;
40
+ }
41
+
42
+ setIsLoading(true);
43
+ setError(null);
44
+
45
+ try {
46
+ await sdk.createSlot(options.slotId, options.containerId, options.adType, {
47
+ width: options.width,
48
+ height: options.height,
49
+ language: options.language,
50
+ deviceType: options.deviceType,
51
+ country: options.country,
52
+ autoSlideInterval: options.autoSlideInterval,
53
+ sliderEffect: options.sliderEffect,
54
+ });
55
+ setIsCreated(true);
56
+ } catch (err) {
57
+ const errorMessage = err instanceof Error ? err.message : '슬롯 생성 중 오류가 발생했습니다.';
58
+ setError(errorMessage);
59
+ setIsCreated(false);
60
+ } finally {
61
+ setIsLoading(false);
62
+ }
63
+ };
64
+
65
+ const resetSlot = () => {
66
+ setIsLoading(false);
67
+ setError(null);
68
+ setIsCreated(false);
69
+ };
70
+
71
+ // SDK가 변경되면 상태 초기화
72
+ useEffect(() => {
73
+ resetSlot();
74
+ }, [sdk]);
75
+
76
+ return {
77
+ isLoading,
78
+ error,
79
+ isCreated,
80
+ createSlot,
81
+ resetSlot,
82
+ };
83
+ };
@@ -0,0 +1,14 @@
1
+ import { useContext } from 'react';
2
+ import { AdStageContext, type AdStageContextValue } from '../providers/AdStageProvider';
3
+
4
+ /**
5
+ * AdStage SDK 인스턴스에 접근하기 위한 Hook
6
+ * AdStageProvider 내부에서만 사용 가능
7
+ */
8
+ export const useAdStage = (): AdStageContextValue => {
9
+ const context = useContext(AdStageContext);
10
+ if (!context) {
11
+ throw new Error('useAdStage must be used within an AdStageProvider');
12
+ }
13
+ return context;
14
+ };
@@ -0,0 +1,61 @@
1
+ import { useCallback } from 'react';
2
+ import { AdEventType } from '../../types/advertisement';
3
+ import { useAdStage } from './useAdStage';
4
+
5
+ interface UseAdTrackingReturn {
6
+ trackEvent: (adId: string, slotId: string, eventType: AdEventType) => void;
7
+ trackClick: (adId: string, slotId: string) => void;
8
+ trackImpression: (adId: string, slotId: string) => void;
9
+ trackView: (adId: string, slotId: string) => void;
10
+ trackClose: (adId: string, slotId: string) => void;
11
+ }
12
+
13
+ /**
14
+ * 광고 이벤트 추적을 위한 Hook
15
+ * 커스텀 이벤트 추적이 필요할 때 사용
16
+ */
17
+ export const useAdTracking = (): UseAdTrackingReturn => {
18
+ const { sdk } = useAdStage();
19
+
20
+ const trackEvent = useCallback((adId: string, slotId: string, eventType: AdEventType) => {
21
+ if (!sdk) {
22
+ console.warn('SDK not initialized - cannot track event');
23
+ return;
24
+ }
25
+
26
+ try {
27
+ // SDK의 eventTracker에 직접 접근할 수 없으므로
28
+ // 여기서는 console.log로 대체하거나 SDK에 public 메서드가 있다면 사용
29
+ console.log('Ad Event Tracked:', { adId, slotId, eventType });
30
+
31
+ // 실제 구현에서는 SDK에 trackEvent 메서드가 있다면 사용
32
+ // sdk.trackEvent(adId, slotId, eventType);
33
+ } catch (err) {
34
+ console.error('Failed to track event:', err);
35
+ }
36
+ }, [sdk]);
37
+
38
+ const trackClick = useCallback((adId: string, slotId: string) => {
39
+ trackEvent(adId, slotId, AdEventType.CLICK);
40
+ }, [trackEvent]);
41
+
42
+ const trackImpression = useCallback((adId: string, slotId: string) => {
43
+ trackEvent(adId, slotId, AdEventType.IMPRESSION);
44
+ }, [trackEvent]);
45
+
46
+ const trackView = useCallback((adId: string, slotId: string) => {
47
+ trackEvent(adId, slotId, AdEventType.VIEWABLE);
48
+ }, [trackEvent]);
49
+
50
+ const trackClose = useCallback((adId: string, slotId: string) => {
51
+ trackEvent(adId, slotId, AdEventType.COMPLETED);
52
+ }, [trackEvent]);
53
+
54
+ return {
55
+ trackEvent,
56
+ trackClick,
57
+ trackImpression,
58
+ trackView,
59
+ trackClose,
60
+ };
61
+ };
@@ -0,0 +1,4 @@
1
+ // React 관련 모든 exports
2
+ export * from './components';
3
+ export * from './hooks';
4
+ export * from './providers';
@@ -0,0 +1,86 @@
1
+ import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
2
+
3
+ // 타입만 import해서 circular dependency 방지
4
+ import type { AdStageConfig } from '../../index';
5
+
6
+ // AdStageSDK 클래스 타입 정의 (런타임에는 dynamic import 사용)
7
+ interface AdStageSDKClass {
8
+ init(config: AdStageConfig): AdStageSDKInstance;
9
+ }
10
+
11
+ interface AdStageSDKInstance {
12
+ createSlot: (...args: any[]) => Promise<void>;
13
+ removeSlot?: (slotId: string) => void;
14
+ }
15
+
16
+ export interface AdStageContextValue {
17
+ sdk: AdStageSDKInstance | null;
18
+ isLoading: boolean;
19
+ error: string | null;
20
+ isInitialized: boolean;
21
+ }
22
+
23
+ const AdStageContext = createContext<AdStageContextValue>({
24
+ sdk: null,
25
+ isLoading: true,
26
+ error: null,
27
+ isInitialized: false,
28
+ });
29
+
30
+ export { AdStageContext };
31
+
32
+ interface AdStageProviderProps {
33
+ config: AdStageConfig;
34
+ children: ReactNode;
35
+ autoInit?: boolean;
36
+ }
37
+
38
+ export const AdStageProvider: React.FC<AdStageProviderProps> = ({
39
+ config,
40
+ children,
41
+ autoInit = true,
42
+ }) => {
43
+ const [sdk, setSdk] = useState<AdStageSDKInstance | null>(null);
44
+ const [isLoading, setIsLoading] = useState(true);
45
+ const [error, setError] = useState<string | null>(null);
46
+ const [isInitialized, setIsInitialized] = useState(false);
47
+
48
+ useEffect(() => {
49
+ const initializeSDK = async () => {
50
+ try {
51
+ setIsLoading(true);
52
+ setError(null);
53
+
54
+ // Dynamic import를 사용해서 circular dependency 방지
55
+ const { AdStageSDK } = await import('../../index');
56
+
57
+ // SDK 인스턴스 생성
58
+ const sdkInstance = AdStageSDK.init(config);
59
+
60
+ setSdk(sdkInstance);
61
+ setIsInitialized(true);
62
+ setIsLoading(false);
63
+ } catch (err) {
64
+ const errorMessage = err instanceof Error ? err.message : 'SDK 초기화 중 오류가 발생했습니다.';
65
+ setError(errorMessage);
66
+ setIsLoading(false);
67
+ console.error('AdStage SDK initialization failed:', err);
68
+ }
69
+ };
70
+
71
+ initializeSDK();
72
+ }, [config.apiKey, config.debug, autoInit]);
73
+
74
+ const value: AdStageContextValue = {
75
+ sdk,
76
+ isLoading,
77
+ error,
78
+ isInitialized,
79
+ };
80
+
81
+ return (
82
+ <AdStageContext.Provider value={value}>
83
+ {children}
84
+ </AdStageContext.Provider>
85
+ );
86
+ };
@@ -0,0 +1,2 @@
1
+ // React Provider
2
+ export { AdStageProvider } from './AdStageProvider';
@@ -0,0 +1,35 @@
1
+ import type { Advertisement, AdSlot } from '../types/advertisement';
2
+ import { BaseAdRenderer } from './base-renderer';
3
+ import { DOMUtils } from '../utils/dom-utils';
4
+
5
+ /**
6
+ * 배너 광고 렌더러 - 이미지만 표시
7
+ */
8
+ export class BannerAdRenderer extends BaseAdRenderer {
9
+ render(ad: Advertisement, slot: AdSlot): HTMLElement {
10
+ const adElement = DOMUtils.safeCreateElement('div');
11
+ if (!adElement) {
12
+ // SSR 환경에서는 기본 div 반환
13
+ return document.createElement('div');
14
+ }
15
+
16
+ // 기본 컨테이너 스타일 적용 (불필요한 스타일 제거)
17
+ this.applyStyles(adElement, this.getBaseContainerStyles(slot));
18
+
19
+ // 배너 광고는 이미지만 표시
20
+ if (!ad.imageUrl) {
21
+ // 이미지가 없는 경우 플레이스홀더 반환
22
+ const placeholder = this.createPlaceholder(slot, '배너 광고');
23
+ return placeholder || adElement;
24
+ }
25
+
26
+ const img = this.createImageElement(ad.imageUrl, '', slot);
27
+ if (img) {
28
+ DOMUtils.safeAppendChild(adElement, img);
29
+ // 클릭 이벤트 추가
30
+ this.addClickHandler(adElement, ad, slot);
31
+ }
32
+
33
+ return adElement;
34
+ }
35
+ }
@@ -0,0 +1,207 @@
1
+ import type { Advertisement, AdSlot, AdEventType } from '../types/advertisement';
2
+ import { DOMUtils } from '../utils/dom-utils';
3
+
4
+ /**
5
+ * 광고 렌더러 인터페이스
6
+ */
7
+ export interface AdRenderer {
8
+ render(ad: Advertisement, slot: AdSlot): HTMLElement;
9
+ }
10
+
11
+ /**
12
+ * 이벤트 추적 콜백 타입
13
+ */
14
+ export type EventTracker = (adId: string, slotId: string, eventType: AdEventType) => void;
15
+
16
+ /**
17
+ * 기본 광고 렌더러 추상 클래스
18
+ */
19
+ export abstract class BaseAdRenderer implements AdRenderer {
20
+ protected trackEvent?: EventTracker;
21
+
22
+ constructor(trackEvent?: EventTracker) {
23
+ this.trackEvent = trackEvent;
24
+ }
25
+
26
+ /**
27
+ * 광고 렌더링 (각 구현체에서 오버라이드)
28
+ */
29
+ abstract render(ad: Advertisement, slot: AdSlot): HTMLElement;
30
+
31
+ /**
32
+ * 공통 클릭 이벤트 핸들러 (SSR 안전)
33
+ */
34
+ protected addClickHandler(element: HTMLElement, ad: Advertisement, slot: AdSlot): void {
35
+ DOMUtils.safeAddEventListener(element, 'click', () => {
36
+ this.trackEvent?.(ad._id, slot.id, 'CLICK' as AdEventType);
37
+ if (ad.linkUrl) {
38
+ DOMUtils.safeWindowOpen(ad.linkUrl, '_blank');
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * 공통 스타일 적용 유틸리티 (SSR 안전)
45
+ */
46
+ protected applyStyles(element: HTMLElement, styles: Record<string, string>): void {
47
+ DOMUtils.safeApplyStyles(element, styles);
48
+ }
49
+
50
+ /**
51
+ * 크기 값 파싱 유틸리티 (px, %, number 지원)
52
+ */
53
+ protected parseSizeValue(value: number | string | undefined): string | undefined {
54
+ if (!value) return undefined;
55
+
56
+ if (typeof value === 'number') {
57
+ return value > 0 ? `${value}px` : undefined;
58
+ }
59
+
60
+ if (typeof value === 'string') {
61
+ const trimmed = value.trim();
62
+ if (!trimmed) return undefined;
63
+
64
+ // 퍼센트 값 처리
65
+ if (trimmed.endsWith('%')) {
66
+ const percent = parseFloat(trimmed);
67
+ return !isNaN(percent) && percent > 0 ? trimmed : undefined;
68
+ }
69
+
70
+ // px 값 처리 (px 단위 포함/미포함 모두 지원)
71
+ const numValue = trimmed.endsWith('px')
72
+ ? parseFloat(trimmed.slice(0, -2))
73
+ : parseFloat(trimmed);
74
+
75
+ return !isNaN(numValue) && numValue > 0 ? `${numValue}px` : undefined;
76
+ }
77
+
78
+ return undefined;
79
+ }
80
+
81
+ /**
82
+ * 기본 컨테이너 스타일 (사용자 지정 크기만 적용)
83
+ */
84
+ protected getBaseContainerStyles(slot: AdSlot): Record<string, string> {
85
+ const styles: Record<string, string> = {
86
+ cursor: 'pointer',
87
+ position: 'relative',
88
+ overflow: 'hidden',
89
+ };
90
+
91
+ // 사용자가 지정한 크기가 있을 때만 적용
92
+ const parsedWidth = this.parseSizeValue(slot.width);
93
+ const parsedHeight = this.parseSizeValue(slot.height);
94
+
95
+ if (parsedWidth) {
96
+ styles.width = parsedWidth;
97
+ }
98
+ if (parsedHeight) {
99
+ styles.height = parsedHeight;
100
+ }
101
+
102
+ return styles;
103
+ }
104
+
105
+ /**
106
+ * 이미지 스타일 (고유 사이즈 유지, 사용자 지정 크기 우선)
107
+ */
108
+ protected getImageStyles(slot?: AdSlot): Record<string, string> {
109
+ const styles: Record<string, string> = {
110
+ display: 'block',
111
+ 'max-width': '100%',
112
+ height: 'auto',
113
+ };
114
+
115
+ // 사용자가 컨테이너 크기를 지정한 경우에만 크기 제한
116
+ const parsedWidth = this.parseSizeValue(slot?.width);
117
+ const parsedHeight = this.parseSizeValue(slot?.height);
118
+
119
+ if (parsedWidth && parsedHeight) {
120
+ styles.width = '100%';
121
+ styles.height = '100%';
122
+ styles['object-fit'] = 'cover';
123
+ }
124
+
125
+ return styles;
126
+ }
127
+
128
+ /**
129
+ * 기본 폰트 스타일
130
+ */
131
+ protected getBaseFontStyles(): Record<string, string> {
132
+ return {
133
+ 'font-family': 'Arial, sans-serif',
134
+ 'line-height': '1.4',
135
+ 'word-break': 'keep-all',
136
+ };
137
+ }
138
+
139
+ /**
140
+ * 이미지 요소 생성 (SSR 안전)
141
+ */
142
+ protected createImageElement(imageUrl: string, alt: string = '', slot?: AdSlot): HTMLImageElement | null {
143
+ const img = DOMUtils.safeCreateElement('img') as HTMLImageElement;
144
+ if (!img) return null;
145
+
146
+ img.src = imageUrl;
147
+ img.alt = alt;
148
+ this.applyStyles(img, this.getImageStyles(slot));
149
+ return img;
150
+ }
151
+
152
+ /**
153
+ * 텍스트 요소 생성 (SSR 안전)
154
+ */
155
+ protected createTextElement(
156
+ text: string,
157
+ tag: keyof HTMLElementTagNameMap = 'div',
158
+ additionalStyles: Record<string, string> = {}
159
+ ): HTMLElement | null {
160
+ const element = DOMUtils.safeCreateElement(tag);
161
+ if (!element) return null;
162
+
163
+ DOMUtils.safeSetTextContent(element, text);
164
+ this.applyStyles(element, {
165
+ ...this.getBaseFontStyles(),
166
+ ...additionalStyles,
167
+ });
168
+ return element;
169
+ }
170
+
171
+ /**
172
+ * 플레이스홀더 요소 생성
173
+ */
174
+ protected createPlaceholder(slot: AdSlot, text: string = '광고'): HTMLElement {
175
+ let placeholder = DOMUtils.safeCreateElement('div');
176
+
177
+ // SSR 환경에서 DOM을 사용할 수 없는 경우, 런타임에 생성되도록 함
178
+ if (!placeholder) {
179
+ // SSR에서는 빈 div를 반환하되, 브라우저에서는 제대로 작동하도록 함
180
+ if (typeof document !== 'undefined') {
181
+ placeholder = document.createElement('div');
182
+ } else {
183
+ // SSR 환경에서는 더미 객체 반환 (타입 단언 사용)
184
+ placeholder = {} as HTMLElement;
185
+ }
186
+ }
187
+
188
+ // DOM이 사용 가능한 경우에만 스타일 적용
189
+ if (DOMUtils.canUseDOM() && placeholder) {
190
+ this.applyStyles(placeholder, {
191
+ ...this.getBaseContainerStyles(slot),
192
+ background: '#f8f9fa',
193
+ display: 'flex',
194
+ 'align-items': 'center',
195
+ 'justify-content': 'center',
196
+ color: '#6c757d',
197
+ ...this.getBaseFontStyles(),
198
+ // 플레이스홀더는 최소 크기 보장
199
+ 'min-width': '100px',
200
+ 'min-height': '100px',
201
+ });
202
+ DOMUtils.safeSetTextContent(placeholder, text);
203
+ }
204
+
205
+ return placeholder;
206
+ }
207
+ }
@@ -0,0 +1,71 @@
1
+ import { AdType } from '../types/advertisement';
2
+ import type { Advertisement, AdSlot } from '../types/advertisement';
3
+ import type { AdRenderer, EventTracker } from './base-renderer';
4
+ import { BannerAdRenderer } from './banner-renderer';
5
+ import { TextAdRenderer } from './text-renderer';
6
+ import { NativeAdRenderer } from './native-renderer';
7
+ import { VideoAdRenderer } from './video-renderer';
8
+ import { InterstitialAdRenderer } from './interstitial-renderer';
9
+
10
+ /**
11
+ * 광고 렌더러 팩토리
12
+ * - 광고 타입에 따라 적절한 렌더러 인스턴스를 반환
13
+ */
14
+ export class AdRendererFactory {
15
+ private static renderers = new Map<AdType, new (trackEvent?: EventTracker) => AdRenderer>();
16
+
17
+ static {
18
+ // 렌더러 등록
19
+ this.renderers.set(AdType.BANNER, BannerAdRenderer);
20
+ this.renderers.set(AdType.TEXT, TextAdRenderer);
21
+ this.renderers.set(AdType.NATIVE, NativeAdRenderer);
22
+ this.renderers.set(AdType.VIDEO, VideoAdRenderer);
23
+ this.renderers.set(AdType.INTERSTITIAL, InterstitialAdRenderer);
24
+ this.renderers.set(AdType.POPUP, InterstitialAdRenderer); // POPUP은 INTERSTITIAL과 동일
25
+ }
26
+
27
+ /**
28
+ * 광고 타입에 맞는 렌더러 생성
29
+ */
30
+ static createRenderer(adType: AdType, trackEvent?: EventTracker): AdRenderer {
31
+ const RendererClass = this.renderers.get(adType);
32
+
33
+ if (!RendererClass) {
34
+ console.warn(`No renderer found for ad type: ${adType}, falling back to Banner renderer`);
35
+ return new BannerAdRenderer(trackEvent);
36
+ }
37
+
38
+ return new RendererClass(trackEvent);
39
+ }
40
+
41
+ /**
42
+ * 광고 렌더링 (편의 메서드)
43
+ */
44
+ static render(ad: Advertisement, slot: AdSlot, trackEvent?: EventTracker): HTMLElement {
45
+ const renderer = this.createRenderer(slot.adType, trackEvent);
46
+ return renderer.render(ad, slot);
47
+ }
48
+
49
+ /**
50
+ * 사용 가능한 렌더러 타입 목록
51
+ */
52
+ static getSupportedAdTypes(): AdType[] {
53
+ return Array.from(this.renderers.keys());
54
+ }
55
+
56
+ /**
57
+ * 커스텀 렌더러 등록
58
+ */
59
+ static registerRenderer(adType: AdType, RendererClass: new (trackEvent?: EventTracker) => AdRenderer): void {
60
+ this.renderers.set(adType, RendererClass);
61
+ }
62
+ }
63
+
64
+ // 개별 렌더러들 export
65
+ export { BannerAdRenderer } from './banner-renderer';
66
+ export { TextAdRenderer } from './text-renderer';
67
+ export { NativeAdRenderer } from './native-renderer';
68
+ export { VideoAdRenderer } from './video-renderer';
69
+ export { InterstitialAdRenderer } from './interstitial-renderer';
70
+ export { BaseAdRenderer } from './base-renderer';
71
+ export type { AdRenderer, EventTracker } from './base-renderer';