From 850f098fa2a9a24e08bde744ad6dccb2dc76ac01 Mon Sep 17 00:00:00 2001 From: manNomi Date: Wed, 3 Jun 2026 15:07:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/README.md | 1 + docs/codex/TEST_MANAGER.md | 72 +++ package-lock.json | 552 +++++++++-------- package.json | 14 +- src/components/dev/DevTestManager.tsx | 562 ++++++++++++++++++ .../onboarding/OnboardingScreen.tsx | 4 +- .../recap-share/RecapCaptureFrame.tsx | 4 +- src/mock-server/delay.ts | 16 + src/providers/AppProviders.tsx | 11 +- src/store/devToolsStore.ts | 44 ++ src/store/playerStore.ts | 8 + 12 files changed, 1014 insertions(+), 275 deletions(-) create mode 100644 docs/codex/TEST_MANAGER.md create mode 100644 src/components/dev/DevTestManager.tsx create mode 100644 src/store/devToolsStore.ts diff --git a/README.md b/README.md index bd0117a..e8cd5fe 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ EXPO_PUBLIC_MOCK_API_FAIL_ENDPOINTS=playlist.detail npm run web - [비개발자용 Codex 개발 가이드](docs/codex/NON_DEVELOPER_CODEX_GUIDE.md) - [Codex 요청 프롬프트 모음](docs/codex/CODEX_PROMPTS.md) - [UI 피드백 루프 운영 문서](docs/codex/UI_FEEDBACK_LOOP.md) +- [개발용 테스트 매니저](docs/codex/TEST_MANAGER.md) - [PR 전용 개발 흐름](docs/codex/PR_ONLY_WORKFLOW.md) - [Mock Server 안내](src/mock-server/README.md) diff --git a/docs/README.md b/docs/README.md index 6088f0a..bfed5ad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ Soundlog 문서는 목적별로 관리합니다. 공모전 기획, RN 프론트 - [비개발자용 Codex 개발 가이드](codex/NON_DEVELOPER_CODEX_GUIDE.md): Codex에게 개발을 맡기는 방식 - [Codex 요청 프롬프트 모음](codex/CODEX_PROMPTS.md): 바로 복사해서 쓸 수 있는 요청문 - [UI 피드백 루프 운영 문서](codex/UI_FEEDBACK_LOOP.md): 자연어 UI 수정 요청을 계획-리뷰-구현-리뷰 루프로 처리하는 방식 +- [개발용 테스트 매니저](codex/TEST_MANAGER.md): 페이지 이동, mock API, seed 데이터 검수를 위한 개발 도구 - [PR 전용 개발 흐름](codex/PR_ONLY_WORKFLOW.md): `main` 직접 push를 막고 PR로만 병합하는 방식 ## Mock API diff --git a/docs/codex/TEST_MANAGER.md b/docs/codex/TEST_MANAGER.md new file mode 100644 index 0000000..80859d8 --- /dev/null +++ b/docs/codex/TEST_MANAGER.md @@ -0,0 +1,72 @@ +# Soundlog 테스트 매니저 구현 계획 + +## 목적 + +개발 모드에서 비개발자와 팀원이 앱의 주요 페이지, 조건문, 로딩/실패 상태, 로컬 데이터 상태를 직접 조작하며 검수할 수 있게 한다. + +## 검수 결과 + +- 온보딩 완료 후 홈 필터 기본값이 뒤집혀 있다. + - 현재: `preferredMoods[0]` -> 상단 필터, `travelStyles[0]` -> 무드 필터 + - 기대: 상단 필터는 추천 범위이므로 `전체`, 무드 필터는 `preferredMoods[0]` +- Mock API 실패/지연은 환경변수로만 설정되어 앱 실행 중 상태 전환 테스트가 어렵다. +- Moment Log, Recap, Library, Player, Session 상태를 한 화면에서 빠르게 seed/clear할 방법이 없다. +- 화면 이동 테스트가 탭/카드 탐색에 의존해 QA 속도가 느리다. + +## 구현 범위 + +1. `DevTestManager` 플로팅 버튼 + - `__DEV__`에서만 렌더링한다. + - 루트 provider 하위에 붙여 모든 화면에서 접근 가능하게 한다. + - 좌측 하단 기본 위치를 사용하고 드래그 이동을 지원한다. + - 탭바와 미니 플레이어를 완전히 대체하지 않도록 작은 원형 버튼으로 시작한다. + +2. 테스트 매니저 패널 + - 페이지 이동: 홈, 온보딩, 플레이리스트 상세, Recap 리스트, Recap 공유, 보관함, 마이, 카메라 + - 추천 조건: 홈 상단 필터, 무드 필터, 여행 모드 + - 위치/세션: 서울/부산 위치 seed, 위치 거부/불가 상태, 여행 시작/종료/리셋 + - 데이터 seed: 샘플 Moment Log, 보관함 좋아요/저장, 현재 재생곡 + - 데이터 clear: Moment Log, 보관함, 추천 이벤트, 플레이어 + - Mock API: 정상, 느림, 전체 실패, endpoint별 실패 + +3. Runtime Mock API 상태 + - `src/store/devToolsStore.ts`에 mock delay/fail 상태를 저장한다. + - `src/mock-server/delay.ts`가 환경변수보다 runtime 설정을 우선 참고한다. + - Query cache를 invalidate해서 변경 상태가 즉시 반영되게 한다. + - 실패/지연 설정은 개발 세션용 상태로 두고 영속 저장하지 않는다. + +4. 미완성/기획 불일치 보정 + - 온보딩 완료 시 상단 필터는 `전체`, 무드 필터는 첫 선호 무드로 설정한다. + - 홈 필터와 무드 필터 개념을 계속 분리한다. + +## 주요 파일 + +- `src/components/dev/DevTestManager.tsx` +- `src/store/devToolsStore.ts` +- `src/mock-server/delay.ts` +- `src/providers/AppProviders.tsx` +- `src/store/playerStore.ts` +- `src/store/libraryStore.ts` +- `src/store/momentLogStore.ts` +- `src/components/onboarding/OnboardingScreen.tsx` +- `docs/codex/TEST_MANAGER.md` + +## 검증 + +- TypeScript: `tsc --noEmit` +- diff whitespace: `git diff --check` +- Runtime hook: mock API 정상/느림/실패 전환 +- UI: 웹에서 플로팅 버튼이 이동 가능하고 패널이 열린다. + +## 위험과 대응 + +- 테스트 매니저가 실제 배포 UI에 노출될 위험: `__DEV__` guard로 제한한다. +- 패널이 화면을 가릴 위험: Modal 패널로 열고 닫기 버튼을 제공한다. +- Mock 실패 상태가 남아 검수를 방해할 위험: `정상` 버튼으로 모든 runtime mock 설정을 초기화한다. +- Store 경계를 깨뜨릴 위험: seed/clear는 store action을 통해 수행하고 컴포넌트에서 배열을 직접 변형하지 않는다. +- Query 결과가 이전 상태로 남을 위험: mock 상태 변경 후 React Query cache를 invalidate한다. + +## 계획 리뷰 반영 + +- Claude CLI 호출은 응답 없이 장시간 대기해 중단했고, `soundlog-ui-reviewer` 체크리스트로 자체 리뷰를 수행했다. +- 리뷰 결과 runtime mock 변경 후 cache invalidation, `__DEV__` guard, store action 경계, 온보딩 필터 불일치 수정이 필요하다고 판단했다. diff --git a/package-lock.json b/package-lock.json index 0fb2694..412d5d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,19 +11,19 @@ "@expo/vector-icons": "^15.1.1", "@react-native-async-storage/async-storage": "2.2.0", "@tanstack/react-query": "^5.100.14", - "expo": "~56.0.4", + "expo": "~56.0.8", "expo-camera": "~56.0.7", "expo-constants": "~56.0.15", - "expo-dev-client": "~56.0.15", + "expo-dev-client": "~56.0.18", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", "expo-image": "~56.0.9", "expo-linear-gradient": "~56.0.4", - "expo-linking": "~56.0.11", - "expo-location": "~56.0.13", + "expo-linking": "~56.0.13", + "expo-location": "~56.0.15", "expo-media-library": "~56.0.6", - "expo-router": "^56.2.6", - "expo-sharing": "~56.0.13", + "expo-router": "~56.2.8", + "expo-sharing": "~56.0.15", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.4", "react": "19.2.3", @@ -32,7 +32,7 @@ "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", - "react-native-view-shot": "4.0.3", + "react-native-view-shot": "5.1.0", "react-native-web": "^0.21.2", "react-native-worklets": "0.8.3", "zustand": "^5.0.13" @@ -1347,9 +1347,9 @@ "license": "MIT" }, "node_modules/@expo/fingerprint": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.19.2.tgz", - "integrity": "sha512-+/cBrRHiHmldvT8ZPrrHobAOMTUTzOq6Qpr1YLSoIg0J9hbEkJOg9vUvpxiLNWSQY0eKtVTvMO03EIdPC2aQdQ==", + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.19.3.tgz", + "integrity": "sha512-w9Au2IVrtc0Ct+WRa05DVHGNHXYq6VyPZWuFP+5x055OeZ5q6k5Yg+aJ1gfShmjdOhDftRcsvmWmTdTZlWaTZw==", "license": "MIT", "dependencies": { "@expo/env": "^2.3.0", @@ -1384,12 +1384,12 @@ } }, "node_modules/@expo/inline-modules": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.0.9.tgz", - "integrity": "sha512-otMUXI4mOjytbe9OQ3i5X4SV0LP1GpzqLdr9+rdxUc1b0FjdvbTM/GkcbrwY4pU0fGSK0qFqX+jgSieyi+XbUA==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.0.10.tgz", + "integrity": "sha512-DKEfq877UTAmL/gOT5aFhlLNDg/EVmTSca7JQMKDGR6mjFSAcrmQf4GJNsi6ztiaqj6cTnIfoSz0hAYdnr6RJQ==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~56.0.7" + "@expo/config-plugins": "~56.0.8" } }, "node_modules/@expo/json-file": { @@ -1403,12 +1403,12 @@ } }, "node_modules/@expo/local-build-cache-provider": { - "version": "56.0.7", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-56.0.7.tgz", - "integrity": "sha512-GedmHPUQeLKbRZNzxZ4ZiN7NKQw65MSOMMnIqJnbXySZYYeBWg5TMuCzafE0Pt0Tsd2vmp2F7OPpsgAFGFoaBw==", + "version": "56.0.8", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-56.0.8.tgz", + "integrity": "sha512-UsuXwpNi57MNhzZ3be4XThc8xW6nzk3Wu37s1+2qcfZGeJcMLKDFfwO6n8YXeIiGlCsOi0Ee1rsTdgjrKt/YJQ==", "license": "MIT", "dependencies": { - "@expo/config": "~56.0.8", + "@expo/config": "~56.0.9", "chalk": "^4.1.2" } }, @@ -1451,6 +1451,73 @@ "metro-transform-worker": "0.84.4" } }, + "node_modules/@expo/metro-config": { + "version": "56.0.13", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-56.0.13.tgz", + "integrity": "sha512-OPyNYiex/6Ms8zT2POdIZsLhcAZYk7O+yJvpz5uG/4QRA7aiESfCy1I+0YHewMlR4P1YQeyxIrfTurs6m9xfZA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~56.0.9", + "@expo/env": "~2.3.0", + "@expo/json-file": "~10.2.0", + "@expo/metro": "~56.0.0", + "@expo/require-utils": "^56.1.3", + "@expo/spawn-async": "^1.8.0", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.33.3", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "msgpackr": "^2.0.1", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/hermes-estree": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", + "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", + "license": "MIT" + }, + "node_modules/@expo/metro-config/node_modules/hermes-parser": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", + "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.33.3" + } + }, + "node_modules/@expo/metro-config/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@expo/metro-file-map": { "version": "56.0.3", "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-56.0.3.tgz", @@ -1465,6 +1532,31 @@ "walker": "^1.0.8" } }, + "node_modules/@expo/metro-runtime": { + "version": "56.0.13", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.13.tgz", + "integrity": "sha512-aMaFa/RPYm2iQoyYOB5q8AxDmWvf4E2yFbZ6rmBIQWaIPDdixGVUlLQeV8DlDAfZ/j+pNYO7l5M+774WbgkTgg==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "^56.0.12", + "anser": "^1.4.9", + "pretty-format": "^29.7.0", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "@expo/log-box": "^56.0.12", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/@expo/osascript": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.6.0.tgz", @@ -1478,9 +1570,9 @@ } }, "node_modules/@expo/package-manager": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.12.0.tgz", - "integrity": "sha512-SWr6093nwBjn94cvElsYZNUnhvs+XtUatUz3h0vAn0IbaWG0B6l/V5ZfOBptX/xq6rMpFG5ibIf/eckLSXw8Gg==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.12.1.tgz", + "integrity": "sha512-fQLiFAcFRWF53mtuLK32SUJQ1ahhrTcBZPZPedYTiUT5ha5FF+UO6bPtCc0Y/hgj0/m3HCGBAuSHjbg2kI9oPQ==", "license": "MIT", "dependencies": { "@expo/json-file": "^10.2.0", @@ -1503,19 +1595,19 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.12.tgz", - "integrity": "sha512-cMI1EwpVhVaZQ92VtkRGpyvBV/iC06NMBwi+p69mwvoQTJKqswgCwYK7txFH5KaavKMmYMUaZ1twiC7jd/jDRQ==", + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.14.tgz", + "integrity": "sha512-JHdMqR7Mf5ApLC50ZwTL0Z86ezrHOMYwoSHcWT6Pha/+1TcC+/J+i7vjhP06wGXQ2Kvjt74p/3mKg2Pd12KjhQ==", "license": "MIT", "dependencies": { - "@expo/config": "~56.0.8", - "@expo/config-plugins": "~56.0.7", + "@expo/config": "~56.0.9", + "@expo/config-plugins": "~56.0.8", "@expo/config-types": "^56.0.5", - "@expo/image-utils": "^0.10.0", + "@expo/image-utils": "^0.10.1", "@expo/json-file": "^10.2.0", "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.1", - "expo-modules-autolinking": "~56.0.11", + "expo-modules-autolinking": "~56.0.14", "resolve-from": "^5.0.0", "semver": "^7.6.0" } @@ -1569,6 +1661,39 @@ "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, + "node_modules/@expo/ui": { + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.15.tgz", + "integrity": "sha512-PFZBzztQGCp2bRFP8wIOb5ntP2ORH2GdQkJMSJcDOd4NldoWMe1pFqv7PdthjNlaaTHHTTHK+RsQrz+M6z6isw==", + "license": "MIT", + "dependencies": { + "sf-symbols-typescript": "^2.1.0", + "vaul": "^1.1.2" + }, + "peerDependencies": { + "@babel/core": "*", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-reanimated": "*", + "react-native-worklets": "*" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + }, + "react-native-worklets": { + "optional": true + } + } + }, "node_modules/@expo/vector-icons": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", @@ -1694,9 +1819,9 @@ } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", "cpu": [ "arm64" ], @@ -1707,9 +1832,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", "cpu": [ "x64" ], @@ -1720,9 +1845,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", "cpu": [ "arm" ], @@ -1733,9 +1858,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", "cpu": [ "arm64" ], @@ -1746,9 +1871,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", "cpu": [ "x64" ], @@ -1759,9 +1884,9 @@ ] }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", "cpu": [ "x64" ], @@ -3168,9 +3293,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-56.0.12.tgz", - "integrity": "sha512-8sOIpdzMXgx81CcCF4wwAQC8xo9akFgy32pciVjHo/4tphLOXez7wfqv5p9StgyMLQPEF4qhXG2Rkbz1QAgu2A==", + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-56.0.14.tgz", + "integrity": "sha512-+JKVMYf3HajO3tPRA9DlKd/VhZOPTHyTzUo2yZajfMAoQ3l5VEdGVxm2MzX4DXMNKXwsC8GOeTRx7CrO/5dBDA==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -3219,7 +3344,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^56.0.14", + "expo-widgets": "^56.0.16", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -4049,31 +4174,31 @@ } }, "node_modules/expo": { - "version": "56.0.4", - "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.4.tgz", - "integrity": "sha512-ZwoOkOTwITJrFQRRO5tUsBp6NlddvjWSs3ADb+zOu1UIQWBCk9dmwmSrdFxu0P+hYnU2hk5k/Y6xq6DPLNSKzg==", + "version": "56.0.8", + "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.8.tgz", + "integrity": "sha512-GzQi5450yrCk5JRSlm0epsmtURBErh0wS77uWLZImFdnPICuX912MaRWooR+Q1Sw/7aQjp9F+KXH+dvrqGxpeQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^56.1.11", + "@expo/cli": "^56.1.13", "@expo/config": "~56.0.9", "@expo/config-plugins": "~56.0.8", "@expo/devtools": "~56.0.2", "@expo/dom-webview": "~56.0.5", - "@expo/fingerprint": "^0.19.2", - "@expo/local-build-cache-provider": "^56.0.7", + "@expo/fingerprint": "^0.19.3", + "@expo/local-build-cache-provider": "^56.0.8", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", - "@expo/metro-config": "~56.0.12", + "@expo/metro-config": "~56.0.13", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~56.0.12", - "expo-asset": "~56.0.14", - "expo-constants": "~56.0.15", + "babel-preset-expo": "~56.0.14", + "expo-asset": "~56.0.15", + "expo-constants": "~56.0.16", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", "expo-keep-awake": "~56.0.3", - "expo-modules-autolinking": "~56.0.12", - "expo-modules-core": "~56.0.12", + "expo-modules-autolinking": "~56.0.14", + "expo-modules-core": "~56.0.14", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -4110,6 +4235,21 @@ } } }, + "node_modules/expo-asset": { + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.15.tgz", + "integrity": "sha512-BHGi2IAOPQTcOelkUdcz1WIknfCTRjkcpYHX1azjMwgYenrVC+J5qcqJGaC8eUOWLCRtkRJWGnmFQRYtLU1nUQ==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.10.1", + "expo-constants": "~56.0.16" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-camera": { "version": "56.0.7", "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-56.0.7.tgz", @@ -4131,9 +4271,9 @@ } }, "node_modules/expo-constants": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.15.tgz", - "integrity": "sha512-7187sd55swLX+CM0noAV5LreEgkUaDG/zEXy9quonfzKpJxy8zJAszp9S++xOx/FcqBVEEEcQhE8tKTujwL8fg==", + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.16.tgz", + "integrity": "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA==", "license": "MIT", "dependencies": { "@expo/env": "~2.3.0" @@ -4144,13 +4284,13 @@ } }, "node_modules/expo-dev-client": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-56.0.15.tgz", - "integrity": "sha512-YJBO0xMv0CRhVhZu4NPWoR0zS/nyhbjpBiEhEd4SOD/mcmW1I1ncURLn8Ej63yJjuCGL6pFQLkCskAak1OJyuA==", + "version": "56.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-56.0.18.tgz", + "integrity": "sha512-pTfDcYTOvrs4vCgAaM+vP2OEO93oGkczgGpTAzCY7ZTIvtPhpekJURHBxfOnKvfn97IF3Hk+8J9tMozsNDj0Gw==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~56.0.15", - "expo-dev-menu": "~56.0.14", + "expo-dev-launcher": "~56.0.18", + "expo-dev-menu": "~56.0.16", "expo-dev-menu-interface": "~56.0.0", "expo-manifests": "~56.0.4", "expo-updates-interface": "~56.0.1" @@ -4160,13 +4300,13 @@ } }, "node_modules/expo-dev-launcher": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-56.0.15.tgz", - "integrity": "sha512-KVG8haacJiYHu7wLJiDYQbKM0CqFBqf0BJ9YvWWBhxOZjNOtwspVIsKS4idiIOeQsFCRb2Axt8svQ+opvuvE6A==", + "version": "56.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-56.0.18.tgz", + "integrity": "sha512-7acFJlkAbp3cMz7Uy787todMR/3A/Row2EOPD21RRoetvzJe4DTm9s7RwJ8PDtyNyued9rooD4+Q6rD8ijpTgw==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^56.0.0", - "expo-dev-menu": "~56.0.14", + "expo-dev-menu": "~56.0.16", "expo-manifests": "~56.0.4" }, "peerDependencies": { @@ -4175,9 +4315,9 @@ } }, "node_modules/expo-dev-menu": { - "version": "56.0.14", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-56.0.14.tgz", - "integrity": "sha512-4dx14nedjWSCdpPKj74IGIfuM5nd2ePMpD3vNraq+srsZzWfNMh9gLFwcXtfQIpgXkHavO5178bJ3VCJVsnNsg==", + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-56.0.16.tgz", + "integrity": "sha512-aVgoe+YGhrQnpwiB5BRI7G+uQnGHMUij32bBnEVdc6eJrVZCStxQlV9NeFbbXxrDhLJt6OSqbCHbLR+XToWUUA==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "~56.0.0" @@ -4269,12 +4409,12 @@ } }, "node_modules/expo-linking": { - "version": "56.0.11", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.11.tgz", - "integrity": "sha512-MEPgML2mqm2Y8rP6zTleOpCmYiFyfQfNSOBpDIb7CYpbDQleStugvceKsEsL4v8C0Dl5u7e8KkkrbqmgpOOIBw==", + "version": "56.0.13", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.13.tgz", + "integrity": "sha512-38YrpTh6xdiDxmYSDIUffDqev1hIcEggw2fZ3IZhNp2DVLF1xvqsbO6hJD1fuBKN8P34B3Ggc9Yy26fkqdfCOA==", "license": "MIT", "dependencies": { - "expo-constants": "~56.0.14", + "expo-constants": "~56.0.16", "invariant": "^2.2.4" }, "peerDependencies": { @@ -4283,9 +4423,9 @@ } }, "node_modules/expo-location": { - "version": "56.0.13", - "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.13.tgz", - "integrity": "sha512-MUHG1IXoQIhi4oadyOG9UtrbVLPeaicOT0ARrQ+X5Tt7+6KAHVbg4TIwLiv4oqpdNh+R0XiHdNXOe4oPQrs8eg==", + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.15.tgz", + "integrity": "sha512-CM5+1untDxsuN0NIgsBS9cRel5xh8UXstQS6KtQw/run5PiArqCl51cnTuG+aqjYgE+9gweSG70PI6A1Ax1XTA==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.10.1" @@ -4317,9 +4457,9 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.12.tgz", - "integrity": "sha512-Sn/LiLSL4as/YOGoatsuRQYS7rxAQHK2oYbFMT/I3iIXHLSeb0DQeuAIytVQ9ypWWck9s2krH2T6NeztyftnaA==", + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.14.tgz", + "integrity": "sha512-9ugtZkheNPYDkW4DZopY1rH2BCbUICaafUEPxRgbLDR5UNRF5K3cdHMIMEt8pxZPq2+eX4wCm+6pbSvdY/DPHg==", "license": "MIT", "dependencies": { "@expo/require-utils": "^56.1.3", @@ -4332,9 +4472,9 @@ } }, "node_modules/expo-modules-core": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.12.tgz", - "integrity": "sha512-2Rf+FBU2EXe27km3m066xHu4kuUSpNT35nzk98fFxIV8B2Ah+FHub2rvAznEcGAUlDArVA2S/6+pMlHWijbicQ==", + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.14.tgz", + "integrity": "sha512-dl1TlYRm1k7xk9QeAyDoMfFE2p6rNyzHUcH5ArcGwUzO8YKku+Z2tQ8+kG7zLe3OhfMoJcFR/czrFy7vGSVI6w==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "~0.0.9", @@ -4362,15 +4502,15 @@ } }, "node_modules/expo-router": { - "version": "56.2.6", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-56.2.6.tgz", - "integrity": "sha512-KouVa/E2zQc1aALWSd5eZjbsLKldgozQ546p+bgAR2nGPRTO4WpMRKNIxjB89We1G4RwWpxQ5vgTU1WZ+FpqOg==", + "version": "56.2.8", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-56.2.8.tgz", + "integrity": "sha512-l387I/ddPY/5SS+Rfpp1SrRV9gBKevxtPuZod7igMjR6L674QrxEwGiAILRq6AKCSbrP2I0ufKj7e5xz8JqA4Q==", "license": "MIT", "dependencies": { "@expo/log-box": "^56.0.12", - "@expo/metro-runtime": "^56.0.12", + "@expo/metro-runtime": "^56.0.13", "@expo/schema-utils": "^56.0.0", - "@expo/ui": "^56.0.13", + "@expo/ui": "^56.0.15", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -4398,11 +4538,11 @@ }, "peerDependencies": { "@expo/log-box": "^56.0.12", - "@expo/metro-runtime": "^56.0.12", + "@expo/metro-runtime": "^56.0.13", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^56.0.15", - "expo-linking": "^56.0.11", + "expo-constants": "^56.0.16", + "expo-linking": "^56.0.13", "react": "*", "react-dom": "*", "react-native": "*", @@ -4434,64 +4574,6 @@ } } }, - "node_modules/expo-router/node_modules/@expo/metro-runtime": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.12.tgz", - "integrity": "sha512-7fWsZfIq+Kn6ilr5lx1YNQGJjukmvwnrl91cTRASdQIKXQoXF7AXRAU0CrDjA+dNMZ6UWDK3l8wpQjk7CA1Z/A==", - "license": "MIT", - "dependencies": { - "@expo/log-box": "^56.0.12", - "anser": "^1.4.9", - "pretty-format": "^29.7.0", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0" - }, - "peerDependencies": { - "@expo/log-box": "^56.0.12", - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@expo/ui": { - "version": "56.0.13", - "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.13.tgz", - "integrity": "sha512-Dx05pO3lo8lzWp0hgvJ011j/a5DD2BwHXtr08hdiRUc03KrWQJ3QzdbqPqNayrr+Usc2COC+bOkmPNX7N0k0+w==", - "license": "MIT", - "dependencies": { - "sf-symbols-typescript": "^2.1.0", - "vaul": "^1.1.2" - }, - "peerDependencies": { - "@babel/core": "*", - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*", - "react-native-reanimated": "*", - "react-native-worklets": "*" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native-reanimated": { - "optional": true - }, - "react-native-worklets": { - "optional": true - } - } - }, "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", @@ -4538,9 +4620,9 @@ } }, "node_modules/expo-sharing": { - "version": "56.0.13", - "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-56.0.13.tgz", - "integrity": "sha512-Yz6mBSqbU5dM6UzCjkKr1+B0JKADRuGj4Dokgs2fLysRTyjvO4mbmSTyY7AGQx3VYz0IUMoyPg4zqvsWPEFeDg==", + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-56.0.15.tgz", + "integrity": "sha512-6Hy1+Mjy4UYXkFiDK3Ea934NUmA71i8dmZkDe+rrUHRzZAv4FR+q/VyiT7LzNFEqpT4wn4wcI66lc2QY526RsA==", "license": "MIT", "dependencies": { "@expo/config-plugins": "^56.0.8", @@ -4590,9 +4672,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "56.1.11", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.11.tgz", - "integrity": "sha512-agoVjJ+cygAAjWSjk278a+UVcHDVZMKkBROkzWxpSzMK0tmRuQvtRZvAgFN/wvGZchxzs+zGur7j3txojVMVZw==", + "version": "56.1.13", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.13.tgz", + "integrity": "sha512-7n5VzlBr7TKW0BgWgpEopWy+v8buPhMvbSEsuXD+bI1YIJBopkfWAub0qTvlc357E8wWOvV5MJXYyoeRvoOjoQ==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -4601,18 +4683,18 @@ "@expo/devcert": "^1.2.1", "@expo/env": "~2.3.0", "@expo/image-utils": "^0.10.1", - "@expo/inline-modules": "^0.0.9", + "@expo/inline-modules": "^0.0.10", "@expo/json-file": "^10.2.0", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", - "@expo/metro-config": "~56.0.12", + "@expo/metro-config": "~56.0.13", "@expo/metro-file-map": "^56.0.3", "@expo/osascript": "^2.6.0", - "@expo/package-manager": "^1.12.0", + "@expo/package-manager": "^1.12.1", "@expo/plist": "^0.7.0", - "@expo/prebuild-config": "^56.0.12", + "@expo/prebuild-config": "^56.0.14", "@expo/require-utils": "^56.1.3", - "@expo/router-server": "^56.0.11", + "@expo/router-server": "^56.0.12", "@expo/schema-utils": "^56.0.0", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^1.0.1", @@ -4671,17 +4753,17 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "56.0.11", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.11.tgz", - "integrity": "sha512-cyROEK3gibypiyz2QR7zm1+LMHUQEj7KQopwZ/Fip75MYrQ/SYOMRFSTvchZXEipwMRjwYecE4jsnqNKyYWFZg==", + "version": "56.0.12", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.12.tgz", + "integrity": "sha512-RqKV2/Z8BH/z8l0ngSpG6//5xxJPaF5dTQvSfPQ0nrvCjikGMeIvyj3B9BeLnmZZhxb3gBtXqrj3irAoiIp2aQ==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^56.0.11", + "@expo/metro-runtime": "^56.0.13", "expo": "*", - "expo-constants": "^56.0.14", + "expo-constants": "^56.0.16", "expo-font": "^56.0.5", "expo-router": "*", "expo-server": "^56.0.4", @@ -4704,46 +4786,6 @@ } } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-56.0.12.tgz", - "integrity": "sha512-L9q423WwY6eUu4A3N8OaBDECuoUNukUQKomb0/LinwzG+QkU8cBvpELXwEngP7eTt1s6LB3tXcnPp/aMvLsojw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~56.0.9", - "@expo/env": "~2.3.0", - "@expo/json-file": "~10.2.0", - "@expo/metro": "~56.0.0", - "@expo/require-utils": "^56.1.3", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.33.3", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "msgpackr": "^2.0.1", - "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, "node_modules/expo/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4772,21 +4814,6 @@ "node": ">=8" } }, - "node_modules/expo/node_modules/expo-asset": { - "version": "56.0.14", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.14.tgz", - "integrity": "sha512-3rN/VGt4jhNOXbAz3gdSJzC/dSmX5ozHgtG/HQTCOzuRGBd1q2lzWoZewX8aU1fiWchQg9LasiLiAJi+u8oBbQ==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.10.1", - "expo-constants": "~56.0.15" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-keep-awake": { "version": "56.0.3", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-56.0.3.tgz", @@ -4797,21 +4824,6 @@ "react": "*" } }, - "node_modules/expo/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT" - }, - "node_modules/expo/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.33.3" - } - }, "node_modules/expo/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5599,9 +5611,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6526,18 +6548,18 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.1.tgz", - "integrity": "sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.2.tgz", + "integrity": "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ==", "license": "MIT", "optionalDependencies": { - "msgpackr-extract": "^3.0.2" + "msgpackr-extract": "^3.0.4" } }, "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -6548,12 +6570,12 @@ "download-msgpackr-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" } }, "node_modules/multitars": { @@ -7923,16 +7945,20 @@ } }, "node_modules/react-native-view-shot": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz", - "integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-5.1.0.tgz", + "integrity": "sha512-JZgElCD82aO+hejIF/leUzI7JufL9mgJ6ChzGWIcdZ2ajpaEvvSnvIcw0qD32XWkrbId8wfSbyz/4u/ulTQzQA==", "license": "MIT", "dependencies": { "html2canvas": "^1.4.1" }, + "engines": { + "node": ">=20", + "npm": ">=10" + }, "peerDependencies": { - "react": "*", - "react-native": "*" + "react": ">=18.0.0", + "react-native": ">=0.76.0" } }, "node_modules/react-native-web": { diff --git a/package.json b/package.json index 152bbdb..a9e9d0e 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,19 @@ "@expo/vector-icons": "^15.1.1", "@react-native-async-storage/async-storage": "2.2.0", "@tanstack/react-query": "^5.100.14", - "expo": "~56.0.4", + "expo": "~56.0.8", "expo-camera": "~56.0.7", "expo-constants": "~56.0.15", - "expo-dev-client": "~56.0.15", + "expo-dev-client": "~56.0.18", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", "expo-image": "~56.0.9", "expo-linear-gradient": "~56.0.4", - "expo-linking": "~56.0.11", - "expo-location": "~56.0.13", + "expo-linking": "~56.0.13", + "expo-location": "~56.0.15", "expo-media-library": "~56.0.6", - "expo-router": "^56.2.6", - "expo-sharing": "~56.0.13", + "expo-router": "~56.2.8", + "expo-sharing": "~56.0.15", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.4", "react": "19.2.3", @@ -27,7 +27,7 @@ "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", - "react-native-view-shot": "4.0.3", + "react-native-view-shot": "5.1.0", "react-native-web": "^0.21.2", "react-native-worklets": "0.8.3", "zustand": "^5.0.13" diff --git a/src/components/dev/DevTestManager.tsx b/src/components/dev/DevTestManager.tsx new file mode 100644 index 0000000..cebda75 --- /dev/null +++ b/src/components/dev/DevTestManager.tsx @@ -0,0 +1,562 @@ +import { Feather } from '@expo/vector-icons'; +import { router } from 'expo-router'; +import { type ReactNode, useMemo, useRef, useState } from 'react'; +import { + Animated, + Modal, + PanResponder, + Pressable, + ScrollView, + useWindowDimensions, + View, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { queryClient } from '@/providers/queryClient'; +import { AppText } from '@/components/AppText'; +import { playlistCurationById } from '@/mocks/playlistMocks'; +import { mockEndpointIds, useDevToolsStore } from '@/store/devToolsStore'; +import { useHomeFilterStore } from '@/store/homeFilterStore'; +import { useLibraryStore } from '@/store/libraryStore'; +import { useMomentLogStore } from '@/store/momentLogStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { useRecommendationEventStore } from '@/store/recommendationEventStore'; +import { useTravelSessionStore } from '@/store/travelSessionStore'; +import { useUserProfileStore } from '@/store/userProfileStore'; +import { GeoPoint, MomentLog, PlaceContext, Track, TravelMode } from '@/types/domain'; + +const BUTTON_SIZE = 58; +const DEFAULT_DELAY_MS = 300; +const SLOW_DELAY_MS = 1600; +const samplePlaylist = playlistCurationById['busan-ocean']; +const sampleTracks = samplePlaylist.tracks; + +const placePresets: Array<{ + label: string; + location: GeoPoint; + place: PlaceContext; +}> = [ + { + label: '부산 광안리', + location: { lat: 35.1532, lng: 129.1186 }, + place: { + category: '해변', + contentType: '관광지', + id: 'dev-busan-gwangalli', + imageUrl: 'https://tong.visitkorea.or.kr/cms2/website/76/2012176.jpg', + location: { lat: 35.1532, lng: 129.1186 }, + overview: '바다와 야경, 산책 맥락을 테스트하는 개발용 장소입니다.', + source: 'mock', + title: '광안리 해수욕장', + }, + }, + { + label: '서울 야경', + location: { lat: 37.5512, lng: 126.9882 }, + place: { + category: '야경', + contentType: '문화시설', + id: 'dev-seoul-night', + imageUrl: 'https://tong.visitkorea.or.kr/cms/resource_photo/96/4033396_image2_1.jpg', + location: { lat: 37.5512, lng: 126.9882 }, + overview: '도시 야경과 감성 음악 추천을 테스트하는 개발용 장소입니다.', + source: 'mock', + title: '남산서울타워', + }, + }, +]; + +const topFilterOptions = ['전체', '근처', '지역 트렌드', '내 취향', '저장 많은']; +const moodFilterOptions = ['전체', '잔잔한', '신나는', '감성적인', '청량한', '활기찬', '로컬한']; +const travelModeOptions: Array<{ label: string; value: TravelMode }> = [ + { label: '산책', value: 'walk' }, + { label: '드라이브', value: 'drive' }, + { label: '카페', value: 'cafe' }, + { label: '바다', value: 'ocean' }, + { label: '축제', value: 'festival' }, + { label: '야경', value: 'night' }, +]; + +type ManagerButtonProps = { + active?: boolean; + destructive?: boolean; + label: string; + onPress: () => void; +}; + +type ManagerSectionProps = { + children: ReactNode; + subtitle?: string; + title: string; +}; + +type StatusPillProps = { + label: string; +}; + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +function invalidateMockQueries() { + void queryClient.invalidateQueries(); +} + +function ManagerSection({ children, subtitle, title }: ManagerSectionProps) { + return ( + + {title} + {subtitle ? ( + {subtitle} + ) : null} + {children} + + ); +} + +function ManagerButton({ + active = false, + destructive = false, + label, + onPress, +}: ManagerButtonProps) { + const backgroundColor = active + ? '#ffffff' + : destructive + ? 'rgba(248,113,113,0.16)' + : 'rgba(255,255,255,0.1)'; + const borderColor = active + ? '#ffffff' + : destructive + ? 'rgba(248,113,113,0.28)' + : 'rgba(255,255,255,0.12)'; + const color = active ? '#050916' : destructive ? '#fecaca' : 'rgba(255,255,255,0.78)'; + + return ( + + + {label} + + + ); +} + +function StatusPill({ label }: StatusPillProps) { + return ( + + {label} + + ); +} + +function getSampleTrack(index = 0): Track { + return sampleTracks[index % sampleTracks.length]; +} + +export function DevTestManager() { + if (!__DEV__) { + return null; + } + + return ; +} + +function DevTestManagerContent() { + const insets = useSafeAreaInsets(); + const { height, width } = useWindowDimensions(); + const [isOpen, setIsOpen] = useState(false); + const maxX = Math.max(width - BUTTON_SIZE - 12, 12); + const maxY = Math.max(height - BUTTON_SIZE - Math.max(insets.bottom, 12) - 12, 80); + const defaultPosition = useMemo( + () => ({ + x: 14, + y: Math.max(height - BUTTON_SIZE - Math.max(insets.bottom, 12) - 110, 90), + }), + [height, insets.bottom], + ); + const pan = useRef(new Animated.ValueXY(defaultPosition)).current; + const lastPosition = useRef(defaultPosition); + const panResponder = useMemo( + () => + PanResponder.create({ + onMoveShouldSetPanResponder: (_, gesture) => + Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4, + onPanResponderMove: (_, gesture) => { + pan.setValue({ + x: clamp(lastPosition.current.x + gesture.dx, 8, maxX), + y: clamp(lastPosition.current.y + gesture.dy, 64, maxY), + }); + }, + onPanResponderRelease: (_, gesture) => { + const nextPosition = { + x: clamp(lastPosition.current.x + gesture.dx, 8, maxX), + y: clamp(lastPosition.current.y + gesture.dy, 64, maxY), + }; + + lastPosition.current = nextPosition; + Animated.spring(pan, { + toValue: nextPosition, + useNativeDriver: false, + }).start(); + }, + }), + [maxX, maxY, pan], + ); + + const { selectedMoodFilter, selectedTopFilter, setSelectedMoodFilter, setSelectedTopFilter } = + useHomeFilterStore(); + const { currentLocation, currentPlace, locationStatus, selectedMode, session } = + useTravelSessionStore(); + const travelSessionActions = useTravelSessionStore((state) => ({ + clearLocation: state.clearLocation, + endSession: state.endSession, + resetSession: state.resetSession, + setLocation: state.setLocation, + setLocationStatus: state.setLocationStatus, + setMode: state.setMode, + setPlace: state.setPlace, + startSession: state.startSession, + })); + const { + failedEndpointIds, + failAllEndpoints, + mockDelayMs, + resetMockRuntime, + setFailAllEndpoints, + setMockDelayMs, + toggleFailedEndpoint, + } = useDevToolsStore(); + const { completeOnboarding, resetOnboarding } = useUserProfileStore(); + const { logs, addLog, removeLog } = useMomentLogStore(); + const { likedTracks, savedTracks, removeLikedTrack, removeSavedTrack, toggleLike, toggleSave } = + useLibraryStore(); + const { clearTrack, currentTrack, setTrack } = usePlayerStore(); + const { clearEvents, events } = useRecommendationEventStore(); + + const navigate = (path: string) => { + setIsOpen(false); + router.push(path as never); + }; + const applyProfilePreset = () => { + completeOnboarding({ + companionType: '친구', + locationRecommendationEnabled: true, + preferredGenres: ['K-POP', '인디', '팝'], + preferredMoods: ['청량한', '잔잔한'], + travelStyles: ['산책', '바다 보기'], + }); + setSelectedTopFilter('전체'); + setSelectedMoodFilter('청량한'); + router.replace('/' as never); + }; + const resetProfile = () => { + resetOnboarding(); + setSelectedTopFilter('전체'); + setSelectedMoodFilter('전체'); + setIsOpen(false); + router.replace('/onboarding' as never); + }; + const applyPlacePreset = (preset: (typeof placePresets)[number]) => { + travelSessionActions.setLocation(preset.location); + travelSessionActions.setPlace(preset.place); + invalidateMockQueries(); + }; + const setLocationState = (status: 'denied' | 'idle' | 'unavailable') => { + travelSessionActions.clearLocation(); + travelSessionActions.setLocationStatus(status); + invalidateMockQueries(); + }; + const addSampleMomentLogs = () => { + const baseTime = Date.now(); + const sampleLogs: MomentLog[] = [ + { + createdAt: new Date(baseTime - 1000 * 60 * 24).toISOString(), + id: `dev-moment-busan-${baseTime}`, + location: placePresets[0].location, + moodTags: ['fresh', 'local'], + photoUri: 'https://tong.visitkorea.or.kr/cms2/website/76/2012176.jpg', + placeCategory: '해변', + placeId: placePresets[0].place.id, + placeName: '광안리 해수욕장', + sessionId: 'dev-session-busan', + source: 'camera', + syncStatus: 'local', + track: getSampleTrack(0), + travelMode: 'walk', + }, + { + createdAt: new Date(baseTime - 1000 * 60 * 12).toISOString(), + id: `dev-moment-cafe-${baseTime}`, + location: placePresets[0].location, + moodTags: ['calm', 'emotional'], + photoUri: 'https://tong.visitkorea.or.kr/cms2/website/75/2012175.jpg', + placeCategory: '카페거리', + placeId: 'dev-busan-cafe', + placeName: '민락동 카페거리', + sessionId: 'dev-session-busan', + source: 'camera', + syncStatus: 'local', + track: getSampleTrack(2), + travelMode: 'cafe', + }, + { + createdAt: new Date(baseTime - 1000 * 60 * 4).toISOString(), + id: `dev-moment-seoul-${baseTime}`, + location: placePresets[1].location, + moodTags: ['emotional'], + photoUri: 'https://tong.visitkorea.or.kr/cms/resource_photo/96/4033396_image2_1.jpg', + placeCategory: '야경', + placeId: placePresets[1].place.id, + placeName: '남산서울타워', + sessionId: 'dev-session-seoul', + source: 'camera', + syncStatus: 'local', + track: getSampleTrack(5), + travelMode: 'night', + }, + ]; + + sampleLogs.forEach(addLog); + }; + const clearMomentLogs = () => { + logs.forEach((log) => removeLog(log.id)); + }; + const seedLibrary = () => { + const libraryState = useLibraryStore.getState(); + + sampleTracks.slice(0, 3).forEach((track) => { + if (!libraryState.isLiked(track.id)) { + libraryState.toggleLike(track, samplePlaylist.id); + } + }); + sampleTracks.slice(1, 4).forEach((track) => { + if (!libraryState.isSaved(track.id)) { + libraryState.toggleSave(track, samplePlaylist.id); + } + }); + }; + const clearLibrary = () => { + likedTracks.forEach((record) => removeLikedTrack(record.track.id)); + savedTracks.forEach((record) => removeSavedTrack(record.track.id)); + }; + const resetMockState = () => { + resetMockRuntime(); + invalidateMockQueries(); + }; + const setSlowMockState = () => { + setMockDelayMs(SLOW_DELAY_MS); + setFailAllEndpoints(false); + invalidateMockQueries(); + }; + const setFailAllMockState = () => { + setFailAllEndpoints(true); + invalidateMockQueries(); + }; + const toggleEndpoint = (endpointId: (typeof mockEndpointIds)[number]) => { + toggleFailedEndpoint(endpointId); + invalidateMockQueries(); + }; + + return ( + <> + + setIsOpen(true)} + > + + + + + + setIsOpen(false)} transparent visible={isOpen}> + + setIsOpen(false)} /> + + + + + + + Test Manager + + + 페이지 이동, 조건문, mock 실패/지연, 로컬 데이터를 빠르게 검수해요. + + + setIsOpen(false)} + > + + + + + + + + + + + + + + + + + navigate('/')} /> + navigate('/onboarding')} /> + navigate('/playlist/busan-ocean')} + /> + navigate('/playlist/seoul-night')} + /> + navigate('/recap')} /> + navigate('/recap-share/log-1')} /> + navigate('/library')} /> + navigate('/my')} /> + navigate('/camera')} /> + + + + + + + + + {topFilterOptions.map((filter) => ( + setSelectedTopFilter(filter)} + /> + ))} + {moodFilterOptions.map((filter) => ( + setSelectedMoodFilter(filter)} + /> + ))} + + + + {placePresets.map((preset) => ( + applyPlacePreset(preset)} + /> + ))} + setLocationState('denied')} + /> + setLocationState('unavailable')} + /> + setLocationState('idle')} + /> + + + + {travelModeOptions.map((mode) => ( + travelSessionActions.setMode(mode.value)} + /> + ))} + + + + + + + + setTrack(getSampleTrack(0), samplePlaylist.id)} + /> + + + + + + + + + {mockEndpointIds.map((endpointId) => ( + toggleEndpoint(endpointId)} + /> + ))} + + + + + + + ); +} diff --git a/src/components/onboarding/OnboardingScreen.tsx b/src/components/onboarding/OnboardingScreen.tsx index 56682d2..6204b4d 100644 --- a/src/components/onboarding/OnboardingScreen.tsx +++ b/src/components/onboarding/OnboardingScreen.tsx @@ -104,8 +104,8 @@ export function OnboardingScreen() { }; const applyHomeFilters = (input: UserProfileInput) => { - setSelectedTopFilter(input.preferredMoods[0] ?? '전체'); - setSelectedMoodFilter(input.travelStyles[0] ?? '전체'); + setSelectedTopFilter('전체'); + setSelectedMoodFilter(input.preferredMoods[0] ?? '전체'); }; const finish = (input: UserProfileInput) => { diff --git a/src/components/recap-share/RecapCaptureFrame.tsx b/src/components/recap-share/RecapCaptureFrame.tsx index 19516c5..ba162d3 100644 --- a/src/components/recap-share/RecapCaptureFrame.tsx +++ b/src/components/recap-share/RecapCaptureFrame.tsx @@ -1,5 +1,5 @@ import { forwardRef, PropsWithChildren, useImperativeHandle, useRef } from 'react'; -import ViewShot from 'react-native-view-shot'; +import ViewShot, { ViewShotRef } from 'react-native-view-shot'; export type RecapCaptureFrameHandle = { capture: () => Promise; @@ -9,7 +9,7 @@ type RecapCaptureFrameProps = PropsWithChildren; export const RecapCaptureFrame = forwardRef( function RecapCaptureFrame({ children }, ref) { - const viewShotRef = useRef(null); + const viewShotRef = useRef(null); useImperativeHandle(ref, () => ({ capture: () => viewShotRef.current?.capture?.() ?? Promise.resolve(undefined), diff --git a/src/mock-server/delay.ts b/src/mock-server/delay.ts index c165728..1eac49a 100644 --- a/src/mock-server/delay.ts +++ b/src/mock-server/delay.ts @@ -1,8 +1,15 @@ import { MockDelayOptions, MockEndpointId } from '@/mock-server/types'; +import { useDevToolsStore } from '@/store/devToolsStore'; const DEFAULT_DELAY_MS = 300; function getConfiguredDelayMs() { + const runtimeDelayMs = useDevToolsStore.getState().mockDelayMs; + + if (typeof runtimeDelayMs === 'number') { + return runtimeDelayMs; + } + const rawValue = process.env.EXPO_PUBLIC_MOCK_API_DELAY_MS; const parsedValue = rawValue ? Number(rawValue) : DEFAULT_DELAY_MS; @@ -25,6 +32,15 @@ function shouldFailEndpoint(endpointId: MockEndpointId, shouldFail?: boolean) { return true; } + const runtimeState = useDevToolsStore.getState(); + + if ( + runtimeState.failAllEndpoints || + runtimeState.failedEndpointIds.includes(endpointId) + ) { + return true; + } + const failedEndpointSet = getFailedEndpointSet(); return failedEndpointSet.has('*') || failedEndpointSet.has(endpointId); diff --git a/src/providers/AppProviders.tsx b/src/providers/AppProviders.tsx index 071db87..3d3b9d7 100644 --- a/src/providers/AppProviders.tsx +++ b/src/providers/AppProviders.tsx @@ -3,6 +3,15 @@ import { PropsWithChildren } from 'react'; import { queryClient } from '@/providers/queryClient'; +const DevTestManager = __DEV__ + ? require('@/components/dev/DevTestManager').DevTestManager + : undefined; + export function AppProviders({ children }: PropsWithChildren) { - return {children}; + return ( + + {children} + {DevTestManager ? : null} + + ); } diff --git a/src/store/devToolsStore.ts b/src/store/devToolsStore.ts new file mode 100644 index 0000000..97359b3 --- /dev/null +++ b/src/store/devToolsStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand'; + +import { MockEndpointId } from '@/mock-server/types'; + +export const mockEndpointIds: MockEndpointId[] = [ + 'home.featuredPlaylists', + 'home.moodRecommendations', + 'home.recentMusicLogs', + 'playlist.detail', + 'recap.list', + 'recap.share', + 'tour.nearbyPlaces', +]; + +type DevToolsState = { + failedEndpointIds: MockEndpointId[]; + failAllEndpoints: boolean; + mockDelayMs?: number; + resetMockRuntime: () => void; + setFailAllEndpoints: (failAllEndpoints: boolean) => void; + setMockDelayMs: (mockDelayMs?: number) => void; + toggleFailedEndpoint: (endpointId: MockEndpointId) => void; +}; + +export const useDevToolsStore = create((set) => ({ + failedEndpointIds: [], + failAllEndpoints: false, + mockDelayMs: undefined, + resetMockRuntime: () => + set({ + failedEndpointIds: [], + failAllEndpoints: false, + mockDelayMs: undefined, + }), + setFailAllEndpoints: (failAllEndpoints) => set({ failAllEndpoints }), + setMockDelayMs: (mockDelayMs) => set({ mockDelayMs }), + toggleFailedEndpoint: (endpointId) => + set((state) => ({ + failedEndpointIds: state.failedEndpointIds.includes(endpointId) + ? state.failedEndpointIds.filter((id) => id !== endpointId) + : [...state.failedEndpointIds, endpointId], + failAllEndpoints: false, + })), +})); diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 68bdb7f..b05a0d3 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -9,6 +9,7 @@ type PlayerState = { isPlaying: boolean; playlistId?: string; source: PlayerSource; + clearTrack: () => void; setTrack: (track: Track, playlistId?: string) => void; toggle: () => void; }; @@ -16,6 +17,13 @@ type PlayerState = { export const usePlayerStore = create((set) => ({ isPlaying: false, source: 'none', + clearTrack: () => + set({ + currentTrack: undefined, + isPlaying: false, + playlistId: undefined, + source: 'none', + }), setTrack: (track, playlistId) => set({ currentTrack: track,