feat(ui): KCG 브랜딩 + 레이아웃 디자인 + 메뉴 한글화 (#48)

- S&P/SNP → KCG 텍스트 변경 (타이틀, 사이드바, 대시보드)
- 사이드 메뉴 한글화 (모니터링, 통계, API 키, 관리자, 부서)
- MainLayout/ApiHubLayout 헤더/사이드바 레퍼런스 디자인 적용
- 서비스 상태 카드 서비스 코드 제거
- 대시보드 배너 브랜드 컬러 그라디언트 적용
- 다크/라이트 테마 전환 .light 클래스 대응

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
HYOJIN 2026-04-15 16:37:31 +09:00
부모 802d1ba464
커밋 2a8723419d
5개의 변경된 파일104개의 추가작업 그리고 102개의 파일을 삭제

파일 보기

@ -122,20 +122,20 @@ const ApiHubLayoutInner = () => {
return (
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="fixed left-0 top-0 h-screen w-[272px] bg-gray-900 flex flex-col">
<aside className="fixed left-0 top-0 h-screen w-[272px] bg-[var(--color-bg-surface)] flex flex-col border-r border-[var(--color-border)]">
{/* 로고 영역 */}
<div className="flex-shrink-0 border-b border-gray-700/70">
<div className="flex-shrink-0 border-b border-[var(--color-border)]">
<button
onClick={() => navigate('/api-hub')}
className="flex items-center gap-3 px-5 h-14 w-full hover:bg-gray-800/60 transition-colors"
className="flex items-center gap-3 px-5 h-14 w-full hover:bg-[var(--color-primary-subtle)] transition-colors"
>
{/* Indigo 그라디언트 아이콘 배경 */}
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 shadow-lg">
{/* 브랜드 컬러 아이콘 배경 */}
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-[var(--color-primary-600)] shadow-lg">
<svg className="h-4.5 w-4.5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<span className="text-sm font-bold tracking-widest text-white uppercase">KCG API HUB</span>
<span className="text-sm font-bold tracking-widest text-[var(--color-text-primary)] uppercase">KCG API HUB</span>
</button>
</div>
@ -143,7 +143,7 @@ const ApiHubLayoutInner = () => {
<div className="flex-shrink-0 px-4 pt-4 pb-2">
<div className="relative">
<svg
className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-500 pointer-events-none"
className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--color-text-tertiary)] pointer-events-none"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@ -155,12 +155,12 @@ const ApiHubLayoutInner = () => {
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="API 검색..."
className="w-full bg-gray-800 border border-gray-700 text-gray-200 placeholder-gray-600 rounded-lg pl-8 pr-7 py-2 text-xs focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none transition-colors"
className="w-full bg-[var(--color-bg-base)] border border-[var(--color-border)] text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] rounded-lg pl-8 pr-7 py-2 text-xs focus:ring-1 focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] focus:outline-none transition-colors"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-600 hover:text-gray-400 transition-colors"
className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@ -170,7 +170,7 @@ const ApiHubLayoutInner = () => {
</div>
{/* API 수 요약 */}
{!loading && (
<p className="mt-2 text-[11px] text-gray-600">
<p className="mt-2 text-[11px] text-[var(--color-text-tertiary)]">
{filteredDomainGroups.length} · {filteredDomainGroups.reduce((s, dg) => s + dg.apis.length, 0)} API
</p>
)}
@ -180,13 +180,13 @@ const ApiHubLayoutInner = () => {
<nav className="flex-1 overflow-y-auto px-2 py-1 space-y-0.5">
{loading ? (
<div className="flex items-center justify-center py-10">
<svg className="h-5 w-5 animate-spin text-gray-600" fill="none" viewBox="0 0 24 24">
<svg className="h-5 w-5 animate-spin text-[var(--color-text-tertiary)]" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div>
) : filteredDomainGroups.length === 0 ? (
<p className="text-xs text-gray-600 text-center py-6"> </p>
<p className="text-xs text-[var(--color-text-tertiary)] text-center py-6"> </p>
) : (
filteredDomainGroups.map((dg, domainIndex) => {
const domainOpen = isSearching || (openDomains[dg.domain] ?? true);
@ -195,7 +195,7 @@ const ApiHubLayoutInner = () => {
return (
<div key={dg.domain} className="mb-0.5">
{/* 도메인 헤더 */}
<div className="flex w-full items-center rounded-lg hover:bg-gray-800/50 transition-colors group">
<div className="flex w-full items-center rounded-lg hover:bg-[var(--color-primary-subtle)] transition-colors group">
{/* 도메인명 클릭 → 도메인 상세 페이지 이동 */}
<button
onClick={() => navigate(`/api-hub/domains/${encodeURIComponent(dg.domain)}`)}
@ -217,7 +217,7 @@ const ApiHubLayoutInner = () => {
))}
</svg>
</div>
<span className="flex-1 truncate text-xs font-semibold text-gray-300 group-hover:text-white transition-colors tracking-wide">
<span className="flex-1 truncate text-xs font-semibold text-[var(--color-text-secondary)] group-hover:text-[var(--color-primary-text)] transition-colors tracking-wide">
{dg.domain}
</span>
{/* API 카운트 뱃지 */}
@ -228,7 +228,7 @@ const ApiHubLayoutInner = () => {
{/* 접힘/펼침 화살표 버튼 */}
<button
onClick={() => toggleDomain(dg.domain)}
className="flex-shrink-0 rounded-md p-1.5 mr-1 text-gray-600 hover:text-gray-400 transition-colors"
className="flex-shrink-0 rounded-md p-1.5 mr-1 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
title={domainOpen ? '접기' : '펼치기'}
>
<svg
@ -244,7 +244,7 @@ const ApiHubLayoutInner = () => {
{/* API 항목 목록 */}
{domainOpen && (
<div className="ml-3 mt-0.5 space-y-0.5 border-l border-gray-800 pl-3">
<div className="ml-3 mt-0.5 space-y-0.5 border-l border-[var(--color-border)] pl-3">
{dg.apis.map((api) => {
const apiPath = `/api-hub/services/${api.serviceId}/apis/${api.apiId}`;
const isActive = location.pathname === apiPath;
@ -255,13 +255,13 @@ const ApiHubLayoutInner = () => {
to={apiPath}
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-xs truncate transition-colors ${
isActive
? 'bg-gray-800 text-white font-medium'
: 'text-gray-500 hover:bg-gray-800/60 hover:text-gray-200'
? 'bg-[var(--color-primary)] text-white font-medium dark:bg-[var(--color-primary-subtle)] dark:text-[var(--color-primary-text)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-primary-subtle)] hover:text-[var(--color-primary-text)]'
}`}
>
{/* 활성 시 좌측 도트 인디케이터 */}
<span className={`flex-shrink-0 h-1.5 w-1.5 rounded-full transition-colors ${
isActive ? `${color.dot}` : 'bg-gray-700'
isActive ? `${color.dot}` : 'bg-[var(--color-border)]'
}`} />
<span className="truncate">{api.apiName}</span>
</NavLink>
@ -277,8 +277,8 @@ const ApiHubLayoutInner = () => {
{/* 하단 정보 */}
{!loading && (
<div className="flex-shrink-0 border-t border-gray-700/70 px-4 py-3">
<div className="flex items-center justify-between text-[11px] text-gray-600">
<div className="flex-shrink-0 border-t border-[var(--color-border)] px-4 py-3">
<div className="flex items-center justify-between text-[11px] text-[var(--color-text-tertiary)]">
<span> {totalApiCount} API</span>
<span>{domainGroups.length} </span>
</div>
@ -289,13 +289,13 @@ const ApiHubLayoutInner = () => {
{/* Main content */}
<div className="flex-1 ml-[272px]">
{/* 헤더 */}
<header className="h-14 bg-gray-900 border-b border-gray-700/70 flex items-center justify-between px-6">
<header className="h-14 bg-[var(--color-bg-surface)] border-b border-[var(--color-border)] flex items-center justify-between px-6">
<div />
<div className="flex items-center gap-3">
{/* 테마 토글 */}
<button
onClick={toggleTheme}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-colors"
className="rounded-lg p-2 text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-base)] hover:text-[var(--color-text-primary)] transition-colors"
title={theme === 'light' ? 'Dark mode' : 'Light mode'}
>
{theme === 'light' ? (
@ -309,15 +309,15 @@ const ApiHubLayoutInner = () => {
)}
</button>
{/* 역할 스위처 (알약 형태) */}
<div className="flex rounded-full overflow-hidden border border-gray-700 bg-gray-800 p-0.5">
<div className="flex rounded-full overflow-hidden border border-[var(--color-border)] bg-[var(--color-bg-base)] p-0.5">
{ROLES.map((role) => (
<button
key={role}
onClick={() => setRole(role)}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
user?.role === role
? 'bg-indigo-600 text-white shadow-sm'
: 'text-gray-500 hover:text-gray-300'
? 'bg-[var(--color-primary)] text-white shadow-sm dark:bg-[var(--color-primary-600)]'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
}`}
>
{role}
@ -328,7 +328,7 @@ const ApiHubLayoutInner = () => {
</header>
{/* 콘텐츠 */}
<main className="p-6 bg-gray-100 dark:bg-gray-900 min-h-[calc(100vh-3.5rem)]">
<main className="p-6 bg-[var(--color-bg-base)] min-h-[calc(100vh-3.5rem)]">
<Outlet />
<BasketFloatingPanel />
</main>
@ -355,22 +355,22 @@ const BasketFloatingPanel = () => {
!isBasketOpen ? (
<button
onClick={() => setBasketOpen(true)}
className="fixed right-6 bottom-6 z-40 flex items-center gap-2 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-3 shadow-lg transition-colors"
className="fixed right-6 bottom-6 z-40 flex items-center gap-2 rounded-xl bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] dark:bg-[var(--color-primary-600)] dark:hover:bg-[var(--color-primary-500)] text-white px-4 py-3 shadow-lg transition-colors"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<span className="text-sm font-semibold"></span>
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white text-indigo-700 text-xs font-bold">{items.length}</span>
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white text-[var(--color-primary-active)] text-xs font-bold">{items.length}</span>
</button>
) : (
<div className="fixed right-6 bottom-6 z-40 w-80 rounded-xl bg-white dark:bg-gray-800 shadow-2xl border border-gray-200 dark:border-gray-700 flex flex-col max-h-96">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
<div className="fixed right-6 bottom-6 z-40 w-80 rounded-xl bg-[var(--color-bg-surface)] shadow-2xl border border-[var(--color-border)] flex flex-col max-h-96">
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--color-border)] flex-shrink-0">
<span className="text-sm font-bold text-[var(--color-text-primary)]">
API
<span className="ml-1.5 inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 text-xs font-bold px-1">{items.length}</span>
<span className="ml-1.5 inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-[var(--color-primary-subtle)] text-[var(--color-primary)] text-xs font-bold px-1">{items.length}</span>
</span>
<button onClick={() => setBasketOpen(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<button onClick={() => setBasketOpen(false)} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
@ -385,12 +385,12 @@ const BasketFloatingPanel = () => {
});
return Array.from(grouped.entries()).map(([domain, apis]) => (
<div key={domain}>
<div className="px-4 py-1.5 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-100 dark:border-gray-700/50">
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">{domain}</span>
<div className="px-4 py-1.5 bg-[var(--color-bg-base)] border-b border-[var(--color-border)]">
<span className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider">{domain}</span>
</div>
{apis.map((item) => (
<div key={item.apiId} className="flex items-center gap-2 px-4 py-2 border-b border-gray-50 dark:border-gray-700/30">
<span className="flex-1 text-sm text-gray-800 dark:text-gray-200 truncate">{item.apiName}</span>
<div key={item.apiId} className="flex items-center gap-2 px-4 py-2 border-b border-[var(--color-border)]">
<span className="flex-1 text-sm text-[var(--color-text-primary)] truncate">{item.apiName}</span>
<button onClick={() => removeItem(item.apiId)} className="flex-shrink-0 text-gray-400 hover:text-red-500 transition-colors">
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
@ -400,9 +400,9 @@ const BasketFloatingPanel = () => {
));
})()}
</div>
<div className="flex gap-2 px-4 py-3 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<button onClick={clearItems} className="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 text-xs font-medium py-2 transition-colors"> </button>
<button onClick={handleOpenModal} className="flex-1 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-medium py-2 transition-colors"> </button>
<div className="flex gap-2 px-4 py-3 border-t border-[var(--color-border)] flex-shrink-0">
<button onClick={clearItems} className="flex-1 rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] hover:bg-[var(--color-bg-base)] text-xs font-medium py-2 transition-colors"> </button>
<button onClick={handleOpenModal} className="flex-1 rounded-lg bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] dark:bg-[var(--color-primary-600)] dark:hover:bg-[var(--color-primary-500)] text-white text-xs font-medium py-2 transition-colors"> </button>
</div>
</div>
)

파일 보기

@ -198,40 +198,40 @@ const MainLayout = () => {
return (
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="fixed left-0 top-0 h-screen w-60 bg-gray-900 text-white flex flex-col z-40">
<aside className="fixed left-0 top-0 h-screen w-60 bg-[var(--color-bg-surface)] text-[var(--color-text-secondary)] flex flex-col z-40 border-r border-[var(--color-border)]">
{/* 로고 영역 */}
<div className="flex items-center gap-3 px-4 h-14 border-b border-gray-700/60 flex-shrink-0">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-700 flex-shrink-0">
<div className="flex items-center gap-3 px-4 h-14 border-b border-[var(--color-border)] flex-shrink-0">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-[var(--color-primary-600)] flex-shrink-0">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={16} height={16} className="text-white">
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
</svg>
</div>
<div className="min-w-0">
<div className="text-sm font-semibold text-white leading-tight truncate">KCG Connection</div>
<div className="text-[10px] text-gray-400 leading-tight truncate">Monitoring Service</div>
<div className="text-sm font-semibold text-[var(--color-text-primary)] leading-tight truncate">KCG Connection</div>
<div className="text-[10px] text-[var(--color-text-tertiary)] leading-tight truncate">Monitoring Service</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto py-3 px-2 space-y-1 scrollbar-thin scrollbar-track-gray-900 scrollbar-thumb-gray-700">
<nav className="flex-1 overflow-y-auto py-3 px-2 space-y-1">
{/* API Hub 바로가기 카드 */}
<Link
to="/api-hub"
className="flex items-center gap-3 mx-1 mb-3 px-3 py-2.5 rounded-lg border border-indigo-500/40 bg-indigo-500/10 hover:bg-indigo-500/20 transition-colors group"
className="flex items-center gap-3 mx-1 mb-3 px-3 py-2.5 rounded-lg border border-[var(--color-primary-active)] bg-[var(--color-primary-subtle)] hover:bg-[var(--color-primary-subtle)] transition-colors group"
>
<div className="flex items-center justify-center w-7 h-7 rounded-md bg-indigo-600/30 flex-shrink-0">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" width={15} height={15} className="text-indigo-300">
<div className="flex items-center justify-center w-7 h-7 rounded-md bg-[var(--color-primary-subtle)] flex-shrink-0">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" width={15} height={15} className="text-[var(--color-primary)]">
<rect x="4" y="4" width="16" height="16" rx="2" />
<path d="M9 9h6M9 13h4" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-indigo-300 leading-tight">API Hub</div>
<div className="text-[10px] text-indigo-400/70 leading-tight truncate">API </div>
<div className="text-xs font-semibold text-[var(--color-primary)] leading-tight">API Hub</div>
<div className="text-[10px] text-[var(--color-text-tertiary)] leading-tight truncate">API </div>
</div>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={14} height={14} className="text-indigo-400/60 flex-shrink-0 group-hover:text-indigo-300 transition-colors">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width={14} height={14} className="text-[var(--color-text-tertiary)] flex-shrink-0 group-hover:text-[var(--color-primary)] transition-colors">
<path d="M9 18l6-6-6-6" />
</svg>
</Link>
@ -242,15 +242,15 @@ const MainLayout = () => {
className={({ isActive }) =>
`relative flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-colors ${
isActive
? 'bg-gray-800 text-white'
: 'text-gray-400 hover:bg-gray-800/60 hover:text-gray-200'
? 'bg-[var(--color-primary)] text-white dark:bg-[var(--color-primary-subtle)] dark:text-[var(--color-primary-text)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-primary-subtle)] hover:text-[var(--color-primary-text)]'
}`
}
>
{({ isActive }) => (
<>
{isActive && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-indigo-500 rounded-r-full" />
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-[var(--color-primary)] rounded-r-full dark:hidden" />
)}
<span className="flex-shrink-0"><IconDashboard /></span>
<span className="font-medium"></span>
@ -267,12 +267,12 @@ const MainLayout = () => {
return (
<div key={group.label}>
{/* 구분선 */}
<div className="my-2 mx-1 border-t border-gray-700/50" />
<div className="my-2 mx-1 border-t border-[var(--color-border)]" />
{/* 섹션 타이틀 */}
<button
onClick={() => toggleGroup(group.label)}
className="flex w-full items-center justify-between px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-gray-500 hover:text-gray-300 transition-colors"
className="flex w-full items-center justify-between px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<span>{group.label}</span>
<svg
@ -302,15 +302,15 @@ const MainLayout = () => {
className={({ isActive }) =>
`relative flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-colors ${
isActive
? 'bg-gray-800 text-white'
: 'text-gray-400 hover:bg-gray-800/60 hover:text-gray-200'
? 'bg-[var(--color-primary)] text-white dark:bg-[var(--color-primary-subtle)] dark:text-[var(--color-primary-text)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-primary-subtle)] hover:text-[var(--color-primary-text)]'
}`
}
>
{({ isActive }) => (
<>
{isActive && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-indigo-500 rounded-r-full" />
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-[var(--color-primary)] rounded-r-full dark:hidden" />
)}
<span className="flex-shrink-0">{item.icon}</span>
<span>{item.label}</span>
@ -326,20 +326,20 @@ const MainLayout = () => {
})}
{/* 마지막 구분선 */}
<div className="my-2 mx-1 border-t border-gray-700/50" />
<div className="my-2 mx-1 border-t border-[var(--color-border)]" />
</nav>
</aside>
{/* Main Content */}
<div className="flex-1 ml-60">
{/* Header */}
<header className="h-14 bg-gray-900 border-b border-gray-700/60 flex items-center justify-between px-6 sticky top-0 z-30">
<header className="h-14 bg-[var(--color-bg-surface)] border-b border-[var(--color-border)] flex items-center justify-between px-6 sticky top-0 z-30">
<div />
<div className="flex items-center gap-3">
{/* 테마 토글 */}
<button
onClick={toggleTheme}
className="flex items-center justify-center w-8 h-8 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-800 transition-colors"
className="flex items-center justify-center w-8 h-8 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-base)] transition-colors"
title={theme === 'light' ? 'Dark mode' : 'Light mode'}
>
{theme === 'light' ? (
@ -354,15 +354,15 @@ const MainLayout = () => {
</button>
{/* 역할 스위처 */}
<div className="flex items-center bg-gray-800 rounded-full p-0.5 gap-0.5">
<div className="flex items-center bg-[var(--color-bg-base)] border border-[var(--color-border)] rounded-full p-0.5 gap-0.5">
{ROLES.map((role) => (
<button
key={role}
onClick={() => setRole(role)}
className={`px-3 py-1 text-xs font-medium rounded-full transition-colors ${
user?.role === role
? 'bg-indigo-600 text-white shadow-sm'
: 'text-gray-400 hover:text-gray-200'
? 'bg-[var(--color-primary)] text-white shadow-sm dark:bg-[var(--color-primary-600)] dark:hover:bg-[var(--color-primary-500)]'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]'
}`}
>
{role}
@ -373,7 +373,7 @@ const MainLayout = () => {
</header>
{/* Content */}
<main className="p-6 bg-gray-100 dark:bg-gray-900 min-h-[calc(100vh-3.5rem)]">
<main className="p-6 bg-[var(--color-bg-base)] min-h-[calc(100vh-3.5rem)]">
<Outlet />
</main>
</div>

파일 보기

@ -128,7 +128,7 @@ const ApiHubDashboardPage = () => {
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500 dark:text-gray-400"> ...</div>
<div className="text-[var(--color-text-secondary)]"> ...</div>
</div>
);
}
@ -136,10 +136,10 @@ const ApiHubDashboardPage = () => {
return (
<div className="max-w-7xl mx-auto space-y-8">
{/* 히어로 배너 */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-950 via-indigo-800 to-indigo-600 p-8">
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-[var(--color-primary-950)] via-[var(--color-primary-800)] to-[var(--color-primary-600)] p-8">
{/* 장식 글로우 원 */}
<div className="pointer-events-none absolute -right-16 -top-16 h-64 w-64 rounded-full bg-indigo-400 opacity-20 blur-3xl" />
<div className="pointer-events-none absolute right-32 -top-8 h-32 w-32 rounded-full bg-purple-400 opacity-10 blur-2xl" />
<div className="pointer-events-none absolute -right-16 -top-16 h-64 w-64 rounded-full bg-[var(--color-primary-400)] opacity-20 blur-3xl" />
<div className="pointer-events-none absolute right-32 -top-8 h-32 w-32 rounded-full bg-[var(--color-secondary-400)] opacity-10 blur-2xl" />
{/* 제목 */}
<h1 className="mb-2 text-4xl font-extrabold tracking-tight text-white">KCG API HUB</h1>
@ -155,8 +155,8 @@ const ApiHubDashboardPage = () => {
<path fillRule="evenodd" d="M12.963 2.286a.75.75 0 00-1.071-.136 9.742 9.742 0 00-3.539 6.176 7.547 7.547 0 01-1.705-1.715.75.75 0 00-1.152-.082A9 9 0 1015.68 4.534a7.46 7.46 0 01-2.717-2.248zM15.75 14.25a3.75 3.75 0 11-7.313-1.172c.628.465 1.35.81 2.133 1a5.99 5.99 0 011.925-3.546 3.75 3.75 0 013.255 3.718z" clipRule="evenodd" />
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> API</h2>
<span className="ml-1 text-xs text-gray-400 dark:text-gray-500"> 7 </span>
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]"> API</h2>
<span className="ml-1 text-xs text-[var(--color-text-tertiary)]"> 7 </span>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{popularApis.map((api, idx) => {
@ -164,7 +164,7 @@ const ApiHubDashboardPage = () => {
return (
<div
key={idx}
className="group flex flex-col rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-indigo-400/50 dark:hover:border-indigo-500/50"
className="group flex flex-col rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-surface)] p-5 shadow-sm cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-indigo-400/50"
onClick={() =>
api.serviceId && api.apiId
? navigate(`/api-hub/services/${api.serviceId}/apis/${api.apiId}`)
@ -187,17 +187,17 @@ const ApiHubDashboardPage = () => {
)}
</div>
<p
className="flex-1 text-sm font-semibold text-gray-900 dark:text-gray-100 truncate mb-4"
className="flex-1 text-sm font-semibold text-[var(--color-text-primary)] truncate mb-4"
title={api.apiName}
>
{api.apiName}
</p>
<div className="flex items-end justify-between border-t border-gray-100 dark:border-gray-700 pt-3">
<div className="flex items-end justify-between border-t border-[var(--color-border)] pt-3">
<div>
<p className="text-xs text-gray-400 dark:text-gray-500 mb-0.5"> </p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
<p className="text-xs text-[var(--color-text-tertiary)] mb-0.5"> </p>
<p className="text-xl font-bold text-[var(--color-text-primary)]">
{api.count.toLocaleString()}
<span className="ml-0.5 text-xs font-normal text-gray-400 dark:text-gray-500"></span>
<span className="ml-0.5 text-xs font-normal text-[var(--color-text-tertiary)]"></span>
</p>
</div>
<svg className="h-7 w-10 text-indigo-400/60" fill="none" viewBox="0 0 40 28" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
@ -219,7 +219,7 @@ const ApiHubDashboardPage = () => {
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z" />
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> API</h2>
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]"> API</h2>
</div>
{recentTop3.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
@ -228,7 +228,7 @@ const ApiHubDashboardPage = () => {
return (
<div
key={api.apiId}
className="group flex flex-col rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-indigo-400/50 dark:hover:border-indigo-500/50"
className="group flex flex-col rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-surface)] p-5 shadow-sm cursor-pointer transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-indigo-400/50"
onClick={() => navigate(`/api-hub/services/${api.serviceId}/apis/${api.apiId}`)}
>
<div className="mb-2 flex items-center gap-2">
@ -241,17 +241,17 @@ const ApiHubDashboardPage = () => {
)}
</div>
<p
className="flex-1 text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1 truncate"
className="flex-1 text-sm font-semibold text-[var(--color-text-primary)] mb-1 truncate"
title={api.apiName}
>
{api.apiName}
</p>
{api.description && (
<p className="mb-3 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
<p className="mb-3 text-xs text-[var(--color-text-secondary)] line-clamp-2">
{truncate(api.description, 80)}
</p>
)}
<div className="mt-auto flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500">
<div className="mt-auto flex items-center gap-1.5 text-xs text-[var(--color-text-tertiary)]">
<svg className="h-3.5 w-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
@ -267,7 +267,7 @@ const ApiHubDashboardPage = () => {
})}
</div>
) : (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-8 text-center text-sm text-gray-400 dark:text-gray-500">
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-surface)] p-8 text-center text-sm text-[var(--color-text-tertiary)]">
API가
</div>
)}
@ -286,7 +286,7 @@ const ApiHubDashboardPage = () => {
/>
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> </h2>
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]"> </h2>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{domainList.map((item) => {
@ -297,9 +297,9 @@ const ApiHubDashboardPage = () => {
<div
key={item.domain}
onClick={() => navigate(`/api-hub/domains/${encodeURIComponent(item.domain)}`)}
className={`group relative overflow-hidden rounded-xl border bg-white dark:bg-gray-800 ${palette.border} cursor-pointer transition-all duration-200 hover:-translate-y-1 hover:shadow-xl`}
className={`group relative overflow-hidden rounded-xl border bg-[var(--color-bg-surface)] ${palette.border} cursor-pointer transition-all duration-200 hover:-translate-y-1 hover:shadow-xl`}
>
<div className="relative h-[200px] overflow-hidden bg-gray-100 dark:bg-gray-700">
<div className="relative h-[200px] overflow-hidden bg-[var(--color-bg-base)]">
<img
src={imgSrc}
alt={item.domain}

파일 보기

@ -57,14 +57,14 @@ const ServiceStatusPage = () => {
const allOperational = services.length > 0 && services.every((s) => s.currentStatus === 'UP');
if (isLoading) {
return <div className="text-center py-20 text-gray-500 dark:text-gray-400"> ...</div>;
return <div className="text-center py-20 text-[var(--color-text-secondary)]"> ...</div>;
}
return (
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Service Status</h1>
<span className="text-sm text-gray-500 dark:text-gray-400"> : {lastUpdated}</span>
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">Service Status</h1>
<span className="text-sm text-[var(--color-text-secondary)]"> : {lastUpdated}</span>
</div>
{/* Overall Status Banner */}
@ -80,30 +80,30 @@ const ServiceStatusPage = () => {
{/* Service List */}
<div className="space-y-6">
{services.map((svc) => (
<div key={svc.serviceId} className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div key={svc.serviceId} className="bg-[var(--color-bg-surface)] rounded-lg shadow">
{/* Service Header */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="p-6 border-b border-[var(--color-border)]">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-3 h-3 rounded-full ${STATUS_COLOR[svc.currentStatus] || 'bg-gray-400'}`} />
<h2
className="text-lg font-semibold text-gray-900 dark:text-gray-100 cursor-pointer hover:text-blue-600"
className="text-lg font-semibold text-[var(--color-text-primary)] cursor-pointer hover:text-[var(--color-primary)]"
onClick={() => navigate(`/monitoring/service-status/${svc.serviceId}`)}
>
{svc.serviceName}
</h2>
</div>
<div className="flex items-center gap-4">
<span className={`text-sm font-medium ${svc.currentStatus === 'UP' ? 'text-green-600' : svc.currentStatus === 'DOWN' ? 'text-red-600' : 'text-gray-500'}`}>
<span className={`text-sm font-medium ${svc.currentStatus === 'UP' ? 'text-green-600' : svc.currentStatus === 'DOWN' ? 'text-red-600' : 'text-[var(--color-text-secondary)]'}`}>
{STATUS_TEXT[svc.currentStatus] || svc.currentStatus}
</span>
{svc.lastResponseTime !== null && (
<span className="text-sm text-gray-400">{svc.lastResponseTime}ms</span>
<span className="text-sm text-[var(--color-text-tertiary)]">{svc.lastResponseTime}ms</span>
)}
</div>
</div>
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400">
90 Uptime: <span className="font-medium text-gray-900 dark:text-gray-100">{svc.uptimePercent90d.toFixed(2)}%</span>
<div className="mt-1 text-sm text-[var(--color-text-secondary)]">
90 Uptime: <span className="font-medium text-[var(--color-text-primary)]">{svc.uptimePercent90d.toFixed(2)}%</span>
</div>
</div>
@ -128,10 +128,10 @@ const ServiceStatusPage = () => {
</div>
))
) : (
<div className="flex-1 h-8 bg-gray-100 dark:bg-gray-700 rounded-sm" />
<div className="flex-1 h-8 bg-[var(--color-bg-base)] rounded-sm" />
)}
</div>
<div className="flex justify-between mt-1 text-xs text-gray-400 dark:text-gray-500">
<div className="flex justify-between mt-1 text-xs text-[var(--color-text-tertiary)]">
<span>{svc.dailyUptime.length > 0 ? formatDate(svc.dailyUptime[0].date) : ''}</span>
<span>Today</span>
</div>
@ -140,7 +140,7 @@ const ServiceStatusPage = () => {
))}
{services.length === 0 && (
<div className="text-center py-20 text-gray-400 dark:text-gray-500"> </div>
<div className="text-center py-20 text-[var(--color-text-tertiary)]"> </div>
)}
</div>
</div>

파일 보기

@ -28,8 +28,10 @@ const ThemeProvider = ({ children }: ThemeProviderProps) => {
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
}
localStorage.setItem('theme', theme);
}, [theme]);