d3ro-voice/src/main/bootstrap.ts
Yun Chan f131b581a9 Phase 3.5 구현: 커서 위치 히스토리 팝업 (D3RO 고유 기능)
- HistoryPopup: Vanilla JS, 다크 카드(#242427), 앰버 악센트(#f25b29)
- Ctrl+Shift+V → 커서 위치에 최근 10건 히스토리 팝업
- Arrow↑↓ 선택, Enter 붙여넣기, 1-9 직접 선택, ESC 닫기
- focusable: false → 활성 앱 포커스 유지
- 2-phase 리사이즈, 등장/퇴장 애니메이션
- WindowManager: HistoryPopup 관리 + 프리로딩
- Bootstrap: globalShortcut 등록 + IPC 연동
2026-04-05 02:37:35 +09:00

196 lines
5.7 KiB
TypeScript

// src/main/bootstrap.ts — 초기화 시퀀스
import { app, dialog, globalShortcut, ipcMain as ipcMainRef } from 'electron'
import { initLoggerService, getLogger } from './services/LoggerService'
import { initConfigService } from './services/ConfigService'
import { getHotkeyService } from './services/HotkeyService'
import { getVoiceModeService } from './services/VoiceModeService'
import { getLocalLLMService } from './services/LocalLLMService'
import { getHistoryService } from './services/HistoryService'
import { initDatabase } from './db'
import {
createMainWindow,
preloadPopupWindows,
showRecordingTip,
hideRecordingTip,
updateRecordingTipState,
sendAudioLevelToTip,
showResultPopup,
showHistoryPopup,
hideHistoryPopup,
sendKeyToHistoryPopup,
isHistoryPopupVisible
} from './windows/WindowManager'
import { createTray } from './windows/TrayManager'
import { registerAllIpcHandlers } from './ipc'
const logger = getLogger('bootstrap')
interface BootstrapStep {
name: string
critical: boolean
fn: () => Promise<void>
}
export async function bootstrap(): Promise<void> {
const steps: BootstrapStep[] = [
{ name: 'logger', critical: false, fn: initLogger },
{ name: 'config', critical: false, fn: initConfig },
{ name: 'database', critical: true, fn: initDB },
{ name: 'create-windows', critical: true, fn: createWindows },
{ name: 'tray', critical: false, fn: initTray },
{ name: 'ipc-handlers', critical: true, fn: initIpcHandlers },
{ name: 'popup-preload', critical: false, fn: initPopupWindows },
{ name: 'hotkey', critical: false, fn: initHotkey },
{ name: 'voice-mode', critical: false, fn: initVoiceMode },
{ name: 'llm-polling', critical: false, fn: initLLMPolling }
]
for (const step of steps) {
try {
await step.fn()
logger.info(`[bootstrap] ${step.name} initialized`)
} catch (error) {
logger.error(`[bootstrap] ${step.name} failed:`, error)
if (step.critical) {
dialog.showErrorBox(
'D3RO-VOICE 초기화 실패',
`${step.name}: ${error instanceof Error ? error.message : String(error)}`
)
app.quit()
return
}
}
}
}
async function initLogger(): Promise<void> {
initLoggerService()
}
async function initConfig(): Promise<void> {
initConfigService()
}
async function initDB(): Promise<void> {
initDatabase()
}
async function createWindows(): Promise<void> {
createMainWindow()
}
async function initTray(): Promise<void> {
createTray()
}
async function initIpcHandlers(): Promise<void> {
registerAllIpcHandlers()
}
async function initHotkey(): Promise<void> {
const hotkey = getHotkeyService()
hotkey.loadFromConfig()
hotkey.start()
}
async function initPopupWindows(): Promise<void> {
preloadPopupWindows()
setupHistoryPopupIPC()
// Ctrl+Shift+V → 히스토리 팝업 토글
globalShortcut.register('Ctrl+Shift+V', () => {
if (isHistoryPopupVisible()) {
hideHistoryPopup()
} else {
const entries = getHistoryService().list({ page: 0, pageSize: 10 }).entries
showHistoryPopup(entries as unknown as Array<Record<string, unknown>>)
}
})
}
async function initVoiceMode(): Promise<void> {
const voiceMode = getVoiceModeService()
voiceMode.connectHotkey()
// RecordingTip 연동: 세션 시작/종료 시 팝업 표시/숨김
voiceMode.on('session-started', () => {
showRecordingTip('recording')
})
voiceMode.on('audio-level', ({ level }) => {
sendAudioLevelToTip(level)
})
voiceMode.on('recognition-state-changed', ({ current }) => {
if (current === 'recognizing') {
updateRecordingTipState('thinking')
}
})
voiceMode.on('session-completed', ({ session, finalText }) => {
hideRecordingTip()
if (finalText.length > 0) {
showResultPopup(finalText)
}
// 이력 저장
try {
const wordCount = finalText.split(/\s+/).filter((w) => w.length > 0).length
getHistoryService().create({
originalText: session.transcription || finalText,
polishedText: session.processedText,
mode: session.mode === 'hands-free' ? 'dictation' : session.mode,
status: 'completed',
duration: (Date.now() - session.startedAt) / 1000,
wordCount
})
} catch (error) {
logger.warn(`Failed to save history: ${error instanceof Error ? error.message : String(error)}`)
}
})
voiceMode.on('session-cancelled', () => {
hideRecordingTip()
})
voiceMode.on('error', ({ error }) => {
updateRecordingTipState('error', { errorMessage: error.message })
setTimeout(() => hideRecordingTip(), 3000)
})
}
async function initLLMPolling(): Promise<void> {
const llm = getLocalLLMService()
llm.startPolling()
}
// ── HistoryPopup IPC 연동 ────────────────────────────
function setupHistoryPopupIPC(): void {
const { ipcMain } = require('electron')
const { getTextInsertService } = require('./services/TextInsertService')
// 히스토리 팝업 열기 (핫키에서 호출)
ipcMain.on('history:showPopup', () => {
const entries = getHistoryService().list({ page: 0, pageSize: 10 }).entries
showHistoryPopup(entries as unknown as Array<Record<string, unknown>>)
})
// 아이템 선택 → 텍스트 삽입
ipcMain.on('history:itemSelected', (_event: Electron.IpcMainEvent, data: { text: string }) => {
hideHistoryPopup()
setTimeout(async () => {
try {
await getTextInsertService().insertText(data.text)
} catch (error) {
logger.warn(`History popup insert failed: ${error instanceof Error ? error.message : String(error)}`)
}
}, 200)
})
// 팝업 닫기
ipcMain.on('history:popupDismissed', () => {
hideHistoryPopup()
})
}