3대 근본 버그 수정 (RE 조사 기반)

1. HotkeyService: modifier 먼저 놓기 대응
   - Alt+1에서 Alt를 먼저 놓아도 release 감지
   - _handleKeyUp에서 pressed 핫키의 구성 키(main/modifier) 매칭

2. ResultPopup auto-close IPC 핸들러 추가
   - window:hideResultPopup 핸들러가 없어서 팝업이 안 꺼지던 버그
   - window-handlers.ts에 hideResultPopup/hideRecordingTip 핸들러 등록

3. bootstrap.ts 이중 팝업 제어 제거
   - session-completed에서 무조건 showResultPopup 호출하던 코드 제거
   - 팝업 제어는 VoiceModeService 내부에서만 처리
   - bootstrap은 효과음만 담당
This commit is contained in:
Yun Chan 2026-04-05 10:50:18 +09:00
parent 788600b66b
commit c64ab3b58f
3 changed files with 49 additions and 29 deletions

View file

@ -15,11 +15,6 @@ import { initDatabase } from './db'
import { import {
createMainWindow, createMainWindow,
preloadPopupWindows, preloadPopupWindows,
showRecordingTip,
hideRecordingTip,
updateRecordingTipState,
sendAudioLevelToTip,
showResultPopup,
showHistoryPopup, showHistoryPopup,
hideHistoryPopup, hideHistoryPopup,
sendKeyToHistoryPopup, sendKeyToHistoryPopup,
@ -134,28 +129,13 @@ async function initVoiceMode(): Promise<void> {
const soundEffect = getSoundEffectService() const soundEffect = getSoundEffectService()
// RecordingTip 연동: 세션 시작/종료 시 팝업 표시/숨김 // 효과음 연동 (팝업 제어는 VoiceModeService 내부에서 처리)
voiceMode.on('session-started', () => { voiceMode.on('session-started', () => {
soundEffect.play('recording-start') 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 }) => { voiceMode.on('session-completed', ({ session, finalText }) => {
soundEffect.play('recording-stop') soundEffect.play('recording-stop')
hideRecordingTip()
if (finalText.length > 0) {
showResultPopup(finalText)
}
// 이력 저장 // 이력 저장
try { try {

View file

@ -3,7 +3,7 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels' import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ipcSuccess } from '@shared/errors' import { ipcSuccess } from '@shared/errors'
import { getMainWindow } from '../windows/WindowManager' import { getMainWindow, hideResultPopup, hideRecordingTip } from '../windows/WindowManager'
export function registerWindowHandlers(): void { export function registerWindowHandlers(): void {
ipcMain.on(IPC_CHANNELS.WINDOW.MINIMIZE, () => { ipcMain.on(IPC_CHANNELS.WINDOW.MINIMIZE, () => {
@ -28,4 +28,13 @@ export function registerWindowHandlers(): void {
ipcMain.handle(IPC_CHANNELS.WINDOW.IS_MAXIMIZED, async () => { ipcMain.handle(IPC_CHANNELS.WINDOW.IS_MAXIMIZED, async () => {
return ipcSuccess(getMainWindow()?.isMaximized() ?? false) return ipcSuccess(getMainWindow()?.isMaximized() ?? false)
}) })
// 팝업 윈도우 hide 요청 (렌더러 → 메인)
ipcMain.on('window:hideResultPopup', () => {
hideResultPopup()
})
ipcMain.on('window:hideRecordingTip', () => {
hideRecordingTip()
})
} }

View file

@ -398,17 +398,36 @@ class HotkeyService extends EventEmitter {
/** /**
* . * .
* Alt+1 Alt를 release .
* 전략: pressed (main key modifier) release.
*/ */
private _handleKeyUp(e: UiohookKeyboardEvent): void { private _handleKeyUp(e: UiohookKeyboardEvent): void {
// 방법 1: 정확한 매칭 시도
const matched = this._findMatchingHotkey(e) const matched = this._findMatchingHotkey(e)
if (!matched) return if (matched && this._isKeyDown.get(matched.id)) {
this._fireRelease(matched)
const { id } = matched
// 눌려있지 않은 키의 release는 무시
if (!this._isKeyDown.get(id)) {
return 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) this._isKeyDown.set(id, false)
const now = Date.now() const now = Date.now()
@ -418,12 +437,24 @@ class HotkeyService extends EventEmitter {
logger.debug(`Hotkey released: "${id}" (duration=${durationMs}ms)`) logger.debug(`Hotkey released: "${id}" (duration=${durationMs}ms)`)
this.emit('hotkey-released', { this.emit('hotkey-released', {
config: matched, config,
durationMs, durationMs,
timestamp: now 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 . * uiohook .
* . * .