@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 +174 -30
- package/dist/index.esm.js +174 -30
- package/dist/index.standalone.js +174 -30
- package/package.json +2 -2
- package/src/index.ts +125 -39
- 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
|
* 광고 슬롯 로드
|
|
@@ -1852,38 +1936,73 @@ class AdStageSDK {
|
|
|
1852
1936
|
...(options?.deviceType && { deviceType: options.deviceType }),
|
|
1853
1937
|
...(options?.country && { country: options.country }),
|
|
1854
1938
|
});
|
|
1855
|
-
const
|
|
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(
|
|
1981
|
+
console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
|
|
1874
1982
|
}
|
|
1875
1983
|
}
|
|
1876
1984
|
catch (error) {
|
|
1877
|
-
console.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(
|
|
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(
|
|
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
|
-
|
|
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
|
* 광고 슬롯 로드
|
|
@@ -1848,38 +1932,73 @@ class AdStageSDK {
|
|
|
1848
1932
|
...(options?.deviceType && { deviceType: options.deviceType }),
|
|
1849
1933
|
...(options?.country && { country: options.country }),
|
|
1850
1934
|
});
|
|
1851
|
-
const
|
|
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(
|
|
1977
|
+
console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
|
|
1870
1978
|
}
|
|
1871
1979
|
}
|
|
1872
1980
|
catch (error) {
|
|
1873
|
-
console.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(
|
|
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(
|
|
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/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
|
* 광고 슬롯 로드
|
|
@@ -1848,38 +1932,73 @@ class AdStageSDK {
|
|
|
1848
1932
|
...(options?.deviceType && { deviceType: options.deviceType }),
|
|
1849
1933
|
...(options?.country && { country: options.country }),
|
|
1850
1934
|
});
|
|
1851
|
-
const
|
|
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(
|
|
1977
|
+
console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
|
|
1870
1978
|
}
|
|
1871
1979
|
}
|
|
1872
1980
|
catch (error) {
|
|
1873
|
-
console.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(
|
|
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(
|
|
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.
|
|
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
|
/**
|
|
@@ -127,34 +145,61 @@ export class AdStageSDK {
|
|
|
127
145
|
...(options?.country && { country: options.country }),
|
|
128
146
|
});
|
|
129
147
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
199
|
+
console.warn(`⚠️ 슬롯 ${slot.id}에 사용 가능한 광고가 없습니다. API 응답:`, data);
|
|
155
200
|
}
|
|
156
201
|
} catch (error) {
|
|
157
|
-
console.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)
|
|
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(
|
|
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)
|
|
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(
|
|
310
|
+
console.log(`✅ 단일 광고 렌더링 완료:`, {
|
|
311
|
+
slotId: slot.id,
|
|
312
|
+
ad: ad,
|
|
313
|
+
containerContent: container.innerHTML.substring(0, 200) + '...'
|
|
314
|
+
});
|
|
229
315
|
}
|
|
230
316
|
}
|
|
231
317
|
|
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
|
}
|