@adstage/web-sdk 2.5.2 → 2.6.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 +59 -0
- package/dist/index.cjs.js +2073 -1050
- package/dist/index.d.ts +55 -12
- package/dist/index.esm.js +2073 -1050
- package/dist/index.standalone.js +2073 -1050
- package/package.json +1 -1
- package/src/constants/endpoints.ts +0 -1
- package/src/core/{AdStage.ts → adstage.ts} +36 -8
- package/src/index.ts +9 -3
- package/src/managers/ads/advertisement-event-tracker.ts +15 -11
- package/src/managers/ads/carousel-slider-manager.ts +91 -19
- package/src/managers/ads/slider-event-tracker.ts +57 -0
- package/src/managers/ads/text-transition-manager.ts +91 -26
- package/src/modules/ads/ad-renderer.ts +259 -0
- package/src/modules/ads/{AdsModule.ts → ads-module.ts} +202 -21
- package/src/modules/ads/interfaces/i-ad-renderer.ts +77 -0
- package/src/modules/ads/renderers/banner-ad-renderer.ts +414 -0
- package/src/modules/ads/renderers/base-ad-renderer.ts +340 -0
- package/src/modules/ads/renderers/interstitial-ad-renderer.ts +256 -0
- package/src/modules/ads/renderers/native-ad-renderer.ts +154 -0
- package/src/modules/ads/renderers/text-ad-renderer.ts +120 -0
- package/src/modules/ads/renderers/video-ad-renderer.ts +433 -0
- package/src/modules/config/{ConfigModule.ts → config-module.ts} +1 -5
- package/src/react/{AdStageProvider.tsx → ad-stage-provider.tsx} +1 -1
- package/src/react/index.ts +2 -2
- package/src/types/config.ts +2 -184
- package/src/utils/ad-click-handler.ts +155 -0
- package/src/utils/text-ad-utils.ts +37 -0
- package/src/dummy/ads_dummy.json +0 -84
- package/src/modules/ads/AdRenderer.ts +0 -735
- package/src/renderers/banner-renderer.ts +0 -35
- package/src/renderers/base-renderer.ts +0 -209
- package/src/renderers/index.ts +0 -71
- package/src/renderers/interstitial-renderer.ts +0 -70
- package/src/renderers/native-renderer.ts +0 -35
- package/src/renderers/text-renderer.ts +0 -94
- package/src/renderers/video-renderer.ts +0 -63
- /package/src/modules/events/{EventsModule.ts → events-module.ts} +0 -0
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
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
|
-
'object-position': 'center', // 🎯 이미지 항상 중앙 정렬
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// 사용자가 컨테이너 크기를 지정한 경우에만 크기 제한
|
|
117
|
-
const parsedWidth = this.parseSizeValue(slot?.width);
|
|
118
|
-
const parsedHeight = this.parseSizeValue(slot?.height);
|
|
119
|
-
|
|
120
|
-
if (parsedWidth && parsedHeight) {
|
|
121
|
-
styles.width = '100%';
|
|
122
|
-
styles.height = '100%';
|
|
123
|
-
styles['object-fit'] = 'cover';
|
|
124
|
-
styles['object-position'] = 'center'; // 🎯 크기 조정 시에도 중앙 정렬
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return styles;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 기본 폰트 스타일
|
|
132
|
-
*/
|
|
133
|
-
protected getBaseFontStyles(): Record<string, string> {
|
|
134
|
-
return {
|
|
135
|
-
'font-family': 'Arial, sans-serif',
|
|
136
|
-
'line-height': '1.4',
|
|
137
|
-
'word-break': 'keep-all',
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 이미지 요소 생성 (SSR 안전)
|
|
143
|
-
*/
|
|
144
|
-
protected createImageElement(imageUrl: string, alt: string = '', slot?: AdSlot): HTMLImageElement | null {
|
|
145
|
-
const img = DOMUtils.safeCreateElement('img') as HTMLImageElement;
|
|
146
|
-
if (!img) return null;
|
|
147
|
-
|
|
148
|
-
img.src = imageUrl;
|
|
149
|
-
img.alt = alt;
|
|
150
|
-
this.applyStyles(img, this.getImageStyles(slot));
|
|
151
|
-
return img;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 텍스트 요소 생성 (SSR 안전)
|
|
156
|
-
*/
|
|
157
|
-
protected createTextElement(
|
|
158
|
-
text: string,
|
|
159
|
-
tag: keyof HTMLElementTagNameMap = 'div',
|
|
160
|
-
additionalStyles: Record<string, string> = {}
|
|
161
|
-
): HTMLElement | null {
|
|
162
|
-
const element = DOMUtils.safeCreateElement(tag);
|
|
163
|
-
if (!element) return null;
|
|
164
|
-
|
|
165
|
-
DOMUtils.safeSetTextContent(element, text);
|
|
166
|
-
this.applyStyles(element, {
|
|
167
|
-
...this.getBaseFontStyles(),
|
|
168
|
-
...additionalStyles,
|
|
169
|
-
});
|
|
170
|
-
return element;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 플레이스홀더 요소 생성
|
|
175
|
-
*/
|
|
176
|
-
protected createPlaceholder(slot: AdSlot, text: string = '광고'): HTMLElement {
|
|
177
|
-
let placeholder = DOMUtils.safeCreateElement('div');
|
|
178
|
-
|
|
179
|
-
// SSR 환경에서 DOM을 사용할 수 없는 경우, 런타임에 생성되도록 함
|
|
180
|
-
if (!placeholder) {
|
|
181
|
-
// SSR에서는 빈 div를 반환하되, 브라우저에서는 제대로 작동하도록 함
|
|
182
|
-
if (typeof document !== 'undefined') {
|
|
183
|
-
placeholder = document.createElement('div');
|
|
184
|
-
} else {
|
|
185
|
-
// SSR 환경에서는 더미 객체 반환 (타입 단언 사용)
|
|
186
|
-
placeholder = {} as HTMLElement;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// DOM이 사용 가능한 경우에만 스타일 적용
|
|
191
|
-
if (DOMUtils.canUseDOM() && placeholder) {
|
|
192
|
-
this.applyStyles(placeholder, {
|
|
193
|
-
...this.getBaseContainerStyles(slot),
|
|
194
|
-
background: '#f8f9fa',
|
|
195
|
-
display: 'flex',
|
|
196
|
-
'align-items': 'center',
|
|
197
|
-
'justify-content': 'center',
|
|
198
|
-
color: '#6c757d',
|
|
199
|
-
...this.getBaseFontStyles(),
|
|
200
|
-
// 플레이스홀더는 최소 크기 보장
|
|
201
|
-
'min-width': '100px',
|
|
202
|
-
'min-height': '100px',
|
|
203
|
-
});
|
|
204
|
-
DOMUtils.safeSetTextContent(placeholder, text);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return placeholder;
|
|
208
|
-
}
|
|
209
|
-
}
|
package/src/renderers/index.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
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';
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { Advertisement, AdSlot, AdEventType } from '../types/advertisement';
|
|
2
|
-
import { DOMUtils } from '../utils/dom-utils';
|
|
3
|
-
import { BaseAdRenderer } from './base-renderer';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 전면/팝업 광고 렌더러 - 핵심 콘텐츠만 표시
|
|
7
|
-
*/
|
|
8
|
-
export class InterstitialAdRenderer extends BaseAdRenderer {
|
|
9
|
-
render(ad: Advertisement, slot: AdSlot): HTMLElement {
|
|
10
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
11
|
-
|
|
12
|
-
if (!adElement) {
|
|
13
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
17
|
-
this.applyStyles(adElement, {
|
|
18
|
-
...this.getBaseContainerStyles(slot),
|
|
19
|
-
display: 'flex',
|
|
20
|
-
'flex-direction': 'column',
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// 우선순위: 1. 이미지, 2. 비디오, 3. 텍스트
|
|
24
|
-
if (ad.imageUrl) {
|
|
25
|
-
const img = this.createImageElement(ad.imageUrl, '', slot);
|
|
26
|
-
if (img) {
|
|
27
|
-
adElement.appendChild(img);
|
|
28
|
-
}
|
|
29
|
-
} else if (ad.videoUrl) {
|
|
30
|
-
// 이미지가 없고 비디오가 있는 경우
|
|
31
|
-
const video = this.createVideoElement(ad.videoUrl, ad, slot);
|
|
32
|
-
if (video) {
|
|
33
|
-
adElement.appendChild(video);
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
// 모든 콘텐츠가 없는 경우
|
|
37
|
-
return this.createPlaceholder(slot, '전면 광고');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 클릭 이벤트 추가
|
|
41
|
-
this.addClickHandler(adElement, ad, slot);
|
|
42
|
-
|
|
43
|
-
return adElement;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 비디오 요소 생성
|
|
48
|
-
*/
|
|
49
|
-
private createVideoElement(videoUrl: string, ad: Advertisement, slot: AdSlot): HTMLVideoElement | null {
|
|
50
|
-
const video = DOMUtils.safeCreateElement('video') as HTMLVideoElement;
|
|
51
|
-
if (!video) return null;
|
|
52
|
-
|
|
53
|
-
video.src = videoUrl;
|
|
54
|
-
video.controls = true;
|
|
55
|
-
|
|
56
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
57
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
58
|
-
|
|
59
|
-
// 비디오 이벤트 추적
|
|
60
|
-
video.addEventListener('play', () => {
|
|
61
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START' as AdEventType);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
video.addEventListener('ended', () => {
|
|
65
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE' as AdEventType);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return video;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { Advertisement, AdSlot } from '../types/advertisement';
|
|
2
|
-
import { DOMUtils } from '../utils/dom-utils';
|
|
3
|
-
import { BaseAdRenderer } from './base-renderer';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 네이티브 광고 렌더러 - 이미지 + textContent 표시
|
|
7
|
-
*/
|
|
8
|
-
export class NativeAdRenderer extends BaseAdRenderer {
|
|
9
|
-
render(ad: Advertisement, slot: AdSlot): HTMLElement {
|
|
10
|
-
const adElement = DOMUtils.safeCreateElement('div');
|
|
11
|
-
if (!adElement) {
|
|
12
|
-
return document.createElement('div');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
16
|
-
this.applyStyles(adElement, this.getBaseContainerStyles(slot));
|
|
17
|
-
|
|
18
|
-
// 네이티브 광고는 이미지만 표시
|
|
19
|
-
if (!ad.imageUrl) {
|
|
20
|
-
// 이미지가 없는 경우 플레이스홀더 반환
|
|
21
|
-
const placeholder = this.createPlaceholder(slot, '네이티브 광고');
|
|
22
|
-
return placeholder || adElement;
|
|
23
|
-
}
|
|
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
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { Advertisement, AdSlot } from '../types/advertisement';
|
|
2
|
-
import { BaseAdRenderer } from './base-renderer';
|
|
3
|
-
import { DOMUtils } from '../utils/dom-utils';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 텍스트 광고 렌더러 - textContent만 표시
|
|
7
|
-
*/
|
|
8
|
-
export class TextAdRenderer extends BaseAdRenderer {
|
|
9
|
-
render(ad: Advertisement, slot: AdSlot): HTMLElement {
|
|
10
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
11
|
-
|
|
12
|
-
if (!adElement) {
|
|
13
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 기본 컨테이너 스타일
|
|
17
|
-
const containerStyles: Record<string, string> = {
|
|
18
|
-
...this.getBaseContainerStyles(slot),
|
|
19
|
-
padding: '4px',
|
|
20
|
-
background: 'transparent',
|
|
21
|
-
display: 'flex',
|
|
22
|
-
'align-items': 'center',
|
|
23
|
-
'justify-content': 'center',
|
|
24
|
-
// text-align은 사용자가 설정할 수 있도록 기본값에서 제외
|
|
25
|
-
...this.getBaseFontStyles(),
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// 사용자가 크기를 지정하지 않은 경우 컨텐츠에 맞춤
|
|
29
|
-
if (!slot.width || slot.width === 0) {
|
|
30
|
-
containerStyles.display = 'inline-flex';
|
|
31
|
-
containerStyles['white-space'] = 'nowrap';
|
|
32
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// height만 자동인 경우 줄바꿈 허용
|
|
36
|
-
if ((slot.width && slot.width !== 0) && (!slot.height || slot.height === 0)) {
|
|
37
|
-
containerStyles['white-space'] = 'normal';
|
|
38
|
-
containerStyles['min-height'] = 'auto';
|
|
39
|
-
containerStyles['justify-content'] = 'flex-start'; // 좌측 정렬로 변경
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.applyStyles(adElement, containerStyles);
|
|
43
|
-
|
|
44
|
-
// 텍스트 광고는 textContent만 표시
|
|
45
|
-
if (!ad.textContent) {
|
|
46
|
-
// 텍스트가 없는 경우 플레이스홀더 반환
|
|
47
|
-
return this.createPlaceholder(slot, '텍스트 광고');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 텍스트 콘텐츠 생성
|
|
51
|
-
const textContent = this.createTextElement(
|
|
52
|
-
ad.textContent,
|
|
53
|
-
'div',
|
|
54
|
-
{
|
|
55
|
-
'font-size': '14px',
|
|
56
|
-
'font-weight': '500',
|
|
57
|
-
color: '#212529',
|
|
58
|
-
width: '100%', // 전체 너비 사용하여 텍스트 정렬이 적용되도록 함
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (textContent) {
|
|
63
|
-
adElement.appendChild(textContent);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 사용자가 text-align을 지정했는지 확인하고 레이아웃 조정
|
|
67
|
-
setTimeout(() => {
|
|
68
|
-
if (!adElement || typeof window === 'undefined') return;
|
|
69
|
-
|
|
70
|
-
const computedStyle = window.getComputedStyle(adElement);
|
|
71
|
-
const textAlign = computedStyle.textAlign;
|
|
72
|
-
|
|
73
|
-
// 사용자가 text-align을 설정했고, width가 없는 경우
|
|
74
|
-
if (textAlign && textAlign !== 'start' && textAlign !== 'left' && (!slot.width || slot.width === 0)) {
|
|
75
|
-
// 블록 레벨로 변경하여 text-align이 제대로 작동하도록 함
|
|
76
|
-
adElement.style.display = 'block';
|
|
77
|
-
adElement.style.whiteSpace = 'normal';
|
|
78
|
-
|
|
79
|
-
// 최소 너비 설정 (텍스트가 한 줄일 때를 위해)
|
|
80
|
-
if (textContent) {
|
|
81
|
-
const textRect = textContent.getBoundingClientRect();
|
|
82
|
-
if (textRect.width > 0) {
|
|
83
|
-
adElement.style.minWidth = `${textRect.width}px`;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}, 0);
|
|
88
|
-
|
|
89
|
-
// 클릭 이벤트 추가
|
|
90
|
-
this.addClickHandler(adElement, ad, slot);
|
|
91
|
-
|
|
92
|
-
return adElement;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { Advertisement, AdSlot, AdEventType } from '../types/advertisement';
|
|
2
|
-
import { DOMUtils } from '../utils/dom-utils';
|
|
3
|
-
import { BaseAdRenderer } from './base-renderer';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 비디오 광고 렌더러 - 비디오 또는 이미지 표시
|
|
7
|
-
*/
|
|
8
|
-
export class VideoAdRenderer extends BaseAdRenderer {
|
|
9
|
-
render(ad: Advertisement, slot: AdSlot): HTMLElement {
|
|
10
|
-
let adElement = DOMUtils.safeCreateElement('div');
|
|
11
|
-
|
|
12
|
-
if (!adElement) {
|
|
13
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 컨테이너 스타일 적용 (불필요한 스타일 제거)
|
|
17
|
-
this.applyStyles(adElement, {
|
|
18
|
-
...this.getBaseContainerStyles(slot),
|
|
19
|
-
background: '#000',
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// 비디오 광고는 비디오만 표시
|
|
23
|
-
if (!ad.videoUrl) {
|
|
24
|
-
// 비디오가 없는 경우 플레이스홀더 반환
|
|
25
|
-
return this.createPlaceholder(slot, '비디오 광고');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const video = this.createVideoElement(ad.videoUrl, ad, slot);
|
|
29
|
-
if (video) {
|
|
30
|
-
adElement.appendChild(video);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 클릭 이벤트 추가
|
|
34
|
-
this.addClickHandler(adElement, ad, slot);
|
|
35
|
-
|
|
36
|
-
return adElement;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 비디오 요소 생성
|
|
41
|
-
*/
|
|
42
|
-
private createVideoElement(videoUrl: string, ad: Advertisement, slot: AdSlot): HTMLVideoElement | null {
|
|
43
|
-
const video = DOMUtils.safeCreateElement('video') as HTMLVideoElement;
|
|
44
|
-
if (!video) return null;
|
|
45
|
-
|
|
46
|
-
video.src = videoUrl;
|
|
47
|
-
video.controls = true;
|
|
48
|
-
|
|
49
|
-
// 비디오도 이미지와 같은 스타일 적용
|
|
50
|
-
this.applyStyles(video, this.getImageStyles(slot));
|
|
51
|
-
|
|
52
|
-
// 비디오 이벤트 추적
|
|
53
|
-
video.addEventListener('play', () => {
|
|
54
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_START' as AdEventType);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
video.addEventListener('ended', () => {
|
|
58
|
-
this.trackEvent?.(ad._id, slot.id, 'VIDEO_COMPLETE' as AdEventType);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return video;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
File without changes
|