From 239fb820b8b3cd37c1fc47fa0ad924cf462475e2 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 8 Apr 2026 11:54:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=83=80=EC=9D=B4=ED=8B=80=20+=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20UI=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전사 완료 후 LLM이 10단어 이내 제목 자동 생성 (fire-and-forget). DB에 title 컬럼 추가 + 마이그레이션. HistoryEntryCard: 제목(볼드, ellipsis) + 내용(2줄 미리보기) 분리. --- src/main/db/index.ts | 11 +++++ src/main/db/schema.ts | 1 + src/main/services/HistoryService.ts | 40 ++++++++++++++++++- .../components/shared/HistoryEntryCard.tsx | 16 ++++++++ src/shared/types.ts | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/main/db/index.ts b/src/main/db/index.ts index ba3ef89..29a26bf 100644 --- a/src/main/db/index.ts +++ b/src/main/db/index.ts @@ -191,6 +191,17 @@ export function initDatabase(): BetterSQLite3Database { 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 }> diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts index 586b629..dd29247 100644 --- a/src/main/db/schema.ts +++ b/src/main/db/schema.ts @@ -8,6 +8,7 @@ export const history = sqliteTable( 'history', { id: text('id').primaryKey(), + title: text('title'), originalText: text('original_text').notNull(), polishedText: text('polished_text'), focusedApp: text('focused_app'), diff --git a/src/main/services/HistoryService.ts b/src/main/services/HistoryService.ts index 29642e7..fe83bcc 100644 --- a/src/main/services/HistoryService.ts +++ b/src/main/services/HistoryService.ts @@ -36,6 +36,10 @@ class HistoryService { this._updateStats(input.duration, input.wordCount) logger.info(`History entry created: ${id}`) + + // 비동기로 LLM 타이틀 자동 생성 (fire-and-forget) + this.generateTitle(id).catch(() => { /* ignore */ }) + return this._toEntry(entry as History) } @@ -182,9 +186,42 @@ class HistoryService { .run() } + /** 전사 완료 후 LLM으로 자동 타이틀 생성 */ + async generateTitle(id: string): Promise { + const entry = this.getById(id) + if (!entry) return null + + const text = entry.polishedText || entry.originalText + if (!text || text.length < 10) return null + + try { + const { getLocalLLMService } = await import('./LocalLLMService') + const llm = getLocalLLMService() + const result = await llm.generate( + text.slice(0, 2000), + { + systemPrompt: '다음 텍스트의 핵심 내용을 10단어 이내의 짧은 제목으로 만들어주세요. 제목만 출력하세요. 따옴표나 마침표를 붙이지 마세요.', + temperature: 0.3, + maxTokens: 50, + }, + ) + const title = result.text.trim().replace(/^["']|["']$/g, '').slice(0, 80) + if (title) { + const db = getDatabase() + db.update(history).set({ title, updatedAt: Date.now() }).where(eq(history.id, id)).run() + logger.info(`Auto title generated: ${id} → "${title}"`) + return title + } + } catch (err) { + logger.warn(`Auto title generation failed: ${err instanceof Error ? err.message : String(err)}`) + } + return null + } + private _toEntry(row: History): HistoryEntry { return { id: row.id, + title: row.title ?? null, originalText: row.originalText, polishedText: row.polishedText, focusedApp: row.focusedApp, @@ -204,7 +241,8 @@ class HistoryService { llmLatencyMs: row.llmLatencyMs, createdAt: row.createdAt, updatedAt: row.updatedAt, - appVersion: row.appVersion + appVersion: row.appVersion, + summaryText: row.summaryText ?? null, } } } diff --git a/src/renderer/components/shared/HistoryEntryCard.tsx b/src/renderer/components/shared/HistoryEntryCard.tsx index 78efa50..51736f5 100644 --- a/src/renderer/components/shared/HistoryEntryCard.tsx +++ b/src/renderer/components/shared/HistoryEntryCard.tsx @@ -100,13 +100,29 @@ export function HistoryEntryCard({ entry, onCopy, onDelete, showTags = false, on + {/* 타이틀 */} + {entry.title ?? displayText.slice(0, 60)} + + {/* 내용 미리보기 */} +