feat: add data explorer to sidebar navigation and fix gitignore
All checks were successful
Deploy to Production / deploy (push) Successful in 1m30s

- Add "데이터 탐색" menu item to sidebar with Search icon
- Add "수집 데이터 조회" link button on data management page
- Fix sidebar active state to correctly distinguish /admin/data
  from /admin/data/explorer
- Add page title mapping for data explorer in header
- Fix .gitignore: add negation for frontend/src/app/admin/data/
  so admin data pages are tracked without needing git add -f
- Fix dashboard loading state (return null → skeleton with layout)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zephyrdark 2026-02-13 23:54:09 +09:00
parent 2858c87b1b
commit c8bb675ba4
5 changed files with 42 additions and 8 deletions

1
.gitignore vendored
View File

@ -55,6 +55,7 @@ Thumbs.db
*.db
*.sqlite3
data/
!frontend/src/app/admin/data/
# Test
.coverage

View File

@ -2,6 +2,7 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { DashboardLayout } from '@/components/layout/dashboard-layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@ -135,7 +136,12 @@ export default function DataManagementPage() {
return (
<DashboardLayout>
<h1 className="text-2xl font-bold text-foreground mb-6"> </h1>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-foreground"> </h1>
<Button asChild variant="outline">
<Link href="/admin/data/explorer"> </Link>
</Button>
</div>
{error && (
<div className="bg-destructive/10 border border-destructive text-destructive px-4 py-3 rounded mb-4">

View File

@ -84,7 +84,26 @@ export default function DashboardPage() {
}, [router]);
if (loading) {
return null; // DashboardLayout handles skeleton
return (
<DashboardLayout>
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardContent className="pt-6">
<div className="h-4 w-20 bg-muted animate-pulse rounded mb-2" />
<div className="h-8 w-32 bg-muted animate-pulse rounded" />
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card><CardContent className="pt-6"><div className="h-64 bg-muted animate-pulse rounded" /></CardContent></Card>
<Card><CardContent className="pt-6"><div className="h-64 bg-muted animate-pulse rounded" /></CardContent></Card>
</div>
</div>
</DashboardLayout>
);
}
const totalValue = portfolios.reduce((sum, p) => sum + (p.total_value ?? 0), 0);

View File

@ -10,7 +10,8 @@ const pageTitles: Record<string, string> = {
'/portfolio': '포트폴리오',
'/strategy': '전략',
'/backtest': '백테스트',
'/admin/data': '데이터 관리',
'/admin/data': '데이터 수집',
'/admin/data/explorer': '데이터 탐색',
};
function getPageTitle(pathname: string): string {
@ -19,12 +20,16 @@ function getPageTitle(pathname: string): string {
return pageTitles[pathname];
}
// Check for partial matches (for nested routes)
// Check for partial matches (for nested routes), prefer longest match
let bestMatch = '';
let bestTitle = '';
for (const [path, title] of Object.entries(pageTitles)) {
if (path !== '/' && pathname.startsWith(path)) {
return title;
if (path !== '/' && pathname.startsWith(path) && path.length > bestMatch.length) {
bestMatch = path;
bestTitle = title;
}
}
if (bestTitle) return bestTitle;
return '대시보드';
}

View File

@ -9,6 +9,7 @@ import {
TrendingUp,
FlaskConical,
Database,
Search,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
@ -27,7 +28,8 @@ const navItems = [
{ href: '/portfolio', label: '포트폴리오', icon: Briefcase },
{ href: '/strategy', label: '전략', icon: TrendingUp },
{ href: '/backtest', label: '백테스트', icon: FlaskConical },
{ href: '/admin/data', label: '데이터 관리', icon: Database },
{ href: '/admin/data', label: '데이터 수집', icon: Database },
{ href: '/admin/data/explorer', label: '데이터 탐색', icon: Search },
];
interface NewSidebarProps {
@ -84,7 +86,8 @@ export function NewSidebar({ collapsed = false, onCollapsedChange }: NewSidebarP
{navItems.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href));
(item.href !== '/' && pathname.startsWith(item.href) &&
!navItems.some((other) => other.href !== item.href && other.href.startsWith(item.href) && pathname.startsWith(other.href)));
const Icon = item.icon;
const linkContent = (