- Login: 카드형 레이아웃, 키 아이콘·표시/숨김 토글, 인라인 에러(aria), 로딩 스피너, 입력 시 에러 자동 해제, 그라디언트 텍스트 제거 - Layout 헤더: 태그라인 삭제, 브랜드명 단일 표기 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
5.6 KiB
TypeScript
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
|