refactor(main): IPC 채널 SSOT 일원화 + AppConfig 타입 강화 (WS2)

- 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
This commit is contained in:
Yun Chan 2026-07-22 01:54:18 +09:00
parent b820c789bb
commit aef44289a5
15 changed files with 161 additions and 126 deletions

View file

@ -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<void> {
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<Record<string, unknown>>, activeId || null)
registerPopupNavKeys('command')
}
@ -250,7 +252,7 @@ async function initVoiceMode(): Promise<void> {
}
// 렌더러 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<void> {
}
/** 메인 윈도우 렌더러에 UI 갱신 이벤트 전송 */
function notifyRenderer(channel: string, data?: Record<string, unknown>): void {
function notifyRenderer(channel: IPCChannel, data?: Record<string, unknown>): void {
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send(channel, data ?? {})
@ -330,13 +332,13 @@ async function initMeetingMode(): Promise<void> {
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<Record<string, unknown>>)
})
// 아이템 선택 → 텍스트 삽입
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()
})

View file

@ -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)
})

View file

@ -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)

View file

@ -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<CustomInstruction> }) => {
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)
})

View file

@ -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)

View file

@ -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()
})

View file

@ -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)}`

View file

@ -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<CustomInstruction, 'id' | 'isBuiltin' | 'order' | 'createdAt' | 'updatedAt'>
// ============================================================
@ -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)}`)
}

View file

@ -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)

View file

@ -67,9 +67,8 @@ const DEFAULT_KEYWORDS: ReadonlyArray<DefaultKeywordEntry> = [
// 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)}`

View file

@ -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

View file

@ -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'

View file

@ -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<Record<string, unknown>>): 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<Record<string, unknown>>, 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)
})

View file

@ -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
// 타입 유틸리티: 채널명 유니온 추출

View file

@ -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)
// ============================================================