대시보드 실시간 기능 추가
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user