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:
parent
ed5541f769
commit
3f4d0c5828
40 changed files with 6034 additions and 580 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue