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:
parent
b820c789bb
commit
aef44289a5
15 changed files with 161 additions and 126 deletions
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)}`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
// 타입 유틸리티: 채널명 유니온 추출
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue