@apps-in-toss/devtools 3.1.0-beta.1 → 3.1.0
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/CHANGELOG.md +34 -0
- package/LICENSE +201 -28
- package/README.md +237 -871
- package/dist/mock/2x.d.ts +3 -155
- package/dist/mock/2x.js +1305 -1559
- package/dist/mock/3x.d.ts +27 -123
- package/dist/mock/3x.js +1275 -1521
- package/dist/mock/index.d.ts +27 -123
- package/dist/mock/index.js +1275 -1521
- package/dist/panel/index.js +154 -106
- package/dist/unplugin/index.cjs +12 -147
- package/dist/unplugin/index.d.cts +408 -78
- package/dist/unplugin/index.d.ts +408 -78
- package/dist/unplugin/index.js +13 -147
- package/package.json +5 -8
- package/dist/tunnel-BvEf1qGV.js +0 -186
- package/dist/tunnel-DtCTOUlp.cjs +0 -187
- package/dist/unplugin/tunnel.cjs +0 -191
- package/dist/unplugin/tunnel.d.cts +0 -140
- package/dist/unplugin/tunnel.d.ts +0 -140
- package/dist/unplugin/tunnel.js +0 -186
package/README.md
CHANGED
|
@@ -1,173 +1,41 @@
|
|
|
1
1
|
# @apps-in-toss/devtools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](./LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
앱인토스(Apps in Toss) 미니앱을 **일반 브라우저에서** 개발·테스트하기 위한 개발 도구예요. `@apps-in-toss/web-framework` SDK를 mock 구현으로 치환하고, mock 상태를 실시간으로 조작하는 플로팅 패널을 띄우며, 이 둘을 모든 주요 번들러에 배선하는 [unplugin](https://github.com/unjs/unplugin) 플러그인을 함께 제공해요.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**devDependency 전용이에요.** 프로덕션 빌드에서는 플러그인이 통째로 비활성화되고, 프로덕션 번들에 기여하는 바이트 수는 0이에요 (아래 [프로덕션 빌드](#프로덕션-빌드) 참고).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
이 프로젝트는 더 이상 유지보수되지 않습니다. repo는 archive되어 read-only가 되며, 소스와 이슈 기록은 GitHub에 그대로 남습니다. npm에 올라간 패키지는 계속 설치할 수 있지만 더 이상 업데이트되지 않습니다. `aitc.dev` 도메인과 그 위에서 서비스하던 사이트도 함께 종료되므로, 문서와 예제는 GitHub 소스를 보세요. 이 패키지에서 그 도메인에 의존하는 부분은 환경 2(실기기 PWA 미리보기)입니다 — 진입점인 launcher PWA가 `https://devtools.aitc.dev/launcher/`에 배포돼 있고, 터널 QR/deep-link도 그 주소로 만들어집니다. 이미 배포된 버전과 폰에 이미 설치된 launcher에는 이 사실을 소급 적용할 수 없습니다. 환경 1(로컬 브라우저 + mock SDK + DevTools 패널)은 외부 호스트에 의존하지 않습니다.
|
|
14
|
-
|
|
15
|
-
- **60+ SDK API mock** — 인증, 결제, IAP, 위치, 카메라, 스토리지 등
|
|
16
|
-
- **Device API 모드 시스템** — mock / web / prompt 세 가지 모드로 디바이스 API 동작 전환
|
|
17
|
-
- **Device simulation** — iPhone/Galaxy 프리셋 + orientation 토글로 데스크탑 브라우저에서 모바일 뷰포트 시뮬레이션
|
|
18
|
-
- **Floating DevTools Panel** — 브라우저에서 SDK 상태를 실시간으로 제어 (12개 탭, mock state preset library 포함)
|
|
19
|
-
- **모든 번들러 지원** — [unplugin](https://github.com/unjs/unplugin) 기반 Vite, Webpack, Rspack, esbuild, Rollup 통합
|
|
20
|
-
|
|
21
|
-
데모 소스: [`e2e/fixture/`](https://github.com/apps-in-toss-community/devtools/tree/main/e2e/fixture) (self-contained 데모 앱. GitHub Pages 호스팅은 도메인 종료와 함께 내려갑니다 — 로컬에서 `pnpm e2e:build`로 빌드해 볼 수 있습니다).
|
|
22
|
-
|
|
23
|
-
## 15초 quickstart — 내 상황에 맞는 환경 고르기
|
|
24
|
-
|
|
25
|
-
3가지 실행 환경이 있습니다. 지금 상황에 맞는 카드 하나를 고르고, 해당 상세 시나리오 문서로 이동하세요.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
**환경 1 — 로컬 브라우저** (가장 빠름, HMR O)
|
|
30
|
-
|
|
31
|
-
데스크탑 Chrome에서 mock SDK + DevTools 패널로 개발합니다. 토스 앱·폰 없이 즉시 시작.
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pnpm add -D @apps-in-toss/devtools
|
|
35
|
-
# vite.config.ts에 unplugin 추가 → pnpm dev
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
DevTools 패널: 화면 우하단 **AIT** 버튼. 상세: [`docs/scenarios/env-1.md`](./docs/scenarios/env-1.md)
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
**환경 2 — 실기기 PWA** (실 WebKit 엔진, HMR O, 토스 검수 불필요)
|
|
43
|
-
|
|
44
|
-
폰에서 실기기 Safari/WebKit 엔진으로 미니앱을 확인합니다. launcher PWA를 한 번 설치하고, 매 세션마다 QR 스캔.
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
# vite.config.ts에 tunnel 옵션 추가 후:
|
|
48
|
-
pnpm dev:phone # AIT_TUNNEL=1 pnpm dev 와 동일
|
|
49
|
-
# 터미널에 QR 출력 → 폰 카메라로 스캔 → launcher PWA에서 자동 열림
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`tunnel: { cdp: true }`를 켜면 같은 QR 한 번으로 화면 미리보기 + on-device CDP가 함께 열려 실기기 WebKit의 DOM·콘솔·예외를 MCP로 관측합니다 (`call_sdk`는 환경 2에서 mock — 실 SDK는 환경 3). CDP를 쓰려면 디버깅 패키지 두 개를 추가로 설치하세요 — 아래 [디버깅 패키지](#디버깅-패키지-환경-23) 참고.
|
|
53
|
-
|
|
54
|
-
사전: 폰에 `https://devtools.aitc.dev/launcher/` 를 홈 화면에 한 번 추가. 이 호스트는 `aitc.dev` 도메인 정리와 함께 사라집니다. 상세: [`docs/scenarios/env-2.md`](./docs/scenarios/env-2.md)
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
**환경 3 — intoss-private** (토스 WebView, HMR X, debug 전용)
|
|
59
|
-
|
|
60
|
-
실기기 토스 앱 WebView에서 dog-food 번들을 로드하고 MCP relay로 디버깅합니다. `@ait-co/debugger` 설치가 필요합니다 — 아래 [디버깅 패키지](#디버깅-패키지-환경-23) 참고.
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npx -y -p @ait-co/debugger debugger # MCP 서버 시작 → QR 출력 (devDep이면 pnpm exec debugger)
|
|
64
|
-
# ait build && ait deploy --scheme-only
|
|
65
|
-
# start_attach(scheme_url) 호출 한 번으로 QR 생성 + 폰 attach까지 — QR 스캔하면 토스 앱 로드 + relay attach
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
HMR 없음(토스 WebView cold-load만). 상세: [`docs/scenarios/env-3.md`](./docs/scenarios/env-3.md)
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## on-device 디버깅 한 줄 설정
|
|
73
|
-
|
|
74
|
-
환경 2·3에서 on-device CDP 디버깅을 활성화하려면 미니앱 entry(`main.tsx` 등)에 **한 줄**을 추가하세요:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
// main.tsx (또는 미니앱 entry 최상단)
|
|
78
|
-
import "@ait-co/debug-console/auto";
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
이 한 줄이 하는 일:
|
|
82
|
-
|
|
83
|
-
- **self-gate**: URL에 `?debug=1` 또는 `?relay=`가 없고, DEV 빌드도 아니라면 아무것도 하지 않습니다. 청크는 dormant 상태로 남아 일반 프로덕션 로드에 영향을 주지 않습니다.
|
|
84
|
-
- **attach**: gate가 통과하면 `maybeAttach()`를 호출해 Chii `target.js`를 주입합니다 (Layer B·C 게이트 시맨틱스는 완전히 유지).
|
|
85
|
-
- **SDK 브리지**: `window.__sdk` / `window.__sdkCall`을 설치해 에이전트가 CDP relay의 `Runtime.evaluate`로 SDK API를 직접 구동할 수 있게 합니다. `@apps-in-toss/web-framework`가 없으면 조용히 skip합니다.
|
|
86
|
-
- **타입**: `Window.__sdk` / `__sdkCall` 글로벌 타입을 자동으로 제공합니다 — 별도 `globals.d.ts` 불필요.
|
|
87
|
-
|
|
88
|
-
환경 3(intoss-private relay) 빌드는 relay QR deep-link가 `?debug=1&relay=<wss>` 파라미터를 실어 보내므로, 이 한 줄만 있으면 별도 게이트 코드가 필요 없습니다. 환경 2(PWA, `tunnel: { cdp: true }`)도 동일하게 동작합니다.
|
|
89
|
-
|
|
90
|
-
옛 경로 `@apps-in-toss/devtools/in-app/auto`는 0.2.x에서도 계속 resolve되지만 inert한 no-op 스텁이며 1.0.0에서 제거됩니다 — import를 위 새 경로로 옮기세요.
|
|
91
|
-
|
|
92
|
-
> TOTP 인증이 필요한 dog-food 빌드는 빌드 define으로 `__DEBUG_TOTP_SECRET__`을 주입하고 `@ait-co/debug-console`을 직접 import해 `evaluateDebugGate({ verifyTotpCode })` + `maybeAttach()`를 사용하세요. `in-app/auto`는 TOTP verifier를 주입하지 않으므로 C3 레이어가 비활성화됩니다.
|
|
93
|
-
|
|
94
|
-
## 자주 겪는 문제 5가지
|
|
95
|
-
|
|
96
|
-
**"QR 창이 안 열림"**
|
|
97
|
-
|
|
98
|
-
`start_attach`을 먼저 호출하지 않았거나, GUI 없는 headless 환경이라 대시보드를 열 수 없는 경우입니다. 도구 결과에 텍스트 QR이 출력되므로 폰 카메라로 직접 스캔하세요. 로컬 GUI 환경에서는 대시보드가 자동으로 브라우저에 열립니다.
|
|
99
|
-
|
|
100
|
-
**"page 미attach" — list_pages가 빈 배열 반환**
|
|
101
|
-
|
|
102
|
-
relay에 붙은 페이지가 없는 상태입니다. `start_attach` → QR 스캔 순서로 폰을 다시 진입시키세요. MCP 에러 메시지가 "페이지가 attach 안 됨. start_attach → QR 스캔."으로 뜨면 이 케이스입니다.
|
|
103
|
-
|
|
104
|
-
**"tunnel down" — 터널 응답 없음 또는 timeout**
|
|
105
|
-
|
|
106
|
-
cloudflared quick tunnel은 수 시간 후 drop될 수 있습니다. `debugger` 프로세스를 재시작하면 새 tunnel URL이 발급됩니다. 재발급 후 QR을 다시 스캔하세요. (관련: [#290](https://github.com/apps-in-toss-community/devtools/issues/290))
|
|
107
|
-
|
|
108
|
-
**"page crash" — list_pages에 crashDetectedAt이 찍힘**
|
|
109
|
-
|
|
110
|
-
폰 측 페이지가 OOM·JS exception·native bridge crash로 죽은 상태입니다. 앱을 재실행 후 `start_attach` → QR 스캔으로 다시 attach하세요. (관련: [#265](https://github.com/apps-in-toss-community/devtools/issues/265))
|
|
111
|
-
|
|
112
|
-
**"SDK 부재" — window.\__sdkCall 미주입**
|
|
113
|
-
|
|
114
|
-
`call_sdk` 호출 시 `ok: false, error: "window.__sdkCall is not available"` 에러가 뜨면 SDK 브리지가 아직 설치되지 않은 상태입니다. 아래 "on-device 디버깅 한 줄 설정" 섹션을 참고해 `import '@ait-co/debug-console/auto'`가 미니앱 entry에 추가돼 있는지 확인하세요. 환경 2(PWA)에서는 이 에러가 예상 결과입니다. (관련: [#285](https://github.com/apps-in-toss-community/devtools/issues/285))
|
|
115
|
-
|
|
116
|
-
**"QR 스캔했는데 인증 실패" — TOTP 만료**
|
|
117
|
-
|
|
118
|
-
`AIT_DEBUG_TOTP_SECRET` 설정 시 `start_attach`이 반환하는 attachUrl에는 TOTP 코드(`at=`)가 자동으로 포함됩니다. 코드 1개는 30초 창이고, relay는 만료 후 ~3분(±6 step) 이내 소급을 허용합니다. `start_attach`은 attach를 기다리는 동안 코드가 만료 창에 가까워지면 호출 안에서 코드를 자동 재발행하므로(재발행 횟수는 응답의 `totp.reminted`로 노출), 대기 중에는 보통 재호출이 필요 없습니다. 그래도 만료된 QR을 스캔해 relay가 인증을 거부하면 `start_attach`을 재호출해 새 URL과 QR을 발급받으세요.
|
|
119
|
-
|
|
120
|
-
---
|
|
9
|
+
- **mock SDK** — `check:sdk-exports`가 SDK 런타임 export를 전수 대조해요. 현재 2.x 라인 74종(도메인 객체 19개)·3.x 라인 92종(도메인 객체 34개)을 빠짐없이 덮어요.
|
|
10
|
+
- **플로팅 DevTools 패널** — 12개 탭에서 권한·네트워크·IAP·광고·위치·뷰포트 등 mock 상태를 즉시 전환해요.
|
|
11
|
+
- **디바이스 시뮬레이션** — iPhone/Galaxy 프리셋 13종 + 프레임/노치/홈 인디케이터/앱인토스 nav bar 오버레이.
|
|
12
|
+
- **모든 번들러 지원** — Vite · Webpack · Rspack · Rollup · esbuild.
|
|
121
13
|
|
|
122
14
|
## 설치
|
|
123
15
|
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
# 또는
|
|
127
|
-
pnpm add -D @apps-in-toss/devtools
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### 지원 SDK 버전
|
|
131
|
-
|
|
132
|
-
stable `latest` 하나가 web-framework 2.x와 3.x를 함께 지원합니다.
|
|
133
|
-
|
|
134
|
-
| SDK 라인 | 검증 버전 | 자동 facade | 수동 alias |
|
|
135
|
-
| -------- | --------- | --------------------------- | ------------------------------------------------- |
|
|
136
|
-
| **2.x** | `2.10.8` | unplugin이 설치 버전을 감지 | `@apps-in-toss/devtools/mock/2x` |
|
|
137
|
-
| **3.x** | `3.0.1` | unplugin이 설치 버전을 감지 | `@apps-in-toss/devtools/mock/3x` (`/mock` 기본값) |
|
|
138
|
-
|
|
139
|
-
peer 범위는 `>=2.6.0 <3.0.0 || >=3.0.1 <4.0.0`이며 optional입니다. MCP/패널만 쓰는 프로젝트에는 SDK를 강제로 설치하지 않습니다. 자동 감지가 어려운 monorepo에서는 `aitDevtools.vite({ sdkVersion: '2' })` 또는 `'3'`으로 명시하세요.
|
|
140
|
-
|
|
141
|
-
### 디버깅 패키지 (환경 2·3)
|
|
142
|
-
|
|
143
|
-
**환경 1(로컬 브라우저 + mock + 패널)만 쓴다면 위 설치가 전부입니다.** 아무것도 더 설치하지 않아도 됩니다.
|
|
144
|
-
|
|
145
|
-
on-device CDP 디버깅(환경 2의 `tunnel: { cdp: true }`, 환경 3의 relay attach)을 쓰려면 디버깅 패키지 두 개를 추가로 설치하세요:
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
pnpm add -D @ait-co/debugger @ait-co/debug-console
|
|
16
|
+
```sh
|
|
17
|
+
yarn add -D @apps-in-toss/devtools
|
|
149
18
|
```
|
|
150
19
|
|
|
151
|
-
|
|
152
|
-
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------ |
|
|
153
|
-
| [`@ait-co/debugger`](https://www.npmjs.com/package/@ait-co/debugger) | MCP 데몬 · 실기기 테스트 러너 · dev-bridge(환경 2 CDP relay + QR 대시보드) | 안 됨 — devDependency / `npx` 전용 |
|
|
154
|
-
| [`@ait-co/debug-console`](https://www.npmjs.com/package/@ait-co/debug-console) | on-device attach + 인앱 eruda 콘솔 | 됨 — debug 빌드에만 들어가는 유일한 패키지 |
|
|
20
|
+
Node 24 이상이 필요해요.
|
|
155
21
|
|
|
156
|
-
|
|
22
|
+
### 지원 SDK 버전
|
|
157
23
|
|
|
158
|
-
|
|
159
|
-
- `@ait-co/debug-console`이 없으면 unplugin이 in-app attach를 아예 주입하지 않습니다 — attach 코드가 번들에 구조적으로 들어갈 수 없다는 뜻이고, 이게 디버그 표면의 기술적 경계입니다.
|
|
24
|
+
하나의 패키지가 web-framework 2.x와 3.x를 함께 지원해요. peer 범위는 `>=2.6.0 <3.0.0 || >=3.0.1 <4.0.0`이고 **optional**이에요 — 패널만 쓰는 프로젝트라면 SDK를 설치하지 않아도 돼요.
|
|
160
25
|
|
|
161
|
-
|
|
26
|
+
| SDK 라인 | facade 자동 선택 | 수동 지정 시 alias 대상 |
|
|
27
|
+
| -------- | -------------------------------- | -------------------------------- |
|
|
28
|
+
| 2.x | unplugin이 설치 버전을 읽어 감지 | `@apps-in-toss/devtools/mock/2x` |
|
|
29
|
+
| 3.x | 위와 동일 (감지 실패 시 기본값) | `@apps-in-toss/devtools/mock/3x` |
|
|
162
30
|
|
|
163
|
-
|
|
31
|
+
자동 감지는 소비자 프로젝트의 `@apps-in-toss/web-framework/package.json` 버전을 읽어요. monorepo처럼 해석이 애매한 환경에서는 `sdkVersion: '2' | '3'`으로 못박으세요.
|
|
164
32
|
|
|
165
|
-
## 번들러
|
|
33
|
+
## 번들러 배선
|
|
166
34
|
|
|
167
35
|
### Vite
|
|
168
36
|
|
|
169
37
|
```ts
|
|
170
|
-
// vite.config.ts
|
|
38
|
+
// vite.config.ts
|
|
171
39
|
import aitDevtools from "@apps-in-toss/devtools/unplugin";
|
|
172
40
|
|
|
173
41
|
export default {
|
|
@@ -175,12 +43,12 @@ export default {
|
|
|
175
43
|
};
|
|
176
44
|
```
|
|
177
45
|
|
|
178
|
-
|
|
46
|
+
### Webpack / Rspack / Rollup / esbuild
|
|
179
47
|
|
|
180
|
-
|
|
48
|
+
같은 default export에서 번들러별 팩토리를 꺼내 써요. unplugin 엔트리는 ESM·CJS를 모두 출하하므로 `require`도 돼요.
|
|
181
49
|
|
|
182
50
|
```js
|
|
183
|
-
// webpack.config.js (ESM
|
|
51
|
+
// webpack.config.js (ESM)
|
|
184
52
|
import aitDevtools from "@apps-in-toss/devtools/unplugin";
|
|
185
53
|
config.plugins.push(aitDevtools.webpack());
|
|
186
54
|
|
|
@@ -189,327 +57,137 @@ const aitDevtools = require("@apps-in-toss/devtools/unplugin");
|
|
|
189
57
|
config.plugins.push(aitDevtools.webpack());
|
|
190
58
|
```
|
|
191
59
|
|
|
192
|
-
|
|
60
|
+
`aitDevtools.rspack()` · `aitDevtools.rollup()` · `aitDevtools.esbuild()`도 같은 방식이에요.
|
|
193
61
|
|
|
194
|
-
|
|
62
|
+
### 플러그인이 없는 환경 — 수동 alias
|
|
195
63
|
|
|
196
|
-
|
|
197
|
-
- Turbopack은 일반적으로 `next dev`에서만 사용되므로 별도의 production 가드가 필요하지 않습니다.
|
|
64
|
+
Turbopack처럼 unplugin을 지원하지 않는 번들러에서는 resolve alias로 직접 지정해요.
|
|
198
65
|
|
|
199
|
-
```
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
66
|
+
```ts
|
|
67
|
+
// vite.config.ts
|
|
68
|
+
export default {
|
|
69
|
+
resolve: {
|
|
70
|
+
alias: {
|
|
204
71
|
"@apps-in-toss/web-framework": "@apps-in-toss/devtools/mock",
|
|
205
72
|
},
|
|
206
73
|
},
|
|
207
74
|
};
|
|
208
75
|
```
|
|
209
76
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
```js
|
|
213
|
-
// next.config.js (Next.js 14 이하, web-framework 3.0+)
|
|
214
|
-
module.exports = {
|
|
215
|
-
experimental: {
|
|
216
|
-
turbo: {
|
|
217
|
-
resolveAlias: {
|
|
218
|
-
"@apps-in-toss/web-framework": "@apps-in-toss/devtools/mock",
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
```
|
|
77
|
+
`@apps-in-toss/web-framework` 하나만 alias하면 충분해요 — SDK 호출은 모두 이 패키지를 거치므로, 치환하는 순간 원본 모듈과 그것이 끌어오던 `@apps-in-toss/webview-bridge`가 모듈 그래프에서 함께 빠져요. 2.x 라인이면 alias 대상을 `@apps-in-toss/devtools/mock/2x`로 바꾸세요.
|
|
224
78
|
|
|
225
|
-
>
|
|
79
|
+
> 수동 alias만 쓰면 패널이 자동 주입되지 않아요. 진입점에 직접 추가하세요.
|
|
226
80
|
>
|
|
227
81
|
> ```ts
|
|
228
|
-
> // app/layout.tsx 또는 pages/_app.tsx
|
|
229
82
|
> import "@apps-in-toss/devtools/panel";
|
|
230
83
|
> ```
|
|
231
84
|
|
|
232
|
-
###
|
|
233
|
-
|
|
234
|
-
Next.js에서 Webpack 모드(`next dev` without `--turbo`, 또는 `next build`)를 사용하는 경우:
|
|
235
|
-
|
|
236
|
-
```js
|
|
237
|
-
// next.config.js (Webpack 모드)
|
|
238
|
-
const aitDevtools = require("@apps-in-toss/devtools/unplugin"); // CJS entrypoint 제공
|
|
239
|
-
|
|
240
|
-
module.exports = {
|
|
241
|
-
webpack: (config, { dev }) => {
|
|
242
|
-
if (dev) {
|
|
243
|
-
config.plugins.push(aitDevtools.webpack());
|
|
244
|
-
}
|
|
245
|
-
return config;
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### 수동 Alias 설정
|
|
85
|
+
### 플러그인이 인터셉트하는 모듈
|
|
251
86
|
|
|
252
|
-
|
|
87
|
+
| import 스펙 | 치환 대상 |
|
|
88
|
+
| ------------------------------ | ----------------------------- |
|
|
89
|
+
| `@apps-in-toss/web-framework` | 감지된 라인의 mock facade |
|
|
90
|
+
| `@apps-in-toss/webview-bridge` | 3.x mock facade |
|
|
91
|
+
| `@apps-in-toss/web-bridge` | 2.x mock facade (back-compat) |
|
|
92
|
+
| `@apps-in-toss/web-analytics` | 2.x mock facade (back-compat) |
|
|
253
93
|
|
|
254
|
-
|
|
255
|
-
// vite.config.ts (web-framework 3.0+)
|
|
256
|
-
import { defineConfig } from "vite";
|
|
257
|
-
|
|
258
|
-
export default defineConfig({
|
|
259
|
-
resolve: {
|
|
260
|
-
alias: {
|
|
261
|
-
"@apps-in-toss/web-framework": "@apps-in-toss/devtools/mock",
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
web-framework 2.x에서 수동 alias를 쓰면 위 값을 `@apps-in-toss/devtools/mock/2x`로 바꾸세요. 3.x는 명시적으로 `@apps-in-toss/devtools/mock/3x`를 써도 됩니다.
|
|
268
|
-
|
|
269
|
-
```js
|
|
270
|
-
// webpack.config.js (Webpack은 절대 경로 필요, web-framework 3.0+)
|
|
271
|
-
module.exports = {
|
|
272
|
-
resolve: {
|
|
273
|
-
alias: {
|
|
274
|
-
"@apps-in-toss/web-framework":
|
|
275
|
-
require.resolve("@apps-in-toss/devtools/mock"),
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
> **주의**: 수동 alias만 사용하면 DevTools Panel이 자동 주입되지 않습니다. 진입점 파일에 직접 import를 추가하세요:
|
|
282
|
-
>
|
|
283
|
-
> ```ts
|
|
284
|
-
> import "@apps-in-toss/devtools/panel"; // 진입점에 추가
|
|
285
|
-
> ```
|
|
94
|
+
서브패스 import(`@apps-in-toss/web-framework/<subpath>`)는 치환되지 않아요. 메인 엔트리만 대상이에요.
|
|
286
95
|
|
|
287
96
|
### 플러그인 옵션
|
|
288
97
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
98
|
+
`AitDevtoolsOptions`의 전부예요.
|
|
99
|
+
|
|
100
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
101
|
+
| -------------- | ------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `panel` | `boolean` | `true` | 진입점에 `@apps-in-toss/devtools/panel` import를 자동 주입 |
|
|
103
|
+
| `entryPattern` | `RegExp` | `/\/(main\|index\|entry\|app)\.[tj]sx?$/i` | 패널 자동 주입 대상 진입점 파일 패턴. 기본 패턴이 매칭하지 않는 파일명(예: 커스텀 브리지 파일)을 쓰는 프로젝트용 — `g`/`y` 플래그는 붙이지 마세요 |
|
|
104
|
+
| `sdkVersion` | `'auto' \| '2' \| '3'` | `'auto'` | mock facade 라인 선택. `'auto'`는 설치된 SDK 버전을 감지하고, 감지 실패 시 3.x |
|
|
105
|
+
| `forceEnable` | `boolean` | `false` | production에서도 devtools 활성화 (mock은 기본 OFF, `mock: true`로 함께 켤 수 있음) |
|
|
106
|
+
| `mock` | `boolean` | `true` (dev) / `false` (prod, forceEnable 포함) | SDK alias 활성화 여부 |
|
|
107
|
+
| `initialState` | `DeepPartial<MockState>` | `undefined` | mock 상태 초기값 커스터마이즈(예: IAP 상품 카탈로그). `DEFAULT_STATE`에 재귀 병합 — 객체는 병합, **배열은 통째 교체**. **Vite 전용** |
|
|
108
|
+
| `mcp` | `boolean` | `false` | Vite dev 서버에 mock state endpoint 등록 ([아래](#mcp-state-endpoint)) |
|
|
297
109
|
|
|
298
110
|
```ts
|
|
299
|
-
aitDevtools.vite({ panel: false }); //
|
|
111
|
+
aitDevtools.vite({ panel: false }); // 패널 없이 mock alias만
|
|
112
|
+
aitDevtools.vite({ entryPattern: /\/my-bridge\.[tj]sx?$/i }); // 비표준 진입점 파일명에도 패널 자동 주입
|
|
113
|
+
aitDevtools.vite({ sdkVersion: "2" }); // 2.x facade 강제
|
|
300
114
|
aitDevtools.vite({ forceEnable: true }); // production에서도 활성화 (mock 기본 OFF, panel ON)
|
|
301
115
|
aitDevtools.vite({ forceEnable: true, mock: true }); // production에서 mock도 활성화
|
|
302
|
-
aitDevtools.vite({
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
번들러 설정에서 플러그인 자체를 조건부로 제외할 수도 있습니다:
|
|
321
|
-
|
|
322
|
-
```ts
|
|
323
|
-
// vite.config.ts
|
|
324
|
-
import { defineConfig } from "vite";
|
|
325
|
-
import aitDevtools from "@apps-in-toss/devtools/unplugin";
|
|
326
|
-
|
|
327
|
-
export default defineConfig(({ command }) => ({
|
|
328
|
-
plugins: [...(command === "serve" ? [aitDevtools.vite()] : [])],
|
|
329
|
-
}));
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
```js
|
|
333
|
-
// webpack.config.js (Rspack도 동일)
|
|
334
|
-
const aitDevtools = require("@apps-in-toss/devtools/unplugin");
|
|
335
|
-
const plugins = [];
|
|
336
|
-
if (process.env.NODE_ENV !== "production") {
|
|
337
|
-
plugins.push(aitDevtools.webpack());
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
> Next.js 설정은 위의 [Next.js (Webpack)](#nextjs-webpack) 및 [Next.js (Turbopack)](#nextjs-turbopack) 섹션을 참고하세요.
|
|
342
|
-
|
|
343
|
-
## Run on a real phone (실기기 미리보기)
|
|
344
|
-
|
|
345
|
-
데스크톱 크롬에서 잘 돌던 미니앱을 **실제 폰**에서 보고 싶을 때. Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, **계정 불필요**)로 노출하고, 폰에는 고정 URL의 launcher PWA를 한 번만 추가해 그 안에서 매번의 tunnel URL을 띄웁니다.
|
|
346
|
-
|
|
347
|
-
셋업은 세 갈래입니다:
|
|
348
|
-
|
|
349
|
-
- **프로젝트당 1회** — `vite.config`에 옵션 + `package.json`에 pnpm 설정 + (선택) `dev:phone` 스크립트
|
|
350
|
-
- **폰당 1회** — launcher PWA를 홈 화면에 추가
|
|
351
|
-
- **매 세션** — `pnpm dev:phone` (또는 `AIT_TUNNEL=1 pnpm dev`) 한 줄
|
|
352
|
-
|
|
353
|
-
### 1. 프로젝트당 1회 셋업
|
|
354
|
-
|
|
355
|
-
(a) **`vite.config.ts`에 `tunnel` 옵션 추가** — 항상 켜져 있어 매번 cloudflared가 떠도 괜찮으면 `tunnel: true`, 평소엔 끄고 명시할 때만 켜고 싶으면 env-gate 권장:
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
// vite.config.ts
|
|
359
|
-
import { defineConfig } from "vite";
|
|
360
|
-
import aitDevtools from "@apps-in-toss/devtools/unplugin";
|
|
361
|
-
|
|
362
|
-
export default defineConfig({
|
|
363
|
-
plugins: [
|
|
364
|
-
aitDevtools.vite({
|
|
365
|
-
tunnel: !!process.env.AIT_TUNNEL, // 평소 OFF, AIT_TUNNEL=1 일 때만 ON
|
|
366
|
-
}),
|
|
367
|
-
],
|
|
368
|
-
});
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
> `process.env.AIT_TUNNEL`은 `vite.config.ts`를 로드하는 시점(= vite 프로세스 기동 시)에 평가됩니다. 따라서 env 변수는 **vite를 띄우기 전에** 설정되어 있어야 합니다 (아래 (c)의 `dev:phone` 스크립트가 이를 자동으로 해결합니다).
|
|
372
|
-
|
|
373
|
-
> on-device CDP 디버깅까지 켜려면 `tunnel: process.env.AIT_TUNNEL ? { cdp: true } : false`처럼 객체 형태로 줍니다. 그러면 HTTP 터널과 별도로 Chii relay가 떠서, QR 한 번으로 화면 미리보기와 CDP attach가 동시에 열립니다. AI host MCP를 그 relay에 붙이면 실기기 WebKit의 DOM·콘솔·예외·`measure_safe_area`를 관측합니다 (`call_sdk`는 환경 2에서 mock).
|
|
374
|
-
|
|
375
|
-
(b) **`package.json`에 pnpm 10+ 빌드 스크립트 허용** — pnpm은 보안상 dependency의 postinstall을 기본 차단합니다. `cloudflared`는 postinstall에서 바이너리(~38 MB)를 받으므로 명시 허용 필요:
|
|
376
|
-
|
|
377
|
-
```json
|
|
378
|
-
{
|
|
379
|
-
"pnpm": {
|
|
380
|
-
"onlyBuiltDependencies": ["cloudflared"]
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
> 명시하지 않아도 동작은 됩니다 — `tunnel.ts`가 첫 기동 시 `cloudflared.install()`을 lazy로 호출. 다만 `pnpm install`마다 "Ignored build scripts" 경고가 남고, 바이너리 다운로드가 첫 `pnpm dev` 시점으로 미뤄집니다. 참고: [`sdk-example#60`](https://github.com/apps-in-toss-community/sdk-example/pull/60).
|
|
386
|
-
|
|
387
|
-
(c) **(선택) `dev:phone` 스크립트** — env 변수 매번 타기 귀찮으면:
|
|
388
|
-
|
|
389
|
-
```json
|
|
390
|
-
{
|
|
391
|
-
"scripts": {
|
|
392
|
-
"dev": "vite",
|
|
393
|
-
"dev:phone": "AIT_TUNNEL=1 vite"
|
|
394
|
-
}
|
|
395
|
-
}
|
|
116
|
+
aitDevtools.vite({
|
|
117
|
+
initialState: {
|
|
118
|
+
iap: {
|
|
119
|
+
products: [
|
|
120
|
+
{
|
|
121
|
+
sku: "my-sku",
|
|
122
|
+
type: "CONSUMABLE",
|
|
123
|
+
displayName: "다이아몬드 10개",
|
|
124
|
+
displayAmount: "1,100원",
|
|
125
|
+
iconUrl: "",
|
|
126
|
+
description: "게임에서 사용할 수 있는 다이아몬드 10개",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}); // IAP mock 카탈로그를 프로젝트 상품으로 교체
|
|
132
|
+
aitDevtools.vite({ mcp: true }); // mock state endpoint 개방
|
|
396
133
|
```
|
|
397
134
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
폰에서 `https://devtools.aitc.dev/launcher/`를 열고 **홈 화면에 추가**합니다. (이 호스트는 `aitc.dev` 도메인 정리와 함께 사라지며, 대체 호스트는 없습니다.) launcher는 페이지 상단에 "Install launcher to your phone" 버튼을 띄우는데, 누르면 플랫폼별 네이티브 설치 흐름이 자동으로 안내됩니다 — Android Chrome은 인앱 설치 프롬프트, iOS Safari는 "공유 → 홈 화면에 추가" 일러스트, Firefox/Samsung Internet 등은 수동 안내 카드. launcher URL은 매번 동일하므로 폰당 한 번만 하면 됩니다.
|
|
401
|
-
|
|
402
|
-
launcher는 **PWA(홈 화면 앱)로 실행할 때만 동작**합니다. 일반 브라우저 탭에서 열면 설치 안내만 노출되고 입력/스캐너 UI는 숨겨집니다 — 크롬리스 standalone 디스플레이가 PWA 셸의 본질이라, 일반 탭에서의 동작은 의도적으로 막아둡니다.
|
|
403
|
-
|
|
404
|
-
### 3. 매 세션
|
|
405
|
-
|
|
406
|
-
1. 데스크톱에서 `pnpm dev:phone`을 실행합니다 (1-(c) 스크립트를 추가하지 않았다면 `AIT_TUNNEL=1 pnpm dev`). 터미널에 `https://*.trycloudflare.com` URL + ASCII QR이 출력됩니다.
|
|
407
|
-
2. 폰의 카메라(또는 launcher 아이콘 안의 "Scan QR")로 QR을 스캔합니다. QR은 `https://devtools.aitc.dev/launcher/?url=<tunnel>` deep-link라 launcher PWA가 자동으로 열리고 그날의 dev 앱이 풀스크린으로 뜹니다 — URL 붙여넣기 단계가 필요 없습니다.
|
|
408
|
-
3. 다음 세션엔 새 QR을 스캔만 하면 됩니다. launcher는 마지막 URL을 기억하고, "Rescan" 버튼으로 언제든 교체할 수 있습니다.
|
|
409
|
-
|
|
410
|
-
> QR을 일반 카메라 앱으로 찍었을 때 Safari/Chrome이 일반 탭이 아닌 설치된 launcher PWA로 곧장 라우팅하는 동작은 Android Chrome에서 가장 안정적이고, iOS Safari는 버전에 따라 일반 탭으로 폴백할 수 있습니다. 그 경우 launcher 홈 화면 아이콘에서 한 번 열어주면 그 안의 QR 스캐너로 다시 시도할 수 있습니다.
|
|
411
|
-
|
|
412
|
-
### 배경
|
|
413
|
-
|
|
414
|
-
> **왜 launcher를 거치나요?** quick tunnel URL은 매 실행마다 바뀌므로 그 URL 자체를 PWA로 설치하면 다음 세션엔 죽은 링크가 됩니다. cross-origin으로 페이지를 전환하면 iOS/Android 모두 standalone(크롬리스)이 깨집니다. → 고정 URL의 launcher를 한 번 설치하고, 그 안의 `<iframe>`으로 그날의 dev 앱을 full-bleed로 보여주는 구조입니다.
|
|
415
|
-
>
|
|
416
|
-
> quick tunnel은 **인증이 없고**, **URL이 매 실행마다 바뀌며**, **프로덕션용이 아닙니다**. (계정·도메인이 있다면 named tunnel로 고정 hostname을 받는 방식은 추후 `tunnel: { hostname }` 옵션으로 확장 여지를 남겨뒀습니다.)
|
|
417
|
-
>
|
|
418
|
-
> `tunnel` 옵션은 Vite dev 모드에서만 동작합니다 — production 빌드는 `forceEnable`이어도 터널을 띄우지 않습니다. 다른 번들러(Webpack/Rspack 등)에서는 무시됩니다. 이 옵션을 켜면 `cloudflared` / `qrcode-terminal`가 동적 import로만 로드되므로, 끄면 번들 그래프에 들어오지 않습니다.
|
|
419
|
-
|
|
420
|
-
### 한 줄 셋업
|
|
421
|
-
|
|
422
|
-
위 "프로젝트당 1회" 단계(vite.config 패치 + `onlyBuiltDependencies` + `dev:phone` 스크립트)는 [`agent-plugin`](https://github.com/apps-in-toss-community/agent-plugin)의 `/ait:setup-phone-preview` 한 명령으로 자동화돼 있습니다. 이 README가 그 자동화의 명세서 역할을 하므로, 수동 셋업 단계가 줄어들어도 동작 모델 자체는 동일합니다.
|
|
135
|
+
패널 자동 주입은 **진입점 파일에만** 걸려요 — `node_modules` 밖의 `.ts/.tsx/.js/.jsx` 중 파일명이 `main` · `index` · `entry` · `app`인 것(대소문자 무시). 진입점 이름이 다르면 직접 import하세요.
|
|
423
136
|
|
|
424
|
-
##
|
|
137
|
+
## 어디서 돌려서 무엇을 보나
|
|
425
138
|
|
|
426
|
-
|
|
139
|
+
**로컬 브라우저 개발**이 이 패키지의 주 용도예요. `yarn dev`로 띄운 데스크톱 브라우저에서 mock SDK가 실제 SDK 자리를 대신하고, 화면 우하단 **AIT** 버튼으로 패널을 열어 상태를 바꿔요. 토스 앱도 실기기도 필요 없고 HMR이 그대로 살아 있어 반복 주기가 가장 짧아요.
|
|
427
140
|
|
|
428
|
-
|
|
429
|
-
| ---------- | ----------------------------------------------------------------- | ------------------------------ |
|
|
430
|
-
| **mock** | `aitState`에 저장된 더미 데이터 반환 | 자동화 테스트, 고정된 시나리오 |
|
|
431
|
-
| **web** | 브라우저 네이티브 API 사용 (Geolocation, File API 등) | 실제 디바이스 기능 테스트 |
|
|
432
|
-
| **prompt** | DevTools Panel이 자동으로 열리고 사용자 입력 대기 (30초 타임아웃) | 수동 QA, 특정 값 입력 |
|
|
141
|
+
**실기기 디버깅**은 이 패키지의 범위 밖이며, 별도 패키지 두 개가 담당해요. 토스 앱 WebView에 올라간 미니앱을 원격으로 관측해야 할 때 써요.
|
|
433
142
|
|
|
434
|
-
|
|
143
|
+
- `@apps-in-toss/debugger` — MCP 디버깅 데몬과 on-device CDP relay를 제공해요. relay는 quick tunnel로 외부 접근 URL을 발급하고, 그 URL을 담은 QR을 띄워 폰이 attach하게 해요. devDependency / `npx` 전용이라 앱 코드에서 import할 표면 자체가 없어요.
|
|
144
|
+
- `@apps-in-toss/debug-console` — 미니앱 안에서 그 relay에 붙는 유일한 런타임 조각이에요. 진입점에 `import '@apps-in-toss/debug-console/auto'` 한 줄을 넣으면, URL에 `?debug=1`과 유효한 `?relay=<wss-url>`이 함께 실려 들어온 경우에만 attach해요. 게이트가 막히면 아무것도 하지 않으므로 평범한 프로덕션 로드에는 영향이 없어요.
|
|
435
145
|
|
|
436
|
-
|
|
437
|
-
| --------------------------------------- | ---- | --- | ------ |
|
|
438
|
-
| `openCamera` | ✅ | ✅ | ✅ |
|
|
439
|
-
| `fetchAlbumPhotos` | ✅ | ✅ | ✅ |
|
|
440
|
-
| `getCurrentLocation` | ✅ | ✅ | ✅ |
|
|
441
|
-
| `startUpdateLocation` | ✅ | ✅ | ✅ |
|
|
442
|
-
| `getNetworkStatus` | ✅ | ✅ | — |
|
|
443
|
-
| `getClipboardText` / `setClipboardText` | ✅ | ✅ | — |
|
|
444
|
-
|
|
445
|
-
### 모드 설정 방법
|
|
446
|
-
|
|
447
|
-
```js
|
|
448
|
-
// 콘솔에서 개별 API 모드 변경
|
|
449
|
-
__ait.patch("deviceModes", { camera: "web", location: "prompt" });
|
|
450
|
-
|
|
451
|
-
// 또는 DevTools Panel의 Device 탭에서 드롭다운으로 전환
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
### 더미 이미지 관리
|
|
455
|
-
|
|
456
|
-
mock 모드에서 카메라/앨범 API는 더미 이미지를 반환합니다.
|
|
457
|
-
|
|
458
|
-
- **기본 플레이스홀더**: 파란색/녹색/주황색 320×240 이미지 3장 자동 생성
|
|
459
|
-
- **커스텀 이미지**: DevTools Panel의 Device 탭에서 파일 추가/제거 가능
|
|
460
|
-
- **콘솔에서 설정**: `__ait.patch('mockData', { images: ['data:image/png;base64,...'] })`
|
|
146
|
+
두 축은 독립적이에요. devtools의 mock·패널은 relay를 필요로 하지 않고, 반대로 실기기 디버깅은 실제 SDK를 상대하므로 mock이 개입하지 않아요. 설치·설정의 정본은 각 패키지 문서예요.
|
|
461
147
|
|
|
462
148
|
## Floating DevTools Panel
|
|
463
149
|
|
|
464
|
-
|
|
150
|
+
`@apps-in-toss/devtools/panel`을 import하면 DOM이 준비되는 대로 자동 마운트돼요. 마운트는 idempotent라 여러 번 import되거나 `mount()`를 다시 불러도 토글 버튼은 하나만 남아요. 패널은 React 19 트리지만 React가 dist에 함께 번들되므로, 소비자 프로젝트에 React를 요구하지 않아요.
|
|
465
151
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
| 탭 | 설명 |
|
|
469
|
-
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
470
|
-
| **Environment** | 플랫폼 OS (ios/android), 앱 버전, 환경 (toss/sandbox), 로케일, 네트워크 상태, Safe Area Insets, Navigation (SDK no-op API 호출값 관측) |
|
|
471
|
-
| **Presets** | 자주 쓰는 QA 시나리오(권한 거부, offline, 미로그인 등)를 한 클릭으로 적용/해제. 사용자 preset 저장/삭제 가능 |
|
|
472
|
-
| **Viewport** | 디바이스 프리셋(iPhone/Galaxy) + orientation 토글로 모바일 뷰포트 시뮬레이션 |
|
|
473
|
-
| **Permissions** | camera, photos, geolocation, clipboard, contacts, microphone 권한 상태 제어 (allowed/denied/notDetermined) |
|
|
474
|
-
| **Notifications** | 알림 동의 흐름의 다음 결과 선택 (신규 동의 / 이미 동의함 / 거부) |
|
|
475
|
-
| **Location** | 위도, 경도, 정확도 설정 |
|
|
476
|
-
| **Device** | API 모드 전환 (mock/web/prompt), 더미 이미지 관리 (추가/제거/기본값/초기화) |
|
|
477
|
-
| **IAP** | 다음 구매 결과 선택 (success/취소/에러 등), TossPay 결제 결과, 완료된 주문 내역 (최근 5건) |
|
|
478
|
-
| **Ads** | 전면 광고 load/show 트리거 및 마지막 광고 이벤트 로그 |
|
|
479
|
-
| **Events** | Back/Home 네비게이션 이벤트 트리거, 로그인 상태 토글 |
|
|
480
|
-
| **Analytics** | 기록된 분석 이벤트 실시간 로그 뷰어 (최근 30건, 타임스탬프/타입/파라미터) |
|
|
481
|
-
| **Storage** | `Storage` API로 저장된 항목 조회 및 초기화 |
|
|
482
|
-
|
|
483
|
-
> **prompt 모드 자동 열림**: prompt 모드로 설정된 API가 호출되면, Panel이 자동으로 Device 탭을 열고 사용자 입력 UI를 표시합니다.
|
|
152
|
+
헤더의 **EDIT / READ-ONLY** 토글로 편집 가능 여부를 전환할 수 있고, 로케일은 `navigator.language`로 자동 판별한 뒤 `localStorage`(`__ait_locale`)에 저장돼요 — 한국어·영어 카탈로그를 모두 출하해요.
|
|
484
153
|
|
|
485
|
-
###
|
|
486
|
-
|
|
487
|
-
실 토스 WebView에서 native bridge로만 발화하던 일부 no-op API(예: `setIosSwipeGestureEnabled`)는 mock에서 그 **마지막 호출값**을 관측 가능한 state로 비춥니다. Environment 탭의 **Navigation** 섹션이 이 값을 read-only로 표시합니다.
|
|
154
|
+
### 12개 탭
|
|
488
155
|
|
|
489
|
-
|
|
156
|
+
| 탭 | 하는 일 |
|
|
157
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| **Environment** | 플랫폼 OS · 앱 버전 · 운영 환경(toss/sandbox) · 로케일 · 네트워크 · Safe Area · Navigation(no-op API 마지막 호출값) |
|
|
159
|
+
| **Presets** | QA 시나리오 preset 적용/해제, 사용자 preset 저장·삭제 |
|
|
160
|
+
| **Viewport** | 디바이스 프리셋 + orientation + 프레임/nav bar 오버레이 |
|
|
161
|
+
| **Permissions** | camera · photos · geolocation · clipboard · contacts · microphone 권한 상태 전환 |
|
|
162
|
+
| **Notifications** | 알림 동의 흐름의 다음 결과 선택 |
|
|
163
|
+
| **Location** | 위도 · 경도 · 정확도 설정 |
|
|
164
|
+
| **Device** | 디바이스 API 모드 전환, 더미 이미지 추가/제거/초기화 |
|
|
165
|
+
| **IAP** | 상품 카탈로그 추가/편집/삭제, 다음 구매 결과, TossPay 결제 결과, 완료 주문 내역 |
|
|
166
|
+
| **Ads** | 광고 load/show 트리거와 마지막 광고 이벤트 로그 |
|
|
167
|
+
| **Events** | Back/Home 네비게이션 이벤트 트리거, 로그인 상태 토글 |
|
|
168
|
+
| **Analytics** | 기록된 분석 이벤트 실시간 로그 뷰어 |
|
|
169
|
+
| **Storage** | `Storage` API로 저장된 항목 조회 및 초기화 |
|
|
490
170
|
|
|
491
|
-
|
|
492
|
-
2. 앱의 toss-gated 가드(예: sdk-example `useDisableIosSwipeGestureInToss`)가 실행되며 `setIosSwipeGestureEnabled({ isEnabled: false })`를 호출.
|
|
493
|
-
3. Navigation 섹션의 `iOS swipe-back` 값이 `미호출` → `disabled`로 실시간 전환되는 것을 패널에서 확인. `AIT.getMockState()`로도 `navigation.iosSwipeGestureEnabled`를 대조할 수 있습니다.
|
|
171
|
+
### toss-gated 코드 경로를 dev에서 확인하기
|
|
494
172
|
|
|
495
|
-
|
|
173
|
+
실기기에서 native bridge로만 발화하던 no-op API는 mock이 **마지막 호출값**을 관측 가능한 상태로 비춰요. 예를 들어 Environment 탭에서 환경을 `sandbox` → `toss`로 바꾸면 `getOperationalEnvironment() === 'toss'`로 게이트된 코드가 실행되고, 그 코드가 부른 `setIosSwipeGestureEnabled({ isEnabled: false })`의 결과가 Environment 탭 Navigation 섹션에 read-only로 나타나요.
|
|
496
174
|
|
|
497
|
-
|
|
175
|
+
### Preset
|
|
498
176
|
|
|
499
|
-
내장 preset
|
|
177
|
+
한 시나리오에 여러 mock 키가 동시에 일정 상태여야 하는 경우를 한 클릭으로 맞춰요. 내장 preset 6종:
|
|
500
178
|
|
|
501
179
|
| ID | 의미 |
|
|
502
180
|
| ------------------- | ---------------------------------------------------------------- |
|
|
503
181
|
| `all-allowed` | 모든 권한 허용, WIFI, 로그인됨, IAP success — 기본 시나리오 복귀 |
|
|
504
182
|
| `permission-denied` | camera / photos / geolocation / contacts 거부 |
|
|
505
|
-
| `offline` | `getNetworkStatus` → OFFLINE, IAP `NETWORK_ERROR`, 결제
|
|
506
|
-
| `logged-out` | `auth.isLoggedIn=false
|
|
183
|
+
| `offline` | `getNetworkStatus` → OFFLINE, IAP `NETWORK_ERROR`, 결제 실패 |
|
|
184
|
+
| `logged-out` | `auth.isLoggedIn = false` |
|
|
507
185
|
| `iap-pending` | IAP `nextResult` → `PAYMENT_PENDING` |
|
|
508
186
|
| `ads-no-fill` | 광고 fill 실패 분기 |
|
|
509
187
|
|
|
510
|
-
|
|
188
|
+
preset이 건드리는 범위는 `networkStatus` · `permissions` · `auth` · `iap.nextResult` · `ads` · `payment`로 좁혀 두었어요 — viewport나 brand 같은 무관한 상태가 함께 흔들리지 않아요. 사용자 preset은 `localStorage`에 `__ait_preset:<id>` 한 키씩 저장돼요.
|
|
511
189
|
|
|
512
|
-
코드에서도
|
|
190
|
+
코드에서도 쓸 수 있어요.
|
|
513
191
|
|
|
514
192
|
```ts
|
|
515
193
|
import {
|
|
@@ -518,11 +196,9 @@ import {
|
|
|
518
196
|
saveUserPreset,
|
|
519
197
|
} from "@apps-in-toss/devtools";
|
|
520
198
|
|
|
521
|
-
// 내장 preset 적용
|
|
522
199
|
const offline = builtInPresets.find((p) => p.id === "offline")!;
|
|
523
200
|
applyPreset(offline.state);
|
|
524
201
|
|
|
525
|
-
// 커스텀 preset 저장
|
|
526
202
|
saveUserPreset("My QA scenario", {
|
|
527
203
|
networkStatus: "OFFLINE",
|
|
528
204
|
permissions: { camera: "denied" },
|
|
@@ -530,366 +206,157 @@ saveUserPreset("My QA scenario", {
|
|
|
530
206
|
});
|
|
531
207
|
```
|
|
532
208
|
|
|
533
|
-
###
|
|
209
|
+
### 디바이스 시뮬레이션 (Viewport 탭)
|
|
534
210
|
|
|
535
|
-
|
|
211
|
+
프리셋을 고르면 CSS 뷰포트 크기, DPR, safe area inset이 함께 적용되고 `SafeAreaInsets.get()` / `.subscribe()`가 그 값을 따라가요.
|
|
536
212
|
|
|
537
|
-
|
|
213
|
+
| 카테고리 | 프리셋 |
|
|
214
|
+
| -------- | ------------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| Apple | iPhone SE (3rd gen), iPhone 15 Pro, iPhone 16e, iPhone 17, iPhone Air, iPhone 17 Pro, iPhone 17 Pro Max |
|
|
216
|
+
| Samsung | Galaxy S26, S26+, S26 Ultra, Z Flip7, Z Fold7 (folded / unfolded) |
|
|
217
|
+
| 기타 | Custom (width/height 직접 입력, 1–4096으로 클램프), None (기본) |
|
|
538
218
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
mount(); // 깨끗한 상태로 다시 마운트. 중복 <style>·listener 없음.
|
|
545
|
-
```
|
|
219
|
+
- **Orientation** — `auto`이면 앱이 부른 `setDeviceOrientation` 값을 따르고, `portrait`/`landscape`를 고르면 패널이 강제해요.
|
|
220
|
+
- **Show frame** — 베젤 · 노치/Dynamic Island/punch-hole · 홈 인디케이터를 그려요. OS 노치는 실기기에서도 WebView 바깥이라 body 위쪽에 그려요.
|
|
221
|
+
- **Apps in Toss nav bar** — 호스트 상단 nav bar를 54px로 오버레이해요. `partner`는 콘텐츠를 그 높이만큼 밀어내고(그래서 `SafeAreaInsets.get().top`이 곧 nav bar 높이), `game`은 투명하게 떠서 밀어내지 않아요.
|
|
222
|
+
- 상태는 `sessionStorage`(`__ait_viewport`)에 저장되어 reload 후 복원돼요.
|
|
223
|
+
- 기기 프리셋이 활성인 동안에는 `navigator.userAgent` · `navigator.platform` · `devicePixelRatio` · `screen.*`과 `getPlatformOS()`가 읽는 플랫폼 값을 그 기기 값으로 override해요. `none`/`custom`에서는 override하지 않아요.
|
|
546
224
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
## Device simulation (Viewport 탭)
|
|
550
|
-
|
|
551
|
-
데스크탑 브라우저에서 모바일 미니앱을 개발할 때, 실제 디바이스 해상도/safe area/노치/홈 인디케이터/앱인토스 nav bar를 반영해 레이아웃을 검증할 수 있습니다.
|
|
552
|
-
|
|
553
|
-
### 프리셋 (2026)
|
|
554
|
-
|
|
555
|
-
| 카테고리 | 기기 |
|
|
556
|
-
| -------- | ---------------------------------------------------------------------------------------- |
|
|
557
|
-
| Apple | iPhone SE (3rd gen), iPhone 16e, iPhone 17, iPhone Air, iPhone 17 Pro, iPhone 17 Pro Max |
|
|
558
|
-
| Samsung | Galaxy S26, S26+, S26 Ultra, Z Flip7, Z Fold7 (folded / unfolded) |
|
|
559
|
-
| 기타 | Custom (width/height 직접 입력), None (기본) |
|
|
560
|
-
|
|
561
|
-
> **Galaxy S26 시리즈** (2026-03-11 출시)의 CSS viewport 값은 [phone-simulator.com](https://www.phone-simulator.com/)의 측정치를 사용합니다. safe area insets는 토스 호스트 환경 실측 전까지 S25 값을 잠정 사용 — 픽셀 단위 정확도가 필요한 QA는 실 기기에서 한 번 더 확인하세요.
|
|
562
|
-
>
|
|
563
|
-
> iPhone 17 시리즈는 2025-09에 출시되어 실제 spec 기반입니다.
|
|
564
|
-
|
|
565
|
-
각 프리셋은 다음 정보를 포함합니다:
|
|
566
|
-
|
|
567
|
-
- **CSS viewport** (portrait `width × height`)
|
|
568
|
-
- **DPR** (devicePixelRatio: 2, 3, 3.5 등)
|
|
569
|
-
- **Notch** 종류 (`none` / `notch` / `dynamic-island` / `punch-hole-center`)
|
|
570
|
-
- **notch inset** (OS 노치/status bar — landscape 좌우 인셋 + 시각 노치용, 기기별)
|
|
571
|
-
- **nav bar height** (토스 호스트 nav bar — `partner` portrait의 `SafeAreaInsets.get().top`, 실측 54px)
|
|
572
|
-
- **home-indicator inset** (`safeAreaBottom`, 기기별)
|
|
573
|
-
|
|
574
|
-
### Orientation
|
|
575
|
-
|
|
576
|
-
- **auto** (기본) — Panel이 강제하지 않음. 앱의 `setDeviceOrientation` 호출이 별도 필드(`appOrientation`)에 기록되어 effective orientation 결정에 쓰입니다. 같은 앱이 여러 번 호출해도 매번 정상 반영됩니다.
|
|
577
|
-
- **portrait / landscape** — Panel이 override. 앱의 `setDeviceOrientation` 호출은 무시되고 `console.warn`으로 알림.
|
|
578
|
-
|
|
579
|
-
Landscape로 전환하면:
|
|
580
|
-
|
|
581
|
-
- CSS viewport width/height가 swap됩니다.
|
|
582
|
-
- iPhone(notch/Dynamic Island) 프리셋은 safe area의 top이 0이 되고, **Notch side** 토글(left/right, default left)에 따라 한쪽 변에만 인셋이 생깁니다 (실 기기 동작과 일치).
|
|
583
|
-
- Android(punch-hole) 프리셋은 status bar가 top에 유지됩니다.
|
|
584
|
-
|
|
585
|
-
### Frame + 노치 + 홈 인디케이터 + 앱인토스 nav bar
|
|
586
|
-
|
|
587
|
-
**Show frame** 토글을 켜면:
|
|
588
|
-
|
|
589
|
-
- 디바이스 베젤을 모사하는 border-radius + box-shadow
|
|
590
|
-
- Notch / Dynamic Island / punch-hole 오버레이 — WebView(body) **밖** 위쪽 status bar 영역에 그립니다. 실기기에서 OS 노치는 WebView 뷰포트 바깥이라 `env(safe-area-inset-top)`이 0이기 때문입니다.
|
|
591
|
-
- 홈 인디케이터 pill (iPhone 등 `safeAreaBottom > 0` 디바이스에 한정, body 하단에 배치)
|
|
592
|
-
- 앱 이름은 `aitState.brand.displayName`을 사용 (Environment 탭에서 변경 가능, 자동 갱신)
|
|
593
|
-
- 뒤로가기 버튼은 `__ait:backEvent`를 트리거하고, X 버튼은 `closeView()`를 호출 — 실제 SDK 이벤트 플러밍을 패널에서 직접 검증할 수 있습니다.
|
|
594
|
-
|
|
595
|
-
**Show Apps in Toss nav bar** 토글(기본 on)을 켜면:
|
|
596
|
-
|
|
597
|
-
- 토스 호스트의 상단 nav bar를 54px 높이로 오버레이. `Nav bar type`에 따라 모양이 다릅니다:
|
|
598
|
-
- `partner` (비게임 기본): 흰 배경 + 뒤로가기 / 앱 아이콘·이름 / ⋯ / ×. 콘텐츠를 nav bar 높이만큼 아래로 밀어냅니다.
|
|
599
|
-
- `game`: 투명 배경 + ⋯ / × 만. 게임 캔버스 위에 떠 있어 콘텐츠를 밀어내지 않습니다 — 인게임 화면은 full-screen이 [출시 요건](https://developers-apps-in-toss.toss.im/checklist/app-game.html).
|
|
600
|
-
- nav bar는 WebView(body) 좌표계의 **최상단(top 0)**에 앉습니다. 실기기에서 OS 노치는 WebView 밖(위쪽 status bar)이라 `env(safe-area-inset-top)`이 0이고, 콘텐츠 영역은 nav bar 바로 아래(= `SafeAreaInsets.get().top`)에서 시작하기 때문입니다 — 시뮬레이터는 이 스택(노치 status bar → nav bar → 콘텐츠)을 그대로 재현합니다.
|
|
601
|
-
- `partner` WebView에서는 **이 nav bar 높이가 곧 `SafeAreaInsets.get().top`** 입니다. iPhone 15 Pro on-device relay 실측([devtools#190](https://github.com/apps-in-toss-community/devtools/issues/190))에서 `env(safe-area-inset-top)`은 0(노치는 WebView 뷰포트 밖)이고 `SafeAreaInsets.get().top`은 54px이었으며, 그 54px가 호스트 nav bar 높이였습니다. 즉 `partner` 앱은 콘텐츠 상단을 `insets.top`만큼만 보정하면 됩니다(별도 `+ navBarHeight` 불필요). `game`은 콘텐츠를 밀어내지 않으므로 top inset이 0입니다. 이 54px는 iOS partner에서 실측됐고 Android nav bar 높이는 같은 값을 잠정 적용합니다. SDK의 `webViewProps.type`은 `partner` / `game` 외에 `external`도 있습니다 (현재 패널은 앞 둘만 시뮬레이션).
|
|
602
|
-
|
|
603
|
-
### 콘솔에서 직접 조작
|
|
225
|
+
콘솔에서 직접 조작할 수도 있어요.
|
|
604
226
|
|
|
605
227
|
```js
|
|
606
|
-
|
|
607
|
-
__ait.patch("viewport", {
|
|
608
|
-
preset: "iphone-17-pro",
|
|
609
|
-
orientation: "auto",
|
|
610
|
-
frame: true,
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
// Landscape 강제 (앱의 setDeviceOrientation 호출은 무시됨)
|
|
228
|
+
__ait.patch("viewport", { preset: "iphone-17-pro", frame: true });
|
|
614
229
|
__ait.patch("viewport", { orientation: "landscape" });
|
|
615
|
-
|
|
616
|
-
// Landscape 시 노치 위치 (iOS 기본 'left')
|
|
617
|
-
__ait.patch("viewport", { landscapeSide: "right" });
|
|
618
|
-
|
|
619
|
-
// Custom 크기 (1 ≤ value ≤ 4096으로 자동 클램프)
|
|
620
230
|
__ait.patch("viewport", {
|
|
621
231
|
preset: "custom",
|
|
622
232
|
customWidth: 360,
|
|
623
233
|
customHeight: 740,
|
|
624
234
|
});
|
|
235
|
+
__ait.patch("viewport", { aitNavBarType: "game" });
|
|
236
|
+
__ait.patch("viewport", { preset: "none" }); // 해제
|
|
237
|
+
```
|
|
625
238
|
|
|
626
|
-
|
|
627
|
-
__ait.patch("viewport", { aitNavBar: false });
|
|
239
|
+
**한계** — 뷰포트가 활성인 동안에는 스크롤 컨테이너가 `window`가 아니라 `document.body`예요. 또한 위 override는 page-JS가 읽는 값만 바꾸며, 실제 CSS media query나 터치 이벤트는 호스트 브라우저 그대로예요. 픽셀·입력 단위까지 정확한 emulation이 필요하면 브라우저 DevTools의 device mode를 쓰세요.
|
|
628
240
|
|
|
629
|
-
|
|
630
|
-
|
|
241
|
+
### mount / dispose
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { disposePanel, mount } from "@apps-in-toss/devtools/panel";
|
|
631
245
|
|
|
632
|
-
//
|
|
633
|
-
|
|
246
|
+
disposePanel(); // 토글·패널·주입된 <style>·리스너 전부 제거. 중복 호출 안전.
|
|
247
|
+
mount(); // 깨끗한 상태로 재마운트
|
|
634
248
|
```
|
|
635
249
|
|
|
636
|
-
|
|
250
|
+
`disposePanel()`은 뷰포트 시뮬레이션도 함께 원복해요.
|
|
637
251
|
|
|
638
|
-
|
|
252
|
+
## 디바이스 API 모드
|
|
639
253
|
|
|
640
|
-
|
|
641
|
-
- **Safe area**: `T54 R0 B34 L0` (partner nav bar 기준 — top이 곧 nav bar 높이)
|
|
642
|
-
- **AIT nav bar**: `54px → SafeArea top · partner`
|
|
254
|
+
카메라·앨범·위치는 세 가지 모드로, 네트워크·클립보드는 두 가지 모드로 동작해요.
|
|
643
255
|
|
|
644
|
-
|
|
256
|
+
| 모드 | 동작 | 쓰임 |
|
|
257
|
+
| -------- | ------------------------------------------------- | ---------------------------- |
|
|
258
|
+
| `mock` | 상태에 저장된 더미 데이터 반환 | 자동화 테스트, 고정 시나리오 |
|
|
259
|
+
| `web` | 브라우저 네이티브 API 사용 (Geolocation, File 등) | 실제 브라우저 기능 확인 |
|
|
260
|
+
| `prompt` | 패널이 Device 탭을 열고 사용자 입력을 기다림 | 수동 QA |
|
|
645
261
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
- 패널을 동적으로 제거하고 싶다면 `disposeViewport()`를 export로 제공합니다.
|
|
651
|
-
- **기기 프리셋이 active일 때(= `none`/`custom` 아님) 브라우저 특성을 그 기기와 정합시킵니다**: `navigator.userAgent`(토스 WebView 형태 — `… AppsInToss TossApp/<appVersion>`), `navigator.platform`, `window.devicePixelRatio`(preset DPR), `screen.width`/`height`(CSS px × DPR), 그리고 `getPlatformOS()`가 읽는 `platform`(Apple→`ios` / Galaxy→`android`)을 모두 그 기기 값으로 override합니다. 특정 기기 frame을 제공하는 이상 UA/DPR만 호스트 데스크톱 값으로 남으면 비일관적이기 때문입니다. `none`/`custom`에선 override를 걸지 않아 일반 dev의 호스트 환경을 건드리지 않습니다.
|
|
262
|
+
| 대상 | mock | web | prompt |
|
|
263
|
+
| -------------------------------- | ---- | --- | ------ |
|
|
264
|
+
| `camera` · `photos` · `location` | ✅ | ✅ | ✅ |
|
|
265
|
+
| `network` · `clipboard` | ✅ | ✅ | — |
|
|
652
266
|
|
|
653
|
-
|
|
267
|
+
```js
|
|
268
|
+
__ait.patch("deviceModes", { camera: "web", location: "prompt" });
|
|
269
|
+
```
|
|
654
270
|
|
|
655
|
-
|
|
656
|
-
- **추정 safe area** — Galaxy S26 시리즈는 출시 spec(phone-simulator.com 측정치) 기반이지만 safe area는 S25 값을 잠정 사용합니다 — 픽셀 단위 정확도가 필요한 QA는 실 기기 확인을 권장합니다.
|
|
657
|
-
- **기기 특성 override는 JS 읽기값만 바꿉니다** — 프리셋이 거는 `navigator.userAgent`·`devicePixelRatio`·`screen.*` override는 page-JS가 읽는 값만 그 기기로 보이게 합니다. 실 CSS media query(`@media (resolution)`, `@media (pointer)`), 실제 터치 이벤트, 엔진 레벨 레이아웃 단위는 호스트 브라우저 값이 그대로입니다 (프리셋 frame이 시각적 width/height는 이미 강제하므로 레이아웃은 근사됩니다). 픽셀·입력 단위까지 완전한 emulation이 필요하면 Chrome DevTools device-mode(또는 CDP)를 쓰세요.
|
|
271
|
+
mock 모드의 카메라/앨범은 320×240 플레이스홀더 3장을 반환해요. Device 탭에서 이미지를 갈아끼우거나, `__ait.patch('mockData', { images: [...] })`로 넣을 수 있어요.
|
|
658
272
|
|
|
659
273
|
## `window.__ait` 콘솔 API
|
|
660
274
|
|
|
661
|
-
브라우저 콘솔에서
|
|
275
|
+
브라우저 콘솔에서 mock 상태를 직접 조작해요.
|
|
662
276
|
|
|
663
277
|
```js
|
|
664
|
-
//
|
|
665
|
-
__ait.state; // 전체 상태
|
|
666
|
-
__ait.state.
|
|
667
|
-
__ait.state.
|
|
668
|
-
__ait.state.deviceModes; // 각 API의 현재 모드
|
|
278
|
+
// 조회
|
|
279
|
+
__ait.state; // 전체 상태
|
|
280
|
+
__ait.state.auth.isLoggedIn;
|
|
281
|
+
__ait.state.deviceModes;
|
|
669
282
|
|
|
670
|
-
//
|
|
283
|
+
// 갱신 (얕은 병합 / 중첩 병합)
|
|
671
284
|
__ait.update({ platform: "android", locale: "en-US" });
|
|
672
|
-
__ait.update({ networkStatus: "OFFLINE" });
|
|
673
|
-
|
|
674
|
-
// 중첩 상태 업데이트
|
|
675
285
|
__ait.patch("permissions", { camera: "denied" });
|
|
676
|
-
__ait.patch("deviceModes", { location: "web" });
|
|
677
286
|
__ait.patch("iap", { nextResult: "USER_CANCELED" });
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
|
|
287
|
+
|
|
288
|
+
// 여러 변경을 notify 1회로 묶기
|
|
289
|
+
__ait.transaction(() => {
|
|
290
|
+
__ait.update({ networkStatus: "OFFLINE" });
|
|
291
|
+
__ait.patch("iap", { nextResult: "NETWORK_ERROR" });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// 실패-모드 다이얼 — 실기기에서만 나던 거부를 로컬에서 재현
|
|
295
|
+
__ait.patch("failureModes", { loadAdMob: "PLACEMENT_ID_FETCH_FAILED" });
|
|
681
296
|
__ait.patch("failureModes", {
|
|
682
297
|
throttled: { methods: ["getCurrentLocation"], intervalMs: 1000 },
|
|
683
298
|
});
|
|
684
299
|
|
|
685
|
-
// 이벤트
|
|
300
|
+
// 이벤트 / 로그 / 초기화 / 구독
|
|
686
301
|
__ait.trigger("backEvent");
|
|
687
|
-
__ait.trigger("homeEvent");
|
|
688
|
-
|
|
689
|
-
// 분석 이벤트 수동 기록
|
|
690
302
|
__ait.logAnalytics({ type: "click", params: { button: "purchase" } });
|
|
303
|
+
__ait.reset(); // deviceId는 유지
|
|
304
|
+
const unsubscribe = __ait.subscribe(() => console.log(__ait.state));
|
|
305
|
+
```
|
|
691
306
|
|
|
692
|
-
|
|
693
|
-
__ait.reset();
|
|
307
|
+
`failureModes`는 값을 설정한 API만 거부하고 나머지는 기존대로 성공해요 — 다이얼을 쓰지 않으면 동작 변화가 전혀 없어요. 실패 envelope의 모양은 `failureModes.sdkLine`(`'2.x'` 기본 / `'3.x'`)을 따라요.
|
|
694
308
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
});
|
|
699
|
-
unsubscribe(); // 구독 해제
|
|
700
|
-
```
|
|
309
|
+
## Mock이 커버하는 SDK 표면
|
|
310
|
+
|
|
311
|
+
mock은 SDK의 **런타임 export 전수**를 덮는 것을 목표로 하고, `check:sdk-exports`가 이를 CI에서 강제해요. 이 스크립트는 두 단계로 검사해요.
|
|
701
312
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
### 화면/네비게이션
|
|
714
|
-
|
|
715
|
-
| API | Mock 동작 |
|
|
716
|
-
| --------------------------- | ---------------------------------------------- |
|
|
717
|
-
| `closeView` | `window.history.back()` 호출 |
|
|
718
|
-
| `openURL` | `window.open()`으로 새 탭 |
|
|
719
|
-
| `share` | `navigator.share()` 사용 (미지원 시 콘솔 출력) |
|
|
720
|
-
| `getTossShareLink` | `https://toss.im/share/mock{path}` 반환 |
|
|
721
|
-
| `setIosSwipeGestureEnabled` | 콘솔 로그 (no-op) |
|
|
722
|
-
| `setDeviceOrientation` | 콘솔 로그 (no-op) |
|
|
723
|
-
| `setScreenAwakeMode` | `{ enabled }` 반환 |
|
|
724
|
-
| `setSecureScreen` | `{ enabled }` 반환 |
|
|
725
|
-
| `requestReview` | no-op (`.isSupported()` 메서드 포함) |
|
|
726
|
-
|
|
727
|
-
### 환경 정보
|
|
728
|
-
|
|
729
|
-
| API | Mock 동작 |
|
|
730
|
-
| --------------------------- | ------------------------------------------------------------------ |
|
|
731
|
-
| `getPlatformOS` | state의 platform 반환 (기본: `'ios'`) |
|
|
732
|
-
| `getOperationalEnvironment` | state의 environment 반환 (기본: `'sandbox'`) |
|
|
733
|
-
| `getTossAppVersion` | state의 appVersion 반환 (기본: `'5.240.0'`) |
|
|
734
|
-
| `isMinVersionSupported` | 시맨틱 버전 비교 수행 |
|
|
735
|
-
| `getSchemeUri` | state의 schemeUri 또는 `window.location.pathname` |
|
|
736
|
-
| `getLocale` | state의 locale 반환 (기본: `'ko-KR'`) |
|
|
737
|
-
| `getDeviceId` | localStorage에 저장된 고유 UUID 반환 |
|
|
738
|
-
| `getGroupId` | state의 groupId 반환 |
|
|
739
|
-
| `getNetworkStatus` | 모드에 따라 state 또는 브라우저 API 사용 |
|
|
740
|
-
| `getServerTime` | `Date.now()` 반환 |
|
|
741
|
-
| `env.getDeploymentId` | state의 deploymentId 반환 |
|
|
742
|
-
| `getAppsInTossGlobals` | `{ deploymentId, brandDisplayName, brandIcon, brandPrimaryColor }` |
|
|
743
|
-
|
|
744
|
-
### Safe Area
|
|
745
|
-
|
|
746
|
-
| API | Mock 동작 |
|
|
747
|
-
| ------------------------------------- | ----------------------------------------------------------------------------- |
|
|
748
|
-
| `SafeArea.get` / `SafeAreaInsets.get` | `{ top, bottom, left, right }` 동기 반환 (3.x 이름 + 2.x alias) |
|
|
749
|
-
| `SafeAreaInsets.subscribe` | 상태 변경 시 콜백 호출, unsubscribe 함수 반환 |
|
|
750
|
-
| `getSafeAreaInsets` | 2.x facade는 실측대로 Promise, 3.x facade는 inset 객체 동기 반환 (deprecated) |
|
|
751
|
-
|
|
752
|
-
### 디바이스 기능
|
|
753
|
-
|
|
754
|
-
| API | Mock 동작 |
|
|
755
|
-
| ----------------------------------------------- | -------------------------------------------------------------------- |
|
|
756
|
-
| `Storage.getItem/setItem/removeItem/clearItems` | localStorage에 `__ait_storage:` prefix로 저장 |
|
|
757
|
-
| `getCurrentLocation` | 모드별: mock(state 좌표), web(Geolocation API), prompt(Panel 입력) |
|
|
758
|
-
| `startUpdateLocation` | mock(랜덤 좌표 변동), web(watchPosition), prompt(반복 입력) |
|
|
759
|
-
| `openCamera` | mock(더미 이미지), web(파일 선택기), prompt(Panel 파일 입력) |
|
|
760
|
-
| `fetchAlbumPhotos` | mock(더미 이미지 배열), web(파일 다중 선택), prompt(Panel 파일 입력) |
|
|
761
|
-
| `fetchContacts` | 페이지네이션 지원 mock 연락처 반환, `query.contains` 검색 |
|
|
762
|
-
| `getClipboardText` / `setClipboardText` | mock(state 저장) 또는 web(Clipboard API) |
|
|
763
|
-
| `generateHapticFeedback` | 콘솔 로그 + analytics 기록 |
|
|
764
|
-
| `saveBase64Data` | anchor 엘리먼트로 파일 다운로드 |
|
|
765
|
-
|
|
766
|
-
### IAP/결제
|
|
767
|
-
|
|
768
|
-
| API | Mock 동작 |
|
|
769
|
-
| ------------------------------------- | ---------------------------------------------------------------- |
|
|
770
|
-
| `IAP.createOneTimePurchaseOrder` | 300ms 딜레이 후 state의 `nextResult`에 따라 성공/실패 시뮬레이션 |
|
|
771
|
-
| `IAP.createSubscriptionPurchaseOrder` | 위와 동일한 흐름 |
|
|
772
|
-
| `IAP.getProductItemList` | state의 상품 목록 반환 |
|
|
773
|
-
| `IAP.getPendingOrders` | 대기 중 주문 목록 |
|
|
774
|
-
| `IAP.getCompletedOrRefundedOrders` | 완료/환불 주문 목록 |
|
|
775
|
-
| `IAP.completeProductGrant` | 대기 → 완료 주문 이동 |
|
|
776
|
-
| `IAP.getSubscriptionInfo` | 활성 구독 mock (30일 만료, 자동 갱신) |
|
|
777
|
-
| `checkoutPayment` | 300ms 딜레이 후 state의 결제 결과 반환 (TossPay) |
|
|
778
|
-
|
|
779
|
-
**IAP 구매 시뮬레이션 흐름:**
|
|
780
|
-
|
|
781
|
-
1. `IAP.createOneTimePurchaseOrder()` 호출
|
|
782
|
-
2. 300ms 딜레이 (결제 UI 시뮬레이션)
|
|
783
|
-
3. `state.iap.nextResult` 확인 → `'success'`가 아니면 `onError` 호출
|
|
784
|
-
4. 성공 시 `processProductGrant` 콜백 실행 → 실패하면 `'PRODUCT_NOT_GRANTED_BY_PARTNER'` 에러
|
|
785
|
-
5. 모두 성공하면 `completedOrders`에 기록, `onEvent`로 주문 결과 전달
|
|
786
|
-
|
|
787
|
-
### 광고
|
|
788
|
-
|
|
789
|
-
| API | Mock 동작 |
|
|
790
|
-
| ---------------------------------------- | ---------------------------------------------------------------------------- |
|
|
791
|
-
| `GoogleAdMob.loadAppsInTossAdMob` | 200ms 후 `loaded` 이벤트 |
|
|
792
|
-
| `GoogleAdMob.showAppsInTossAdMob` | 50ms~1.5s에 걸쳐 requested→show→impression→reward→dismissed 이벤트 순차 발행 |
|
|
793
|
-
| `GoogleAdMob.isAppsInTossAdMobLoaded` | 로드 여부 boolean 반환 |
|
|
794
|
-
| `TossAds.initialize/attach/attachBanner` | 회색 플레이스홀더 div 렌더링 |
|
|
795
|
-
| `TossAds.destroy/destroyAll` | no-op |
|
|
796
|
-
| `loadFullScreenAd` / `showFullScreenAd` | GoogleAdMob과 유사한 흐름 |
|
|
797
|
-
|
|
798
|
-
> 실패 다이얼(`failureModes.loadAdMob`, 패널 `forceNoFill`)을 설정하지 않으면 위 이벤트들은 매번 동일하게 발화합니다 — mock은 `adGroupId`를 판정에 쓰지 않고, 서버 측 지면(placement) 조회 단계를 모델링하지 않습니다. 실기기는 광고가 존재하기도 전에 지면 조회 자체가 실패해(예: `PLACEMENT_ID_FETCH_FAILED`) 즉시 거부될 수 있으므로, mock의 `loaded` 발화가 "실기기에서 광고가 실제로 나간다"는 신호는 아닙니다. 로컬에서 이 실패를 재현하려면: `__ait.patch('failureModes', { loadAdMob: 'PLACEMENT_ID_FETCH_FAILED' })`.
|
|
799
|
-
|
|
800
|
-
### 이벤트
|
|
801
|
-
|
|
802
|
-
| API | Mock 동작 |
|
|
803
|
-
| -------------------------------------------- | ------------------------------------------------------- |
|
|
804
|
-
| `graniteEvent.addEventListener` | `__ait:backEvent`, `__ait:homeEvent` 커스텀 이벤트 수신 |
|
|
805
|
-
| `appsInTossEvent.addEventListener` | no-op |
|
|
806
|
-
| `tdsEvent.addEventListener` | `__ait:navigationAccessoryEvent` 수신 |
|
|
807
|
-
| `onVisibilityChangedByTransparentServiceWeb` | `document.visibilitychange` 이벤트 위임 |
|
|
808
|
-
|
|
809
|
-
### 분석
|
|
810
|
-
|
|
811
|
-
| API | Mock 동작 |
|
|
812
|
-
| ----------------------------------- | ----------------------------------------------------- |
|
|
813
|
-
| `Analytics.screen/impression/click` | analyticsLog에 타입별 기록, Panel에서 실시간 확인 |
|
|
814
|
-
| `eventLog` | `log_name`, `log_type`, `params`로 커스텀 이벤트 기록 |
|
|
815
|
-
|
|
816
|
-
### 게임/프로모션
|
|
817
|
-
|
|
818
|
-
| API | Mock 동작 |
|
|
819
|
-
| ---------------------------------- | ---------------------------------------------- |
|
|
820
|
-
| `grantPromotionReward` | 타임스탬프 기반 mock key 반환 |
|
|
821
|
-
| `grantPromotionRewardForGame` | 위와 동일 |
|
|
822
|
-
| `submitGameCenterLeaderBoardScore` | state에 점수 추가, `{ statusCode: 'SUCCESS' }` |
|
|
823
|
-
| `getGameCenterGameProfile` | mock 프로필 반환 (없으면 `PROFILE_NOT_FOUND`) |
|
|
824
|
-
| `openGameCenterLeaderboard` | 콘솔 로그 (no-op) |
|
|
825
|
-
| `contactsViral` | 500ms 후 close 이벤트 발행 |
|
|
826
|
-
|
|
827
|
-
### 권한
|
|
828
|
-
|
|
829
|
-
| API | Mock 동작 |
|
|
830
|
-
| ---------------------- | ----------------------------------------------------- |
|
|
831
|
-
| `getPermission` | state의 권한 상태 반환 (allowed/denied/notDetermined) |
|
|
832
|
-
| `openPermissionDialog` | 상태를 `allowed`로 변경 |
|
|
833
|
-
| `requestPermission` | `openPermissionDialog`에 위임 |
|
|
834
|
-
|
|
835
|
-
> 권한이 필요한 함수(openCamera, getCurrentLocation 등)는 `withPermission()`으로 래핑되어 `.getPermission()`, `.openPermissionDialog()` 메서드가 자동 부착됩니다.
|
|
836
|
-
|
|
837
|
-
### 파트너
|
|
838
|
-
|
|
839
|
-
| API | Mock 동작 |
|
|
840
|
-
| ------------------------------- | ----------------- |
|
|
841
|
-
| `partner.addAccessoryButton` | 콘솔 로그 (no-op) |
|
|
842
|
-
| `partner.removeAccessoryButton` | 콘솔 로그 (no-op) |
|
|
313
|
+
1. 최상위 export 이름 — SDK가 노출하는 값 export가 mock에도 있는가.
|
|
314
|
+
2. 도메인 객체 멤버 — `Analytics.log`처럼 객체 멤버로만 늘어나는 표면. mock 쪽은 `as typeof SDK.X` 캐스트를 벗긴 **추론 타입**에서 멤버를 세므로, 캐스트가 누락을 가리지 못해요.
|
|
315
|
+
|
|
316
|
+
현재 통과 기준선은 2.x 라인 export 74종 / 도메인 객체 19개, 3.x 라인 export 92종 / 도메인 객체 34개예요.
|
|
317
|
+
|
|
318
|
+
덮이지 않은 API에 접근하면 mock proxy가 즉시 throw해요. "devtools에서는 되는데 실기기에서는 안 되는" 배포 사고를 원천 차단하기 위한 의도적 동작이에요.
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
[@apps-in-toss/devtools] IAP.newMethod is not mocked. This API may exist in
|
|
322
|
+
@apps-in-toss/web-framework, but devtools' mock does not cover it yet.
|
|
323
|
+
```
|
|
843
324
|
|
|
844
325
|
## 테스트에서의 활용
|
|
845
326
|
|
|
846
|
-
vitest/jest
|
|
327
|
+
mock을 직접 import해서 vitest/jest 테스트에 쓸 수 있어요. mock이 `window` · `localStorage` 등을 쓰므로 **jsdom 환경**이 필요해요.
|
|
847
328
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
> export default defineConfig({ test: { environment: "jsdom" } });
|
|
854
|
-
> ```
|
|
329
|
+
```ts
|
|
330
|
+
// vitest.config.ts
|
|
331
|
+
import { defineConfig } from "vitest/config";
|
|
332
|
+
export default defineConfig({ test: { environment: "jsdom" } });
|
|
333
|
+
```
|
|
855
334
|
|
|
856
335
|
```ts
|
|
857
|
-
import {
|
|
336
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
858
337
|
import {
|
|
859
|
-
|
|
860
|
-
Storage,
|
|
861
|
-
getCurrentLocation,
|
|
338
|
+
aitState,
|
|
862
339
|
getNetworkStatus,
|
|
863
|
-
openCamera,
|
|
864
340
|
IAP,
|
|
341
|
+
openCamera,
|
|
342
|
+
Storage,
|
|
865
343
|
} from "@apps-in-toss/devtools/mock";
|
|
866
|
-
import { aitState } from "@apps-in-toss/devtools/mock";
|
|
867
344
|
|
|
868
345
|
beforeEach(() => {
|
|
869
|
-
aitState.reset();
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
// 인증 테스트
|
|
873
|
-
it("appLogin은 authorizationCode를 반환한다", async () => {
|
|
874
|
-
const result = await appLogin();
|
|
875
|
-
expect(result.authorizationCode).toBeDefined();
|
|
346
|
+
aitState.reset();
|
|
876
347
|
});
|
|
877
348
|
|
|
878
|
-
|
|
879
|
-
it("오프라인 상태에서 네트워크 조회", async () => {
|
|
349
|
+
it("오프라인 상태를 반영한다", async () => {
|
|
880
350
|
aitState.update({ networkStatus: "OFFLINE" });
|
|
881
|
-
|
|
882
|
-
expect(status).toBe("OFFLINE");
|
|
351
|
+
await expect(getNetworkStatus()).resolves.toBe("OFFLINE");
|
|
883
352
|
});
|
|
884
353
|
|
|
885
|
-
|
|
886
|
-
it("카메라 권한이 denied면 에러를 던진다", async () => {
|
|
354
|
+
it("카메라 권한이 denied면 거부한다", async () => {
|
|
887
355
|
aitState.patch("permissions", { camera: "denied" });
|
|
888
356
|
await expect(openCamera()).rejects.toThrow();
|
|
889
357
|
});
|
|
890
358
|
|
|
891
|
-
|
|
892
|
-
it("구매 취소 시 onError가 호출된다", async () => {
|
|
359
|
+
it("구매가 취소되면 onError가 호출된다", async () => {
|
|
893
360
|
vi.useFakeTimers();
|
|
894
361
|
aitState.patch("iap", { nextResult: "USER_CANCELED" });
|
|
895
362
|
const onError = vi.fn();
|
|
@@ -903,180 +370,79 @@ it("구매 취소 시 onError가 호출된다", async () => {
|
|
|
903
370
|
vi.useRealTimers();
|
|
904
371
|
});
|
|
905
372
|
|
|
906
|
-
|
|
907
|
-
it("Storage에 값을 저장하고 읽을 수 있다", async () => {
|
|
373
|
+
it("Storage에 값을 저장하고 읽는다", async () => {
|
|
908
374
|
await Storage.setItem("key1", "value1");
|
|
909
|
-
|
|
910
|
-
expect(result).toBe("value1");
|
|
375
|
+
await expect(Storage.getItem("key1")).resolves.toBe("value1");
|
|
911
376
|
});
|
|
912
377
|
```
|
|
913
378
|
|
|
914
|
-
|
|
379
|
+
`Storage`는 앱 자체 `localStorage`와 섞이지 않도록 `__ait_storage:` prefix로 저장해요.
|
|
915
380
|
|
|
916
|
-
|
|
381
|
+
## MCP state endpoint
|
|
917
382
|
|
|
918
|
-
|
|
919
|
-
pnpm add -D @ait-co/debugger
|
|
920
|
-
pnpm exec debugger-test 'src/**/*.ait.test.ts' \
|
|
921
|
-
--scheme-url "intoss-private://my-mini-app?_deploymentId=<uuid>" \
|
|
922
|
-
--cell-sdk-line 3.x --cell-platform ios --report-dir .ait-report
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
전체 플래그·QR 스캔 절차·산출물 레퍼런스는 이제 `@ait-co/debugger` 패키지가 정본입니다 — `debugger-test --help`를 참고하세요. 미니앱 entry에 필요한 한 줄만 이 패키지 쪽에 남아 있습니다: `import '@ait-co/debug-console/auto'` ([위 섹션](#on-device-디버깅-한-줄-설정)).
|
|
383
|
+
`mcp: true`는 Vite dev 서버에 `/api/ait-devtools/state`를 등록해요. 브라우저 패널이 상태가 바뀔 때마다 이 경로로 POST하고, 외부 도구(AI 에이전트의 MCP 서버 등)가 GET으로 마지막 스냅샷을 읽어요.
|
|
926
384
|
|
|
927
|
-
|
|
385
|
+
- `GET` — 마지막 스냅샷 JSON. 아직 브라우저가 한 번도 push하지 않았으면 `503`.
|
|
386
|
+
- `POST` — 패널이 자동으로 호출해요. 파싱 불가능한 본문은 `400`.
|
|
387
|
+
- CORS는 전면 허용이에요 (로컬 에이전트가 읽을 수 있어야 하므로).
|
|
928
388
|
|
|
929
|
-
|
|
389
|
+
**Vite 전용**이며 기본값은 `false`예요. 옵션을 켜지 않으면 endpoint를 등록하지 않고, 브라우저 패널도 상태를 직렬화하거나 네트워크 요청을 보내지 않아요 — 런타임 플래그(`__AIT_DEVTOOLS_MCP_ENABLED__`)가 없으면 패널의 동기화 헬퍼가 즉시 반환해요.
|
|
930
390
|
|
|
931
|
-
|
|
391
|
+
## 프로덕션 빌드
|
|
932
392
|
|
|
933
|
-
|
|
393
|
+
`NODE_ENV === 'production'`이면 플러그인이 통째로 비활성화돼요 — alias도, 패널 주입도, MCP endpoint도 걸리지 않아요. 별도의 조건부 설정 없이 안전하고, 프로덕션에서 강제로 켜는 옵션은 제공하지 않아요.
|
|
934
394
|
|
|
935
|
-
|
|
395
|
+
번들러 설정에서 아예 빼고 싶다면:
|
|
936
396
|
|
|
937
397
|
```ts
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
### 2. Proxy 트립와이어 (런타임 차단)
|
|
944
|
-
|
|
945
|
-
`createMockProxy()`는 미구현 API 접근 시 즉시 `Error`를 throw합니다. mock에 없는 API가 실 SDK에는 있을 수 있어 "devtools에서는 잘 되는데 실제 SDK에서는 안 되는" 배포 사고를 원천 차단하기 위한 의도적 동작입니다. 누락된 API는 [이슈](https://github.com/apps-in-toss-community/devtools/issues)로 제보하거나 직접 mock을 추가해 주세요.
|
|
946
|
-
|
|
947
|
-
```
|
|
948
|
-
[@apps-in-toss/devtools] IAP.newMethod is not mocked. This API may exist in
|
|
949
|
-
@apps-in-toss/web-framework, but devtools' mock does not cover it yet.
|
|
950
|
-
Please file an issue: https://github.com/apps-in-toss-community/devtools/issues
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
### 3. GitHub Actions 주간 CI
|
|
954
|
-
|
|
955
|
-
`.github/workflows/check-sdk-update.yml`이 **매주 월요일** 자동으로:
|
|
956
|
-
|
|
957
|
-
1. `@apps-in-toss/web-framework`의 새 버전 확인
|
|
958
|
-
2. 최신 버전으로 업데이트 후 타입 체크 실행
|
|
959
|
-
3. 새 버전 감지 시 자동으로 GitHub Issue 생성 (타입 에러 여부 포함)
|
|
960
|
-
|
|
961
|
-
## Fidelity QA
|
|
962
|
-
|
|
963
|
-
`scripts/fidelity-qa/`는 mock SDK와 실기기 relay 사이의 **SDK API 정합성을 자동으로 측정**하는 도구다.
|
|
964
|
-
|
|
965
|
-
```bash
|
|
966
|
-
pnpm qa:fidelity --runner=mock # mock-only (CI 기본값, 회귀 감지)
|
|
967
|
-
pnpm qa:fidelity --runner=relay # 실기기 relay 필요 (devtools MCP 연결)
|
|
968
|
-
pnpm qa:fidelity --runner=both --diff # mock+relay 동시 실행 + diff 출력
|
|
969
|
-
pnpm qa:fidelity --include-writes # Storage write 사이클 포함 (기본 OFF)
|
|
970
|
-
pnpm qa:fidelity --output=results.json # JSON 결과 파일 저장
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
CI는 `pnpm qa:fidelity --runner=mock`을 자동 실행한다 (mock-only, exit 0이면 통과).
|
|
974
|
-
|
|
975
|
-
**Diff 레이블**:
|
|
976
|
-
|
|
977
|
-
- `MATCH` — mock과 relay 값이 동일
|
|
978
|
-
- `EXPECTED_MISMATCH` — `scripts/fidelity-qa/whitelist.json`에 등록된 알려진 차이 (예: jsdom UA vs 실 WebView UA)
|
|
979
|
-
- `UNEXPECTED` — whitelist에 없는 불일치 → exit 1 (회귀 의심)
|
|
980
|
-
|
|
981
|
-
**whitelist 갱신 절차**: relay 세션에서 의도적 차이가 발견되면 `scripts/fidelity-qa/whitelist.json`에 `{ "id": "<probe-id>", "reason": "<설명>" }`을 추가한다.
|
|
982
|
-
|
|
983
|
-
relay runner는 현재 stub (devtools#261 follow-up에서 CDP Runtime.evaluate 구현 예정).
|
|
984
|
-
|
|
985
|
-
## Contributing
|
|
986
|
-
|
|
987
|
-
### 새 API mock 추가 절차
|
|
988
|
-
|
|
989
|
-
1. 해당 카테고리 디렉토리에 함수 구현 (예: `src/mock/device/`)
|
|
990
|
-
2. 공통 API는 `src/mock/index.ts`, SDK별 계약은 `src/mock/index-2x.ts` 또는 `src/mock/index-3x.ts`에 export 추가
|
|
991
|
-
3. `src/__typecheck.ts`와 `src/__typecheck-2x.ts`에 타입 호환성 assertion 추가
|
|
992
|
-
4. `pnpm typecheck`로 2.10.8·3.0.1 모두와 호환되는지 검증
|
|
993
|
-
5. `src/__tests/`에 테스트 작성
|
|
398
|
+
// vite.config.ts
|
|
399
|
+
import { defineConfig } from "vite";
|
|
400
|
+
import aitDevtools from "@apps-in-toss/devtools/unplugin";
|
|
994
401
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
pnpm test # 전체 테스트 실행
|
|
402
|
+
export default defineConfig(({ command }) => ({
|
|
403
|
+
plugins: [...(command === "serve" ? [aitDevtools.vite()] : [])],
|
|
404
|
+
}));
|
|
999
405
|
```
|
|
1000
406
|
|
|
1001
|
-
|
|
407
|
+
이 "0 바이트" 주장은 `check:footprint-absent`가 기계적으로 증명해요 — 실제 소비자 fixture를 minify 빌드해 mock/패널의 문자열 sentinel이 남지 않는지 grep하고, 강제 주입 빌드에서는 **반드시 검출되는지**까지 확인(positive control)해 grep 자체가 무력화된 상태로 통과하는 일을 막아요.
|
|
1002
408
|
|
|
1003
|
-
|
|
409
|
+
## 개발
|
|
1004
410
|
|
|
1005
411
|
```sh
|
|
1006
|
-
|
|
412
|
+
yarn workspace @apps-in-toss/devtools build # tsdown 번들
|
|
413
|
+
yarn workspace @apps-in-toss/devtools typecheck # 2.x·3.x 타입 호환 + check:sdk-exports
|
|
414
|
+
yarn workspace @apps-in-toss/devtools test # vitest
|
|
1007
415
|
```
|
|
1008
416
|
|
|
1009
|
-
|
|
417
|
+
새 mock을 추가하는 절차:
|
|
1010
418
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
### DevTools Panel이 안 보일 때
|
|
1018
|
-
|
|
1019
|
-
- 플러그인 옵션에서 `panel: false`로 설정하지 않았는지 확인
|
|
1020
|
-
- 수동 alias 설정을 사용 중이라면, 진입점 파일에 직접 import를 추가하세요:
|
|
1021
|
-
```ts
|
|
1022
|
-
import "@apps-in-toss/devtools/panel";
|
|
1023
|
-
```
|
|
1024
|
-
- 플러그인은 파일명이 `main`, `index`, `entry`, `app` 중 하나인 진입점에만 자동 주입합니다 (대소문자 무시). 파일명이 이 패턴에 맞지 않으면 수동으로 `import '@apps-in-toss/devtools/panel'`을 추가하세요.
|
|
1025
|
-
|
|
1026
|
-
### 서브패스 import는 mock되지 않음
|
|
1027
|
-
|
|
1028
|
-
`@apps-in-toss/web-framework/some-subpath` 형태의 서브패스 import는 alias가 적용되지 않습니다. SDK의 메인 엔트리(`@apps-in-toss/web-framework`)만 mock됩니다. 특정 서브패스도 mock이 필요하다면 번들러의 `resolve.alias`에 해당 서브패스를 수동으로 추가하세요.
|
|
1029
|
-
|
|
1030
|
-
### Next.js Turbopack에서 설정하는 법
|
|
1031
|
-
|
|
1032
|
-
Turbopack은 unplugin을 지원하지 않으므로, `next.config.js`에서 `resolveAlias`를 사용하세요 (위의 [Next.js (Turbopack)](#nextjs-turbopack) 섹션 참고). Panel은 진입점에서 직접 import해야 합니다:
|
|
419
|
+
1. 해당 카테고리 디렉터리에 구현을 추가해요 (예: `src/mock/device/`).
|
|
420
|
+
2. 공통 표면은 `src/mock/index.ts`, 라인별 계약은 `src/mock/index-2x.ts` / `src/mock/index-3x.ts`(+ `src/mock/domains-3x.ts`)에 export해요.
|
|
421
|
+
3. `src/__typecheck.ts` / `src/__typecheck-2x.ts`에 타입 호환 assertion을 추가해요.
|
|
422
|
+
4. `typecheck`로 두 라인 모두와 호환되는지 확인해요 — `check:sdk-exports`가 함께 돌아요.
|
|
423
|
+
5. `src/__tests__/`에 테스트를 추가해요.
|
|
1033
424
|
|
|
1034
|
-
|
|
1035
|
-
// app/layout.tsx 또는 pages/_app.tsx
|
|
1036
|
-
import "@apps-in-toss/devtools/panel";
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
## MCP Server
|
|
1040
|
-
|
|
1041
|
-
MCP 표면(데몬 · attach · CDP tool)은 `@ait-co/debugger`로 이동했습니다(#818). 에이전트 등록도 이제 devtools가 아니라 debugger를 가리킵니다:
|
|
1042
|
-
|
|
1043
|
-
```json
|
|
1044
|
-
{
|
|
1045
|
-
"mcpServers": {
|
|
1046
|
-
"ait-debug": {
|
|
1047
|
-
"command": "npx",
|
|
1048
|
-
"args": ["-y", "-p", "@ait-co/debugger", "debugger"]
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
이 서버가 주는 것: 로컬 브라우저(환경 1)·PWA(환경 2)·intoss-private WebView(환경 3)에서 실행 중인 미니앱을 CDP로 관측 — console, network, DOM, snapshot, screenshot — 그리고 환경 2·3 on-device 진입용 `start_attach`. 전체 tool 목록·모드/타깃 매트릭스·환경별 설정은 [`@ait-co/debugger`](https://www.npmjs.com/package/@ait-co/debugger) 패키지 문서가 정본입니다.
|
|
1055
|
-
|
|
1056
|
-
## 패키지 Export 구조
|
|
425
|
+
## Troubleshooting
|
|
1057
426
|
|
|
1058
|
-
이
|
|
427
|
+
**`... is not mocked` 에러** — 그 API가 아직 mock에 없어요. 위 [Mock이 커버하는 SDK 표면](#mock이-커버하는-sdk-표면) 절차대로 mock을 추가하세요.
|
|
1059
428
|
|
|
1060
|
-
|
|
1061
|
-
| ------------------------------------ | ------------------------------------------------------------- |
|
|
1062
|
-
| `@apps-in-toss/devtools` (= `/mock`) | 번들러 alias 대상, 모든 mock export |
|
|
1063
|
-
| `@apps-in-toss/devtools/panel` | Floating DevTools Panel (import 시 자동 마운트) |
|
|
1064
|
-
| `@apps-in-toss/devtools/unplugin` | 번들러 플러그인 (.vite, .webpack, .rspack, .esbuild, .rollup) |
|
|
429
|
+
**패널이 안 보임** — `panel: false`가 아닌지, 그리고 진입점 파일명이 `main` · `index` · `entry` · `app` 중 하나인지 확인하세요. 아니라면 `import '@apps-in-toss/devtools/panel'`을 직접 넣어야 해요. 수동 alias만 쓰는 설정도 마찬가지예요.
|
|
1065
430
|
|
|
1066
|
-
|
|
431
|
+
**mock이 안 걸림** — 서브패스 import는 치환되지 않아요. 메인 엔트리(`@apps-in-toss/web-framework`)로 import하거나, 필요한 서브패스를 `resolve.alias`에 직접 추가하세요.
|
|
1067
432
|
|
|
1068
|
-
|
|
1069
|
-
| ------------------------------------ | ------------------------------ | --------------------------- |
|
|
1070
|
-
| `@apps-in-toss/devtools/mcp/server` | `@ait-co/debugger/mcp/server` | throw |
|
|
1071
|
-
| `@apps-in-toss/devtools/mcp/cli` | `@ait-co/debugger/mcp/cli` | throw |
|
|
1072
|
-
| `@apps-in-toss/devtools/test-runner` | `@ait-co/debugger/test-runner` | throw |
|
|
1073
|
-
| `@apps-in-toss/devtools/in-app` | `@ait-co/debug-console` | no-op + `console.error` 1회 |
|
|
1074
|
-
| `@apps-in-toss/devtools/in-app/auto` | `@ait-co/debug-console/auto` | no-op + `console.error` 1회 |
|
|
433
|
+
**2.x인데 3.x mock이 잡힘** — SDK 버전 자동 감지가 실패한 경우예요. `sdkVersion: '2'`로 못박으세요.
|
|
1075
434
|
|
|
1076
|
-
##
|
|
435
|
+
## 패키지 export 구조
|
|
1077
436
|
|
|
1078
|
-
|
|
437
|
+
| import 경로 | 내용 |
|
|
438
|
+
| --------------------------------- | ---------------------------------------------------------------------- |
|
|
439
|
+
| `@apps-in-toss/devtools` | `/mock`과 동일 (3.x facade) |
|
|
440
|
+
| `@apps-in-toss/devtools/mock` | 번들러 alias 대상 기본 진입점 (3.x facade) |
|
|
441
|
+
| `@apps-in-toss/devtools/mock/2x` | 2.x facade |
|
|
442
|
+
| `@apps-in-toss/devtools/mock/3x` | 3.x facade |
|
|
443
|
+
| `@apps-in-toss/devtools/panel` | 플로팅 패널 (import 시 자동 마운트, `mount`/`disposePanel` export) |
|
|
444
|
+
| `@apps-in-toss/devtools/unplugin` | 번들러 플러그인 (`vite` · `webpack` · `rspack` · `rollup` · `esbuild`) |
|
|
1079
445
|
|
|
1080
|
-
|
|
446
|
+
## 라이선스
|
|
1081
447
|
|
|
1082
|
-
|
|
448
|
+
[Apache-2.0](./LICENSE) © 2026 Viva Republica, Inc.
|