@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,174 @@
1
+ // SDK 설정 타입 정의
2
+
3
+ // 메인 SDK 설정
4
+ /**
5
+ * SDK 설정
6
+ */
7
+ export interface SDKConfig {
8
+ clientId: string;
9
+ baseUrl: string;
10
+ debug?: boolean;
11
+ batchSize?: number;
12
+ retryCount?: number;
13
+ timeout?: number;
14
+ cache?: {
15
+ enabled: boolean;
16
+ maxSize?: number;
17
+ ttl?: number;
18
+ };
19
+ viewability?: {
20
+ threshold?: number;
21
+ minDuration?: number;
22
+ scrollDepthTracking?: boolean;
23
+ attentionTracking?: boolean;
24
+ };
25
+ }
26
+
27
+ // 환경별 설정
28
+ export interface EnvironmentConfig {
29
+ production: Partial<SDKConfig>;
30
+ development: Partial<SDKConfig>;
31
+ test: Partial<SDKConfig>;
32
+ }
33
+
34
+ // 로깅 설정
35
+ export interface LoggingConfig {
36
+ level: 'debug' | 'info' | 'warn' | 'error' | 'silent';
37
+ enableConsole?: boolean;
38
+ enableRemote?: boolean;
39
+ remoteEndpoint?: string;
40
+ maxLogSize?: number;
41
+ enablePerformanceLogs?: boolean;
42
+ }
43
+
44
+ // 뷰어빌리티 엔진 설정
45
+ export interface ViewabilityConfig {
46
+ threshold: number; // 0-1 (기본값: 0.5)
47
+ timeThreshold: number; // 밀리초 (기본값: 1000)
48
+ enableIABStandard?: boolean;
49
+ enableAttentionTracking?: boolean;
50
+ enableScrollTracking?: boolean;
51
+ intersectionThresholds?: number[];
52
+ }
53
+
54
+ // 렌더링 엔진 설정
55
+ export interface RenderingConfig {
56
+ enableLazyLoading?: boolean;
57
+ lazyLoadThreshold?: number;
58
+ enableResponsive?: boolean;
59
+ breakpoints?: {
60
+ mobile: number;
61
+ tablet: number;
62
+ desktop: number;
63
+ };
64
+ enableAnimation?: boolean;
65
+ enableTemplating?: boolean;
66
+ templateEngine?: 'mustache' | 'handlebars' | 'custom';
67
+ }
68
+
69
+ // 타겟팅 설정
70
+ export interface TargetingConfig {
71
+ enableGeoTargeting?: boolean;
72
+ enableDeviceTargeting?: boolean;
73
+ enableBehavioralTargeting?: boolean;
74
+ enableContextualTargeting?: boolean;
75
+ enableTimeTargeting?: boolean;
76
+ enableFrequencyCapping?: boolean;
77
+ maxFrequency?: number;
78
+ frequencyWindow?: number; // 시간 단위 (시간)
79
+ }
80
+
81
+ // A/B 테스트 설정
82
+ export interface ABTestConfig {
83
+ enableABTesting?: boolean;
84
+ defaultVariant?: string;
85
+ variantSelection?: 'random' | 'hash' | 'user-agent';
86
+ persistVariant?: boolean;
87
+ variantStorageKey?: string;
88
+ }
89
+
90
+ // 스타일 설정
91
+ export interface StyleConfig {
92
+ enableCustomCSS?: boolean;
93
+ theme?: 'light' | 'dark' | 'auto';
94
+ borderRadius?: number;
95
+ shadows?: boolean;
96
+ animations?: {
97
+ fadeIn?: boolean;
98
+ slideIn?: boolean;
99
+ duration?: number;
100
+ easing?: string;
101
+ };
102
+ responsive?: {
103
+ mobile?: Partial<CSSStyleDeclaration>;
104
+ tablet?: Partial<CSSStyleDeclaration>;
105
+ desktop?: Partial<CSSStyleDeclaration>;
106
+ };
107
+ }
108
+
109
+ // 통합 설정 (모든 설정 포함)
110
+ export interface AdstageConfig extends SDKConfig {
111
+ logging?: LoggingConfig;
112
+ viewability?: ViewabilityConfig;
113
+ rendering?: RenderingConfig;
114
+ targeting?: TargetingConfig;
115
+ abTest?: ABTestConfig;
116
+ styling?: StyleConfig;
117
+ environment?: keyof EnvironmentConfig;
118
+ }
119
+
120
+ // 설정 검증 결과
121
+ export interface ConfigValidationResult {
122
+ isValid: boolean;
123
+ errors: string[];
124
+ warnings: string[];
125
+ sanitizedConfig: AdstageConfig;
126
+ }
127
+
128
+ // 기본 설정 값들
129
+ export const DEFAULT_CONFIG: Required<SDKConfig> = {
130
+ clientId: '',
131
+ baseUrl: 'https://api.adstage.io',
132
+ debug: false,
133
+ batchSize: 10,
134
+ retryCount: 3,
135
+ timeout: 10000,
136
+
137
+ cache: {
138
+ enabled: true,
139
+ maxSize: 100,
140
+ ttl: 300000,
141
+ },
142
+
143
+ viewability: {
144
+ threshold: 0.5,
145
+ minDuration: 1000,
146
+ scrollDepthTracking: true,
147
+ attentionTracking: true,
148
+ },
149
+ };
150
+
151
+ // 환경별 기본 설정
152
+ export const ENVIRONMENT_DEFAULTS: EnvironmentConfig = {
153
+ production: {
154
+ debug: false,
155
+ cache: {
156
+ enabled: true,
157
+ },
158
+ timeout: 10000,
159
+ },
160
+ development: {
161
+ debug: true,
162
+ cache: {
163
+ enabled: false,
164
+ },
165
+ timeout: 30000,
166
+ },
167
+ test: {
168
+ debug: false,
169
+ cache: {
170
+ enabled: false,
171
+ },
172
+ timeout: 5000,
173
+ },
174
+ };
@@ -0,0 +1,60 @@
1
+ // 이벤트 및 추적 관련 타입
2
+ export interface SessionInfo {
3
+ sessionId: string;
4
+ userId?: string;
5
+ startTime: number;
6
+ lastActivity: number;
7
+ pageViews: number;
8
+ totalAdViews: number;
9
+ totalClicks: number;
10
+ }
11
+
12
+ // 오프라인 저장소
13
+ export interface OfflineEventStore {
14
+ events: AdEvent[];
15
+ maxSize: number;
16
+ compressionEnabled: boolean;
17
+ }
18
+
19
+ // 성능 모니터링
20
+ export interface PerformanceMonitor {
21
+ metrics: {
22
+ sdkLoadTime: number;
23
+ averageAdLoadTime: number;
24
+ memoryUsage: number;
25
+ errorRate: number;
26
+ cacheHitRate: number;
27
+ };
28
+ alerts: PerformanceAlert[];
29
+ }
30
+
31
+ export interface PerformanceAlert {
32
+ type: 'warning' | 'error' | 'critical';
33
+ message: string;
34
+ timestamp: number;
35
+ metric: string;
36
+ value: number;
37
+ threshold: number;
38
+ }
39
+
40
+ // 에러 타입
41
+ export enum ErrorType {
42
+ NETWORK_ERROR = 'NETWORK_ERROR',
43
+ API_ERROR = 'API_ERROR',
44
+ RENDER_ERROR = 'RENDER_ERROR',
45
+ CONFIG_ERROR = 'CONFIG_ERROR',
46
+ TRACKING_ERROR = 'TRACKING_ERROR',
47
+ VIEWABILITY_ERROR = 'VIEWABILITY_ERROR',
48
+ PERMISSION_ERROR = 'PERMISSION_ERROR',
49
+ TIMEOUT_ERROR = 'TIMEOUT_ERROR',
50
+ }
51
+
52
+ export interface SDKError extends Error {
53
+ type: ErrorType;
54
+ code: string;
55
+ context?: any;
56
+ timestamp: number;
57
+ retryable: boolean;
58
+ }
59
+
60
+ import type { AdEvent } from './advertisement';
@@ -0,0 +1,6 @@
1
+ // 타입 정의 통합 인덱스
2
+ export * from './advertisement';
3
+ export * from './api';
4
+ export * from './config';
5
+ export * from './events';
6
+
@@ -0,0 +1,237 @@
1
+ /**
2
+ * SSR 안전한 DOM API 래퍼 클래스
3
+ * 서버사이드 렌더링 환경에서 DOM API 접근 시 오류를 방지합니다.
4
+ */
5
+ export class DOMUtils {
6
+ /**
7
+ * 브라우저 환경 여부 체크
8
+ */
9
+ static isBrowser(): boolean {
10
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
11
+ }
12
+
13
+ /**
14
+ * SSR 환경 여부 체크
15
+ */
16
+ static isSSR(): boolean {
17
+ return !this.isBrowser();
18
+ }
19
+
20
+ /**
21
+ * DOM 사용 가능 여부 체크
22
+ */
23
+ static canUseDOM(): boolean {
24
+ return this.isBrowser() && document.readyState !== undefined;
25
+ }
26
+
27
+ /**
28
+ * 안전한 getElementById
29
+ */
30
+ static safeGetElementById(id: string): HTMLElement | null {
31
+ if (!this.canUseDOM()) return null;
32
+ return document.getElementById(id);
33
+ }
34
+
35
+ /**
36
+ * 안전한 querySelector
37
+ */
38
+ static safeQuerySelector(selector: string): HTMLElement | null {
39
+ if (!this.canUseDOM()) return null;
40
+ return document.querySelector(selector);
41
+ }
42
+
43
+ /**
44
+ * 안전한 querySelectorAll
45
+ */
46
+ static safeQuerySelectorAll(selector: string): HTMLElement[] {
47
+ if (!this.canUseDOM()) return [];
48
+ return Array.from(document.querySelectorAll(selector)) as HTMLElement[];
49
+ }
50
+
51
+ /**
52
+ * 안전한 createElement
53
+ */
54
+ static safeCreateElement(tagName: string): HTMLElement | null {
55
+ if (!this.canUseDOM()) return null;
56
+ return document.createElement(tagName);
57
+ }
58
+
59
+ /**
60
+ * 안전한 addEventListener
61
+ */
62
+ static safeAddEventListener(
63
+ element: HTMLElement | null,
64
+ event: string,
65
+ handler: EventListener,
66
+ options?: boolean | AddEventListenerOptions
67
+ ): void {
68
+ if (!this.canUseDOM() || !element) return;
69
+ element.addEventListener(event, handler, options);
70
+ }
71
+
72
+ /**
73
+ * 안전한 removeEventListener
74
+ */
75
+ static safeRemoveEventListener(
76
+ element: HTMLElement | null,
77
+ event: string,
78
+ handler: EventListener,
79
+ options?: boolean | EventListenerOptions
80
+ ): void {
81
+ if (!this.canUseDOM() || !element) return;
82
+ element.removeEventListener(event, handler, options);
83
+ }
84
+
85
+ /**
86
+ * 안전한 window 속성 접근
87
+ */
88
+ static getWindowProperty<T>(property: keyof Window, defaultValue: T): T {
89
+ if (!this.isBrowser()) return defaultValue;
90
+ return (window[property] as T) ?? defaultValue;
91
+ }
92
+
93
+ /**
94
+ * 안전한 document 속성 접근
95
+ */
96
+ static getDocumentProperty<T>(property: keyof Document, defaultValue: T): T {
97
+ if (!this.canUseDOM()) return defaultValue;
98
+ return (document[property] as T) ?? defaultValue;
99
+ }
100
+
101
+ /**
102
+ * 안전한 window.open
103
+ */
104
+ static safeWindowOpen(url: string, target?: string, features?: string): Window | null {
105
+ if (!this.isBrowser()) return null;
106
+ return window.open(url, target, features);
107
+ }
108
+
109
+ /**
110
+ * 안전한 getComputedStyle
111
+ */
112
+ static safeGetComputedStyle(element: HTMLElement): CSSStyleDeclaration | null {
113
+ if (!this.isBrowser() || !element) return null;
114
+ return window.getComputedStyle(element);
115
+ }
116
+
117
+ /**
118
+ * DOM Ready 상태 체크
119
+ */
120
+ static isDOMReady(): boolean {
121
+ if (!this.canUseDOM()) return false;
122
+ return document.readyState !== 'loading';
123
+ }
124
+
125
+ /**
126
+ * DOM Ready 대기 (SSR 안전)
127
+ */
128
+ static waitForDOM(): Promise<void> {
129
+ return new Promise((resolve) => {
130
+ if (!this.canUseDOM()) {
131
+ resolve(); // SSR 환경에서는 즉시 resolve
132
+ return;
133
+ }
134
+
135
+ if (this.isDOMReady()) {
136
+ resolve();
137
+ } else {
138
+ this.safeAddEventListener(document as any, 'DOMContentLoaded', () => resolve());
139
+ }
140
+ });
141
+ }
142
+
143
+ /**
144
+ * 안전한 스타일 적용
145
+ */
146
+ static safeApplyStyles(element: HTMLElement | null, styles: Record<string, string>): void {
147
+ if (!this.canUseDOM() || !element) return;
148
+
149
+ Object.entries(styles).forEach(([property, value]) => {
150
+ element.style.setProperty(property, value);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * 안전한 클래스 추가
156
+ */
157
+ static safeAddClass(element: HTMLElement | null, className: string): void {
158
+ if (!this.canUseDOM() || !element) return;
159
+ element.classList.add(className);
160
+ }
161
+
162
+ /**
163
+ * 안전한 클래스 제거
164
+ */
165
+ static safeRemoveClass(element: HTMLElement | null, className: string): void {
166
+ if (!this.canUseDOM() || !element) return;
167
+ element.classList.remove(className);
168
+ }
169
+
170
+ /**
171
+ * 안전한 텍스트 콘텐츠 설정
172
+ */
173
+ static safeSetTextContent(element: HTMLElement | null, text: string): void {
174
+ if (!this.canUseDOM() || !element) return;
175
+ element.textContent = text;
176
+ }
177
+
178
+ /**
179
+ * 안전한 HTML 콘텐츠 설정
180
+ */
181
+ static safeSetInnerHTML(element: HTMLElement | null, html: string): void {
182
+ if (!this.canUseDOM() || !element) return;
183
+ element.innerHTML = html;
184
+ }
185
+
186
+ /**
187
+ * 안전한 자식 요소 추가
188
+ */
189
+ static safeAppendChild(parent: HTMLElement | null, child: HTMLElement | null): void {
190
+ if (!this.canUseDOM() || !parent || !child) return;
191
+ parent.appendChild(child);
192
+ }
193
+
194
+ /**
195
+ * 안전한 자식 요소 제거
196
+ */
197
+ static safeRemoveChild(parent: HTMLElement | null, child: HTMLElement | null): void {
198
+ if (!this.canUseDOM() || !parent || !child) return;
199
+ parent.removeChild(child);
200
+ }
201
+
202
+ /**
203
+ * 현재 페이지 정보 가져오기 (SSR 안전)
204
+ */
205
+ static getPageInfo() {
206
+ return {
207
+ url: this.getWindowProperty('location', { href: '' }).href,
208
+ title: this.getDocumentProperty('title', ''),
209
+ referrer: this.getDocumentProperty('referrer', ''),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * 뷰포트 정보 가져오기 (SSR 안전)
215
+ */
216
+ static getViewportInfo() {
217
+ return {
218
+ width: this.getWindowProperty('innerWidth', 0),
219
+ height: this.getWindowProperty('innerHeight', 0),
220
+ pixelRatio: this.getWindowProperty('devicePixelRatio', 1),
221
+ };
222
+ }
223
+
224
+ /**
225
+ * 스크롤 정보 가져오기 (SSR 안전)
226
+ */
227
+ static getScrollInfo() {
228
+ const scrollTop = this.canUseDOM()
229
+ ? (window.pageYOffset || document.documentElement.scrollTop)
230
+ : 0;
231
+
232
+ return {
233
+ scrollTop,
234
+ scrollLeft: this.getWindowProperty('pageXOffset', 0),
235
+ };
236
+ }
237
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SDK 유틸리티 함수 모음
3
+ * - DOM 요소 속성 처리
4
+ * - 자동 슬롯 검색
5
+ * - 기타 헬퍼 함수들
6
+ */
7
+ export class SDKUtils {
8
+ /**
9
+ * HTML 요소에서 속성값 가져오기 (data- 프리픽스 선택적)
10
+ */
11
+ static getElementAttribute(element: HTMLElement, attrName: string): string | undefined {
12
+ // 1. data- 프리픽스가 있는 경우 우선
13
+ const dataAttr = element.dataset[attrName];
14
+ if (dataAttr) return dataAttr;
15
+
16
+ // 2. 직접 속성 확인
17
+ const directAttr = element.getAttribute(attrName);
18
+ if (directAttr) return directAttr;
19
+
20
+ // 3. 케밥 케이스로 확인 (ad-type -> adType)
21
+ const kebabAttr = element.getAttribute(attrName.replace(/[A-Z]/g, '-$&').toLowerCase());
22
+ if (kebabAttr) return kebabAttr;
23
+
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * DOM에서 자동 슬롯 요소 찾기 (SSR 안전)
29
+ */
30
+ static findAutoSlotElements(): HTMLElement[] {
31
+ if (typeof document === 'undefined') return [];
32
+ const elements = document.querySelectorAll('[data-adstage], [adstage]');
33
+ return Array.from(elements) as HTMLElement[];
34
+ }
35
+
36
+ /**
37
+ * 요소에서 슬롯 정보 추출
38
+ */
39
+ static extractSlotInfo(element: HTMLElement): {
40
+ slotId: string | null;
41
+ adType: string;
42
+ width: string | undefined;
43
+ height: string | undefined;
44
+ language: string | undefined;
45
+ deviceType: string | undefined;
46
+ country: string | undefined;
47
+ sliderEffect: string | undefined;
48
+ } {
49
+ // 슬롯 ID 가져오기
50
+ const slotId = SDKUtils.getElementAttribute(element, 'adstage');
51
+
52
+ // 광고 타입 가져오기
53
+ const adType = SDKUtils.getElementAttribute(element, 'adType') ||
54
+ SDKUtils.getElementAttribute(element, 'ad-type') ||
55
+ 'BANNER';
56
+
57
+ // 크기 정보 가져오기 (다양한 형태 지원)
58
+ const width = SDKUtils.getElementAttribute(element, 'width');
59
+ const height = SDKUtils.getElementAttribute(element, 'height');
60
+
61
+ // 기타 옵션들
62
+ const language = SDKUtils.getElementAttribute(element, 'language');
63
+ const deviceType = SDKUtils.getElementAttribute(element, 'deviceType') ||
64
+ SDKUtils.getElementAttribute(element, 'device-type');
65
+ const country = SDKUtils.getElementAttribute(element, 'country');
66
+ const sliderEffect = SDKUtils.getElementAttribute(element, 'sliderEffect') ||
67
+ SDKUtils.getElementAttribute(element, 'slider-effect');
68
+
69
+ return {
70
+ slotId: slotId || null,
71
+ adType,
72
+ width,
73
+ height,
74
+ language,
75
+ deviceType,
76
+ country,
77
+ sliderEffect,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * AdType enum 값으로 변환
83
+ */
84
+ static parseAdType(adTypeStr: string, AdType: any): any {
85
+ return AdType[adTypeStr as keyof typeof AdType] || AdType.BANNER;
86
+ }
87
+
88
+ /**
89
+ * 브라우저 환경 체크
90
+ */
91
+ static isBrowser(): boolean {
92
+ return typeof window !== 'undefined';
93
+ }
94
+
95
+ /**
96
+ * SSR 환경 체크
97
+ */
98
+ static isSSR(): boolean {
99
+ return typeof window === 'undefined';
100
+ }
101
+
102
+ /**
103
+ * DOM 사용 가능 체크
104
+ */
105
+ static canUseDOM(): boolean {
106
+ return !this.isSSR() && typeof document !== 'undefined';
107
+ }
108
+
109
+ /**
110
+ * DOM 로드 완료 체크 (SSR 안전)
111
+ */
112
+ static isDOMReady(): boolean {
113
+ if (!this.canUseDOM()) return false;
114
+ return document.readyState !== 'loading';
115
+ }
116
+
117
+ /**
118
+ * DOM 로드 완료 대기 (SSR 안전)
119
+ */
120
+ static waitForDOM(): Promise<void> {
121
+ return new Promise((resolve) => {
122
+ if (!this.canUseDOM()) {
123
+ resolve(); // SSR 환경에서는 즉시 resolve
124
+ return;
125
+ }
126
+
127
+ if (SDKUtils.isDOMReady()) {
128
+ resolve();
129
+ } else {
130
+ document.addEventListener('DOMContentLoaded', () => resolve());
131
+ }
132
+ });
133
+ }
134
+ }