@ait-co/devtools 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DaveDev42
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # ait-devtools
2
+
3
+ `@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics` import도 함께 mock됩니다.
4
+
5
+ 앱인토스(Apps in Toss) 미니앱을 **일반 브라우저**에서 개발하고 테스트할 수 있게 해줍니다. 토스 앱 없이도 SDK의 모든 기능을 시뮬레이션하여 빠른 개발 사이클을 지원합니다.
6
+
7
+ - **60+ SDK API mock** — 인증, 결제, IAP, 위치, 카메라, 스토리지 등
8
+ - **Floating DevTools Panel** — 브라우저에서 SDK 상태를 실시간으로 제어
9
+ - **모든 번들러 지원** — [unplugin](https://github.com/unjs/unplugin) 기반 Vite, Webpack, Rspack, esbuild, Rollup 통합
10
+
11
+ ## 설치
12
+
13
+ ```bash
14
+ npm install -D ait-devtools
15
+ ```
16
+
17
+ > `@apps-in-toss/web-framework ^2.0.0`이 peerDependency로 설정되어 있습니다 (optional).
18
+
19
+ ## 사용법
20
+
21
+ ### 1. Vite 플러그인
22
+
23
+ ```ts
24
+ // vite.config.ts
25
+ import aitDevtools from 'ait-devtools/unplugin';
26
+
27
+ export default {
28
+ plugins: [aitDevtools.vite()],
29
+ };
30
+ ```
31
+
32
+ ### 2. Webpack
33
+
34
+ ```js
35
+ // webpack.config.js (ESM)
36
+ import aitDevtools from 'ait-devtools/unplugin';
37
+ config.plugins.push(aitDevtools.webpack());
38
+
39
+ // webpack.config.js (CommonJS)
40
+ const aitDevtools = require('ait-devtools/unplugin');
41
+ config.plugins.push(aitDevtools.webpack());
42
+ ```
43
+
44
+ ### 3. Next.js (Turbopack)
45
+
46
+ Turbopack은 플러그인 시스템을 지원하지 않으므로 `resolveAlias`를 사용합니다:
47
+
48
+ ```js
49
+ // next.config.js (Next.js 15+)
50
+ module.exports = {
51
+ turbo: {
52
+ resolveAlias: {
53
+ '@apps-in-toss/web-framework': 'ait-devtools/mock',
54
+ },
55
+ },
56
+ };
57
+
58
+ // Next.js 14 이하
59
+ module.exports = {
60
+ experimental: {
61
+ turbo: {
62
+ resolveAlias: {
63
+ '@apps-in-toss/web-framework': 'ait-devtools/mock',
64
+ },
65
+ },
66
+ },
67
+ };
68
+ ```
69
+
70
+ ### 4. 수동 Alias 설정
71
+
72
+ 번들러의 `resolve.alias` 설정으로 직접 지정할 수도 있습니다:
73
+
74
+ ```js
75
+ // vite.config.ts 또는 webpack.config.js
76
+ {
77
+ resolve: {
78
+ 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',
82
+ },
83
+ },
84
+ }
85
+ ```
86
+
87
+ ## Floating DevTools Panel
88
+
89
+ 플러그인 사용 시 진입점 파일에 패널이 자동 주입됩니다 (`aitDevtools.vite({ panel: false })`로 비활성화 가능).
90
+
91
+ 화면 우하단의 **'AIT' 버튼**을 클릭하면 DevTools 패널이 토글됩니다.
92
+
93
+ ### 7개 탭
94
+
95
+ | 탭 | 설명 |
96
+ |---|---|
97
+ | **Environment** | 플랫폼 OS (ios/android), 앱 버전, 환경 (toss/sandbox), 로케일, 네트워크 상태, Safe Area Insets (top/bottom) 설정 |
98
+ | **Permissions** | camera, photos, geolocation, clipboard, contacts, microphone 권한 상태 제어 (allowed/denied/notDetermined) |
99
+ | **Location** | 위도, 경도, 정확도 등 GPS 좌표 설정 |
100
+ | **IAP** | 인앱 구매 시뮬레이션 — 다음 구매 결과(success/취소/에러 등), TossPay 결제 결과, 완료된 주문 내역 |
101
+ | **Events** | Back/Home 네비게이션 이벤트 트리거, 로그인 상태 토글 |
102
+ | **Analytics** | 기록된 분석 이벤트 실시간 로그 뷰어 (타임스탬프, 타입, 파라미터) |
103
+ | **Storage** | `Storage` API로 저장된 항목 조회 및 초기화 |
104
+
105
+ ## 브라우저 콘솔 사용법
106
+
107
+ `window.__ait`를 통해 mock 상태를 직접 제어할 수 있습니다:
108
+
109
+ ```js
110
+ // 네트워크 상태 변경
111
+ __ait.update({ networkStatus: 'OFFLINE' });
112
+
113
+ // 여러 상태 한번에 업데이트
114
+ __ait.update({ platform: 'android', locale: 'en-US' });
115
+
116
+ // 이벤트 트리거
117
+ __ait.trigger('backEvent');
118
+
119
+ // 중첩 상태 업데이트 (permissions, iap 등)
120
+ __ait.patch('permissions', { camera: 'denied' });
121
+
122
+ // 현재 상태 조회
123
+ console.log(__ait.state.platform);
124
+ ```
125
+
126
+ ## Mock API 목록
127
+
128
+ ### 인증/로그인
129
+
130
+ | API | 설명 |
131
+ |---|---|
132
+ | `appLogin` | 앱 로그인 |
133
+ | `getIsTossLoginIntegratedService` | 토스 로그인 통합 서비스 여부 |
134
+ | `getUserKeyForGame` | 게임용 유저 키 조회 |
135
+ | `appsInTossSignTossCert` | 토스 인증서 서명 |
136
+
137
+ ### 화면/네비게이션
138
+
139
+ | API | 설명 |
140
+ |---|---|
141
+ | `closeView` | 현재 뷰 닫기 |
142
+ | `openURL` | URL 열기 |
143
+ | `share` | 공유하기 |
144
+ | `getTossShareLink` | 토스 공유 링크 조회 |
145
+ | `setIosSwipeGestureEnabled` | iOS 스와이프 제스처 활성화 설정 |
146
+ | `setDeviceOrientation` | 디바이스 방향 설정 |
147
+ | `setScreenAwakeMode` | 화면 꺼짐 방지 설정 |
148
+ | `setSecureScreen` | 보안 화면 설정 (캡처 방지) |
149
+ | `requestReview` | 앱 리뷰 요청 |
150
+
151
+ ### 환경 정보
152
+
153
+ | API | 설명 |
154
+ |---|---|
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` | 글로벌 설정 조회 |
167
+
168
+ ### Safe Area
169
+
170
+ | API | 설명 |
171
+ |---|---|
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` | 이벤트 로그 기록 |
186
+
187
+ ### 디바이스 기능
188
+
189
+ | API | 설명 |
190
+ |---|---|
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 데이터 저장 |
202
+
203
+ ### IAP/결제
204
+
205
+ | API | 설명 |
206
+ |---|---|
207
+ | `IAP.createOneTimePurchaseOrder` | 일회성 구매 주문 생성 |
208
+ | `IAP.createSubscriptionPurchaseOrder` | 구독 구매 주문 생성 |
209
+ | `IAP.getProductItemList` | 상품 목록 조회 |
210
+ | `IAP.getPendingOrders` | 대기 중 주문 조회 |
211
+ | `IAP.getCompletedOrRefundedOrders` | 완료/환불 주문 조회 |
212
+ | `IAP.completeProductGrant` | 상품 지급 완료 |
213
+ | `IAP.getSubscriptionInfo` | 구독 정보 조회 |
214
+ | `checkoutPayment` | TossPay 결제 |
215
+
216
+ ### 광고
217
+
218
+ | API | 설명 |
219
+ |---|---|
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` | 전면 광고 표시 |
230
+
231
+ ### 게임/프로모션
232
+
233
+ | API | 설명 |
234
+ |---|---|
235
+ | `grantPromotionReward` | 프로모션 보상 지급 |
236
+ | `grantPromotionRewardForGame` | 게임 프로모션 보상 지급 |
237
+ | `submitGameCenterLeaderBoardScore` | 리더보드 점수 등록 |
238
+ | `getGameCenterGameProfile` | 게임센터 프로필 조회 |
239
+ | `openGameCenterLeaderboard` | 리더보드 열기 |
240
+ | `contactsViral` | 연락처 바이럴 |
241
+
242
+ ### 권한
243
+
244
+ | API | 설명 |
245
+ |---|---|
246
+ | `getPermission` | 권한 상태 조회 |
247
+ | `openPermissionDialog` | 권한 설정 다이얼로그 열기 |
248
+ | `requestPermission` | 권한 요청 |
249
+
250
+ ### 파트너
251
+
252
+ | API | 설명 |
253
+ |---|---|
254
+ | `partner.addAccessoryButton` | 액세서리 버튼 추가 |
255
+ | `partner.removeAccessoryButton` | 액세서리 버튼 제거 |
256
+
257
+ ## SDK 업데이트 대응
258
+
259
+ ait-devtools는 세 가지 메커니즘으로 SDK 변경에 대응합니다:
260
+
261
+ ### 1. peerDependencies + typeof 타입 강제
262
+
263
+ `src/__typecheck.ts`에서 mock의 주요 export가 원본 SDK와 타입 호환되는지 컴파일 타임에 검증합니다. SDK 시그니처가 변경되면 `tsc --noEmit`에서 즉시 에러가 발생합니다.
264
+
265
+ ```ts
266
+ type Assert<TMock, TOriginal> = TMock extends TOriginal ? true : never;
267
+ type _AppLogin = Assert<typeof Mock.appLogin, typeof Original.appLogin>;
268
+ ```
269
+
270
+ ### 2. Proxy Fallback
271
+
272
+ 미구현 API에 접근하면 에러 대신 경고 로그와 함께 no-op 함수를 반환하는 Proxy fallback으로 graceful하게 처리합니다. 새로운 SDK API가 추가되어도 앱이 크래시하지 않습니다.
273
+
274
+ ### 3. GitHub Actions 주간 CI
275
+
276
+ `.github/workflows/check-sdk-update.yml`이 **매주 월요일** 자동으로 실행되어:
277
+
278
+ 1. `@apps-in-toss/web-framework`의 새 버전 확인
279
+ 2. 최신 버전으로 업데이트 후 타입 체크 실행
280
+ 3. 새 버전 감지 시 자동으로 GitHub Issue 생성 (타입 에러 여부 포함)
281
+
282
+ ## 라이센스
283
+
284
+ BSD 3-Clause
@@ -0,0 +1,146 @@
1
+ // src/mock/state.ts
2
+ var DEFAULT_STATE = {
3
+ platform: "ios",
4
+ environment: "sandbox",
5
+ appVersion: "5.240.0",
6
+ locale: "ko-KR",
7
+ schemeUri: "/",
8
+ groupId: "mock-group-id",
9
+ deploymentId: "mock-deployment-id",
10
+ deviceId: "",
11
+ brand: {
12
+ displayName: "Mock App",
13
+ icon: "",
14
+ primaryColor: "#3182F6"
15
+ },
16
+ networkStatus: "WIFI",
17
+ permissions: {
18
+ clipboard: "allowed",
19
+ contacts: "allowed",
20
+ photos: "allowed",
21
+ geolocation: "allowed",
22
+ camera: "allowed",
23
+ microphone: "notDetermined"
24
+ },
25
+ location: {
26
+ coords: {
27
+ latitude: 37.5665,
28
+ longitude: 126.978,
29
+ altitude: 0,
30
+ accuracy: 10,
31
+ altitudeAccuracy: 0,
32
+ heading: 0
33
+ },
34
+ timestamp: Date.now(),
35
+ accessLocation: "FINE"
36
+ },
37
+ safeAreaInsets: { top: 47, bottom: 34, left: 0, right: 0 },
38
+ contacts: [
39
+ { name: "\uD64D\uAE38\uB3D9", phoneNumber: "010-1234-5678" },
40
+ { name: "\uAE40\uD1A0\uC2A4", phoneNumber: "010-9876-5432" }
41
+ ],
42
+ iap: {
43
+ products: [
44
+ {
45
+ sku: "mock-gem-100",
46
+ type: "CONSUMABLE",
47
+ displayName: "\uBCF4\uC11D 100\uAC1C",
48
+ displayAmount: "1,000\uC6D0",
49
+ iconUrl: "",
50
+ description: "\uAC8C\uC784\uC5D0\uC11C \uC0AC\uC6A9\uD560 \uC218 \uC788\uB294 \uBCF4\uC11D 100\uAC1C"
51
+ }
52
+ ],
53
+ nextResult: "success",
54
+ pendingOrders: [],
55
+ completedOrders: []
56
+ },
57
+ payment: {
58
+ nextResult: "success",
59
+ failReason: ""
60
+ },
61
+ auth: {
62
+ isLoggedIn: true,
63
+ isTossLoginIntegrated: true,
64
+ userKeyHash: "mock-user-hash-abc123"
65
+ },
66
+ ads: {
67
+ isLoaded: false,
68
+ nextEvent: "loaded"
69
+ },
70
+ game: {
71
+ profile: { nickname: "MockPlayer", profileImageUri: "" },
72
+ leaderboardScores: []
73
+ },
74
+ analyticsLog: []
75
+ };
76
+ function generateDeviceId() {
77
+ const stored = localStorage.getItem("__ait_device_id");
78
+ if (stored) return stored;
79
+ const id = crypto.randomUUID();
80
+ localStorage.setItem("__ait_device_id", id);
81
+ return id;
82
+ }
83
+ var AitStateManager = class {
84
+ _state;
85
+ _listeners = /* @__PURE__ */ new Set();
86
+ constructor() {
87
+ this._state = structuredClone(DEFAULT_STATE);
88
+ try {
89
+ this._state.deviceId = generateDeviceId();
90
+ } catch {
91
+ this._state.deviceId = "mock-device-" + Math.random().toString(36).slice(2);
92
+ }
93
+ }
94
+ get state() {
95
+ return this._state;
96
+ }
97
+ update(partial) {
98
+ this._state = { ...this._state, ...partial };
99
+ this._notify();
100
+ }
101
+ /** 중첩 객체 업데이트용 */
102
+ patch(key, partial) {
103
+ const current = this._state[key];
104
+ if (typeof current === "object" && current !== null && !Array.isArray(current)) {
105
+ this._state = { ...this._state, [key]: { ...current, ...partial } };
106
+ } else {
107
+ this._state = { ...this._state, [key]: partial };
108
+ }
109
+ this._notify();
110
+ }
111
+ subscribe(listener) {
112
+ this._listeners.add(listener);
113
+ return () => this._listeners.delete(listener);
114
+ }
115
+ /** 분석 로그 추가 */
116
+ logAnalytics(entry) {
117
+ this._state = {
118
+ ...this._state,
119
+ analyticsLog: [...this._state.analyticsLog, { ...entry, timestamp: Date.now() }]
120
+ };
121
+ this._notify();
122
+ }
123
+ /** 이벤트 트리거 (backEvent, homeEvent 등) */
124
+ trigger(event) {
125
+ window.dispatchEvent(new CustomEvent(`__ait:${event}`));
126
+ }
127
+ reset() {
128
+ const deviceId = this._state.deviceId;
129
+ this._state = { ...structuredClone(DEFAULT_STATE), deviceId };
130
+ this._notify();
131
+ }
132
+ _notify() {
133
+ for (const listener of this._listeners) {
134
+ listener();
135
+ }
136
+ }
137
+ };
138
+ var aitState = new AitStateManager();
139
+ if (typeof window !== "undefined") {
140
+ window.__ait = aitState;
141
+ }
142
+
143
+ export {
144
+ aitState
145
+ };
146
+ //# sourceMappingURL=chunk-YYIIG3JT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mock/state.ts"],"sourcesContent":["/**\n * ait-devtools 중앙 상태 관리\n * DevTools Panel과 mock 구현체가 이 상태를 공유한다.\n */\n\nexport type PlatformOS = 'ios' | 'android';\nexport type OperationalEnvironment = 'toss' | 'sandbox';\nexport type NetworkStatus = 'OFFLINE' | 'WIFI' | '2G' | '3G' | '4G' | '5G' | 'WWAN' | 'UNKNOWN';\nexport type PermissionStatus = 'notDetermined' | 'denied' | 'allowed';\nexport type PermissionName = 'clipboard' | 'contacts' | 'photos' | 'geolocation' | 'camera' | 'microphone';\nexport type HapticFeedbackType = 'tickWeak' | 'tap' | 'tickMedium' | 'softMedium' | 'basicWeak' | 'basicMedium' | 'success' | 'error' | 'wiggle' | 'confetti';\n\nexport interface LocationCoords {\n latitude: number;\n longitude: number;\n altitude: number;\n accuracy: number;\n altitudeAccuracy: number;\n heading: number;\n}\n\nexport interface MockLocation {\n coords: LocationCoords;\n timestamp: number;\n accessLocation?: 'FINE' | 'COARSE';\n}\n\nexport interface MockContact {\n name: string;\n phoneNumber: string;\n}\n\nexport interface MockIapProduct {\n sku: string;\n type: 'CONSUMABLE' | 'NON_CONSUMABLE' | 'SUBSCRIPTION';\n displayName: string;\n displayAmount: string;\n iconUrl: string;\n description: string;\n renewalCycle?: 'WEEKLY' | 'MONTHLY' | 'YEARLY';\n}\n\nexport type IapNextResult = 'success' | 'USER_CANCELED' | 'INVALID_PRODUCT_ID' | 'PAYMENT_PENDING' | 'NETWORK_ERROR' | 'ITEM_ALREADY_OWNED' | 'INTERNAL_ERROR';\n\nexport interface AnalyticsLogEntry {\n timestamp: number;\n type: string;\n params: Record<string, unknown>;\n}\n\nexport interface SafeAreaInsets {\n top: number;\n bottom: number;\n left: number;\n right: number;\n}\n\ntype Listener = () => void;\n\nexport interface AitDevtoolsState {\n // 환경\n platform: PlatformOS;\n environment: OperationalEnvironment;\n appVersion: string;\n locale: string;\n schemeUri: string;\n groupId: string;\n deploymentId: string;\n deviceId: string;\n\n // 브랜드\n brand: {\n displayName: string;\n icon: string;\n primaryColor: string;\n };\n\n // 네트워크\n networkStatus: NetworkStatus;\n\n // 권한\n permissions: Record<PermissionName, PermissionStatus>;\n\n // 위치\n location: MockLocation;\n\n // Safe Area\n safeAreaInsets: SafeAreaInsets;\n\n // 연락처\n contacts: MockContact[];\n\n // IAP\n iap: {\n products: MockIapProduct[];\n nextResult: IapNextResult;\n pendingOrders: Array<{ orderId: string; sku: string; paymentCompletedDate: string }>;\n completedOrders: Array<{ orderId: string; sku: string; status: 'COMPLETED' | 'REFUNDED'; date: string }>;\n };\n\n // 결제 (TossPay)\n payment: {\n nextResult: 'success' | 'fail';\n failReason: string;\n };\n\n // 로그인\n auth: {\n isLoggedIn: boolean;\n isTossLoginIntegrated: boolean;\n userKeyHash: string;\n };\n\n // 광고\n ads: {\n isLoaded: boolean;\n nextEvent: 'loaded' | 'clicked' | 'dismissed' | 'failedToShow' | 'impression' | 'userEarnedReward';\n };\n\n // 게임\n game: {\n profile: { nickname: string; profileImageUri: string } | null;\n leaderboardScores: Array<{ score: string; timestamp: number }>;\n };\n\n // 분석 로그\n analyticsLog: AnalyticsLogEntry[];\n}\n\nconst DEFAULT_STATE: AitDevtoolsState = {\n platform: 'ios',\n environment: 'sandbox',\n appVersion: '5.240.0',\n locale: 'ko-KR',\n schemeUri: '/',\n groupId: 'mock-group-id',\n deploymentId: 'mock-deployment-id',\n deviceId: '',\n\n brand: {\n displayName: 'Mock App',\n icon: '',\n primaryColor: '#3182F6',\n },\n\n networkStatus: 'WIFI',\n\n permissions: {\n clipboard: 'allowed',\n contacts: 'allowed',\n photos: 'allowed',\n geolocation: 'allowed',\n camera: 'allowed',\n microphone: 'notDetermined',\n },\n\n location: {\n coords: {\n latitude: 37.5665,\n longitude: 126.978,\n altitude: 0,\n accuracy: 10,\n altitudeAccuracy: 0,\n heading: 0,\n },\n timestamp: Date.now(),\n accessLocation: 'FINE',\n },\n\n safeAreaInsets: { top: 47, bottom: 34, left: 0, right: 0 },\n\n contacts: [\n { name: '홍길동', phoneNumber: '010-1234-5678' },\n { name: '김토스', phoneNumber: '010-9876-5432' },\n ],\n\n iap: {\n products: [\n {\n sku: 'mock-gem-100',\n type: 'CONSUMABLE',\n displayName: '보석 100개',\n displayAmount: '1,000원',\n iconUrl: '',\n description: '게임에서 사용할 수 있는 보석 100개',\n },\n ],\n nextResult: 'success',\n pendingOrders: [],\n completedOrders: [],\n },\n\n payment: {\n nextResult: 'success',\n failReason: '',\n },\n\n auth: {\n isLoggedIn: true,\n isTossLoginIntegrated: true,\n userKeyHash: 'mock-user-hash-abc123',\n },\n\n ads: {\n isLoaded: false,\n nextEvent: 'loaded',\n },\n\n game: {\n profile: { nickname: 'MockPlayer', profileImageUri: '' },\n leaderboardScores: [],\n },\n\n analyticsLog: [],\n};\n\nfunction generateDeviceId(): string {\n const stored = localStorage.getItem('__ait_device_id');\n if (stored) return stored;\n const id = crypto.randomUUID();\n localStorage.setItem('__ait_device_id', id);\n return id;\n}\n\nclass AitStateManager {\n private _state: AitDevtoolsState;\n private _listeners = new Set<Listener>();\n\n constructor() {\n this._state = structuredClone(DEFAULT_STATE);\n try {\n this._state.deviceId = generateDeviceId();\n } catch {\n this._state.deviceId = 'mock-device-' + Math.random().toString(36).slice(2);\n }\n }\n\n get state(): AitDevtoolsState {\n return this._state;\n }\n\n update(partial: Partial<AitDevtoolsState>) {\n this._state = { ...this._state, ...partial };\n this._notify();\n }\n\n /** 중첩 객체 업데이트용 */\n patch<K extends keyof AitDevtoolsState>(\n key: K,\n partial: Partial<AitDevtoolsState[K]>,\n ) {\n const current = this._state[key];\n if (typeof current === 'object' && current !== null && !Array.isArray(current)) {\n this._state = { ...this._state, [key]: { ...(current as Record<string, unknown>), ...(partial as Record<string, unknown>) } };\n } else {\n this._state = { ...this._state, [key]: partial as AitDevtoolsState[K] };\n }\n this._notify();\n }\n\n subscribe(listener: Listener): () => void {\n this._listeners.add(listener);\n return () => this._listeners.delete(listener);\n }\n\n /** 분석 로그 추가 */\n logAnalytics(entry: Omit<AnalyticsLogEntry, 'timestamp'>) {\n this._state = {\n ...this._state,\n analyticsLog: [...this._state.analyticsLog, { ...entry, timestamp: Date.now() }],\n };\n this._notify();\n }\n\n /** 이벤트 트리거 (backEvent, homeEvent 등) */\n trigger(event: string) {\n window.dispatchEvent(new CustomEvent(`__ait:${event}`));\n }\n\n reset() {\n const deviceId = this._state.deviceId;\n this._state = { ...structuredClone(DEFAULT_STATE), deviceId };\n this._notify();\n }\n\n private _notify() {\n for (const listener of this._listeners) {\n listener();\n }\n }\n}\n\nexport const aitState = new AitStateManager();\n\n// 브라우저 콘솔에서 접근 가능하도록\nif (typeof window !== 'undefined') {\n (window as unknown as Record<string, unknown>).__ait = aitState;\n}\n"],"mappings":";AAiIA,IAAM,gBAAkC;AAAA,EACtC,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,SAAS;AAAA,EACT,cAAc;AAAA,EACd,UAAU;AAAA,EAEV,OAAO;AAAA,IACL,aAAa;AAAA,IACb,MAAM;AAAA,IACN,cAAc;AAAA,EAChB;AAAA,EAEA,eAAe;AAAA,EAEf,aAAa;AAAA,IACX,WAAW;AAAA,IACX,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AAAA,EAEA,UAAU;AAAA,IACR,QAAQ;AAAA,MACN,UAAU;AAAA,MACV,WAAW;AAAA,MACX,UAAU;AAAA,MACV,UAAU;AAAA,MACV,kBAAkB;AAAA,MAClB,SAAS;AAAA,IACX;AAAA,IACA,WAAW,KAAK,IAAI;AAAA,IACpB,gBAAgB;AAAA,EAClB;AAAA,EAEA,gBAAgB,EAAE,KAAK,IAAI,QAAQ,IAAI,MAAM,GAAG,OAAO,EAAE;AAAA,EAEzD,UAAU;AAAA,IACR,EAAE,MAAM,sBAAO,aAAa,gBAAgB;AAAA,IAC5C,EAAE,MAAM,sBAAO,aAAa,gBAAgB;AAAA,EAC9C;AAAA,EAEA,KAAK;AAAA,IACH,UAAU;AAAA,MACR;AAAA,QACE,KAAK;AAAA,QACL,MAAM;AAAA,QACN,aAAa;AAAA,QACb,eAAe;AAAA,QACf,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,YAAY;AAAA,IACZ,eAAe,CAAC;AAAA,IAChB,iBAAiB,CAAC;AAAA,EACpB;AAAA,EAEA,SAAS;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA,EAEA,MAAM;AAAA,IACJ,YAAY;AAAA,IACZ,uBAAuB;AAAA,IACvB,aAAa;AAAA,EACf;AAAA,EAEA,KAAK;AAAA,IACH,UAAU;AAAA,IACV,WAAW;AAAA,EACb;AAAA,EAEA,MAAM;AAAA,IACJ,SAAS,EAAE,UAAU,cAAc,iBAAiB,GAAG;AAAA,IACvD,mBAAmB,CAAC;AAAA,EACtB;AAAA,EAEA,cAAc,CAAC;AACjB;AAEA,SAAS,mBAA2B;AAClC,QAAM,SAAS,aAAa,QAAQ,iBAAiB;AACrD,MAAI,OAAQ,QAAO;AACnB,QAAM,KAAK,OAAO,WAAW;AAC7B,eAAa,QAAQ,mBAAmB,EAAE;AAC1C,SAAO;AACT;AAEA,IAAM,kBAAN,MAAsB;AAAA,EACZ;AAAA,EACA,aAAa,oBAAI,IAAc;AAAA,EAEvC,cAAc;AACZ,SAAK,SAAS,gBAAgB,aAAa;AAC3C,QAAI;AACF,WAAK,OAAO,WAAW,iBAAiB;AAAA,IAC1C,QAAQ;AACN,WAAK,OAAO,WAAW,iBAAiB,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,IAAI,QAA0B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,OAAO,SAAoC;AACzC,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,QAAQ;AAC3C,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,MACE,KACA,SACA;AACA,UAAM,UAAU,KAAK,OAAO,GAAG;AAC/B,QAAI,OAAO,YAAY,YAAY,YAAY,QAAQ,CAAC,MAAM,QAAQ,OAAO,GAAG;AAC9E,WAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,GAAG,EAAE,GAAI,SAAqC,GAAI,QAAoC,EAAE;AAAA,IAC9H,OAAO;AACL,WAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,GAAG,QAA+B;AAAA,IACxE;AACA,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,UAAU,UAAgC;AACxC,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO,MAAM,KAAK,WAAW,OAAO,QAAQ;AAAA,EAC9C;AAAA;AAAA,EAGA,aAAa,OAA6C;AACxD,SAAK,SAAS;AAAA,MACZ,GAAG,KAAK;AAAA,MACR,cAAc,CAAC,GAAG,KAAK,OAAO,cAAc,EAAE,GAAG,OAAO,WAAW,KAAK,IAAI,EAAE,CAAC;AAAA,IACjF;AACA,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,QAAQ,OAAe;AACrB,WAAO,cAAc,IAAI,YAAY,SAAS,KAAK,EAAE,CAAC;AAAA,EACxD;AAAA,EAEA,QAAQ;AACN,UAAM,WAAW,KAAK,OAAO;AAC7B,SAAK,SAAS,EAAE,GAAG,gBAAgB,aAAa,GAAG,SAAS;AAC5D,SAAK,QAAQ;AAAA,EACf;AAAA,EAEQ,UAAU;AAChB,eAAW,YAAY,KAAK,YAAY;AACtC,eAAS;AAAA,IACX;AAAA,EACF;AACF;AAEO,IAAM,WAAW,IAAI,gBAAgB;AAG5C,IAAI,OAAO,WAAW,aAAa;AACjC,EAAC,OAA8C,QAAQ;AACzD;","names":[]}