feat(V2-1a): Monorepo 구조 전환 — apps/desktop으로 V1 이동

- npm workspaces 루트 (apps/*, packages/*) 세팅
- V1 전체를 apps/desktop/으로 git mv (src, resources, tests, sidecar,
  scripts, electron.vite.config.ts, electron-builder.yml, vitest.config.ts,
  tsconfig.node.json, tsconfig.web.json)
- apps/desktop/package.json 신규 (name=@d3ro/desktop)
- productName: 'd3ro-voice' 명시 — app.getName()을 고정하여 userData 경로
  %APPDATA%\d3ro-voice\ 그대로 유지 (기존 DB/설정 연속성 보장)
- 루트 package.json을 workspace 루트로 재구성, 공통 devDep만 유지
  (typescript, eslint, prettier)
- turbo.json, tsconfig.base.json 추가 (Turborepo 자체 설치는 별도 sub-phase)
- memory/project_status.md 생성 (규칙 13)

검증:
- npm run typecheck 통과
- npm run build 통과 (electron-vite main+preload+renderer)
- npm run dev 실제 실행 → DB/핫키/Ollama 자동 실행 모두 정상
This commit is contained in:
yunchan8804 2026-04-08 14:04:41 +09:00
parent 3a160b9032
commit 45a580878a
178 changed files with 214 additions and 0 deletions

View file

@ -1,237 +0,0 @@
// src/main/db/index.ts
// 설계서 03의 DB 초기화 코드
import Database from 'better-sqlite3'
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { app } from 'electron'
import path from 'path'
import * as schema from './schema'
import { getLogger } from '../services/LoggerService'
const logger = getLogger('database')
let db: BetterSQLite3Database<typeof schema> | null = null
let sqlite: Database.Database | null = null
export function initDatabase(): BetterSQLite3Database<typeof schema> {
const dbPath = path.join(app.getPath('userData'), 'd3ro-voice.db')
logger.info(`Initializing database at: ${dbPath}`)
sqlite = new Database(dbPath)
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')
sqlite.pragma('busy_timeout = 5000')
// 테이블 생성 (마이그레이션 대신 직접 생성)
sqlite.exec(`
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
original_text TEXT NOT NULL,
polished_text TEXT,
focused_app TEXT,
focused_app_name TEXT,
focused_app_window_title TEXT,
mode TEXT NOT NULL DEFAULT 'dictation',
status TEXT NOT NULL DEFAULT 'completed',
error_code TEXT,
audio_local_path TEXT,
duration REAL NOT NULL,
detected_language TEXT,
mic_device TEXT,
word_count INTEGER NOT NULL DEFAULT 0,
stt_model TEXT,
llm_model TEXT,
stt_latency_ms INTEGER,
llm_latency_ms INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
app_version TEXT NOT NULL DEFAULT '1.0.0'
);
CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_history_status ON history(status);
CREATE INDEX IF NOT EXISTS idx_history_detected_language ON history(detected_language);
CREATE INDEX IF NOT EXISTS idx_history_focused_app_name ON history(focused_app_name);
CREATE INDEX IF NOT EXISTS idx_history_mode ON history(mode);
CREATE TABLE IF NOT EXISTS dictionary (
id TEXT PRIMARY KEY,
word TEXT NOT NULL,
pronunciation TEXT,
category TEXT NOT NULL DEFAULT 'user',
usage_count INTEGER NOT NULL DEFAULT 0,
last_used_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_dictionary_word_category ON dictionary(word, category);
CREATE INDEX IF NOT EXISTS idx_dictionary_created_at ON dictionary(created_at);
CREATE INDEX IF NOT EXISTS idx_dictionary_usage_count ON dictionary(usage_count DESC);
CREATE TABLE IF NOT EXISTS stats (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_duration REAL NOT NULL DEFAULT 0,
total_words INTEGER NOT NULL DEFAULT 0,
session_count INTEGER NOT NULL DEFAULT 0,
streak_days INTEGER NOT NULL DEFAULT 0,
last_session_at INTEGER,
last_updated INTEGER NOT NULL
);
INSERT OR IGNORE INTO stats (id, total_duration, total_words, session_count, streak_days, last_updated)
VALUES (1, 0, 0, 0, 0, ${Date.now()});
CREATE TABLE IF NOT EXISTS memo_tags (
id TEXT PRIMARY KEY,
history_id TEXT NOT NULL,
tag TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_memo_tags_unique ON memo_tags(history_id, tag);
CREATE INDEX IF NOT EXISTS idx_memo_tags_history_id ON memo_tags(history_id);
CREATE INDEX IF NOT EXISTS idx_memo_tags_tag ON memo_tags(tag);
CREATE TABLE IF NOT EXISTS daily_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
feature TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_daily_usage_date_feature ON daily_usage(date, feature);
CREATE INDEX IF NOT EXISTS idx_daily_usage_date ON daily_usage(date);
`)
// Phase 13.2: RAG 테이블 생성
sqlite.exec(`
CREATE TABLE IF NOT EXISTS rag_documents (
id TEXT PRIMARY KEY,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_type TEXT NOT NULL,
chunk_count INTEGER NOT NULL DEFAULT 0,
indexed INTEGER NOT NULL DEFAULT 0,
indexed_at INTEGER,
added_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_rag_documents_added_at ON rag_documents(added_at);
CREATE TABLE IF NOT EXISTS rag_chunks (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding TEXT NOT NULL,
chunk_index INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_rag_chunks_document_id ON rag_chunks(document_id);
`)
// Phase 14: Meeting Mode 테이블 생성
sqlite.exec(`
CREATE TABLE IF NOT EXISTS meeting_sessions (
id TEXT PRIMARY KEY,
title TEXT,
status TEXT NOT NULL DEFAULT 'recording',
started_at INTEGER NOT NULL,
ended_at INTEGER,
duration_ms INTEGER,
raw_transcript TEXT,
minutes_markdown TEXT,
minutes_json TEXT,
stt_model TEXT,
llm_model TEXT,
stt_latency_ms INTEGER,
llm_latency_ms INTEGER,
error_message TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_sessions_created_at ON meeting_sessions(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_meeting_sessions_status ON meeting_sessions(status);
CREATE TABLE IF NOT EXISTS meeting_memos (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
content TEXT NOT NULL,
timestamp_ms INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_memos_session_id ON meeting_memos(session_id);
CREATE INDEX IF NOT EXISTS idx_meeting_memos_timestamp ON meeting_memos(timestamp_ms);
`)
// Phase 14.5: meeting_documents 테이블 + edited_transcript 컬럼
sqlite.exec(`
CREATE TABLE IF NOT EXISTS meeting_documents (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
template_type TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
prompt_used TEXT,
llm_model TEXT,
llm_latency_ms INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_documents_session_id ON meeting_documents(session_id);
`)
// Phase 14.5: meeting_sessions에 edited_transcript 컬럼 마이그레이션
try {
const msCols = sqlite.pragma('table_info(meeting_sessions)') as Array<{ name: string }>
if (!msCols.some((c) => c.name === 'edited_transcript')) {
sqlite.exec('ALTER TABLE meeting_sessions ADD COLUMN edited_transcript TEXT')
logger.info('Migrated: added edited_transcript column to meeting_sessions')
}
} catch (err) {
logger.warn('edited_transcript migration check failed:', err)
}
// Phase 15: history 테이블에 title 컬럼 마이그레이션
try {
const histCols = sqlite.pragma('table_info(history)') as Array<{ name: string }>
if (!histCols.some((c) => c.name === 'title')) {
sqlite.exec('ALTER TABLE history ADD COLUMN title TEXT')
logger.info('Migrated: added title column to history')
}
} catch (err) {
logger.warn('title migration check failed:', err)
}
// Phase 12.2: history 테이블에 summary_text 컬럼 마이그레이션
try {
const columns = sqlite.pragma('table_info(history)') as Array<{ name: string }>
const hasSummaryText = columns.some((c) => c.name === 'summary_text')
if (!hasSummaryText) {
sqlite.exec('ALTER TABLE history ADD COLUMN summary_text TEXT')
logger.info('Migrated: added summary_text column to history')
}
} catch (err) {
logger.warn('summary_text migration check failed:', err)
}
db = drizzle(sqlite, { schema })
logger.info('Database initialized')
return db
}
export function getDatabase(): BetterSQLite3Database<typeof schema> {
if (!db) {
throw new Error('Database not initialized. Call initDatabase() first.')
}
return db
}
export function closeDatabase(): void {
if (sqlite) {
sqlite.close()
sqlite = null
db = null
logger.info('Database closed')
}
}