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

@ -0,0 +1,101 @@
// src/main/ipc/audio-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getAudioCaptureService, calculateRMS } from '../services/AudioCaptureService'
import { getMainWindow } from '../windows/WindowManager'
import { configGet, configSet } from '../services/ConfigService'
import { getLogger } from '../services/LoggerService'
import type { SetDeviceParams } from '@shared/types'
const logger = getLogger('audio-handlers')
let testTimer: ReturnType<typeof setTimeout> | null = null
let testAudioHandler: ((payload: { buffer: Buffer; timestamp: number }) => void) | null = null
let testLevelInterval: ReturnType<typeof setInterval> | null = null
let testLastRms = 0
function stopAudioTest(): void {
const service = getAudioCaptureService()
if (testAudioHandler) {
service.off('audio-data', testAudioHandler)
testAudioHandler = null
}
if (testLevelInterval) {
clearInterval(testLevelInterval)
testLevelInterval = null
}
if (testTimer) {
clearTimeout(testTimer)
testTimer = null
}
testLastRms = 0
service.stop().catch(() => { /* ignore */ })
}
export function registerAudioHandlers(): void {
ipcMain.handle(IPC_CHANNELS.AUDIO.GET_DEVICES, async () => {
try {
const devices = await getAudioCaptureService().getDevices()
return ipcSuccess(devices)
} catch {
return ipcError(ErrorCode.AudioDeviceNotFound, 'Failed to enumerate audio devices')
}
})
ipcMain.handle(IPC_CHANNELS.AUDIO.GET_SELECTED_DEVICE, async () => {
return ipcSuccess(configGet('selectedDeviceId'))
})
ipcMain.handle(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, async (_event, params: SetDeviceParams) => {
try {
configSet('selectedDeviceId', params.deviceId)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.AudioDeviceNotFound, 'Failed to set device')
}
})
ipcMain.handle(IPC_CHANNELS.AUDIO.TEST_DEVICE, async () => {
try {
// 이전 테스트가 진행 중이면 정리
stopAudioTest()
const service = getAudioCaptureService()
await service.start()
// 오디오 데이터에서 RMS 추적
testAudioHandler = (payload: { buffer: Buffer; timestamp: number }) => {
testLastRms = calculateRMS(payload.buffer)
}
service.on('audio-data', testAudioHandler)
// 100ms 간격으로 레벨을 렌더러에 전송
testLevelInterval = setInterval(() => {
const level = testLastRms > 0 ? Math.min(1.0, Math.pow(testLastRms, 0.28)) : 0
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send('audio:testLevel', { level })
}
}, 100)
// 5초 후 자동 중지
testTimer = setTimeout(() => {
stopAudioTest()
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send('audio:testLevel', { level: 0 })
}
}, 5000)
return ipcSuccess({ averageLevel: 0, peakLevel: 0, hasAudio: true })
} catch (err) {
stopAudioTest()
return ipcError(
ErrorCode.AudioDeviceNotFound,
`Audio test failed: ${err instanceof Error ? err.message : String(err)}`,
)
}
})
}

View file

@ -0,0 +1,69 @@
// src/main/ipc/caption-handlers.ts
// Phase 10.1: Live Caption IPC 핸들러
import { ipcMain, session, desktopCapturer } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getCaptionService } from '../services/CaptionService'
import type { CaptionConfig } from '@shared/types'
export function registerCaptionHandlers(): void {
// 시스템 오디오 루프백: setDisplayMediaRequestHandler로 audio: 'loopback' 설정
ipcMain.handle('system-audio:enable-loopback', async () => {
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
const sources = await desktopCapturer.getSources({ types: ['screen'] })
if (sources.length === 0) {
callback({})
return
}
callback({ video: sources[0], audio: 'loopback' })
})
return ipcSuccess(undefined)
})
ipcMain.handle('system-audio:disable-loopback', async () => {
session.defaultSession.setDisplayMediaRequestHandler(null)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.CAPTION.START, async () => {
try {
await getCaptionService().start()
return ipcSuccess(undefined)
} catch (err) {
const code =
err instanceof Error && 'code' in err
? (err as { code: number }).code
: ErrorCode.CaptionStartFailed
const message = err instanceof Error ? err.message : String(err)
return ipcError(code, message)
}
})
ipcMain.handle(IPC_CHANNELS.CAPTION.STOP, async () => {
try {
await getCaptionService().stop()
return ipcSuccess(undefined)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.CaptionStartFailed, message)
}
})
ipcMain.handle(IPC_CHANNELS.CAPTION.GET_STATE, async () => {
return ipcSuccess(getCaptionService().getState())
})
ipcMain.handle(IPC_CHANNELS.CAPTION.SET_CONFIG, async (_event, config: Partial<CaptionConfig>) => {
getCaptionService().setConfig(config)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.CAPTION.GET_CONFIG, async () => {
return ipcSuccess(getCaptionService().getConfig())
})
// 렌더러 → 메인: 시스템 오디오 PCM 데이터 수신
ipcMain.on(IPC_CHANNELS.CAPTION.SYSTEM_AUDIO_DATA, (_event, data: ArrayBuffer) => {
getCaptionService().onSystemAudioData(Buffer.from(data))
})
}

View file

@ -0,0 +1,88 @@
// src/main/ipc/chain-handlers.ts
// Phase 10.4: Multi-LLM Chain IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode, D3ROError } from '@shared/errors'
import { getChainService } from '../services/ChainService'
import type {
CreateChainParams,
UpdateChainParams,
DeleteChainParams,
ExecuteChainParams
} from '@shared/types'
export function registerChainHandlers(): void {
ipcMain.handle(IPC_CHANNELS.CHAIN.GET_ALL, async () => {
return ipcSuccess(getChainService().getAll())
})
ipcMain.handle(
IPC_CHANNELS.CHAIN.CREATE,
async (_event, params: CreateChainParams) => {
try {
const result = getChainService().create(params)
return ipcSuccess(result)
} catch (err) {
return ipcError(
ErrorCode.ConfigWriteFailed,
`Failed to create chain: ${err instanceof Error ? err.message : String(err)}`
)
}
}
)
ipcMain.handle(
IPC_CHANNELS.CHAIN.UPDATE,
async (_event, params: UpdateChainParams) => {
try {
const result = getChainService().update(params)
return ipcSuccess(result)
} catch (err) {
if (err instanceof D3ROError && err.code === ErrorCode.ChainNotFound) {
return ipcError(ErrorCode.ChainNotFound, err.message)
}
return ipcError(
ErrorCode.ConfigWriteFailed,
`Failed to update chain: ${err instanceof Error ? err.message : String(err)}`
)
}
}
)
ipcMain.handle(
IPC_CHANNELS.CHAIN.DELETE,
async (_event, params: DeleteChainParams) => {
try {
getChainService().delete(params.id)
return ipcSuccess(undefined)
} catch (err) {
if (err instanceof D3ROError && err.code === ErrorCode.ChainNotFound) {
return ipcError(ErrorCode.ChainNotFound, err.message)
}
return ipcError(
ErrorCode.ConfigWriteFailed,
`Failed to delete chain: ${err instanceof Error ? err.message : String(err)}`
)
}
}
)
ipcMain.handle(
IPC_CHANNELS.CHAIN.EXECUTE,
async (_event, params: ExecuteChainParams) => {
try {
const result = await getChainService().execute(params.chainId, params.text)
return ipcSuccess(result)
} catch (err) {
if (err instanceof D3ROError) {
return ipcError(err.code, err.message)
}
return ipcError(
ErrorCode.ChainExecutionFailed,
`Chain execution failed: ${err instanceof Error ? err.message : String(err)}`
)
}
}
)
}

View file

@ -0,0 +1,98 @@
// src/main/ipc/config-handlers.ts
import { ipcMain, BrowserWindow } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { configGet, configSet, configGetAll, configReset } from '../services/ConfigService'
import { getAutoLaunchService } from '../services/AutoLaunchService'
import { reapplyThemeToAllPopups } from '../windows/WindowManager'
/** 설정 변경 시 모든 렌더러에 broadcast */
function broadcastConfigChanged(key: string, value: unknown, previousValue?: unknown): void {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(IPC_CHANNELS.CONFIG.CHANGED, { key, value, previousValue })
}
}
import type {
ConfigGetParams,
ConfigSetParams,
ConfigResetParams,
SetThemeParams,
SetLanguageParams,
SetAutoLaunchParams,
SetCloseToTrayParams,
AppConfig
} from '@shared/types'
export function registerConfigHandlers(): void {
ipcMain.handle(IPC_CHANNELS.CONFIG.GET, async (_event, params: ConfigGetParams) => {
try {
return ipcSuccess(configGet(params.key))
} catch {
return ipcError(ErrorCode.ConfigReadFailed, `Failed to read config key: ${params.key}`)
}
})
ipcMain.handle(IPC_CHANNELS.CONFIG.SET, async (_event, params: ConfigSetParams) => {
try {
const prev = configGet(params.key)
configSet(params.key, params.value as AppConfig[typeof params.key])
broadcastConfigChanged(params.key, params.value, prev)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.ConfigWriteFailed, `Failed to write config key: ${params.key}`)
}
})
ipcMain.handle(IPC_CHANNELS.CONFIG.GET_ALL, async () => {
return ipcSuccess(configGetAll())
})
ipcMain.handle(IPC_CHANNELS.CONFIG.RESET, async (_event, params: ConfigResetParams) => {
try {
configReset(params.key)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.ConfigResetFailed, 'Failed to reset config')
}
})
ipcMain.handle(IPC_CHANNELS.CONFIG.GET_THEME, async () => {
return ipcSuccess(configGet('theme'))
})
ipcMain.handle(IPC_CHANNELS.CONFIG.SET_THEME, async (_event, params: SetThemeParams) => {
const prev = configGet('theme')
configSet('theme', params.theme)
broadcastConfigChanged('theme', params.theme, prev)
reapplyThemeToAllPopups()
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.CONFIG.GET_LANGUAGE, async () => {
return ipcSuccess(configGet('language'))
})
ipcMain.handle(IPC_CHANNELS.CONFIG.SET_LANGUAGE, async (_event, params: SetLanguageParams) => {
configSet('language', params.language)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.CONFIG.GET_AUTO_LAUNCH, async () => {
return ipcSuccess(configGet('autoLaunch'))
})
ipcMain.handle(IPC_CHANNELS.CONFIG.SET_AUTO_LAUNCH, async (_event, params: SetAutoLaunchParams) => {
getAutoLaunchService().setEnabled(params.enabled)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.CONFIG.GET_CLOSE_TO_TRAY, async () => {
return ipcSuccess(configGet('closeToTray'))
})
ipcMain.handle(IPC_CHANNELS.CONFIG.SET_CLOSE_TO_TRAY, async (_event, params: SetCloseToTrayParams) => {
configSet('closeToTray', params.enabled)
return ipcSuccess(undefined)
})
}

View file

@ -0,0 +1,47 @@
// src/main/ipc/context-handlers.ts
// Phase 10.2 스크린 컨텍스트 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getScreenContextService } from '../services/ScreenContextService'
export function registerContextHandlers(): void {
// context:capture — 활성 윈도우 + 선택 텍스트 캡처
ipcMain.handle(
IPC_CHANNELS.CONTEXT.CAPTURE,
async (_event, params?: { captureSelectedText?: boolean }) => {
try {
const service = getScreenContextService()
const captureSelectedText = params?.captureSelectedText ?? true
const result = await service.captureContext(captureSelectedText)
return ipcSuccess(result)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return ipcError(ErrorCode.ContextCaptureFailed, message)
}
}
)
// context:isEnabled — 스크린 컨텍스트 활성화 여부
ipcMain.handle(IPC_CHANNELS.CONTEXT.IS_ENABLED, async () => {
return ipcSuccess(getScreenContextService().isEnabled())
})
// context:setEnabled — 스크린 컨텍스트 활성화/비활성화
ipcMain.handle(
IPC_CHANNELS.CONTEXT.SET_ENABLED,
async (_event, params: { enabled: boolean }) => {
getScreenContextService().setEnabled(params.enabled)
return ipcSuccess(undefined)
}
)
// context:getConfig — 현재 설정 조회 (향후 앱별 설정 확장용)
ipcMain.handle(IPC_CHANNELS.CONTEXT.GET_CONFIG, async () => {
const service = getScreenContextService()
return ipcSuccess({
enabled: service.isEnabled(),
})
})
}

View file

@ -0,0 +1,56 @@
// src/main/ipc/dictionary-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getDictionaryService } from '../services/DictionaryService'
import type {
DictionaryQueryParams,
DictionaryAddParams,
DictionaryUpdateParams,
DictionaryDeleteParams,
DictionarySearchParams
} from '@shared/types'
export function registerDictionaryHandlers(): void {
ipcMain.handle(IPC_CHANNELS.DICTIONARY.GET_ALL, async (_event, params: DictionaryQueryParams) => {
try {
return ipcSuccess(getDictionaryService().list(params))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get dictionary')
}
})
ipcMain.handle(IPC_CHANNELS.DICTIONARY.ADD, async (_event, params: DictionaryAddParams) => {
try {
return ipcSuccess(getDictionaryService().add(params))
} catch {
return ipcError(ErrorCode.DBWriteFailed, 'Failed to add dictionary entry')
}
})
ipcMain.handle(IPC_CHANNELS.DICTIONARY.UPDATE, async (_event, params: DictionaryUpdateParams) => {
try {
return ipcSuccess(getDictionaryService().update(params))
} catch {
return ipcError(ErrorCode.DBWriteFailed, 'Failed to update dictionary entry')
}
})
ipcMain.handle(IPC_CHANNELS.DICTIONARY.DELETE, async (_event, params: DictionaryDeleteParams) => {
try {
getDictionaryService().delete(params.id)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete dictionary entry')
}
})
ipcMain.handle(IPC_CHANNELS.DICTIONARY.SEARCH, async (_event, params: DictionarySearchParams) => {
try {
return ipcSuccess(getDictionaryService().search(params))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to search dictionary')
}
})
}

View file

@ -0,0 +1,69 @@
// src/main/ipc/file-transcription-handlers.ts
// Phase 12.1: 파일 전사 IPC 핸들러
import { ipcMain, dialog } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getFileTranscriptionService } from '../services/FileTranscriptionService'
import { getLogger } from '../services/LoggerService'
import type { FileTranscriptionStartParams } from '@shared/types'
const logger = getLogger('file-transcription-handlers')
export function registerFileTranscriptionHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.FILE_TRANSCRIPTION.START,
async (_event, params: FileTranscriptionStartParams) => {
try {
let filePath = params.filePath
// filePath가 없으면 파일 선택 다이얼로그
if (!filePath) {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{
name: 'Audio/Video',
extensions: [
'mp3', 'wav', 'm4a', 'ogg', 'flac', 'wma', 'aac',
'mp4', 'mkv', 'webm', 'avi', 'mov',
],
},
],
})
if (result.canceled || result.filePaths.length === 0) {
return ipcError(ErrorCode.FileTranscriptionCancelled, 'File selection cancelled')
}
filePath = result.filePaths[0]
}
const service = getFileTranscriptionService()
const resultData = await service.startTranscription(filePath, params.language)
return ipcSuccess(resultData)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('File transcription failed:', msg)
return ipcError(ErrorCode.FileTranscriptionChunkFailed, msg)
}
},
)
ipcMain.handle(IPC_CHANNELS.FILE_TRANSCRIPTION.CANCEL, async () => {
try {
getFileTranscriptionService().cancel()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.FileTranscriptionCancelled, msg)
}
})
ipcMain.handle(IPC_CHANNELS.FILE_TRANSCRIPTION.GET_STATE, async () => {
try {
return ipcSuccess(getFileTranscriptionService().getStateInfo())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
}

View file

@ -0,0 +1,64 @@
// src/main/ipc/history-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getHistoryService } from '../services/HistoryService'
import type {
HistoryQueryParams,
HistoryGetByIdParams,
HistoryDeleteParams,
HistorySearchParams
} from '@shared/types'
export function registerHistoryHandlers(): void {
ipcMain.handle(IPC_CHANNELS.HISTORY.GET_ALL, async (_event, params: HistoryQueryParams) => {
try {
return ipcSuccess(getHistoryService().list(params))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get history')
}
})
ipcMain.handle(IPC_CHANNELS.HISTORY.GET_BY_ID, async (_event, params: HistoryGetByIdParams) => {
try {
return ipcSuccess(getHistoryService().getById(params.id))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get history entry')
}
})
ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE, async (_event, params: HistoryDeleteParams) => {
try {
getHistoryService().delete(params.id)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete history entry')
}
})
ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE_ALL, async () => {
try {
getHistoryService().deleteAll()
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.DBWriteFailed, 'Failed to delete all history')
}
})
ipcMain.handle(IPC_CHANNELS.HISTORY.SEARCH, async (_event, params: HistorySearchParams) => {
try {
return ipcSuccess(getHistoryService().search(params))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to search history')
}
})
ipcMain.handle(IPC_CHANNELS.STATS.GET_SUMMARY, async () => {
try {
return ipcSuccess(getHistoryService().getStats())
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get stats')
}
})
}

View file

@ -0,0 +1,81 @@
// src/main/ipc/hotkey-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getHotkeyService } from '../services/HotkeyService'
import { configGet, configSet } from '../services/ConfigService'
import type { SetHotkeyParams, SetEnabledParams } from '@shared/types'
export function registerHotkeyHandlers(): void {
ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_DICTATION_SHORTCUT, async () => {
return ipcSuccess(configGet('dictationShortcut'))
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_DICTATION_SHORTCUT, async (_event, params: SetHotkeyParams) => {
try {
configSet('dictationShortcut', params.binding)
getHotkeyService().loadFromConfig()
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set dictation shortcut')
}
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_HANDS_FREE_SHORTCUT, async () => {
return ipcSuccess(configGet('handsFreeShortcut'))
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_HANDS_FREE_SHORTCUT, async (_event, params: SetHotkeyParams) => {
try {
configSet('handsFreeShortcut', params.binding)
getHotkeyService().loadFromConfig()
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set hands-free shortcut')
}
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_COMMAND_SHORTCUT, async () => {
return ipcSuccess(configGet('commandShortcut'))
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_COMMAND_SHORTCUT, async (_event, params: SetHotkeyParams) => {
try {
configSet('commandShortcut', params.binding)
getHotkeyService().loadFromConfig()
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set command shortcut')
}
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.GET_CAPTION_SHORTCUT, async () => {
return ipcSuccess(configGet('captionShortcut'))
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_CAPTION_SHORTCUT, async (_event, params: SetHotkeyParams) => {
try {
configSet('captionShortcut', params.binding)
getHotkeyService().loadFromConfig()
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.HotkeyRegistrationFailed, 'Failed to set caption shortcut')
}
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.IS_ENABLED, async () => {
return ipcSuccess(configGet('hotkeyEnabled'))
})
ipcMain.handle(IPC_CHANNELS.HOTKEY.SET_ENABLED, async (_event, params: SetEnabledParams) => {
configSet('hotkeyEnabled', params.enabled)
const hotkey = getHotkeyService()
if (params.enabled) {
hotkey.start()
} else {
hotkey.stop()
}
return ipcSuccess(undefined)
})
}

View file

@ -0,0 +1,59 @@
// src/main/ipc/index.ts — IPC 핸들러 일괄 등록
import { registerAudioHandlers } from './audio-handlers'
import { registerConfigHandlers } from './config-handlers'
import { registerWindowHandlers } from './window-handlers'
import { registerSystemHandlers } from './system-handlers'
import { registerVoiceHandlers } from './voice-handlers'
import { registerSTTHandlers } from './stt-handlers'
import { registerHotkeyHandlers } from './hotkey-handlers'
import { registerLLMHandlers } from './llm-handlers'
import { registerHistoryHandlers } from './history-handlers'
import { registerDictionaryHandlers } from './dictionary-handlers'
import { registerInstructionHandlers } from './instruction-handlers'
import { registerMemoHandlers } from './memo-handlers'
import { registerVoiceCommandHandlers } from './voice-command-handlers'
import { registerContextHandlers } from './context-handlers'
import { registerCaptionHandlers } from './caption-handlers'
import { registerChainHandlers } from './chain-handlers'
import { registerLicenseHandlers } from './license-handlers'
import { registerFileTranscriptionHandlers } from './file-transcription-handlers'
import { registerMeetingSummaryHandlers } from './meeting-summary-handlers'
import { registerTemplateHandlers } from './template-handlers'
import { registerVoiceConversationHandlers } from './voice-conversation-handlers'
import { registerRAGHandlers } from './rag-handlers'
import { registerVoiceActionHandlers } from './voice-action-handlers'
import { registerMeetingModeHandlers } from './meeting-mode-handlers'
import { registerMeetingDocTemplateHandlers } from './meeting-doc-template-handlers'
import { getLogger } from '../services/LoggerService'
const logger = getLogger('ipc')
export function registerAllIpcHandlers(): void {
registerAudioHandlers()
registerConfigHandlers()
registerWindowHandlers()
registerSystemHandlers()
registerVoiceHandlers()
registerSTTHandlers()
registerHotkeyHandlers()
registerLLMHandlers()
registerHistoryHandlers()
registerDictionaryHandlers()
registerInstructionHandlers()
registerMemoHandlers()
registerVoiceCommandHandlers()
registerContextHandlers()
registerCaptionHandlers()
registerChainHandlers()
registerLicenseHandlers()
registerFileTranscriptionHandlers()
registerMeetingSummaryHandlers()
registerTemplateHandlers()
registerVoiceConversationHandlers()
registerRAGHandlers()
registerVoiceActionHandlers()
registerMeetingModeHandlers()
registerMeetingDocTemplateHandlers()
logger.info('All IPC handlers registered')
}

View file

@ -0,0 +1,67 @@
// src/main/ipc/instruction-handlers.ts
import { ipcMain } from 'electron'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getCustomInstructionService } from '../services/CustomInstructionService'
import type { CustomInstruction } from '../services/CustomInstructionService'
// IPC_CHANNELS에 instruction 채널이 없으므로 직접 문자열 사용
// (Phase 6 전용, 설계서 02에는 미포함)
const CHANNELS = {
GET_ALL: 'instruction:getAll',
GET_BY_ID: 'instruction:getById',
CREATE: 'instruction:create',
UPDATE: 'instruction:update',
DELETE: 'instruction:delete',
REORDER: 'instruction:reorder'
} as const
export function registerInstructionHandlers(): void {
ipcMain.handle(CHANNELS.GET_ALL, async () => {
return ipcSuccess(getCustomInstructionService().getAll())
})
ipcMain.handle(CHANNELS.GET_BY_ID, async (_event, params: { id: string }) => {
return ipcSuccess(getCustomInstructionService().getById(params.id))
})
ipcMain.handle(
CHANNELS.CREATE,
async (
_event,
params: { name: string; description: string; prompt: string; icon?: string }
) => {
try {
const result = getCustomInstructionService().create(params)
return ipcSuccess(result)
} catch {
return ipcError(ErrorCode.ConfigWriteFailed, 'Failed to create instruction')
}
}
)
ipcMain.handle(
CHANNELS.UPDATE,
async (_event, params: { id: string; data: Partial<CustomInstruction> }) => {
try {
const result = getCustomInstructionService().update(params.id, params.data)
return ipcSuccess(result)
} catch {
return ipcError(ErrorCode.ConfigWriteFailed, 'Failed to update instruction')
}
}
)
ipcMain.handle(CHANNELS.DELETE, async (_event, params: { id: string }) => {
const result = getCustomInstructionService().delete(params.id)
if (!result) {
return ipcError(ErrorCode.ConfigWriteFailed, 'Cannot delete builtin instruction')
}
return ipcSuccess(undefined)
})
ipcMain.handle(CHANNELS.REORDER, async (_event, params: { ids: string[] }) => {
getCustomInstructionService().reorder(params.ids)
return ipcSuccess(undefined)
})
}

View file

@ -0,0 +1,100 @@
// src/main/ipc/license-handlers.ts
// Phase 11: 라이센스 IPC 핸들러
import { ipcMain, BrowserWindow } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getLicenseService } from '../services/LicenseService'
import type {
ActivateLicenseParams,
UpgradePromptEvent,
LicenseInfo,
} from '@shared/types'
import { Feature } from '@shared/types'
/** 업그레이드 유도 이벤트를 모든 렌더러에 broadcast */
function broadcastUpgradePrompt(event: UpgradePromptEvent): void {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(IPC_CHANNELS.LICENSE.UPGRADE_PROMPT, event)
}
}
/** 티어 변경을 모든 렌더러에 broadcast */
function broadcastTierChanged(info: LicenseInfo): void {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(IPC_CHANNELS.LICENSE.TIER_CHANGED, info)
}
}
export function registerLicenseHandlers(): void {
const license = getLicenseService()
// 이벤트 -> 렌더러 전파
license.on('upgrade-prompt', (event: UpgradePromptEvent) => {
broadcastUpgradePrompt(event)
})
license.on('tier-changed', (info: LicenseInfo) => {
broadcastTierChanged(info)
})
// ── handle 채널 ──
ipcMain.handle(IPC_CHANNELS.LICENSE.GET_INFO, async () => {
try {
return ipcSuccess(license.getInfo())
} catch (err) {
return ipcError(ErrorCode.UnknownError, `Failed to get license info: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.ACTIVATE, async (_event, params: ActivateLicenseParams) => {
try {
const result = await license.activate(params.licenseKey)
return ipcSuccess(result)
} catch (err) {
return ipcError(ErrorCode.LicenseActivationFailed, `Activation failed: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.DEACTIVATE, async () => {
try {
await license.deactivate()
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.LicenseDeactivationFailed, `Deactivation failed: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.CHECK_FEATURE, async (_event, params: { feature: Feature }) => {
try {
return ipcSuccess(license.canUse(params.feature))
} catch (err) {
return ipcError(ErrorCode.UnknownError, `Feature check failed: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.GET_USAGE, async (_event, params: { feature: Feature }) => {
try {
return ipcSuccess(license.getUsage(params.feature))
} catch (err) {
return ipcError(ErrorCode.UnknownError, `Usage query failed: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.GET_ALL_USAGE, async () => {
try {
return ipcSuccess(license.getAllUsage())
} catch (err) {
return ipcError(ErrorCode.UnknownError, `Usage query failed: ${err}`)
}
})
ipcMain.handle(IPC_CHANNELS.LICENSE.GET_TIER_COMPARISON, async () => {
try {
return ipcSuccess(license.getTierComparison())
} catch (err) {
return ipcError(ErrorCode.UnknownError, `Tier comparison failed: ${err}`)
}
})
}

View file

@ -0,0 +1,77 @@
// src/main/ipc/llm-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getLocalLLMService } from '../services/LocalLLMService'
import { configGet, configSet } from '../services/ConfigService'
import { getMainWindow } from '../windows/WindowManager'
import type { SetLLMModelParams, SetServerUrlParams, LLMProcessParams } from '@shared/types'
export function registerLLMHandlers(): void {
// LLM 가용성 변경 시 렌더러에 상태 전파
const llm = getLocalLLMService()
llm.on('availability-changed', () => {
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send(IPC_CHANNELS.LLM.STATUS_CHANGED, { status: llm.getStatus() })
}
})
ipcMain.handle(IPC_CHANNELS.LLM.GET_STATUS, async () => {
return ipcSuccess(getLocalLLMService().getStatus())
})
ipcMain.handle(IPC_CHANNELS.LLM.GET_MODELS, async () => {
try {
const models = await getLocalLLMService().getModels()
return ipcSuccess(models)
} catch {
return ipcError(ErrorCode.LLMServerUnreachable, 'Failed to get models')
}
})
ipcMain.handle(IPC_CHANNELS.LLM.GET_ACTIVE_MODEL, async () => {
return ipcSuccess(configGet('llmModelId'))
})
ipcMain.handle(IPC_CHANNELS.LLM.SET_MODEL, async (_event, params: SetLLMModelParams) => {
configSet('llmModelId', params.modelId)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.LLM.PROCESS, async (_event, params: LLMProcessParams) => {
try {
const llm = getLocalLLMService()
const start = performance.now()
const processedText = await llm.processText(
params.text,
params.action,
params.targetLanguage,
params.customPrompt
)
return ipcSuccess({
originalText: params.text,
processedText,
action: params.action,
processingTimeMs: Math.round(performance.now() - start),
tokenCount: 0
})
} catch {
return ipcError(ErrorCode.LLMProcessingFailed, 'LLM processing failed')
}
})
ipcMain.handle(IPC_CHANNELS.LLM.CANCEL_PROCESS, async () => {
getLocalLLMService().cancelGeneration()
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.LLM.GET_SERVER_URL, async () => {
return ipcSuccess(configGet('ollamaServerUrl'))
})
ipcMain.handle(IPC_CHANNELS.LLM.SET_SERVER_URL, async (_event, params: SetServerUrlParams) => {
configSet('ollamaServerUrl', params.url)
return ipcSuccess(undefined)
})
}

View file

@ -0,0 +1,63 @@
// src/main/ipc/meeting-doc-template-handlers.ts
// Phase 14.5: Meeting Document Template IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getMeetingDocTemplateService } from '../services/MeetingDocTemplateService'
import { getLogger } from '../services/LoggerService'
import type {
CreateMeetingDocTemplateParams,
UpdateMeetingDocTemplateParams,
DeleteMeetingDocTemplateParams,
} from '@shared/types'
const logger = getLogger('meeting-doc-template-handlers')
export function registerMeetingDocTemplateHandlers(): void {
const ch = IPC_CHANNELS.MEETING_DOC_TEMPLATE
ipcMain.handle(ch.GET_ALL, () => {
try {
return ipcSuccess(getMeetingDocTemplateService().getAll())
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.CREATE, (_event, params: CreateMeetingDocTemplateParams) => {
try {
const result = getMeetingDocTemplateService().create(params)
return ipcSuccess(result)
} catch (err) {
logger.error(`meetingDocTemplate create 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.UPDATE, (_event, params: UpdateMeetingDocTemplateParams) => {
try {
const result = getMeetingDocTemplateService().update(params)
return ipcSuccess(result)
} catch (err) {
return ipcError(
ErrorCode.MeetingDocTemplateNotFound,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.DELETE, (_event, params: DeleteMeetingDocTemplateParams) => {
try {
getMeetingDocTemplateService().delete(params.id)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(
ErrorCode.MeetingDocTemplateNotFound,
err instanceof Error ? err.message : String(err),
)
}
})
logger.info('Meeting Doc Template IPC handlers registered')
}

View file

@ -0,0 +1,266 @@
// src/main/ipc/meeting-mode-handlers.ts
// Phase 14 / 14.5: Meeting Mode IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getMeetingModeService } from '../services/MeetingModeService'
import { getLogger } from '../services/LoggerService'
import type {
MeetingAddMemoParams,
MeetingGetSessionsParams,
MeetingGetSessionParams,
MeetingDeleteSessionParams,
MeetingExportParams,
MeetingGenerateDocParams,
MeetingExportFormat,
MeetingChatSendParams,
} from '@shared/types'
const logger = getLogger('meeting-mode-handlers')
export function registerMeetingModeHandlers(): void {
const ch = IPC_CHANNELS.MEETING_MODE
ipcMain.handle(ch.START_RECORDING, async () => {
try {
const result = await getMeetingModeService().startRecording()
return ipcSuccess(result)
} catch (err) {
logger.error(`startRecording 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingAlreadyRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.STOP_RECORDING, async () => {
try {
await getMeetingModeService().stopRecording()
return ipcSuccess(undefined)
} catch (err) {
logger.error(`stopRecording 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingNotRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.ADD_MEMO, async (_event, params: MeetingAddMemoParams) => {
try {
const memo = await getMeetingModeService().addMemo(params.content)
return ipcSuccess(memo)
} catch (err) {
logger.error(`addMemo 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingNotRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_STATE, () => {
try {
return ipcSuccess(getMeetingModeService().getStateInfo())
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_SESSIONS, (_event, params: MeetingGetSessionsParams) => {
try {
return ipcSuccess(getMeetingModeService().getSessions(params.page, params.pageSize))
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_SESSION, (_event, params: MeetingGetSessionParams) => {
try {
logger.info(`getSession 호출: ${params.sessionId}`)
const result = getMeetingModeService().getSession(params.sessionId)
logger.info(`getSession 성공: documents=${result.documents.length}`)
return ipcSuccess(result)
} catch (err) {
logger.error(`getSession 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.DELETE_SESSION, (_event, params: MeetingDeleteSessionParams) => {
try {
getMeetingModeService().deleteSession(params.sessionId)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.UPDATE_TITLE, (_event, params: { sessionId: string; title: string }) => {
try {
getMeetingModeService().updateTitle(params.sessionId, params.title)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.EXPORT_PDF, async (_event, params: MeetingExportParams) => {
try {
const filePath = await getMeetingModeService().exportPdf(params.sessionId)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportPdf 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingPdfFailed, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.EXPORT_MARKDOWN, async (_event, params: MeetingExportParams) => {
try {
const filePath = await getMeetingModeService().exportMarkdown(params.sessionId)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportMarkdown 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingExportFailed, err instanceof Error ? err.message : String(err))
}
})
// ── Phase 14.5 핸들러 ──
ipcMain.handle(ch.UPDATE_TRANSCRIPT, (_event, params: { sessionId: string; editedTranscript: string }) => {
try {
getMeetingModeService().updateTranscript(params.sessionId, params.editedTranscript)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(
ErrorCode.MeetingTranscriptUpdateFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.GENERATE_DOCUMENT, async (_event, params: MeetingGenerateDocParams) => {
try {
const doc = await getMeetingModeService().generateDocument(params)
return ipcSuccess(doc)
} catch (err) {
logger.error(`generateDocument 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingDocGenerationFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.GET_DOCUMENTS, (_event, params: { sessionId: string }) => {
try {
return ipcSuccess(getMeetingModeService().getDocuments(params.sessionId))
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.UPDATE_DOCUMENT, (_event, params: { documentId: string; content: string }) => {
try {
getMeetingModeService().updateDocument(params.documentId, params.content)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(
ErrorCode.MeetingDocumentNotFound,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.DELETE_DOCUMENT, (_event, params: { documentId: string }) => {
try {
getMeetingModeService().deleteDocument(params.documentId)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(
ErrorCode.MeetingDocumentNotFound,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.EXPORT_DOCUMENT, async (_event, params: { documentId: string; format: MeetingExportFormat }) => {
try {
const filePath = await getMeetingModeService().exportDocument(params.documentId, params.format)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportDocument 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingDocExportFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(ch.EXPORT_TRANSCRIPT, async (_event, params: { sessionId: string }) => {
try {
const filePath = await getMeetingModeService().exportTranscript(params.sessionId)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportTranscript 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingExportFailed,
err instanceof Error ? err.message : String(err),
)
}
})
// ── Phase 15 핸들러 ──
ipcMain.handle(ch.POLISH_TRANSCRIPT, async (_event, params: { sessionId: string }) => {
try {
const result = await getMeetingModeService().polishTranscript(params.sessionId)
return ipcSuccess(result)
} catch (err) {
logger.error(`polishTranscript 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingPolishFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.SEND, async (_event, params: MeetingChatSendParams) => {
try {
await getMeetingModeService().chatWithMeeting(params.sessionId, params.message)
return ipcSuccess(undefined)
} catch (err) {
logger.error(`meetingChat:send 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.MeetingChatFailed,
err instanceof Error ? err.message : String(err),
)
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.CANCEL, () => {
try {
getMeetingModeService().cancelMeetingChat()
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(IPC_CHANNELS.MEETING_CHAT.CLEAR, () => {
try {
getMeetingModeService().clearMeetingChatHistory()
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.DIARIZE, async (_event, params: { sessionId: string; numSpeakers?: number }) => {
try {
await getMeetingModeService().diarizeSession(params.sessionId, params.numSpeakers)
return ipcSuccess(undefined)
} catch (err) {
logger.error(`diarize 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(
ErrorCode.DiarizationFailed,
err instanceof Error ? err.message : String(err),
)
}
})
logger.info('Meeting Mode IPC handlers registered')
}

View file

@ -0,0 +1,54 @@
// src/main/ipc/meeting-summary-handlers.ts
// Phase 12.2: 회의록 요약 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getMeetingSummaryService } from '../services/MeetingSummaryService'
import { getLogger } from '../services/LoggerService'
import type { MeetingSummarizeParams, MeetingSummaryGetParams, MeetingSummaryExportParams } from '@shared/types'
const logger = getLogger('meeting-summary-handlers')
export function registerMeetingSummaryHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.MEETING_SUMMARY.SUMMARIZE,
async (_event, params: MeetingSummarizeParams) => {
try {
const result = await getMeetingSummaryService().summarize(params.historyId)
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Meeting summary failed:', msg)
return ipcError(ErrorCode.MeetingSummaryGenerationFailed, msg)
}
},
)
ipcMain.handle(
IPC_CHANNELS.MEETING_SUMMARY.GET_SUMMARY,
async (_event, params: MeetingSummaryGetParams) => {
try {
const result = getMeetingSummaryService().getSummary(params.historyId)
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.MeetingSummaryGenerationFailed, msg)
}
},
)
ipcMain.handle(
IPC_CHANNELS.MEETING_SUMMARY.EXPORT_MARKDOWN,
async (_event, params: MeetingSummaryExportParams) => {
try {
const filePath = await getMeetingSummaryService().exportMarkdown(params.historyId)
return ipcSuccess(filePath)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Meeting summary export failed:', msg)
return ipcError(ErrorCode.MeetingSummaryExportFailed, msg)
}
},
)
}

View file

@ -0,0 +1,74 @@
// src/main/ipc/memo-handlers.ts
// Phase 10.3: 음성 메모 태그 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode, D3ROError } from '@shared/errors'
import { getMemoService } from '../services/MemoService'
import type {
GetTagsParams,
AddTagParams,
RemoveTagParams,
SearchByTagParams,
ExportMemoParams
} from '@shared/types'
export function registerMemoHandlers(): void {
ipcMain.handle(IPC_CHANNELS.MEMO.GET_TAGS, async (_event, params: GetTagsParams) => {
try {
return ipcSuccess(getMemoService().getTagsForEntry(params.historyId))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get tags for history entry')
}
})
ipcMain.handle(IPC_CHANNELS.MEMO.ADD_TAG, async (_event, params: AddTagParams) => {
try {
return ipcSuccess(getMemoService().addTag(params.historyId, params.tag))
} catch (err) {
if (err instanceof D3ROError && err.code === ErrorCode.MemoTagDuplicate) {
return ipcError(ErrorCode.MemoTagDuplicate, err.message)
}
return ipcError(ErrorCode.DBWriteFailed, 'Failed to add tag')
}
})
ipcMain.handle(IPC_CHANNELS.MEMO.REMOVE_TAG, async (_event, params: RemoveTagParams) => {
try {
getMemoService().removeTag(params.historyId, params.tag)
return ipcSuccess(undefined)
} catch (err) {
if (err instanceof D3ROError && err.code === ErrorCode.MemoTagNotFound) {
return ipcError(ErrorCode.MemoTagNotFound, err.message)
}
return ipcError(ErrorCode.DBWriteFailed, 'Failed to remove tag')
}
})
ipcMain.handle(IPC_CHANNELS.MEMO.GET_ALL_TAGS, async () => {
try {
return ipcSuccess(getMemoService().getAllTags())
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to get all tags')
}
})
ipcMain.handle(IPC_CHANNELS.MEMO.SEARCH_BY_TAG, async (_event, params: SearchByTagParams) => {
try {
return ipcSuccess(getMemoService().searchByTag(params))
} catch {
return ipcError(ErrorCode.DBQueryFailed, 'Failed to search by tag')
}
})
ipcMain.handle(IPC_CHANNELS.MEMO.EXPORT, async (_event, params: ExportMemoParams) => {
try {
return ipcSuccess(getMemoService().exportMarkdown(params))
} catch (err) {
if (err instanceof D3ROError && err.code === ErrorCode.MemoExportFailed) {
return ipcError(ErrorCode.MemoExportFailed, err.message)
}
return ipcError(ErrorCode.MemoExportFailed, 'Failed to export memo')
}
})
}

View file

@ -0,0 +1,84 @@
// src/main/ipc/rag-handlers.ts
// Phase 13.2: 로컬 RAG IPC 핸들러
import { ipcMain, dialog } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getRAGService } from '../services/RAGService'
import { getLogger } from '../services/LoggerService'
import type { RAGQueryParams, RAGRemoveDocumentParams } from '@shared/types'
const logger = getLogger('rag-handlers')
export function registerRAGHandlers(): void {
ipcMain.handle(IPC_CHANNELS.RAG.ADD_DOCUMENT, async (_event, params: { filePath?: string }) => {
try {
let filePath = params?.filePath
if (!filePath) {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Documents', extensions: ['txt', 'md', 'pdf', 'docx'] }],
})
if (result.canceled || result.filePaths.length === 0) {
return ipcError(ErrorCode.RAGDocumentNotFound, 'File selection cancelled')
}
filePath = result.filePaths[0]
}
const doc = await getRAGService().addDocument(filePath)
return ipcSuccess(doc)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('RAG add document failed:', msg)
return ipcError(ErrorCode.RAGIndexingFailed, msg)
}
})
ipcMain.handle(IPC_CHANNELS.RAG.REMOVE_DOCUMENT, async (_event, params: RAGRemoveDocumentParams) => {
try {
getRAGService().removeDocument(params.documentId)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.RAGDocumentNotFound, msg)
}
})
ipcMain.handle(IPC_CHANNELS.RAG.GET_DOCUMENTS, async () => {
try {
return ipcSuccess(getRAGService().getDocuments())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.RAG.QUERY, async (_event, params: RAGQueryParams) => {
try {
const result = await getRAGService().query(params.query, params.topK)
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('RAG query failed:', msg)
return ipcError(ErrorCode.RAGQueryFailed, msg)
}
})
ipcMain.handle(IPC_CHANNELS.RAG.GET_STATE, async () => {
try {
return ipcSuccess(getRAGService().getStateInfo())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.RAG.REINDEX, async (_event, params: { documentId: string }) => {
try {
await getRAGService().reindex(params.documentId)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.RAGIndexingFailed, msg)
}
})
}

View file

@ -0,0 +1,52 @@
// src/main/ipc/stt-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getLocalSTTService } from '../services/LocalSTTService'
import { configGet, configSet } from '../services/ConfigService'
import type { SetSTTModelParams, SetSTTLanguageParams } from '@shared/types'
export function registerSTTHandlers(): void {
ipcMain.handle(IPC_CHANNELS.STT.GET_STATUS, async () => {
try {
const stt = getLocalSTTService()
return ipcSuccess(stt.getStatus())
} catch {
return ipcError(ErrorCode.STTSidecarCommunicationFailed, 'Failed to get STT status')
}
})
ipcMain.handle(IPC_CHANNELS.STT.GET_MODELS, async () => {
try {
const stt = getLocalSTTService()
return ipcSuccess(await stt.getModels())
} catch {
return ipcError(ErrorCode.STTModelNotFound, 'Failed to get models')
}
})
ipcMain.handle(IPC_CHANNELS.STT.GET_ACTIVE_MODEL, async () => {
return ipcSuccess(configGet('sttModelId'))
})
ipcMain.handle(IPC_CHANNELS.STT.SET_MODEL, async (_event, params: SetSTTModelParams) => {
try {
configSet('sttModelId', params.modelId)
const stt = getLocalSTTService()
await stt.initialize(params.modelId)
return ipcSuccess(undefined)
} catch {
return ipcError(ErrorCode.STTModelLoadFailed, 'Failed to set STT model')
}
})
ipcMain.handle(IPC_CHANNELS.STT.GET_LANGUAGE, async () => {
return ipcSuccess(configGet('sttLanguage'))
})
ipcMain.handle(IPC_CHANNELS.STT.SET_LANGUAGE, async (_event, params: SetSTTLanguageParams) => {
configSet('sttLanguage', params.language)
return ipcSuccess(undefined)
})
}

View file

@ -0,0 +1,45 @@
// src/main/ipc/system-handlers.ts
import { ipcMain, app, systemPreferences } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess } from '@shared/errors'
import type { PermissionStatus, PlaySoundParams, SetSoundEnabledParams } from '@shared/types'
import { getSoundEffectService } from '../services/SoundEffectService'
export function registerSystemHandlers(): void {
ipcMain.handle(IPC_CHANNELS.SYSTEM.GET_PLATFORM, async () => {
return ipcSuccess(process.platform)
})
ipcMain.handle(IPC_CHANNELS.SYSTEM.GET_VERSION, async () => {
return ipcSuccess(app.getVersion())
})
ipcMain.handle(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION, async () => {
let status: PermissionStatus = 'unknown'
if (process.platform === 'darwin') {
const macStatus = systemPreferences.getMediaAccessStatus('microphone')
status = macStatus === 'granted' ? 'granted' : macStatus === 'denied' ? 'denied' : 'unknown'
} else {
// Windows: 마이크 권한은 별도 체크 불필요 (시스템 설정에서 관리)
status = 'granted'
}
return ipcSuccess(status)
})
// ── Sound Effect ──
ipcMain.handle(IPC_CHANNELS.SYSTEM.PLAY_SOUND, async (_event, params: PlaySoundParams) => {
getSoundEffectService().play(params.sound as 'recording-start' | 'recording-stop' | 'error' | 'cancel')
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.SYSTEM.SET_SOUND_ENABLED, async (_event, params: SetSoundEnabledParams) => {
getSoundEffectService().setEnabled(params.enabled)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.SYSTEM.IS_SOUND_ENABLED, async () => {
return ipcSuccess(getSoundEffectService().isEnabled())
})
}

View file

@ -0,0 +1,128 @@
// src/main/ipc/template-handlers.ts
// Phase 12.3: 딕테이션 템플릿 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getDictationTemplateService } from '../services/DictationTemplateService'
import { getLogger } from '../services/LoggerService'
import type {
CreateTemplateParams,
UpdateTemplateParams,
DeleteTemplateParams,
StartTemplateSessionParams,
SetFieldValueParams,
} from '@shared/types'
const logger = getLogger('template-handlers')
export function registerTemplateHandlers(): void {
ipcMain.handle(IPC_CHANNELS.DICTATION_TEMPLATE.GET_ALL, async () => {
try {
return ipcSuccess(getDictationTemplateService().getAll())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(
IPC_CHANNELS.DICTATION_TEMPLATE.CREATE,
async (_event, params: CreateTemplateParams) => {
try {
// 라이센스 체크
try {
const { getLicenseService } = await import('../services/LicenseService')
const { Feature } = await import('@shared/types')
const license = getLicenseService()
const access = license.canUse(Feature.DICTATION_TEMPLATE)
if (!access.allowed) {
license.promptUpgrade(Feature.DICTATION_TEMPLATE, 'tier_required')
return ipcError(ErrorCode.FeatureNotAvailable, 'Pro+ required')
}
} catch {
// LicenseService 미초기화 시 허용
}
const result = getDictationTemplateService().create(params)
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Template create failed:', msg)
return ipcError(ErrorCode.TemplateInvalidFormat, msg)
}
},
)
ipcMain.handle(
IPC_CHANNELS.DICTATION_TEMPLATE.UPDATE,
async (_event, params: UpdateTemplateParams) => {
try {
const result = getDictationTemplateService().update(params)
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.TemplateNotFound, msg)
}
},
)
ipcMain.handle(
IPC_CHANNELS.DICTATION_TEMPLATE.DELETE,
async (_event, params: DeleteTemplateParams) => {
try {
getDictationTemplateService().delete(params.id)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.TemplateNotFound, msg)
}
},
)
ipcMain.handle(
IPC_CHANNELS.DICTATION_TEMPLATE.START_SESSION,
async (_event, params: StartTemplateSessionParams) => {
try {
getDictationTemplateService().startSession(params.templateId)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Template session start failed:', msg)
return ipcError(ErrorCode.TemplateSessionAlreadyActive, msg)
}
},
)
ipcMain.handle(IPC_CHANNELS.DICTATION_TEMPLATE.CANCEL_SESSION, async () => {
try {
getDictationTemplateService().cancelSession()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.TemplateSessionNotActive, msg)
}
})
ipcMain.handle(IPC_CHANNELS.DICTATION_TEMPLATE.GET_SESSION_STATE, async () => {
try {
return ipcSuccess(getDictationTemplateService().getSessionState())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(
IPC_CHANNELS.DICTATION_TEMPLATE.SET_FIELD_VALUE,
async (_event, params: SetFieldValueParams) => {
try {
getDictationTemplateService().setFieldValue(params.fieldId, params.value)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.TemplateFieldRecordingFailed, msg)
}
},
)
}

View file

@ -0,0 +1,74 @@
// src/main/ipc/voice-action-handlers.ts
// Phase 13.3: OS 자동화 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getVoiceActionService } from '../services/VoiceActionService'
import { getLogger } from '../services/LoggerService'
import type { VoiceActionExecuteParams } from '@shared/types'
const logger = getLogger('voice-action-handlers')
export function registerVoiceActionHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.VOICE_ACTION.EXECUTE,
async (_event, params: VoiceActionExecuteParams) => {
try {
await getVoiceActionService().execute(params.text)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Voice action failed:', msg)
return ipcError(ErrorCode.VoiceActionExecutionFailed, msg)
}
},
)
ipcMain.handle(IPC_CHANNELS.VOICE_ACTION.GET_PRESETS, async () => {
try {
return ipcSuccess(getVoiceActionService().getPresets())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_ACTION.GET_HISTORY, async () => {
try {
return ipcSuccess(getVoiceActionService().getHistory())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_ACTION.CLEAR_HISTORY, async () => {
try {
getVoiceActionService().clearHistory()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_ACTION.SET_ENABLED, async (_event, params: { enabled: boolean }) => {
try {
getVoiceActionService().setEnabled(params.enabled)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_ACTION.IS_ENABLED, async () => {
try {
return ipcSuccess(getVoiceActionService().isEnabled)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
}

View file

@ -0,0 +1,51 @@
// src/main/ipc/voice-command-handlers.ts
// Phase 10.5: 음성 단축키 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getVoiceCommandService } from '../services/VoiceCommandService'
import type { SetVoiceCommandKeywordsParams, SetVoiceCommandEnabledParams } from '@shared/types'
export function registerVoiceCommandHandlers(): void {
const CH = IPC_CHANNELS.VOICE_COMMAND
ipcMain.handle(CH.GET_ALL, async () => {
try {
return ipcSuccess(getVoiceCommandService().getAllRules())
} catch (error) {
return ipcError(
ErrorCode.VoiceCommandMatchFailed,
`Failed to get voice command rules: ${error instanceof Error ? error.message : String(error)}`
)
}
})
ipcMain.handle(CH.SET_KEYWORDS, async (_event, params: SetVoiceCommandKeywordsParams) => {
try {
getVoiceCommandService().setKeywordsForInstruction(params.instructionId, params.keywords)
return ipcSuccess(undefined)
} catch (error) {
return ipcError(
ErrorCode.VoiceCommandMatchFailed,
`Failed to set keywords: ${error instanceof Error ? error.message : String(error)}`
)
}
})
ipcMain.handle(CH.SET_ENABLED, async (_event, params: SetVoiceCommandEnabledParams) => {
try {
getVoiceCommandService().setEnabled(params.enabled)
return ipcSuccess(undefined)
} catch (error) {
return ipcError(
ErrorCode.VoiceCommandMatchFailed,
`Failed to set enabled state: ${error instanceof Error ? error.message : String(error)}`
)
}
})
ipcMain.handle(CH.IS_ENABLED, async () => {
return ipcSuccess(getVoiceCommandService().isEnabled())
})
}

View file

@ -0,0 +1,97 @@
// src/main/ipc/voice-conversation-handlers.ts
// Phase 13.1: 음성 대화 모드 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getVoiceConversationService } from '../services/VoiceConversationService'
import { getLogger } from '../services/LoggerService'
import type { ConversationSendParams } from '@shared/types'
const logger = getLogger('voice-conversation-handlers')
export function registerVoiceConversationHandlers(): void {
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.START_SESSION, async () => {
try {
await getVoiceConversationService().startSession()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Conversation start failed:', msg)
return ipcError(ErrorCode.ConversationSessionAlreadyActive, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.STOP_SESSION, async () => {
try {
getVoiceConversationService().stopSession()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.ConversationNoActiveSession, msg)
}
})
ipcMain.handle(
IPC_CHANNELS.VOICE_CONVERSATION.SEND_MESSAGE,
async (_event, params: ConversationSendParams) => {
try {
await getVoiceConversationService().sendTextMessage(params.text)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Conversation send failed:', msg)
return ipcError(ErrorCode.ConversationLLMFailed, msg)
}
},
)
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.GET_STATE, async () => {
try {
return ipcSuccess(getVoiceConversationService().getSessionInfo())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.GET_HISTORY, async () => {
try {
return ipcSuccess(getVoiceConversationService().getHistory())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.CLEAR_HISTORY, async () => {
try {
getVoiceConversationService().clearHistory()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.CANCEL_RESPONSE, async () => {
try {
getVoiceConversationService().cancelResponse()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
// finishListening — 렌더러에서 녹음 종료 버튼 클릭 시
ipcMain.handle('voiceConversation:finishListening', async () => {
try {
await getVoiceConversationService().finishListening()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.ConversationLLMFailed, msg)
}
})
}

View file

@ -0,0 +1,53 @@
// src/main/ipc/voice-handlers.ts
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess, ipcError, ErrorCode } from '@shared/errors'
import { getVoiceModeService } from '../services/VoiceModeService'
import type { StartRecordingParams, StopRecordingParams, CancelRecordingParams, SetVoiceModeParams } from '@shared/types'
export function registerVoiceHandlers(): void {
ipcMain.handle(IPC_CHANNELS.VOICE.START_RECORDING, async (_event, params: StartRecordingParams) => {
try {
const voice = getVoiceModeService()
await voice.startSession('dictation')
const session = voice.currentSession
return ipcSuccess({ sessionId: session?.id ?? params.sessionId ?? '' })
} catch {
return ipcError(ErrorCode.AudioCaptureStartFailed, 'Failed to start recording')
}
})
ipcMain.handle(IPC_CHANNELS.VOICE.STOP_RECORDING, async (_event, params: StopRecordingParams) => {
try {
const voice = getVoiceModeService()
await voice.stopSession()
return ipcSuccess({
sessionId: params.sessionId,
text: voice.currentSession?.transcription ?? '',
durationMs: voice.currentSession ? Date.now() - voice.currentSession.startedAt : 0
})
} catch {
return ipcError(ErrorCode.STTTranscriptionFailed, 'Failed to stop recording')
}
})
ipcMain.handle(IPC_CHANNELS.VOICE.CANCEL_RECORDING, async (_event, _params: CancelRecordingParams) => {
getVoiceModeService().cancelSession()
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.VOICE.GET_STATE, async () => {
return ipcSuccess(getVoiceModeService().getState())
})
ipcMain.handle(IPC_CHANNELS.VOICE.SET_MODE, async (_event, params: SetVoiceModeParams) => {
// Phase 2: 모드만 설정에 저장 (실제 모드 전환은 핫키에서 처리)
return ipcSuccess(undefined)
})
ipcMain.handle(IPC_CHANNELS.VOICE.GET_MODE, async () => {
const voice = getVoiceModeService()
return ipcSuccess(voice.currentSession?.mode ?? 'dictation')
})
}

View file

@ -0,0 +1,46 @@
// src/main/ipc/window-handlers.ts
import { ipcMain, shell } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess } from '@shared/errors'
import { getMainWindow, hideResultPopup, hideRecordingTip } from '../windows/WindowManager'
export function registerWindowHandlers(): void {
ipcMain.on(IPC_CHANNELS.WINDOW.MINIMIZE, () => {
getMainWindow()?.minimize()
})
ipcMain.on(IPC_CHANNELS.WINDOW.MAXIMIZE, () => {
const win = getMainWindow()
if (win) {
if (win.isMaximized()) {
win.unmaximize()
} else {
win.maximize()
}
}
})
ipcMain.on(IPC_CHANNELS.WINDOW.CLOSE, () => {
getMainWindow()?.close()
})
ipcMain.handle(IPC_CHANNELS.WINDOW.IS_MAXIMIZED, async () => {
return ipcSuccess(getMainWindow()?.isMaximized() ?? false)
})
// 팝업 윈도우 hide 요청 (렌더러 → 메인)
ipcMain.on('window:hideResultPopup', () => {
hideResultPopup()
})
ipcMain.on('window:hideRecordingTip', () => {
hideRecordingTip()
})
// 외부 URL 열기
ipcMain.handle(IPC_CHANNELS.SYSTEM.OPEN_EXTERNAL, async (_event, params: { url: string }) => {
await shell.openExternal(params.url)
return ipcSuccess(undefined)
})
}