Files
auto-stock/frontend/src/components/Layout.tsx
shuim f8c4612650 style: 로그인 페이지 재설계 + 'GEN 3 ALGO' 태그라인 제거
- Login: 카드형 레이아웃, 키 아이콘·표시/숨김 토글, 인라인 에러(aria), 로딩 스피너,
  입력 시 에러 자동 해제, 그라디언트 텍스트 제거
- Layout 헤더: 태그라인 삭제, 브랜드명 단일 표기

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:58:28 +09:00

128 lines
5.6 KiB
TypeScript

/**
* Layout.tsx — 전체 페이지 레이아웃 컴포넌트
*
* 역할:
* - 상단 네비게이션 바 (브랜드 로고, 메뉴, 연결 상태 표시기)
* - 백엔드 및 MCP 서버 연결 상태 실시간 모니터링 (5초 폴링)
* - children으로 전달된 페이지 컨텐츠 렌더링
*
* 구조:
* [Header] — sticky 상단 바 (z-50)
* - 브랜드 로고 (AutoStock)
* - 네비게이션 링크 (대시보드, 성과분석, 전략설정, 백테스트, 매매로그, MCP상태)
* - 연결 상태 표시 (Backend LIVE/OFFLINE, MCP connected/unknown)
* [Main] — 페이지 컨텐츠 영역
*/
import { Link, useLocation } from 'react-router-dom'
import { LayoutDashboard, History, Cpu, Activity, SlidersHorizontal, BarChart3, TrendingUp } from 'lucide-react'
import { useQuery } from '@tanstack/react-query'
import api from '../lib/api'
/** Layout Props 정의 */
interface LayoutProps {
children: React.ReactNode
}
/**
* Layout — 전체 페이지 레이아웃 (헤더 + 컨텐츠)
*
* 쿼리:
* - backend-health: 백엔드 연결 상태 확인 (portfolio API 호출, 3초 타임아웃)
* - mcp-status: MCP 서버 연결 상태 확인
*/
const Layout = ({ children }: LayoutProps) => {
const location = useLocation()
// 백엔드 연결 상태 확인 (30초 폴링, 3초 타임아웃 — /health 경량 엔드포인트 사용)
const { data: healthData } = useQuery({
queryKey: ['backend-health'],
queryFn: async () => {
try {
const r = await fetch('/health', {
signal: AbortSignal.timeout(3000),
})
return r.ok
} catch { return false }
},
refetchInterval: 30000,
})
const backendConnected = healthData === true
const { data: mcpStatus } = useQuery({
queryKey: ['mcp-status'],
queryFn: async () => {
try {
return (await api.get('/mcp/status')).data
} catch { return null }
},
refetchInterval: 5000,
})
/** 네비게이션 메뉴 항목 (경로, 아이콘, 레이블) */
const navItems = [
{ path: '/', icon: LayoutDashboard, label: '대시보드' },
{ path: '/analysis', icon: TrendingUp, label: '성과 분석' },
{ path: '/strategy', icon: SlidersHorizontal, label: '전략 설정' },
{ path: '/backtest', icon: BarChart3, label: '백테스트' },
{ path: '/trades', icon: History, label: '매매 로그' },
{ path: '/mcp', icon: Cpu, label: 'MCP 상태' },
]
return (
<div className="min-h-screen bg-[#0f1115] text-gray-100 font-sans flex flex-col">
{/* Top Navigation Bar */}
<header className="border-b border-gray-800 bg-[#161b22] h-16 flex items-center justify-between px-6 sticky top-0 z-50 backdrop-blur-md bg-opacity-90">
<div className="flex items-center gap-8">
{/* Brand */}
<div className="flex items-center gap-3">
<div className="bg-blue-600 p-2 rounded-lg shadow-lg shadow-blue-500/20">
<Activity className="text-white" size={20} />
</div>
<h1 className="text-lg font-semibold tracking-tight leading-none">AutoStock</h1>
</div>
{/* Navigation */}
<nav className="flex items-center gap-1">
{navItems.map((item) => {
const Icon = item.icon
const isActive = location.pathname === item.path
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-all text-sm font-medium ${isActive
? 'bg-gray-800 text-blue-400'
: 'text-gray-400 hover:text-white hover:bg-gray-800/50'
}`}
>
<Icon size={16} />
<span>{item.label}</span>
</Link>
)
})}
</nav>
</div>
{/* Status Indicators */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-800/50 rounded-full border border-gray-700/50">
<div className={`w-2 h-2 rounded-full ${backendConnected ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : 'bg-red-500'}`} />
<span className="text-xs text-gray-400">{backendConnected ? 'LIVE' : 'OFFLINE'}</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-800/50 rounded-full border border-gray-700/50">
<div className={`w-2 h-2 rounded-full ${mcpStatus?.status === 'connected' ? 'bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'bg-yellow-500'}`} />
<span className="text-xs text-gray-400">MCP: {mcpStatus?.status || 'Unknown'}</span>
</div>
</div>
</header>
{/* Main Content Area */}
<main className="flex-1 overflow-x-hidden">
{children}
</main>
</div>
)
}
export default Layout