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

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