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/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/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/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초 만에 성공. | --- 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",