@ait-co/devtools 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,284 +1,588 @@
1
- # ait-devtools
1
+ # @ait-co/devtools
2
+
3
+ > **데모: https://apps-in-toss-community.github.io/devtools/**
2
4
 
3
5
  `@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics` import도 함께 mock됩니다.
4
6
 
5
7
  앱인토스(Apps in Toss) 미니앱을 **일반 브라우저**에서 개발하고 테스트할 수 있게 해줍니다. 토스 앱 없이도 SDK의 모든 기능을 시뮬레이션하여 빠른 개발 사이클을 지원합니다.
6
8
 
7
9
  - **60+ SDK API mock** — 인증, 결제, IAP, 위치, 카메라, 스토리지 등
8
- - **Floating DevTools Panel**브라우저에서 SDK 상태를 실시간으로 제어
10
+ - **Device API 모드 시스템** mock / web / prompt 세 가지 모드로 디바이스 API 동작 전환
11
+ - **Floating DevTools Panel** — 브라우저에서 SDK 상태를 실시간으로 제어 (8개 탭)
9
12
  - **모든 번들러 지원** — [unplugin](https://github.com/unjs/unplugin) 기반 Vite, Webpack, Rspack, esbuild, Rollup 통합
10
13
 
11
14
  ## 설치
12
15
 
13
16
  ```bash
14
- npm install -D ait-devtools
17
+ npm install -D @ait-co/devtools
18
+ # 또는
19
+ pnpm add -D @ait-co/devtools
15
20
  ```
16
21
 
17
- > `@apps-in-toss/web-framework ^2.0.0`이 peerDependency로 설정되어 있습니다 (optional).
22
+ > **지원 SDK 버전**: `@apps-in-toss/web-framework >=2.4.0 <2.4.8` (peer, required).
23
+ >
24
+ > devtools는 위 범위의 SDK 버전에서만 동작이 검증됩니다. 범위 밖 SDK를 설치하면
25
+ > 패키지 매니저가 install-time에 peer 경고를 표시합니다. 또한 devtools가 아직 mock하지
26
+ > 않은 API를 호출하면 런타임에 에러가 발생합니다 — "devtools에서는 잘 되는데 실제 SDK에서는
27
+ > 안 되는" 상황을 방지하기 위한 의도적 동작입니다. 누락된 API는
28
+ > [이슈](https://github.com/apps-in-toss-community/devtools/issues)로 알려주세요.
18
29
 
19
- ## 사용법
30
+ ## 번들러 설정
20
31
 
21
- ### 1. Vite 플러그인
32
+ ### Vite
22
33
 
23
34
  ```ts
24
- // vite.config.ts
25
- import aitDevtools from 'ait-devtools/unplugin';
35
+ // vite.config.ts (개발 전용)
36
+ import aitDevtools from '@ait-co/devtools/unplugin';
26
37
 
27
38
  export default {
28
39
  plugins: [aitDevtools.vite()],
29
40
  };
30
41
  ```
31
42
 
32
- ### 2. Webpack
43
+ > 개발 전용 설정입니다. Production 빌드에서 제외하려면 아래 [Production 빌드](#production-빌드) 섹션을 참고하세요.
44
+
45
+ ### Webpack / Rspack
33
46
 
34
47
  ```js
35
- // webpack.config.js (ESM)
36
- import aitDevtools from 'ait-devtools/unplugin';
48
+ // webpack.config.js (ESM, 개발 환경에서만 사용 권장)
49
+ import aitDevtools from '@ait-co/devtools/unplugin';
37
50
  config.plugins.push(aitDevtools.webpack());
38
51
 
39
52
  // webpack.config.js (CommonJS)
40
- const aitDevtools = require('ait-devtools/unplugin');
53
+ const aitDevtools = require('@ait-co/devtools/unplugin');
41
54
  config.plugins.push(aitDevtools.webpack());
42
55
  ```
43
56
 
44
- ### 3. Next.js (Turbopack)
57
+ ### Next.js (Turbopack)
58
+
59
+ Turbopack은 플러그인 시스템을 지원하지 않으므로 `resolveAlias`를 사용합니다.
45
60
 
46
- Turbopack은 플러그인 시스템을 지원하지 않으므로 `resolveAlias`를 사용합니다:
61
+ - `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics`도 함께 alias해야 합니다.
62
+ - Turbopack은 일반적으로 `next dev`에서만 사용되므로 별도의 production 가드가 필요하지 않습니다.
47
63
 
48
64
  ```js
49
65
  // next.config.js (Next.js 15+)
50
66
  module.exports = {
51
67
  turbo: {
52
68
  resolveAlias: {
53
- '@apps-in-toss/web-framework': 'ait-devtools/mock',
69
+ '@apps-in-toss/web-framework': '@ait-co/devtools/mock',
70
+ '@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
71
+ '@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
54
72
  },
55
73
  },
56
74
  };
75
+ ```
76
+
77
+ Next.js 14 이하에서는 `experimental.turbo`를 사용합니다:
57
78
 
58
- // Next.js 14 이하
79
+ ```js
80
+ // next.config.js (Next.js 14 이하)
59
81
  module.exports = {
60
82
  experimental: {
61
83
  turbo: {
62
84
  resolveAlias: {
63
- '@apps-in-toss/web-framework': 'ait-devtools/mock',
85
+ '@apps-in-toss/web-framework': '@ait-co/devtools/mock',
86
+ '@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
87
+ '@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
64
88
  },
65
89
  },
66
90
  },
67
91
  };
68
92
  ```
69
93
 
70
- ### 4. 수동 Alias 설정
94
+ > **Panel 주입**: Turbopack은 unplugin을 지원하지 않으므로 Panel이 자동 주입되지 않습니다. 진입점에서 직접 import하세요:
95
+ > ```ts
96
+ > // app/layout.tsx 또는 pages/_app.tsx
97
+ > import '@ait-co/devtools/panel';
98
+ > ```
99
+
100
+ ### Next.js (Webpack)
101
+
102
+ Next.js에서 Webpack 모드(`next dev` without `--turbo`, 또는 `next build`)를 사용하는 경우:
103
+
104
+ ```js
105
+ // next.config.js (Webpack 모드)
106
+ const aitDevtools = require('@ait-co/devtools/unplugin'); // CJS entrypoint 제공
107
+
108
+ module.exports = {
109
+ webpack: (config, { dev }) => {
110
+ if (dev) {
111
+ config.plugins.push(aitDevtools.webpack());
112
+ }
113
+ return config;
114
+ },
115
+ };
116
+ ```
117
+
118
+ ### 수동 Alias 설정
71
119
 
72
120
  번들러의 `resolve.alias` 설정으로 직접 지정할 수도 있습니다:
73
121
 
122
+ ```ts
123
+ // vite.config.ts
124
+ import { defineConfig } from 'vite';
125
+
126
+ export default defineConfig({
127
+ resolve: {
128
+ alias: {
129
+ '@apps-in-toss/web-framework': '@ait-co/devtools/mock',
130
+ '@apps-in-toss/web-bridge': '@ait-co/devtools/mock',
131
+ '@apps-in-toss/web-analytics': '@ait-co/devtools/mock',
132
+ },
133
+ },
134
+ });
135
+ ```
136
+
74
137
  ```js
75
- // vite.config.ts 또는 webpack.config.js
76
- {
138
+ // webpack.config.js (Webpack은 절대 경로 필요)
139
+ module.exports = {
77
140
  resolve: {
78
141
  alias: {
79
- '@apps-in-toss/web-framework': 'ait-devtools/mock',
80
- '@apps-in-toss/web-bridge': 'ait-devtools/mock',
81
- '@apps-in-toss/web-analytics': 'ait-devtools/mock',
142
+ '@apps-in-toss/web-framework': require.resolve('@ait-co/devtools/mock'),
143
+ '@apps-in-toss/web-bridge': require.resolve('@ait-co/devtools/mock'),
144
+ '@apps-in-toss/web-analytics': require.resolve('@ait-co/devtools/mock'),
82
145
  },
83
146
  },
147
+ };
148
+ ```
149
+
150
+ > **주의**: 수동 alias만 사용하면 DevTools Panel이 자동 주입되지 않습니다. 진입점 파일에 직접 import를 추가하세요:
151
+ > ```ts
152
+ > import '@ait-co/devtools/panel'; // 진입점에 추가
153
+ > ```
154
+
155
+ ### 플러그인 옵션
156
+
157
+ | 옵션 | 타입 | 기본값 | 설명 |
158
+ |---|---|---|---|
159
+ | `panel` | `boolean` | `true` | DevTools Panel 자동 주입 여부 |
160
+ | `forceEnable` | `boolean` | `false` | production에서도 devtools 활성화 |
161
+ | `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | mock alias 활성화 여부 |
162
+
163
+ ```ts
164
+ aitDevtools.vite({ panel: false }); // Panel 없이 mock만 사용
165
+ aitDevtools.vite({ forceEnable: true }); // production에서도 활성화 (mock 기본 OFF, panel ON)
166
+ aitDevtools.vite({ forceEnable: true, mock: true }); // production에서 mock도 활성화
167
+ ```
168
+
169
+ ## Production 빌드
170
+
171
+ 기본적으로 devtools 플러그인은 **production 빌드에서 자동 비활성화**됩니다 (`NODE_ENV === 'production'`이면 alias 변환과 Panel 주입이 모두 스킵). 별도의 조건부 설정 없이도 안전합니다.
172
+
173
+ 스테이징 환경 등에서 production 빌드에서도 devtools를 사용하려면 `forceEnable` 옵션을 사용하세요:
174
+
175
+ ```ts
176
+ aitDevtools.vite({ forceEnable: true }); // panel ON, mock OFF (모니터링 전용)
177
+ aitDevtools.vite({ forceEnable: true, mock: true }); // panel + mock 모두 ON
178
+ ```
179
+
180
+ 번들러 설정에서 플러그인 자체를 조건부로 제외할 수도 있습니다:
181
+
182
+ ```ts
183
+ // vite.config.ts
184
+ import { defineConfig } from 'vite';
185
+ import aitDevtools from '@ait-co/devtools/unplugin';
186
+
187
+ export default defineConfig(({ command }) => ({
188
+ plugins: [
189
+ ...(command === 'serve' ? [aitDevtools.vite()] : []),
190
+ ],
191
+ }));
192
+ ```
193
+
194
+ ```js
195
+ // webpack.config.js (Rspack도 동일)
196
+ const aitDevtools = require('@ait-co/devtools/unplugin');
197
+ const plugins = [];
198
+ if (process.env.NODE_ENV !== 'production') {
199
+ plugins.push(aitDevtools.webpack());
84
200
  }
85
201
  ```
86
202
 
87
- ## Floating DevTools Panel
203
+ > Next.js 설정은 위의 [Next.js (Webpack)](#nextjs-webpack) 및 [Next.js (Turbopack)](#nextjs-turbopack) 섹션을 참고하세요.
204
+
205
+ ## Device API 모드 시스템
206
+
207
+ 디바이스 관련 API(카메라, 위치, 클립보드 등)는 세 가지 모드로 동작합니다:
208
+
209
+ | 모드 | 동작 | 사용 사례 |
210
+ |---|---|---|
211
+ | **mock** | `aitState`에 저장된 더미 데이터 반환 | 자동화 테스트, 고정된 시나리오 |
212
+ | **web** | 브라우저 네이티브 API 사용 (Geolocation, File API 등) | 실제 디바이스 기능 테스트 |
213
+ | **prompt** | DevTools Panel이 자동으로 열리고 사용자 입력 대기 (30초 타임아웃) | 수동 QA, 특정 값 입력 |
214
+
215
+ ### 모드별 지원 API
216
+
217
+ | API | mock | web | prompt |
218
+ |---|---|---|---|
219
+ | `openCamera` | ✅ | ✅ | ✅ |
220
+ | `fetchAlbumPhotos` | ✅ | ✅ | ✅ |
221
+ | `getCurrentLocation` | ✅ | ✅ | ✅ |
222
+ | `startUpdateLocation` | ✅ | ✅ | ✅ |
223
+ | `getNetworkStatus` | ✅ | ✅ | — |
224
+ | `getClipboardText` / `setClipboardText` | ✅ | ✅ | — |
225
+
226
+ ### 모드 설정 방법
227
+
228
+ ```js
229
+ // 콘솔에서 개별 API 모드 변경
230
+ __ait.patch('deviceModes', { camera: 'web', location: 'prompt' });
231
+
232
+ // 또는 DevTools Panel의 Device 탭에서 드롭다운으로 전환
233
+ ```
234
+
235
+ ### 더미 이미지 관리
88
236
 
89
- 플러그인 사용 진입점 파일에 패널이 자동 주입됩니다 (`aitDevtools.vite({ panel: false })`로 비활성화 가능).
237
+ mock 모드에서 카메라/앨범 API는 더미 이미지를 반환합니다.
238
+
239
+ - **기본 플레이스홀더**: 파란색/녹색/주황색 320×240 이미지 3장 자동 생성
240
+ - **커스텀 이미지**: DevTools Panel의 Device 탭에서 파일 추가/제거 가능
241
+ - **콘솔에서 설정**: `__ait.patch('mockData', { images: ['data:image/png;base64,...'] })`
242
+
243
+ ## Floating DevTools Panel
90
244
 
91
- 화면 우하단의 **'AIT' 버튼**을 클릭하면 DevTools 패널이 토글됩니다.
245
+ 플러그인 사용 시 진입점 파일에 패널이 자동 주입됩니다. 화면 우하단의 **'AIT' 버튼**을 클릭하면 토글됩니다.
92
246
 
93
- ### 7개 탭
247
+ ### 8개 탭
94
248
 
95
249
  | 탭 | 설명 |
96
250
  |---|---|
97
- | **Environment** | 플랫폼 OS (ios/android), 앱 버전, 환경 (toss/sandbox), 로케일, 네트워크 상태, Safe Area Insets (top/bottom) 설정 |
251
+ | **Environment** | 플랫폼 OS (ios/android), 앱 버전, 환경 (toss/sandbox), 로케일, 네트워크 상태, Safe Area Insets |
98
252
  | **Permissions** | camera, photos, geolocation, clipboard, contacts, microphone 권한 상태 제어 (allowed/denied/notDetermined) |
99
- | **Location** | 위도, 경도, 정확도 등 GPS 좌표 설정 |
100
- | **IAP** | 인앱 구매 시뮬레이션 — 다음 구매 결과(success/취소/에러 등), TossPay 결제 결과, 완료된 주문 내역 |
253
+ | **Location** | 위도, 경도, 정확도 설정 |
254
+ | **Device** | API 모드 전환 (mock/web/prompt), 더미 이미지 관리 (추가/제거/기본값/초기화) |
255
+ | **IAP** | 다음 구매 결과 선택 (success/취소/에러 등), TossPay 결제 결과, 완료된 주문 내역 (최근 5건) |
101
256
  | **Events** | Back/Home 네비게이션 이벤트 트리거, 로그인 상태 토글 |
102
- | **Analytics** | 기록된 분석 이벤트 실시간 로그 뷰어 (타임스탬프, 타입, 파라미터) |
257
+ | **Analytics** | 기록된 분석 이벤트 실시간 로그 뷰어 (최근 30건, 타임스탬프/타입/파라미터) |
103
258
  | **Storage** | `Storage` API로 저장된 항목 조회 및 초기화 |
104
259
 
105
- ## 브라우저 콘솔 사용법
260
+ > **prompt 모드 자동 열림**: prompt 모드로 설정된 API가 호출되면, Panel이 자동으로 Device 탭을 열고 사용자 입력 UI를 표시합니다.
106
261
 
107
- `window.__ait`를 통해 mock 상태를 직접 제어할 수 있습니다:
262
+ ## `window.__ait` 콘솔 API
263
+
264
+ 브라우저 콘솔에서 `window.__ait`(또는 `__ait`)로 mock 상태를 직접 제어할 수 있습니다:
108
265
 
109
266
  ```js
110
- // 네트워크 상태 변경
111
- __ait.update({ networkStatus: 'OFFLINE' });
267
+ // 현재 상태 조회
268
+ __ait.state // 전체 상태 객체
269
+ __ait.state.platform // 'ios' 또는 'android'
270
+ __ait.state.auth.isLoggedIn // 로그인 상태
271
+ __ait.state.deviceModes // 각 API의 현재 모드
112
272
 
113
- // 여러 상태 한번에 업데이트
273
+ // 상태 업데이트 (얕은 병합)
114
274
  __ait.update({ platform: 'android', locale: 'en-US' });
275
+ __ait.update({ networkStatus: 'OFFLINE' });
276
+
277
+ // 중첩 상태 업데이트
278
+ __ait.patch('permissions', { camera: 'denied' });
279
+ __ait.patch('deviceModes', { location: 'web' });
280
+ __ait.patch('iap', { nextResult: 'USER_CANCELED' });
115
281
 
116
282
  // 이벤트 트리거
117
283
  __ait.trigger('backEvent');
284
+ __ait.trigger('homeEvent');
118
285
 
119
- // 중첩 상태 업데이트 (permissions, iap 등)
120
- __ait.patch('permissions', { camera: 'denied' });
286
+ // 분석 이벤트 수동 기록
287
+ __ait.logAnalytics({ type: 'click', params: { button: 'purchase' } });
121
288
 
122
- // 현재 상태 조회
123
- console.log(__ait.state.platform);
289
+ // 상태 초기화 (deviceId는 유지됨)
290
+ __ait.reset();
291
+
292
+ // 상태 변경 구독
293
+ const unsubscribe = __ait.subscribe(() => {
294
+ console.log('상태 변경됨:', __ait.state);
295
+ });
296
+ unsubscribe(); // 구독 해제
124
297
  ```
125
298
 
126
299
  ## Mock API 목록
127
300
 
128
301
  ### 인증/로그인
129
302
 
130
- | API | 설명 |
303
+ | API | Mock 동작 |
131
304
  |---|---|
132
- | `appLogin` | 로그인 |
133
- | `getIsTossLoginIntegratedService` | 토스 로그인 통합 서비스 여부 |
134
- | `getUserKeyForGame` | 게임용 유저 조회 |
135
- | `appsInTossSignTossCert` | 토스 인증서 서명 |
305
+ | `appLogin` | `{ authorizationCode, referrer }` 반환 |
306
+ | `getIsTossLoginIntegratedService` | state의 `isTossLoginIntegrated` 반환 |
307
+ | `getUserKeyForGame` | `{ hash, type: 'HASH' }` 반환 (비로그인 시 `undefined`) |
308
+ | `appsInTossSignTossCert` | 콘솔 로그만 출력 (no-op) |
136
309
 
137
310
  ### 화면/네비게이션
138
311
 
139
- | API | 설명 |
312
+ | API | Mock 동작 |
140
313
  |---|---|
141
- | `closeView` | 현재 닫기 |
142
- | `openURL` | URL 열기 |
143
- | `share` | 공유하기 |
144
- | `getTossShareLink` | 토스 공유 링크 조회 |
145
- | `setIosSwipeGestureEnabled` | iOS 스와이프 제스처 활성화 설정 |
146
- | `setDeviceOrientation` | 디바이스 방향 설정 |
147
- | `setScreenAwakeMode` | 화면 꺼짐 방지 설정 |
148
- | `setSecureScreen` | 보안 화면 설정 (캡처 방지) |
149
- | `requestReview` | 리뷰 요청 |
314
+ | `closeView` | `window.history.back()` 호출 |
315
+ | `openURL` | `window.open()`으로 |
316
+ | `share` | `navigator.share()` 사용 (미지원 시 콘솔 출력) |
317
+ | `getTossShareLink` | `https://toss.im/share/mock{path}` 반환 |
318
+ | `setIosSwipeGestureEnabled` | 콘솔 로그 (no-op) |
319
+ | `setDeviceOrientation` | 콘솔 로그 (no-op) |
320
+ | `setScreenAwakeMode` | `{ enabled }` 반환 |
321
+ | `setSecureScreen` | `{ enabled }` 반환 |
322
+ | `requestReview` | no-op (`.isSupported()` 메서드 포함) |
150
323
 
151
324
  ### 환경 정보
152
325
 
153
- | API | 설명 |
326
+ | API | Mock 동작 |
154
327
  |---|---|
155
- | `getPlatformOS` | 플랫폼 OS 조회 (ios/android) |
156
- | `getOperationalEnvironment` | 운영 환경 조회 (toss/sandbox) |
157
- | `getTossAppVersion` | 토스 버전 조회 |
158
- | `isMinVersionSupported` | 최소 버전 지원 여부 |
159
- | `getSchemeUri` | 스킴 URI 조회 |
160
- | `getLocale` | 로케일 조회 |
161
- | `getDeviceId` | 디바이스 ID 조회 |
162
- | `getGroupId` | 그룹 ID 조회 |
163
- | `getNetworkStatus` | 네트워크 상태 조회 |
164
- | `getServerTime` | 서버 시간 조회 |
165
- | `env.getDeploymentId` | 배포 ID 조회 |
166
- | `getAppsInTossGlobals` | 글로벌 설정 조회 |
328
+ | `getPlatformOS` | state의 platform 반환 (기본: `'ios'`) |
329
+ | `getOperationalEnvironment` | state의 environment 반환 (기본: `'sandbox'`) |
330
+ | `getTossAppVersion` | state의 appVersion 반환 (기본: `'5.240.0'`) |
331
+ | `isMinVersionSupported` | 시맨틱 버전 비교 수행 |
332
+ | `getSchemeUri` | state의 schemeUri 또는 `window.location.pathname` |
333
+ | `getLocale` | state의 locale 반환 (기본: `'ko-KR'`) |
334
+ | `getDeviceId` | localStorage에 저장된 고유 UUID 반환 |
335
+ | `getGroupId` | state의 groupId 반환 |
336
+ | `getNetworkStatus` | 모드에 따라 state 또는 브라우저 API 사용 |
337
+ | `getServerTime` | `Date.now()` 반환 |
338
+ | `env.getDeploymentId` | state의 deploymentId 반환 |
339
+ | `getAppsInTossGlobals` | `{ deploymentId, brandDisplayName, brandIcon, brandPrimaryColor }` |
167
340
 
168
341
  ### Safe Area
169
342
 
170
- | API | 설명 |
343
+ | API | Mock 동작 |
171
344
  |---|---|
172
- | `SafeAreaInsets.get` | Safe Area Insets 조회 |
173
- | `SafeAreaInsets.subscribe` | Safe Area Insets 변경 구독 |
174
- | `getSafeAreaInsets` | Safe Area Insets 조회 (함수형) |
175
-
176
- ### 이벤트/분석
177
-
178
- | API | 설명 |
179
- |---|---|
180
- | `graniteEvent` | Granite 이벤트 발행 |
181
- | `appsInTossEvent` | 앱인토스 이벤트 발행 |
182
- | `tdsEvent` | TDS 이벤트 발행 |
183
- | `onVisibilityChangedByTransparentServiceWeb` | 투명 서비스웹 가시성 변경 핸들러 |
184
- | `Analytics` | 분석 네임스페이스 |
185
- | `eventLog` | 이벤트 로그 기록 |
345
+ | `SafeAreaInsets.get` | `{ top, bottom, left: 0, right: 0 }` 반환 |
346
+ | `SafeAreaInsets.subscribe` | 상태 변경 콜백 호출, unsubscribe 함수 반환 |
347
+ | `getSafeAreaInsets` | top inset 반환 (deprecated) |
186
348
 
187
349
  ### 디바이스 기능
188
350
 
189
- | API | 설명 |
351
+ | API | Mock 동작 |
190
352
  |---|---|
191
- | `Storage` | 로컬 스토리지 (getItem, setItem, removeItem, clearItems) |
192
- | `getCurrentLocation` | 현재 위치 조회 |
193
- | `startUpdateLocation` | 위치 업데이트 시작 |
194
- | `Accuracy` | 위치 정확도 enum |
195
- | `openCamera` | 카메라 열기 |
196
- | `fetchAlbumPhotos` | 앨범 사진 가져오기 |
197
- | `fetchContacts` | 연락처 가져오기 |
198
- | `getClipboardText` | 클립보드 텍스트 읽기 |
199
- | `setClipboardText` | 클립보드 텍스트 쓰기 |
200
- | `generateHapticFeedback` | 햅틱 피드백 생성 |
201
- | `saveBase64Data` | Base64 데이터 저장 |
353
+ | `Storage.getItem/setItem/removeItem/clearItems` | localStorage에 `__ait_storage:` prefix로 저장 |
354
+ | `getCurrentLocation` | 모드별: mock(state 좌표), web(Geolocation API), prompt(Panel 입력) |
355
+ | `startUpdateLocation` | mock(랜덤 좌표 변동), web(watchPosition), prompt(반복 입력) |
356
+ | `openCamera` | mock(더미 이미지), web(파일 선택기), prompt(Panel 파일 입력) |
357
+ | `fetchAlbumPhotos` | mock(더미 이미지 배열), web(파일 다중 선택), prompt(Panel 파일 입력) |
358
+ | `fetchContacts` | 페이지네이션 지원 mock 연락처 반환, `query.contains` 검색 |
359
+ | `getClipboardText` / `setClipboardText` | mock(state 저장) 또는 web(Clipboard API) |
360
+ | `generateHapticFeedback` | 콘솔 로그 + analytics 기록 |
361
+ | `saveBase64Data` | anchor 엘리먼트로 파일 다운로드 |
202
362
 
203
363
  ### IAP/결제
204
364
 
205
- | API | 설명 |
365
+ | API | Mock 동작 |
206
366
  |---|---|
207
- | `IAP.createOneTimePurchaseOrder` | 일회성 구매 주문 생성 |
208
- | `IAP.createSubscriptionPurchaseOrder` | 구독 구매 주문 생성 |
209
- | `IAP.getProductItemList` | 상품 목록 조회 |
210
- | `IAP.getPendingOrders` | 대기 중 주문 조회 |
211
- | `IAP.getCompletedOrRefundedOrders` | 완료/환불 주문 조회 |
212
- | `IAP.completeProductGrant` | 상품 지급 완료 |
213
- | `IAP.getSubscriptionInfo` | 구독 정보 조회 |
214
- | `checkoutPayment` | TossPay 결제 |
367
+ | `IAP.createOneTimePurchaseOrder` | 300ms 딜레이 state의 `nextResult`에 따라 성공/실패 시뮬레이션 |
368
+ | `IAP.createSubscriptionPurchaseOrder` | 위와 동일한 흐름 |
369
+ | `IAP.getProductItemList` | state의 상품 목록 반환 |
370
+ | `IAP.getPendingOrders` | 대기 중 주문 목록 |
371
+ | `IAP.getCompletedOrRefundedOrders` | 완료/환불 주문 목록 |
372
+ | `IAP.completeProductGrant` | 대기 완료 주문 이동 |
373
+ | `IAP.getSubscriptionInfo` | 활성 구독 mock (30일 만료, 자동 갱신) |
374
+ | `checkoutPayment` | 300ms 딜레이 후 state의 결제 결과 반환 (TossPay) |
375
+
376
+ **IAP 구매 시뮬레이션 흐름:**
377
+
378
+ 1. `IAP.createOneTimePurchaseOrder()` 호출
379
+ 2. 300ms 딜레이 (결제 UI 시뮬레이션)
380
+ 3. `state.iap.nextResult` 확인 → `'success'`가 아니면 `onError` 호출
381
+ 4. 성공 시 `processProductGrant` 콜백 실행 → 실패하면 `'PRODUCT_NOT_GRANTED_BY_PARTNER'` 에러
382
+ 5. 모두 성공하면 `completedOrders`에 기록, `onEvent`로 주문 결과 전달
215
383
 
216
384
  ### 광고
217
385
 
218
- | API | 설명 |
386
+ | API | Mock 동작 |
387
+ |---|---|
388
+ | `GoogleAdMob.loadAppsInTossAdMob` | 200ms 후 `loaded` 이벤트 |
389
+ | `GoogleAdMob.showAppsInTossAdMob` | 50ms~1.5s에 걸쳐 requested→show→impression→reward→dismissed 이벤트 순차 발행 |
390
+ | `GoogleAdMob.isAppsInTossAdMobLoaded` | 로드 여부 boolean 반환 |
391
+ | `TossAds.initialize/attach/attachBanner` | 회색 플레이스홀더 div 렌더링 |
392
+ | `TossAds.destroy/destroyAll` | no-op |
393
+ | `loadFullScreenAd` / `showFullScreenAd` | GoogleAdMob과 유사한 흐름 |
394
+
395
+ ### 이벤트
396
+
397
+ | API | Mock 동작 |
398
+ |---|---|
399
+ | `graniteEvent.addEventListener` | `__ait:backEvent`, `__ait:homeEvent` 커스텀 이벤트 수신 |
400
+ | `appsInTossEvent.addEventListener` | no-op |
401
+ | `tdsEvent.addEventListener` | `__ait:navigationAccessoryEvent` 수신 |
402
+ | `onVisibilityChangedByTransparentServiceWeb` | `document.visibilitychange` 이벤트 위임 |
403
+
404
+ ### 분석
405
+
406
+ | API | Mock 동작 |
219
407
  |---|---|
220
- | `GoogleAdMob.loadAppsInTossAdMob` | AdMob 광고 로드 |
221
- | `GoogleAdMob.showAppsInTossAdMob` | AdMob 광고 표시 |
222
- | `GoogleAdMob.isAppsInTossAdMobLoaded` | AdMob 광고 로드 여부 확인 |
223
- | `TossAds.initialize` | 토스 광고 초기화 |
224
- | `TossAds.attach` | 광고 슬롯 부착 |
225
- | `TossAds.attachBanner` | 배너 광고 부착 |
226
- | `TossAds.destroy` | 광고 슬롯 제거 |
227
- | `TossAds.destroyAll` | 모든 광고 슬롯 제거 |
228
- | `loadFullScreenAd` | 전면 광고 로드 |
229
- | `showFullScreenAd` | 전면 광고 표시 |
408
+ | `Analytics.screen/impression/click` | analyticsLog에 타입별 기록, Panel에서 실시간 확인 |
409
+ | `eventLog` | `log_name`, `log_type`, `params`로 커스텀 이벤트 기록 |
230
410
 
231
411
  ### 게임/프로모션
232
412
 
233
- | API | 설명 |
413
+ | API | Mock 동작 |
234
414
  |---|---|
235
- | `grantPromotionReward` | 프로모션 보상 지급 |
236
- | `grantPromotionRewardForGame` | 게임 프로모션 보상 지급 |
237
- | `submitGameCenterLeaderBoardScore` | 리더보드 점수 등록 |
238
- | `getGameCenterGameProfile` | 게임센터 프로필 조회 |
239
- | `openGameCenterLeaderboard` | 리더보드 열기 |
240
- | `contactsViral` | 연락처 바이럴 |
415
+ | `grantPromotionReward` | 타임스탬프 기반 mock key 반환 |
416
+ | `grantPromotionRewardForGame` | 위와 동일 |
417
+ | `submitGameCenterLeaderBoardScore` | state에 점수 추가, `{ statusCode: 'SUCCESS' }` |
418
+ | `getGameCenterGameProfile` | mock 프로필 반환 (없으면 `PROFILE_NOT_FOUND`) |
419
+ | `openGameCenterLeaderboard` | 콘솔 로그 (no-op) |
420
+ | `contactsViral` | 500ms close 이벤트 발행 |
241
421
 
242
422
  ### 권한
243
423
 
244
- | API | 설명 |
424
+ | API | Mock 동작 |
245
425
  |---|---|
246
- | `getPermission` | 권한 상태 조회 |
247
- | `openPermissionDialog` | 권한 설정 다이얼로그 열기 |
248
- | `requestPermission` | 권한 요청 |
426
+ | `getPermission` | state의 권한 상태 반환 (allowed/denied/notDetermined) |
427
+ | `openPermissionDialog` | 상태를 `allowed`로 변경 |
428
+ | `requestPermission` | `openPermissionDialog`에 위임 |
429
+
430
+ > 권한이 필요한 함수(openCamera, getCurrentLocation 등)는 `withPermission()`으로 래핑되어 `.getPermission()`, `.openPermissionDialog()` 메서드가 자동 부착됩니다.
249
431
 
250
432
  ### 파트너
251
433
 
252
- | API | 설명 |
434
+ | API | Mock 동작 |
253
435
  |---|---|
254
- | `partner.addAccessoryButton` | 액세서리 버튼 추가 |
255
- | `partner.removeAccessoryButton` | 액세서리 버튼 제거 |
436
+ | `partner.addAccessoryButton` | 콘솔 로그 (no-op) |
437
+ | `partner.removeAccessoryButton` | 콘솔 로그 (no-op) |
438
+
439
+ ## 테스트에서의 활용
440
+
441
+ vitest/jest에서 mock 라이브러리를 직접 import하여 테스트할 수 있습니다.
442
+
443
+ > mock 함수들이 `window`, `document`, `localStorage` 등 브라우저 API를 사용하므로 **jsdom 환경**이 필요합니다.
444
+ >
445
+ > ```ts
446
+ > // vitest.config.ts
447
+ > import { defineConfig } from 'vitest/config';
448
+ > export default defineConfig({ test: { environment: 'jsdom' } });
449
+ > ```
450
+
451
+ ```ts
452
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
453
+ import { appLogin, Storage, getCurrentLocation, getNetworkStatus, openCamera, IAP } from '@ait-co/devtools/mock';
454
+ import { aitState } from '@ait-co/devtools/mock';
455
+
456
+ beforeEach(() => {
457
+ aitState.reset(); // 매 테스트 전 상태 초기화
458
+ });
459
+
460
+ // 인증 테스트
461
+ it('appLogin은 authorizationCode를 반환한다', async () => {
462
+ const result = await appLogin();
463
+ expect(result.authorizationCode).toBeDefined();
464
+ });
465
+
466
+ // 상태를 세팅하고 함수 호출
467
+ it('오프라인 상태에서 네트워크 조회', async () => {
468
+ aitState.update({ networkStatus: 'OFFLINE' });
469
+ const status = await getNetworkStatus();
470
+ expect(status).toBe('OFFLINE');
471
+ });
472
+
473
+ // 권한 denied 시나리오
474
+ it('카메라 권한이 denied면 에러를 던진다', async () => {
475
+ aitState.patch('permissions', { camera: 'denied' });
476
+ await expect(openCamera()).rejects.toThrow();
477
+ });
478
+
479
+ // IAP 실패 시나리오 (fake timers 필요)
480
+ it('구매 취소 시 onError가 호출된다', async () => {
481
+ vi.useFakeTimers();
482
+ aitState.patch('iap', { nextResult: 'USER_CANCELED' });
483
+ const onError = vi.fn();
484
+ IAP.createOneTimePurchaseOrder({
485
+ options: { sku: 'item_01', processProductGrant: async () => true },
486
+ onEvent: vi.fn(),
487
+ onError,
488
+ });
489
+ await vi.advanceTimersByTimeAsync(500);
490
+ expect(onError).toHaveBeenCalledWith({ code: 'USER_CANCELED' });
491
+ vi.useRealTimers();
492
+ });
493
+
494
+ // Storage 테스트
495
+ it('Storage에 값을 저장하고 읽을 수 있다', async () => {
496
+ await Storage.setItem('key1', 'value1');
497
+ const result = await Storage.getItem('key1');
498
+ expect(result).toBe('value1');
499
+ });
500
+ ```
256
501
 
257
502
  ## SDK 업데이트 대응
258
503
 
259
- ait-devtools는 세 가지 메커니즘으로 SDK 변경에 대응합니다:
504
+ 세 가지 메커니즘으로 SDK 변경에 안전하게 대응합니다:
260
505
 
261
- ### 1. peerDependencies + typeof 타입 강제
506
+ ### 1. 컴파일 타임 타입 검증 (`__typecheck.ts`)
262
507
 
263
- `src/__typecheck.ts`에서 mock의 주요 export가 원본 SDK와 타입 호환되는지 컴파일 타임에 검증합니다. SDK 시그니처가 변경되면 `tsc --noEmit`에서 즉시 에러가 발생합니다.
508
+ `src/__typecheck.ts`에서 mock의 주요 export가 원본 SDK와 타입 호환되는지 검증합니다. SDK 시그니처가 변경되면 `pnpm typecheck`에서 즉시 에러가 발생합니다.
264
509
 
265
510
  ```ts
266
511
  type Assert<TMock, TOriginal> = TMock extends TOriginal ? true : never;
267
512
  type _AppLogin = Assert<typeof Mock.appLogin, typeof Original.appLogin>;
513
+ // 40+ 타입 호환성 assertion
268
514
  ```
269
515
 
270
- ### 2. Proxy Fallback
516
+ ### 2. Proxy 트립와이어 (런타임 차단)
271
517
 
272
- 미구현 API 접근하면 에러 대신 경고 로그와 함께 no-op 함수를 반환하는 Proxy fallback으로 graceful하게 처리합니다. 새로운 SDK API 추가되어도 앱이 크래시하지 않습니다.
518
+ `createMockProxy()`는 미구현 API 접근 즉시 `Error`를 throw합니다. mock에 없는 API가 SDK에는 있을 있어 "devtools에서는 잘 되는데 실제 SDK에서는 안 되는" 배포 사고를 원천 차단하기 위한 의도적 동작입니다. 누락된 API [이슈](https://github.com/apps-in-toss-community/devtools/issues)로 제보하거나 직접 mock을 추가해 주세요.
519
+
520
+ ```
521
+ [@ait-co/devtools] IAP.newMethod is not mocked. This API may exist in
522
+ @apps-in-toss/web-framework, but devtools' mock does not cover it yet.
523
+ Please file an issue: https://github.com/apps-in-toss-community/devtools/issues
524
+ ```
273
525
 
274
526
  ### 3. GitHub Actions 주간 CI
275
527
 
276
- `.github/workflows/check-sdk-update.yml`이 **매주 월요일** 자동으로 실행되어:
528
+ `.github/workflows/check-sdk-update.yml`이 **매주 월요일** 자동으로:
277
529
 
278
530
  1. `@apps-in-toss/web-framework`의 새 버전 확인
279
531
  2. 최신 버전으로 업데이트 후 타입 체크 실행
280
532
  3. 새 버전 감지 시 자동으로 GitHub Issue 생성 (타입 에러 여부 포함)
281
533
 
534
+ ## Contributing
535
+
536
+ ### 새 API mock 추가 절차
537
+
538
+ 1. 해당 카테고리 디렉토리에 함수 구현 (예: `src/mock/device/`)
539
+ 2. `src/mock/index.ts`에 export 추가
540
+ 3. `src/__typecheck.ts`에 타입 호환성 assertion 추가
541
+ 4. `pnpm typecheck`로 원본과 호환되는지 검증
542
+ 5. `src/__tests/`에 테스트 작성
543
+
544
+ ```bash
545
+ pnpm build # tsup으로 빌드
546
+ pnpm typecheck # 타입 호환성 검증
547
+ pnpm test # 전체 테스트 실행
548
+ ```
549
+
550
+ ## Troubleshooting
551
+
552
+ ### `[@ait-co/devtools] XXX.method is not mocked` 에러가 날 때
553
+
554
+ 사용 중인 SDK API가 아직 mock으로 구현되지 않았습니다. devtools는 "잘 되는 척" 배포를 막기 위해 미구현 API 접근 시 throw합니다. [이슈를 등록](https://github.com/apps-in-toss-community/devtools/issues)하거나 직접 mock을 추가한 뒤 다시 실행하세요.
555
+
556
+ ### DevTools Panel이 안 보일 때
557
+
558
+ - 플러그인 옵션에서 `panel: false`로 설정하지 않았는지 확인
559
+ - 수동 alias 설정을 사용 중이라면, 진입점 파일에 직접 import를 추가하세요:
560
+ ```ts
561
+ import '@ait-co/devtools/panel';
562
+ ```
563
+ - 플러그인은 파일명이 `main`, `index`, `entry`, `app` 중 하나인 진입점에만 자동 주입합니다 (대소문자 무시). 파일명이 이 패턴에 맞지 않으면 수동으로 `import '@ait-co/devtools/panel'`을 추가하세요.
564
+
565
+ ### 서브패스 import는 mock되지 않음
566
+
567
+ `@apps-in-toss/web-framework/some-subpath` 형태의 서브패스 import는 alias가 적용되지 않습니다. SDK의 메인 엔트리(`@apps-in-toss/web-framework`)만 mock됩니다. 특정 서브패스도 mock이 필요하다면 번들러의 `resolve.alias`에 해당 서브패스를 수동으로 추가하세요.
568
+
569
+ ### Next.js Turbopack에서 설정하는 법
570
+
571
+ Turbopack은 unplugin을 지원하지 않으므로, `next.config.js`에서 `resolveAlias`를 사용하세요 (위의 [Next.js (Turbopack)](#nextjs-turbopack) 섹션 참고). Panel은 진입점에서 직접 import해야 합니다:
572
+
573
+ ```ts
574
+ // app/layout.tsx 또는 pages/_app.tsx
575
+ import '@ait-co/devtools/panel';
576
+ ```
577
+
578
+ ## 패키지 Export 구조
579
+
580
+ | Import path | 용도 |
581
+ |---|---|
582
+ | `@ait-co/devtools` 또는 `@ait-co/devtools/mock` | 모든 mock export (번들러 alias 대상) |
583
+ | `@ait-co/devtools/panel` | Floating DevTools Panel (import 시 자동 마운트) |
584
+ | `@ait-co/devtools/unplugin` | 번들러 플러그인 (.vite, .webpack, .rspack, .esbuild, .rollup) |
585
+
282
586
  ## 라이센스
283
587
 
284
588
  BSD 3-Clause