@adstage/web-sdk 1.3.2 → 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
  * 광고 슬롯 로드
@@ -1852,38 +1936,73 @@ class AdStageSDK {
1852
1936
  ...(options?.deviceType && { deviceType: options.deviceType }),
1853
1937
  ...(options?.country && { country: options.country }),
1854
1938
  });
1855
- const response = await fetch(`${this.baseUrl}/advertisements/list?${queryParams}`, {
1939
+ const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
1940
+ if (this.config.debug) {
1941
+ console.log(`🌐 광고 API 요청 시작:`, {
1942
+ url: requestUrl,
1943
+ apiKey: this.config.apiKey.substring(0, 10) + '...',
1944
+ slot: slot.id
1945
+ });
1946
+ }
1947
+ const response = await fetch(requestUrl, {
1856
1948
  headers: {
1857
1949
  'x-api-key': this.config.apiKey,
1858
1950
  'Content-Type': 'application/json',
1859
1951
  },
1860
1952
  });
1953
+ if (this.config.debug) {
1954
+ console.log(`📡 API 응답 상태:`, {
1955
+ status: response.status,
1956
+ statusText: response.statusText,
1957
+ ok: response.ok
1958
+ });
1959
+ }
1861
1960
  if (!response.ok) {
1862
1961
  throw new Error(`HTTP error! status: ${response.status}`);
1863
1962
  }
1864
1963
  const data = await response.json();
1964
+ if (this.config.debug) {
1965
+ console.log(`📊 API 응답 데이터:`, {
1966
+ data,
1967
+ advertisementsCount: data.advertisements ? data.advertisements.length : 0
1968
+ });
1969
+ }
1865
1970
  const advertisements = data.advertisements || [];
1866
1971
  if (advertisements.length > 0) {
1972
+ if (this.config.debug) {
1973
+ console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
1974
+ }
1867
1975
  // 여러 광고가 있을 경우 슬라이드로 렌더링
1868
1976
  this.renderSlotWithSlider(slot, advertisements, options);
1869
1977
  // 첫 번째 광고에 대해서만 노출 이벤트 추적
1870
1978
  await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, exports.AdEventType.IMPRESSION);
1871
1979
  }
1872
1980
  else {
1873
- console.warn(`No advertisements available for slot ${slot.id}`);
1981
+ console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
1874
1982
  }
1875
1983
  }
1876
1984
  catch (error) {
1877
- console.error(`Failed to load slot ${slot.id}:`, error);
1985
+ console.error(`❌ 슬롯 ${slot.id} 로드 실패:`, error);
1878
1986
  }
1879
1987
  }
1880
1988
  /**
1881
1989
  * 광고 슬롯 렌더링 (슬라이더 포함)
1882
1990
  */
1883
1991
  renderSlotWithSlider(slot, advertisements, options) {
1992
+ // 💡 한 번 더 안전하게 컨테이너 확인
1884
1993
  const container = DOMUtils.safeGetElementById(slot.containerId);
1885
- if (!container)
1994
+ if (!container) {
1995
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1886
1996
  return;
1997
+ }
1998
+ if (this.config.debug) {
1999
+ console.log(`🎨 광고 렌더링 시작:`, {
2000
+ slotId: slot.id,
2001
+ containerId: slot.containerId,
2002
+ advertisementCount: advertisements.length,
2003
+ container: container
2004
+ });
2005
+ }
1887
2006
  if (advertisements.length === 1) {
1888
2007
  // 광고가 하나뿐이면 기본 렌더링
1889
2008
  this.renderSlot(slot, advertisements[0]);
@@ -1907,7 +2026,11 @@ class AdStageSDK {
1907
2026
  slot.isLoaded = true;
1908
2027
  if (this.config.debug) {
1909
2028
  const sliderType = useFadeEffect ? 'fade slider' : 'slider';
1910
- console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
2029
+ console.log(`✅ ${advertisements.length} 광고를 ${sliderType} 렌더링 완료:`, {
2030
+ slotId: slot.id,
2031
+ container: container,
2032
+ sliderContainer: sliderContainer
2033
+ });
1911
2034
  }
1912
2035
  }
1913
2036
  /**
@@ -1915,15 +2038,36 @@ class AdStageSDK {
1915
2038
  */
1916
2039
  renderSlot(slot, ad) {
1917
2040
  const container = DOMUtils.safeGetElementById(slot.containerId);
1918
- if (!container)
2041
+ if (!container) {
2042
+ console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1919
2043
  return;
2044
+ }
2045
+ if (this.config.debug) {
2046
+ console.log(`🎨 단일 광고 렌더링 시작:`, {
2047
+ slotId: slot.id,
2048
+ containerId: slot.containerId,
2049
+ ad: ad,
2050
+ container: container
2051
+ });
2052
+ }
1920
2053
  // 팩토리를 사용해서 적절한 렌더러로 광고 생성
1921
2054
  const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
2055
+ if (this.config.debug) {
2056
+ console.log(`🔧 광고 요소 생성됨:`, {
2057
+ adElement: adElement,
2058
+ tagName: adElement.tagName,
2059
+ innerHTML: adElement.innerHTML.substring(0, 200) + '...'
2060
+ });
2061
+ }
1922
2062
  container.innerHTML = '';
1923
2063
  container.appendChild(adElement);
1924
2064
  slot.isLoaded = true;
1925
2065
  if (this.config.debug) {
1926
- console.log(`Rendered ad for slot ${slot.id}:`, ad);
2066
+ console.log(`✅ 단일 광고 렌더링 완료:`, {
2067
+ slotId: slot.id,
2068
+ ad: ad,
2069
+ containerContent: container.innerHTML.substring(0, 200) + '...'
2070
+ });
1927
2071
  }
1928
2072
  }
1929
2073
  /**
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
  * 광고 슬롯 로드
@@ -1848,38 +1932,73 @@ class AdStageSDK {
1848
1932
  ...(options?.deviceType && { deviceType: options.deviceType }),
1849
1933
  ...(options?.country && { country: options.country }),
1850
1934
  });
1851
- const response = await fetch(`${this.baseUrl}/advertisements/list?${queryParams}`, {
1935
+ const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
1936
+ if (this.config.debug) {
1937
+ console.log(`🌐 광고 API 요청 시작:`, {
1938
+ url: requestUrl,
1939
+ apiKey: this.config.apiKey.substring(0, 10) + '...',
1940
+ slot: slot.id
1941
+ });
1942
+ }
1943
+ const response = await fetch(requestUrl, {
1852
1944
  headers: {
1853
1945
  'x-api-key': this.config.apiKey,
1854
1946
  'Content-Type': 'application/json',
1855
1947
  },
1856
1948
  });
1949
+ if (this.config.debug) {
1950
+ console.log(`📡 API 응답 상태:`, {
1951
+ status: response.status,
1952
+ statusText: response.statusText,
1953
+ ok: response.ok
1954
+ });
1955
+ }
1857
1956
  if (!response.ok) {
1858
1957
  throw new Error(`HTTP error! status: ${response.status}`);
1859
1958
  }
1860
1959
  const data = await response.json();
1960
+ if (this.config.debug) {
1961
+ console.log(`📊 API 응답 데이터:`, {
1962
+ data,
1963
+ advertisementsCount: data.advertisements ? data.advertisements.length : 0
1964
+ });
1965
+ }
1861
1966
  const advertisements = data.advertisements || [];
1862
1967
  if (advertisements.length > 0) {
1968
+ if (this.config.debug) {
1969
+ console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
1970
+ }
1863
1971
  // 여러 광고가 있을 경우 슬라이드로 렌더링
1864
1972
  this.renderSlotWithSlider(slot, advertisements, options);
1865
1973
  // 첫 번째 광고에 대해서만 노출 이벤트 추적
1866
1974
  await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, AdEventType.IMPRESSION);
1867
1975
  }
1868
1976
  else {
1869
- console.warn(`No advertisements available for slot ${slot.id}`);
1977
+ console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
1870
1978
  }
1871
1979
  }
1872
1980
  catch (error) {
1873
- console.error(`Failed to load slot ${slot.id}:`, error);
1981
+ console.error(`❌ 슬롯 ${slot.id} 로드 실패:`, error);
1874
1982
  }
1875
1983
  }
1876
1984
  /**
1877
1985
  * 광고 슬롯 렌더링 (슬라이더 포함)
1878
1986
  */
1879
1987
  renderSlotWithSlider(slot, advertisements, options) {
1988
+ // 💡 한 번 더 안전하게 컨테이너 확인
1880
1989
  const container = DOMUtils.safeGetElementById(slot.containerId);
1881
- if (!container)
1990
+ if (!container) {
1991
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1882
1992
  return;
1993
+ }
1994
+ if (this.config.debug) {
1995
+ console.log(`🎨 광고 렌더링 시작:`, {
1996
+ slotId: slot.id,
1997
+ containerId: slot.containerId,
1998
+ advertisementCount: advertisements.length,
1999
+ container: container
2000
+ });
2001
+ }
1883
2002
  if (advertisements.length === 1) {
1884
2003
  // 광고가 하나뿐이면 기본 렌더링
1885
2004
  this.renderSlot(slot, advertisements[0]);
@@ -1903,7 +2022,11 @@ class AdStageSDK {
1903
2022
  slot.isLoaded = true;
1904
2023
  if (this.config.debug) {
1905
2024
  const sliderType = useFadeEffect ? 'fade slider' : 'slider';
1906
- console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
2025
+ console.log(`✅ ${advertisements.length} 광고를 ${sliderType} 렌더링 완료:`, {
2026
+ slotId: slot.id,
2027
+ container: container,
2028
+ sliderContainer: sliderContainer
2029
+ });
1907
2030
  }
1908
2031
  }
1909
2032
  /**
@@ -1911,15 +2034,36 @@ class AdStageSDK {
1911
2034
  */
1912
2035
  renderSlot(slot, ad) {
1913
2036
  const container = DOMUtils.safeGetElementById(slot.containerId);
1914
- if (!container)
2037
+ if (!container) {
2038
+ console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1915
2039
  return;
2040
+ }
2041
+ if (this.config.debug) {
2042
+ console.log(`🎨 단일 광고 렌더링 시작:`, {
2043
+ slotId: slot.id,
2044
+ containerId: slot.containerId,
2045
+ ad: ad,
2046
+ container: container
2047
+ });
2048
+ }
1916
2049
  // 팩토리를 사용해서 적절한 렌더러로 광고 생성
1917
2050
  const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
2051
+ if (this.config.debug) {
2052
+ console.log(`🔧 광고 요소 생성됨:`, {
2053
+ adElement: adElement,
2054
+ tagName: adElement.tagName,
2055
+ innerHTML: adElement.innerHTML.substring(0, 200) + '...'
2056
+ });
2057
+ }
1918
2058
  container.innerHTML = '';
1919
2059
  container.appendChild(adElement);
1920
2060
  slot.isLoaded = true;
1921
2061
  if (this.config.debug) {
1922
- console.log(`Rendered ad for slot ${slot.id}:`, ad);
2062
+ console.log(`✅ 단일 광고 렌더링 완료:`, {
2063
+ slotId: slot.id,
2064
+ ad: ad,
2065
+ containerContent: container.innerHTML.substring(0, 200) + '...'
2066
+ });
1923
2067
  }
1924
2068
  }
1925
2069
  /**
@@ -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
  * 광고 슬롯 로드
@@ -1848,38 +1932,73 @@ class AdStageSDK {
1848
1932
  ...(options?.deviceType && { deviceType: options.deviceType }),
1849
1933
  ...(options?.country && { country: options.country }),
1850
1934
  });
1851
- const response = await fetch(`${this.baseUrl}/advertisements/list?${queryParams}`, {
1935
+ const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
1936
+ if (this.config.debug) {
1937
+ console.log(`🌐 광고 API 요청 시작:`, {
1938
+ url: requestUrl,
1939
+ apiKey: this.config.apiKey.substring(0, 10) + '...',
1940
+ slot: slot.id
1941
+ });
1942
+ }
1943
+ const response = await fetch(requestUrl, {
1852
1944
  headers: {
1853
1945
  'x-api-key': this.config.apiKey,
1854
1946
  'Content-Type': 'application/json',
1855
1947
  },
1856
1948
  });
1949
+ if (this.config.debug) {
1950
+ console.log(`📡 API 응답 상태:`, {
1951
+ status: response.status,
1952
+ statusText: response.statusText,
1953
+ ok: response.ok
1954
+ });
1955
+ }
1857
1956
  if (!response.ok) {
1858
1957
  throw new Error(`HTTP error! status: ${response.status}`);
1859
1958
  }
1860
1959
  const data = await response.json();
1960
+ if (this.config.debug) {
1961
+ console.log(`📊 API 응답 데이터:`, {
1962
+ data,
1963
+ advertisementsCount: data.advertisements ? data.advertisements.length : 0
1964
+ });
1965
+ }
1861
1966
  const advertisements = data.advertisements || [];
1862
1967
  if (advertisements.length > 0) {
1968
+ if (this.config.debug) {
1969
+ console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
1970
+ }
1863
1971
  // 여러 광고가 있을 경우 슬라이드로 렌더링
1864
1972
  this.renderSlotWithSlider(slot, advertisements, options);
1865
1973
  // 첫 번째 광고에 대해서만 노출 이벤트 추적
1866
1974
  await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, AdEventType.IMPRESSION);
1867
1975
  }
1868
1976
  else {
1869
- console.warn(`No advertisements available for slot ${slot.id}`);
1977
+ console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
1870
1978
  }
1871
1979
  }
1872
1980
  catch (error) {
1873
- console.error(`Failed to load slot ${slot.id}:`, error);
1981
+ console.error(`❌ 슬롯 ${slot.id} 로드 실패:`, error);
1874
1982
  }
1875
1983
  }
1876
1984
  /**
1877
1985
  * 광고 슬롯 렌더링 (슬라이더 포함)
1878
1986
  */
1879
1987
  renderSlotWithSlider(slot, advertisements, options) {
1988
+ // 💡 한 번 더 안전하게 컨테이너 확인
1880
1989
  const container = DOMUtils.safeGetElementById(slot.containerId);
1881
- if (!container)
1990
+ if (!container) {
1991
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1882
1992
  return;
1993
+ }
1994
+ if (this.config.debug) {
1995
+ console.log(`🎨 광고 렌더링 시작:`, {
1996
+ slotId: slot.id,
1997
+ containerId: slot.containerId,
1998
+ advertisementCount: advertisements.length,
1999
+ container: container
2000
+ });
2001
+ }
1883
2002
  if (advertisements.length === 1) {
1884
2003
  // 광고가 하나뿐이면 기본 렌더링
1885
2004
  this.renderSlot(slot, advertisements[0]);
@@ -1903,7 +2022,11 @@ class AdStageSDK {
1903
2022
  slot.isLoaded = true;
1904
2023
  if (this.config.debug) {
1905
2024
  const sliderType = useFadeEffect ? 'fade slider' : 'slider';
1906
- console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
2025
+ console.log(`✅ ${advertisements.length} 광고를 ${sliderType} 렌더링 완료:`, {
2026
+ slotId: slot.id,
2027
+ container: container,
2028
+ sliderContainer: sliderContainer
2029
+ });
1907
2030
  }
1908
2031
  }
1909
2032
  /**
@@ -1911,15 +2034,36 @@ class AdStageSDK {
1911
2034
  */
1912
2035
  renderSlot(slot, ad) {
1913
2036
  const container = DOMUtils.safeGetElementById(slot.containerId);
1914
- if (!container)
2037
+ if (!container) {
2038
+ console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
1915
2039
  return;
2040
+ }
2041
+ if (this.config.debug) {
2042
+ console.log(`🎨 단일 광고 렌더링 시작:`, {
2043
+ slotId: slot.id,
2044
+ containerId: slot.containerId,
2045
+ ad: ad,
2046
+ container: container
2047
+ });
2048
+ }
1916
2049
  // 팩토리를 사용해서 적절한 렌더러로 광고 생성
1917
2050
  const adElement = AdRendererFactory.render(ad, slot, (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType));
2051
+ if (this.config.debug) {
2052
+ console.log(`🔧 광고 요소 생성됨:`, {
2053
+ adElement: adElement,
2054
+ tagName: adElement.tagName,
2055
+ innerHTML: adElement.innerHTML.substring(0, 200) + '...'
2056
+ });
2057
+ }
1918
2058
  container.innerHTML = '';
1919
2059
  container.appendChild(adElement);
1920
2060
  slot.isLoaded = true;
1921
2061
  if (this.config.debug) {
1922
- console.log(`Rendered ad for slot ${slot.id}:`, ad);
2062
+ console.log(`✅ 단일 광고 렌더링 완료:`, {
2063
+ slotId: slot.id,
2064
+ ad: ad,
2065
+ containerContent: container.innerHTML.substring(0, 200) + '...'
2066
+ });
1923
2067
  }
1924
2068
  }
1925
2069
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adstage/web-sdk",
3
- "version": "1.3.2",
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
  /**
@@ -127,34 +145,61 @@ export class AdStageSDK {
127
145
  ...(options?.country && { country: options.country }),
128
146
  });
129
147
 
130
- const response = await fetch(
131
- `${this.baseUrl}/advertisements/list?${queryParams}`,
132
- {
133
- headers: {
134
- 'x-api-key': this.config.apiKey,
135
- 'Content-Type': 'application/json',
136
- },
137
- }
138
- );
148
+ const requestUrl = `${this.baseUrl}/advertisements/list?${queryParams}`;
149
+
150
+ if (this.config.debug) {
151
+ console.log(`🌐 광고 API 요청 시작:`, {
152
+ url: requestUrl,
153
+ apiKey: this.config.apiKey.substring(0, 10) + '...',
154
+ slot: slot.id
155
+ });
156
+ }
157
+
158
+ const response = await fetch(requestUrl, {
159
+ headers: {
160
+ 'x-api-key': this.config.apiKey,
161
+ 'Content-Type': 'application/json',
162
+ },
163
+ });
164
+
165
+ if (this.config.debug) {
166
+ console.log(`📡 API 응답 상태:`, {
167
+ status: response.status,
168
+ statusText: response.statusText,
169
+ ok: response.ok
170
+ });
171
+ }
139
172
 
140
173
  if (!response.ok) {
141
174
  throw new Error(`HTTP error! status: ${response.status}`);
142
175
  }
143
176
 
144
177
  const data = await response.json();
178
+
179
+ if (this.config.debug) {
180
+ console.log(`📊 API 응답 데이터:`, {
181
+ data,
182
+ advertisementsCount: data.advertisements ? data.advertisements.length : 0
183
+ });
184
+ }
185
+
145
186
  const advertisements = data.advertisements || [];
146
187
 
147
188
  if (advertisements.length > 0) {
189
+ if (this.config.debug) {
190
+ console.log(`✅ ${advertisements.length}개 광고 발견:`, advertisements);
191
+ }
192
+
148
193
  // 여러 광고가 있을 경우 슬라이드로 렌더링
149
194
  this.renderSlotWithSlider(slot, advertisements, options);
150
195
 
151
196
  // 첫 번째 광고에 대해서만 노출 이벤트 추적
152
197
  await this.eventTracker.trackEvent(advertisements[0]._id, slot.id, AdEventType.IMPRESSION);
153
198
  } else {
154
- console.warn(`No advertisements available for slot ${slot.id}`);
199
+ console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
155
200
  }
156
201
  } catch (error) {
157
- console.error(`Failed to load slot ${slot.id}:`, error);
202
+ console.error(`❌ 슬롯 ${slot.id} 로드 실패:`, error);
158
203
  }
159
204
  }
160
205
 
@@ -162,8 +207,21 @@ export class AdStageSDK {
162
207
  * 광고 슬롯 렌더링 (슬라이더 포함)
163
208
  */
164
209
  private renderSlotWithSlider(slot: AdSlot, advertisements: Advertisement[], options?: any): void {
210
+ // 💡 한 번 더 안전하게 컨테이너 확인
165
211
  const container = DOMUtils.safeGetElementById(slot.containerId);
166
- if (!container) return;
212
+ if (!container) {
213
+ console.error(`❌ 렌더링 시점에 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
214
+ return;
215
+ }
216
+
217
+ if (this.config.debug) {
218
+ console.log(`🎨 광고 렌더링 시작:`, {
219
+ slotId: slot.id,
220
+ containerId: slot.containerId,
221
+ advertisementCount: advertisements.length,
222
+ container: container
223
+ });
224
+ }
167
225
 
168
226
  if (advertisements.length === 1) {
169
227
  // 광고가 하나뿐이면 기본 렌더링
@@ -202,7 +260,11 @@ export class AdStageSDK {
202
260
 
203
261
  if (this.config.debug) {
204
262
  const sliderType = useFadeEffect ? 'fade slider' : 'slider';
205
- console.log(`Rendered ${advertisements.length} ads with ${sliderType} for slot ${slot.id}:`, advertisements);
263
+ console.log(`✅ ${advertisements.length} 광고를 ${sliderType} 렌더링 완료:`, {
264
+ slotId: slot.id,
265
+ container: container,
266
+ sliderContainer: sliderContainer
267
+ });
206
268
  }
207
269
  }
208
270
 
@@ -211,7 +273,19 @@ export class AdStageSDK {
211
273
  */
212
274
  private renderSlot(slot: AdSlot, ad: Advertisement): void {
213
275
  const container = DOMUtils.safeGetElementById(slot.containerId);
214
- if (!container) return;
276
+ if (!container) {
277
+ console.error(`❌ 컨테이너를 찾을 수 없습니다: ${slot.containerId}`);
278
+ return;
279
+ }
280
+
281
+ if (this.config.debug) {
282
+ console.log(`🎨 단일 광고 렌더링 시작:`, {
283
+ slotId: slot.id,
284
+ containerId: slot.containerId,
285
+ ad: ad,
286
+ container: container
287
+ });
288
+ }
215
289
 
216
290
  // 팩토리를 사용해서 적절한 렌더러로 광고 생성
217
291
  const adElement = AdRendererFactory.render(
@@ -220,12 +294,24 @@ export class AdStageSDK {
220
294
  (adId, slotId, eventType) => this.eventTracker.trackEvent(adId, slotId, eventType)
221
295
  );
222
296
 
297
+ if (this.config.debug) {
298
+ console.log(`🔧 광고 요소 생성됨:`, {
299
+ adElement: adElement,
300
+ tagName: adElement.tagName,
301
+ innerHTML: adElement.innerHTML.substring(0, 200) + '...'
302
+ });
303
+ }
304
+
223
305
  container.innerHTML = '';
224
306
  container.appendChild(adElement);
225
307
  slot.isLoaded = true;
226
308
 
227
309
  if (this.config.debug) {
228
- console.log(`Rendered ad for slot ${slot.id}:`, ad);
310
+ console.log(`✅ 단일 광고 렌더링 완료:`, {
311
+ slotId: slot.id,
312
+ ad: ad,
313
+ containerContent: container.innerHTML.substring(0, 200) + '...'
314
+ });
229
315
  }
230
316
  }
231
317
 
@@ -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
  }