대시보드 실시간 기능 추가

This commit is contained in:
2026-02-13 12:17:35 +09:00
parent 12feeb2775
commit 1ac907cd27
35 changed files with 2790 additions and 1032 deletions

View File

@@ -5,7 +5,8 @@ import { Card, CardContent } from "@/components/ui/card";
import type { DashboardBalanceSummary } from "@/features/dashboard/types/dashboard.types";
import {
formatCurrency,
formatPercent,
formatSignedCurrency,
formatSignedPercent,
getChangeToneClass,
} from "@/features/dashboard/utils/dashboard-format";
import { cn } from "@/lib/utils";
@@ -14,6 +15,7 @@ interface StatusHeaderProps {
summary: DashboardBalanceSummary | null;
isKisRestConnected: boolean;
isWebSocketReady: boolean;
isRealtimePending: boolean;
isProfileVerified: boolean;
verifiedAccountNo: string | null;
isRefreshing: boolean;
@@ -29,6 +31,7 @@ export function StatusHeader({
summary,
isKisRestConnected,
isWebSocketReady,
isRealtimePending,
isProfileVerified,
verifiedAccountNo,
isRefreshing,
@@ -41,6 +44,20 @@ export function StatusHeader({
hour12: false,
})
: "--:--:--";
const hasApiTotalAmount =
Boolean(summary) && (summary?.apiReportedTotalAmount ?? 0) > 0;
const hasApiNetAssetAmount =
Boolean(summary) && (summary?.apiReportedNetAssetAmount ?? 0) > 0;
const isApiTotalAmountDifferent =
Boolean(summary) &&
Math.abs(
(summary?.apiReportedTotalAmount ?? 0) - (summary?.totalAmount ?? 0),
) >= 1;
const realtimeStatusLabel = isWebSocketReady
? isRealtimePending
? "수신 대기중"
: "연결됨"
: "미연결";
return (
<Card className="relative overflow-hidden border-brand-200 shadow-sm dark:border-brand-800/50">
@@ -50,32 +67,61 @@ export function StatusHeader({
<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="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)}` : "-"}
() {summary ? `${formatCurrency(summary.cashBalance)}` : "-"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{summary ? `${formatCurrency(summary.netAssetAmount)}` : "-"}
{" "}
{summary ? `${formatCurrency(summary.evaluationAmount)}` : "-"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
(KIS){" "}
{summary ? `${formatCurrency(summary.totalDepositAmount)}` : "-"}
</p>
<p className="mt-1 text-[11px] text-muted-foreground/80">
.
</p>
<p className="mt-1 text-xs text-muted-foreground">
( ){" "}
{summary ? `${formatCurrency(summary.netAssetAmount)}` : "-"}
</p>
{hasApiTotalAmount && isApiTotalAmountDifferent ? (
<p className="mt-1 text-xs text-muted-foreground">
KIS {formatCurrency(summary?.apiReportedTotalAmount ?? 0)}
</p>
) : null}
{hasApiNetAssetAmount ? (
<p className="mt-1 text-xs text-muted-foreground">
KIS {formatCurrency(summary?.apiReportedNetAssetAmount ?? 0)}
</p>
) : null}
</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
className={cn(
"mt-1 text-xl font-semibold tracking-tight",
toneClass,
)}
>
{summary ? `${formatSignedCurrency(summary.totalProfitLoss)}` : "-"}
</p>
<p className={cn("mt-1 text-xs font-medium", toneClass)}>
{summary ? formatPercent(summary.totalProfitRate) : "-"}
{summary ? formatSignedPercent(summary.totalProfitRate) : "-"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{summary ? `${formatCurrency(summary.evaluationAmount)}` : "-"}
{" "}
{summary ? `${formatCurrency(summary.evaluationAmount)}` : "-"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{summary ? `${formatCurrency(summary.purchaseAmount)}` : "-"}
{" "}
{summary ? `${formatCurrency(summary.purchaseAmount)}` : "-"}
</p>
</div>
@@ -98,12 +144,14 @@ export function StatusHeader({
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"
? isRealtimePending
? "bg-amber-500/10 text-amber-700 dark:text-amber-400"
: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
: "bg-muted text-muted-foreground",
)}
>
<Activity className="h-3.5 w-3.5" />
{isWebSocketReady ? "연결됨" : "미연결"}
{realtimeStatusLabel}
</span>
<span
className={cn(
@@ -129,7 +177,7 @@ export function StatusHeader({
</div>
{/* ========== QUICK ACTIONS ========== */}
<div className="flex items-end gap-2 md:flex-col md:items-stretch md:justify-between">
<div className="flex flex-col gap-2 sm:flex-row md:flex-col md:items-stretch md:justify-center">
<Button
type="button"
variant="outline"
@@ -138,16 +186,16 @@ export function StatusHeader({
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" : "")}
className={cn("h-4 w-4 mr-2", isRefreshing ? "animate-spin" : "")}
/>
</Button>
<Button
asChild
className="w-full bg-brand-600 text-white hover:bg-brand-700"
className="w-full bg-brand-600 text-white hover:bg-brand-700 dark:bg-brand-600 dark:text-white dark:hover:bg-brand-500"
>
<Link href="/settings">
<Settings2 className="h-4 w-4" />
<Settings2 className="h-4 w-4 mr-2" />
</Link>
</Button>