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,148 +1,192 @@
// src/renderer/theme.ts — MUI 7 테마 정의 (설계서 03 기반)
// src/renderer/theme.ts
// 08-design-system.md SSOT 기반 MUI 테마.
// D3RO 다크(기본) + 라이트 + auto(시스템). 나중에 커스텀 테마 추가 가능.
import { createTheme, type ThemeOptions } from '@mui/material/styles'
import { createTheme, type Theme } from '@mui/material/styles'
const commonOptions: ThemeOptions = {
typography: {
fontFamily: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif'
].join(','),
h4: { fontWeight: 600, fontSize: '1.5rem' },
h5: { fontWeight: 600, fontSize: '1.25rem' },
h6: { fontWeight: 600, fontSize: '1rem' },
subtitle1: { fontWeight: 500 },
body1: { fontSize: '0.9375rem' },
body2: { fontSize: '0.8125rem' },
button: { textTransform: 'none' as const, fontWeight: 500 }
// ── SSOT: 디자인 시스템 팔레트 상수 ───────────────────────
export const d3roPalette = {
bg: {
app: '#19191b',
card: '#242427',
cardHover: '#2a2a2d',
elevated: '#2e2e32',
input: '#1e1e21',
},
shape: {
borderRadius: 12
accent: {
amber: '#f25b29',
amberDim: 'rgba(242, 91, 41, 0.15)',
amberGlow: 'rgba(242, 91, 41, 0.6)',
},
components: {
MuiButton: {
defaultProps: {
disableElevation: true
},
styleOverrides: {
root: {
textTransform: 'none',
fontWeight: 500,
borderRadius: 8,
padding: '8px 16px'
}
}
},
MuiCard: {
defaultProps: {
elevation: 0
},
styleOverrides: {
root: {
borderRadius: 12,
border: '1px solid'
}
}
},
MuiDrawer: {
styleOverrides: {
paper: {
width: 240,
borderRight: 'none'
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
borderRadius: 8,
marginLeft: 8,
marginRight: 8
}
}
},
MuiTextField: {
defaultProps: {
size: 'small',
variant: 'outlined'
}
},
MuiChip: {
styleOverrides: {
root: {
borderRadius: 6,
fontWeight: 500
}
}
}
}
tag: {
purple: '#b854f5',
purpleBg: 'rgba(184, 84, 245, 0.12)',
orange: '#f59e0b',
orangeBg: 'rgba(245, 158, 11, 0.12)',
red: '#ef4444',
redBg: 'rgba(239, 68, 68, 0.12)',
green: '#22c55e',
greenBg: 'rgba(34, 197, 94, 0.12)',
},
text: {
primary: '#ffffff',
secondary: '#8e8e93',
label: '#7c7c82',
disabled: '#4a4a4e',
},
border: {
subtle: 'rgba(255, 255, 255, 0.04)',
default: 'rgba(255, 255, 255, 0.08)',
strong: 'rgba(255, 255, 255, 0.12)',
},
crt: {
phosphor: '#f25b29',
phosphorDim: '#c44a22',
scanline: 'rgba(0, 0, 0, 0.15)',
bg: '#242528',
},
} as const
export const d3roFontSans = [
'-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto',
'"Helvetica Neue"', 'Arial', 'sans-serif',
].join(',')
export const d3roFontMono = [
'ui-monospace', 'SFMono-Regular', '"SF Mono"', 'Menlo', 'Consolas',
'"Liberation Mono"', 'monospace',
].join(',')
// ── 모드별 변동 팔레트 ────────────────────────────────────
interface ModePalette {
bg: { app: string; card: string; cardHover: string; elevated: string; input: string }
text: { primary: string; secondary: string; label: string; disabled: string }
border: { subtle: string; default: string; strong: string }
}
export const lightTheme = createTheme({
...commonOptions,
palette: {
mode: 'light',
primary: {
main: 'rgb(31, 93, 242)',
light: 'rgb(71, 133, 255)',
dark: 'rgb(20, 65, 180)',
contrastText: '#FFFFFF'
},
secondary: {
main: 'rgb(108, 117, 125)',
light: 'rgb(173, 181, 189)',
dark: 'rgb(73, 80, 87)'
},
background: {
default: '#F9F9F9',
paper: '#FFFFFF'
},
text: {
primary: 'rgba(0, 0, 0, 0.87)',
secondary: 'rgba(0, 0, 0, 0.6)'
},
divider: 'rgba(0, 0, 0, 0.08)',
error: { main: '#D32F2F' },
success: { main: '#2E7D32' },
warning: { main: '#ED6C02' }
}
})
const darkPalette: ModePalette = {
bg: { app: '#19191b', card: '#242427', cardHover: '#2a2a2d', elevated: '#2e2e32', input: '#1e1e21' },
text: { primary: '#ffffff', secondary: '#8e8e93', label: '#7c7c82', disabled: '#4a4a4e' },
border: { subtle: 'rgba(255,255,255,0.04)', default: 'rgba(255,255,255,0.08)', strong: 'rgba(255,255,255,0.12)' },
}
export const darkTheme = createTheme({
...commonOptions,
palette: {
mode: 'dark',
primary: {
main: 'rgb(71, 133, 255)',
light: 'rgb(120, 170, 255)',
dark: 'rgb(31, 93, 242)',
contrastText: '#FFFFFF'
},
secondary: {
main: 'rgb(173, 181, 189)',
light: 'rgb(206, 212, 218)',
dark: 'rgb(108, 117, 125)'
},
background: {
default: '#121212',
paper: '#1E1E1E'
},
text: {
primary: 'rgba(255, 255, 255, 0.87)',
secondary: 'rgba(255, 255, 255, 0.6)'
},
divider: 'rgba(255, 255, 255, 0.08)',
error: { main: '#EF5350' },
success: { main: '#4CAF50' },
warning: { main: '#FFA726' }
}
})
const lightPalette: ModePalette = {
bg: { app: '#f5f5f7', card: '#ffffff', cardHover: '#fafafa', elevated: '#f0f0f2', input: '#ffffff' },
text: { primary: '#1a1a1c', secondary: '#6e6e73', label: '#8e8e93', disabled: '#c7c7cc' },
border: { subtle: 'rgba(0,0,0,0.04)', default: 'rgba(0,0,0,0.08)', strong: 'rgba(0,0,0,0.12)' },
}
export function getTheme(mode: 'light' | 'dark') {
// ── 테마 팩토리 ───────────────────────────────────────────
function createD3ROTheme(mode: 'dark' | 'light'): Theme {
const isDark = mode === 'dark'
const p = isDark ? darkPalette : lightPalette
const accent = d3roPalette.accent
return createTheme({
palette: {
mode,
primary: { main: accent.amber, light: '#ff7a4d', dark: '#c44a22', contrastText: '#fff' },
secondary: { main: d3roPalette.tag.purple, light: '#d084ff', dark: '#8a3cc4' },
error: { main: d3roPalette.tag.red },
warning: { main: d3roPalette.tag.orange },
success: { main: d3roPalette.tag.green },
background: { default: p.bg.app, paper: p.bg.card },
text: { primary: p.text.primary, secondary: p.text.secondary, disabled: p.text.disabled },
divider: p.border.default,
},
typography: {
fontFamily: d3roFontSans,
h4: { fontWeight: 700, fontSize: '22px', lineHeight: 1.3 },
h5: { fontWeight: 700, fontSize: '18px', lineHeight: 1.4 },
h6: { fontWeight: 600, fontSize: '14px', lineHeight: 1.5 },
subtitle1: { fontWeight: 500, fontSize: '18px', lineHeight: 1.4 },
body1: { fontSize: '14px', lineHeight: 1.5 },
body2: { fontSize: '12px', lineHeight: 1.4 },
button: { textTransform: 'none' as const, fontWeight: 600, fontSize: '14px' },
caption: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, color: p.text.label },
overline: { fontSize: '11px', fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' as const, lineHeight: 1.2 },
},
shape: { borderRadius: 22 },
components: {
MuiCssBaseline: {
styleOverrides: { body: { backgroundColor: p.bg.app, color: p.text.primary } },
},
MuiButton: {
defaultProps: { disableElevation: true },
styleOverrides: {
root: {
textTransform: 'none', fontWeight: 600, borderRadius: 10, padding: '10px 20px',
transition: 'transform 0.05s linear, box-shadow 0.05s linear',
boxShadow: '0 2px 0 rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06)',
'&:active': { transform: 'translateY(2px)', boxShadow: '0 0 0 rgba(0,0,0,0.4), inset 0 2px 4px rgba(0,0,0,0.3)' },
},
containedPrimary: { '&:hover': { backgroundColor: '#d94f24' } },
containedSecondary: { backgroundColor: p.bg.elevated, color: p.text.primary, '&:hover': { backgroundColor: isDark ? '#353539' : '#e5e5e7' } },
},
},
MuiCard: {
defaultProps: { elevation: 0 },
styleOverrides: {
root: {
backgroundColor: p.bg.card, borderRadius: 22,
borderTop: `1px solid ${p.border.subtle}`,
boxShadow: isDark ? '0 8px 30px rgba(0,0,0,0.3)' : '0 4px 20px rgba(0,0,0,0.06)',
transition: 'background-color 0.2s ease',
'&:hover': { backgroundColor: p.bg.cardHover },
},
},
},
MuiChip: {
styleOverrides: {
root: { borderRadius: 999, fontWeight: 700, fontSize: '11px', letterSpacing: '0.1em', textTransform: 'uppercase', height: 24 },
colorPrimary: { backgroundColor: accent.amberDim, color: accent.amber },
colorSecondary: { backgroundColor: d3roPalette.tag.purpleBg, color: d3roPalette.tag.purple },
colorSuccess: { backgroundColor: d3roPalette.tag.greenBg, color: d3roPalette.tag.green },
colorError: { backgroundColor: d3roPalette.tag.redBg, color: d3roPalette.tag.red },
colorWarning: { backgroundColor: d3roPalette.tag.orangeBg, color: d3roPalette.tag.orange },
},
},
MuiDrawer: { styleOverrides: { paper: { width: 240, backgroundColor: p.bg.app, borderRight: `1px solid ${p.border.subtle}` } } },
MuiListItemButton: {
styleOverrides: {
root: {
borderRadius: 10, marginLeft: 8, marginRight: 8,
'&.Mui-selected': { backgroundColor: accent.amberDim, color: accent.amber, fontWeight: 600, '&:hover': { backgroundColor: 'rgba(242,91,41,0.2)' } },
},
},
},
MuiDialog: { styleOverrides: { paper: { backgroundColor: p.bg.card, borderRadius: 22, border: `1px solid ${p.border.subtle}`, boxShadow: '0 16px 48px rgba(0,0,0,0.5)' } } },
MuiTextField: {
defaultProps: { size: 'small', variant: 'outlined' },
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
backgroundColor: p.bg.input, borderRadius: 10,
'& fieldset': { borderColor: p.border.default },
'&:hover fieldset': { borderColor: p.border.strong },
'&.Mui-focused fieldset': { borderColor: accent.amber },
},
},
},
},
MuiTooltip: { defaultProps: { arrow: true }, styleOverrides: { tooltip: { backgroundColor: p.bg.elevated, fontSize: '12px', borderRadius: 8, border: `1px solid ${p.border.subtle}` } } },
MuiTabs: { styleOverrides: { indicator: { backgroundColor: accent.amber } } },
MuiTab: { styleOverrides: { root: { textTransform: 'none', fontWeight: 500, fontSize: '14px', '&.Mui-selected': { color: accent.amber, fontWeight: 600 } } } },
},
})
}
// ── Export ─────────────────────────────────────────────────
export const darkTheme = createD3ROTheme('dark')
export const lightTheme = createD3ROTheme('light')
/** 테마 모드에 따라 Theme 반환. auto일 때는 prefersDark 파라미터 사용. */
export function getTheme(mode: 'dark' | 'light' | 'auto', prefersDark = true): Theme {
if (mode === 'auto') return prefersDark ? darkTheme : lightTheme
return mode === 'dark' ? darkTheme : lightTheme
}
/** 현재 모드의 ModePalette 가져오기 (컴포넌트에서 직접 참조용) */
export function getModePalette(mode: 'dark' | 'light'): ModePalette {
return mode === 'dark' ? darkPalette : lightPalette
}