대시보드 구현
This commit is contained in:
128
features/dashboard/components/StatusHeader.tsx
Normal file
128
features/dashboard/components/StatusHeader.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import Link from "next/link";
|
||||
import { Activity, RefreshCcw, Settings2, Wifi } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { DashboardBalanceSummary } from "@/features/dashboard/types/dashboard.types";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatPercent,
|
||||
getChangeToneClass,
|
||||
} from "@/features/dashboard/utils/dashboard-format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface StatusHeaderProps {
|
||||
summary: DashboardBalanceSummary | null;
|
||||
isKisRestConnected: boolean;
|
||||
isWebSocketReady: boolean;
|
||||
isRefreshing: boolean;
|
||||
lastUpdatedAt: string | null;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 대시보드 상단 상태 헤더(총자산/손익/연결상태/빠른액션) 컴포넌트입니다.
|
||||
* @see features/dashboard/components/DashboardContainer.tsx 대시보드 루트에서 상태 값을 전달받아 렌더링합니다.
|
||||
*/
|
||||
export function StatusHeader({
|
||||
summary,
|
||||
isKisRestConnected,
|
||||
isWebSocketReady,
|
||||
isRefreshing,
|
||||
lastUpdatedAt,
|
||||
onRefresh,
|
||||
}: StatusHeaderProps) {
|
||||
const toneClass = getChangeToneClass(summary?.totalProfitLoss ?? 0);
|
||||
const updatedLabel = lastUpdatedAt
|
||||
? new Date(lastUpdatedAt).toLocaleTimeString("ko-KR", {
|
||||
hour12: false,
|
||||
})
|
||||
: "--:--:--";
|
||||
|
||||
return (
|
||||
<Card className="relative overflow-hidden border-brand-200 shadow-sm dark:border-brand-800/50">
|
||||
{/* ========== BACKGROUND DECORATION ========== */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-linear-to-r from-brand-100/70 via-brand-50/50 to-transparent dark:from-brand-900/35 dark:via-brand-900/20" />
|
||||
|
||||
<CardContent className="relative grid gap-3 p-4 md:grid-cols-[1fr_1fr_1fr_auto]">
|
||||
{/* ========== TOTAL ASSET ========== */}
|
||||
<div className="rounded-xl border border-border/70 bg-background/90 px-4 py-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">총 자산</p>
|
||||
<p className="mt-1 text-xl font-semibold tracking-tight">
|
||||
{summary ? `${formatCurrency(summary.totalAmount)}원` : "-"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
예수금 {summary ? `${formatCurrency(summary.cashBalance)}원` : "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ========== PROFIT/LOSS ========== */}
|
||||
<div className="rounded-xl border border-border/70 bg-background/90 px-4 py-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">실시간 손익</p>
|
||||
<p className={cn("mt-1 text-xl font-semibold tracking-tight", toneClass)}>
|
||||
{summary ? `${formatCurrency(summary.totalProfitLoss)}원` : "-"}
|
||||
</p>
|
||||
<p className={cn("mt-1 text-xs font-medium", toneClass)}>
|
||||
{summary ? formatPercent(summary.totalProfitRate) : "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ========== CONNECTION STATUS ========== */}
|
||||
<div className="rounded-xl border border-border/70 bg-background/90 px-4 py-3">
|
||||
<p className="text-xs font-medium text-muted-foreground">시스템 상태</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs font-medium">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-1",
|
||||
isKisRestConnected
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
||||
: "bg-red-500/10 text-red-600 dark:text-red-400",
|
||||
)}
|
||||
>
|
||||
<Wifi className="h-3.5 w-3.5" />
|
||||
REST {isKisRestConnected ? "연결됨" : "연결 끊김"}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-1",
|
||||
isWebSocketReady
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
||||
: "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
WS {isWebSocketReady ? "준비됨" : "미연결"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
마지막 갱신 {updatedLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ========== QUICK ACTIONS ========== */}
|
||||
<div className="flex items-end gap-2 md:flex-col md:items-stretch md:justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="w-full border-brand-200 text-brand-700 hover:bg-brand-50 dark:border-brand-700 dark:text-brand-300 dark:hover:bg-brand-900/30"
|
||||
>
|
||||
<RefreshCcw
|
||||
className={cn("h-4 w-4", isRefreshing ? "animate-spin" : "")}
|
||||
/>
|
||||
새로고침
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-brand-600 text-white hover:bg-brand-700"
|
||||
>
|
||||
<Link href="/settings">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
자동매매 설정
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user