feat(icon)+fix(audio): 앱 아이콘 적용 + 트레이 아이콘 + 마이크 테스트 조기 종료 수정
Some checks failed
Build macOS / Build & Package (macOS) (push) Failing after 5s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s

- 앱 아이콘: icon.svg 마스터(d3ro 브랜드 — 메탈 섀시+오렌지 웨이브) → png/ico
  생성 스크립트(generate-icons.mjs, sharp) + electron-builder win/mac 연결
- 트레이: createEmpty() 빈 아이콘 → 실제 앱 아이콘 (getAppIconPath)
- 마이크 테스트: 초기 무음 level:0을 종료로 오인하던 조기 종료 버그 —
  AUDIO.TEST_LEVEL에 done 플래그 신설(SSOT), STOP 버튼이 실제 캡처 중지,
  testDevice 실패 시 상태 롤백
This commit is contained in:
Yun Chan 2026-07-21 19:00:23 +09:00
parent 26b3fd1f63
commit cbed451209
12 changed files with 184 additions and 11 deletions

View file

@ -71,12 +71,13 @@ export function registerAudioHandlers(): void {
}
service.on('audio-data', testAudioHandler)
// 100ms 간격으로 레벨을 렌더러에 전송
// 100ms 간격으로 레벨을 렌더러에 전송.
// level 0은 "무음/초기화 중"일 뿐 종료가 아님 — 종료는 done:true로만 알린다.
testLevelInterval = setInterval(() => {
const level = testLastRms > 0 ? Math.min(1.0, Math.pow(testLastRms, 0.28)) : 0
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send('audio:testLevel', { level })
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level, done: false })
}
}, 100)
@ -85,7 +86,7 @@ export function registerAudioHandlers(): void {
stopAudioTest()
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send('audio:testLevel', { level: 0 })
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true })
}
}, 5000)
@ -98,4 +99,14 @@ export function registerAudioHandlers(): void {
)
}
})
// 테스트 수동 중지 (STOP 버튼)
ipcMain.handle(IPC_CHANNELS.AUDIO.STOP_TEST, async () => {
stopAudioTest()
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send(IPC_CHANNELS.AUDIO.TEST_LEVEL, { level: 0, done: true })
}
return ipcSuccess(undefined)
})
}

View file

@ -151,3 +151,17 @@ export function getUserDataPath(): string {
export function getWhisperModelsDir(): string {
return path.join(app.getPath('userData'), 'whisper-models')
}
/**
* (/).
* - production: extraResources로 icons/ (win은 ico DPI )
* - dev: build/icon.png
* null ( ).
*/
export function getAppIconPath(): string | null {
const filename = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
const candidate = isPackaged()
? path.join(process.resourcesPath, 'icons', filename)
: path.join(app.getAppPath(), 'build', filename)
return existsSync(candidate) ? candidate : null
}

View file

@ -1,9 +1,9 @@
// src/main/windows/TrayManager.ts
import { Tray, Menu, app, nativeImage } from 'electron'
import { join } from 'path'
import { getMainWindow } from './WindowManager'
import { getLogger } from '../services/LoggerService'
import { getAppIconPath } from '../utils/paths'
import { setIsQuitting } from '../lifecycle'
const logger = getLogger('TrayManager')
@ -11,8 +11,9 @@ const logger = getLogger('TrayManager')
let tray: Tray | null = null
export function createTray(): void {
// 16x16 빈 아이콘 생성 (리소스 아이콘이 없을 때 폴백)
const icon = nativeImage.createEmpty()
// 앱 아이콘 (win=멀티사이즈 ico, 그 외=png). 없으면 빈 아이콘 폴백.
const iconPath = getAppIconPath()
const icon = iconPath ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty()
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([

View file

@ -203,10 +203,11 @@ const electronAPI = {
invoke<void>(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, params),
testDevice: (params: TestDeviceParams) =>
invoke<TestDeviceResult>(IPC_CHANNELS.AUDIO.TEST_DEVICE, params),
stopTest: () => invoke<void>(IPC_CHANNELS.AUDIO.STOP_TEST),
onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb),
onTestLevel: (cb: (e: { level: number }) => void): Unsubscribe =>
on('audio:testLevel', cb),
onTestLevel: (cb: (e: { level: number; done: boolean }) => void): Unsubscribe =>
on(IPC_CHANNELS.AUDIO.TEST_LEVEL, cb),
},
// ── Config ─────────────────────────────────────────────

View file

@ -547,19 +547,28 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
size="small"
onClick={async () => {
if (micTesting) {
// 메인 프로세스 캡처까지 실제로 중지 — done 이벤트가 구독 해제를 마무리
await window.electronAPI.audio.stopTest()
setMicTesting(false)
setMicLevel(0)
} else {
setMicTesting(true)
setMicLevel(0)
// level 0은 무음/초기화 중일 뿐 — 종료 판정은 done 플래그로만
const unsub = window.electronAPI.audio.onTestLevel((e) => {
setMicLevel(e.level)
if (e.level === 0) {
if (e.done) {
setMicTesting(false)
setMicLevel(0)
unsub()
}
})
await window.electronAPI.audio.testDevice({ deviceId: 'default' })
const result = await window.electronAPI.audio.testDevice({ deviceId: 'default' })
if (!result.success) {
setMicTesting(false)
setMicLevel(0)
unsub()
}
}
}}
sx={{ fontFamily: d3roFontMono, fontSize: '11px', minWidth: 80 }}