From 27facb85693b709ef861bcbab46639497ad006c9 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Fri, 18 Sep 2026 20:26:26 +0900 Subject: [PATCH 1/2] fix(desktop): verify runtime parts on disk so engine install stops failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part checks counted bytes from the network stream while the joined archive was hashed from disk, so a truncated write passed part verification and only failed later as "런타임 아카이브 해시 불일치". Verify size and hash from the written file, check the joined size before its hash, and retry a failed part up to 3 times. --- .../src/main/services/RuntimeProvisioner.ts | 147 ++++++++++++------ docs/map/10-feature-catalog.md | 2 +- docs/map/11-gap-backlog.md | 1 + 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/apps/desktop/src/main/services/RuntimeProvisioner.ts b/apps/desktop/src/main/services/RuntimeProvisioner.ts index 923a3c6..35e012f 100644 --- a/apps/desktop/src/main/services/RuntimeProvisioner.ts +++ b/apps/desktop/src/main/services/RuntimeProvisioner.ts @@ -14,8 +14,8 @@ import { EventEmitter, once } from 'events' import { createHash } from 'node:crypto' import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs' -import { mkdir, rm } from 'node:fs/promises' -import { Readable } from 'node:stream' +import { mkdir, rm, stat } from 'node:fs/promises' +import { Readable, Writable } from 'node:stream' import { pipeline } from 'node:stream/promises' import { join } from 'node:path' import { app } from 'electron' @@ -69,6 +69,8 @@ export interface RuntimeStatus { const RUNTIME_DIR_NAME = 'runtime' const DOWNLOAD_TIMEOUT_MS = 120_000 +/** 부품 다운로드 재시도 횟수 — 전송 중 잘림/일시적 네트워크 오류 대비 */ +const PART_DOWNLOAD_ATTEMPTS = 3 class RuntimeProvisioner extends EventEmitter { constructor() { @@ -248,68 +250,101 @@ class RuntimeProvisioner extends EventEmitter { for (const part of entry.parts) { const partPath = join(tempDir, part.name) - const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }) - if (!response.ok || !response.body) { - throw new D3ROError( - ErrorCode.STTSidecarSpawnFailed, - `런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`, - ) - } + const partSize = await this._downloadPart(part, partPath) - const hash = createHash('sha256') - let partBytes = 0 - const source = Readable.fromWeb(response.body as never) - source.on('data', (chunk: Buffer) => { - hash.update(chunk) - partBytes += chunk.length - downloadedBytes += chunk.length - const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000) - this._emitProgress( - component, - 'downloading', - totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0, - downloadedBytes, - totalBytes, - Math.round(downloadedBytes / elapsed), - ) - }) - - await pipeline(source, createWriteStream(partPath)) - - const actual = hash.digest('hex') - if (part.sha256 && actual !== part.sha256) { - throw new D3ROError( - ErrorCode.STTSidecarSpawnFailed, - `런타임 부품 해시 불일치 (${part.name})`, - ) - } - if (partBytes !== part.size) { - throw new D3ROError( - ErrorCode.STTSidecarSpawnFailed, - `런타임 부품 크기 불일치 (${part.name}: ${partBytes} != ${part.size})`, - ) - } + downloadedBytes += partSize + const elapsed = Math.max(0.001, (Date.now() - startedAt) / 1000) + this._emitProgress( + component, + 'downloading', + totalBytes > 0 ? Math.min(100, Math.round((downloadedBytes * 100) / totalBytes)) : 0, + downloadedBytes, + totalBytes, + Math.round(downloadedBytes / elapsed), + ) } - // 부품을 순서대로 이어 인다 (스트리밍 — 메모리에 통째로 올리지 않는다) - const archiveHash = createHash('sha256') + // 부품을 순서대로 이어 붙인다 (스트리밍 — 메모리에 통째로 올리지 않는다) const archiveStream = createWriteStream(archivePath) for (const part of entry.parts) { - const source = createReadStream(join(tempDir, part.name)) - source.on('data', (chunk: string | Buffer) => archiveHash.update(chunk)) - await pipeline(source, archiveStream, { end: false }) + await pipeline(createReadStream(join(tempDir, part.name)), archiveStream, { end: false }) } archiveStream.end() await once(archiveStream, 'finish') - const actualArchive = archiveHash.digest('hex') + + // 크기를 먼저 본다 — 불일치하면 "어디까지 받았는지"가 로그에 남아 진단이 가능하다. + const archiveSize = (await stat(archivePath)).size + const expectedSize = entry.parts.reduce((sum, part) => sum + part.size, 0) + if (archiveSize !== expectedSize) { + throw new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `런타임 아카이브 크기 불일치 (${component}: ${archiveSize} != ${expectedSize})`, + ) + } + + const actualArchive = await sha256File(archivePath) if (entry.sha256 && actualArchive !== entry.sha256) { throw new D3ROError( ErrorCode.STTSidecarSpawnFailed, - `런타임 아카이브 해시 불일치 (${component})`, + `런타임 아카이브 해시 불일치 (${component}: ${actualArchive} != ${entry.sha256})`, ) } } + /** + * 부품 하나를 디스크로 내려받고 디스크 기준으로 크기·해시를 검증한다. + * 전송이 도중에 끊기면 같은 부품을 다시 받는다 (기존에는 1회 실패가 곧 설치 실패였다). + */ + private async _downloadPart(part: RuntimePart, partPath: string): Promise { + let lastError: Error | null = null + + for (let attempt = 1; attempt <= PART_DOWNLOAD_ATTEMPTS; attempt += 1) { + try { + const response = await fetch(part.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }) + if (!response.ok || !response.body) { + throw new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `런타임 부품을 받을 수 없습니다 (HTTP ${response.status}): ${part.name}`, + ) + } + + // 스트림을 파일로 저장한 뒤 "디스크에 실제로 남은 파일"에서 크기와 해시를 계산한다. + // 메모리 스트림에서 센 값으로 검증하면, 디스크 쓰기가 잘려도 부품 검사를 통과해 + // 결합 단계에 가서야 해시 불일치로 터진다 — 실측 사고. + await pipeline(Readable.fromWeb(response.body as never), createWriteStream(partPath)) + + const partSize = (await stat(partPath)).size + if (partSize !== part.size) { + throw new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `런타임 부품 크기 불일치 (${part.name}: ${partSize} != ${part.size})`, + ) + } + + const actualPartHash = await sha256File(partPath) + if (part.sha256 && actualPartHash !== part.sha256) { + throw new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `런타임 부품 해시 불일치 (${part.name})`, + ) + } + + return partSize + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)) + await rm(partPath, { force: true }).catch(() => undefined) + logger.warn( + `런타임 부품 다운로드 실패 (${part.name}, ${attempt}/${PART_DOWNLOAD_ATTEMPTS}): ${lastError.message}`, + ) + } + } + + throw lastError ?? new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `런타임 부품 다운로드 실패 (${part.name})`, + ) + } + private _emitProgress( component: RuntimeComponent, phase: RuntimeProgressEvent['phase'], @@ -329,6 +364,18 @@ class RuntimeProvisioner extends EventEmitter { } } +/** 파일 SHA-256 (스트림 — 메모리에 통째로 올리지 않는다) */ +async function sha256File(path: string): Promise { + const hash = createHash('sha256') + await pipeline(createReadStream(path), new Writable({ + write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { + hash.update(chunk) + callback() + }, + })) + return hash.digest('hex') +} + let _instance: RuntimeProvisioner | null = null export function getRuntimeProvisioner(): RuntimeProvisioner { @@ -340,4 +387,4 @@ export function getRuntimeProvisioner(): RuntimeProvisioner { export function resetRuntimeProvisionerForTests(): void { _instance = null -} \ No newline at end of file +} diff --git a/docs/map/10-feature-catalog.md b/docs/map/10-feature-catalog.md index 20e1714..6a6ae29 100644 --- a/docs/map/10-feature-catalog.md +++ b/docs/map/10-feature-catalog.md @@ -194,7 +194,7 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]` | 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`; 7z 분할 볼륨(Scoop, 162MiB) + zip 분할 부품(수동 설치, 243MiB, 7-Zip 불필요); updater feed와 분리. 2026-09-18 `portable-1.3.1` 게시 + 실제 설치 검증. | -| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/17초, ffmpeg 21.7MiB/5초). | +| INFRA-18 | 로컬 런타임 온디맨드 설치 | [x] | `RuntimeProvisioner`(부품 다운로드 + SHA-256 검증 + tar 해제, `%APPDATA%/d3ro-voice/runtime`), `POST runtime:ensure` / `runtime:progress` IPC, 설정 > STT 상태/내려받기 UI. 설치본에서 엔진/ffmpeg를 분리해 189MB → 90.6MiB, 업데이트 피드 게시 복구. 검증은 전부 디스크에 기록된 파일 기준이며(부품 크기·해시 → 결합본 크기·해시), 부품 다운로드는 최대 3회 재시도한다. 2026-09-18 실제 feed 통합 검증(엔진 94.4MiB/18초, ffmpeg 21.7MiB/5초). | | INFRA-19 | 네이티브 ABI + updater 설정 게이트 | [x] | `scripts/ci/verify-native-abi.mjs`(패키징된 `better_sqlite3.node`가 Electron ABI인지 호스트 Node 로드 거부로 판별) + `scripts/ci/fix-native-abi.mjs`(로컬 잠금 우회용 주입). GitLab/Forgejo/GitHub 패키징 단계에 검증 삽입. 2026-09-18: Node ABI 모듈로 앱이 시작 즉시 죽은 사고 + 누락으로 자동 업데이트가 죽은 사고를 함께 방지(). | --- diff --git a/docs/map/11-gap-backlog.md b/docs/map/11-gap-backlog.md index 43282a1..131d02b 100644 --- a/docs/map/11-gap-backlog.md +++ b/docs/map/11-gap-backlog.md @@ -53,6 +53,7 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res | GAP-REL-04 | Release | canonical feed는 Cloudflare 뒤에 있어 업로드 본문이 **100MiB**를 넘으면 HTTP 413으로 거부한다. | `scripts/ci/publish-updater-release.mjs`, `docs/deployment/unsigned-distribution.md` | `[~]` 2026-09-18: 설치본을 90.6MiB로 줄여 업데이트 피드 게시를 복구했다(GAP-STT-07). 휴대용/Scoop 채널은 여전히 95MiB 분할이 필요하다. | | GAP-STT-07 | Local STT | 진(사이드카)을 앱 번들에 넣으면 설치본이 100MiB를 넘고 매 업데이트마다 162MiB를 다시 받는다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts`, `apps/desktop/electron-builder.yml` | `[x]` 2026-09-18: 설치본에서 엔진/ffmpeg를 제거하고 처음 필요할 때 `runtime-latest`에서 내려받는다(부품별 + 결합본 SHA-256 검증). 설치본 189MB → 90.6MiB, 런타임 1회 116MiB(엔진 94.4 + ffmpeg 21.7). 실제 feed로 통합 검증 완료. | | GAP-STT-06 | Local STT | Decode settings were untuned: previous-text conditioning let repeated hallucinations compound, and no VAD parameters meant slow, uneven segments. | `apps/desktop/sidecar/main.py` | `[x]` 2026-09-18: `condition_on_previous_text=false`, bounded low-temperature fallback, `no_speech`/`compression_ratio`/`log_prob` thresholds, 300 ms silence trimming. Same transcript, ~5x faster on the reference machine (7.7 s audio: 1609 ms → 303 ms). | +| GAP-STT-08 | Local STT | 1.3.5 설치본에서 엔진 설치가 "런타임 아카이브 해시 불일치 (sidecar)"로 항상 실패했다. 부품 검증은 **메모리 스트림**에서 센 값으로, 결합 검증은 **디스크 파일**에서 계산해 기준이 서로 달랐다. 디스크 쓰기가 잘려도 부품 검사를 통과하고 결합 단계에서만 터지므로 원인 파악도 불가능했다. 재시도가 없어 전송이 한 번 끊기면 곧바로 설치 실패였다. | `apps/desktop/src/main/services/RuntimeProvisioner.ts` | `[x]` 2026-09-18: 부품 크기·해시를 디스크 파일 기준으로 통일하고, 결합본은 크기를 먼저 검사한 뒤 해시를 본다(오류 메시지에 실제/기대값 포함). 부품 다운로드는 실패 시 해당 파일을 지우고 최대 3회 재시도한다. 서버 아티팩트는 무결함을 확인했고(부품 2개 해시 일치, 결합본 `e203aa53…` = 인덱스 기대값), 실제 feed로 설치를 재현해 18초 만에 성공. | --- From 6766cb8c8ef27834ea43a91538528c15ca9f2fff Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Fri, 18 Sep 2026 20:58:52 +0900 Subject: [PATCH 2/2] chore(release): 1.3.6 --- CHANGELOG.md | 11 ++++++++++ apps/admin-swagger/openapi.json | 2 +- apps/admin/package.json | 2 +- apps/api-server/D3ROVoice.Api.csproj | 2 +- apps/desktop/package.json | 2 +- apps/mobile-rn/android/app/build.gradle | 4 ++-- .../ios/D3ROVoice.xcodeproj/project.pbxproj | 8 ++++---- .../android/en-US/changelogs/1031006.txt | 1 + .../android/ko-KR/changelogs/1031006.txt | 1 + apps/mobile-rn/package-lock.json | 14 ++++++------- apps/mobile-rn/package.json | 2 +- apps/web/package.json | 2 +- apps/web/src/components/layout/sidebar.tsx | 2 +- package-lock.json | 20 +++++++++---------- package.json | 2 +- packages/api-client/package.json | 2 +- packages/core/package.json | 2 +- packages/i18n/package.json | 2 +- packages/ui-native/package.json | 2 +- packages/ui/package.json | 2 +- release/product-version.json | 6 +++--- site/package-lock.json | 4 ++-- site/package.json | 2 +- 23 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt create mode 100644 apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0418b8c..f6f813e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ 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.6] - 2026-09-18 + +### Fixed +- **Installing the local speech engine always failed** with a hash mismatch, leaving local + transcription unusable. The downloaded parts were checked against bytes counted from the + network stream, while the joined archive was checked against what was actually written to + disk, so a truncated write passed part verification and only surfaced later as an archive + mismatch with no usable diagnostic. Every check now reads the file on disk, the joined + archive is size-checked before hashing, mismatch errors report the actual and expected + values, and a failed part is discarded and retried up to three times. + ## [1.3.5] - 2026-09-18 ### Fixed diff --git a/apps/admin-swagger/openapi.json b/apps/admin-swagger/openapi.json index 54249a4..943a0e2 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.5" + "version": "1.3.6" }, "servers": [ { diff --git a/apps/admin/package.json b/apps/admin/package.json index 188da87..ec9a77f 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/admin", - "version": "1.3.5", + "version": "1.3.6", "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 a7ada74..4543483 100644 --- a/apps/api-server/D3ROVoice.Api.csproj +++ b/apps/api-server/D3ROVoice.Api.csproj @@ -2,7 +2,7 @@ net10.0 - 1.3.5 + 1.3.6 enable enable diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d05f6ec..35b1769 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/desktop", - "version": "1.3.5", + "version": "1.3.6", "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 747ebae..745b471 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.5" -def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031005 +def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.6" +def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031006 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 9c745ae..faf7162 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 = 1031005; + CURRENT_PROJECT_VERSION = 1031006; 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.5; + MARKETING_VERSION = 1.3.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -287,14 +287,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 1031005; + CURRENT_PROJECT_VERSION = 1031006; INFOPLIST_FILE = D3ROVoice/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.5; + MARKETING_VERSION = 1.3.6; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt b/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt new file mode 100644 index 0000000..9435f6d --- /dev/null +++ b/apps/mobile-rn/metadata/android/en-US/changelogs/1031006.txt @@ -0,0 +1 @@ +Stability improvements. diff --git a/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt new file mode 100644 index 0000000..e80179f --- /dev/null +++ b/apps/mobile-rn/metadata/android/ko-KR/changelogs/1031006.txt @@ -0,0 +1 @@ +안정성을 개선했습니다. diff --git a/apps/mobile-rn/package-lock.json b/apps/mobile-rn/package-lock.json index cf44daf..351b628 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.5", + "version": "1.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@d3ro/mobile-rn", - "version": "1.3.5", + "version": "1.3.6", "dependencies": { "@d3ro/api-client": "file:../../packages/api-client", "@d3ro/core": "file:../../packages/core", @@ -62,7 +62,7 @@ }, "../..": { "name": "d3ro-voice-monorepo", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "workspaces": [ "apps/desktop", @@ -81,7 +81,7 @@ }, "../../packages/api-client": { "name": "@d3ro/api-client", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -98,7 +98,7 @@ }, "../../packages/core": { "name": "@d3ro/core", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "docx": "^9.6.1" @@ -109,7 +109,7 @@ }, "../../packages/i18n": { "name": "@d3ro/i18n", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "devDependencies": { "@types/react": "^19.0.0" @@ -120,7 +120,7 @@ }, "../../packages/ui-native": { "name": "@d3ro/ui-native", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "devDependencies": { "@types/react": "*" diff --git a/apps/mobile-rn/package.json b/apps/mobile-rn/package.json index e9ccec2..d18dab4 100644 --- a/apps/mobile-rn/package.json +++ b/apps/mobile-rn/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/mobile-rn", - "version": "1.3.5", + "version": "1.3.6", "private": true, "scripts": { "android": "react-native run-android", diff --git a/apps/web/package.json b/apps/web/package.json index a2ae0af..137143b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/web", - "version": "1.3.5", + "version": "1.3.6", "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 191b896..b678848 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.5 + v1.3.6 diff --git a/package-lock.json b/package-lock.json index 7170c3e..7ac4ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "d3ro-voice-monorepo", - "version": "1.3.5", + "version": "1.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "d3ro-voice-monorepo", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "workspaces": [ "apps/desktop", @@ -25,7 +25,7 @@ }, "apps/admin": { "name": "@d3ro/admin", - "version": "1.3.5", + "version": "1.3.6", "dependencies": { "@d3ro/api-client": "*", "@d3ro/core": "*", @@ -109,7 +109,7 @@ }, "apps/desktop": { "name": "@d3ro/desktop", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -158,7 +158,7 @@ }, "apps/web": { "name": "@d3ro/web", - "version": "1.3.5", + "version": "1.3.6", "dependencies": { "@d3ro/api-client": "*", "@d3ro/core": "*", @@ -16861,7 +16861,7 @@ }, "packages/api-client": { "name": "@d3ro/api-client", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "@d3ro/core": "*", @@ -16878,7 +16878,7 @@ }, "packages/core": { "name": "@d3ro/core", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "docx": "^9.6.1" @@ -16889,7 +16889,7 @@ }, "packages/i18n": { "name": "@d3ro/i18n", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "devDependencies": { "@types/react": "^19.0.0" @@ -16900,7 +16900,7 @@ }, "packages/ui": { "name": "@d3ro/ui", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "dependencies": { "@d3ro/core": "*" @@ -16920,7 +16920,7 @@ }, "packages/ui-native": { "name": "@d3ro/ui-native", - "version": "1.3.5", + "version": "1.3.6", "license": "MIT", "devDependencies": { "@types/react": "*" diff --git a/package.json b/package.json index 495dcfa..0a3e44d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "d3ro-voice-monorepo", - "version": "1.3.5", + "version": "1.3.6", "private": true, "description": "D3RO Voice — 멀티플랫폼 AI 음성 어시스턴트 (Monorepo)", "author": "D3RO", diff --git a/packages/api-client/package.json b/packages/api-client/package.json index e2c0fcb..b3c6e7c 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/api-client", - "version": "1.3.5", + "version": "1.3.6", "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 0f3d2fb..55b2f35 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/core", - "version": "1.3.5", + "version": "1.3.6", "private": true, "description": "D3RO Voice 공유 비즈니스 로직 — 타입, 에러, IPC 채널, 상수, 유틸", "license": "MIT", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 94b25ab..e7a4b7c 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/i18n", - "version": "1.3.5", + "version": "1.3.6", "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 c18e43f..8e1d456 100644 --- a/packages/ui-native/package.json +++ b/packages/ui-native/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/ui-native", - "version": "1.3.5", + "version": "1.3.6", "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 0f6a60f..63c0f40 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@d3ro/ui", - "version": "1.3.5", + "version": "1.3.6", "private": true, "description": "D3RO Voice 공유 UI — 디자인 시스템 컴포넌트 + 테마 토큰 + CSS 변수 맵", "license": "MIT", diff --git a/release/product-version.json b/release/product-version.json index 5b2648c..4008e30 100644 --- a/release/product-version.json +++ b/release/product-version.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "version": "1.3.5", - "androidVersionCode": 1031005, - "iosBuildNumber": 1031005, + "version": "1.3.6", + "androidVersionCode": 1031006, + "iosBuildNumber": 1031006, "releaseDate": "2026-09-18", "desktopLicensePublicKeyId": "5c52b765135ee2531c681b53cd4dc0e96fff8737f496c0b04ba8334fc81a887f" } diff --git a/site/package-lock.json b/site/package-lock.json index 8d40b37..e3778a9 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1,12 +1,12 @@ { "name": "d3ro-voice-site", - "version": "1.3.5", + "version": "1.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "d3ro-voice-site", - "version": "1.3.5", + "version": "1.3.6", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/site/package.json b/site/package.json index 9755ffc..2a62652 100644 --- a/site/package.json +++ b/site/package.json @@ -1,7 +1,7 @@ { "name": "d3ro-voice-site", "private": true, - "version": "1.3.5", + "version": "1.3.6", "type": "module", "scripts": { "dev": "vite --port 5199 --host",