@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs.js +2304 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.esm.js +2288 -0
- package/dist/index.standalone.js +2331 -0
- package/examples/README.md +33 -0
- package/examples/banner-ads.html +512 -0
- package/examples/index.html +338 -0
- package/examples/native-ads.html +634 -0
- package/examples/react-app/README.md +70 -0
- package/examples/react-app/index.html +13 -0
- package/examples/react-app/package-lock.json +3042 -0
- package/examples/react-app/package.json +26 -0
- package/examples/react-app/pnpm-lock.yaml +1857 -0
- package/examples/react-app/public/index.standalone.js +2331 -0
- package/examples/react-app/src/App.tsx +226 -0
- package/examples/react-app/src/index.css +37 -0
- package/examples/react-app/src/main.tsx +10 -0
- package/examples/react-app/tsconfig.json +25 -0
- package/examples/react-app/tsconfig.node.json +10 -0
- package/examples/react-app/vite.config.ts +15 -0
- package/examples/react-nextjs/app/globals.css +200 -0
- package/examples/react-nextjs/app/layout.tsx +27 -0
- package/examples/react-nextjs/app/page.tsx +258 -0
- package/examples/react-nextjs/next.config.js +9 -0
- package/examples/react-nextjs/package.json +22 -0
- package/examples/react-nextjs/pnpm-lock.yaml +343 -0
- package/examples/react-nextjs/tsconfig.json +34 -0
- package/examples/text-ads.html +597 -0
- package/examples/video-ads.html +739 -0
- package/package.json +83 -0
- package/src/global.d.ts +20 -0
- package/src/index.ts +350 -0
- package/src/managers/device-info-collector.ts +127 -0
- package/src/managers/event-tracker.ts +131 -0
- package/src/managers/fade-slider-manager.ts +276 -0
- package/src/managers/impression-tracker.ts +88 -0
- package/src/managers/slider-manager.ts +405 -0
- package/src/react/components/AdErrorBoundary.tsx +75 -0
- package/src/react/components/AdSlot.tsx +144 -0
- package/src/react/components/BannerAd.tsx +24 -0
- package/src/react/components/InterstitialAd.tsx +24 -0
- package/src/react/components/NativeAd.tsx +24 -0
- package/src/react/components/TextAd.tsx +24 -0
- package/src/react/components/VideoAd.tsx +24 -0
- package/src/react/components/index.ts +8 -0
- package/src/react/hooks/index.ts +4 -0
- package/src/react/hooks/useAdSlot.ts +83 -0
- package/src/react/hooks/useAdStage.ts +14 -0
- package/src/react/hooks/useAdTracking.ts +61 -0
- package/src/react/index.ts +4 -0
- package/src/react/providers/AdStageProvider.tsx +86 -0
- package/src/react/providers/index.ts +2 -0
- package/src/renderers/banner-renderer.ts +35 -0
- package/src/renderers/base-renderer.ts +207 -0
- package/src/renderers/index.ts +71 -0
- package/src/renderers/interstitial-renderer.ts +70 -0
- package/src/renderers/native-renderer.ts +35 -0
- package/src/renderers/text-renderer.ts +94 -0
- package/src/renderers/video-renderer.ts +63 -0
- package/src/types/advertisement.ts +197 -0
- package/src/types/api.ts +173 -0
- package/src/types/config.ts +174 -0
- package/src/types/events.ts +60 -0
- package/src/types/index.ts +6 -0
- package/src/utils/dom-utils.ts +237 -0
- package/src/utils/sdk-utils.ts +134 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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: '20px',
|
|
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': '16px',
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// 광고 타입 정의
|
|
2
|
+
export enum AdType {
|
|
3
|
+
BANNER = 'BANNER',
|
|
4
|
+
POPUP = 'POPUP',
|
|
5
|
+
INTERSTITIAL = 'INTERSTITIAL',
|
|
6
|
+
NATIVE = 'NATIVE',
|
|
7
|
+
VIDEO = 'VIDEO',
|
|
8
|
+
TEXT = 'TEXT',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 플랫폼 정의
|
|
12
|
+
export enum Platform {
|
|
13
|
+
WEB = 'WEB',
|
|
14
|
+
MOBILE = 'MOBILE',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 광고 이벤트 타입
|
|
18
|
+
export enum AdEventType {
|
|
19
|
+
IMPRESSION = 'IMPRESSION',
|
|
20
|
+
CLICK = 'CLICK',
|
|
21
|
+
HOVER = 'HOVER',
|
|
22
|
+
VIEWABLE = 'VIEWABLE',
|
|
23
|
+
VIEWABLE_IMPRESSION = 'VIEWABLE_IMPRESSION',
|
|
24
|
+
COMPLETED = 'COMPLETED',
|
|
25
|
+
VIDEO_START = 'VIDEO_START',
|
|
26
|
+
VIDEO_COMPLETE = 'VIDEO_COMPLETE',
|
|
27
|
+
ERROR = 'ERROR',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 디바이스 타입
|
|
31
|
+
export enum DeviceType {
|
|
32
|
+
DESKTOP = 'DESKTOP',
|
|
33
|
+
MOBILE = 'MOBILE',
|
|
34
|
+
TABLET = 'TABLET',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 광고 데이터 인터페이스
|
|
38
|
+
export interface Advertisement {
|
|
39
|
+
_id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
adType: AdType;
|
|
43
|
+
orgId: string;
|
|
44
|
+
textContent?: string;
|
|
45
|
+
imageUrl?: string;
|
|
46
|
+
videoUrl?: string;
|
|
47
|
+
linkUrl: string;
|
|
48
|
+
startDate: Date | string;
|
|
49
|
+
endDate: Date | string;
|
|
50
|
+
status: 'ACTIVE' | 'INACTIVE' | 'PENDING';
|
|
51
|
+
order: number;
|
|
52
|
+
language: string;
|
|
53
|
+
countries: string[];
|
|
54
|
+
deviceType: DeviceType;
|
|
55
|
+
createdAt: Date | string;
|
|
56
|
+
updatedAt: Date | string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 광고 슬롯 설정
|
|
60
|
+
export interface AdSlotConfig {
|
|
61
|
+
type: AdType;
|
|
62
|
+
placement?: string;
|
|
63
|
+
width?: number;
|
|
64
|
+
height?: number;
|
|
65
|
+
autoRefresh?: number; // 초 단위
|
|
66
|
+
fallback?: string;
|
|
67
|
+
targeting?: Record<string, any>;
|
|
68
|
+
lazyLoad?: boolean;
|
|
69
|
+
responsive?: boolean;
|
|
70
|
+
className?: string;
|
|
71
|
+
style?: Partial<CSSStyleDeclaration>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 뷰어빌리티 메트릭
|
|
75
|
+
/**
|
|
76
|
+
* 가시성 메트릭스
|
|
77
|
+
*/
|
|
78
|
+
export interface ViewabilityMetrics {
|
|
79
|
+
isViewable: boolean;
|
|
80
|
+
visibilityRatio: number;
|
|
81
|
+
duration: number;
|
|
82
|
+
impressions: number;
|
|
83
|
+
attentionTime: number;
|
|
84
|
+
scrollDepth: number;
|
|
85
|
+
completionRate: number;
|
|
86
|
+
firstView: number;
|
|
87
|
+
timestamps: {
|
|
88
|
+
firstView: number;
|
|
89
|
+
lastView: number;
|
|
90
|
+
totalViewTime: number;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 가시성 설정
|
|
96
|
+
*/
|
|
97
|
+
export interface ViewabilityConfiguration {
|
|
98
|
+
threshold: number;
|
|
99
|
+
minDuration: number;
|
|
100
|
+
scrollDepthTracking: boolean;
|
|
101
|
+
attentionTracking: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 디바이스 정보
|
|
105
|
+
export interface DeviceInfo {
|
|
106
|
+
deviceId: string;
|
|
107
|
+
platform: Platform;
|
|
108
|
+
osVersion?: string;
|
|
109
|
+
deviceModel?: string;
|
|
110
|
+
appVersion?: string;
|
|
111
|
+
sdkVersion: string;
|
|
112
|
+
viewport: {
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
};
|
|
116
|
+
screen: {
|
|
117
|
+
width: number;
|
|
118
|
+
height: number;
|
|
119
|
+
};
|
|
120
|
+
userAgent: string;
|
|
121
|
+
language: string;
|
|
122
|
+
timezone: string;
|
|
123
|
+
connectionType?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 광고 이벤트
|
|
127
|
+
export interface AdEvent {
|
|
128
|
+
id: string;
|
|
129
|
+
type: AdEventType;
|
|
130
|
+
adId: string;
|
|
131
|
+
slotId: string;
|
|
132
|
+
timestamp: number;
|
|
133
|
+
deviceInfo: DeviceInfo;
|
|
134
|
+
contextInfo: {
|
|
135
|
+
pageUrl: string;
|
|
136
|
+
pageTitle: string;
|
|
137
|
+
referrer: string;
|
|
138
|
+
sessionId: string;
|
|
139
|
+
userId?: string;
|
|
140
|
+
};
|
|
141
|
+
adInfo: {
|
|
142
|
+
type: AdType;
|
|
143
|
+
placement: string;
|
|
144
|
+
creative: string;
|
|
145
|
+
campaign?: string;
|
|
146
|
+
targeting?: Record<string, any>;
|
|
147
|
+
};
|
|
148
|
+
viewabilityMetrics?: ViewabilityMetrics;
|
|
149
|
+
performanceMetrics?: {
|
|
150
|
+
pageLoadTime?: number;
|
|
151
|
+
adLoadTime?: number;
|
|
152
|
+
renderTime?: number;
|
|
153
|
+
};
|
|
154
|
+
abTestVariant?: string;
|
|
155
|
+
metadata?: Record<string, any>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 광고 슬롯 인스턴스
|
|
159
|
+
export interface AdSlot {
|
|
160
|
+
id: string;
|
|
161
|
+
containerId: string;
|
|
162
|
+
adType: AdType;
|
|
163
|
+
width: number | string; // 숫자(px) 또는 문자열(%, px 등) 지원
|
|
164
|
+
height: number | string; // 숫자(px) 또는 문자열(%, px 등) 지원
|
|
165
|
+
isLoaded: boolean;
|
|
166
|
+
isVisible: boolean;
|
|
167
|
+
refreshRate: number;
|
|
168
|
+
lazyLoad: boolean;
|
|
169
|
+
targeting: Record<string, any>;
|
|
170
|
+
element?: HTMLElement;
|
|
171
|
+
config?: AdSlotConfig;
|
|
172
|
+
advertisement?: Advertisement;
|
|
173
|
+
isViewable?: boolean;
|
|
174
|
+
impressionSent?: boolean;
|
|
175
|
+
loadTime?: number;
|
|
176
|
+
renderTime?: number;
|
|
177
|
+
events?: AdEvent[];
|
|
178
|
+
|
|
179
|
+
// 메서드
|
|
180
|
+
load(): Promise<Advertisement | null>;
|
|
181
|
+
render(ad: Advertisement): void;
|
|
182
|
+
refresh(): Promise<void>;
|
|
183
|
+
destroy(): void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 광고 성과 지표
|
|
187
|
+
export interface AdAnalytics {
|
|
188
|
+
impressions: number;
|
|
189
|
+
clicks: number;
|
|
190
|
+
hovers: number;
|
|
191
|
+
viewableImpressions: number;
|
|
192
|
+
errors: number;
|
|
193
|
+
ctr: number; // Click Through Rate
|
|
194
|
+
viewabilityRate: number;
|
|
195
|
+
averageViewTime: number;
|
|
196
|
+
totalViewTime: number;
|
|
197
|
+
}
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// API 응답 타입 정의
|
|
2
|
+
|
|
3
|
+
// 기본 API 응답
|
|
4
|
+
export interface ApiResponse<T = any> {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
data?: T;
|
|
8
|
+
error?: {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
details?: any;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 광고 목록 응답
|
|
16
|
+
export interface AdvertisementsResponse {
|
|
17
|
+
advertisements: Advertisement[];
|
|
18
|
+
total: number;
|
|
19
|
+
hasMore?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 광고 요청 파라미터
|
|
23
|
+
export interface AdRequest {
|
|
24
|
+
adType?: AdType;
|
|
25
|
+
language?: string;
|
|
26
|
+
deviceType?: DeviceType;
|
|
27
|
+
country?: string;
|
|
28
|
+
placement?: string;
|
|
29
|
+
limit?: number;
|
|
30
|
+
targeting?: Record<string, any>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 배치 광고 요청
|
|
34
|
+
export interface BatchAdRequest {
|
|
35
|
+
requests: AdRequest[];
|
|
36
|
+
sessionId: string;
|
|
37
|
+
userId?: string;
|
|
38
|
+
contextInfo?: {
|
|
39
|
+
pageUrl: string;
|
|
40
|
+
referrer: string;
|
|
41
|
+
userAgent: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 이벤트 추적 요청
|
|
46
|
+
export interface TrackEventRequest {
|
|
47
|
+
adType: AdType;
|
|
48
|
+
platform: Platform;
|
|
49
|
+
deviceId: string;
|
|
50
|
+
pageUrl: string;
|
|
51
|
+
slotId: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
viewabilityMetrics?: Partial<ViewabilityMetrics>;
|
|
54
|
+
performanceMetrics?: {
|
|
55
|
+
pageLoadTime?: number;
|
|
56
|
+
adLoadTime?: number;
|
|
57
|
+
renderTime?: number;
|
|
58
|
+
};
|
|
59
|
+
metadata?: Record<string, any>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 이벤트 추적 응답
|
|
63
|
+
export interface TrackEventResponse {
|
|
64
|
+
success: boolean;
|
|
65
|
+
timestamp: Date | string;
|
|
66
|
+
message?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 광고 분석 요청
|
|
70
|
+
export interface AnalyticsRequest {
|
|
71
|
+
advertisementId?: string;
|
|
72
|
+
startDate?: string;
|
|
73
|
+
endDate?: string;
|
|
74
|
+
groupBy?: 'day' | 'hour' | 'country' | 'deviceType';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 광고 분석 응답
|
|
78
|
+
export interface AnalyticsResponse {
|
|
79
|
+
impressions: number;
|
|
80
|
+
clicks: number;
|
|
81
|
+
hovers: number;
|
|
82
|
+
viewableImpressions: number;
|
|
83
|
+
errors: number;
|
|
84
|
+
ctr: number;
|
|
85
|
+
viewabilityRate: number;
|
|
86
|
+
breakdown?: Record<string, AdAnalytics>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 광고 추천 요청
|
|
90
|
+
export interface RecommendationRequest {
|
|
91
|
+
adType?: AdType;
|
|
92
|
+
placement?: string;
|
|
93
|
+
targeting?: Record<string, any>;
|
|
94
|
+
limit?: number;
|
|
95
|
+
excludeIds?: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 광고 추천 응답
|
|
99
|
+
export interface RecommendationResponse {
|
|
100
|
+
ads: Advertisement[];
|
|
101
|
+
context: {
|
|
102
|
+
totalAvailable: number;
|
|
103
|
+
recommendationScore: number;
|
|
104
|
+
targetingApplied: string[];
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// API 클라이언트 설정
|
|
109
|
+
export interface ApiClientConfig {
|
|
110
|
+
baseURL: string;
|
|
111
|
+
apiKey: string;
|
|
112
|
+
orgId: string;
|
|
113
|
+
timeout?: number;
|
|
114
|
+
retryCount?: number;
|
|
115
|
+
enableCache?: boolean;
|
|
116
|
+
cacheTTL?: number;
|
|
117
|
+
enableBatching?: boolean;
|
|
118
|
+
batchSize?: number;
|
|
119
|
+
batchInterval?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 캐시 엔트리
|
|
123
|
+
export interface CacheEntry<T = any> {
|
|
124
|
+
data: T;
|
|
125
|
+
timestamp: number;
|
|
126
|
+
ttl: number;
|
|
127
|
+
hits: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 배치 요청 큐
|
|
131
|
+
export interface BatchRequest {
|
|
132
|
+
id: string;
|
|
133
|
+
request: AdRequest;
|
|
134
|
+
resolve: (value: Advertisement[]) => void;
|
|
135
|
+
reject: (error: Error) => void;
|
|
136
|
+
timestamp: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 재시도 정책
|
|
140
|
+
export interface RetryPolicy {
|
|
141
|
+
maxAttempts: number;
|
|
142
|
+
backoff: 'linear' | 'exponential';
|
|
143
|
+
delay: number;
|
|
144
|
+
maxDelay?: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 네트워크 상태
|
|
148
|
+
export interface NetworkInfo {
|
|
149
|
+
effectiveType: '2g' | '3g' | '4g' | 'slow-2g' | undefined;
|
|
150
|
+
downlink: number;
|
|
151
|
+
rtt: number;
|
|
152
|
+
saveData: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 성능 메트릭
|
|
156
|
+
export interface PerformanceMetrics {
|
|
157
|
+
requestDuration: number;
|
|
158
|
+
cacheHitRate: number;
|
|
159
|
+
errorRate: number;
|
|
160
|
+
averageResponseSize: number;
|
|
161
|
+
totalRequests: number;
|
|
162
|
+
totalErrors: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
import type {
|
|
166
|
+
AdAnalytics,
|
|
167
|
+
AdType,
|
|
168
|
+
Advertisement,
|
|
169
|
+
DeviceType,
|
|
170
|
+
Platform,
|
|
171
|
+
ViewabilityMetrics
|
|
172
|
+
} from './advertisement';
|
|
173
|
+
|