// 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 } export async function bootstrap(): Promise { 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 { initLoggerService() } async function initConfig(): Promise { initConfigService() } async function initDB(): Promise { initDatabase() } async function createWindows(): Promise { createMainWindow() } async function initTray(): Promise { createTray() } async function initIpcHandlers(): Promise { registerAllIpcHandlers() } async function initHotkey(): Promise { const hotkey = getHotkeyService() hotkey.loadFromConfig() hotkey.start() } async function initPopupWindows(): Promise { 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>) } }) } async function initVoiceMode(): Promise { 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 { 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>) }) // 아이템 선택 → 텍스트 삽입 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() }) }