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:
parent
3a160b9032
commit
45a580878a
178 changed files with 214 additions and 0 deletions
101
apps/desktop/src/main/ipc/audio-handlers.ts
Normal file
101
apps/desktop/src/main/ipc/audio-handlers.ts
Normal 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)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
69
apps/desktop/src/main/ipc/caption-handlers.ts
Normal file
69
apps/desktop/src/main/ipc/caption-handlers.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
88
apps/desktop/src/main/ipc/chain-handlers.ts
Normal file
88
apps/desktop/src/main/ipc/chain-handlers.ts
Normal 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)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
98
apps/desktop/src/main/ipc/config-handlers.ts
Normal file
98
apps/desktop/src/main/ipc/config-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
47
apps/desktop/src/main/ipc/context-handlers.ts
Normal file
47
apps/desktop/src/main/ipc/context-handlers.ts
Normal 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(),
|
||||
})
|
||||
})
|
||||
}
|
||||
56
apps/desktop/src/main/ipc/dictionary-handlers.ts
Normal file
56
apps/desktop/src/main/ipc/dictionary-handlers.ts
Normal 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')
|
||||
}
|
||||
})
|
||||
}
|
||||
69
apps/desktop/src/main/ipc/file-transcription-handlers.ts
Normal file
69
apps/desktop/src/main/ipc/file-transcription-handlers.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
64
apps/desktop/src/main/ipc/history-handlers.ts
Normal file
64
apps/desktop/src/main/ipc/history-handlers.ts
Normal 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')
|
||||
}
|
||||
})
|
||||
}
|
||||
81
apps/desktop/src/main/ipc/hotkey-handlers.ts
Normal file
81
apps/desktop/src/main/ipc/hotkey-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
59
apps/desktop/src/main/ipc/index.ts
Normal file
59
apps/desktop/src/main/ipc/index.ts
Normal 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')
|
||||
}
|
||||
67
apps/desktop/src/main/ipc/instruction-handlers.ts
Normal file
67
apps/desktop/src/main/ipc/instruction-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
100
apps/desktop/src/main/ipc/license-handlers.ts
Normal file
100
apps/desktop/src/main/ipc/license-handlers.ts
Normal 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}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
77
apps/desktop/src/main/ipc/llm-handlers.ts
Normal file
77
apps/desktop/src/main/ipc/llm-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
63
apps/desktop/src/main/ipc/meeting-doc-template-handlers.ts
Normal file
63
apps/desktop/src/main/ipc/meeting-doc-template-handlers.ts
Normal 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')
|
||||
}
|
||||
266
apps/desktop/src/main/ipc/meeting-mode-handlers.ts
Normal file
266
apps/desktop/src/main/ipc/meeting-mode-handlers.ts
Normal 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')
|
||||
}
|
||||
54
apps/desktop/src/main/ipc/meeting-summary-handlers.ts
Normal file
54
apps/desktop/src/main/ipc/meeting-summary-handlers.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
74
apps/desktop/src/main/ipc/memo-handlers.ts
Normal file
74
apps/desktop/src/main/ipc/memo-handlers.ts
Normal 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')
|
||||
}
|
||||
})
|
||||
}
|
||||
84
apps/desktop/src/main/ipc/rag-handlers.ts
Normal file
84
apps/desktop/src/main/ipc/rag-handlers.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
52
apps/desktop/src/main/ipc/stt-handlers.ts
Normal file
52
apps/desktop/src/main/ipc/stt-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
45
apps/desktop/src/main/ipc/system-handlers.ts
Normal file
45
apps/desktop/src/main/ipc/system-handlers.ts
Normal 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())
|
||||
})
|
||||
}
|
||||
128
apps/desktop/src/main/ipc/template-handlers.ts
Normal file
128
apps/desktop/src/main/ipc/template-handlers.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
74
apps/desktop/src/main/ipc/voice-action-handlers.ts
Normal file
74
apps/desktop/src/main/ipc/voice-action-handlers.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
51
apps/desktop/src/main/ipc/voice-command-handlers.ts
Normal file
51
apps/desktop/src/main/ipc/voice-command-handlers.ts
Normal 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())
|
||||
})
|
||||
}
|
||||
97
apps/desktop/src/main/ipc/voice-conversation-handlers.ts
Normal file
97
apps/desktop/src/main/ipc/voice-conversation-handlers.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
53
apps/desktop/src/main/ipc/voice-handlers.ts
Normal file
53
apps/desktop/src/main/ipc/voice-handlers.ts
Normal 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')
|
||||
})
|
||||
}
|
||||
46
apps/desktop/src/main/ipc/window-handlers.ts
Normal file
46
apps/desktop/src/main/ipc/window-handlers.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue