@ait-co/devtools 0.1.32 → 0.1.34
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/in-app/index.d.ts +68 -14
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +7 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +372 -51
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +64 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts +124 -20
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +293 -51
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +503 -67
- package/dist/panel/index.js.map +1 -1
- package/package.json +5 -3
package/dist/panel/index.js
CHANGED
|
@@ -90,6 +90,10 @@ const en = {
|
|
|
90
90
|
"device.prompt.label.lng": "Lng",
|
|
91
91
|
"device.prompt.send": "Send",
|
|
92
92
|
"device.prompt.cancel": "Cancel",
|
|
93
|
+
"device.section.haptic": "Haptic",
|
|
94
|
+
"device.haptic.lastCall": "Last haptic",
|
|
95
|
+
"device.haptic.noneYet": "(none yet)",
|
|
96
|
+
"device.haptic.trigger": "Trigger haptic",
|
|
93
97
|
"viewport.section.device": "Device",
|
|
94
98
|
"viewport.row.preset": "Preset",
|
|
95
99
|
"viewport.row.orientation": "Orientation",
|
|
@@ -125,6 +129,9 @@ const en = {
|
|
|
125
129
|
"events.row.tossLoginIntegrated": "Toss Login Integrated",
|
|
126
130
|
"analytics.section.log": "Analytics Log ({count})",
|
|
127
131
|
"analytics.btn.clear": "Clear",
|
|
132
|
+
"analytics.calls.section": "SDK Calls ({count})",
|
|
133
|
+
"analytics.calls.btn.clear": "Clear",
|
|
134
|
+
"analytics.calls.empty": "(no SDK calls yet)",
|
|
128
135
|
"storage.section.title": "Storage ({count} items)",
|
|
129
136
|
"storage.btn.clearAll": "Clear All",
|
|
130
137
|
"storage.empty": "No items in storage",
|
|
@@ -149,6 +156,13 @@ const en = {
|
|
|
149
156
|
"ads.section.fullScreenAd": "FullScreenAd",
|
|
150
157
|
"ads.btn.load": "Load",
|
|
151
158
|
"ads.btn.show": "Show",
|
|
159
|
+
"ads.section.tossAdsBanner": "TossAds Banner",
|
|
160
|
+
"ads.row.rewardUnitType": "Reward unit type",
|
|
161
|
+
"ads.row.rewardAmount": "Reward amount",
|
|
162
|
+
"ads.btn.render": "Render",
|
|
163
|
+
"ads.btn.noFill": "No-fill",
|
|
164
|
+
"ads.btn.click": "Click",
|
|
165
|
+
"ads.btn.destroy": "Destroy",
|
|
152
166
|
"notifications.section.title": "requestNotificationAgreement",
|
|
153
167
|
"notifications.option.newAgreement": "newAgreement (first-time agree)",
|
|
154
168
|
"notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
|
|
@@ -247,6 +261,10 @@ const ko = {
|
|
|
247
261
|
"device.prompt.label.lng": "Lng",
|
|
248
262
|
"device.prompt.send": "Send",
|
|
249
263
|
"device.prompt.cancel": "Cancel",
|
|
264
|
+
"device.section.haptic": "Haptic",
|
|
265
|
+
"device.haptic.lastCall": "마지막 haptic",
|
|
266
|
+
"device.haptic.noneYet": "(아직 없음)",
|
|
267
|
+
"device.haptic.trigger": "Haptic 트리거",
|
|
250
268
|
"viewport.section.device": "Device",
|
|
251
269
|
"viewport.row.preset": "Preset",
|
|
252
270
|
"viewport.row.orientation": "Orientation",
|
|
@@ -282,6 +300,9 @@ const ko = {
|
|
|
282
300
|
"events.row.tossLoginIntegrated": "Toss Login Integrated",
|
|
283
301
|
"analytics.section.log": "Analytics Log ({count})",
|
|
284
302
|
"analytics.btn.clear": "Clear",
|
|
303
|
+
"analytics.calls.section": "SDK Calls ({count})",
|
|
304
|
+
"analytics.calls.btn.clear": "Clear",
|
|
305
|
+
"analytics.calls.empty": "(아직 SDK 호출 없음)",
|
|
285
306
|
"storage.section.title": "Storage ({count} items)",
|
|
286
307
|
"storage.btn.clearAll": "Clear All",
|
|
287
308
|
"storage.empty": "저장된 항목이 없습니다",
|
|
@@ -306,6 +327,13 @@ const ko = {
|
|
|
306
327
|
"ads.section.fullScreenAd": "FullScreenAd",
|
|
307
328
|
"ads.btn.load": "Load",
|
|
308
329
|
"ads.btn.show": "Show",
|
|
330
|
+
"ads.section.tossAdsBanner": "TossAds 배너",
|
|
331
|
+
"ads.row.rewardUnitType": "리워드 단위 타입",
|
|
332
|
+
"ads.row.rewardAmount": "리워드 수량",
|
|
333
|
+
"ads.btn.render": "Render",
|
|
334
|
+
"ads.btn.noFill": "No-fill",
|
|
335
|
+
"ads.btn.click": "Click",
|
|
336
|
+
"ads.btn.destroy": "Destroy",
|
|
309
337
|
"notifications.section.title": "requestNotificationAgreement",
|
|
310
338
|
"notifications.option.newAgreement": "newAgreement (최초 동의)",
|
|
311
339
|
"notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
|
|
@@ -392,6 +420,8 @@ function t(key, vars) {
|
|
|
392
420
|
}
|
|
393
421
|
//#endregion
|
|
394
422
|
//#region src/mock/state.ts
|
|
423
|
+
/** SDK 호출 로그 ring buffer 상한 */
|
|
424
|
+
const SDK_CALL_LOG_MAX = 200;
|
|
395
425
|
const DEFAULT_STATE = {
|
|
396
426
|
platform: "ios",
|
|
397
427
|
environment: "sandbox",
|
|
@@ -469,7 +499,9 @@ const DEFAULT_STATE = {
|
|
|
469
499
|
isLoaded: false,
|
|
470
500
|
nextEvent: "loaded",
|
|
471
501
|
forceNoFill: false,
|
|
472
|
-
lastEvent: null
|
|
502
|
+
lastEvent: null,
|
|
503
|
+
rewardUnitType: "coins",
|
|
504
|
+
rewardAmount: 10
|
|
473
505
|
},
|
|
474
506
|
game: {
|
|
475
507
|
profile: {
|
|
@@ -479,6 +511,7 @@ const DEFAULT_STATE = {
|
|
|
479
511
|
leaderboardScores: []
|
|
480
512
|
},
|
|
481
513
|
analyticsLog: [],
|
|
514
|
+
sdkCallLog: [],
|
|
482
515
|
deviceModes: {
|
|
483
516
|
camera: "mock",
|
|
484
517
|
photos: "mock",
|
|
@@ -591,6 +624,19 @@ var AitStateManager = class {
|
|
|
591
624
|
};
|
|
592
625
|
this._notify();
|
|
593
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* SDK 호출 로그 추가 (ring buffer, 상한 SDK_CALL_LOG_MAX).
|
|
629
|
+
* `observe()`가 호출하고, proxy의 KNOWN_UNIMPLEMENTED 경로도 직접 호출한다.
|
|
630
|
+
*/
|
|
631
|
+
logSdkCall(entry) {
|
|
632
|
+
const log = this._state.sdkCallLog;
|
|
633
|
+
const next = log.length >= SDK_CALL_LOG_MAX ? log.slice(1 - SDK_CALL_LOG_MAX) : log;
|
|
634
|
+
this._state = {
|
|
635
|
+
...this._state,
|
|
636
|
+
sdkCallLog: [...next, entry]
|
|
637
|
+
};
|
|
638
|
+
this._notify();
|
|
639
|
+
}
|
|
594
640
|
/** 이벤트 트리거 (backEvent, homeEvent 등) */
|
|
595
641
|
trigger(event) {
|
|
596
642
|
window.dispatchEvent(new CustomEvent(`__ait:${event}`));
|
|
@@ -1063,7 +1109,7 @@ function readGlobalString(key) {
|
|
|
1063
1109
|
}
|
|
1064
1110
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
1065
1111
|
function getVersion() {
|
|
1066
|
-
return "0.1.
|
|
1112
|
+
return "0.1.34";
|
|
1067
1113
|
}
|
|
1068
1114
|
let panelVisibleSince = null;
|
|
1069
1115
|
let accumulatedMs = 0;
|
|
@@ -2073,6 +2119,75 @@ const _fetchContacts = async (options) => {
|
|
|
2073
2119
|
};
|
|
2074
2120
|
withPermission(_fetchContacts, "contacts");
|
|
2075
2121
|
//#endregion
|
|
2122
|
+
//#region src/mock/device/haptic.ts
|
|
2123
|
+
/**
|
|
2124
|
+
* Haptic Feedback & saveBase64Data mock
|
|
2125
|
+
*
|
|
2126
|
+
* generateHapticFeedback — 영역 3 (하드웨어 API 관측):
|
|
2127
|
+
* - 10종 HapticFeedbackType을 navigator.vibrate 패턴으로 매핑(근사, best-effort).
|
|
2128
|
+
* - `typeof navigator.vibrate === 'function'` 가드 — API 없는 환경에서 throw 없이 skip.
|
|
2129
|
+
* - sdkCallLog에 🟡(partial)로 기록. params: { hapticType, vibrated: boolean }.
|
|
2130
|
+
* - 시그니처 불변 — __typecheck.ts의 Assert<Mock, Original> 통과.
|
|
2131
|
+
*/
|
|
2132
|
+
/**
|
|
2133
|
+
* HapticFeedbackType 10종 → navigator.vibrate 패턴 매핑.
|
|
2134
|
+
* 숫자: 진동 ms. 배열: [진동, 정지, 진동, …] 교대 패턴.
|
|
2135
|
+
*/
|
|
2136
|
+
const HAPTIC_VIBRATE_PATTERN = {
|
|
2137
|
+
tickWeak: 10,
|
|
2138
|
+
tap: 20,
|
|
2139
|
+
tickMedium: 30,
|
|
2140
|
+
softMedium: 40,
|
|
2141
|
+
basicWeak: 15,
|
|
2142
|
+
basicMedium: 50,
|
|
2143
|
+
success: [
|
|
2144
|
+
10,
|
|
2145
|
+
40,
|
|
2146
|
+
10
|
|
2147
|
+
],
|
|
2148
|
+
error: [
|
|
2149
|
+
40,
|
|
2150
|
+
30,
|
|
2151
|
+
40
|
|
2152
|
+
],
|
|
2153
|
+
wiggle: [
|
|
2154
|
+
20,
|
|
2155
|
+
20,
|
|
2156
|
+
20,
|
|
2157
|
+
20,
|
|
2158
|
+
20
|
|
2159
|
+
],
|
|
2160
|
+
confetti: [
|
|
2161
|
+
10,
|
|
2162
|
+
20,
|
|
2163
|
+
10,
|
|
2164
|
+
20,
|
|
2165
|
+
10,
|
|
2166
|
+
20,
|
|
2167
|
+
10
|
|
2168
|
+
]
|
|
2169
|
+
};
|
|
2170
|
+
async function generateHapticFeedback(options) {
|
|
2171
|
+
const timestamp = Date.now();
|
|
2172
|
+
aitState.logAnalytics({
|
|
2173
|
+
type: "haptic",
|
|
2174
|
+
params: { hapticType: options.type }
|
|
2175
|
+
});
|
|
2176
|
+
const pattern = HAPTIC_VIBRATE_PATTERN[options.type] ?? 30;
|
|
2177
|
+
const vibrated = typeof navigator.vibrate === "function" ? navigator.vibrate(pattern) : false;
|
|
2178
|
+
aitState.logSdkCall({
|
|
2179
|
+
method: "generateHapticFeedback",
|
|
2180
|
+
args: [{ type: options.type }],
|
|
2181
|
+
timestamp,
|
|
2182
|
+
status: "resolved",
|
|
2183
|
+
result: {
|
|
2184
|
+
hapticType: options.type,
|
|
2185
|
+
vibrated
|
|
2186
|
+
},
|
|
2187
|
+
fidelity: "partial"
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
//#endregion
|
|
2076
2191
|
//#region src/mock/device/location.ts
|
|
2077
2192
|
/**
|
|
2078
2193
|
* Location mock (getCurrentLocation, startUpdateLocation)
|
|
@@ -2184,12 +2299,35 @@ withPermission(_startUpdateLocation, "geolocation");
|
|
|
2184
2299
|
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
2185
2300
|
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
2186
2301
|
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
2302
|
+
*
|
|
2303
|
+
* ## KNOWN_UNIMPLEMENTED 정책
|
|
2304
|
+
* SDK에 존재하는 것으로 알려져 있으나 현재 mock이 없는 API 이름만 이 집합에 둔다.
|
|
2305
|
+
* 이 경우에만 throw 대신 🔴 inert no-op을 반환하고 sdkCallLog에 기록한다.
|
|
2306
|
+
* 완전히 미지의 이름은 여전히 throw — "잘 되는 척" 방지.
|
|
2187
2307
|
*/
|
|
2188
2308
|
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
2309
|
+
/**
|
|
2310
|
+
* SDK에 존재하나 mock이 아직 없는 것으로 확인된 이름 목록.
|
|
2311
|
+
* 새 API가 SDK에 추가되면 여기에 추가하고 별도 PR에서 mock 구현으로 이동한다.
|
|
2312
|
+
* 확인되지 않은 이름은 절대 여기에 추가하지 않는다 — throw가 더 안전하다.
|
|
2313
|
+
*/
|
|
2314
|
+
const KNOWN_UNIMPLEMENTED = /* @__PURE__ */ new Set([]);
|
|
2189
2315
|
function createMockProxy(moduleName, implementations) {
|
|
2190
2316
|
return new Proxy(implementations, { get(target, prop) {
|
|
2191
2317
|
if (typeof prop === "symbol") return void 0;
|
|
2192
2318
|
if (prop in target) return target[prop];
|
|
2319
|
+
const name = String(prop);
|
|
2320
|
+
if (KNOWN_UNIMPLEMENTED.has(name)) return (...args) => {
|
|
2321
|
+
console.warn(`[@ait-co/devtools] ${moduleName}.${name} is known-unimplemented (🔴 inert). Returning undefined. Please file or upvote an issue: ${ISSUES_URL}`);
|
|
2322
|
+
aitState.logSdkCall({
|
|
2323
|
+
method: `${moduleName}.${name}`,
|
|
2324
|
+
args,
|
|
2325
|
+
timestamp: Date.now(),
|
|
2326
|
+
status: "resolved",
|
|
2327
|
+
result: void 0,
|
|
2328
|
+
fidelity: "inert"
|
|
2329
|
+
});
|
|
2330
|
+
};
|
|
2193
2331
|
throw new Error(`[@ait-co/devtools] ${moduleName}.${prop} is not mocked. This API may exist in @apps-in-toss/web-framework, but devtools' mock does not cover it yet. Please file an issue: ${ISSUES_URL}`);
|
|
2194
2332
|
} });
|
|
2195
2333
|
}
|
|
@@ -2397,19 +2535,127 @@ function renderDeviceTab() {
|
|
|
2397
2535
|
});
|
|
2398
2536
|
if (disabled) clearImagesBtn.disabled = true;
|
|
2399
2537
|
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("device.section.mockImages", { count: images.length })), imageGrid, h("div", { className: "ait-btn-row" }, addBtn, defaultsBtn, clearImagesBtn)));
|
|
2538
|
+
container.appendChild(renderHapticSection());
|
|
2400
2539
|
return container;
|
|
2401
2540
|
}
|
|
2541
|
+
function renderHapticSection() {
|
|
2542
|
+
const lastResult = [...aitState.state.sdkCallLog].reverse().find((e) => e.method === "generateHapticFeedback")?.result;
|
|
2543
|
+
const lastCallValue = lastResult ? `${lastResult.hapticType} (vibrated: ${String(lastResult.vibrated)})` : t("device.haptic.noneYet");
|
|
2544
|
+
const lastCallRow = h("div", { className: "ait-row" }, h("label", {}, t("device.haptic.lastCall")), h("span", { className: "ait-value" }, lastCallValue));
|
|
2545
|
+
const hapticTypes = Object.keys(HAPTIC_VIBRATE_PATTERN);
|
|
2546
|
+
const triggerSection = h("div", { className: "ait-section-subtitle" }, t("device.haptic.trigger"));
|
|
2547
|
+
const btnRow = h("div", { className: "ait-btn-row" }, ...hapticTypes.map((type) => {
|
|
2548
|
+
const btn = h("button", {
|
|
2549
|
+
className: "ait-btn-secondary",
|
|
2550
|
+
"data-testid": `haptic-${type}-btn`
|
|
2551
|
+
}, type);
|
|
2552
|
+
btn.addEventListener("click", () => {
|
|
2553
|
+
generateHapticFeedback({ type }).then(() => {
|
|
2554
|
+
refreshPanel$1();
|
|
2555
|
+
});
|
|
2556
|
+
});
|
|
2557
|
+
return btn;
|
|
2558
|
+
}));
|
|
2559
|
+
return h("div", {
|
|
2560
|
+
className: "ait-section",
|
|
2561
|
+
"data-testid": "section-haptic"
|
|
2562
|
+
}, h("div", { className: "ait-section-title" }, t("device.section.haptic")), lastCallRow, triggerSection, btnRow);
|
|
2563
|
+
}
|
|
2564
|
+
//#endregion
|
|
2565
|
+
//#region src/mock/observe.ts
|
|
2566
|
+
/**
|
|
2567
|
+
* fn을 observe로 감싼다.
|
|
2568
|
+
*
|
|
2569
|
+
* @param apiName - 로그에 기록할 SDK 메서드 이름 (예: `'setScreenAwakeMode'`)
|
|
2570
|
+
* @param fidelity - 이 mock의 fidelity grade ('faithful' | 'partial' | 'inert')
|
|
2571
|
+
* @param fn - 실제 mock 구현체. 시그니처를 그대로 통과시킨다.
|
|
2572
|
+
* @returns fn과 동일한 타입의 래퍼 함수
|
|
2573
|
+
*/
|
|
2574
|
+
function observe(apiName, fidelity, fn) {
|
|
2575
|
+
return (...args) => {
|
|
2576
|
+
const timestamp = Date.now();
|
|
2577
|
+
const safeArgs = args.map((a) => safeSerialize(a));
|
|
2578
|
+
const result = fn(...args);
|
|
2579
|
+
if (result instanceof Promise) {
|
|
2580
|
+
aitState.logSdkCall({
|
|
2581
|
+
method: apiName,
|
|
2582
|
+
args: safeArgs,
|
|
2583
|
+
timestamp,
|
|
2584
|
+
status: "pending",
|
|
2585
|
+
fidelity
|
|
2586
|
+
});
|
|
2587
|
+
result.then((value) => {
|
|
2588
|
+
aitState.logSdkCall({
|
|
2589
|
+
method: apiName,
|
|
2590
|
+
args: safeArgs,
|
|
2591
|
+
timestamp,
|
|
2592
|
+
status: "resolved",
|
|
2593
|
+
result: safeSerialize(value),
|
|
2594
|
+
fidelity
|
|
2595
|
+
});
|
|
2596
|
+
}, (err) => {
|
|
2597
|
+
aitState.logSdkCall({
|
|
2598
|
+
method: apiName,
|
|
2599
|
+
args: safeArgs,
|
|
2600
|
+
timestamp,
|
|
2601
|
+
status: "rejected",
|
|
2602
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2603
|
+
fidelity
|
|
2604
|
+
});
|
|
2605
|
+
});
|
|
2606
|
+
return result;
|
|
2607
|
+
}
|
|
2608
|
+
aitState.logSdkCall({
|
|
2609
|
+
method: apiName,
|
|
2610
|
+
args: safeArgs,
|
|
2611
|
+
timestamp,
|
|
2612
|
+
status: "resolved",
|
|
2613
|
+
result: safeSerialize(result),
|
|
2614
|
+
fidelity
|
|
2615
|
+
});
|
|
2616
|
+
return result;
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* 값을 JSON-safe한 형태로 변환한다.
|
|
2621
|
+
* - null / primitive — 그대로.
|
|
2622
|
+
* - 함수 — `'[Function: name]'` 문자열.
|
|
2623
|
+
* - 기타 객체 — JSON.stringify 실패 시 `'[unserializable]'`.
|
|
2624
|
+
*/
|
|
2625
|
+
function safeSerialize(value) {
|
|
2626
|
+
if (value === null || value === void 0) return value;
|
|
2627
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
2628
|
+
if (typeof value !== "object") return value;
|
|
2629
|
+
try {
|
|
2630
|
+
return JSON.parse(JSON.stringify(value));
|
|
2631
|
+
} catch {
|
|
2632
|
+
return "[unserializable]";
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2402
2635
|
//#endregion
|
|
2403
2636
|
//#region src/mock/ads/index.ts
|
|
2404
2637
|
/**
|
|
2405
2638
|
* 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
|
|
2639
|
+
*
|
|
2640
|
+
* 변경 이력 (#196):
|
|
2641
|
+
* - slot 레지스트리로 TossAds destroy/destroyAll 누수 수정 (🟡→🟢)
|
|
2642
|
+
* - attachBanner BannerSlotCallbacks 발화 (onAdRendered/onAdImpression/onNoFill 등)
|
|
2643
|
+
* - initialize onInitialized/onInitializationFailed 발화
|
|
2644
|
+
* - AdMob reward 파라미터화 (state.ads.rewardUnitType/rewardAmount)
|
|
2645
|
+
* - 모든 호출 observe()로 sdkCallLog에 기록
|
|
2406
2646
|
*/
|
|
2407
2647
|
function withIsSupported(fn) {
|
|
2408
2648
|
fn.isSupported = () => true;
|
|
2409
2649
|
return fn;
|
|
2410
2650
|
}
|
|
2651
|
+
const _slotRegistry = /* @__PURE__ */ new Map();
|
|
2652
|
+
let _slotCounter = 0;
|
|
2653
|
+
function _nextSlotId(adGroupId) {
|
|
2654
|
+
_slotCounter += 1;
|
|
2655
|
+
return `mock-slot-${adGroupId}-${_slotCounter}`;
|
|
2656
|
+
}
|
|
2411
2657
|
const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
2412
|
-
loadAppsInTossAdMob: withIsSupported((args) => {
|
|
2658
|
+
loadAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.loadAppsInTossAdMob", "faithful", (args) => {
|
|
2413
2659
|
setTimeout(() => {
|
|
2414
2660
|
if (aitState.state.ads.forceNoFill) {
|
|
2415
2661
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -2422,8 +2668,8 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
2422
2668
|
});
|
|
2423
2669
|
}, 200);
|
|
2424
2670
|
return () => {};
|
|
2425
|
-
}),
|
|
2426
|
-
showAppsInTossAdMob: withIsSupported((args) => {
|
|
2671
|
+
})),
|
|
2672
|
+
showAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.showAppsInTossAdMob", "faithful", (args) => {
|
|
2427
2673
|
if (!aitState.state.ads.isLoaded) {
|
|
2428
2674
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
2429
2675
|
return () => {};
|
|
@@ -2432,11 +2678,12 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
2432
2678
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
2433
2679
|
setTimeout(() => args.onEvent({ type: "impression" }), 150);
|
|
2434
2680
|
setTimeout(() => {
|
|
2681
|
+
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
2435
2682
|
args.onEvent({
|
|
2436
2683
|
type: "userEarnedReward",
|
|
2437
2684
|
data: {
|
|
2438
|
-
unitType:
|
|
2439
|
-
unitAmount:
|
|
2685
|
+
unitType: rewardUnitType,
|
|
2686
|
+
unitAmount: rewardAmount
|
|
2440
2687
|
}
|
|
2441
2688
|
});
|
|
2442
2689
|
}, 1e3);
|
|
@@ -2445,14 +2692,18 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
2445
2692
|
aitState.patch("ads", { isLoaded: false });
|
|
2446
2693
|
}, 1500);
|
|
2447
2694
|
return () => {};
|
|
2448
|
-
}),
|
|
2449
|
-
isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
|
|
2695
|
+
})),
|
|
2696
|
+
isAppsInTossAdMobLoaded: withIsSupported(observe("GoogleAdMob.isAppsInTossAdMobLoaded", "faithful", async (_options) => aitState.state.ads.isLoaded))
|
|
2450
2697
|
});
|
|
2451
|
-
createMockProxy("TossAds", {
|
|
2452
|
-
initialize: withIsSupported((
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2698
|
+
const TossAds = createMockProxy("TossAds", {
|
|
2699
|
+
initialize: withIsSupported(observe("TossAds.initialize", "partial", (options) => {
|
|
2700
|
+
if (aitState.state.ads.forceNoFill) {
|
|
2701
|
+
options.callbacks?.onInitializationFailed?.(/* @__PURE__ */ new Error("No fill"));
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
options.callbacks?.onInitialized?.();
|
|
2705
|
+
})),
|
|
2706
|
+
attach: withIsSupported(observe("TossAds.attach", "partial", (_adGroupId, target, _options) => {
|
|
2456
2707
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
2457
2708
|
if (el) {
|
|
2458
2709
|
const placeholder = document.createElement("div");
|
|
@@ -2460,21 +2711,76 @@ createMockProxy("TossAds", {
|
|
|
2460
2711
|
placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
|
|
2461
2712
|
el.appendChild(placeholder);
|
|
2462
2713
|
}
|
|
2463
|
-
}),
|
|
2464
|
-
attachBanner: withIsSupported((
|
|
2714
|
+
})),
|
|
2715
|
+
attachBanner: withIsSupported(observe("TossAds.attachBanner", "faithful", (adGroupId, target, options) => {
|
|
2465
2716
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
2717
|
+
const slotId = _nextSlotId(adGroupId);
|
|
2718
|
+
const placeholder = document.createElement("div");
|
|
2719
|
+
const theme = options?.theme ?? "auto";
|
|
2720
|
+
const variant = options?.variant ?? "card";
|
|
2721
|
+
const isDark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
2722
|
+
const bg = isDark ? "#1a1a1a" : "#f0f0f0";
|
|
2723
|
+
const textColor = isDark ? "#aaa" : "#666";
|
|
2724
|
+
const borderColor = isDark ? "#555" : "#999";
|
|
2725
|
+
const height = variant === "expanded" ? "120px" : "60px";
|
|
2726
|
+
placeholder.dataset.aitSlotId = slotId;
|
|
2727
|
+
placeholder.style.cssText = `background:${bg};border:1px dashed ${borderColor};padding:8px 12px;text-align:center;color:${textColor};font-size:12px;min-height:${height};display:flex;align-items:center;justify-content:center;`;
|
|
2728
|
+
placeholder.textContent = `[@ait-co/devtools] Banner Ad (${variant})`;
|
|
2466
2729
|
if (el) {
|
|
2467
|
-
const placeholder = document.createElement("div");
|
|
2468
|
-
placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
|
|
2469
|
-
placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
|
|
2470
2730
|
el.appendChild(placeholder);
|
|
2731
|
+
_slotRegistry.set(slotId, placeholder);
|
|
2732
|
+
}
|
|
2733
|
+
const destroySlot = () => {
|
|
2734
|
+
const registered = _slotRegistry.get(slotId);
|
|
2735
|
+
if (registered) {
|
|
2736
|
+
registered.remove();
|
|
2737
|
+
_slotRegistry.delete(slotId);
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
setTimeout(() => {
|
|
2741
|
+
if (aitState.state.ads.forceNoFill) {
|
|
2742
|
+
options?.callbacks?.onNoFill?.({
|
|
2743
|
+
slotId,
|
|
2744
|
+
adGroupId,
|
|
2745
|
+
adMetadata: {}
|
|
2746
|
+
});
|
|
2747
|
+
options?.callbacks?.onAdFailedToRender?.({
|
|
2748
|
+
slotId,
|
|
2749
|
+
adGroupId,
|
|
2750
|
+
adMetadata: {},
|
|
2751
|
+
error: {
|
|
2752
|
+
code: 0,
|
|
2753
|
+
message: "No fill"
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const eventPayload = {
|
|
2759
|
+
slotId,
|
|
2760
|
+
adGroupId,
|
|
2761
|
+
adMetadata: {
|
|
2762
|
+
creativeId: `mock-creative-${slotId}`,
|
|
2763
|
+
requestId: `mock-req-${slotId}`
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
options?.callbacks?.onAdRendered?.(eventPayload);
|
|
2767
|
+
options?.callbacks?.onAdImpression?.(eventPayload);
|
|
2768
|
+
}, 100);
|
|
2769
|
+
return { destroy: destroySlot };
|
|
2770
|
+
})),
|
|
2771
|
+
destroy: withIsSupported(observe("TossAds.destroy", "faithful", (slotId) => {
|
|
2772
|
+
const el = _slotRegistry.get(slotId);
|
|
2773
|
+
if (el) {
|
|
2774
|
+
el.remove();
|
|
2775
|
+
_slotRegistry.delete(slotId);
|
|
2471
2776
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2777
|
+
})),
|
|
2778
|
+
destroyAll: withIsSupported(observe("TossAds.destroyAll", "faithful", () => {
|
|
2779
|
+
for (const el of _slotRegistry.values()) el.remove();
|
|
2780
|
+
_slotRegistry.clear();
|
|
2781
|
+
}))
|
|
2476
2782
|
});
|
|
2477
|
-
const loadFullScreenAd = withIsSupported((args) => {
|
|
2783
|
+
const loadFullScreenAd = withIsSupported(observe("loadFullScreenAd", "faithful", (args) => {
|
|
2478
2784
|
setTimeout(() => {
|
|
2479
2785
|
if (aitState.state.ads.forceNoFill) {
|
|
2480
2786
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -2487,8 +2793,8 @@ const loadFullScreenAd = withIsSupported((args) => {
|
|
|
2487
2793
|
});
|
|
2488
2794
|
}, 200);
|
|
2489
2795
|
return () => {};
|
|
2490
|
-
});
|
|
2491
|
-
const showFullScreenAd = withIsSupported((args) => {
|
|
2796
|
+
}));
|
|
2797
|
+
const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful", (args) => {
|
|
2492
2798
|
if (!aitState.state.ads.isLoaded) {
|
|
2493
2799
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
2494
2800
|
return () => {};
|
|
@@ -2496,7 +2802,7 @@ const showFullScreenAd = withIsSupported((args) => {
|
|
|
2496
2802
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
2497
2803
|
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
2498
2804
|
return () => {};
|
|
2499
|
-
});
|
|
2805
|
+
}));
|
|
2500
2806
|
//#endregion
|
|
2501
2807
|
//#region src/panel/tabs/ads.ts
|
|
2502
2808
|
function recordEvent(type) {
|
|
@@ -2531,6 +2837,57 @@ function adSection(title, onLoad, onShow, disabled) {
|
|
|
2531
2837
|
showBtn.addEventListener("click", onShow);
|
|
2532
2838
|
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, title), h("div", { className: "ait-btn-row" }, loadBtn, showBtn));
|
|
2533
2839
|
}
|
|
2840
|
+
/** TossAds 배너 인터랙티브 섹션 — Render/No-fill/Click/Destroy 버튼 */
|
|
2841
|
+
function tossAdsBannerSection(disabled) {
|
|
2842
|
+
const mountTarget = h("div", { style: "min-height:60px;background:#111;border-radius:4px;margin-bottom:6px;overflow:hidden;" });
|
|
2843
|
+
const renderBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.render"));
|
|
2844
|
+
const noFillBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.noFill"));
|
|
2845
|
+
const clickBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.click"));
|
|
2846
|
+
const destroyBtn = h("button", { className: "ait-btn ait-btn-sm" }, t("ads.btn.destroy"));
|
|
2847
|
+
if (disabled) {
|
|
2848
|
+
renderBtn.disabled = true;
|
|
2849
|
+
noFillBtn.disabled = true;
|
|
2850
|
+
clickBtn.disabled = true;
|
|
2851
|
+
destroyBtn.disabled = true;
|
|
2852
|
+
}
|
|
2853
|
+
let currentHandle = null;
|
|
2854
|
+
renderBtn.addEventListener("click", () => {
|
|
2855
|
+
currentHandle?.destroy();
|
|
2856
|
+
currentHandle = null;
|
|
2857
|
+
mountTarget.innerHTML = "";
|
|
2858
|
+
currentHandle = TossAds.attachBanner("mock-banner-group", mountTarget, {
|
|
2859
|
+
theme: "auto",
|
|
2860
|
+
variant: "card",
|
|
2861
|
+
callbacks: {
|
|
2862
|
+
onAdRendered: (p) => recordEvent(`onAdRendered(${p.slotId})`),
|
|
2863
|
+
onAdImpression: (p) => recordEvent(`onAdImpression(${p.slotId})`),
|
|
2864
|
+
onAdClicked: (p) => recordEvent(`onAdClicked(${p.slotId})`),
|
|
2865
|
+
onAdFailedToRender: (p) => recordEvent(`error: onAdFailedToRender(${p.slotId})`),
|
|
2866
|
+
onNoFill: (p) => recordEvent(`error: onNoFill(${p.slotId})`)
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
});
|
|
2870
|
+
noFillBtn.addEventListener("click", () => {
|
|
2871
|
+
currentHandle?.destroy();
|
|
2872
|
+
currentHandle = null;
|
|
2873
|
+
mountTarget.innerHTML = "";
|
|
2874
|
+
const slotId = `mock-slot-no-fill-${Date.now()}`;
|
|
2875
|
+
recordEvent(`error: onNoFill(${slotId})`);
|
|
2876
|
+
recordEvent(`error: onAdFailedToRender(${slotId})`);
|
|
2877
|
+
});
|
|
2878
|
+
clickBtn.addEventListener("click", () => {
|
|
2879
|
+
recordEvent("banner:clicked");
|
|
2880
|
+
});
|
|
2881
|
+
destroyBtn.addEventListener("click", () => {
|
|
2882
|
+
if (currentHandle) {
|
|
2883
|
+
currentHandle.destroy();
|
|
2884
|
+
currentHandle = null;
|
|
2885
|
+
mountTarget.innerHTML = "";
|
|
2886
|
+
recordEvent("banner:destroyed");
|
|
2887
|
+
} else recordError("No banner to destroy");
|
|
2888
|
+
});
|
|
2889
|
+
return h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("ads.section.tossAdsBanner")), mountTarget, h("div", { className: "ait-btn-row" }, renderBtn, noFillBtn, clickBtn, destroyBtn));
|
|
2890
|
+
}
|
|
2534
2891
|
function renderAdsTab() {
|
|
2535
2892
|
const s = aitState.state;
|
|
2536
2893
|
const disabled = !s.panelEditable;
|
|
@@ -2545,7 +2902,10 @@ function renderAdsTab() {
|
|
|
2545
2902
|
forceNoFillCb.addEventListener("change", () => {
|
|
2546
2903
|
aitState.patch("ads", { forceNoFill: forceNoFillCb.checked });
|
|
2547
2904
|
});
|
|
2548
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("ads.section.state")), statusRow(t("ads.row.isLoaded"), String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, t("ads.row.forceNoFill")), forceNoFillCb),
|
|
2905
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-section-title" }, t("ads.section.state")), statusRow(t("ads.row.isLoaded"), String(s.ads.isLoaded)), h("div", { className: "ait-row" }, h("label", {}, t("ads.row.forceNoFill")), forceNoFillCb), inputRow(t("ads.row.rewardUnitType"), s.ads.rewardUnitType, (v) => aitState.patch("ads", { rewardUnitType: v }), disabled), inputRow(t("ads.row.rewardAmount"), String(s.ads.rewardAmount), (v) => {
|
|
2906
|
+
const n = Number(v);
|
|
2907
|
+
if (!Number.isNaN(n)) aitState.patch("ads", { rewardAmount: n });
|
|
2908
|
+
}, disabled), lastEventLine()), adSection(t("ads.section.googleAdMob"), () => {
|
|
2549
2909
|
GoogleAdMob.loadAppsInTossAdMob({
|
|
2550
2910
|
onEvent: (e) => recordEvent(e.type),
|
|
2551
2911
|
onError: (err) => recordError(err.message)
|
|
@@ -2556,12 +2916,13 @@ function renderAdsTab() {
|
|
|
2556
2916
|
onError: (err) => recordError(err.message)
|
|
2557
2917
|
});
|
|
2558
2918
|
}, disabled), adSection(t("ads.section.tossAds"), () => {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2919
|
+
TossAds.initialize({ callbacks: {
|
|
2920
|
+
onInitialized: () => {
|
|
2921
|
+
aitState.patch("ads", { isLoaded: true });
|
|
2922
|
+
recordEvent("loaded");
|
|
2923
|
+
},
|
|
2924
|
+
onInitializationFailed: (err) => recordError(err.message)
|
|
2925
|
+
} });
|
|
2565
2926
|
}, () => {
|
|
2566
2927
|
if (!aitState.state.ads.isLoaded) {
|
|
2567
2928
|
recordError("Ad not loaded");
|
|
@@ -2572,7 +2933,7 @@ function renderAdsTab() {
|
|
|
2572
2933
|
recordEvent("dismissed");
|
|
2573
2934
|
aitState.patch("ads", { isLoaded: false });
|
|
2574
2935
|
}, 1500);
|
|
2575
|
-
}, disabled), adSection(t("ads.section.fullScreenAd"), () => {
|
|
2936
|
+
}, disabled), tossAdsBannerSection(disabled), adSection(t("ads.section.fullScreenAd"), () => {
|
|
2576
2937
|
loadFullScreenAd({
|
|
2577
2938
|
onEvent: (e) => recordEvent(e.type),
|
|
2578
2939
|
onError: (err) => recordError(err.message)
|
|
@@ -2587,19 +2948,37 @@ function renderAdsTab() {
|
|
|
2587
2948
|
}
|
|
2588
2949
|
//#endregion
|
|
2589
2950
|
//#region src/panel/tabs/analytics.ts
|
|
2951
|
+
const FIDELITY_BADGE = {
|
|
2952
|
+
faithful: "🟢",
|
|
2953
|
+
partial: "🟡",
|
|
2954
|
+
inert: "🔴"
|
|
2955
|
+
};
|
|
2590
2956
|
function renderAnalyticsTab() {
|
|
2591
2957
|
const disabled = !aitState.state.panelEditable;
|
|
2592
2958
|
const container = h("div");
|
|
2593
2959
|
if (disabled) container.appendChild(monitoringNotice());
|
|
2594
2960
|
const logs = aitState.state.analyticsLog;
|
|
2595
|
-
const
|
|
2596
|
-
if (disabled)
|
|
2597
|
-
|
|
2961
|
+
const clearAnalyticsBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("analytics.btn.clear"));
|
|
2962
|
+
if (disabled) clearAnalyticsBtn.disabled = true;
|
|
2963
|
+
clearAnalyticsBtn.addEventListener("click", () => {
|
|
2598
2964
|
aitState.update({ analyticsLog: [] });
|
|
2599
2965
|
});
|
|
2600
|
-
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("analytics.section.log", { count: logs.length })),
|
|
2966
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("analytics.section.log", { count: logs.length })), clearAnalyticsBtn), ...logs.slice(-30).reverse().map((entry) => {
|
|
2601
2967
|
return h("div", { className: "ait-log-entry" }, h("span", { className: "ait-log-time" }, new Date(entry.timestamp).toLocaleTimeString()), h("span", { className: "ait-log-type" }, entry.type), JSON.stringify(entry.params));
|
|
2602
2968
|
})));
|
|
2969
|
+
const calls = aitState.state.sdkCallLog;
|
|
2970
|
+
const clearCallsBtn = h("button", { className: "ait-btn ait-btn-sm ait-btn-danger" }, t("analytics.calls.btn.clear"));
|
|
2971
|
+
if (disabled) clearCallsBtn.disabled = true;
|
|
2972
|
+
clearCallsBtn.addEventListener("click", () => {
|
|
2973
|
+
aitState.update({ sdkCallLog: [] });
|
|
2974
|
+
});
|
|
2975
|
+
container.append(h("div", { className: "ait-section" }, h("div", { className: "ait-row" }, h("div", { className: "ait-section-title" }, t("analytics.calls.section", { count: calls.length })), clearCallsBtn), calls.length === 0 ? h("div", { className: "ait-log-entry ait-log-empty" }, t("analytics.calls.empty")) : h("div", {}, ...calls.slice(-50).reverse().map((call) => {
|
|
2976
|
+
const badge = FIDELITY_BADGE[call.fidelity] ?? "⬜";
|
|
2977
|
+
const time = new Date(call.timestamp).toLocaleTimeString();
|
|
2978
|
+
const argsStr = call.args.length > 0 ? `(${call.args.map((a) => JSON.stringify(a)).join(", ")})` : "()";
|
|
2979
|
+
const statusSuffix = call.status === "rejected" ? ` ✗ ${call.error ?? "error"}` : call.status === "pending" ? " …" : "";
|
|
2980
|
+
return h("div", { className: `ait-log-entry ait-sdk-call ait-sdk-call-${call.fidelity}` }, h("span", { className: "ait-log-badge" }, badge), h("span", { className: "ait-log-method" }, call.method), h("span", { className: "ait-log-args" }, argsStr), h("span", { className: "ait-log-time" }, ` · ${time}`), statusSuffix ? h("span", { className: "ait-log-status" }, statusSuffix) : "");
|
|
2981
|
+
}))));
|
|
2603
2982
|
return container;
|
|
2604
2983
|
}
|
|
2605
2984
|
//#endregion
|
|
@@ -3341,9 +3720,17 @@ async function closeView() {
|
|
|
3341
3720
|
console.log("[@ait-co/devtools] closeView called");
|
|
3342
3721
|
window.history.back();
|
|
3343
3722
|
}
|
|
3344
|
-
async
|
|
3723
|
+
observe("setScreenAwakeMode", "inert", async (options) => {
|
|
3724
|
+
console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
|
|
3725
|
+
return { enabled: options.enabled };
|
|
3726
|
+
});
|
|
3727
|
+
observe("setSecureScreen", "inert", async (options) => {
|
|
3728
|
+
console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
|
|
3729
|
+
return { enabled: options.enabled };
|
|
3730
|
+
});
|
|
3731
|
+
const requestReview = observe("requestReview", "inert", async () => {
|
|
3345
3732
|
console.log("[@ait-co/devtools] requestReview called");
|
|
3346
|
-
}
|
|
3733
|
+
});
|
|
3347
3734
|
requestReview.isSupported = () => true;
|
|
3348
3735
|
async function getServerTime() {
|
|
3349
3736
|
return Date.now();
|
|
@@ -3473,6 +3860,20 @@ const NONE_PRESET = {
|
|
|
3473
3860
|
navBarHeight: 0,
|
|
3474
3861
|
safeAreaBottom: 0
|
|
3475
3862
|
};
|
|
3863
|
+
const CUSTOM_PRESET = {
|
|
3864
|
+
id: "custom",
|
|
3865
|
+
label: "Custom",
|
|
3866
|
+
width: 0,
|
|
3867
|
+
height: 0,
|
|
3868
|
+
dpr: 1,
|
|
3869
|
+
notch: "none",
|
|
3870
|
+
notchInset: 0,
|
|
3871
|
+
navBarHeight: 0,
|
|
3872
|
+
safeAreaBottom: 0
|
|
3873
|
+
};
|
|
3874
|
+
/** Shorthands used when building preset provenance entries. */
|
|
3875
|
+
const EXTRAPOLATED = { source: "extrapolated" };
|
|
3876
|
+
const PLACEHOLDER = { source: "placeholder" };
|
|
3476
3877
|
/**
|
|
3477
3878
|
* Device presets (2026). CSS viewport 크기는 실제 기기의 `window.innerWidth/innerHeight`.
|
|
3478
3879
|
* iPhone 17 시리즈는 2025-09 출시. iPhone Air는 2026-04 출시.
|
|
@@ -3493,6 +3894,13 @@ const NONE_PRESET = {
|
|
|
3493
3894
|
*
|
|
3494
3895
|
* iPhone 17과 17 Pro는 CSS viewport / DPR / safe area가 동일 — 이는 의도이며 카피-페이스트
|
|
3495
3896
|
* 실수가 아니다. Apple의 17 lineup은 base와 Pro의 web-relevant 스펙이 같다.
|
|
3897
|
+
*
|
|
3898
|
+
* safeAreaProvenance: 각 preset의 safe-area 값 신뢰도 출처.
|
|
3899
|
+
* - `measured` — relay 실기기 세션(measure_safe_area)으로 직접 확인한 값.
|
|
3900
|
+
* 현재 iPhone 15 Pro portrait iOS partner만 해당 (devtools#190).
|
|
3901
|
+
* - `extrapolated` — Apple 스펙/같은 시리즈 기기에서 유추한 값.
|
|
3902
|
+
* - `placeholder` — 연결 기기 없이 추정한 값. QA ground truth로 쓰지 말 것.
|
|
3903
|
+
* `measure_safe_area` MCP 툴로 relay 세션에서 `measured`로 승급 필요.
|
|
3496
3904
|
*/
|
|
3497
3905
|
const VIEWPORT_PRESETS = [
|
|
3498
3906
|
NONE_PRESET,
|
|
@@ -3505,7 +3913,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3505
3913
|
notch: "none",
|
|
3506
3914
|
notchInset: 20,
|
|
3507
3915
|
navBarHeight: 54,
|
|
3508
|
-
safeAreaBottom: 0
|
|
3916
|
+
safeAreaBottom: 0,
|
|
3917
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3509
3918
|
},
|
|
3510
3919
|
{
|
|
3511
3920
|
id: "iphone-15-pro",
|
|
@@ -3516,7 +3925,12 @@ const VIEWPORT_PRESETS = [
|
|
|
3516
3925
|
notch: "dynamic-island",
|
|
3517
3926
|
notchInset: 59,
|
|
3518
3927
|
navBarHeight: 54,
|
|
3519
|
-
safeAreaBottom: 34
|
|
3928
|
+
safeAreaBottom: 34,
|
|
3929
|
+
safeAreaProvenance: {
|
|
3930
|
+
source: "measured",
|
|
3931
|
+
device: "iPhone 15 Pro",
|
|
3932
|
+
date: "2026-05-25"
|
|
3933
|
+
}
|
|
3520
3934
|
},
|
|
3521
3935
|
{
|
|
3522
3936
|
id: "iphone-16e",
|
|
@@ -3527,7 +3941,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3527
3941
|
notch: "notch",
|
|
3528
3942
|
notchInset: 47,
|
|
3529
3943
|
navBarHeight: 54,
|
|
3530
|
-
safeAreaBottom: 34
|
|
3944
|
+
safeAreaBottom: 34,
|
|
3945
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3531
3946
|
},
|
|
3532
3947
|
{
|
|
3533
3948
|
id: "iphone-17",
|
|
@@ -3538,7 +3953,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3538
3953
|
notch: "dynamic-island",
|
|
3539
3954
|
notchInset: 59,
|
|
3540
3955
|
navBarHeight: 54,
|
|
3541
|
-
safeAreaBottom: 34
|
|
3956
|
+
safeAreaBottom: 34,
|
|
3957
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3542
3958
|
},
|
|
3543
3959
|
{
|
|
3544
3960
|
id: "iphone-air",
|
|
@@ -3549,7 +3965,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3549
3965
|
notch: "dynamic-island",
|
|
3550
3966
|
notchInset: 59,
|
|
3551
3967
|
navBarHeight: 54,
|
|
3552
|
-
safeAreaBottom: 34
|
|
3968
|
+
safeAreaBottom: 34,
|
|
3969
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3553
3970
|
},
|
|
3554
3971
|
{
|
|
3555
3972
|
id: "iphone-17-pro",
|
|
@@ -3560,7 +3977,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3560
3977
|
notch: "dynamic-island",
|
|
3561
3978
|
notchInset: 59,
|
|
3562
3979
|
navBarHeight: 54,
|
|
3563
|
-
safeAreaBottom: 34
|
|
3980
|
+
safeAreaBottom: 34,
|
|
3981
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3564
3982
|
},
|
|
3565
3983
|
{
|
|
3566
3984
|
id: "iphone-17-pro-max",
|
|
@@ -3571,7 +3989,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3571
3989
|
notch: "dynamic-island",
|
|
3572
3990
|
notchInset: 62,
|
|
3573
3991
|
navBarHeight: 54,
|
|
3574
|
-
safeAreaBottom: 34
|
|
3992
|
+
safeAreaBottom: 34,
|
|
3993
|
+
safeAreaProvenance: EXTRAPOLATED
|
|
3575
3994
|
},
|
|
3576
3995
|
{
|
|
3577
3996
|
id: "galaxy-s26",
|
|
@@ -3582,7 +4001,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3582
4001
|
notch: "punch-hole-center",
|
|
3583
4002
|
notchInset: 32,
|
|
3584
4003
|
navBarHeight: 54,
|
|
3585
|
-
safeAreaBottom: 0
|
|
4004
|
+
safeAreaBottom: 0,
|
|
4005
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3586
4006
|
},
|
|
3587
4007
|
{
|
|
3588
4008
|
id: "galaxy-s26-plus",
|
|
@@ -3593,7 +4013,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3593
4013
|
notch: "punch-hole-center",
|
|
3594
4014
|
notchInset: 32,
|
|
3595
4015
|
navBarHeight: 54,
|
|
3596
|
-
safeAreaBottom: 0
|
|
4016
|
+
safeAreaBottom: 0,
|
|
4017
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3597
4018
|
},
|
|
3598
4019
|
{
|
|
3599
4020
|
id: "galaxy-s26-ultra",
|
|
@@ -3604,7 +4025,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3604
4025
|
notch: "punch-hole-center",
|
|
3605
4026
|
notchInset: 40,
|
|
3606
4027
|
navBarHeight: 54,
|
|
3607
|
-
safeAreaBottom: 0
|
|
4028
|
+
safeAreaBottom: 0,
|
|
4029
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3608
4030
|
},
|
|
3609
4031
|
{
|
|
3610
4032
|
id: "galaxy-z-flip7",
|
|
@@ -3615,7 +4037,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3615
4037
|
notch: "punch-hole-center",
|
|
3616
4038
|
notchInset: 36,
|
|
3617
4039
|
navBarHeight: 54,
|
|
3618
|
-
safeAreaBottom: 0
|
|
4040
|
+
safeAreaBottom: 0,
|
|
4041
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3619
4042
|
},
|
|
3620
4043
|
{
|
|
3621
4044
|
id: "galaxy-z-fold7-folded",
|
|
@@ -3626,7 +4049,8 @@ const VIEWPORT_PRESETS = [
|
|
|
3626
4049
|
notch: "punch-hole-center",
|
|
3627
4050
|
notchInset: 32,
|
|
3628
4051
|
navBarHeight: 54,
|
|
3629
|
-
safeAreaBottom: 0
|
|
4052
|
+
safeAreaBottom: 0,
|
|
4053
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3630
4054
|
},
|
|
3631
4055
|
{
|
|
3632
4056
|
id: "galaxy-z-fold7-unfolded",
|
|
@@ -3637,19 +4061,10 @@ const VIEWPORT_PRESETS = [
|
|
|
3637
4061
|
notch: "punch-hole-center",
|
|
3638
4062
|
notchInset: 32,
|
|
3639
4063
|
navBarHeight: 54,
|
|
3640
|
-
safeAreaBottom: 0
|
|
4064
|
+
safeAreaBottom: 0,
|
|
4065
|
+
safeAreaProvenance: PLACEHOLDER
|
|
3641
4066
|
},
|
|
3642
|
-
|
|
3643
|
-
id: "custom",
|
|
3644
|
-
label: "Custom",
|
|
3645
|
-
width: 0,
|
|
3646
|
-
height: 0,
|
|
3647
|
-
dpr: 1,
|
|
3648
|
-
notch: "none",
|
|
3649
|
-
notchInset: 0,
|
|
3650
|
-
navBarHeight: 0,
|
|
3651
|
-
safeAreaBottom: 0
|
|
3652
|
-
}
|
|
4067
|
+
CUSTOM_PRESET
|
|
3653
4068
|
];
|
|
3654
4069
|
function getPreset(id) {
|
|
3655
4070
|
return VIEWPORT_PRESETS.find((p) => p.id === id) ?? NONE_PRESET;
|
|
@@ -4015,6 +4430,24 @@ function initViewport() {
|
|
|
4015
4430
|
}
|
|
4016
4431
|
//#endregion
|
|
4017
4432
|
//#region src/panel/tabs/viewport.ts
|
|
4433
|
+
/**
|
|
4434
|
+
* Renders a small inline provenance badge for safe-area values.
|
|
4435
|
+
* - `measured` — no badge (confirmed value)
|
|
4436
|
+
* - `extrapolated` — "(추정치)" in muted gray
|
|
4437
|
+
* - `placeholder` — "(미측정)" in amber
|
|
4438
|
+
*/
|
|
4439
|
+
function provenanceBadge(provenance) {
|
|
4440
|
+
if (!provenance || provenance.source === "measured") return null;
|
|
4441
|
+
const text = provenance.source === "placeholder" ? "(미측정)" : "(추정치)";
|
|
4442
|
+
const color = provenance.source === "placeholder" ? "#b45309" : "#888";
|
|
4443
|
+
const badge = h("span", {
|
|
4444
|
+
className: "ait-provenance-badge",
|
|
4445
|
+
title: provenance.source === "placeholder" ? "safe-area 값이 미실측 추정치입니다. relay 세션에서 measure_safe_area로 실측 후 승급하세요." : "safe-area 값이 기기 스펙에서 유추한 추정치입니다. relay 세션에서 measure_safe_area로 확인하세요."
|
|
4446
|
+
});
|
|
4447
|
+
badge.textContent = text;
|
|
4448
|
+
badge.style.cssText = `font-size:10px;color:${color};margin-left:4px`;
|
|
4449
|
+
return badge;
|
|
4450
|
+
}
|
|
4018
4451
|
function renderViewportTab() {
|
|
4019
4452
|
const s = aitState.state;
|
|
4020
4453
|
const vp = s.viewport;
|
|
@@ -4134,7 +4567,10 @@ function renderViewportTab() {
|
|
|
4134
4567
|
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.cssPhysical")), h("span", { className: "ait-status-value" }, `${size.width}×${size.height}@${dpr}x | ${physW}×${physH} ${orientDisplay}`)));
|
|
4135
4568
|
if (preset) {
|
|
4136
4569
|
const insets = computeSafeAreaInsets(preset, landscape, vp.landscapeSide, vp.aitNavBar, vp.aitNavBarType);
|
|
4137
|
-
|
|
4570
|
+
const safeAreaValueEl = h("span", { className: "ait-status-value" }, `T${insets.top} R${insets.right} B${insets.bottom} L${insets.left}`);
|
|
4571
|
+
const badge = provenanceBadge(preset.safeAreaProvenance);
|
|
4572
|
+
if (badge) safeAreaValueEl.appendChild(badge);
|
|
4573
|
+
rows.push(h("div", { className: "ait-status-row" }, h("span", {}, t("viewport.status.safeArea")), safeAreaValueEl));
|
|
4138
4574
|
}
|
|
4139
4575
|
if (vp.aitNavBar && !landscape) {
|
|
4140
4576
|
const navBarTop = vp.aitNavBarType === "partner" ? preset?.navBarHeight ?? 0 : 0;
|
|
@@ -4437,7 +4873,7 @@ function mount() {
|
|
|
4437
4873
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4438
4874
|
refreshPanel();
|
|
4439
4875
|
});
|
|
4440
|
-
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.
|
|
4876
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.34`), closeBtn);
|
|
4441
4877
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4442
4878
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4443
4879
|
for (const tab of getTabs()) {
|