@ait-co/devtools 0.0.1 → 0.0.2

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