@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 +109 -24
- package/dist/index.esm.js +109 -24
- package/dist/index.standalone.js +109 -24
- package/package.json +2 -2
- package/src/index.ts +44 -25
- package/src/utils/dom-utils.ts +93 -0
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
|
-
|
|
1818
|
-
|
|
1819
|
-
if (
|
|
1820
|
-
console.
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
if (
|
|
1816
|
-
console.
|
|
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/dist/index.standalone.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
|
-
|
|
1814
|
-
|
|
1815
|
-
if (
|
|
1816
|
-
console.
|
|
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.
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
console.
|
|
88
|
+
try {
|
|
89
|
+
// 💡 사용자 친화적 개선: 컨테이너가 나타날 때까지 자동으로 기다림
|
|
90
|
+
if (this.config.debug) {
|
|
91
|
+
console.log(`🔍 컨테이너 검색 시작: ${containerId}`);
|
|
92
92
|
}
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
package/src/utils/dom-utils.ts
CHANGED
|
@@ -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
|
}
|