@adstage/web-sdk 1.3.3 → 1.3.4

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/dist/index.cjs.js CHANGED
@@ -257,6 +257,72 @@ class DOMUtils {
257
257
  scrollLeft: this.getWindowProperty('pageXOffset', 0),
258
258
  };
259
259
  }
260
+ /**
261
+ * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
262
+ */
263
+ static async waitForElement(id, options = {}) {
264
+ const { timeout = 3000, retryInterval = 100, debug = false } = options;
265
+ if (!this.canUseDOM()) {
266
+ throw new Error('DOM을 사용할 수 없는 환경입니다.');
267
+ }
268
+ // 즉시 찾을 수 있으면 바로 반환
269
+ const immediateElement = document.getElementById(id);
270
+ if (immediateElement) {
271
+ if (debug) {
272
+ console.log(`✅ 컨테이너 즉시 발견: ${id}`);
273
+ }
274
+ return immediateElement;
275
+ }
276
+ if (debug) {
277
+ console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
278
+ }
279
+ return new Promise((resolve, reject) => {
280
+ let attempts = 0;
281
+ const maxAttempts = Math.ceil(timeout / retryInterval);
282
+ const checkElement = () => {
283
+ attempts++;
284
+ const element = document.getElementById(id);
285
+ if (element) {
286
+ if (debug) {
287
+ console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
288
+ }
289
+ resolve(element);
290
+ return;
291
+ }
292
+ if (attempts >= maxAttempts) {
293
+ const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
294
+
295
+ 다음을 확인해보세요:
296
+ 1. HTML에 id="${id}" 요소가 있는지 확인
297
+ 2. React/Vue 등에서 컴포넌트가 렌더링된 후 SDK 호출
298
+ 3. 철자가 정확한지 확인
299
+ 4. 중복된 ID가 없는지 확인
300
+
301
+ 대기 시간: ${timeout}ms (${attempts}번 시도)`;
302
+ if (debug) {
303
+ console.error(errorMessage);
304
+ }
305
+ reject(new Error(errorMessage));
306
+ return;
307
+ }
308
+ if (debug && attempts % 10 === 0) {
309
+ console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
310
+ }
311
+ // Exponential backoff: 처음엔 빠르게, 나중엔 느리게
312
+ const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
313
+ setTimeout(checkElement, nextInterval);
314
+ };
315
+ // 첫 번째 체크는 즉시 실행
316
+ setTimeout(checkElement, retryInterval);
317
+ });
318
+ }
319
+ /**
320
+ * 여러 DOM 요소를 동시에 기다리기
321
+ */
322
+ static async waitForElements(ids, options = {}) {
323
+ const promises = ids.map(id => this.waitForElement(id, options));
324
+ return Promise.all(promises);
325
+ }
260
326
  }
261
327
 
262
328
  /**
@@ -1814,31 +1880,49 @@ class AdStageSDK {
1814
1880
  * 광고 슬롯 생성 및 로드
1815
1881
  */
1816
1882
  async createSlot(id, containerId, adType = exports.AdType.BANNER, options) {
1817
- const container = DOMUtils.safeGetElementById(containerId);
1818
- if (!container) {
1819
- if (DOMUtils.canUseDOM()) {
1820
- console.error(`Container with ID "${containerId}" not found`);
1883
+ try {
1884
+ // 💡 사용자 친화적 개선: 컨테이너가 나타날 때까지 자동으로 기다림
1885
+ if (this.config.debug) {
1886
+ console.log(`🔍 컨테이너 검색 시작: ${containerId}`);
1887
+ }
1888
+ const container = await DOMUtils.waitForElement(containerId, {
1889
+ timeout: 5000, // 최대 5초 대기
1890
+ retryInterval: 50, // 50ms마다 체크 (부드러운 사용자 경험)
1891
+ debug: this.config.debug
1892
+ });
1893
+ if (this.config.debug) {
1894
+ console.log(`✅ 컨테이너 확인됨: ${containerId}`, container);
1895
+ }
1896
+ const slot = {
1897
+ id,
1898
+ containerId,
1899
+ adType,
1900
+ width: options?.width || 0, // 문자열도 지원
1901
+ height: options?.height || 0, // 문자열도 지원
1902
+ isLoaded: false,
1903
+ isVisible: false,
1904
+ refreshRate: 0,
1905
+ lazyLoad: false,
1906
+ targeting: {},
1907
+ load: async () => { await this.loadSlot(slot, options); return null; },
1908
+ render: (ad) => this.renderSlot(slot, ad),
1909
+ refresh: () => this.refreshSlot(slot.id),
1910
+ destroy: () => this.destroySlot(slot.id),
1911
+ };
1912
+ this.slots.set(id, slot);
1913
+ await this.loadSlot(slot, options);
1914
+ }
1915
+ catch (error) {
1916
+ // 친절한 에러 메시지로 사용자 가이드
1917
+ if (error instanceof Error && error.message.includes('컨테이너를 찾을 수 없습니다')) {
1918
+ console.error(error.message);
1919
+ throw error;
1920
+ }
1921
+ else {
1922
+ console.error(`❌ 광고 슬롯 생성 실패 (${id}):`, error);
1923
+ throw new Error(`광고 슬롯 생성에 실패했습니다: ${error}`);
1821
1924
  }
1822
- return;
1823
1925
  }
1824
- const slot = {
1825
- id,
1826
- containerId,
1827
- adType,
1828
- width: options?.width || 0, // 문자열도 지원
1829
- height: options?.height || 0, // 문자열도 지원
1830
- isLoaded: false,
1831
- isVisible: false,
1832
- refreshRate: 0,
1833
- lazyLoad: false,
1834
- targeting: {},
1835
- load: async () => { await this.loadSlot(slot, options); return null; },
1836
- render: (ad) => this.renderSlot(slot, ad),
1837
- refresh: () => this.refreshSlot(slot.id),
1838
- destroy: () => this.destroySlot(slot.id),
1839
- };
1840
- this.slots.set(id, slot);
1841
- await this.loadSlot(slot, options);
1842
1926
  }
1843
1927
  /**
1844
1928
  * 광고 슬롯 로드
@@ -1905,9 +1989,10 @@ class AdStageSDK {
1905
1989
  * 광고 슬롯 렌더링 (슬라이더 포함)
1906
1990
  */
1907
1991
  renderSlotWithSlider(slot, advertisements, options) {
1992
+ // 💡 한 번 더 안전하게 컨테이너 확인
1908
1993
  const container = DOMUtils.safeGetElementById(slot.containerId);
1909
1994
  if (!container) {
1910
- console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1995
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1911
1996
  return;
1912
1997
  }
1913
1998
  if (this.config.debug) {
package/dist/index.esm.js CHANGED
@@ -253,6 +253,72 @@ class DOMUtils {
253
253
  scrollLeft: this.getWindowProperty('pageXOffset', 0),
254
254
  };
255
255
  }
256
+ /**
257
+ * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
258
+ */
259
+ static async waitForElement(id, options = {}) {
260
+ const { timeout = 3000, retryInterval = 100, debug = false } = options;
261
+ if (!this.canUseDOM()) {
262
+ throw new Error('DOM을 사용할 수 없는 환경입니다.');
263
+ }
264
+ // 즉시 찾을 수 있으면 바로 반환
265
+ const immediateElement = document.getElementById(id);
266
+ if (immediateElement) {
267
+ if (debug) {
268
+ console.log(`✅ 컨테이너 즉시 발견: ${id}`);
269
+ }
270
+ return immediateElement;
271
+ }
272
+ if (debug) {
273
+ console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
274
+ }
275
+ return new Promise((resolve, reject) => {
276
+ let attempts = 0;
277
+ const maxAttempts = Math.ceil(timeout / retryInterval);
278
+ const checkElement = () => {
279
+ attempts++;
280
+ const element = document.getElementById(id);
281
+ if (element) {
282
+ if (debug) {
283
+ console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
284
+ }
285
+ resolve(element);
286
+ return;
287
+ }
288
+ if (attempts >= maxAttempts) {
289
+ const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
290
+
291
+ 다음을 확인해보세요:
292
+ 1. HTML에 id="${id}" 요소가 있는지 확인
293
+ 2. React/Vue 등에서 컴포넌트가 렌더링된 후 SDK 호출
294
+ 3. 철자가 정확한지 확인
295
+ 4. 중복된 ID가 없는지 확인
296
+
297
+ 대기 시간: ${timeout}ms (${attempts}번 시도)`;
298
+ if (debug) {
299
+ console.error(errorMessage);
300
+ }
301
+ reject(new Error(errorMessage));
302
+ return;
303
+ }
304
+ if (debug && attempts % 10 === 0) {
305
+ console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
306
+ }
307
+ // Exponential backoff: 처음엔 빠르게, 나중엔 느리게
308
+ const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
309
+ setTimeout(checkElement, nextInterval);
310
+ };
311
+ // 첫 번째 체크는 즉시 실행
312
+ setTimeout(checkElement, retryInterval);
313
+ });
314
+ }
315
+ /**
316
+ * 여러 DOM 요소를 동시에 기다리기
317
+ */
318
+ static async waitForElements(ids, options = {}) {
319
+ const promises = ids.map(id => this.waitForElement(id, options));
320
+ return Promise.all(promises);
321
+ }
256
322
  }
257
323
 
258
324
  /**
@@ -1810,31 +1876,49 @@ class AdStageSDK {
1810
1876
  * 광고 슬롯 생성 및 로드
1811
1877
  */
1812
1878
  async createSlot(id, containerId, adType = AdType.BANNER, options) {
1813
- const container = DOMUtils.safeGetElementById(containerId);
1814
- if (!container) {
1815
- if (DOMUtils.canUseDOM()) {
1816
- console.error(`Container with ID "${containerId}" not found`);
1879
+ try {
1880
+ // 💡 사용자 친화적 개선: 컨테이너가 나타날 때까지 자동으로 기다림
1881
+ if (this.config.debug) {
1882
+ console.log(`🔍 컨테이너 검색 시작: ${containerId}`);
1883
+ }
1884
+ const container = await DOMUtils.waitForElement(containerId, {
1885
+ timeout: 5000, // 최대 5초 대기
1886
+ retryInterval: 50, // 50ms마다 체크 (부드러운 사용자 경험)
1887
+ debug: this.config.debug
1888
+ });
1889
+ if (this.config.debug) {
1890
+ console.log(`✅ 컨테이너 확인됨: ${containerId}`, container);
1891
+ }
1892
+ const slot = {
1893
+ id,
1894
+ containerId,
1895
+ adType,
1896
+ width: options?.width || 0, // 문자열도 지원
1897
+ height: options?.height || 0, // 문자열도 지원
1898
+ isLoaded: false,
1899
+ isVisible: false,
1900
+ refreshRate: 0,
1901
+ lazyLoad: false,
1902
+ targeting: {},
1903
+ load: async () => { await this.loadSlot(slot, options); return null; },
1904
+ render: (ad) => this.renderSlot(slot, ad),
1905
+ refresh: () => this.refreshSlot(slot.id),
1906
+ destroy: () => this.destroySlot(slot.id),
1907
+ };
1908
+ this.slots.set(id, slot);
1909
+ await this.loadSlot(slot, options);
1910
+ }
1911
+ catch (error) {
1912
+ // 친절한 에러 메시지로 사용자 가이드
1913
+ if (error instanceof Error && error.message.includes('컨테이너를 찾을 수 없습니다')) {
1914
+ console.error(error.message);
1915
+ throw error;
1916
+ }
1917
+ else {
1918
+ console.error(`❌ 광고 슬롯 생성 실패 (${id}):`, error);
1919
+ throw new Error(`광고 슬롯 생성에 실패했습니다: ${error}`);
1817
1920
  }
1818
- return;
1819
1921
  }
1820
- const slot = {
1821
- id,
1822
- containerId,
1823
- adType,
1824
- width: options?.width || 0, // 문자열도 지원
1825
- height: options?.height || 0, // 문자열도 지원
1826
- isLoaded: false,
1827
- isVisible: false,
1828
- refreshRate: 0,
1829
- lazyLoad: false,
1830
- targeting: {},
1831
- load: async () => { await this.loadSlot(slot, options); return null; },
1832
- render: (ad) => this.renderSlot(slot, ad),
1833
- refresh: () => this.refreshSlot(slot.id),
1834
- destroy: () => this.destroySlot(slot.id),
1835
- };
1836
- this.slots.set(id, slot);
1837
- await this.loadSlot(slot, options);
1838
1922
  }
1839
1923
  /**
1840
1924
  * 광고 슬롯 로드
@@ -1901,9 +1985,10 @@ class AdStageSDK {
1901
1985
  * 광고 슬롯 렌더링 (슬라이더 포함)
1902
1986
  */
1903
1987
  renderSlotWithSlider(slot, advertisements, options) {
1988
+ // 💡 한 번 더 안전하게 컨테이너 확인
1904
1989
  const container = DOMUtils.safeGetElementById(slot.containerId);
1905
1990
  if (!container) {
1906
- console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1991
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1907
1992
  return;
1908
1993
  }
1909
1994
  if (this.config.debug) {
@@ -253,6 +253,72 @@ class DOMUtils {
253
253
  scrollLeft: this.getWindowProperty('pageXOffset', 0),
254
254
  };
255
255
  }
256
+ /**
257
+ * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
258
+ */
259
+ static async waitForElement(id, options = {}) {
260
+ const { timeout = 3000, retryInterval = 100, debug = false } = options;
261
+ if (!this.canUseDOM()) {
262
+ throw new Error('DOM을 사용할 수 없는 환경입니다.');
263
+ }
264
+ // 즉시 찾을 수 있으면 바로 반환
265
+ const immediateElement = document.getElementById(id);
266
+ if (immediateElement) {
267
+ if (debug) {
268
+ console.log(`✅ 컨테이너 즉시 발견: ${id}`);
269
+ }
270
+ return immediateElement;
271
+ }
272
+ if (debug) {
273
+ console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
274
+ }
275
+ return new Promise((resolve, reject) => {
276
+ let attempts = 0;
277
+ const maxAttempts = Math.ceil(timeout / retryInterval);
278
+ const checkElement = () => {
279
+ attempts++;
280
+ const element = document.getElementById(id);
281
+ if (element) {
282
+ if (debug) {
283
+ console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
284
+ }
285
+ resolve(element);
286
+ return;
287
+ }
288
+ if (attempts >= maxAttempts) {
289
+ const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
290
+
291
+ 다음을 확인해보세요:
292
+ 1. HTML에 id="${id}" 요소가 있는지 확인
293
+ 2. React/Vue 등에서 컴포넌트가 렌더링된 후 SDK 호출
294
+ 3. 철자가 정확한지 확인
295
+ 4. 중복된 ID가 없는지 확인
296
+
297
+ 대기 시간: ${timeout}ms (${attempts}번 시도)`;
298
+ if (debug) {
299
+ console.error(errorMessage);
300
+ }
301
+ reject(new Error(errorMessage));
302
+ return;
303
+ }
304
+ if (debug && attempts % 10 === 0) {
305
+ console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
306
+ }
307
+ // Exponential backoff: 처음엔 빠르게, 나중엔 느리게
308
+ const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
309
+ setTimeout(checkElement, nextInterval);
310
+ };
311
+ // 첫 번째 체크는 즉시 실행
312
+ setTimeout(checkElement, retryInterval);
313
+ });
314
+ }
315
+ /**
316
+ * 여러 DOM 요소를 동시에 기다리기
317
+ */
318
+ static async waitForElements(ids, options = {}) {
319
+ const promises = ids.map(id => this.waitForElement(id, options));
320
+ return Promise.all(promises);
321
+ }
256
322
  }
257
323
 
258
324
  /**
@@ -1810,31 +1876,49 @@ class AdStageSDK {
1810
1876
  * 광고 슬롯 생성 및 로드
1811
1877
  */
1812
1878
  async createSlot(id, containerId, adType = AdType.BANNER, options) {
1813
- const container = DOMUtils.safeGetElementById(containerId);
1814
- if (!container) {
1815
- if (DOMUtils.canUseDOM()) {
1816
- console.error(`Container with ID "${containerId}" not found`);
1879
+ try {
1880
+ // 💡 사용자 친화적 개선: 컨테이너가 나타날 때까지 자동으로 기다림
1881
+ if (this.config.debug) {
1882
+ console.log(`🔍 컨테이너 검색 시작: ${containerId}`);
1883
+ }
1884
+ const container = await DOMUtils.waitForElement(containerId, {
1885
+ timeout: 5000, // 최대 5초 대기
1886
+ retryInterval: 50, // 50ms마다 체크 (부드러운 사용자 경험)
1887
+ debug: this.config.debug
1888
+ });
1889
+ if (this.config.debug) {
1890
+ console.log(`✅ 컨테이너 확인됨: ${containerId}`, container);
1891
+ }
1892
+ const slot = {
1893
+ id,
1894
+ containerId,
1895
+ adType,
1896
+ width: options?.width || 0, // 문자열도 지원
1897
+ height: options?.height || 0, // 문자열도 지원
1898
+ isLoaded: false,
1899
+ isVisible: false,
1900
+ refreshRate: 0,
1901
+ lazyLoad: false,
1902
+ targeting: {},
1903
+ load: async () => { await this.loadSlot(slot, options); return null; },
1904
+ render: (ad) => this.renderSlot(slot, ad),
1905
+ refresh: () => this.refreshSlot(slot.id),
1906
+ destroy: () => this.destroySlot(slot.id),
1907
+ };
1908
+ this.slots.set(id, slot);
1909
+ await this.loadSlot(slot, options);
1910
+ }
1911
+ catch (error) {
1912
+ // 친절한 에러 메시지로 사용자 가이드
1913
+ if (error instanceof Error && error.message.includes('컨테이너를 찾을 수 없습니다')) {
1914
+ console.error(error.message);
1915
+ throw error;
1916
+ }
1917
+ else {
1918
+ console.error(`❌ 광고 슬롯 생성 실패 (${id}):`, error);
1919
+ throw new Error(`광고 슬롯 생성에 실패했습니다: ${error}`);
1817
1920
  }
1818
- return;
1819
1921
  }
1820
- const slot = {
1821
- id,
1822
- containerId,
1823
- adType,
1824
- width: options?.width || 0, // 문자열도 지원
1825
- height: options?.height || 0, // 문자열도 지원
1826
- isLoaded: false,
1827
- isVisible: false,
1828
- refreshRate: 0,
1829
- lazyLoad: false,
1830
- targeting: {},
1831
- load: async () => { await this.loadSlot(slot, options); return null; },
1832
- render: (ad) => this.renderSlot(slot, ad),
1833
- refresh: () => this.refreshSlot(slot.id),
1834
- destroy: () => this.destroySlot(slot.id),
1835
- };
1836
- this.slots.set(id, slot);
1837
- await this.loadSlot(slot, options);
1838
1922
  }
1839
1923
  /**
1840
1924
  * 광고 슬롯 로드
@@ -1901,9 +1985,10 @@ class AdStageSDK {
1901
1985
  * 광고 슬롯 렌더링 (슬라이더 포함)
1902
1986
  */
1903
1987
  renderSlotWithSlider(slot, advertisements, options) {
1988
+ // 💡 한 번 더 안전하게 컨테이너 확인
1904
1989
  const container = DOMUtils.safeGetElementById(slot.containerId);
1905
1990
  if (!container) {
1906
- console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1991
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1907
1992
  return;
1908
1993
  }
1909
1994
  if (this.config.debug) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adstage/web-sdk",
3
- "version": "1.3.3",
4
- "description": "AdStage Web SDK for displaying advertisements",
3
+ "version": "1.3.4",
4
+ "description": "AdStage Web SDK for displaying advertisements with auto DOM-ready detection",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.esm.js",
package/src/index.ts CHANGED
@@ -85,33 +85,51 @@ export class AdStageSDK {
85
85
  sliderEffect?: 'slide' | 'fade'; // 슬라이더 효과 선택 (기본값: slide)
86
86
  }
87
87
  ): Promise<void> {
88
- const container = DOMUtils.safeGetElementById(containerId);
89
- if (!container) {
90
- if (DOMUtils.canUseDOM()) {
91
- console.error(`Container with ID "${containerId}" not found`);
88
+ try {
89
+ // 💡 사용자 친화적 개선: 컨테이너가 나타날 때까지 자동으로 기다림
90
+ if (this.config.debug) {
91
+ console.log(`🔍 컨테이너 검색 시작: ${containerId}`);
92
92
  }
93
- return;
94
- }
93
+
94
+ const container = await DOMUtils.waitForElement(containerId, {
95
+ timeout: 5000, // 최대 5초 대기
96
+ retryInterval: 50, // 50ms마다 체크 (부드러운 사용자 경험)
97
+ debug: this.config.debug
98
+ });
95
99
 
96
- const slot: AdSlot = {
97
- id,
98
- containerId,
99
- adType,
100
- width: options?.width || 0, // 문자열도 지원
101
- height: options?.height || 0, // 문자열도 지원
102
- isLoaded: false,
103
- isVisible: false,
104
- refreshRate: 0,
105
- lazyLoad: false,
106
- targeting: {},
107
- load: async () => { await this.loadSlot(slot, options); return null; },
108
- render: (ad: Advertisement) => this.renderSlot(slot, ad),
109
- refresh: () => this.refreshSlot(slot.id),
110
- destroy: () => this.destroySlot(slot.id),
111
- };
100
+ if (this.config.debug) {
101
+ console.log(`✅ 컨테이너 확인됨: ${containerId}`, container);
102
+ }
112
103
 
113
- this.slots.set(id, slot);
114
- await this.loadSlot(slot, options);
104
+ const slot: AdSlot = {
105
+ id,
106
+ containerId,
107
+ adType,
108
+ width: options?.width || 0, // 문자열도 지원
109
+ height: options?.height || 0, // 문자열도 지원
110
+ isLoaded: false,
111
+ isVisible: false,
112
+ refreshRate: 0,
113
+ lazyLoad: false,
114
+ targeting: {},
115
+ load: async () => { await this.loadSlot(slot, options); return null; },
116
+ render: (ad: Advertisement) => this.renderSlot(slot, ad),
117
+ refresh: () => this.refreshSlot(slot.id),
118
+ destroy: () => this.destroySlot(slot.id),
119
+ };
120
+
121
+ this.slots.set(id, slot);
122
+ await this.loadSlot(slot, options);
123
+ } catch (error) {
124
+ // 친절한 에러 메시지로 사용자 가이드
125
+ if (error instanceof Error && error.message.includes('컨테이너를 찾을 수 없습니다')) {
126
+ console.error(error.message);
127
+ throw error;
128
+ } else {
129
+ console.error(`❌ 광고 슬롯 생성 실패 (${id}):`, error);
130
+ throw new Error(`광고 슬롯 생성에 실패했습니다: ${error}`);
131
+ }
132
+ }
115
133
  }
116
134
 
117
135
  /**
@@ -189,9 +207,10 @@ export class AdStageSDK {
189
207
  * 광고 슬롯 렌더링 (슬라이더 포함)
190
208
  */
191
209
  private renderSlotWithSlider(slot: AdSlot, advertisements: Advertisement[], options?: any): void {
210
+ // 💡 한 번 더 안전하게 컨테이너 확인
192
211
  const container = DOMUtils.safeGetElementById(slot.containerId);
193
212
  if (!container) {
194
- console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
213
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
195
214
  return;
196
215
  }
197
216
 
@@ -234,4 +234,97 @@ export class DOMUtils {
234
234
  scrollLeft: this.getWindowProperty('pageXOffset', 0),
235
235
  };
236
236
  }
237
+
238
+ /**
239
+ * DOM 요소가 나타날 때까지 기다리기 (사용자 친화적 API)
240
+ */
241
+ static async waitForElement(
242
+ id: string,
243
+ options: {
244
+ timeout?: number; // 최대 대기 시간 (ms), 기본값: 3000
245
+ retryInterval?: number; // 재시도 간격 (ms), 기본값: 100
246
+ debug?: boolean; // 디버그 로그 출력 여부
247
+ } = {}
248
+ ): Promise<HTMLElement> {
249
+ const { timeout = 3000, retryInterval = 100, debug = false } = options;
250
+
251
+ if (!this.canUseDOM()) {
252
+ throw new Error('DOM을 사용할 수 없는 환경입니다.');
253
+ }
254
+
255
+ // 즉시 찾을 수 있으면 바로 반환
256
+ const immediateElement = document.getElementById(id);
257
+ if (immediateElement) {
258
+ if (debug) {
259
+ console.log(`✅ 컨테이너 즉시 발견: ${id}`);
260
+ }
261
+ return immediateElement;
262
+ }
263
+
264
+ if (debug) {
265
+ console.log(`⏳ 컨테이너 대기 시작: ${id} (최대 ${timeout}ms)`);
266
+ }
267
+
268
+ return new Promise((resolve, reject) => {
269
+ let attempts = 0;
270
+ const maxAttempts = Math.ceil(timeout / retryInterval);
271
+
272
+ const checkElement = () => {
273
+ attempts++;
274
+ const element = document.getElementById(id);
275
+
276
+ if (element) {
277
+ if (debug) {
278
+ console.log(`✅ 컨테이너 발견: ${id} (${attempts}번째 시도, ${attempts * retryInterval}ms 경과)`);
279
+ }
280
+ resolve(element);
281
+ return;
282
+ }
283
+
284
+ if (attempts >= maxAttempts) {
285
+ const errorMessage = `❌ 컨테이너를 찾을 수 없습니다: "${id}"
286
+
287
+ 다음을 확인해보세요:
288
+ 1. HTML에 id="${id}" 요소가 있는지 확인
289
+ 2. React/Vue 등에서 컴포넌트가 렌더링된 후 SDK 호출
290
+ 3. 철자가 정확한지 확인
291
+ 4. 중복된 ID가 없는지 확인
292
+
293
+ 대기 시간: ${timeout}ms (${attempts}번 시도)`;
294
+
295
+ if (debug) {
296
+ console.error(errorMessage);
297
+ }
298
+ reject(new Error(errorMessage));
299
+ return;
300
+ }
301
+
302
+ if (debug && attempts % 10 === 0) {
303
+ console.log(`⏳ 컨테이너 대기 중: ${id} (${attempts}/${maxAttempts})`);
304
+ }
305
+
306
+ // Exponential backoff: 처음엔 빠르게, 나중엔 느리게
307
+ const nextInterval = Math.min(retryInterval * Math.pow(1.2, attempts), 500);
308
+ setTimeout(checkElement, nextInterval);
309
+ };
310
+
311
+ // 첫 번째 체크는 즉시 실행
312
+ setTimeout(checkElement, retryInterval);
313
+ });
314
+ }
315
+
316
+ /**
317
+ * 여러 DOM 요소를 동시에 기다리기
318
+ */
319
+ static async waitForElements(
320
+ ids: string[],
321
+ options: {
322
+ timeout?: number;
323
+ retryInterval?: number;
324
+ debug?: boolean;
325
+ } = {}
326
+ ): Promise<HTMLElement[]> {
327
+ const promises = ids.map(id => this.waitForElement(id, options));
328
+ return Promise.all(promises);
329
+ }
237
330
  }