feat(design): ITSM+Manager Variant style applied

ITSM (style.css):
- CSS tokens: indigo -> cyan(#00A0C8)+navy(#003366) palette
- Background: deeper navy (#001020, #001530, #001e3c)
- Sidebar active: cyan left bar + light bg (not full gradient)
- Buttons: solid cyan, rounded
- Logo icon: navy-to-cyan gradient

Manager (React):
- GNB: white header, navy branding, cyan badge
- Sidebar: white bg, cyan active border + light bg, navy text
- StatCard: cyan top bar, light blue icon box (screenshot9 pattern)
- AppLayout: navy page title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DESKTOP-TKLFCPRython 2026-05-31 20:18:22 +09:00
parent 0fee6dcab9
commit 5ffe7f5744
4 changed files with 124 additions and 42 deletions

View File

@ -8,32 +8,58 @@ interface Props {
onClick?: () => void
}
export function StatCard({ title, value, sub, icon, color = '#e8ecff', trend, onClick }: Props) {
/* Variant 스타일 StatCard — screenshot9 카드 패턴 적용 */
export function StatCard({ title, value, sub, icon, color = '#E8F0F8', trend, onClick }: Props) {
return (
<div onClick={onClick} style={{
background: '#fff', border: '1px solid #e2e8f0',
borderRadius: 10, padding: '20px 22px',
cursor: onClick ? 'pointer' : 'default',
transition: 'box-shadow .15s',
}}
onMouseEnter={e => { if (onClick) (e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 16px rgba(0,0,0,.1)' }}
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.boxShadow = 'none' }}
<div
onClick={onClick}
style={{
background: '#ffffff',
border: '1px solid #E2E8F0',
borderTop: '3px solid #00A0C8', /* 상단 시안 바 */
borderRadius: 12,
padding: '20px 22px',
cursor: onClick ? 'pointer' : 'default',
transition: 'all .2s',
boxShadow: '0 2px 8px rgba(0,51,102,.06)',
}}
onMouseEnter={e => {
const el = e.currentTarget as HTMLDivElement
el.style.boxShadow = '0 8px 24px rgba(0,51,102,.12)'
el.style.transform = 'translateY(-2px)'
}}
onMouseLeave={e => {
const el = e.currentTarget as HTMLDivElement
el.style.boxShadow = '0 2px 8px rgba(0,51,102,.06)'
el.style.transform = 'translateY(0)'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 14 }}>
{/* 아이콘 박스 — screenshot9 연파랑 박스 스타일 */}
<div style={{
width: 46, height: 46, borderRadius: 10, background: color,
width: 48, height: 48, borderRadius: 10,
background: '#E8F0F8',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22, flexShrink: 0,
}}>{icon}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 26, fontWeight: 700, lineHeight: 1.2 }}>{value}</div>
<div style={{ fontSize: 12, color: '#64748b', marginTop: 2 }}>{title}</div>
{sub && <div style={{ fontSize: 11, color: '#94a3b8' }}>{sub}</div>}
<div style={{ fontSize: 11, fontWeight: 700, color: '#00A0C8',
letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 4 }}>
{title}
</div>
<div style={{ fontSize: 28, fontWeight: 800, lineHeight: 1.1,
color: '#003366', letterSpacing: '-.02em' }}>
{value}
</div>
{sub && <div style={{ fontSize: 11, color: '#94A3B8', marginTop: 3 }}>{sub}</div>}
</div>
</div>
{trend && (
<div style={{ marginTop: 10, fontSize: 11,
color: trend.up ? '#16a34a' : '#dc2626' }}>
<div style={{
marginTop: 12, fontSize: 12, fontWeight: 600,
color: trend.up ? '#16a34a' : '#dc2626',
display: 'flex', alignItems: 'center', gap: 4,
}}>
{trend.up ? '▲' : '▼'} {Math.abs(trend.val)}%
</div>
)}

View File

@ -35,7 +35,7 @@ export function AppLayout() {
<main style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
{/* 페이지 타이틀 바 */}
<div style={{
padding: '14px 28px', background: '#fff',
padding: '12px 28px', background: '#ffffff',
borderBottom: '1px solid #e2e8f0',
display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
}}>

View File

@ -12,34 +12,53 @@ export function GNB() {
return (
<header style={{
height: 'var(--gnb-h)', background: 'var(--gnb-bg)',
height: 'var(--gnb-h)',
background: '#ffffff',
borderBottom: '1px solid #E2E8F0',
boxShadow: '0 1px 8px rgba(0,51,102,.06)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 20px', flexShrink: 0, position: 'sticky', top: 0, zIndex: 100,
padding: '0 24px', flexShrink: 0, position: 'sticky', top: 0, zIndex: 100,
}}>
{/* 로고 */}
{/* 로고 — Variant 스타일 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 20 }}>🛡</span>
<span style={{ color: '#fff', fontWeight: 700, fontSize: 15 }}>GUARDiA Manager</span>
<span style={{ color: '#7c85a8', fontSize: 11, marginLeft: 4 }}>v2.0</span>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #003366, #00A0C8)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontWeight: 900, fontSize: 16, flexShrink: 0,
}}>G</div>
<span style={{ color: '#003366', fontWeight: 800, fontSize: 15, letterSpacing: '-.01em' }}>
GUARDiA Manager
</span>
<span style={{
fontSize: 10, fontWeight: 700, color: '#00A0C8',
background: 'rgba(0,160,200,.1)', borderRadius: 20,
padding: '2px 8px', letterSpacing: '.04em',
}}>v2.0</span>
</div>
{/* 우측 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<a href="http://zioinfo.co.kr:8001" target="_blank" rel="noreferrer"
style={{ color: '#7c85a8', fontSize: 11, display: 'flex', alignItems: 'center', gap: 4 }}>
<a href="http://zioinfo.co.kr:8443" target="_blank" rel="noreferrer"
style={{
color: '#64748B', fontSize: 12, display: 'flex', alignItems: 'center', gap: 4,
textDecoration: 'none', padding: '5px 10px', borderRadius: 6,
border: '1px solid #E2E8F0',
}}>
🌐 ITSM
</a>
{user && (
<>
<span style={{
background: '#4f6ef7', color: '#fff',
padding: '3px 12px', borderRadius: 20, fontSize: 11, fontWeight: 600,
background: 'rgba(0,90,140,.08)', color: '#005A8C',
padding: '4px 14px', borderRadius: 20, fontSize: 12, fontWeight: 600,
border: '1px solid rgba(0,90,140,.18)',
}}>
👤 {user.display_name ?? user.username}
</span>
<button onClick={logout} style={{
background: 'transparent', border: '1px solid rgba(255,255,255,.2)',
color: '#b0b7cc', padding: '4px 12px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
background: 'transparent', border: '1px solid #E2E8F0',
color: '#64748B', padding: '5px 12px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
}}></button>
</>
)}

View File

@ -37,42 +37,66 @@ const NAV: NavItem[] = [
]},
]
/* Variant 스타일 색상 상수 */
const V = {
navy: '#003366',
blue: '#005A8C',
cyan: '#00A0C8',
cyanLt: 'rgba(0,160,200,.10)',
cyanAct: 'rgba(0,160,200,.15)',
muted: '#64748B',
text: '#334155',
border: '#E2E8F0',
bg: '#F8FAFC',
}
function NavGroup({ item }: { item: NavItem }) {
const [open, setOpen] = useState(true)
if (!item.children) {
return (
<NavLink to={item.path!} style={({ isActive }) => ({
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 16px', fontSize: 13, color: isActive ? '#1a3a6b' : '#475569',
background: isActive ? '#dde4f5' : 'transparent',
borderLeft: isActive ? '3px solid #4f6ef7' : '3px solid transparent',
padding: '8px 16px', fontSize: 13,
color: isActive ? V.navy : V.muted,
background: isActive ? V.cyanAct : 'transparent',
borderLeft: isActive ? `3px solid ${V.cyan}` : '3px solid transparent',
fontWeight: isActive ? 700 : 400,
transition: 'all .15s',
textDecoration: 'none',
})}>
<span style={{ width: 18, textAlign: 'center' }}>{item.icon}</span>
{item.label}
</NavLink>
)
}
return (
<div>
<button onClick={() => setOpen(o => !o)} style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 16px', fontSize: 13, color: '#1e293b', background: 'none',
border: 'none', cursor: 'pointer', fontWeight: 600,
padding: '8px 16px', fontSize: 13, color: V.navy, background: 'none',
border: 'none', cursor: 'pointer', fontWeight: 700,
letterSpacing: '-.01em',
}}>
<span style={{ width: 18, textAlign: 'center' }}>{item.icon}</span>
{item.label}
<span style={{ marginLeft: 'auto', fontSize: 10, transition: 'transform .2s',
transform: open ? 'rotate(180deg)' : 'rotate(0)' }}></span>
<span style={{
marginLeft: 'auto', fontSize: 9, color: V.cyan,
transition: 'transform .2s',
transform: open ? 'rotate(180deg)' : 'rotate(0)',
}}></span>
</button>
{open && item.children.map(c => (
<NavLink key={c.path} to={c.path!} style={({ isActive }) => ({
display: 'flex', alignItems: 'center',
padding: '7px 16px 7px 42px', fontSize: 12.5,
color: isActive ? '#1a3a6b' : '#64748b',
background: isActive ? '#dde4f5' : 'transparent',
borderLeft: isActive ? '3px solid #4f6ef7' : '3px solid transparent',
color: isActive ? V.navy : V.muted,
background: isActive ? V.cyanAct : 'transparent',
borderLeft: isActive ? `3px solid ${V.cyan}` : '3px solid transparent',
fontWeight: isActive ? 600 : 400,
transition: 'all .15s',
textDecoration: 'none',
})}>
{c.label}
</NavLink>
@ -84,11 +108,24 @@ function NavGroup({ item }: { item: NavItem }) {
export function Sidebar() {
return (
<aside style={{
width: 'var(--sidebar-w)', background: 'var(--sidebar-bg)',
borderRight: '1px solid var(--border)', height: '100%',
width: 'var(--sidebar-w)',
background: '#ffffff',
borderRight: `1px solid ${V.border}`,
height: '100%',
display: 'flex', flexDirection: 'column', overflowY: 'auto',
boxShadow: '2px 0 12px rgba(0,51,102,.06)',
}}>
<div style={{ padding: '14px 0', flex: 1 }}>
{/* 사이드바 헤더 */}
<div style={{
padding: '16px 16px 12px',
borderBottom: `1px solid ${V.border}`,
}}>
<div style={{ fontSize: 10, fontWeight: 700, color: V.cyan,
letterSpacing: '.1em', textTransform: 'uppercase', marginBottom: 2 }}>
Navigation
</div>
</div>
<div style={{ padding: '8px 0', flex: 1 }}>
{NAV.map((item, i) => <NavGroup key={i} item={item} />)}
</div>
</aside>