Phase 7~8 구현: 테스트 + 빌드 + CI/CD + SoundEffect + AutoLaunch + UI 리디자인

- vitest 41개 단위 테스트 (HistoryService, DictionaryService, CustomInstructionService, VoiceModeService, D3ROError)
- electron-builder.yml (NSIS, asarUnpack, extraResources)
- .gitlab-ci.yml (lint, typecheck, test, build, release)
- SoundEffectService: WAV 프리로드 + PowerShell 재생 + VoiceMode 연동
- AutoLaunchService: app.setLoginItemSettings + ConfigService 동기화
- TextInsertService: 간이 삽입 검증 (EditMonitor 경량)
- 번들링 인프라: SoX 다운로드 스크립트, PyInstaller 빌드, 경로 해상도 유틸
- AudioCaptureService/LocalSTTService: 번들 경로 자동 감지
- 08-design-system.md 기반 MUI 테마 (다크+라이트+auto 테마 시스템)
- 전체 UI 컴포넌트 리디자인: AppLayout, Dashboard, StatusBar, History, Dictionary, Commands, Settings
- 효과음 WAV 생성: recording-start, recording-stop, error
- EPIPE 에러 핸들링 추가
This commit is contained in:
Yun Chan 2026-04-05 09:12:56 +09:00
parent ed5541f769
commit 3f4d0c5828
40 changed files with 6034 additions and 580 deletions

View file

@ -1,26 +1,40 @@
// src/renderer/components/StatusBar.tsx
// 하단 상태 표시: Ollama 연결 상태
// 하단 상태 표시: LED 인디케이터 + 태그 시스템. 08-design-system.md SSOT.
import { useState, useEffect } from 'react'
import { Box, Chip } from '@mui/material'
import CircleIcon from '@mui/icons-material/Circle'
import { Box, Typography, Chip } from '@mui/material'
import { d3roPalette, d3roFontMono } from '../theme'
import type { LLMStatus } from '@shared/types'
function Led({ active, color }: { active: boolean; color?: string }): React.ReactElement {
const c = color ?? d3roPalette.tag.green
return (
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: active ? c : d3roPalette.text.disabled,
boxShadow: active ? `0 0 4px ${c}, 0 0 8px ${c}40` : 'none',
flexShrink: 0,
transition: 'background 0.3s ease, box-shadow 0.3s ease',
}}
/>
)
}
export function StatusBar(): React.ReactElement {
const [llmStatus, setLlmStatus] = useState<LLMStatus | null>(null)
useEffect(() => {
// 초기 상태 조회
window.electronAPI.llm.getStatus().then((result) => {
if (result.success) setLlmStatus(result.data)
})
// 상태 변경 구독
const unsub = window.electronAPI.llm.onStatusChanged((event) => {
setLlmStatus(event.status)
})
// 5초마다 폴링 (main에서 이벤트를 보내지 않을 수 있으므로)
const interval = setInterval(() => {
window.electronAPI.llm.getStatus().then((result) => {
if (result.success) setLlmStatus(result.data)
@ -40,30 +54,53 @@ export function StatusBar(): React.ReactElement {
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
gap: 2,
px: 2,
py: 0.5,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper'
py: 0.75,
borderTop: `1px solid ${d3roPalette.border.subtle}`,
bgcolor: 'background.paper',
minHeight: 32,
}}
>
<Chip
icon={<CircleIcon sx={{ fontSize: 8 }} />}
label={connected ? 'Ollama Connected' : 'Ollama Offline'}
size="small"
variant="outlined"
color={connected ? 'success' : 'default'}
sx={{ height: 22, '& .MuiChip-label': { fontSize: 11 } }}
/>
{/* Ollama 상태 */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Led active={connected} color={connected ? d3roPalette.tag.green : d3roPalette.tag.red} />
<Typography
sx={{
fontFamily: d3roFontMono,
fontSize: '11px',
color: d3roPalette.text.secondary,
letterSpacing: '0.02em',
}}
>
{connected ? 'OLLAMA' : 'OFFLINE'}
</Typography>
</Box>
{/* 활성 모델 태그 */}
{llmStatus?.activeModel && (
<Chip
label={llmStatus.activeModel}
size="small"
variant="outlined"
sx={{ height: 22, '& .MuiChip-label': { fontSize: 11 } }}
color="primary"
sx={{ height: 20, '& .MuiChip-label': { px: 1, fontSize: '10px' } }}
/>
)}
{/* 스페이서 */}
<Box sx={{ flex: 1 }} />
{/* 핫키 힌트 */}
<Typography
sx={{
fontFamily: d3roFontMono,
fontSize: '10px',
color: d3roPalette.text.label,
letterSpacing: '0.05em',
}}
>
RIGHT ALT DICTATE
</Typography>
</Box>
)
}