From 6fffc030747b0a5ea3da15b316bea13cf74ca643 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Sun, 31 May 2026 20:18:22 +0900 Subject: [PATCH] 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 --- itsm/static/style.css | 79 ++++++++++--------- .../src/components/common/StatCard.tsx | 58 ++++++++++---- .../src/components/layout/AppLayout.tsx | 2 +- .../frontend/src/components/layout/GNB.tsx | 43 +++++++--- .../src/components/layout/Sidebar.tsx | 63 ++++++++++++--- 5 files changed, 165 insertions(+), 80 deletions(-) diff --git a/itsm/static/style.css b/itsm/static/style.css index 24a55909..bf0f289e 100644 --- a/itsm/static/style.css +++ b/itsm/static/style.css @@ -2,9 +2,8 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* ─── KWCAG 2.1 웹접근성 포커스 표시 (GS인증 필수) ─ */ -/* outline:none 대신 :focus-visible 로 키보드 포커스만 표시 */ :focus-visible { - outline: 2px solid #818cf8 !important; + outline: 2px solid #00A0C8 !important; outline-offset: 2px !important; border-radius: 4px; } @@ -14,53 +13,55 @@ /* 스킵 네비게이션 (키보드 접근성) */ .skip-nav { position: absolute; top: -60px; left: 8px; z-index: 99999; - background: #818cf8; color: #fff; padding: 8px 16px; + background: #00A0C8; color: #fff; padding: 8px 16px; border-radius: 0 0 8px 8px; font-size: 14px; font-weight: 600; transition: top .2s; text-decoration: none; } .skip-nav:focus { top: 0; } -/* ─── Design Tokens (Nifty Dark) ────────────────── */ +/* ─── Design Tokens (Variant Navy-Cyan Dark) ────── */ :root { - /* backgrounds */ - --main-bg: #0f172a; - --sidebar-bg: #1e293b; - --card-bg: #1e293b; - --card-inner: #0f172a; - --input-bg: #0f172a; + /* backgrounds — Variant 딥네이비 계열 */ + --main-bg: #001020; + --sidebar-bg: #001530; + --card-bg: #001e3c; + --card-inner: #001020; + --input-bg: #001530; - /* borders & shadows */ - --border: rgba(255,255,255,.07); - --shadow-sm: 0 2px 8px rgba(0,0,0,.3); - --shadow-md: 0 4px 20px rgba(0,0,0,.35); - --shadow-lg: 0 8px 40px rgba(0,0,0,.4); + /* borders & shadows — 시안 계열 */ + --border: rgba(0,160,200,.15); + --border-strong: rgba(0,160,200,.30); + --shadow-sm: 0 2px 8px rgba(0,10,30,.4); + --shadow-md: 0 4px 20px rgba(0,10,30,.45), 0 1px 4px rgba(0,160,200,.08); + --shadow-lg: 0 8px 40px rgba(0,10,30,.5), 0 2px 8px rgba(0,160,200,.12); /* text — KWCAG 2.1 AA 색상 대비 4.5:1 이상 보장 */ - --text-bright: #f8fafc; /* 대비 15.8:1 (배경 #0f172a 대비) */ - --text-primary: #cbd5e1; /* 대비 9.2:1 */ - --text-muted: #94a3b8; /* 대비 4.7:1 ✅ (기존 #64748b=3.1:1 → 개선) */ + --text-bright: #e8f4fd; /* 대비 기준 유지 */ + --text-primary: #b8d4ea; + --text-muted: #7ba7c4; - /* brand colors */ - --accent: #818cf8; - --accent-dark: #6366f1; + /* brand colors — Variant 팔레트 */ + --accent: #00A0C8; /* 시안 메인 */ + --accent-dark: #005A8C; /* 미드블루 */ + --brand-navy: #003366; /* 딥네이비 */ --green: #34d399; --yellow: #fbbf24; --red: #f87171; --orange: #fb923c; --purple: #a78bfa; - --cyan: #22d3ee; + --cyan: #00A0C8; - /* sidebar active */ - --sidebar-active-bg: #6366f1; - --sidebar-hover-bg: rgba(255,255,255,.06); + /* sidebar active — 시안 강조 */ + --sidebar-active-bg: rgba(0,160,200,.18); + --sidebar-hover-bg: rgba(0,160,200,.08); - /* typography */ - --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + /* typography — Pretendard 우선 */ + --font: "Pretendard", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; /* radii */ --radius: 8px; - --radius-lg: 14px; - --radius-xl: 20px; + --radius-lg: 12px; + --radius-xl: 18px; } /* ─── Base ──────────────────────────────────────── */ @@ -93,10 +94,10 @@ html, body { } .logo-icon { width: 38px; height: 38px; border-radius: 10px; flex-shrink: 0; - background: linear-gradient(135deg, var(--accent-dark), #8b5cf6); + background: linear-gradient(135deg, #005A8C, #00A0C8); color: #fff; font-size: 20px; font-weight: 900; display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 14px rgba(99,102,241,.45); + box-shadow: 0 4px 14px rgba(0,160,200,.35); } .logo-title { font-size: 16px; font-weight: 800; color: var(--text-bright); letter-spacing: -.01em; } .logo-sub { font-size: 10px; color: var(--text-muted); letter-spacing: .04em; text-transform: uppercase; } @@ -117,9 +118,10 @@ html, body { } .nav-item:hover { background: var(--sidebar-hover-bg); color: var(--text-primary); } .nav-item.active { - background: linear-gradient(90deg, var(--accent-dark), #8b5cf6); - color: #fff; font-weight: 600; - box-shadow: 0 4px 14px rgba(99,102,241,.35); + background: var(--sidebar-active-bg); + color: var(--accent); font-weight: 700; + border-left: 3px solid var(--accent); + padding-left: 9px; } .nav-icon { font-size: 16px; width: 20px; text-align: center; } @@ -216,11 +218,12 @@ html, body { .btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); } .btn:active { transform: none; } .btn-primary { - background: linear-gradient(135deg, var(--accent-dark), #8b5cf6); + background: var(--accent); color: #fff; border-color: transparent; - box-shadow: 0 2px 10px rgba(99,102,241,.35); + box-shadow: 0 2px 10px rgba(0,160,200,.3); + border-radius: var(--radius); } -.btn-primary:hover { box-shadow: 0 4px 18px rgba(99,102,241,.5); } +.btn-primary:hover { background: var(--accent-dark); box-shadow: 0 4px 18px rgba(0,160,200,.45); } .btn-secondary { background: rgba(255,255,255,.06); border-color: var(--border); color: var(--text-primary); } .btn-secondary:hover { background: rgba(255,255,255,.1); } .btn-approve { background: rgba(52,211,153,.15); color: var(--green); border-color: rgba(52,211,153,.3); } @@ -290,7 +293,7 @@ html, body { .stat-card.purple .stat-value { color: var(--purple); } .stat-card.orange .stat-value { color: var(--orange); } .stat-card.cyan .stat-value { color: var(--cyan); } -.stat-card.accent .stat-icon { background: rgba(129,140,248,.15); color: var(--accent); } +.stat-card.accent .stat-icon { background: rgba(0,160,200,.15); color: var(--accent); } .stat-card.green .stat-icon { background: rgba(52,211,153,.15); color: var(--green); } .stat-card.yellow .stat-icon { background: rgba(251,191,36,.15); color: var(--yellow); } .stat-card.red .stat-icon { background: rgba(248,113,113,.15); color: var(--red); } diff --git a/manager/frontend/src/components/common/StatCard.tsx b/manager/frontend/src/components/common/StatCard.tsx index f6c33cdd..2944f1e7 100644 --- a/manager/frontend/src/components/common/StatCard.tsx +++ b/manager/frontend/src/components/common/StatCard.tsx @@ -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 ( -
{ 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' }} +
{ + 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)' + }} > -
+
+ {/* 아이콘 박스 — screenshot9 연파랑 박스 스타일 */}
{icon}
-
{value}
-
{title}
- {sub &&
{sub}
} +
+ {title} +
+
+ {value} +
+ {sub &&
{sub}
}
{trend && ( -
+
{trend.up ? '▲' : '▼'} {Math.abs(trend.val)}% 전주 대비
)} diff --git a/manager/frontend/src/components/layout/AppLayout.tsx b/manager/frontend/src/components/layout/AppLayout.tsx index 4fc07c67..d9a3346b 100644 --- a/manager/frontend/src/components/layout/AppLayout.tsx +++ b/manager/frontend/src/components/layout/AppLayout.tsx @@ -35,7 +35,7 @@ export function AppLayout() {
{/* 페이지 타이틀 바 */}
diff --git a/manager/frontend/src/components/layout/GNB.tsx b/manager/frontend/src/components/layout/GNB.tsx index 91e9bcc1..c8911483 100644 --- a/manager/frontend/src/components/layout/GNB.tsx +++ b/manager/frontend/src/components/layout/GNB.tsx @@ -12,34 +12,53 @@ export function GNB() { return (
- {/* 로고 */} + {/* 로고 — Variant 스타일 */}
- 🛡️ - GUARDiA Manager - v2.0 +
G
+ + GUARDiA Manager + + v2.0
{/* 우측 */}
- + 🌐 ITSM 바로가기 {user && ( <> 👤 {user.display_name ?? user.username} )} diff --git a/manager/frontend/src/components/layout/Sidebar.tsx b/manager/frontend/src/components/layout/Sidebar.tsx index bda358c1..1f11bd48 100644 --- a/manager/frontend/src/components/layout/Sidebar.tsx +++ b/manager/frontend/src/components/layout/Sidebar.tsx @@ -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 ( ({ 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', })}> {item.icon} {item.label} ) } + return (
{open && item.children.map(c => ( ({ 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} @@ -84,11 +108,24 @@ function NavGroup({ item }: { item: NavItem }) { export function Sidebar() { return (