안녕하세요.
수고하십니다.
광고 시청 후에 다음 광고를 위해 프리로드를 진행하도록 구현했습니다.
이전에는 잘 작동했었는데 광고 2.0 적용 후 보상광고 시청 후에 다음 광고를 로드할 때 로드 에러가 발생하고 있습니다.
에러 내용은 다음과 같습니다.
code=0, message=Internal error
전면광고는 정상적으로 로드되고 있습니다.
보상광고 로드만 문제가 발생하고 있습니다.
광고 ID는 제대로 설정한 상태이고, 테스트 광고ID와 실제 광고ID 모두 같은 문제가 발생하고 있습니다.
확인 부탁 드립니다.
// appintoss-ads.ts — 기존 기능 유지 + 이벤트 중복 전달 방지 (로드=load만 / 시청=ensure_show만)
// Toss Web Framework SDK
import { GoogleAdMob } from '@apps-in-toss/web-framework';
// ==== 타입 (기존과 동일/호환) ====
type FullScreenEvent = { type: string; data?: any };
type LoadOpts = {
adUnitId: string;
onEvent?: (e: FullScreenEvent) => void;
onError?: (e: unknown) => void;
};
// 전역 브리지 타입 (jslib가 런타임에 주입)
declare global {
interface Window {
AITAds?: {
// 광고 지원여부 확인 (캐시됨)
supported: () => { interstitial: boolean; rewarded: boolean };
// 광고 미리 로드
loadInterstitial: (opts: LoadOpts) => Promise<void>;
loadRewarded: (opts: LoadOpts) => Promise<void>;
// 로드 보장 + 즉시 표시 (표시 후 자동 프리로드)
ensureAndShowInterstitial: (opts: { adUnitId: string }) => Promise<void>;
ensureAndShowRewarded: (opts: { adUnitId: string }) => Promise<void>;
};
__AIT_SendAdEvent?: (p: {
kind: "interstitial" | "rewarded";
phase: "load" | "ensure_show";
eventType: string;
adUnitId: string;
error?: string;
responseInfo?: unknown;
}) => void;
}
}
// ==== 내부 상태(최소) ====
// - 로딩/로디드 상태: 기존 s.loaded/s.loading 사용 유지
const interstitialSlots = new Map<string, { loading: boolean; loaded: boolean; lastLoadedAt?: number }>();
const rewardedSlots = new Map<string, { loading: boolean; loaded: boolean; lastLoadedAt?: number }>();
function getSlot(kind: "interstitial" | "rewarded", adUnitId: string) {
const map = kind === "interstitial" ? interstitialSlots : rewardedSlots;
let s = map.get(adUnitId);
if (!s) {
s = { loading: false, loaded: false };
map.set(adUnitId, s);
}
return s;
}
// - 지금 표시(show) 중인지 (이벤트 phase 자동 결정용)
const showingInterstitial = new Set<string>();
const showingRewarded = new Set<string>();
// ==== 지원 여부 (캐시) ====
let _supportCache: { interstitial: boolean; rewarded: boolean } | null = null;
function supported(): { interstitial: boolean; rewarded: boolean } {
if (_supportCache) return _supportCache;
const ok = !!GoogleAdMob;
_supportCache = { interstitial: ok, rewarded: ok };
return _supportCache;
}
// ==== 프리로드 후처리 (기존 명칭 유지) ====
function markConsumed(kind: "interstitial" | "rewarded", adUnitId: string) {
const s = getSlot(kind, adUnitId);
s.loaded = false;
s.loading = false;
}
// ==== 이벤트 전송 ====
function sendEvent(
kind: "interstitial" | "rewarded",
_phase: "load" | "ensure_show", // 호출부의 인자는 무시하고 내부에서 재결정
adUnitId: string,
eventType: string,
extra?: { error?: string; responseInfo?: unknown }
) {
// 1) 이벤트 타입으로 phase 결정
let phase: "load" | "ensure_show";
switch (eventType) {
case "loaded":
phase = "load";
break;
case "requested":
case "show":
case "impression":
case "clicked":
case "dismissed":
case "failedToShow":
case "userEarnedReward":
case "done":
phase = "ensure_show";
break;
case "error":
// 에러는 로드/표시 둘 다에서 발생 가능
phase = (kind === "interstitial"
? showingInterstitial.has(adUnitId)
: showingRewarded.has(adUnitId))
? "ensure_show"
: "load";
break;
default:
// 혹시 새로운 타입이 추가돼도 표시 중이면 표시 단계로 보내는 게 안전
phase = (kind === "interstitial"
? showingInterstitial.has(adUnitId)
: showingRewarded.has(adUnitId))
? "ensure_show"
: "load";
break;
}
window.__AIT_SendAdEvent?.({
kind,
phase,
adUnitId,
eventType,
...(extra ?? {}),
});
}
// ==== 로드 ====
function doLoadInterstitial(opts: LoadOpts): Promise<void> {
const { adUnitId, onEvent, onError } = opts;
const s = getSlot("interstitial", adUnitId);
if (s.loaded) return Promise.resolve();
if (s.loading) {
return new Promise<void>((resolve) => {
const check = () => (s.loaded ? resolve() : setTimeout(check, 50));
check();
});
}
s.loading = true;
s.loaded = false;
return new Promise<void>((resolve, reject) => {
const cleanup = GoogleAdMob.loadAppsInTossAdMob({
options: { adGroupId: adUnitId } as any,
onEvent: (e: FullScreenEvent) => {
if (e.type === "loaded") {
s.loaded = true;
s.loading = false;
s.lastLoadedAt = Date.now();
resolve();
}
onEvent?.(e);
const t = String((e as any)?.type ?? "");
sendEvent("interstitial", "load", adUnitId, t);
const ri = (e as any)?.data?.responseInfo;
if (t === "loaded" && ri) {
sendEvent("interstitial", "load", adUnitId, "loaded", { responseInfo: ri });
}
},
onError: (err: unknown) => {
s.loading = false;
s.loaded = false;
reject(err);
onError?.(err);
sendEvent("interstitial", "load", adUnitId, "error", { error: String(err) });
},
});
if (typeof cleanup === "function") {
// 필요 시 정리 로직
}
});
}
function doLoadRewarded(opts: LoadOpts): Promise<void> {
const { adUnitId, onEvent, onError } = opts;
const s = getSlot("rewarded", adUnitId);
if (s.loaded) return Promise.resolve();
if (s.loading) {
return new Promise<void>((resolve) => {
const check = () => (s.loaded ? resolve() : setTimeout(check, 50));
check();
});
}
s.loading = true;
s.loaded = false;
return new Promise<void>((resolve, reject) => {
const cleanup = GoogleAdMob.loadAppsInTossAdMob({
options: { adGroupId: adUnitId } as any,
onEvent: (e: FullScreenEvent) => {
if (e.type === "loaded") {
s.loaded = true;
s.loading = false;
s.lastLoadedAt = Date.now();
resolve();
}
onEvent?.(e);
const t = String((e as any)?.type ?? "");
sendEvent("rewarded", "load", adUnitId, t);
const ri = (e as any)?.data?.responseInfo;
if (t === "loaded" && ri) {
sendEvent("rewarded", "load", adUnitId, "loaded", { responseInfo: ri });
}
},
onError: (err: unknown) => {
s.loading = false;
s.loaded = false;
reject(err);
onError?.(err);
sendEvent("rewarded", "load", adUnitId, "error", { error: String(err) });
},
});
if (typeof cleanup === "function") {
// 필요 시 정리 로직
}
});
}
// ==== 표시(Show) ====
async function doShowInterstitial(adUnitId: string): Promise<void> {
try {
showingInterstitial.add(adUnitId);
await GoogleAdMob.showAppsInTossAdMob({
options: { adGroupId: adUnitId } as any,
onEvent: (e: any) => {
const eventType = String(e?.type ?? "");
sendEvent("interstitial", "ensure_show", adUnitId, eventType);
},
onError: (e: unknown) => {
sendEvent("interstitial", "ensure_show", adUnitId, "error", { error: String(e) });
throw e;
},
});
} finally {
showingInterstitial.delete(adUnitId);
markConsumed("interstitial", adUnitId);
}
}
async function doShowRewarded(adUnitId: string): Promise<void> {
try {
showingRewarded.add(adUnitId);
await GoogleAdMob.showAppsInTossAdMob({
options: { adGroupId: adUnitId } as any,
onEvent: (e: any) => {
const eventType = String(e?.type ?? "");
sendEvent("rewarded", "ensure_show", adUnitId, eventType);
},
onError: (e: unknown) => {
sendEvent("rewarded", "ensure_show", adUnitId, "error", { error: String(e) });
throw e;
},
});
} finally {
showingRewarded.delete(adUnitId);
markConsumed("rewarded", adUnitId);
}
}
// ==== 퍼블릭 API(기존 시그니처 유지) ====
async function ensureAndShowInterstitial(opts: { adUnitId: string }): Promise<void> {
const { adUnitId } = opts;
const s = getSlot("interstitial", adUnitId);
if (!s.loaded) {
await doLoadInterstitial({ adUnitId });
}
await doShowInterstitial(adUnitId);
}
async function ensureAndShowRewarded(opts: { adUnitId: string }): Promise<void> {
const { adUnitId } = opts;
const s = getSlot("rewarded", adUnitId);
if (!s.loaded) {
await doLoadRewarded({ adUnitId });
}
await doShowRewarded(adUnitId);
}
// ==== window 등록(기존 이름/동작) ====
window.AITAds = {
supported,
loadInterstitial: (opts: LoadOpts) => doLoadInterstitial(opts),
loadRewarded: (opts: LoadOpts) => doLoadRewarded(opts),
ensureAndShowInterstitial,
ensureAndShowRewarded,
};