diff --git a/src/main/bootstrap.ts b/src/main/bootstrap.ts index 4128fde..57f55da 100644 --- a/src/main/bootstrap.ts +++ b/src/main/bootstrap.ts @@ -15,11 +15,6 @@ import { initDatabase } from './db' import { createMainWindow, preloadPopupWindows, - showRecordingTip, - hideRecordingTip, - updateRecordingTipState, - sendAudioLevelToTip, - showResultPopup, showHistoryPopup, hideHistoryPopup, sendKeyToHistoryPopup, @@ -134,28 +129,13 @@ async function initVoiceMode(): Promise { const soundEffect = getSoundEffectService() - // RecordingTip 연동: 세션 시작/종료 시 팝업 표시/숨김 + // 효과음 연동 (팝업 제어는 VoiceModeService 내부에서 처리) voiceMode.on('session-started', () => { soundEffect.play('recording-start') - showRecordingTip('recording') - }) - - voiceMode.on('audio-level', ({ level }) => { - sendAudioLevelToTip(level) - }) - - voiceMode.on('recognition-state-changed', ({ current }) => { - if (current === 'recognizing') { - updateRecordingTipState('thinking') - } }) voiceMode.on('session-completed', ({ session, finalText }) => { soundEffect.play('recording-stop') - hideRecordingTip() - if (finalText.length > 0) { - showResultPopup(finalText) - } // 이력 저장 try { diff --git a/src/main/ipc/window-handlers.ts b/src/main/ipc/window-handlers.ts index 602dd04..e59f32d 100644 --- a/src/main/ipc/window-handlers.ts +++ b/src/main/ipc/window-handlers.ts @@ -3,7 +3,7 @@ import { ipcMain } from 'electron' import { IPC_CHANNELS } from '@shared/ipc-channels' import { ipcSuccess } from '@shared/errors' -import { getMainWindow } from '../windows/WindowManager' +import { getMainWindow, hideResultPopup, hideRecordingTip } from '../windows/WindowManager' export function registerWindowHandlers(): void { ipcMain.on(IPC_CHANNELS.WINDOW.MINIMIZE, () => { @@ -28,4 +28,13 @@ export function registerWindowHandlers(): void { ipcMain.handle(IPC_CHANNELS.WINDOW.IS_MAXIMIZED, async () => { return ipcSuccess(getMainWindow()?.isMaximized() ?? false) }) + + // 팝업 윈도우 hide 요청 (렌더러 → 메인) + ipcMain.on('window:hideResultPopup', () => { + hideResultPopup() + }) + + ipcMain.on('window:hideRecordingTip', () => { + hideRecordingTip() + }) } diff --git a/src/main/services/HotkeyService.ts b/src/main/services/HotkeyService.ts index 9f7cfa4..60bdee8 100644 --- a/src/main/services/HotkeyService.ts +++ b/src/main/services/HotkeyService.ts @@ -398,17 +398,36 @@ class HotkeyService extends EventEmitter { /** * 키 업 이벤트를 처리한다. + * Alt+1 같은 조합에서 Alt를 먼저 놓아도 release 감지해야 한다. + * 전략: pressed 상태인 핫키 중 구성 키(main key 또는 modifier)가 놓아지면 release. */ private _handleKeyUp(e: UiohookKeyboardEvent): void { + // 방법 1: 정확한 매칭 시도 const matched = this._findMatchingHotkey(e) - if (!matched) return - - const { id } = matched - - // 눌려있지 않은 키의 release는 무시 - if (!this._isKeyDown.get(id)) { + if (matched && this._isKeyDown.get(matched.id)) { + this._fireRelease(matched) return } + + // 방법 2: 현재 pressed인 핫키 중 구성 키가 놓아진 경우 (modifier 먼저 놓기 대응) + for (const config of this._registeredHotkeys.values()) { + if (!this._isKeyDown.get(config.id)) continue + + // 놓아진 키가 이 핫키의 main key인가? + const isMainKey = e.keycode === config.keyCode + // 놓아진 키가 이 핫키의 modifier 중 하나인가? + const isModifier = this._isEventModifierOf(e, config) + + if (isMainKey || isModifier) { + this._fireRelease(config) + return + } + } + } + + /** pressed 핫키의 release 이벤트 발행 */ + private _fireRelease(config: HotkeyConfig): void { + const { id } = config this._isKeyDown.set(id, false) const now = Date.now() @@ -418,12 +437,24 @@ class HotkeyService extends EventEmitter { logger.debug(`Hotkey released: "${id}" (duration=${durationMs}ms)`) this.emit('hotkey-released', { - config: matched, + config, durationMs, timestamp: now }) } + /** 키 이벤트가 핫키의 modifier 구성요소인지 확인 */ + private _isEventModifierOf(e: UiohookKeyboardEvent, config: HotkeyConfig): boolean { + const modifiers = config.modifiers ?? [] + for (const mod of modifiers) { + if (mod === 'ctrl' && (this._isCtrlKeyCode(e.keycode))) return true + if (mod === 'alt' && (this._isAltKeyCode(e.keycode))) return true + if (mod === 'shift' && (this._isShiftKeyCode(e.keycode))) return true + if (mod === 'meta' && (this._isMetaKeyCode(e.keycode))) return true + } + return false + } + /** * uiohook 키 이벤트와 등록된 핫키를 매칭한다. * 키코드와 수정자 키가 모두 일치해야 매칭 성공.