@adstage/web-sdk 1.3.4 → 2.0.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 (60) hide show
  1. package/README.md +539 -34
  2. package/dist/index.cjs.js +753 -509
  3. package/dist/index.d.ts +286 -97
  4. package/dist/index.esm.js +737 -485
  5. package/dist/index.standalone.js +737 -485
  6. package/package.json +12 -13
  7. package/src/constants/endpoints.ts +93 -0
  8. package/src/core/AdStage.ts +128 -0
  9. package/src/index.ts +14 -432
  10. package/src/managers/{slider-manager.ts → carousel-slider-manager.ts} +9 -8
  11. package/src/managers/event-tracker.ts +2 -4
  12. package/src/managers/{fade-slider-manager.ts → text-transition-manager.ts} +7 -7
  13. package/src/modules/ads/AdsModule.ts +525 -0
  14. package/src/modules/config/ConfigModule.ts +124 -0
  15. package/src/modules/events/EventsModule.ts +106 -0
  16. package/src/types/config.ts +74 -3
  17. package/src/types/index.ts +2 -1
  18. package/src/utils/api-headers.ts +52 -0
  19. package/src/utils/dom-utils.ts +1 -1
  20. package/examples/README.md +0 -33
  21. package/examples/banner-ads.html +0 -512
  22. package/examples/index.html +0 -338
  23. package/examples/native-ads.html +0 -634
  24. package/examples/react-app/README.md +0 -70
  25. package/examples/react-app/index.html +0 -13
  26. package/examples/react-app/package-lock.json +0 -3042
  27. package/examples/react-app/package.json +0 -26
  28. package/examples/react-app/pnpm-lock.yaml +0 -1857
  29. package/examples/react-app/public/index.standalone.js +0 -2331
  30. package/examples/react-app/src/App.tsx +0 -226
  31. package/examples/react-app/src/index.css +0 -37
  32. package/examples/react-app/src/main.tsx +0 -10
  33. package/examples/react-app/tsconfig.json +0 -25
  34. package/examples/react-app/tsconfig.node.json +0 -10
  35. package/examples/react-app/vite.config.ts +0 -15
  36. package/examples/react-nextjs/app/globals.css +0 -200
  37. package/examples/react-nextjs/app/layout.tsx +0 -27
  38. package/examples/react-nextjs/app/page.tsx +0 -258
  39. package/examples/react-nextjs/next.config.js +0 -9
  40. package/examples/react-nextjs/package.json +0 -22
  41. package/examples/react-nextjs/pnpm-lock.yaml +0 -343
  42. package/examples/react-nextjs/tsconfig.json +0 -34
  43. package/examples/text-ads.html +0 -597
  44. package/examples/video-ads.html +0 -739
  45. package/src/react/components/AdErrorBoundary.tsx +0 -75
  46. package/src/react/components/AdSlot.tsx +0 -144
  47. package/src/react/components/BannerAd.tsx +0 -24
  48. package/src/react/components/InterstitialAd.tsx +0 -24
  49. package/src/react/components/NativeAd.tsx +0 -24
  50. package/src/react/components/TextAd.tsx +0 -24
  51. package/src/react/components/VideoAd.tsx +0 -24
  52. package/src/react/components/index.ts +0 -8
  53. package/src/react/hooks/index.ts +0 -4
  54. package/src/react/hooks/useAdSlot.ts +0 -83
  55. package/src/react/hooks/useAdStage.ts +0 -14
  56. package/src/react/hooks/useAdTracking.ts +0 -61
  57. package/src/react/index.ts +0 -4
  58. package/src/react/providers/AdStageProvider.tsx +0 -86
  59. package/src/react/providers/index.ts +0 -2
  60. package/src/utils/sdk-standalone.ts +0 -155
@@ -0,0 +1,525 @@
1
+ /**
2
+ * AdStage SDK - Ads 모듈
3
+ * 광고 관리 및 렌더링 기능
4
+ */
5
+
6
+ import { AdStageConfig, BaseModule } from '../../types/config';
7
+ import { AdType, AdEventType } from '../../types/advertisement';
8
+ import type { AdSlot, Advertisement } from '../../types/advertisement';
9
+ import { CarouselSliderManager } from '../../managers/carousel-slider-manager';
10
+ import { TextTransitionManager } from '../../managers/text-transition-manager';
11
+ import { ImpressionTracker } from '../../managers/impression-tracker';
12
+ import { EventTracker } from '../../managers/event-tracker';
13
+ import { endpoints } from '../../constants/endpoints';
14
+ import { ApiHeaders } from '../../utils/api-headers';
15
+
16
+ export interface AdOptions {
17
+ width?: string | number;
18
+ height?: number;
19
+ autoSlide?: boolean;
20
+ slideInterval?: number;
21
+ maxLines?: number;
22
+ style?: string;
23
+ autoplay?: boolean;
24
+ muted?: boolean;
25
+ onClick?: (adData: any) => void;
26
+ }
27
+
28
+ export class AdsModule implements BaseModule {
29
+ private _isReady = false;
30
+ private _config: AdStageConfig | null = null;
31
+ private slots = new Map<string, AdSlot>();
32
+ private eventTracker: EventTracker | null = null;
33
+
34
+ /**
35
+ * Ads 모듈 초기화 (동기)
36
+ */
37
+ init(config: AdStageConfig): void {
38
+ this._config = config;
39
+
40
+ // EventTracker 초기화 (환경 자동 감지된 엔드포인트 사용)
41
+ this.eventTracker = new EventTracker(
42
+ endpoints.getBaseUrl(),
43
+ config.apiKey,
44
+ config.debug || false,
45
+ this.slots
46
+ );
47
+
48
+ this._isReady = true;
49
+
50
+ if (config.debug) {
51
+ console.log('🎯 Ads module initialized (sync mode)');
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 모듈 준비 상태 확인
57
+ */
58
+ isReady(): boolean {
59
+ return this._isReady;
60
+ }
61
+
62
+ /**
63
+ * 모듈 설정 반환
64
+ */
65
+ getConfig(): AdStageConfig | null {
66
+ return this._config;
67
+ }
68
+
69
+ /**
70
+ * 배너 광고 생성 (동기)
71
+ */
72
+ banner(containerId: string, options?: AdOptions): string {
73
+ this.ensureReady();
74
+
75
+ const adstageOptions = {
76
+ width: options?.width || '100%',
77
+ height: options?.height || 250,
78
+ autoSlide: options?.autoSlide || false,
79
+ slideInterval: options?.slideInterval || 5000,
80
+ onClick: options?.onClick
81
+ };
82
+
83
+ return this.createAd(containerId, AdType.BANNER, adstageOptions);
84
+ }
85
+
86
+ /**
87
+ * 텍스트 광고 생성 (동기)
88
+ */
89
+ text(containerId: string, options?: AdOptions): string {
90
+ this.ensureReady();
91
+
92
+ const adstageOptions = {
93
+ maxLines: options?.maxLines || 3,
94
+ style: options?.style || 'default',
95
+ onClick: options?.onClick
96
+ };
97
+
98
+ return this.createAd(containerId, AdType.TEXT, adstageOptions);
99
+ }
100
+
101
+ /**
102
+ * 비디오 광고 생성 (동기)
103
+ */
104
+ video(containerId: string, options?: AdOptions): string {
105
+ this.ensureReady();
106
+
107
+ const adstageOptions = {
108
+ width: options?.width || 640,
109
+ height: options?.height || 360,
110
+ autoplay: options?.autoplay || false,
111
+ muted: options?.muted || true,
112
+ onClick: options?.onClick
113
+ };
114
+
115
+ return this.createAd(containerId, AdType.VIDEO, adstageOptions);
116
+ }
117
+
118
+ /**
119
+ * 네이티브 광고 생성 (동기)
120
+ */
121
+ native(containerId: string, options?: AdOptions): string {
122
+ this.ensureReady();
123
+
124
+ return this.createAd(containerId, AdType.NATIVE, options || {});
125
+ }
126
+
127
+ /**
128
+ * 전면 광고 생성 (동기)
129
+ */
130
+ interstitial(containerId: string, options?: AdOptions): string {
131
+ this.ensureReady();
132
+
133
+ return this.createAd(containerId, AdType.INTERSTITIAL, options || {});
134
+ }
135
+
136
+ /**
137
+ * 광고 새로고침
138
+ */
139
+ refresh(slotId: string): void {
140
+ this.ensureReady();
141
+
142
+ const slot = this.slots.get(slotId);
143
+ if (!slot) {
144
+ throw new Error(`Ad slot not found: ${slotId}`);
145
+ }
146
+
147
+ // 광고 새로고침 로직
148
+ this.refreshAdSlot(slot);
149
+
150
+ if (this._config?.debug) {
151
+ console.log(`🔄 Ad slot refreshed: ${slotId}`);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 광고 제거
157
+ */
158
+ destroy(slotId: string): void {
159
+ this.ensureReady();
160
+
161
+ const slot = this.slots.get(slotId);
162
+ if (!slot) {
163
+ throw new Error(`Ad slot not found: ${slotId}`);
164
+ }
165
+
166
+ // DOM에서 제거
167
+ const container = document.getElementById(slot.containerId);
168
+ if (container) {
169
+ container.innerHTML = '';
170
+ }
171
+
172
+ // 슬롯 제거
173
+ this.slots.delete(slotId);
174
+
175
+ if (this._config?.debug) {
176
+ console.log(`🗑️ Ad slot destroyed: ${slotId}`);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 모든 광고 슬롯 반환
182
+ */
183
+ getAllSlots(): AdSlot[] {
184
+ this.ensureReady();
185
+ return Array.from(this.slots.values());
186
+ }
187
+
188
+ /**
189
+ * 특정 광고 슬롯 반환
190
+ */
191
+ getSlotById(slotId: string): AdSlot | null {
192
+ this.ensureReady();
193
+ return this.slots.get(slotId) || null;
194
+ }
195
+
196
+ /**
197
+ * 광고 생성 내부 메소드 (동기 + Lazy 로딩)
198
+ */
199
+ private createAd(containerId: string, type: AdType, options: any): string {
200
+ if (!this._config?.apiKey) {
201
+ throw new Error('API key not configured');
202
+ }
203
+
204
+ const container = document.getElementById(containerId);
205
+ if (!container) {
206
+ throw new Error(`Container not found: ${containerId}`);
207
+ }
208
+
209
+ // 고유한 슬롯 ID 생성
210
+ const slotId = `adstage-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
211
+
212
+ // 즉시 placeholder 생성
213
+ this.createAdSlot(container, slotId, type, options);
214
+
215
+ // 광고 슬롯 정보 저장
216
+ const slot: AdSlot = {
217
+ id: slotId,
218
+ containerId,
219
+ adType: type,
220
+ width: options.width || '100%',
221
+ height: options.height || 250,
222
+ isLoaded: false,
223
+ isVisible: false,
224
+ refreshRate: 0,
225
+ lazyLoad: false,
226
+ targeting: {},
227
+ advertisement: undefined, // 나중에 로드
228
+ config: { type, ...options },
229
+ load: async () => this.fetchAdData(type, options).then(ads => ads[0] || null),
230
+ render: (ad: Advertisement) => this.renderAdElement(slot, ad),
231
+ refresh: async () => this.refreshAdSlot(slot),
232
+ destroy: () => this.destroy(slotId)
233
+ };
234
+
235
+ // 슬롯 저장
236
+ this.slots.set(slotId, slot);
237
+
238
+ // 백그라운드에서 광고 로드
239
+ this.loadAdContentInBackground(slot);
240
+
241
+ // 이벤트 추적 준비
242
+ if (this.eventTracker && this._config?.debug) {
243
+ console.log(`📊 Event tracking enabled for slot: ${slotId}`);
244
+ }
245
+
246
+ return slotId;
247
+ }
248
+
249
+ /**
250
+ * 즉시 광고 슬롯 생성 (placeholder)
251
+ */
252
+ private createAdSlot(container: HTMLElement, slotId: string, type: AdType, options: any): void {
253
+ const adElement = document.createElement('div');
254
+ adElement.id = slotId;
255
+ adElement.className = `adstage-slot adstage-${type.toLowerCase()}`;
256
+ adElement.style.width = typeof options.width === 'number' ? `${options.width}px` : (options.width || '100%');
257
+ adElement.style.height = typeof options.height === 'number' ? `${options.height}px` : (options.height || '250px');
258
+ adElement.style.border = '1px dashed #ccc';
259
+ adElement.style.display = 'flex';
260
+ adElement.style.alignItems = 'center';
261
+ adElement.style.justifyContent = 'center';
262
+ adElement.style.backgroundColor = '#f9f9f9';
263
+ adElement.style.color = '#666';
264
+ adElement.innerHTML = `<span>Loading ${type} ad...</span>`;
265
+
266
+ container.appendChild(adElement);
267
+
268
+ if (this._config?.debug) {
269
+ console.log(`📦 Placeholder created for slot: ${slotId}`);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * 백그라운드에서 광고 콘텐츠 로드
275
+ */
276
+ private async loadAdContentInBackground(slot: AdSlot): Promise<void> {
277
+ try {
278
+ // 광고 데이터 가져오기 - 여러 개 로드
279
+ const adstageData = await this.fetchAdData(slot.adType, slot.config);
280
+
281
+ if (!adstageData || adstageData.length === 0) {
282
+ this.renderFallback(slot);
283
+ return;
284
+ }
285
+
286
+ // 광고가 여러 개이거나 autoSlide 옵션이 있으면 슬라이더로 렌더링
287
+ if (adstageData.length > 1 || (slot.config as any)?.autoSlide) {
288
+ await this.renderAdSlider(slot, adstageData);
289
+ } else {
290
+ // 광고가 1개면 일반 렌더링
291
+ slot.advertisement = adstageData[0];
292
+ await this.renderAdElement(slot, adstageData[0]);
293
+ }
294
+
295
+ slot.isLoaded = true;
296
+
297
+ if (this._config?.debug) {
298
+ console.log(`✅ Ad loaded for slot: ${slot.id} (${adstageData.length} ads)`);
299
+ }
300
+ } catch (error) {
301
+ console.error(`❌ Failed to load ad for slot: ${slot.id}`, error);
302
+ this.renderFallback(slot);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Fallback 광고 렌더링
308
+ */
309
+ private renderFallback(slot: AdSlot): void {
310
+ const element = document.getElementById(slot.id);
311
+ if (element) {
312
+ element.innerHTML = `<span>Ad not available</span>`;
313
+ element.style.color = '#999';
314
+
315
+ if (this._config?.debug) {
316
+ console.warn(`⚠️ Fallback rendered for slot: ${slot.id}`);
317
+ }
318
+ }
319
+ }
320
+
321
+ /**
322
+ * 광고 데이터 가져오기
323
+ */
324
+ private async fetchAdData(type: AdType, options: any): Promise<Advertisement[]> {
325
+ if (!this._config?.apiKey) {
326
+ throw new Error('API key not configured');
327
+ }
328
+
329
+ // GET 요청용 query parameters 구성
330
+ const params = new URLSearchParams();
331
+ params.append('adType', type);
332
+
333
+ // userAgent와 url은 header나 자동으로 처리되므로 query에서 제외
334
+ // 기타 옵션들을 필요시 query parameter로 추가 가능
335
+
336
+ const url = `${endpoints.advertisements.list()}?${params.toString()}`;
337
+
338
+ const response = await fetch(url, {
339
+ method: 'GET',
340
+ headers: ApiHeaders.create(this._config.apiKey)
341
+ });
342
+
343
+ if (!response.ok) {
344
+ throw new Error(`Failed to fetch ad data: ${response.status}`);
345
+ }
346
+
347
+ const result = await response.json();
348
+ return result.advertisements || [];
349
+ }
350
+
351
+ /**
352
+ * 광고 슬라이더 렌더링 (여러 광고 또는 autoSlide 옵션)
353
+ */
354
+ private async renderAdSlider(slot: AdSlot, advertisements: Advertisement[]): Promise<void> {
355
+ const container = document.getElementById(slot.containerId);
356
+ if (!container) {
357
+ throw new Error(`Container not found: ${slot.containerId}`);
358
+ }
359
+
360
+ // 이벤트 추적 콜백 함수 (중복 노출 방지 포함)
361
+ const trackEventCallback = (adId: string, slotId: string, eventType: AdEventType) => {
362
+ // 노출 이벤트인 경우 중복 확인
363
+ if (eventType === AdEventType.IMPRESSION) {
364
+ if (ImpressionTracker.isDuplicateImpression(adId, slotId, this._config?.debug)) {
365
+ if (this._config?.debug) {
366
+ console.log(`🚫 Duplicate impression blocked for ad ${adId} in slot ${slotId}`);
367
+ }
368
+ return; // 중복 노출이면 추적하지 않음
369
+ }
370
+
371
+ if (this._config?.debug) {
372
+ console.log(`✅ New impression recorded for ad ${adId} in slot ${slotId}`);
373
+ }
374
+ }
375
+
376
+ if (this.eventTracker && this._config?.debug) {
377
+ console.log(`📊 Event tracked: ${eventType} for ad ${adId} in slot ${slotId}`);
378
+ }
379
+ };
380
+
381
+ let sliderElement: HTMLElement;
382
+
383
+ // 텍스트 광고는 TextTransitionManager 사용, 그 외는 CarouselSliderManager 사용
384
+ if (slot.adType === AdType.TEXT) {
385
+ sliderElement = TextTransitionManager.createTextTransitionContainer(
386
+ slot,
387
+ advertisements,
388
+ {
389
+ autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
390
+ ...slot.config
391
+ },
392
+ trackEventCallback
393
+ );
394
+
395
+ if (this._config?.debug) {
396
+ console.log(`✨ Text transition created for TEXT slot: ${slot.id} with ${advertisements.length} ads`);
397
+ }
398
+ } else {
399
+ sliderElement = CarouselSliderManager.createSliderContainer(
400
+ slot,
401
+ advertisements,
402
+ {
403
+ autoSlideInterval: ((slot.config as any)?.slideInterval || 5000) / 1000,
404
+ ...slot.config
405
+ },
406
+ trackEventCallback
407
+ );
408
+
409
+ if (this._config?.debug) {
410
+ console.log(`🎠 Carousel slider created for ${slot.adType} slot: ${slot.id} with ${advertisements.length} ads`);
411
+ }
412
+ }
413
+
414
+ // 기존 내용 제거하고 슬라이더 추가
415
+ container.innerHTML = '';
416
+ container.appendChild(sliderElement);
417
+ }
418
+
419
+ /**
420
+ * 광고 렌더링 (단일 광고용)
421
+ */
422
+ private async renderAd(slot: AdSlot): Promise<void> {
423
+ if (!slot.advertisement) {
424
+ throw new Error('No advertisement to render');
425
+ }
426
+
427
+ await this.renderAdElement(slot, slot.advertisement);
428
+ slot.isLoaded = true;
429
+ }
430
+
431
+ /**
432
+ * 광고 요소 렌더링 (기본 구현)
433
+ */
434
+ private async renderAdElement(slot: AdSlot, ad: Advertisement): Promise<void> {
435
+ const container = document.getElementById(slot.containerId);
436
+ if (!container) return;
437
+
438
+ // 기본 HTML 구조 생성
439
+ const adElement = document.createElement('div');
440
+ adElement.className = 'adstage-ad';
441
+ adElement.style.width = typeof slot.width === 'string' ? slot.width : `${slot.width}px`;
442
+ adElement.style.height = typeof slot.height === 'string' ? slot.height : `${slot.height}px`;
443
+
444
+ // 광고 타입별 렌더링
445
+ switch (slot.adType) {
446
+ case AdType.BANNER:
447
+ if (ad.imageUrl) {
448
+ const img = document.createElement('img');
449
+ img.src = ad.imageUrl;
450
+ img.alt = ad.title;
451
+ img.style.width = '100%';
452
+ img.style.height = '100%';
453
+ img.style.objectFit = 'cover';
454
+ adElement.appendChild(img);
455
+ }
456
+ break;
457
+
458
+ case AdType.TEXT:
459
+ const textDiv = document.createElement('div');
460
+ textDiv.innerHTML = `
461
+ <h3>${ad.title}</h3>
462
+ ${ad.description ? `<p>${ad.description}</p>` : ''}
463
+ ${ad.textContent ? `<div>${ad.textContent}</div>` : ''}
464
+ `;
465
+ adElement.appendChild(textDiv);
466
+ break;
467
+
468
+ case AdType.VIDEO:
469
+ if (ad.videoUrl) {
470
+ const video = document.createElement('video');
471
+ video.src = ad.videoUrl;
472
+ video.controls = true;
473
+ video.style.width = '100%';
474
+ video.style.height = '100%';
475
+ adElement.appendChild(video);
476
+ }
477
+ break;
478
+
479
+ default:
480
+ adElement.innerHTML = `<div>${ad.title}</div>`;
481
+ }
482
+
483
+ // 클릭 이벤트 추가
484
+ if (ad.linkUrl) {
485
+ adElement.style.cursor = 'pointer';
486
+ adElement.addEventListener('click', () => {
487
+ window.open(ad.linkUrl, '_blank');
488
+ });
489
+ }
490
+
491
+ container.innerHTML = '';
492
+ container.appendChild(adElement);
493
+ }
494
+
495
+ /**
496
+ * 광고 슬롯 새로고침
497
+ */
498
+ private async refreshAdSlot(slot: AdSlot): Promise<void> {
499
+ try {
500
+ // 새로운 광고 데이터 가져오기 (config에서 타입과 옵션 정보 사용)
501
+ const newAdData = await this.fetchAdData(slot.adType, slot.config || {});
502
+
503
+ if (newAdData && newAdData.length > 0) {
504
+ slot.advertisement = newAdData[0]; // 첫 번째 광고로 업데이트
505
+ await this.renderAd(slot);
506
+
507
+ // 새로운 노출 추적
508
+ if (this.eventTracker) {
509
+ console.log('New impression tracked for slot:', slot.id);
510
+ }
511
+ }
512
+ } catch (error) {
513
+ console.error(`Failed to refresh ad slot: ${slot.id}`, error);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * 모듈 준비 상태 확인
519
+ */
520
+ private ensureReady(): void {
521
+ if (!this._isReady) {
522
+ throw new Error('Ads module not initialized. Call AdStage.init() first.');
523
+ }
524
+ }
525
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AdStage SDK - Config 모듈
3
+ * 설정 관리 및 API 키 검증
4
+ */
5
+
6
+ import { AdStageConfig, BaseModule, ApiResponse, OrganizationInfo } from '../../types/config';
7
+ import { endpoints } from '../../constants/endpoints';
8
+ import { ApiHeaders } from '../../utils/api-headers';
9
+
10
+ export class ConfigModule implements BaseModule {
11
+ private _isReady = false;
12
+ private _config: AdStageConfig | null = null;
13
+ private _organizationInfo: OrganizationInfo | null = null;
14
+
15
+ /**
16
+ * Config 모듈 초기화 (동기)
17
+ */
18
+ init(config: AdStageConfig): void {
19
+ // 설정만 저장 (서버 검증 없음)
20
+ this._config = {
21
+ timeout: 30000,
22
+ debug: false,
23
+ modules: ['ads', 'events', 'config'],
24
+ validateOnInit: false,
25
+ fallbackMode: true,
26
+ offlineMode: false,
27
+ productionMode: false,
28
+ ...config
29
+ };
30
+
31
+ // 사용자가 baseUrl을 제공한 경우 endpoints에 설정
32
+ if (config.baseUrl) {
33
+ endpoints.setBaseUrl(config.baseUrl);
34
+ }
35
+
36
+ this._isReady = true;
37
+
38
+ if (config.debug) {
39
+ console.log('✅ Config module initialized (sync mode)', {
40
+ modules: this._config.modules,
41
+ endpoint: endpoints.getBaseUrl(),
42
+ mode: config.productionMode ? 'production' : 'development'
43
+ });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 모듈 준비 상태 확인
49
+ */
50
+ isReady(): boolean {
51
+ return this._isReady;
52
+ }
53
+
54
+ /**
55
+ * 현재 설정 반환
56
+ */
57
+ getConfig(): AdStageConfig | null {
58
+ return this._config;
59
+ }
60
+
61
+ /**
62
+ * 조직 정보 반환
63
+ */
64
+ getOrganizationInfo(): OrganizationInfo | null {
65
+ return this._organizationInfo;
66
+ }
67
+
68
+ /**
69
+ * API 엔드포인트 반환
70
+ */
71
+ getApiEndpoint(): string {
72
+ return endpoints.getBaseUrl();
73
+ }
74
+
75
+ /**
76
+ * 디버그 모드 여부 확인
77
+ */
78
+ isDebugMode(): boolean {
79
+ return this._config?.debug || false;
80
+ }
81
+
82
+ /**
83
+ * 활성화된 모듈 목록 반환
84
+ */
85
+ getEnabledModules(): string[] {
86
+ return this._config?.modules || [];
87
+ }
88
+
89
+ /**
90
+ * 특정 모듈이 활성화되어 있는지 확인
91
+ */
92
+ isModuleEnabled(moduleName: string): boolean {
93
+ return this.getEnabledModules().includes(moduleName);
94
+ }
95
+
96
+ /**
97
+ * 설정 업데이트 (런타임)
98
+ */
99
+ updateConfig(updates: Partial<AdStageConfig>): void {
100
+ if (!this._config) {
101
+ throw new Error('Config module not initialized');
102
+ }
103
+
104
+ this._config = {
105
+ ...this._config,
106
+ ...updates
107
+ };
108
+
109
+ if (this.isDebugMode()) {
110
+ console.log('🔄 Config updated', updates);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * API 헤더 생성 (공통 유틸리티 사용)
116
+ */
117
+ getApiHeaders(): Record<string, string> {
118
+ if (!this._config?.apiKey) {
119
+ throw new Error('API key not available');
120
+ }
121
+
122
+ return ApiHeaders.create(this._config.apiKey);
123
+ }
124
+ }