From aef44289a5c3ae51463a07c8fe3f551b902c2dc8 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 22 Jul 2026 01:54:18 +0900 Subject: [PATCH] =?UTF-8?q?refactor(main):=20IPC=20=EC=B1=84=EB=84=90=20SS?= =?UTF-8?q?OT=20=EC=9D=BC=EC=9B=90=ED=99=94=20+=20AppConfig=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B0=95=ED=99=94=20(WS2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ipc-channels.ts SSOT에 9개 그룹/키 추가: INSTRUCTION, SYSTEM_AUDIO, POPUP_RESULT/HISTORY/COMMAND/CAPTION, VOICE_PARTIAL, CLIPBOARD, APP, VOICE_CONVERSATION.FINISH_LISTENING - WindowManager/handlers/bootstrap 하드코딩 채널 -> IPC_CHANNELS 교체. 불일치 4건(result:* vs window:tip*)은 preload 재검증 후 SSOT로 통일. notifyRenderer channel: string -> IPCChannel 타입 좁힘. - AppConfig에 7개 누락 키 추가(customInstructions, llmChains, voiceCommandRules, voiceCommandsEnabled, activeInstructionId, activeChainId, captionAudioSource) -> as never 16건 제거. - ChainService/CustomInstructionService/WindowManager dynamic require -> static import. - services/index.ts 데드 레지스트리 제거 (import 0건). SKIP: ipcSuccess/ipcError 헬퍼 통일, catch 패턴(별도) 정책: docs/REFACTOR_POLICY.md DP2, DP9 --- apps/desktop/src/main/bootstrap.ts | 24 +++---- apps/desktop/src/main/ipc/caption-handlers.ts | 4 +- .../src/main/ipc/cloud-sync-handlers.ts | 5 +- .../src/main/ipc/instruction-handlers.ts | 26 +++----- .../main/ipc/voice-conversation-handlers.ts | 2 +- apps/desktop/src/main/ipc/window-handlers.ts | 4 +- .../desktop/src/main/services/ChainService.ts | 10 +-- .../main/services/CustomInstructionService.ts | 24 ++----- .../src/main/services/MeetingModeService.ts | 9 ++- .../src/main/services/VoiceCommandService.ts | 9 ++- .../src/main/services/VoiceModeService.ts | 4 +- apps/desktop/src/main/services/index.ts | 31 --------- .../desktop/src/main/windows/WindowManager.ts | 40 ++++++------ packages/core/src/ipc-channels.ts | 65 +++++++++++++++++++ packages/core/src/types.ts | 30 +++++++++ 15 files changed, 161 insertions(+), 126 deletions(-) delete mode 100644 apps/desktop/src/main/services/index.ts diff --git a/apps/desktop/src/main/bootstrap.ts b/apps/desktop/src/main/bootstrap.ts index 0fa5763..a7a970c 100644 --- a/apps/desktop/src/main/bootstrap.ts +++ b/apps/desktop/src/main/bootstrap.ts @@ -33,6 +33,8 @@ import { } from './windows/WindowManager' import { createTray } from './windows/TrayManager' import { registerAllIpcHandlers } from './ipc' +import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' +import type { IPCChannel } from '@d3ro/core/ipc-channels' const logger = getLogger('bootstrap') @@ -171,7 +173,7 @@ async function initPopupWindows(): Promise { unregisterPopupNavKeys() } else { const instructions = getCustomInstructionService().getAll() - const activeId = configGet('activeInstructionId' as keyof import('@d3ro/core/types').AppConfig) as unknown as string + const activeId = configGet('activeInstructionId') as string | null showCommandPopup(instructions as unknown as Array>, activeId || null) registerPopupNavKeys('command') } @@ -250,7 +252,7 @@ async function initVoiceMode(): Promise { } // 렌더러 UI 갱신 알림 (Dashboard 통계 + History 목록) - notifyRenderer('app:dataChanged', { type: 'session-completed' }) + notifyRenderer(IPC_CHANNELS.APP.DATA_CHANGED, { type: 'session-completed' }) }) voiceMode.on('session-cancelled', ({ reason }) => { @@ -268,7 +270,7 @@ async function initVoiceMode(): Promise { } /** 메인 윈도우 렌더러에 UI 갱신 이벤트 전송 */ -function notifyRenderer(channel: string, data?: Record): void { +function notifyRenderer(channel: IPCChannel, data?: Record): void { const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(channel, data ?? {}) @@ -330,13 +332,13 @@ async function initMeetingMode(): Promise { function setupHistoryPopupIPC(): void { // 히스토리 팝업 열기 (핫키에서 호출) - ipcMainRef.on('history:showPopup', () => { + ipcMainRef.on(IPC_CHANNELS.POPUP_HISTORY.SHOW_POPUP, () => { const entries = getHistoryService().list({ page: 0, pageSize: 10 }).entries showHistoryPopup(entries as unknown as Array>) }) // 아이템 선택 → 텍스트 삽입 - ipcMainRef.on('history:itemSelected', (_event: Electron.IpcMainEvent, data: { text: string }) => { + ipcMainRef.on(IPC_CHANNELS.POPUP_HISTORY.ITEM_SELECTED, (_event: Electron.IpcMainEvent, data: { text: string }) => { hideHistoryPopup() unregisterPopupNavKeys() setTimeout(async () => { @@ -349,7 +351,7 @@ function setupHistoryPopupIPC(): void { }) // 팝업 닫기 - ipcMainRef.on('history:popupDismissed', () => { + ipcMainRef.on(IPC_CHANNELS.POPUP_HISTORY.POPUP_DISMISSED, () => { hideHistoryPopup() unregisterPopupNavKeys() }) @@ -359,28 +361,28 @@ function setupHistoryPopupIPC(): void { function setupCommandPopupIPC(): void { // 명령어 선택 → 활성 명령어로 설정 - ipcMainRef.on('command:selected', (_event: Electron.IpcMainEvent, data: { id: string; name: string }) => { + ipcMainRef.on(IPC_CHANNELS.POPUP_COMMAND.SELECTED, (_event: Electron.IpcMainEvent, data: { id: string; name: string }) => { hideCommandPopup() unregisterPopupNavKeys() if (data.id) { // 명령어 선택 → 활성 명령어로 설정 - configSet('activeInstructionId' as keyof import('@d3ro/core/types').AppConfig, data.id as never) + configSet('activeInstructionId', data.id) configSet('defaultLLMAction', 'custom') logger.info(`Active command set: ${data.name} (${data.id})`) } else { // 선택 해제 → 명령어 없음 (원본 삽입) - configSet('activeInstructionId' as keyof import('@d3ro/core/types').AppConfig, '' as never) + configSet('activeInstructionId', '') configSet('defaultLLMAction', 'none') logger.info('Active command cleared (none)') } // CMD 페이지 UI 갱신 알림 - notifyRenderer('app:dataChanged', { type: 'command-changed', activeId: data.id }) + notifyRenderer(IPC_CHANNELS.APP.DATA_CHANGED, { type: 'command-changed', activeId: data.id }) }) // 팝업 닫기 - ipcMainRef.on('command:dismissed', () => { + ipcMainRef.on(IPC_CHANNELS.POPUP_COMMAND.DISMISSED, () => { hideCommandPopup() unregisterPopupNavKeys() }) diff --git a/apps/desktop/src/main/ipc/caption-handlers.ts b/apps/desktop/src/main/ipc/caption-handlers.ts index 4e54599..0c53df3 100644 --- a/apps/desktop/src/main/ipc/caption-handlers.ts +++ b/apps/desktop/src/main/ipc/caption-handlers.ts @@ -9,7 +9,7 @@ import type { CaptionConfig } from '@d3ro/core/types' export function registerCaptionHandlers(): void { // 시스템 오디오 루프백: setDisplayMediaRequestHandler로 audio: 'loopback' 설정 - ipcMain.handle('system-audio:enable-loopback', async () => { + ipcMain.handle(IPC_CHANNELS.SYSTEM_AUDIO.ENABLE_LOOPBACK, async () => { session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => { const sources = await desktopCapturer.getSources({ types: ['screen'] }) if (sources.length === 0) { @@ -21,7 +21,7 @@ export function registerCaptionHandlers(): void { return ipcSuccess(undefined) }) - ipcMain.handle('system-audio:disable-loopback', async () => { + ipcMain.handle(IPC_CHANNELS.SYSTEM_AUDIO.DISABLE_LOOPBACK, async () => { session.defaultSession.setDisplayMediaRequestHandler(null) return ipcSuccess(undefined) }) diff --git a/apps/desktop/src/main/ipc/cloud-sync-handlers.ts b/apps/desktop/src/main/ipc/cloud-sync-handlers.ts index 9a496cb..ed7ddb2 100644 --- a/apps/desktop/src/main/ipc/cloud-sync-handlers.ts +++ b/apps/desktop/src/main/ipc/cloud-sync-handlers.ts @@ -7,6 +7,7 @@ import { D3ROError, ErrorCode } from '@d3ro/core/errors' import type { IPCResult } from '@d3ro/core/errors' import { getCloudSyncService } from '../services/CloudSyncService' import { getLogger } from '../services/LoggerService' +import { configSet } from '../services/ConfigService' const logger = getLogger('cloud-sync-handlers') @@ -70,7 +71,7 @@ export function registerCloudSyncHandlers(): void { ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.PUSH_ALL, async () => { try { const result = await sync.pushAll() - configSet('cloudSyncLastAt' as never, Date.now() as never) + configSet('cloudSyncLastAt', Date.now()) return ok(result) } catch (e) { return fail(e) @@ -80,7 +81,7 @@ export function registerCloudSyncHandlers(): void { ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.PULL_ALL, async () => { try { const result = await sync.pullAll() - configSet('cloudSyncLastAt' as never, Date.now() as never) + configSet('cloudSyncLastAt', Date.now()) return ok(result) } catch (e) { return fail(e) diff --git a/apps/desktop/src/main/ipc/instruction-handlers.ts b/apps/desktop/src/main/ipc/instruction-handlers.ts index 4fe3097..3669844 100644 --- a/apps/desktop/src/main/ipc/instruction-handlers.ts +++ b/apps/desktop/src/main/ipc/instruction-handlers.ts @@ -1,32 +1,22 @@ // src/main/ipc/instruction-handlers.ts import { ipcMain } from 'electron' +import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/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 +import type { CustomInstruction } from '@d3ro/core/types' export function registerInstructionHandlers(): void { - ipcMain.handle(CHANNELS.GET_ALL, async () => { + ipcMain.handle(IPC_CHANNELS.INSTRUCTION.GET_ALL, async () => { return ipcSuccess(getCustomInstructionService().getAll()) }) - ipcMain.handle(CHANNELS.GET_BY_ID, async (_event, params: { id: string }) => { + ipcMain.handle(IPC_CHANNELS.INSTRUCTION.GET_BY_ID, async (_event, params: { id: string }) => { return ipcSuccess(getCustomInstructionService().getById(params.id)) }) ipcMain.handle( - CHANNELS.CREATE, + IPC_CHANNELS.INSTRUCTION.CREATE, async ( _event, params: { name: string; description: string; prompt: string; icon?: string } @@ -41,7 +31,7 @@ export function registerInstructionHandlers(): void { ) ipcMain.handle( - CHANNELS.UPDATE, + IPC_CHANNELS.INSTRUCTION.UPDATE, async (_event, params: { id: string; data: Partial }) => { try { const result = getCustomInstructionService().update(params.id, params.data) @@ -52,7 +42,7 @@ export function registerInstructionHandlers(): void { } ) - ipcMain.handle(CHANNELS.DELETE, async (_event, params: { id: string }) => { + ipcMain.handle(IPC_CHANNELS.INSTRUCTION.DELETE, async (_event, params: { id: string }) => { const result = getCustomInstructionService().delete(params.id) if (!result) { return ipcError(ErrorCode.ConfigWriteFailed, 'Cannot delete builtin instruction') @@ -60,7 +50,7 @@ export function registerInstructionHandlers(): void { return ipcSuccess(undefined) }) - ipcMain.handle(CHANNELS.REORDER, async (_event, params: { ids: string[] }) => { + ipcMain.handle(IPC_CHANNELS.INSTRUCTION.REORDER, async (_event, params: { ids: string[] }) => { getCustomInstructionService().reorder(params.ids) return ipcSuccess(undefined) }) diff --git a/apps/desktop/src/main/ipc/voice-conversation-handlers.ts b/apps/desktop/src/main/ipc/voice-conversation-handlers.ts index 123e1b0..23100ad 100644 --- a/apps/desktop/src/main/ipc/voice-conversation-handlers.ts +++ b/apps/desktop/src/main/ipc/voice-conversation-handlers.ts @@ -131,7 +131,7 @@ export function registerVoiceConversationHandlers(): void { ) // finishListening — 렌더러에서 녹음 종료 버튼 클릭 시 - ipcMain.handle('voiceConversation:finishListening', async () => { + ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.FINISH_LISTENING, async () => { try { await getVoiceConversationService().finishListening() return ipcSuccess(undefined) diff --git a/apps/desktop/src/main/ipc/window-handlers.ts b/apps/desktop/src/main/ipc/window-handlers.ts index c7dfed1..89b1590 100644 --- a/apps/desktop/src/main/ipc/window-handlers.ts +++ b/apps/desktop/src/main/ipc/window-handlers.ts @@ -30,11 +30,11 @@ export function registerWindowHandlers(): void { }) // 팝업 윈도우 hide 요청 (렌더러 → 메인) - ipcMain.on('window:hideResultPopup', () => { + ipcMain.on(IPC_CHANNELS.WINDOW.HIDE_RESULT_POPUP, () => { hideResultPopup() }) - ipcMain.on('window:hideRecordingTip', () => { + ipcMain.on(IPC_CHANNELS.WINDOW.HIDE_RECORDING_TIP, () => { hideRecordingTip() }) diff --git a/apps/desktop/src/main/services/ChainService.ts b/apps/desktop/src/main/services/ChainService.ts index 818073e..f50f13a 100644 --- a/apps/desktop/src/main/services/ChainService.ts +++ b/apps/desktop/src/main/services/ChainService.ts @@ -3,7 +3,7 @@ // LLMChain을 electron-store에 저장하고, 체인을 순차 실행한다. import { getLogger } from './LoggerService' -import { configGet } from './ConfigService' +import { configGet, configSet } from './ConfigService' import { getCustomInstructionService } from './CustomInstructionService' import { getLocalLLMService } from './LocalLLMService' import { getMainWindow } from '../windows/WindowManager' @@ -29,7 +29,7 @@ let initialized = false function loadChains(): LLMChain[] { try { - const stored = configGet('llmChains' as never) as LLMChain[] | undefined + const stored = configGet('llmChains') as LLMChain[] | undefined if (Array.isArray(stored) && stored.length > 0) { return stored } @@ -41,11 +41,7 @@ function loadChains(): LLMChain[] { function saveChains(): void { try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { configSet } = require('./ConfigService') as { - configSet: (key: never, value: never) => void - } - configSet('llmChains' as never, chains as never) + configSet('llmChains', chains) } catch (error) { logger.warn( `Failed to save chains: ${error instanceof Error ? error.message : String(error)}` diff --git a/apps/desktop/src/main/services/CustomInstructionService.ts b/apps/desktop/src/main/services/CustomInstructionService.ts index c1d5597..9fdbd07 100644 --- a/apps/desktop/src/main/services/CustomInstructionService.ts +++ b/apps/desktop/src/main/services/CustomInstructionService.ts @@ -3,26 +3,11 @@ // electron-store에 저장, 프리셋 5개 기본 제공. import { getLogger } from './LoggerService' -import { configGet } from './ConfigService' +import { configGet, configSet } from './ConfigService' +import type { CustomInstruction } from '@d3ro/core/types' const logger = getLogger('CustomInstructionService') -// ============================================================ -// 타입 -// ============================================================ - -export interface CustomInstruction { - id: string - name: string - description: string - prompt: string - icon: string - isBuiltin: boolean - order: number - createdAt: number - updatedAt: number -} - type CreateInput = Omit // ============================================================ @@ -91,7 +76,7 @@ let initialized = false function loadInstructions(): CustomInstruction[] { // electron-store에서 로드 시도 try { - const stored = configGet('customInstructions' as never) as CustomInstruction[] | undefined + const stored = configGet('customInstructions') as CustomInstruction[] | undefined if (Array.isArray(stored) && stored.length > 0) { return stored } @@ -110,8 +95,7 @@ function loadInstructions(): CustomInstruction[] { function saveInstructions(): void { try { - const { configSet } = require('./ConfigService') - configSet('customInstructions' as never, instructions as never) + configSet('customInstructions', instructions) } catch (error) { logger.warn(`Failed to save instructions: ${error instanceof Error ? error.message : String(error)}`) } diff --git a/apps/desktop/src/main/services/MeetingModeService.ts b/apps/desktop/src/main/services/MeetingModeService.ts index 0818beb..b81e5f6 100644 --- a/apps/desktop/src/main/services/MeetingModeService.ts +++ b/apps/desktop/src/main/services/MeetingModeService.ts @@ -145,9 +145,8 @@ class MeetingModeService extends EventEmitter { // 회의 모드는 마이크 캡처 강제 — ConfigService 값도 임시 변경 // (CaptionService.start()가 ConfigService에서 다시 읽기 때문) - type AppConfigKey = keyof import('@d3ro/core/types').AppConfig - const prevAudioSource = configGet('captionAudioSource' as AppConfigKey) as unknown as string - configSet('captionAudioSource' as AppConfigKey, 'mic' as never) + const prevAudioSource = configGet('captionAudioSource') + configSet('captionAudioSource', 'mic') try { await captionService.start() @@ -156,12 +155,12 @@ class MeetingModeService extends EventEmitter { hideCaptionOverlay() // ConfigService 원래 값 복원 if (prevAudioSource) { - configSet('captionAudioSource' as AppConfigKey, prevAudioSource as never) + configSet('captionAudioSource', prevAudioSource) } } catch (err) { // ConfigService 원래 값 복원 if (prevAudioSource) { - configSet('captionAudioSource' as AppConfigKey, prevAudioSource as never) + configSet('captionAudioSource', prevAudioSource) } // 시작 실패 시 복원 captionService.off('segment', this._segmentHandler) diff --git a/apps/desktop/src/main/services/VoiceCommandService.ts b/apps/desktop/src/main/services/VoiceCommandService.ts index 1d4299e..3e30551 100644 --- a/apps/desktop/src/main/services/VoiceCommandService.ts +++ b/apps/desktop/src/main/services/VoiceCommandService.ts @@ -67,9 +67,8 @@ const DEFAULT_KEYWORDS: ReadonlyArray = [ // electron-store 키 (ConfigService와 별도 네임스페이스) // ============================================================ -// configGet/configSet에 타입이 없는 키를 사용하므로 as never 캐스팅 필요 -const STORE_KEY_RULES = 'voiceCommandRules' as never -const STORE_KEY_ENABLED = 'voiceCommandsEnabled' as never +const STORE_KEY_RULES: keyof import('@d3ro/core/types').AppConfig = 'voiceCommandRules' +const STORE_KEY_ENABLED: keyof import('@d3ro/core/types').AppConfig = 'voiceCommandsEnabled' // ============================================================ // 키워드 매칭 엔진 @@ -292,7 +291,7 @@ class VoiceCommandService { private saveRules(): void { try { - configSet(STORE_KEY_RULES, this.rules as never) + configSet(STORE_KEY_RULES, this.rules) } catch (error) { logger.warn( `Failed to save voice command rules: ${error instanceof Error ? error.message : String(error)}` @@ -311,7 +310,7 @@ class VoiceCommandService { private saveEnabled(): void { try { - configSet(STORE_KEY_ENABLED, this.enabled as never) + configSet(STORE_KEY_ENABLED, this.enabled) } catch (error) { logger.warn( `Failed to save voice command enabled state: ${error instanceof Error ? error.message : String(error)}` diff --git a/apps/desktop/src/main/services/VoiceModeService.ts b/apps/desktop/src/main/services/VoiceModeService.ts index e2f3f34..e6019b0 100644 --- a/apps/desktop/src/main/services/VoiceModeService.ts +++ b/apps/desktop/src/main/services/VoiceModeService.ts @@ -752,7 +752,7 @@ class VoiceModeService extends EventEmitter { if (action === 'chain') { try { const { getChainService } = await import('./ChainService') - const activeChainId = configGet('activeChainId' as keyof import('@d3ro/core/types').AppConfig) as unknown as string + const activeChainId = configGet('activeChainId') if (activeChainId) { const chainResult = await getChainService().execute(activeChainId, contextPrefix + transcribedText) if (this._isInTerminalState()) return @@ -769,7 +769,7 @@ class VoiceModeService extends EventEmitter { // 음성 단축키 오버라이드 또는 활성 명령어 const effectiveInstructionId = overrideInstructionId - ?? (configGet('activeInstructionId' as keyof import('@d3ro/core/types').AppConfig) as unknown as string) + ?? configGet('activeInstructionId') if (action === 'custom' || overrideInstructionId) { let customPrompt = contextPrefix + transcribedText diff --git a/apps/desktop/src/main/services/index.ts b/apps/desktop/src/main/services/index.ts deleted file mode 100644 index ec67098..0000000 --- a/apps/desktop/src/main/services/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -// src/main/services/index.ts — 서비스 레지스트리 - -export { initLoggerService, getLogger } from './LoggerService' -export { - initConfigService, - getConfigService, - configGet, - configSet, - configGetAll, - configReset, - onConfigChanged -} from './ConfigService' -export { - getLocalSTTService, - LocalSTTService, - STTState -} from './LocalSTTService' -export type { - TranscriptionResult, - TranscriptionSegment, - TranscribeOptions, - LocalSTTEvents -} from './LocalSTTService' -export { getHotkeyService } from './HotkeyService' -export { getVoiceModeService } from './VoiceModeService' -export { getAudioCaptureService } from './AudioCaptureService' -export { getTextInsertService } from './TextInsertService' -export { getLocalLLMService } from './LocalLLMService' -export { getHistoryService } from './HistoryService' -export { getDictionaryService } from './DictionaryService' -export { getCustomInstructionService } from './CustomInstructionService' diff --git a/apps/desktop/src/main/windows/WindowManager.ts b/apps/desktop/src/main/windows/WindowManager.ts index ed64a99..0c2a7fb 100644 --- a/apps/desktop/src/main/windows/WindowManager.ts +++ b/apps/desktop/src/main/windows/WindowManager.ts @@ -1,10 +1,11 @@ // src/main/windows/WindowManager.ts // 설계서 01 WindowManagerService: 메인 윈도우 + 팝업 프리로딩 + 2-phase 리사이즈 -import { BrowserWindow, shell, screen, ipcMain, Menu } from 'electron' +import { BrowserWindow, shell, screen, ipcMain, Menu, clipboard } from 'electron' import { join } from 'path' import { is } from '@electron-toolkit/utils' import { WINDOW_SIZE } from '@d3ro/core/constants' +import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getLogger } from '../services/LoggerService' import { getIsQuitting } from '../lifecycle' import { configGet } from '../services/ConfigService' @@ -180,7 +181,7 @@ export function showRecordingTip( win.setBounds({ x, y, width: TIP_WIDTH, height: TIP_HEIGHT }) // 상태 전송 후 즉시 show (2-phase 제거 — 숨겨진 윈도우의 렌더러 비활성 문제 방지) - win.webContents.send('window:tipStateChanged', { state, ...params }) + win.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params }) if (!win.isVisible()) { win.showInactive() } @@ -197,20 +198,20 @@ export function updateRecordingTipState( params?: { text?: string; errorMessage?: string } ): void { if (recordingTipWindow && !recordingTipWindow.isDestroyed()) { - recordingTipWindow.webContents.send('window:tipStateChanged', { state, ...params }) + recordingTipWindow.webContents.send(IPC_CHANNELS.WINDOW.TIP_STATE_CHANGED, { state, ...params }) } } export function sendAudioLevelToTip(level: number): void { if (recordingTipWindow && !recordingTipWindow.isDestroyed()) { - recordingTipWindow.webContents.send('voice:audioLevel', { level }) + recordingTipWindow.webContents.send(IPC_CHANNELS.VOICE.AUDIO_LEVEL, { level }) } } /** 실시간 부분 전사 텍스트를 RecordingTip에 전달 */ export function sendPartialTranscriptToTip(text: string): void { if (recordingTipWindow && !recordingTipWindow.isDestroyed()) { - recordingTipWindow.webContents.send('voice:partialTranscript', { text }) + recordingTipWindow.webContents.send(IPC_CHANNELS.VOICE_PARTIAL.PARTIAL_TRANSCRIPT, { text }) } } @@ -267,10 +268,10 @@ export function showResultPopup(text: string, autoHideMs = 5000): void { const win = getResultPopupWindow() // Phase 1: prepare - win.webContents.send('result:prepare', { text }) + win.webContents.send(IPC_CHANNELS.POPUP_RESULT.PREPARE, { text }) const handler = (_event: Electron.IpcMainEvent, data: { width: number; height: number }) => { - ipcMain.removeListener('result:measured', handler) + ipcMain.removeListener(IPC_CHANNELS.POPUP_RESULT.MEASURED, handler) const cursorPos = screen.getCursorScreenPoint() const display = screen.getDisplayNearestPoint(cursorPos) @@ -290,10 +291,10 @@ export function showResultPopup(text: string, autoHideMs = 5000): void { } // Phase 2: show - win.webContents.send('result:show', { autoHideMs }) + win.webContents.send(IPC_CHANNELS.POPUP_RESULT.SHOW, { autoHideMs }) } - ipcMain.on('result:measured', handler) + ipcMain.on(IPC_CHANNELS.POPUP_RESULT.MEASURED, handler) } export function hideResultPopup(): void { @@ -368,18 +369,18 @@ export function showHistoryPopup(entries: Array>): void win.setBounds({ x, y, width: popupWidth, height: popupHeight }) - win.webContents.send('history:showItems', { entries }) + win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW_ITEMS, { entries }) if (!win.isVisible()) { win.showInactive() } - win.webContents.send('history:show', {}) + win.webContents.send(IPC_CHANNELS.POPUP_HISTORY.SHOW, {}) } export function hideHistoryPopup(): void { if (historyPopupWindow && !historyPopupWindow.isDestroyed()) { - historyPopupWindow.webContents.send('history:hide', {}) + historyPopupWindow.webContents.send(IPC_CHANNELS.POPUP_HISTORY.HIDE, {}) setTimeout(() => { if (historyPopupWindow && !historyPopupWindow.isDestroyed()) { historyPopupWindow.hide() @@ -390,7 +391,7 @@ export function hideHistoryPopup(): void { export function sendKeyToHistoryPopup(key: string): void { if (historyPopupWindow && !historyPopupWindow.isDestroyed() && historyPopupWindow.isVisible()) { - historyPopupWindow.webContents.send('history:keyEvent', { key }) + historyPopupWindow.webContents.send(IPC_CHANNELS.POPUP_HISTORY.KEY_EVENT, { key }) } } @@ -455,15 +456,15 @@ export function showCommandPopup(commands: Array>, activ if (y < display.workArea.y) { y = cursorPos.y + 20 } win.setBounds({ x, y, width: popupWidth, height: popupHeight }) - win.webContents.send('command:showItems', { commands, activeId }) + win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW_ITEMS, { commands, activeId }) if (!win.isVisible()) { win.showInactive() } - win.webContents.send('command:show', {}) + win.webContents.send(IPC_CHANNELS.POPUP_COMMAND.SHOW, {}) } export function hideCommandPopup(): void { if (commandPopupWindow && !commandPopupWindow.isDestroyed()) { - commandPopupWindow.webContents.send('command:hide', {}) + commandPopupWindow.webContents.send(IPC_CHANNELS.POPUP_COMMAND.HIDE, {}) setTimeout(() => { if (commandPopupWindow && !commandPopupWindow.isDestroyed()) { commandPopupWindow.hide() @@ -474,7 +475,7 @@ export function hideCommandPopup(): void { export function sendKeyToCommandPopup(key: string): void { if (commandPopupWindow && !commandPopupWindow.isDestroyed() && commandPopupWindow.isVisible()) { - commandPopupWindow.webContents.send('command:keyEvent', { key }) + commandPopupWindow.webContents.send(IPC_CHANNELS.POPUP_COMMAND.KEY_EVENT, { key }) } } @@ -547,7 +548,7 @@ export function showCaptionOverlay(): void { export function hideCaptionOverlay(): void { if (captionOverlayWindow && !captionOverlayWindow.isDestroyed()) { - captionOverlayWindow.webContents.send('caption:hide', {}) + captionOverlayWindow.webContents.send(IPC_CHANNELS.POPUP_CAPTION.HIDE, {}) captionOverlayWindow.hide() } } @@ -591,7 +592,6 @@ export function reapplyThemeToAllPopups(): void { // ── clipboard:copy IPC (ResultPopup에서 사용) ───────── -ipcMain.on('clipboard:copy', (_event, text: string) => { - const { clipboard } = require('electron') +ipcMain.on(IPC_CHANNELS.CLIPBOARD.COPY, (_event, text: string) => { clipboard.writeText(text) }) diff --git a/packages/core/src/ipc-channels.ts b/packages/core/src/ipc-channels.ts index 762b244..a251624 100644 --- a/packages/core/src/ipc-channels.ts +++ b/packages/core/src/ipc-channels.ts @@ -279,6 +279,7 @@ export const IPC_CHANNELS = { GET_HISTORY: 'voiceConversation:getHistory', CLEAR_HISTORY: 'voiceConversation:clearHistory', CANCEL_RESPONSE: 'voiceConversation:cancelResponse', + FINISH_LISTENING: 'voiceConversation:finishListening', /** OpenAI Realtime ephemeral token 발급 (Supabase realtime-token 경유) */ GET_REALTIME_TOKEN: 'voiceConversation:getRealtimeToken', // Main → Renderer events @@ -399,6 +400,70 @@ export const IPC_CHANNELS = { SYNC_COMPLETE: 'cloudSync:syncComplete', SYNC_ERROR: 'cloudSync:syncError', }, + + // ── Custom Instructions ── + INSTRUCTION: { + GET_ALL: 'instruction:getAll', + GET_BY_ID: 'instruction:getById', + CREATE: 'instruction:create', + UPDATE: 'instruction:update', + DELETE: 'instruction:delete', + REORDER: 'instruction:reorder', + }, + + // ── System Audio (Caption loopback) ── + SYSTEM_AUDIO: { + ENABLE_LOOPBACK: 'system-audio:enable-loopback', + DISABLE_LOOPBACK: 'system-audio:disable-loopback', + }, + + // ── Popup Internal Channels (ResultPopup 2-phase) ── + POPUP_RESULT: { + PREPARE: 'result:prepare', + MEASURED: 'result:measured', + SHOW: 'result:show', + }, + + // ── Popup Internal Channels (HistoryPopup) ── + POPUP_HISTORY: { + SHOW_POPUP: 'history:showPopup', + ITEM_SELECTED: 'history:itemSelected', + POPUP_DISMISSED: 'history:popupDismissed', + SHOW_ITEMS: 'history:showItems', + SHOW: 'history:show', + HIDE: 'history:hide', + KEY_EVENT: 'history:keyEvent', + }, + + // ── Popup Internal Channels (CommandPopup) ── + POPUP_COMMAND: { + SELECTED: 'command:selected', + DISMISSED: 'command:dismissed', + SHOW_ITEMS: 'command:showItems', + SHOW: 'command:show', + HIDE: 'command:hide', + KEY_EVENT: 'command:keyEvent', + }, + + // ── Popup Internal Channels (Caption) ── + POPUP_CAPTION: { + HIDE: 'caption:hide', + }, + + // ── Voice Partial Transcript (RecordingTip) ── + VOICE_PARTIAL: { + PARTIAL_TRANSCRIPT: 'voice:partialTranscript', + }, + + // ── Clipboard ── + CLIPBOARD: { + COPY: 'clipboard:copy', + }, + + // ── App-wide events ── + APP: { + DATA_CHANGED: 'app:dataChanged', + }, } as const // 타입 유틸리티: 채널명 유니온 추출 diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 985ef34..e81afec 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -410,6 +410,20 @@ export interface AppConfig { cloudSyncLastAt: number | null /** 빅뱅 Phase 2: 첫 실행 온보딩 완료 여부 (로컬 모드 entry point) */ onboardingCompleted: boolean + /** Phase 6: Custom instructions (CustomInstructionService) */ + customInstructions: import('@d3ro/core/types').CustomInstruction[] + /** Phase 10.4: LLM chains (ChainService) */ + llmChains: import('@d3ro/core/types').LLMChain[] + /** Phase 10.5: Voice command rules (VoiceCommandService) */ + voiceCommandRules: import('@d3ro/core/types').VoiceCommandRule[] + /** Phase 10.5: Voice commands enabled flag */ + voiceCommandsEnabled: boolean + /** Phase 6: Active instruction ID (CommandPopup) */ + activeInstructionId: string + /** Phase 10.4: Active chain ID (VoiceModeService) */ + activeChainId: string | null + /** Phase 10.1: Caption audio source (CaptionService, MeetingModeService) */ + captionAudioSource: import('@d3ro/core/types').CaptionAudioSource } export interface ConfigGetParams { @@ -777,6 +791,22 @@ export interface SetVoiceCommandEnabledParams { enabled: boolean } +// ============================================================ +// Phase 6: Custom Instructions +// ============================================================ + +export interface CustomInstruction { + id: string + name: string + description: string + prompt: string + icon: string + isBuiltin: boolean + order: number + createdAt: number + updatedAt: number +} + // ============================================================ // Phase 10: Screen Context (10.2) // ============================================================