diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cb3e4..e7f86eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cloud-optional backup (encrypted, opt-in) - Plugin system for custom pipelines +## [1.3.1] - 2026-09-18 + +### Fixed +- **Manual install no longer needs 7-Zip**: the signing-free channel now also publishes the + app as byte-split `zip` parts. The install script joins them and extracts with the built-in + Windows `Expand-Archive`, so a user with nothing but Windows can install (the 7z volumes + remain the smaller Scoop path). Split volumes from a different build are never mixed: + every artifact of a release comes from one build, and a published version is not overwritten. + ## [1.3.0] - 2026-09-18 > Published from an annotated tag through CI. Installer and update metadata are diff --git a/apps/admin-swagger/openapi.json b/apps/admin-swagger/openapi.json index 1032f14..ef5d178 100644 --- a/apps/admin-swagger/openapi.json +++ b/apps/admin-swagger/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "D3RO-VOICE Admin API", "description": "Admin CRM Edge Functions for user management, subscription CRUD, payment history, and audit logs.", - "version": "1.3.0" + "version": "1.3.1" }, "servers": [ { diff --git a/apps/admin/package.json b/apps/admin/package.json index 17c7840..78c4008 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/admin", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice Admin CRM — SaaS 관리 도구", "scripts": { diff --git a/apps/api-server/D3ROVoice.Api.csproj b/apps/api-server/D3ROVoice.Api.csproj index 9b8fda2..5cb08ba 100644 --- a/apps/api-server/D3ROVoice.Api.csproj +++ b/apps/api-server/D3ROVoice.Api.csproj @@ -2,7 +2,7 @@ net10.0 - 1.3.0 + 1.3.1 enable enable diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b7f828c..26006f5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/desktop", - "version": "1.3.0", + "version": "1.3.1", "productName": "d3ro-voice", "description": "로컬 AI 음성 어시스턴트 (Electron)", "main": "./out/main/index.js", diff --git a/apps/mobile-rn/android/app/build.gradle b/apps/mobile-rn/android/app/build.gradle index c99b448..43fde46 100644 --- a/apps/mobile-rn/android/app/build.gradle +++ b/apps/mobile-rn/android/app/build.gradle @@ -151,8 +151,8 @@ def versionSettingsValid = configuredVersionName != null && configuredVersionName ==~ strictSemver && configuredVersionCodeValue != null && configuredVersionCodeValue <= 2100000000L -def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.0" -def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1030001 +def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.1" +def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031001 def requiredReleaseSettings = [ D3RO_RELEASE_STORE_FILE: releaseStoreFilePath, diff --git a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj index 81d4618..fb6b4d2 100644 --- a/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj +++ b/apps/mobile-rn/ios/D3ROVoice.xcodeproj/project.pbxproj @@ -257,7 +257,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1030001; + CURRENT_PROJECT_VERSION = 1031001; ENABLE_BITCODE = NO; INFOPLIST_FILE = D3ROVoice/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -265,7 +265,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.0; + MARKETING_VERSION = 1.3.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -287,14 +287,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1030001; + CURRENT_PROJECT_VERSION = 1031001; INFOPLIST_FILE = D3ROVoice/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.0; + MARKETING_VERSION = 1.3.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/apps/mobile-rn/metadata/android/en-US/changelogs/1031001.txt b/apps/mobile-rn/metadata/android/en-US/changelogs/1031001.txt new file mode 100644 index 0000000..16d8ac2 --- /dev/null +++ b/apps/mobile-rn/metadata/android/en-US/changelogs/1031001.txt @@ -0,0 +1 @@ +Added a signing-free install path: the download is served in verifiable parts and installed after checking them. diff --git a/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031001.txt b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031001.txt new file mode 100644 index 0000000..df7a195 --- /dev/null +++ b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031001.txt @@ -0,0 +1 @@ +서명 인증서 없이도 설치할 수 있는 배포 경로를 추가했습니다. 설치 파일이 나뉘어 제공되고 검증 후에 설치됩니다. diff --git a/apps/mobile-rn/package-lock.json b/apps/mobile-rn/package-lock.json index dc24c46..713eaad 100644 --- a/apps/mobile-rn/package-lock.json +++ b/apps/mobile-rn/package-lock.json @@ -1,12 +1,12 @@ { "name": "@d3ro/mobile-rn", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@d3ro/mobile-rn", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@d3ro/api-client": "file:../../packages/api-client", "@d3ro/core": "file:../../packages/core", @@ -62,7 +62,7 @@ }, "../..": { "name": "d3ro-voice-monorepo", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "workspaces": [ "apps/desktop", @@ -81,7 +81,7 @@ }, "../../packages/api-client": { "name": "@d3ro/api-client", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -98,7 +98,7 @@ }, "../../packages/core": { "name": "@d3ro/core", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "docx": "^9.6.1" @@ -109,7 +109,7 @@ }, "../../packages/i18n": { "name": "@d3ro/i18n", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@types/react": "^19.0.0" @@ -120,7 +120,7 @@ }, "../../packages/ui-native": { "name": "@d3ro/ui-native", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@types/react": "*" diff --git a/apps/mobile-rn/package.json b/apps/mobile-rn/package.json index 4d33564..dc24dfb 100644 --- a/apps/mobile-rn/package.json +++ b/apps/mobile-rn/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/mobile-rn", - "version": "1.3.0", + "version": "1.3.1", "private": true, "scripts": { "android": "react-native run-android", diff --git a/apps/web/package.json b/apps/web/package.json index 2aef4ab..85e5343 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/web", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice 웹 앱 — Next.js 기반 SaaS 인터페이스", "scripts": { diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index ef28ded..255994d 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -139,7 +139,7 @@ export function Sidebar(): React.ReactElement { - v1.3.0 + v1.3.1 diff --git a/bucket/d3ro-voice.json b/bucket/d3ro-voice.json index dc2d85e..f09fb46 100644 --- a/bucket/d3ro-voice.json +++ b/bucket/d3ro-voice.json @@ -1,17 +1,17 @@ { - "version": "1.3.0", + "version": "1.3.1", "description": "로컬 AI 음성 어시스턴트 (faster-whisper + Ollama, 100% 오프라인 지원)", "homepage": "https://d3ro.chanpaca.net", "license": "MIT", "architecture": { "64bit": { "url": [ - "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.0/D3RO-Voice-1.3.0-x64-portable.7z.001", - "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.0/D3RO-Voice-1.3.0-x64-portable.7z.002" + "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.1/D3RO-Voice-1.3.1-x64-portable.7z.001", + "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-1.3.1/D3RO-Voice-1.3.1-x64-portable.7z.002" ], "hash": [ - "c4824bd625a2f0fd685f6fd3dd8521f4ced7d46954b59f0c10efcd432f82bd3f", - "187f4917e53525e736df838b15323e840691d602eadcfd9024407498316932ce" + "917f9d6e078517b6c924c30d26b59cf530d8c69e4adbdf679d2196bc645fa993", + "06f32ca809fed42cda9900e04a8e50da42506ed71944462314ecef40773a3197" ] } }, @@ -29,8 +29,8 @@ "architecture": { "64bit": { "url": [ - "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.0-x64-portable.7z.001", - "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.0-x64-portable.7z.002" + "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.1-x64-portable.7z.001", + "https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-$version/D3RO-Voice-1.3.1-x64-portable.7z.002" ] } } diff --git a/docs/deployment/release-guide.md b/docs/deployment/release-guide.md index 756ce11..685ffd0 100644 --- a/docs/deployment/release-guide.md +++ b/docs/deployment/release-guide.md @@ -155,7 +155,13 @@ npm run release:portable # Forgejo portable 채널 게시 - 자동 업데이트 피드(`latest.yml`)는 **건드리지 않는다** — 서명된 릴리스 전용이다. - 볼륨은 불변이다: 같은 버전 경로에 다른 바이트가 있으면 게시가 중단된다. - 태그/수동 실행 워크플로: `.forgejo/workflows/portable.yml` (필요 시크릿: `FORGEJO_TOKEN`). +- 산출물은 7z 분할 볼륨(Scoop용, 162MiB)과 zip 분할 부품(수동 설치용, 243MiB) 두 가지다. + 수동 설치 스크립트는 Windows 내장 `Expand-Archive`만 쓰므로 7-Zip이 필요 없다. - 사용자 설치: Scoop 버킷(`bucket/`) 또는 `install-d3ro-voice.ps1`. +- 한 버전의 산출물은 한 번의 빌드에서만 나온다(볼륨은 불변). CI 태그 파이프라인이 최초 게시자가 되게 하고, + 이미 게시된 버전을 같은 번호로 다시 게시하지 않는다 — 필요하면 버전을 올린다. +- 로컬에서 게시할 때는 `npm run release:portable:build`가 out/와 sidecar-dist/를 먼저 요구한다 + (`npm run build --workspace=@d3ro/desktop`, `npm run sidecar:build --workspace=@d3ro/desktop`). ## `1.2.0` 릴리스 절차 (기록) diff --git a/docs/deployment/unsigned-distribution.md b/docs/deployment/unsigned-distribution.md index 09ec446..6455451 100644 --- a/docs/deployment/unsigned-distribution.md +++ b/docs/deployment/unsigned-distribution.md @@ -46,8 +46,10 @@ ### 3-1. 서명 없이 지금 배포되는 것 (portable 채널) -- 산출물: 95MiB 단위 **7z 분할 볼륨**(`D3RO-Voice-x64-portable.7z.001/.002`) — LZMA2로 - 688MB → 162MiB까지 줄어 각 볼륨이 Cloudflare 한도 안에 들어간다. +- 산출물 두 가지(같은 버라도 목적이 다름): + - **7z 분할 볼륨** `D3RO-Voice--x64-portable.7z.001/.002` — LZMA2로 688MB → 162MiB. Scoop 전용. + - **zip 분할 부품** `...zip.001/.002/.003` — 243MiB. 수동 설치 스크립트용. Windows 내장 + `Expand-Archive`만으로 풀 수 있어 사용자가 7-Zip을 설치할 필요가 없다. - 게시 위치: `.../generic/d3ro-voice/portable-/` 와 `.../portable-latest/` (**자동 업데이트 피드 `latest.yml`과 완전히 분리**). - 설치 방법 두 가지: @@ -58,8 +60,9 @@ scoop install d3ro/d3ro-voice ``` 2. **수동 설치 스크립트** — `scripts/install/install-d3ro-voice.ps1` (피드에도 게시됨). - 볼륨을 내려받아 SHA-256 검증 → 결합 → 7-Zip으로 해제 → `%LOCALAPPDATA%\Programs\D3RO Voice` - 에 설치 + 시작 메뉴 바로가기. 관리자 권한 불필요, 7-Zip 필요. + zip 부품을 내려받아 부품별 SHA-256 검증 → 결합 → 결합본 SHA-256 재검증 → + `Expand-Archive`로 해제 → `%LOCALAPPDATA%\Programs\D3RO Voice`에 설치 + 시작 메뉴 바로가기. + 관리자 권한 불필요, 추가 도구 불필요(Windows 10/11 기본). ```powershell irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex ``` @@ -69,6 +72,10 @@ **안전 규칙(구현에 반영)**: - 볼륨은 불변 — 같은 버전 경로에 다른 바이트가 있으면 게시 중단. - 메타데이터(`portable.json`, 설치 스크립트)만 갱신 허용. +- 같은 버전을 다시 빌드하면 7z/zip 바이트가 달라져(내부 타임스탬프) 게시가 중단된다. + 즉 **한 버전의 portable 산출물은 한 번의 빌드에서만 나온다**. 태그 파이프라인(CI)이 최초 + 게시자가 되도록 하고, 로컬 재게시로 채널을 덮어쓰지 않는다. 이미 게시된 세트를 갱신해야 하면 + 버전을 올린다(이 문서 작성 시 `1.3.0` 부분 게시분을 `1.3.1`로 대체). - 파일명에 `-portable`을 넣어 서명된 릴리스 자산과 혼동되지 않게 한다. - `latest.yml`/`update-policy.json`은 절대 건드리지 않는다(자동 업데이트는 서명 릴리스 전용). @@ -95,11 +102,21 @@ --- -## 5. 검증 기록 (2026-09-18) +## 5. 현재 상태 (2026-09-18) + +- 게시된 채널: `portable-1.3.1` + `portable-latest` (7z 볼륨 2개, zip 부품 3개, 인덱스, 설치 스크립트). +- `portable-1.3.0`에는 7z 볼륨 2개만 있다(부분 게시, zip 부품 없음) — `1.3.1`이 대체한다. +- updater feed(`latest.yml`)는 여전히 `1.1.0`이며 **이 채널은 그것을 건드리지 않는다**. + +## 6. 검증 기록 (2026-09-18) - 7z 볼륨 게시 후 **무인증 공개 GET**으로 인덱스/볼륨/스크립트 제공 확인. - 설치 스크립트 end-to-end 실행: 볼륨 2개 다운로드 → SHA-256 검증 → 결합(162.1MiB) → 7-Zip 해제 → 설치 디렉토리에 `D3RO Voice.exe`, `resources/sidecar/sidecar.exe`, `resources/sidecar/_internal/faster_whisper/assets/silero_vad_v6.onnx`, `resources/sox/sox.exe` 존재 확인 → 시작 메뉴 바로가기 생성. (검증 후 테스트 설치/바로가기는 제거) -- 재게시 시도 시 동일 볼륨은 "이미 동일한 파일" 로 건너뛰는 것 확인(불변 가드 동작). \ No newline at end of file +- 재게시 시도 시 동일 볼륨은 "이미 동일한 파일" 로 건너뛰는 것 확인(불변 가드 동작). +- 1.3.1 수동 설치 스크립트를 **7-Zip 없이** end-to-end 실행: zip 부품 3개 다운로드 → + 부품별 SHA-256 → 결합(242.9MiB) → 결합본 SHA-256 → Expand-Archive → 설치 디렉토리에 + `D3RO Voice.exe`, `resources/sidecar/sidecar.exe`, VAD `silero_vad_v6.onnx`, `resources/sox/sox.exe` + 확인 → 시작 메뉴 바로가기 생성. (검증 후 테스트 설치/바로가기 제거) \ No newline at end of file diff --git a/docs/map/10-feature-catalog.md b/docs/map/10-feature-catalog.md index a6edc6c..6ee02cb 100644 --- a/docs/map/10-feature-catalog.md +++ b/docs/map/10-feature-catalog.md @@ -193,7 +193,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]` | INFRA-13 | Site deploy (Cloudflare Pages + GitHub Pages) | [x] | `.forgejo/workflows/deploy-site.yml`, `.github/workflows/deploy-site.yml` | | INFRA-15 | Update & release system | [x] | Canonical Forgejo feed + channels/policy (`release/update-policy.json`, `src/main/update-policy.ts`), canonical publisher `scripts/ci/publish-forgejo-release.mjs`, legacy GitLab mirror; `npm run release:metadata:test`. v1.1.0 was published to Forgejo on 2026-09-15; product version moved to `1.2.0` as a forward-fix with CI-only publication, a same-version re-release guard, and download centers that link the feed instead of repository paths. `1.3.0` (2026-09-18) carries the local-STT fixes; Windows publication still needs the CI signing secrets (`11` GAP-REL-02). | | INFRA-16 | Desktop STT engine packaging | [x] | `apps/desktop/scripts/setup-sidecar.mjs` + `build-sidecar.mjs`, `electron-builder.yml` `extraResources` (`sidecar-dist/sidecar` → `resources/sidecar`, `resources/ffmpeg`), and `scripts/ci/verify-sidecar-bundle.mjs` run in `package-windows`/`package-macos` before electron-builder. Verified on the real bundle: `sidecar.exe` + `_internal` including `faster_whisper/assets/silero_vad_v6.onnx`, plus a packaged-engine transcription round-trip on GPU. | -| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; updater feed와 분리. 2026-09-18 게시 + 실제 설치 검증. | +| INFRA-17 | 서명 없는 배포 채널 (portable + Scoop) | [x] | `scripts/ci/build-portable.mjs` (95MiB 7z 분할 볼륨 + Scoop 매니페스트), `scripts/ci/publish-portable-release.mjs`, `scripts/local/install-d3ro-voice.ps1`, `bucket/` 버킷, `.forgejo/workflows/portable.yml`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. | --- diff --git a/package-lock.json b/package-lock.json index 3e672a8..fc196af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "d3ro-voice-monorepo", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "d3ro-voice-monorepo", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "workspaces": [ "apps/desktop", @@ -25,7 +25,7 @@ }, "apps/admin": { "name": "@d3ro/admin", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@d3ro/api-client": "*", "@d3ro/core": "*", @@ -109,7 +109,7 @@ }, "apps/desktop": { "name": "@d3ro/desktop", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -157,7 +157,7 @@ }, "apps/web": { "name": "@d3ro/web", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@d3ro/api-client": "*", "@d3ro/core": "*", @@ -16866,7 +16866,7 @@ }, "packages/api-client": { "name": "@d3ro/api-client", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -16883,7 +16883,7 @@ }, "packages/core": { "name": "@d3ro/core", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "docx": "^9.6.1" @@ -16894,7 +16894,7 @@ }, "packages/i18n": { "name": "@d3ro/i18n", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@types/react": "^19.0.0" @@ -16905,7 +16905,7 @@ }, "packages/ui": { "name": "@d3ro/ui", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "@d3ro/core": "*" @@ -16925,7 +16925,7 @@ }, "packages/ui-native": { "name": "@d3ro/ui-native", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@types/react": "*" diff --git a/package.json b/package.json index 1f02434..6109887 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "d3ro-voice-monorepo", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)", "author": "D3RO", diff --git a/packages/api-client/package.json b/packages/api-client/package.json index e56a903..2ec3d14 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/api-client", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice API 클라이언트 — Supabase 래퍼 (web/desktop/mobile 공유)", "license": "MIT", diff --git a/packages/core/package.json b/packages/core/package.json index f337d21..0653bcb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/core", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸", "license": "MIT", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index d496837..9c9d457 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/i18n", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice 공유 i18n — 12개 locale, 타입 안전 키, Context, 포맷 유틸", "license": "MIT", diff --git a/packages/ui-native/package.json b/packages/ui-native/package.json index 746c3ce..8ad0f52 100644 --- a/packages/ui-native/package.json +++ b/packages/ui-native/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/ui-native", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index ce578c6..c949d91 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/ui", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵", "license": "MIT", diff --git a/release/product-version.json b/release/product-version.json index ec8a3a1..941ec32 100644 --- a/release/product-version.json +++ b/release/product-version.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "version": "1.3.0", - "androidVersionCode": 1030001, - "iosBuildNumber": 1030001, + "version": "1.3.1", + "androidVersionCode": 1031001, + "iosBuildNumber": 1031001, "releaseDate": "2026-09-18", "desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f" } diff --git a/scripts/ci/build-portable.mjs b/scripts/ci/build-portable.mjs index 1e3f81a..a1143fc 100644 --- a/scripts/ci/build-portable.mjs +++ b/scripts/ci/build-portable.mjs @@ -11,6 +11,9 @@ // apps/desktop/release//portable.json (볼륨 인덱스: 이름/크기/sha256) // bucket/d3ro-voice.json (Scoop 매니페스트, 커밋 대상) // +// zip 분할 부품(수동 설치용)도 함께 만든다 — Windows 내장 Expand-Archive로 해제할 수 있어 +// 사용자에게 7-Zip 설치를 요구하지 않는다. 7z 볼륨은 Scoop 전용으로 더 작다(162MiB vs 243MiB). +// // 사용: // npm run build --workspace=@d3ro/desktop // node scripts/ci/build-portable.mjs # 7z 분할 볼륨 @@ -18,7 +21,15 @@ import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' -import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { + existsSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -34,9 +45,12 @@ const FEED = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice' const PORTABLE_VERSION_PATH = `${FEED}/portable-${version}` const PORTABLE_LATEST_PATH = `${FEED}/portable-latest` const ARCHIVE_BASE = `D3RO-Voice-${version}-x64-portable` -/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 볼륨 크기 */ +/** Cloudflare 본문 한도(100MiB)보다 여유를 둔 7z 볼륨 크기 (Scoop 경로) */ const VOLUME_SIZE = '95m' const MAX_VOLUME_BYTES = 95 * 1024 * 1024 +/** 수동 설치 스크립트용 zip 분할 부품 크기 */ +const ZIP_PART_SIZE = '90m' +const MAX_ZIP_PART_BYTES = 95 * 1024 * 1024 const wantZip = process.argv.includes('--zip') const releaseDir = join(desktopDir, 'release', version) @@ -225,6 +239,73 @@ writeFileSync( 'utf8', ) +// ── 수동 설치용 zip 분할 부품 ───────────────────────────── +const zipTarget = join(releaseDir, `${ARCHIVE_BASE}.zip`) +console.log(`[portable] 수동 설치용 zip 생성 (분할 ${ZIP_PART_SIZE})`) +const zipBuild = spawnSync( + 'npx', + [ + 'electron-builder', + '--win', + 'zip', + '--x64', + '--config', + 'electron-builder.yml', + '--publish', + 'never', + '-c.win.forceCodeSigning=false', + '-c.npmRebuild=false', + ], + { cwd: desktopDir, stdio: 'inherit', shell: process.platform === 'win32' }, +) +if (zipBuild.status !== 0) { + console.error(`[portable] zip 빌드 실패 (exit ${zipBuild.status ?? 'null'})`) + process.exit(zipBuild.status ?? 1) +} + +const producedZip = readdirSync(releaseDir).find((name) => + name.endsWith('.zip') && name.includes(version) && !name.includes('.part'), +) +if (!producedZip) { + console.error('[portable] zip 산출물을 찾을 수 없습니다.') + process.exit(1) +} +if (join(releaseDir, producedZip) !== zipTarget) { + renameSync(join(releaseDir, producedZip), zipTarget) +} + +// zip을 90MiB 단위로 바이트 분할한다 (사용자가 이어 붙여 Expand-Archive로 해제) +const zipBytes = readFileSync(zipTarget) +const partSize = 90 * 1024 * 1024 +const zipParts = [] +for (let offset = 0, index = 1; offset < zipBytes.length; offset += partSize, index += 1) { + const slice = zipBytes.subarray(offset, Math.min(offset + partSize, zipBytes.length)) + const name = `${ARCHIVE_BASE}.zip.${String(index).padStart(3, '0')}` + if (slice.length > MAX_ZIP_PART_BYTES) { + console.error(`[portable] zip 부품이 너무 큽니다: ${name}`) + process.exit(1) + } + writeFileSync(join(releaseDir, name), slice) + zipParts.push({ + name, + size: slice.length, + sha256: createHash('sha256').update(slice).digest('hex'), + url: `${PORTABLE_VERSION_PATH}/${name}`, + }) +} +const zipSha256 = createHash('sha256').update(zipBytes).digest('hex') + +// 인덱스에 zip 부품 정보를 추가한다 (설치 스크립트가 사용) +const indexJson = JSON.parse(readFileSync(join(releaseDir, 'portable.json'), 'utf8')) +indexJson.zipArchive = `${ARCHIVE_BASE}.zip` +indexJson.zipSize = zipBytes.length +indexJson.zipSha256 = zipSha256 +indexJson.zipParts = zipParts +writeFileSync( + join(releaseDir, 'portable.json'), + `${JSON.stringify(indexJson, null, 2)}\n`, + 'utf8', +) console.log( [ '[portable] 완료', @@ -232,6 +313,7 @@ console.log( ...volumeEntries.map( (entry) => ` ${entry.name} (${(entry.size / 1048576).toFixed(1)}MiB)`, ), + ` zip : ${zipParts.length}개 부품 / 합계 ${(zipBytes.length / 1048576).toFixed(1)}MiB`, ` 인덱스 : ${join(releaseDir, 'portable.json')}`, ` scoop : ${join(root, 'bucket', 'd3ro-voice.json')}`, ` 게시 : node scripts/ci/publish-portable-release.mjs`, diff --git a/scripts/ci/publish-portable-release.mjs b/scripts/ci/publish-portable-release.mjs index 88d3d2e..5fafdb0 100644 --- a/scripts/ci/publish-portable-release.mjs +++ b/scripts/ci/publish-portable-release.mjs @@ -67,6 +67,21 @@ for (const volume of index.volumes) { payloads.push({ name: volume.name, bytes, contentType: 'application/octet-stream' }) } +// 수동 설치용 zip 분할 부품 (Windows 내장 Expand-Archive로 해제 — 7-Zip 불필요) +for (const part of index.zipParts ?? []) { + const partPath = join(releaseDir, part.name) + if (!existsSync(partPath)) { + console.error(`[portable] zip 부품이 없습니다: ${partPath}`) + process.exit(1) + } + const partBytes = await readFile(partPath) + const partSha = createHash('sha256').update(partBytes).digest('hex') + if (partSha !== part.sha256) { + console.error(`[portable] zip 부품 sha256 불일치 (${part.name})`) + process.exit(1) + } + payloads.push({ name: part.name, bytes: partBytes, contentType: 'application/octet-stream' }) +} payloads.push({ name: 'portable.json', bytes: Buffer.from(`${JSON.stringify(index, null, 2)}\n`, 'utf8'), @@ -145,7 +160,7 @@ console.log( ' scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git', ' scoop install d3ro/d3ro-voice', '', - ' 수동 설치(7-Zip 필요):', + ' 수동 설치(추가 도구 불필요):', ` irm ${FEED}/portable-latest/install-d3ro-voice.ps1 | iex`, '', ' 참고: 이 채널은 서명이 없어 자동 업데이트 피드를 갱신하지 않습니다.', diff --git a/scripts/install/install-d3ro-voice.ps1 b/scripts/install/install-d3ro-voice.ps1 index c3892c5..fc6feee 100644 --- a/scripts/install/install-d3ro-voice.ps1 +++ b/scripts/install/install-d3ro-voice.ps1 @@ -1,23 +1,24 @@ -# scripts/local/install-d3ro-voice.ps1 +# scripts/install/install-d3ro-voice.ps1 # 서명 없이 D3RO Voice를 설치하는 수동 설치 스크립트. # # 왜 스크립트인가: canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 100MiB를 넘으면 -# 거부된다. 사이드카(faster-whisper)를 포함한 앱은 95MiB 단위 7z 볼으로 나뉘어 있고, -# 이 스크립트가 볼륨을 이어 붙여 해제한다. Scoop을 쓰면 Scoop이 같은 일을 자동으로 한다. +# 거부된다. 사이드카(faster-whisper)를 포함한 앱은 그보다 크므로 zip을 90MiB 단위로 +# 나누어 게시하고, 이 스크립트가 부품을 이어 붙여 설치한다. Windows 내장 +# Expand-Archive만 사용하므로 7-Zip 같은 추가 도구가 필요 없고 관리자 권한도 필요 없다. # # 사용: # irm https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest/install-d3ro-voice.ps1 | iex # 또는 저장 후: # powershell -ExecutionPolicy Bypass -File install-d3ro-voice.ps1 # -# 요구 사항: Windows 10/11 x64, 7-Zip(없으면 Scoop 사용을 권장). -# 관리자 권한 불필요 — %LOCALAPPDATA%\Programs 아래에 설치한다. +# Scoop을 쓸 수 있으면 그쪽이 더 작고(7z 162MiB) 업데이트도 자동이다: +# scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git +# scoop install d3ro/d3ro-voice [CmdletBinding()] param( [string]$FeedBase = 'https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/portable-latest', [string]$InstallDir = (Join-Path $env:LOCALAPPDATA 'Programs\D3RO Voice'), - [string]$SevenZipPath = '', [switch]$Force ) @@ -30,98 +31,81 @@ function Get-Sha256($path) { $sha = [System.Security.Cryptography.SHA256]::Create() try { $stream = [System.IO.File]::OpenRead($path) - try { - $bytes = $sha.ComputeHash($stream) - } finally { $stream.Dispose() } + try { $bytes = $sha.ComputeHash($stream) } finally { $stream.Dispose() } } finally { $sha.Dispose() } - return ($bytes | ForEach-Object { $_.ToString("x2") }) -join '' + return ($bytes | ForEach-Object { $_.ToString('x2') }) -join '' } -Write-Step 'D3RO Voice 휴대용 배포본 설치를 시작합니다 (서명되지 않은 빌드).' +Write-Step 'D3RO Voice 설치를 시작합니다 (서명되지 않은 빌드).' # 1. 인덱스 내려받기 $indexUrl = "$FeedBase/portable.json" Write-Step "인덱스: $indexUrl" $index = Invoke-RestMethod -Uri $indexUrl -UseBasicParsing $version = $index.version -Write-Step "버전 $version, 볼륨 $($index.volumeCount)개 (합계 $([math]::Round($index.totalSize / 1MB, 1)) MB)" -# 2. 임시 디렉터리에 볼 내려받기 + 해시 검증 +if (-not $index.zipParts -or $index.zipParts.Count -eq 0) { + throw '인덱스에 zip 부품 정보가 없습니다. 이 스크립트는 zipParts가 있는 버전(1.3.0+)을 지원합니다.' +} + +Write-Step "버전 $version, 부품 $($index.zipParts.Count)개 (합계 $([math]::Round($index.zipSize / 1MB, 1)) MB)" + +# 2. 임시 디렉터리에 부품 내려받기 + 해시 검증 $tempRoot = [System.IO.Path]::GetTempPath() if ($env:TEMP) { $tempRoot = $env:TEMP } elseif ($env:TMP) { $tempRoot = $env:TMP } + $workDir = Join-Path $tempRoot "d3ro-voice-$version-portable" if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir } New-Item -ItemType Directory -Path $workDir | Out-Null -foreach ($volume in $index.volumes) { - $dest = Join-Path $workDir $volume.name - $url = "$FeedBase/$($volume.name)" - Write-Step "내려받기: $($volume.name) ($([math]::Round($volume.size / 1MB, 1)) MB)" - Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing +foreach ($part in $index.zipParts) { + $dest = Join-Path $workDir $part.name + Write-Step "내려받기: $($part.name) ($([math]::Round($part.size / 1MB, 1)) MB)" + Invoke-WebRequest -Uri "$FeedBase/$($part.name)" -OutFile $dest -UseBasicParsing $hash = Get-Sha256 $dest - if ($hash -ne $volume.sha256) { - throw "해시가 일치하지 않습니다: $($volume.name)`n 기대: $($volume.sha256)`n 실제: $hash" + if ($hash -ne $part.sha256) { + throw "해시가 일치하지 않습니다: $($part.name)`n 기대: $($part.sha256)`n 실제: $hash" } } -Write-Step '모든 볼륨의 SHA-256 검증 완료' +Write-Step '모든 부품의 SHA-256 검증 완료' -# 3. 볼륨 이어 붙이기 -$archive = Join-Path $workDir "$($index.archive)" +# 3. 부품 이어 붙이기 +$archive = Join-Path $workDir $index.zipArchive $stream = [System.IO.File]::Create($archive) try { - foreach ($volume in $index.volumes) { - $part = [System.IO.File]::OpenRead((Join-Path $workDir $volume.name)) - try { $part.CopyTo($stream) } finally { $part.Dispose() } + foreach ($part in $index.zipParts) { + $piece = [System.IO.File]::OpenRead((Join-Path $workDir $part.name)) + try { $piece.CopyTo($stream) } finally { $piece.Dispose() } } } finally { $stream.Dispose() } + +$archiveHash = Get-Sha256 $archive +if ($index.zipSha256 -and $archiveHash -ne $index.zipSha256) { + throw "결합한 아카이브의 해시가 인덱스와 다릅니다.`n 기대: $($index.zipSha256)`n 실제: $archiveHash" +} Write-Step "아카이브 결합 완료: $([math]::Round((Get-Item $archive).Length / 1MB, 1)) MB" -# 4. 해제 (7-Zip 필요; 없으면 안내) -$sevenZipCandidates = @() -foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) { - if ($base) { $sevenZipCandidates += (Join-Path $base '7-Zip\7z.exe') } -} -$sevenZip = $sevenZipCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 - -if ($SevenZipPath) { $sevenZip = $SevenZipPath } - -if (-not $sevenZip) { - # Windows PowerShell 5.1 호환 (?. 연산자는 PowerShell 7 전용) - $sevenZipCommand = Get-Command 7z -ErrorAction SilentlyContinue - if ($sevenZipCommand) { $sevenZip = $sevenZipCommand.Source } -} - -if (-not $sevenZip) { - throw @' -7-Zip을 찾을 수 없습니다. 두 가지 방법이 있습니다. - 1) Scoop 사용(권장, 7-Zip 자동 준비): - scoop bucket add d3ro https://git.chanpaca.net/yunchan/d3ro-voice.git - scoop install d3ro/d3ro-voice - 2) 7-Zip 설치 후 이 스크립트를 다시 실행: https://www.7-zip.org/ -'@ -} - +# 4. 압축 해제 (Windows 내장 Expand-Archive — 추가 도구 불필요) $extractDir = Join-Path $workDir 'extract' +Write-Step '압축 해제 중 (수백 MB, 시간이 걸릴 수 있습니다)' +Expand-Archive -LiteralPath $archive -DestinationPath $extractDir -Force + +# 5. 설치 디렉터리로 배치 if (Test-Path $InstallDir) { if (-not $Force) { - throw "설치 경로가 이미 있습니다: $InstallDir`n 다시 설치하려면 -Force 붙이세요." + throw "설치 경로가 이미 있습니다: $InstallDir`n 다시 설치하려면 -Force 를 붙이세요." } Write-Step "기존 설치를 교체합니다: $InstallDir" Remove-Item -Recurse -Force $InstallDir } New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null - -Write-Step '압축 해제 중 (수백 MB, 시간이 걸릴 수 있습니다)' -& $sevenZip x $archive "-o$extractDir" -y | Out-Null -if ($LASTEXITCODE -ne 0) { throw "압축 해제 실패 (7-Zip exit $LASTEXITCODE)" } - Copy-Item -Path (Join-Path $extractDir '*') -Destination $InstallDir -Recurse -Force -# 5. 시작 메뉴 바로가기 +# 6. 시작 메뉴 바로가기 $exe = Join-Path $InstallDir 'D3RO Voice.exe' if (-not (Test-Path $exe)) { throw "실행 파일을 찾을 수 없습니다: $exe" } @@ -139,8 +123,9 @@ Write-Step "설치 완료: $InstallDir" Write-Step "시작 메뉴 바로가기: $shortcutPath" Write-Host '' Write-Host '참고:' -ForegroundColor Yellow -Write-Host ' - 이 빌드는 Authenticode 서명이 없어 첫 실행 시 SmartScreen 경고가 뜰 수 있습니다.' +Write-Host ' - 이 빌드는 Authenticode 서명이 없어 SmartScreen 경고가 뜰 수 있습니다("추가 정보 -> 실행").' Write-Host ' - 자동 업데이트는 서명된 릴리스가 게시된 뒤부터 동작합니다(현재 설치본은 그 피드를 봅니다).' Write-Host ' - 설정/모델/기록은 %APPDATA%\d3ro-voice 를 공유하므로 기존 설치와 동일하게 유지됩니다.' +Write-Host ' - Scoop 사용자는 scoop update d3ro-voice 로 갱신할 수 있습니다(7z 162MiB로 더 작음).' Write-Host '' Write-Host "실행: `"$exe`"" -ForegroundColor Green \ No newline at end of file diff --git a/site/package-lock.json b/site/package-lock.json index 57d4b69..5f33c08 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1,12 +1,12 @@ { "name": "d3ro-voice-site", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "d3ro-voice-site", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/site/package.json b/site/package.json index 1c5a97d..7aaef9d 100644 --- a/site/package.json +++ b/site/package.json @@ -1,7 +1,7 @@ { "name": "d3ro-voice-site", "private": true, - "version": "1.3.0", + "version": "1.3.1", "type": "module", "scripts": { "dev": "vite --port 5199 --host",