Phase 5 구현: SQLite DB + History/Dictionary 서비스 + UI

- DB: better-sqlite3 + drizzle-orm (history/dictionary/stats, WAL 모드)
- HistoryService: CRUD + 검색 + 통계 + 30일 보존 정책, 세션 완료 시 자동 저장
- DictionaryService: CRUD + 검색 + 사용 횟수 추적 + STT 프롬프트 힌트
- HistoryPage: 목록 + 검색 + 삭제 + 복사 + 페이지네이션
- DictionaryPage: 목록 + 검색 + 추가 다이얼로그 + 삭제
- DashboardPage: 실데이터 통계 (총/오늘 세션, 시간, 단어수, 연속일수)
- IPC: history/dictionary/stats 핸들러 + preload API
- Bootstrap: DB 초기화 (critical) + 이력 자동 저장 연동
This commit is contained in:
Yun Chan 2026-04-05 02:32:53 +09:00
parent 4a5cf6c819
commit 291e2a29d0
17 changed files with 3111 additions and 35 deletions

83
src/main/db/schema.ts Normal file
View file

@ -0,0 +1,83 @@
// src/main/db/schema.ts
// 설계서 03의 drizzle-orm 스키마 정의
import { sqliteTable, text, integer, real, index, uniqueIndex } from 'drizzle-orm/sqlite-core'
// ── history ──────────────────────────────────────────────
export const history = sqliteTable(
'history',
{
id: text('id').primaryKey(),
originalText: text('original_text').notNull(),
polishedText: text('polished_text'),
focusedApp: text('focused_app'),
focusedAppName: text('focused_app_name'),
focusedAppWindowTitle: text('focused_app_window_title'),
mode: text('mode', { enum: ['dictation', 'translate', 'command'] })
.notNull()
.default('dictation'),
status: text('status', { enum: ['completed', 'cancelled', 'error'] })
.notNull()
.default('completed'),
errorCode: text('error_code'),
audioLocalPath: text('audio_local_path'),
duration: real('duration').notNull(),
detectedLanguage: text('detected_language'),
micDevice: text('mic_device'),
wordCount: integer('word_count').notNull().default(0),
sttModel: text('stt_model'),
llmModel: text('llm_model'),
sttLatencyMs: integer('stt_latency_ms'),
llmLatencyMs: integer('llm_latency_ms'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
appVersion: text('app_version').notNull().default('1.0.0')
},
(table) => [
index('idx_history_created_at').on(table.createdAt),
index('idx_history_status').on(table.status),
index('idx_history_detected_language').on(table.detectedLanguage),
index('idx_history_focused_app_name').on(table.focusedAppName),
index('idx_history_mode').on(table.mode)
]
)
// ── dictionary ───────────────────────────────────────────
export const dictionary = sqliteTable(
'dictionary',
{
id: text('id').primaryKey(),
word: text('word').notNull(),
pronunciation: text('pronunciation'),
category: text('category', { enum: ['user', 'auto', 'technical'] })
.notNull()
.default('user'),
usageCount: integer('usage_count').notNull().default(0),
lastUsedAt: integer('last_used_at'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull()
},
(table) => [
uniqueIndex('idx_dictionary_word_category').on(table.word, table.category),
index('idx_dictionary_created_at').on(table.createdAt),
index('idx_dictionary_usage_count').on(table.usageCount)
]
)
// ── stats ────────────────────────────────────────────────
export const stats = sqliteTable('stats', {
id: integer('id').primaryKey(),
totalDuration: real('total_duration').notNull().default(0),
totalWords: integer('total_words').notNull().default(0),
sessionCount: integer('session_count').notNull().default(0),
streakDays: integer('streak_days').notNull().default(0),
lastSessionAt: integer('last_session_at'),
lastUpdated: integer('last_updated').notNull()
})
// ── 타입 추출 ────────────────────────────────────────────
export type History = typeof history.$inferSelect
export type NewHistory = typeof history.$inferInsert
export type Dictionary = typeof dictionary.$inferSelect
export type NewDictionary = typeof dictionary.$inferInsert
export type Stats = typeof stats.$inferSelect