@ait-co/devtools 0.1.32 → 0.1.33
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 +226 -26
- 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 +53 -2
- 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 +1 -1
package/dist/mock/index.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
//#region src/mock/proxy.ts
|
|
2
|
-
/**
|
|
3
|
-
* 미구현 API용 Proxy 트립와이어.
|
|
4
|
-
*
|
|
5
|
-
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
6
|
-
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
7
|
-
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
8
|
-
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
9
|
-
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
10
|
-
*/
|
|
11
|
-
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
12
|
-
function createMockProxy(moduleName, implementations) {
|
|
13
|
-
return new Proxy(implementations, { get(target, prop) {
|
|
14
|
-
if (typeof prop === "symbol") return void 0;
|
|
15
|
-
if (prop in target) return target[prop];
|
|
16
|
-
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}`);
|
|
17
|
-
} });
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
1
|
//#region src/mock/state.ts
|
|
2
|
+
/** SDK 호출 로그 ring buffer 상한 */
|
|
3
|
+
const SDK_CALL_LOG_MAX = 200;
|
|
21
4
|
const DEFAULT_STATE = {
|
|
22
5
|
platform: "ios",
|
|
23
6
|
environment: "sandbox",
|
|
@@ -95,7 +78,9 @@ const DEFAULT_STATE = {
|
|
|
95
78
|
isLoaded: false,
|
|
96
79
|
nextEvent: "loaded",
|
|
97
80
|
forceNoFill: false,
|
|
98
|
-
lastEvent: null
|
|
81
|
+
lastEvent: null,
|
|
82
|
+
rewardUnitType: "coins",
|
|
83
|
+
rewardAmount: 10
|
|
99
84
|
},
|
|
100
85
|
game: {
|
|
101
86
|
profile: {
|
|
@@ -105,6 +90,7 @@ const DEFAULT_STATE = {
|
|
|
105
90
|
leaderboardScores: []
|
|
106
91
|
},
|
|
107
92
|
analyticsLog: [],
|
|
93
|
+
sdkCallLog: [],
|
|
108
94
|
deviceModes: {
|
|
109
95
|
camera: "mock",
|
|
110
96
|
photos: "mock",
|
|
@@ -217,6 +203,19 @@ var AitStateManager = class {
|
|
|
217
203
|
};
|
|
218
204
|
this._notify();
|
|
219
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* SDK 호출 로그 추가 (ring buffer, 상한 SDK_CALL_LOG_MAX).
|
|
208
|
+
* `observe()`가 호출하고, proxy의 KNOWN_UNIMPLEMENTED 경로도 직접 호출한다.
|
|
209
|
+
*/
|
|
210
|
+
logSdkCall(entry) {
|
|
211
|
+
const log = this._state.sdkCallLog;
|
|
212
|
+
const next = log.length >= SDK_CALL_LOG_MAX ? log.slice(1 - SDK_CALL_LOG_MAX) : log;
|
|
213
|
+
this._state = {
|
|
214
|
+
...this._state,
|
|
215
|
+
sdkCallLog: [...next, entry]
|
|
216
|
+
};
|
|
217
|
+
this._notify();
|
|
218
|
+
}
|
|
220
219
|
/** 이벤트 트리거 (backEvent, homeEvent 등) */
|
|
221
220
|
trigger(event) {
|
|
222
221
|
window.dispatchEvent(new CustomEvent(`__ait:${event}`));
|
|
@@ -240,16 +239,142 @@ if (!globalRef[SINGLETON_KEY]) globalRef[SINGLETON_KEY] = new AitStateManager();
|
|
|
240
239
|
const aitState = globalRef[SINGLETON_KEY];
|
|
241
240
|
if (typeof window !== "undefined") window.__ait = aitState;
|
|
242
241
|
//#endregion
|
|
242
|
+
//#region src/mock/observe.ts
|
|
243
|
+
/**
|
|
244
|
+
* fn을 observe로 감싼다.
|
|
245
|
+
*
|
|
246
|
+
* @param apiName - 로그에 기록할 SDK 메서드 이름 (예: `'setScreenAwakeMode'`)
|
|
247
|
+
* @param fidelity - 이 mock의 fidelity grade ('faithful' | 'partial' | 'inert')
|
|
248
|
+
* @param fn - 실제 mock 구현체. 시그니처를 그대로 통과시킨다.
|
|
249
|
+
* @returns fn과 동일한 타입의 래퍼 함수
|
|
250
|
+
*/
|
|
251
|
+
function observe(apiName, fidelity, fn) {
|
|
252
|
+
return (...args) => {
|
|
253
|
+
const timestamp = Date.now();
|
|
254
|
+
const safeArgs = args.map((a) => safeSerialize(a));
|
|
255
|
+
const result = fn(...args);
|
|
256
|
+
if (result instanceof Promise) {
|
|
257
|
+
aitState.logSdkCall({
|
|
258
|
+
method: apiName,
|
|
259
|
+
args: safeArgs,
|
|
260
|
+
timestamp,
|
|
261
|
+
status: "pending",
|
|
262
|
+
fidelity
|
|
263
|
+
});
|
|
264
|
+
result.then((value) => {
|
|
265
|
+
aitState.logSdkCall({
|
|
266
|
+
method: apiName,
|
|
267
|
+
args: safeArgs,
|
|
268
|
+
timestamp,
|
|
269
|
+
status: "resolved",
|
|
270
|
+
result: safeSerialize(value),
|
|
271
|
+
fidelity
|
|
272
|
+
});
|
|
273
|
+
}, (err) => {
|
|
274
|
+
aitState.logSdkCall({
|
|
275
|
+
method: apiName,
|
|
276
|
+
args: safeArgs,
|
|
277
|
+
timestamp,
|
|
278
|
+
status: "rejected",
|
|
279
|
+
error: err instanceof Error ? err.message : String(err),
|
|
280
|
+
fidelity
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
aitState.logSdkCall({
|
|
286
|
+
method: apiName,
|
|
287
|
+
args: safeArgs,
|
|
288
|
+
timestamp,
|
|
289
|
+
status: "resolved",
|
|
290
|
+
result: safeSerialize(result),
|
|
291
|
+
fidelity
|
|
292
|
+
});
|
|
293
|
+
return result;
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 값을 JSON-safe한 형태로 변환한다.
|
|
298
|
+
* - null / primitive — 그대로.
|
|
299
|
+
* - 함수 — `'[Function: name]'` 문자열.
|
|
300
|
+
* - 기타 객체 — JSON.stringify 실패 시 `'[unserializable]'`.
|
|
301
|
+
*/
|
|
302
|
+
function safeSerialize(value) {
|
|
303
|
+
if (value === null || value === void 0) return value;
|
|
304
|
+
if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
|
|
305
|
+
if (typeof value !== "object") return value;
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(JSON.stringify(value));
|
|
308
|
+
} catch {
|
|
309
|
+
return "[unserializable]";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/mock/proxy.ts
|
|
314
|
+
/**
|
|
315
|
+
* 미구현 API용 Proxy 트립와이어.
|
|
316
|
+
*
|
|
317
|
+
* 미구현 프로퍼티에 접근하면 throw한다. 이는 "devtools에서는 멀쩡히 돌지만
|
|
318
|
+
* 실 SDK에선 실제로 동작하는" 시나리오를 차단하기 위한 의도적 선택이다.
|
|
319
|
+
* mock이 미구현인 API는 실 SDK에서는 존재할 수 있고, 사용자가 이를 인지하지
|
|
320
|
+
* 못한 채 개발을 이어가면 배포 시점에 놀라게 된다. 에러 메시지에 이슈 URL을
|
|
321
|
+
* 포함해 사용자가 mock 누락을 제보할 수 있게 한다.
|
|
322
|
+
*
|
|
323
|
+
* ## KNOWN_UNIMPLEMENTED 정책
|
|
324
|
+
* SDK에 존재하는 것으로 알려져 있으나 현재 mock이 없는 API 이름만 이 집합에 둔다.
|
|
325
|
+
* 이 경우에만 throw 대신 🔴 inert no-op을 반환하고 sdkCallLog에 기록한다.
|
|
326
|
+
* 완전히 미지의 이름은 여전히 throw — "잘 되는 척" 방지.
|
|
327
|
+
*/
|
|
328
|
+
const ISSUES_URL = "https://github.com/apps-in-toss-community/devtools/issues";
|
|
329
|
+
/**
|
|
330
|
+
* SDK에 존재하나 mock이 아직 없는 것으로 확인된 이름 목록.
|
|
331
|
+
* 새 API가 SDK에 추가되면 여기에 추가하고 별도 PR에서 mock 구현으로 이동한다.
|
|
332
|
+
* 확인되지 않은 이름은 절대 여기에 추가하지 않는다 — throw가 더 안전하다.
|
|
333
|
+
*/
|
|
334
|
+
const KNOWN_UNIMPLEMENTED = /* @__PURE__ */ new Set([]);
|
|
335
|
+
function createMockProxy(moduleName, implementations) {
|
|
336
|
+
return new Proxy(implementations, { get(target, prop) {
|
|
337
|
+
if (typeof prop === "symbol") return void 0;
|
|
338
|
+
if (prop in target) return target[prop];
|
|
339
|
+
const name = String(prop);
|
|
340
|
+
if (KNOWN_UNIMPLEMENTED.has(name)) return (...args) => {
|
|
341
|
+
console.warn(`[@ait-co/devtools] ${moduleName}.${name} is known-unimplemented (🔴 inert). Returning undefined. Please file or upvote an issue: ${ISSUES_URL}`);
|
|
342
|
+
aitState.logSdkCall({
|
|
343
|
+
method: `${moduleName}.${name}`,
|
|
344
|
+
args,
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
status: "resolved",
|
|
347
|
+
result: void 0,
|
|
348
|
+
fidelity: "inert"
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
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}`);
|
|
352
|
+
} });
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
243
355
|
//#region src/mock/ads/index.ts
|
|
244
356
|
/**
|
|
245
357
|
* 광고 mock (GoogleAdMob, TossAds, FullScreenAd)
|
|
358
|
+
*
|
|
359
|
+
* 변경 이력 (#196):
|
|
360
|
+
* - slot 레지스트리로 TossAds destroy/destroyAll 누수 수정 (🟡→🟢)
|
|
361
|
+
* - attachBanner BannerSlotCallbacks 발화 (onAdRendered/onAdImpression/onNoFill 등)
|
|
362
|
+
* - initialize onInitialized/onInitializationFailed 발화
|
|
363
|
+
* - AdMob reward 파라미터화 (state.ads.rewardUnitType/rewardAmount)
|
|
364
|
+
* - 모든 호출 observe()로 sdkCallLog에 기록
|
|
246
365
|
*/
|
|
247
366
|
function withIsSupported(fn) {
|
|
248
367
|
fn.isSupported = () => true;
|
|
249
368
|
return fn;
|
|
250
369
|
}
|
|
370
|
+
const _slotRegistry = /* @__PURE__ */ new Map();
|
|
371
|
+
let _slotCounter = 0;
|
|
372
|
+
function _nextSlotId(adGroupId) {
|
|
373
|
+
_slotCounter += 1;
|
|
374
|
+
return `mock-slot-${adGroupId}-${_slotCounter}`;
|
|
375
|
+
}
|
|
251
376
|
const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
252
|
-
loadAppsInTossAdMob: withIsSupported((args) => {
|
|
377
|
+
loadAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.loadAppsInTossAdMob", "faithful", (args) => {
|
|
253
378
|
setTimeout(() => {
|
|
254
379
|
if (aitState.state.ads.forceNoFill) {
|
|
255
380
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -262,8 +387,8 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
262
387
|
});
|
|
263
388
|
}, 200);
|
|
264
389
|
return () => {};
|
|
265
|
-
}),
|
|
266
|
-
showAppsInTossAdMob: withIsSupported((args) => {
|
|
390
|
+
})),
|
|
391
|
+
showAppsInTossAdMob: withIsSupported(observe("GoogleAdMob.showAppsInTossAdMob", "faithful", (args) => {
|
|
267
392
|
if (!aitState.state.ads.isLoaded) {
|
|
268
393
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
269
394
|
return () => {};
|
|
@@ -272,11 +397,12 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
272
397
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
273
398
|
setTimeout(() => args.onEvent({ type: "impression" }), 150);
|
|
274
399
|
setTimeout(() => {
|
|
400
|
+
const { rewardUnitType, rewardAmount } = aitState.state.ads;
|
|
275
401
|
args.onEvent({
|
|
276
402
|
type: "userEarnedReward",
|
|
277
403
|
data: {
|
|
278
|
-
unitType:
|
|
279
|
-
unitAmount:
|
|
404
|
+
unitType: rewardUnitType,
|
|
405
|
+
unitAmount: rewardAmount
|
|
280
406
|
}
|
|
281
407
|
});
|
|
282
408
|
}, 1e3);
|
|
@@ -285,14 +411,18 @@ const GoogleAdMob = createMockProxy("GoogleAdMob", {
|
|
|
285
411
|
aitState.patch("ads", { isLoaded: false });
|
|
286
412
|
}, 1500);
|
|
287
413
|
return () => {};
|
|
288
|
-
}),
|
|
289
|
-
isAppsInTossAdMobLoaded: withIsSupported(async (_options) => aitState.state.ads.isLoaded)
|
|
414
|
+
})),
|
|
415
|
+
isAppsInTossAdMobLoaded: withIsSupported(observe("GoogleAdMob.isAppsInTossAdMobLoaded", "faithful", async (_options) => aitState.state.ads.isLoaded))
|
|
290
416
|
});
|
|
291
417
|
const TossAds = createMockProxy("TossAds", {
|
|
292
|
-
initialize: withIsSupported((
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
418
|
+
initialize: withIsSupported(observe("TossAds.initialize", "partial", (options) => {
|
|
419
|
+
if (aitState.state.ads.forceNoFill) {
|
|
420
|
+
options.callbacks?.onInitializationFailed?.(/* @__PURE__ */ new Error("No fill"));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
options.callbacks?.onInitialized?.();
|
|
424
|
+
})),
|
|
425
|
+
attach: withIsSupported(observe("TossAds.attach", "partial", (_adGroupId, target, _options) => {
|
|
296
426
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
297
427
|
if (el) {
|
|
298
428
|
const placeholder = document.createElement("div");
|
|
@@ -300,21 +430,76 @@ const TossAds = createMockProxy("TossAds", {
|
|
|
300
430
|
placeholder.textContent = "[@ait-co/devtools] TossAds Placeholder";
|
|
301
431
|
el.appendChild(placeholder);
|
|
302
432
|
}
|
|
303
|
-
}),
|
|
304
|
-
attachBanner: withIsSupported((
|
|
433
|
+
})),
|
|
434
|
+
attachBanner: withIsSupported(observe("TossAds.attachBanner", "faithful", (adGroupId, target, options) => {
|
|
305
435
|
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
436
|
+
const slotId = _nextSlotId(adGroupId);
|
|
437
|
+
const placeholder = document.createElement("div");
|
|
438
|
+
const theme = options?.theme ?? "auto";
|
|
439
|
+
const variant = options?.variant ?? "card";
|
|
440
|
+
const isDark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
441
|
+
const bg = isDark ? "#1a1a1a" : "#f0f0f0";
|
|
442
|
+
const textColor = isDark ? "#aaa" : "#666";
|
|
443
|
+
const borderColor = isDark ? "#555" : "#999";
|
|
444
|
+
const height = variant === "expanded" ? "120px" : "60px";
|
|
445
|
+
placeholder.dataset.aitSlotId = slotId;
|
|
446
|
+
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;`;
|
|
447
|
+
placeholder.textContent = `[@ait-co/devtools] Banner Ad (${variant})`;
|
|
306
448
|
if (el) {
|
|
307
|
-
const placeholder = document.createElement("div");
|
|
308
|
-
placeholder.style.cssText = "background:#f0f0f0;border:1px dashed #999;padding:12px;text-align:center;color:#666;font-size:12px;";
|
|
309
|
-
placeholder.textContent = "[@ait-co/devtools] Banner Ad Placeholder";
|
|
310
449
|
el.appendChild(placeholder);
|
|
450
|
+
_slotRegistry.set(slotId, placeholder);
|
|
311
451
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
452
|
+
const destroySlot = () => {
|
|
453
|
+
const registered = _slotRegistry.get(slotId);
|
|
454
|
+
if (registered) {
|
|
455
|
+
registered.remove();
|
|
456
|
+
_slotRegistry.delete(slotId);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
if (aitState.state.ads.forceNoFill) {
|
|
461
|
+
options?.callbacks?.onNoFill?.({
|
|
462
|
+
slotId,
|
|
463
|
+
adGroupId,
|
|
464
|
+
adMetadata: {}
|
|
465
|
+
});
|
|
466
|
+
options?.callbacks?.onAdFailedToRender?.({
|
|
467
|
+
slotId,
|
|
468
|
+
adGroupId,
|
|
469
|
+
adMetadata: {},
|
|
470
|
+
error: {
|
|
471
|
+
code: 0,
|
|
472
|
+
message: "No fill"
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const eventPayload = {
|
|
478
|
+
slotId,
|
|
479
|
+
adGroupId,
|
|
480
|
+
adMetadata: {
|
|
481
|
+
creativeId: `mock-creative-${slotId}`,
|
|
482
|
+
requestId: `mock-req-${slotId}`
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
options?.callbacks?.onAdRendered?.(eventPayload);
|
|
486
|
+
options?.callbacks?.onAdImpression?.(eventPayload);
|
|
487
|
+
}, 100);
|
|
488
|
+
return { destroy: destroySlot };
|
|
489
|
+
})),
|
|
490
|
+
destroy: withIsSupported(observe("TossAds.destroy", "faithful", (slotId) => {
|
|
491
|
+
const el = _slotRegistry.get(slotId);
|
|
492
|
+
if (el) {
|
|
493
|
+
el.remove();
|
|
494
|
+
_slotRegistry.delete(slotId);
|
|
495
|
+
}
|
|
496
|
+
})),
|
|
497
|
+
destroyAll: withIsSupported(observe("TossAds.destroyAll", "faithful", () => {
|
|
498
|
+
for (const el of _slotRegistry.values()) el.remove();
|
|
499
|
+
_slotRegistry.clear();
|
|
500
|
+
}))
|
|
316
501
|
});
|
|
317
|
-
const loadFullScreenAd = withIsSupported((args) => {
|
|
502
|
+
const loadFullScreenAd = withIsSupported(observe("loadFullScreenAd", "faithful", (args) => {
|
|
318
503
|
setTimeout(() => {
|
|
319
504
|
if (aitState.state.ads.forceNoFill) {
|
|
320
505
|
args.onError(/* @__PURE__ */ new Error("No fill"));
|
|
@@ -327,8 +512,8 @@ const loadFullScreenAd = withIsSupported((args) => {
|
|
|
327
512
|
});
|
|
328
513
|
}, 200);
|
|
329
514
|
return () => {};
|
|
330
|
-
});
|
|
331
|
-
const showFullScreenAd = withIsSupported((args) => {
|
|
515
|
+
}));
|
|
516
|
+
const showFullScreenAd = withIsSupported(observe("showFullScreenAd", "faithful", (args) => {
|
|
332
517
|
if (!aitState.state.ads.isLoaded) {
|
|
333
518
|
args.onError(/* @__PURE__ */ new Error("Ad not loaded"));
|
|
334
519
|
return () => {};
|
|
@@ -336,7 +521,7 @@ const showFullScreenAd = withIsSupported((args) => {
|
|
|
336
521
|
setTimeout(() => args.onEvent({ type: "show" }), 100);
|
|
337
522
|
setTimeout(() => args.onEvent({ type: "dismissed" }), 1500);
|
|
338
523
|
return () => {};
|
|
339
|
-
});
|
|
524
|
+
}));
|
|
340
525
|
//#endregion
|
|
341
526
|
//#region src/mock/analytics/index.ts
|
|
342
527
|
/**
|
|
@@ -735,13 +920,70 @@ const fetchContacts = withPermission(_fetchContacts, "contacts");
|
|
|
735
920
|
//#region src/mock/device/haptic.ts
|
|
736
921
|
/**
|
|
737
922
|
* Haptic Feedback & saveBase64Data mock
|
|
923
|
+
*
|
|
924
|
+
* generateHapticFeedback — 영역 3 (하드웨어 API 관측):
|
|
925
|
+
* - 10종 HapticFeedbackType을 navigator.vibrate 패턴으로 매핑(근사, best-effort).
|
|
926
|
+
* - `typeof navigator.vibrate === 'function'` 가드 — API 없는 환경에서 throw 없이 skip.
|
|
927
|
+
* - sdkCallLog에 🟡(partial)로 기록. params: { hapticType, vibrated: boolean }.
|
|
928
|
+
* - 시그니처 불변 — __typecheck.ts의 Assert<Mock, Original> 통과.
|
|
929
|
+
*/
|
|
930
|
+
/**
|
|
931
|
+
* HapticFeedbackType 10종 → navigator.vibrate 패턴 매핑.
|
|
932
|
+
* 숫자: 진동 ms. 배열: [진동, 정지, 진동, …] 교대 패턴.
|
|
738
933
|
*/
|
|
934
|
+
const HAPTIC_VIBRATE_PATTERN = {
|
|
935
|
+
tickWeak: 10,
|
|
936
|
+
tap: 20,
|
|
937
|
+
tickMedium: 30,
|
|
938
|
+
softMedium: 40,
|
|
939
|
+
basicWeak: 15,
|
|
940
|
+
basicMedium: 50,
|
|
941
|
+
success: [
|
|
942
|
+
10,
|
|
943
|
+
40,
|
|
944
|
+
10
|
|
945
|
+
],
|
|
946
|
+
error: [
|
|
947
|
+
40,
|
|
948
|
+
30,
|
|
949
|
+
40
|
|
950
|
+
],
|
|
951
|
+
wiggle: [
|
|
952
|
+
20,
|
|
953
|
+
20,
|
|
954
|
+
20,
|
|
955
|
+
20,
|
|
956
|
+
20
|
|
957
|
+
],
|
|
958
|
+
confetti: [
|
|
959
|
+
10,
|
|
960
|
+
20,
|
|
961
|
+
10,
|
|
962
|
+
20,
|
|
963
|
+
10,
|
|
964
|
+
20,
|
|
965
|
+
10
|
|
966
|
+
]
|
|
967
|
+
};
|
|
739
968
|
async function generateHapticFeedback(options) {
|
|
740
|
-
|
|
969
|
+
const timestamp = Date.now();
|
|
741
970
|
aitState.logAnalytics({
|
|
742
971
|
type: "haptic",
|
|
743
972
|
params: { hapticType: options.type }
|
|
744
973
|
});
|
|
974
|
+
const pattern = HAPTIC_VIBRATE_PATTERN[options.type] ?? 30;
|
|
975
|
+
const vibrated = typeof navigator.vibrate === "function" ? navigator.vibrate(pattern) : false;
|
|
976
|
+
aitState.logSdkCall({
|
|
977
|
+
method: "generateHapticFeedback",
|
|
978
|
+
args: [{ type: options.type }],
|
|
979
|
+
timestamp,
|
|
980
|
+
status: "resolved",
|
|
981
|
+
result: {
|
|
982
|
+
hapticType: options.type,
|
|
983
|
+
vibrated
|
|
984
|
+
},
|
|
985
|
+
fidelity: "partial"
|
|
986
|
+
});
|
|
745
987
|
}
|
|
746
988
|
async function saveBase64Data(params) {
|
|
747
989
|
const a = document.createElement("a");
|
|
@@ -1122,17 +1364,17 @@ async function setDeviceOrientation(options) {
|
|
|
1122
1364
|
}
|
|
1123
1365
|
console.warn(`[@ait-co/devtools] setDeviceOrientation(${options.type}) ignored — Panel is forcing "${current}". Change the Viewport tab's orientation to "auto" to let the app control rotation.`);
|
|
1124
1366
|
}
|
|
1125
|
-
|
|
1367
|
+
const setScreenAwakeMode = observe("setScreenAwakeMode", "inert", async (options) => {
|
|
1126
1368
|
console.log("[@ait-co/devtools] setScreenAwakeMode:", options.enabled);
|
|
1127
1369
|
return { enabled: options.enabled };
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1370
|
+
});
|
|
1371
|
+
const setSecureScreen = observe("setSecureScreen", "inert", async (options) => {
|
|
1130
1372
|
console.log("[@ait-co/devtools] setSecureScreen:", options.enabled);
|
|
1131
1373
|
return { enabled: options.enabled };
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1374
|
+
});
|
|
1375
|
+
const requestReview = observe("requestReview", "inert", async () => {
|
|
1134
1376
|
console.log("[@ait-co/devtools] requestReview called");
|
|
1135
|
-
}
|
|
1377
|
+
});
|
|
1136
1378
|
requestReview.isSupported = () => true;
|
|
1137
1379
|
function getPlatformOS() {
|
|
1138
1380
|
return aitState.state.platform;
|