전체적인 리팩토링
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { Activity, RefreshCcw, Settings2, Wifi } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
@@ -58,153 +59,155 @@ export function StatusHeader({
|
||||
? "수신 대기중"
|
||||
: "연결됨"
|
||||
: "미연결";
|
||||
const displayGrossTotalAmount = hasApiTotalAmount
|
||||
? summary?.apiReportedTotalAmount ?? 0
|
||||
: summary?.totalAmount ?? 0;
|
||||
|
||||
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" />
|
||||
<Card className="relative overflow-hidden border-brand-200/80 bg-linear-to-br from-brand-50/80 via-background to-brand-50/20 shadow-sm dark:border-brand-800/45 dark:from-brand-900/25 dark:via-background dark:to-background">
|
||||
<div className="pointer-events-none absolute -right-14 -top-14 h-52 w-52 rounded-full bg-brand-300/30 blur-3xl dark:bg-brand-700/20" />
|
||||
<div className="pointer-events-none absolute -left-16 bottom-0 h-44 w-44 rounded-full bg-brand-200/25 blur-3xl dark:bg-brand-800/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>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
주식 평가금{" "}
|
||||
{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)}원
|
||||
<CardContent className="relative space-y-3 p-4 md:p-5">
|
||||
<div className="grid gap-3 xl:grid-cols-[1fr_1fr_auto]">
|
||||
<div className="rounded-2xl border border-brand-200/70 bg-background/85 p-4 shadow-sm dark:border-brand-800/60 dark:bg-brand-950/20">
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground">
|
||||
TOTAL ASSET
|
||||
</p>
|
||||
) : null}
|
||||
{hasApiNetAssetAmount ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
KIS 집계 순자산 {formatCurrency(summary?.apiReportedNetAssetAmount ?? 0)}원
|
||||
<p className="mt-2 text-2xl font-bold tracking-tight text-foreground md:text-3xl">
|
||||
{summary ? `${formatCurrency(displayGrossTotalAmount)}원` : "-"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
순자산 {summary ? `${formatCurrency(summary.netAssetAmount)}원` : "-"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
현금 {summary ? `${formatCurrency(summary.cashBalance)}원` : "-"} · 평가금{" "}
|
||||
{summary ? `${formatCurrency(summary.evaluationAmount)}원` : "-"}
|
||||
</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 ? `${formatSignedCurrency(summary.totalProfitLoss)}원` : "-"}
|
||||
</p>
|
||||
<p className={cn("mt-1 text-xs font-medium", toneClass)}>
|
||||
{summary ? formatSignedPercent(summary.totalProfitRate) : "-"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
현재 평가금액{" "}
|
||||
{summary ? `${formatCurrency(summary.evaluationAmount)}원` : "-"}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
총 매수금액{" "}
|
||||
{summary ? `${formatCurrency(summary.purchaseAmount)}원` : "-"}
|
||||
</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" />
|
||||
서버 {isKisRestConnected ? "연결됨" : "연결 끊김"}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-1",
|
||||
isWebSocketReady
|
||||
? 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" />
|
||||
실시간 시세 {realtimeStatusLabel}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-1",
|
||||
isProfileVerified
|
||||
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
||||
: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
|
||||
)}
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
계좌 인증 {isProfileVerified ? "완료" : "미완료"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
마지막 업데이트 {updatedLabel}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
계좌 {maskAccountNo(verifiedAccountNo)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
대출금 {summary ? `${formatCurrency(summary.loanAmount)}원` : "-"}
|
||||
</p>
|
||||
|
||||
<div className="rounded-2xl border border-border/70 bg-background/85 p-4 shadow-sm">
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground">
|
||||
TODAY P/L
|
||||
</p>
|
||||
<p className={cn("mt-2 text-2xl font-bold tracking-tight md:text-3xl", toneClass)}>
|
||||
{summary ? `${formatSignedCurrency(summary.totalProfitLoss)}원` : "-"}
|
||||
</p>
|
||||
<p className={cn("mt-1 text-sm font-semibold", toneClass)}>
|
||||
{summary ? formatSignedPercent(summary.totalProfitRate) : "-"}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
매수금 {summary ? `${formatCurrency(summary.purchaseAmount)}원` : "-"} · 대출금{" "}
|
||||
{summary ? `${formatCurrency(summary.loanAmount)}원` : "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-2xl border border-border/70 bg-background/85 p-3">
|
||||
<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("mr-2 h-4 w-4", isRefreshing ? "animate-spin" : "")} />
|
||||
다시 불러오기
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
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="mr-2 h-4 w-4" />
|
||||
연결 설정
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="mt-1 rounded-xl border border-border/70 bg-muted/30 px-2.5 py-2 text-[11px] text-muted-foreground">
|
||||
<p>업데이트 {updatedLabel}</p>
|
||||
<p>계좌 {maskAccountNo(verifiedAccountNo)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ========== QUICK ACTIONS ========== */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row md:flex-col md:items-stretch md:justify-center">
|
||||
<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 mr-2", isRefreshing ? "animate-spin" : "")}
|
||||
/>
|
||||
지금 다시 불러오기
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
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 mr-2" />
|
||||
연결 설정
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<OverviewMetric
|
||||
icon={<Wifi className="h-3.5 w-3.5" />}
|
||||
label="REST 연결"
|
||||
value={isKisRestConnected ? "정상" : "끊김"}
|
||||
toneClass={
|
||||
isKisRestConnected
|
||||
? "border-emerald-300/70 bg-emerald-50/60 text-emerald-700 dark:border-emerald-700/50 dark:bg-emerald-950/30 dark:text-emerald-300"
|
||||
: "border-red-300/70 bg-red-50/60 text-red-700 dark:border-red-700/50 dark:bg-red-950/30 dark:text-red-300"
|
||||
}
|
||||
/>
|
||||
<OverviewMetric
|
||||
icon={<Activity className="h-3.5 w-3.5" />}
|
||||
label="실시간 시세"
|
||||
value={realtimeStatusLabel}
|
||||
toneClass={
|
||||
isWebSocketReady
|
||||
? isRealtimePending
|
||||
? "border-amber-300/70 bg-amber-50/60 text-amber-700 dark:border-amber-700/50 dark:bg-amber-950/30 dark:text-amber-300"
|
||||
: "border-emerald-300/70 bg-emerald-50/60 text-emerald-700 dark:border-emerald-700/50 dark:bg-emerald-950/30 dark:text-emerald-300"
|
||||
: "border-slate-300/70 bg-slate-50/60 text-slate-700 dark:border-slate-700/50 dark:bg-slate-900/30 dark:text-slate-300"
|
||||
}
|
||||
/>
|
||||
<OverviewMetric
|
||||
icon={<Activity className="h-3.5 w-3.5" />}
|
||||
label="계좌 인증"
|
||||
value={isProfileVerified ? "완료" : "미완료"}
|
||||
toneClass={
|
||||
isProfileVerified
|
||||
? "border-emerald-300/70 bg-emerald-50/60 text-emerald-700 dark:border-emerald-700/50 dark:bg-emerald-950/30 dark:text-emerald-300"
|
||||
: "border-amber-300/70 bg-amber-50/60 text-amber-700 dark:border-amber-700/50 dark:bg-amber-950/30 dark:text-amber-300"
|
||||
}
|
||||
/>
|
||||
<OverviewMetric
|
||||
icon={<Activity className="h-3.5 w-3.5" />}
|
||||
label="총예수금(KIS)"
|
||||
value={summary ? `${formatCurrency(summary.totalDepositAmount)}원` : "-"}
|
||||
toneClass="border-brand-200/80 bg-brand-50/70 text-brand-700 dark:border-brand-700/60 dark:bg-brand-900/35 dark:text-brand-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasApiTotalAmount && isApiTotalAmountDifferent ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
내부 계산 총자산 {formatCurrency(summary?.totalAmount ?? 0)}원 · KIS 총자산{" "}
|
||||
{formatCurrency(summary?.apiReportedTotalAmount ?? 0)}원
|
||||
</p>
|
||||
) : null}
|
||||
{hasApiNetAssetAmount ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
KIS 집계 순자산 {formatCurrency(summary?.apiReportedNetAssetAmount ?? 0)}원
|
||||
</p>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewMetric({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
toneClass,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
toneClass: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-xl border px-3 py-2", toneClass)}>
|
||||
<p className="flex items-center gap-1 text-[11px] font-medium opacity-85">
|
||||
{icon}
|
||||
{label}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs font-semibold">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 계좌번호를 마스킹해 표시합니다.
|
||||
* @param value 계좌번호(8-2)
|
||||
|
||||
Reference in New Issue
Block a user