refactor(ui): 팝업 테마 SSOT 정합 + DS 데드코드 3종 제거 (WS1)

- theme-vars.ts dark/light POPUP_THEME_VARS를 theme.ts RAW와 값 정합:
  v1 잔존 주황(#f25b29) -> 파랑(dark #3b82f6 / light #2563eb),
  --d3-bg-card, --d3-text-dimLabel 동기화. 팝업=렌더러 색상 충돌 해소.
- DS 미사용 3종 제거: MetalDial, ButtonGroup, CrtDisplay + barrel export
  (렌더러 전체 import 0건 재검증)
- StatRing RING_COLORS hex 10종 -> d3roPalette.gradient.wave/tag 토큰
  (다크 고정 -> 테마 전환 대응)
SKIP: theme-vars 평행 SSOT 구조 단일화, theme.ts amber 별칭 제거
  (web/admin/site가 accent.amber 사용 -> 전체 치환 별도 wave)
정책: docs/REFACTOR_POLICY.md DP1, DP9
This commit is contained in:
Yun Chan 2026-07-22 01:53:59 +09:00
parent 4259cce9f5
commit b820c789bb
6 changed files with 41 additions and 324 deletions

View file

@ -1,31 +0,0 @@
'use client'
// src/renderer/components/ds/ButtonGroup.tsx
// v2 "Midnight Glass": 세그먼트 글래스 그룹 — 반투명 표면에 버튼 클러스터
import { Box } from '@mui/material'
import { d3roPalette, d3roRadius } from '../../theme'
interface ButtonGroupProps {
children: React.ReactNode
/** 가로 배치 (기본 세로) */
horizontal?: boolean
}
export function ButtonGroup({ children, horizontal = false }: ButtonGroupProps): React.ReactElement {
return (
<Box
sx={{
bgcolor: d3roPalette.bg.inset,
p: '6px',
borderRadius: d3roRadius.inner,
border: `1px solid ${d3roPalette.glass.hairline}`,
display: 'flex',
flexDirection: horizontal ? 'row' : 'column',
gap: '6px',
}}
>
{children}
</Box>
)
}

View file

@ -1,113 +0,0 @@
'use client'
// src/renderer/components/ds/CrtDisplay.tsx
// v2 "Midnight Glass": 히어로 디스플레이 — 딥 글래스 패널 + 그라디언트 스펙트럼 웨이브.
// (v1 WebGL 오실로스코프를 대체. props API는 v1과 호환 유지.)
import { useState, useEffect } from 'react'
import { Box } from '@mui/material'
import { d3roPalette, d3roFontSans, d3roRadius } from '../../theme'
import { GradientWave } from './GradientWave'
interface CrtDisplayProps {
/** 유휴 파형 진폭 (0.0 ~ 1.0) */
amplitude?: number
/** 파형 속도 배율 (v1 주파수 개념 호환 — 8 기준 1배) */
frequency?: number
/** 글리치 트리거 (변경 시 플래시 발생) */
glitchTrigger?: number
/** 실시간 오디오 레벨 (0.0~1.0) — 파형 진폭에 반영 */
audioLevel?: number
/** 오버레이 콘텐츠 */
children?: React.ReactNode
/** 높이 (기본 280px) */
height?: number | string
}
export function CrtDisplay({
amplitude = 0.25,
frequency = 8.0,
glitchTrigger = 0,
audioLevel = 0,
children,
height = 280,
}: CrtDisplayProps): React.ReactElement {
const [flash, setFlash] = useState(false)
// v1 글리치 → v2 소프트 플래시
useEffect(() => {
if (glitchTrigger > 0) {
setFlash(true)
const timer = setTimeout(() => setFlash(false), 220)
return () => clearTimeout(timer)
}
return undefined
}, [glitchTrigger])
return (
<Box
sx={{
position: 'relative',
height,
bgcolor: d3roPalette.bg.crtGlass,
borderRadius: d3roRadius.inner,
border: `1px solid ${d3roPalette.glass.hairline}`,
overflow: 'hidden',
}}
>
{/* 앰비언트 배경 글로우 */}
<Box
sx={{
position: 'absolute',
inset: 0,
background: `radial-gradient(ellipse 70% 55% at 50% 100%, ${d3roPalette.accent.dim} 0%, transparent 70%)`,
pointerEvents: 'none',
}}
/>
{/* 그라디언트 스펙트럼 웨이브 — 하단 영역 */}
<Box sx={{ position: 'absolute', left: 12, right: 12, bottom: 8, height: '38%' }}>
<GradientWave audioLevel={audioLevel} idleAmplitude={amplitude} speed={frequency / 8} />
</Box>
{/* 상단 시인 */}
<Box
sx={{
position: 'absolute',
inset: 0,
background: d3roPalette.glass.sheen,
pointerEvents: 'none',
}}
/>
{/* 플래시 오버레이 (글리치 호환) */}
<Box
sx={{
position: 'absolute',
inset: 0,
bgcolor: d3roPalette.accent.dim,
opacity: flash ? 1 : 0,
transition: 'opacity 0.2s ease',
pointerEvents: 'none',
}}
/>
{/* 콘텐츠 오버레이 */}
<Box
sx={{
position: 'absolute',
inset: '16px',
zIndex: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
color: d3roPalette.text.primary,
pointerEvents: 'none',
fontFamily: d3roFontSans,
}}
>
{children}
</Box>
</Box>
)
}

View file

@ -1,136 +0,0 @@
'use client'
// src/renderer/components/ds/MetalDial.tsx
// 시안 A: 메탈 다이얼 — 정밀기기 회전 노브, 동심원 그루브, 금속 광택, LED 인디케이터
// 보강: 레퍼런스의 conic-gradient 정적 라이팅 + 방향성 그림자 추가
import { Box } from '@mui/material'
import { d3roPalette } from '../../theme'
import { PhosphorText } from './PhosphorText'
interface MetalDialProps {
/** 0.0 ~ 1.0 값 (다이얼 위치) */
value?: number
/** 라벨 텍스트 */
label?: string
/** 크기 (px) */
size?: number
/** LED 인디케이터 색상 */
ledColor?: string
}
export function MetalDial({
value = 0,
label,
size = 120,
ledColor = d3roPalette.accent.amber,
}: MetalDialProps): React.ReactElement {
const rotation = value * 270 - 135 // -135° ~ +135° 범위
const knobSize = size - 12 // 웰 패딩
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1.5 }}>
{/* 다이얼 웰 (inset well) */}
<Box
sx={{
width: size,
height: size,
borderRadius: '50%',
bgcolor: d3roPalette.bg.crtBezel,
boxShadow: `
inset 0 3px 8px rgba(0,0,0,0.8),
inset 0 -1px 2px rgba(255,255,255,0.08),
0 1px 1px rgba(255,255,255,0.05)
`,
position: 'relative',
p: '6px',
}}
>
{/* 노브 회전체 (동심원 그루브) */}
<Box
sx={{
width: knobSize,
height: knobSize,
borderRadius: '50%',
position: 'absolute',
top: 6,
left: 6,
background: `
repeating-radial-gradient(
circle at 50% 50%,
${d3roPalette.text.disabled} 0px,
${d3roPalette.text.disabled} 1px,
${d3roPalette.text.label} 1.5px,
${d3roPalette.text.label} 2.5px
)
`,
transform: `rotate(${rotation}deg)`,
transition: 'transform 0.2s ease-out',
}}
>
{/* 포인터 인디케이터 점 */}
<Box
sx={{
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: d3roPalette.bg.crtBezel,
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&::after': {
content: '""',
width: 4,
height: 4,
borderRadius: '50%',
bgcolor: ledColor,
},
}}
/>
</Box>
{/* 정적 금속 광택 오버레이 (노브와 별개, 회전 안 함) */}
<Box
sx={{
position: 'absolute',
inset: '6px',
borderRadius: '50%',
pointerEvents: 'none',
// 레퍼런스의 핵심: directional light + conic reflections
background: `
linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0) 40%, rgba(0,0,0,0.6) 100%),
conic-gradient(from 180deg at 50% 50%,
rgba(255,255,255,0) 0deg,
rgba(255,255,255,0.4) 45deg,
rgba(255,255,255,0) 90deg,
rgba(255,255,255,0.2) 180deg,
rgba(255,255,255,0) 270deg,
rgba(255,255,255,0.4) 315deg,
rgba(255,255,255,0) 360deg
)
`,
mixBlendMode: 'overlay',
// 방향성 그림자: 좌상 하이라이트 + 우하 쉐이드
boxShadow: `
-4px -4px 8px rgba(255,255,255,0.3),
12px 16px 20px rgba(0,0,0,0.7),
inset 0 2px 3px rgba(255,255,255,0.8)
`,
}}
/>
</Box>
{/* 라벨 */}
{label && (
<PhosphorText variant="label" sx={{ color: d3roPalette.text.inactive }}>
{label}
</PhosphorText>
)}
</Box>
)
}

View file

@ -9,11 +9,11 @@ import { d3roPalette } from '../../theme'
type RingColor = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'accent' type RingColor = 'blue' | 'purple' | 'green' | 'orange' | 'red' | 'accent'
const RING_COLORS: Record<RingColor, { from: string; to: string; glow: string; icon: string }> = { const RING_COLORS: Record<RingColor, { from: string; to: string; glow: string; icon: string }> = {
blue: { from: '#22d3ee', to: '#3b82f6', glow: 'rgba(59,130,246,0.35)', icon: d3roPalette.tag.blue }, blue: { from: 'var(--d3-gradient-wave1)', to: 'var(--d3-gradient-wave2)', glow: d3roPalette.tag.blueBg, icon: d3roPalette.tag.blue },
purple: { from: '#8b5cf6', to: '#d946ef', glow: 'rgba(167,139,250,0.35)', icon: d3roPalette.tag.purple }, purple: { from: 'var(--d3-gradient-wave3)', to: 'var(--d3-gradient-wave4)', glow: d3roPalette.tag.purpleBg, icon: d3roPalette.tag.purple },
green: { from: '#34d399', to: '#10b981', glow: 'rgba(52,211,153,0.35)', icon: d3roPalette.tag.green }, green: { from: d3roPalette.tag.green, to: d3roPalette.tag.green, glow: d3roPalette.tag.greenGlow, icon: d3roPalette.tag.green },
orange: { from: '#fbbf24', to: '#f97316', glow: 'rgba(251,146,60,0.35)', icon: d3roPalette.tag.orange }, orange: { from: d3roPalette.tag.orange, to: d3roPalette.tag.orange, glow: d3roPalette.tag.orangeGlow, icon: d3roPalette.tag.orange },
red: { from: '#f87171', to: '#ef4444', glow: 'rgba(248,113,113,0.35)', icon: d3roPalette.tag.red }, red: { from: d3roPalette.tag.red, to: d3roPalette.tag.red, glow: d3roPalette.tag.redGlow, icon: d3roPalette.tag.red },
accent: { from: 'var(--d3-accent-light)', to: 'var(--d3-accent-dark)', glow: 'var(--d3-accent-dim)', icon: d3roPalette.accent.light }, accent: { from: 'var(--d3-accent-light)', to: 'var(--d3-accent-dark)', glow: 'var(--d3-accent-dim)', icon: d3roPalette.accent.light },
} }

View file

@ -1,16 +1,13 @@
// src/renderer/components/ds/index.ts // src/renderer/components/ds/index.ts
// 디자인 시스템 컴포넌트 SSOT barrel export // 디자인 시스템 컴포넌트 SSOT barrel export
export { CrtDisplay } from './CrtDisplay'
export { InstrumentPanel } from './InstrumentPanel' export { InstrumentPanel } from './InstrumentPanel'
export { Led } from './Led' export { Led } from './Led'
export { PhysicalButton } from './PhysicalButton' export { PhysicalButton } from './PhysicalButton'
export { MetalCard } from './MetalCard' export { MetalCard } from './MetalCard'
export { TiltCard } from './TiltCard' export { TiltCard } from './TiltCard'
export { PhosphorText } from './PhosphorText' export { PhosphorText } from './PhosphorText'
export { MetalDial } from './MetalDial'
export { ScreenPanel } from './ScreenPanel' export { ScreenPanel } from './ScreenPanel'
export { ButtonGroup } from './ButtonGroup'
// v2 "Midnight Glass" 신규 // v2 "Midnight Glass" 신규
export { GradientWave } from './GradientWave' export { GradientWave } from './GradientWave'
export { StatRing } from './StatRing' export { StatRing } from './StatRing'

View file

@ -49,46 +49,46 @@ interface PopupThemeVars {
const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = { const POPUP_THEME_VARS: Record<PopupThemeKey, PopupThemeVars> = {
dark: { dark: {
'--d3-bg-card': '#242427', '--d3-bg-card': '#111a30',
'--d3-bg-tip': 'rgba(0,0,0,0.85)', '--d3-bg-tip': 'rgba(10,14,28,0.85)',
'--d3-text-primary': 'rgba(255,255,255,0.87)', '--d3-text-primary': '#eef2fb',
'--d3-text-secondary': 'rgba(255,255,255,0.6)', '--d3-text-secondary': '#93a4c8',
'--d3-text-inactive': 'rgba(255,255,255,0.3)', '--d3-text-inactive': '#7385ab',
'--d3-text-muted': 'rgba(255,255,255,0.25)', '--d3-text-muted': '#2b3550',
'--d3-text-dimLabel': '#5c2615', '--d3-text-dimLabel': '#4e5f85',
'--d3-border-default': 'rgba(255,255,255,0.08)', '--d3-border-default': 'rgba(148,180,255,0.12)',
'--d3-border-subtle': 'rgba(255,255,255,0.06)', '--d3-border-subtle': 'rgba(148,180,255,0.07)',
'--d3-border-strong': 'rgba(255,255,255,0.15)', '--d3-border-strong': 'rgba(148,180,255,0.20)',
'--d3-accent-main': '#f25b29', '--d3-accent-main': '#3b82f6',
'--d3-accent-dim': 'rgba(242,91,41,0.08)', '--d3-accent-dim': 'rgba(59,130,246,0.14)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.5)', '--d3-shadow-popup': '0 8px 32px rgba(3,7,18,0.5)',
'--d3-bg-result': '#1e1e1e', '--d3-bg-result': '#111a30',
'--d3-border-result': 'rgba(255,255,255,0.08)', '--d3-border-result': 'rgba(148,180,255,0.12)',
'--d3-text-result': 'rgba(255,255,255,0.87)', '--d3-text-result': '#eef2fb',
'--d3-action-btn': 'rgba(255,255,255,0.4)', '--d3-action-btn': '#60a5fa',
'--d3-action-btn-hover-bg': 'rgba(255,255,255,0.08)', '--d3-action-btn-hover-bg': 'rgba(59,130,246,0.14)',
'--d3-action-btn-hover': 'rgba(255,255,255,0.7)', '--d3-action-btn-hover': '#93a4c8',
}, },
light: { light: {
'--d3-bg-card': '#ffffff', '--d3-bg-card': '#ffffff',
'--d3-bg-tip': 'rgba(255,255,255,0.92)', '--d3-bg-tip': 'rgba(242,245,251,0.92)',
'--d3-text-primary': 'rgba(0,0,0,0.87)', '--d3-text-primary': '#111827',
'--d3-text-secondary': 'rgba(0,0,0,0.6)', '--d3-text-secondary': '#4b5a75',
'--d3-text-inactive': 'rgba(0,0,0,0.35)', '--d3-text-inactive': '#8593ab',
'--d3-text-muted': 'rgba(0,0,0,0.25)', '--d3-text-muted': '#c3cddd',
'--d3-text-dimLabel': '#b07040', '--d3-text-dimLabel': '#5a6f96',
'--d3-border-default': 'rgba(0,0,0,0.10)', '--d3-border-default': 'rgba(30,58,138,0.12)',
'--d3-border-subtle': 'rgba(0,0,0,0.06)', '--d3-border-subtle': 'rgba(30,58,138,0.07)',
'--d3-border-strong': 'rgba(0,0,0,0.14)', '--d3-border-strong': 'rgba(30,58,138,0.20)',
'--d3-accent-main': '#f25b29', '--d3-accent-main': '#2563eb',
'--d3-accent-dim': 'rgba(242,91,41,0.08)', '--d3-accent-dim': 'rgba(37,99,235,0.10)',
'--d3-shadow-popup': '0 8px 32px rgba(0,0,0,0.12)', '--d3-shadow-popup': '0 8px 32px rgba(30,58,138,0.12)',
'--d3-bg-result': '#ffffff', '--d3-bg-result': '#ffffff',
'--d3-border-result': 'rgba(0,0,0,0.08)', '--d3-border-result': 'rgba(30,58,138,0.12)',
'--d3-text-result': 'rgba(0,0,0,0.87)', '--d3-text-result': '#111827',
'--d3-action-btn': 'rgba(0,0,0,0.4)', '--d3-action-btn': '#3b82f6',
'--d3-action-btn-hover-bg': 'rgba(0,0,0,0.06)', '--d3-action-btn-hover-bg': 'rgba(37,99,235,0.10)',
'--d3-action-btn-hover': 'rgba(0,0,0,0.7)', '--d3-action-btn-hover': '#4b5a75',
}, },
nord: { nord: {
'--d3-bg-card': '#3b4252', '--d3-bg-card': '#3b4252',