전체적인 리팩토링
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { BriefcaseBusiness, Gauge, Sparkles } from "lucide-react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ActivitySection } from "@/features/dashboard/components/ActivitySection";
|
||||
import { DashboardAccessGate } from "@/features/dashboard/components/DashboardAccessGate";
|
||||
import { DashboardSkeleton } from "@/features/dashboard/components/DashboardSkeleton";
|
||||
import { HoldingsList } from "@/features/dashboard/components/HoldingsList";
|
||||
import { MarketHubSection } from "@/features/dashboard/components/MarketHubSection";
|
||||
import { MarketSummary } from "@/features/dashboard/components/MarketSummary";
|
||||
import { StatusHeader } from "@/features/dashboard/components/StatusHeader";
|
||||
import { StockDetailPreview } from "@/features/dashboard/components/StockDetailPreview";
|
||||
@@ -70,6 +73,8 @@ export function DashboardContainer() {
|
||||
activityError,
|
||||
balanceError,
|
||||
indicesError,
|
||||
marketHub,
|
||||
marketHubError,
|
||||
lastUpdatedAt,
|
||||
refresh,
|
||||
} = useDashboardData(canAccess ? verifiedCredentials : null);
|
||||
@@ -125,6 +130,15 @@ export function DashboardContainer() {
|
||||
wsApprovalKey && wsUrl && isWsConnected && !hasRealtimeStreaming,
|
||||
);
|
||||
const effectiveIndicesError = indices.length === 0 ? indicesError : null;
|
||||
const restStatusLabel = isKisRestConnected ? "REST 정상" : "REST 점검 필요";
|
||||
const realtimeStatusLabel = isWsConnected
|
||||
? isRealtimePending
|
||||
? "실시간 대기중"
|
||||
: "실시간 수신중"
|
||||
: "실시간 미연결";
|
||||
const profileStatusLabel = isKisProfileVerified
|
||||
? "계좌 인증 완료"
|
||||
: "계좌 인증 필요";
|
||||
const indicesWarning =
|
||||
indices.length > 0 && indicesError
|
||||
? "지수 API 재요청 중입니다. 화면 값은 실시간/최근 성공 데이터입니다."
|
||||
@@ -181,71 +195,161 @@ export function DashboardContainer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mx-auto flex w-full max-w-7xl flex-col gap-4 p-4 md:p-6">
|
||||
{/* ========== 상단 상태 영역: 계좌 연결 정보 및 새로고침 ========== */}
|
||||
<StatusHeader
|
||||
summary={mergedSummary}
|
||||
isKisRestConnected={isKisRestConnected}
|
||||
isWebSocketReady={Boolean(wsApprovalKey && wsUrl && isWsConnected)}
|
||||
isRealtimePending={isRealtimePending}
|
||||
isProfileVerified={isKisProfileVerified}
|
||||
verifiedAccountNo={verifiedAccountNo}
|
||||
isRefreshing={isRefreshing}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
onRefresh={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
<section className="relative mx-auto flex w-full max-w-7xl flex-col gap-4 overflow-hidden p-4 md:p-6">
|
||||
<div className="pointer-events-none absolute -left-24 top-10 h-52 w-52 rounded-full bg-brand-400/15 blur-3xl dark:bg-brand-600/15" />
|
||||
<div className="pointer-events-none absolute -right-28 top-36 h-64 w-64 rounded-full bg-brand-300/20 blur-3xl dark:bg-brand-700/20" />
|
||||
|
||||
{/* ========== 메인 그리드 구성 ========== */}
|
||||
<div className="grid gap-4 lg:grid-cols-[1.15fr_1fr]">
|
||||
{/* 왼쪽 섹션: 보유 종목 목록 리스트 */}
|
||||
<HoldingsList
|
||||
holdings={mergedHoldings}
|
||||
selectedSymbol={selectedSymbol}
|
||||
isLoading={isLoading}
|
||||
error={balanceError}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
onSelect={setSelectedSymbol}
|
||||
/>
|
||||
<div className="relative rounded-3xl border border-brand-200/70 bg-linear-to-br from-brand-100/70 via-brand-50/30 to-background p-4 shadow-sm dark:border-brand-800/50 dark:from-brand-900/35 dark:via-brand-950/15">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p className="inline-flex items-center gap-1.5 rounded-full border border-brand-300/70 bg-background/80 px-3 py-1 text-[11px] font-semibold tracking-wide text-brand-700 dark:border-brand-700 dark:bg-brand-950/50 dark:text-brand-300">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
TRADING OVERVIEW
|
||||
</p>
|
||||
<h1 className="text-xl font-bold tracking-tight text-foreground md:text-2xl">
|
||||
시장과 내 자산을 한 화면에서 빠르게 확인하세요
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
핵심 지표를 상단에 모으고, 시장 흐름과 자산 상태를 탭으로 분리했습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽 섹션: 시장 지수 요약 및 선택 종목 상세 정보 */}
|
||||
<div className="grid gap-4">
|
||||
{/* 시장 지수 현황 (코스피/코스닥) */}
|
||||
<MarketSummary
|
||||
items={indices}
|
||||
isLoading={isLoading}
|
||||
error={effectiveIndicesError}
|
||||
warning={indicesWarning}
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<TopStatusPill label="서버" value={restStatusLabel} ok={isKisRestConnected} />
|
||||
<TopStatusPill
|
||||
label="실시간"
|
||||
value={realtimeStatusLabel}
|
||||
ok={isWsConnected}
|
||||
warn={isRealtimePending}
|
||||
/>
|
||||
<TopStatusPill
|
||||
label="계좌"
|
||||
value={profileStatusLabel}
|
||||
ok={isKisProfileVerified}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="assets" className="relative gap-4">
|
||||
<TabsList className="h-auto w-full justify-start rounded-2xl border border-brand-200/80 bg-background/90 p-1 backdrop-blur-sm dark:border-brand-800/50 dark:bg-background/60">
|
||||
<TabsTrigger
|
||||
value="market"
|
||||
className="h-10 rounded-xl px-4 data-[state=active]:bg-brand-600 data-[state=active]:text-white data-[state=active]:shadow-md dark:data-[state=active]:bg-brand-600"
|
||||
>
|
||||
<Gauge className="h-4 w-4" />
|
||||
시장 인사이트
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="assets"
|
||||
className="h-10 rounded-xl px-4 data-[state=active]:bg-brand-600 data-[state=active]:text-white data-[state=active]:shadow-md dark:data-[state=active]:bg-brand-600"
|
||||
>
|
||||
<BriefcaseBusiness className="h-4 w-4" />
|
||||
내 자산
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="market" className="space-y-4 animate-in fade-in-0 slide-in-from-bottom-2 duration-300">
|
||||
<div className="grid gap-4 xl:grid-cols-[1.05fr_1.45fr]">
|
||||
<MarketSummary
|
||||
items={indices}
|
||||
isLoading={isLoading}
|
||||
error={effectiveIndicesError}
|
||||
warning={indicesWarning}
|
||||
isWebSocketReady={isWsConnected}
|
||||
isRealtimePending={isRealtimePending}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
<MarketHubSection
|
||||
marketHub={marketHub}
|
||||
isLoading={isLoading}
|
||||
error={marketHubError}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="assets" className="space-y-4 animate-in fade-in-0 slide-in-from-bottom-2 duration-300">
|
||||
<StatusHeader
|
||||
summary={mergedSummary}
|
||||
isKisRestConnected={isKisRestConnected}
|
||||
isWebSocketReady={Boolean(wsApprovalKey && wsUrl && isWsConnected)}
|
||||
isRealtimePending={isRealtimePending}
|
||||
onRetry={() => {
|
||||
isProfileVerified={isKisProfileVerified}
|
||||
verifiedAccountNo={verifiedAccountNo}
|
||||
isRefreshing={isRefreshing}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
onRefresh={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 선택된 종목의 실시간 상세 요약 정보 */}
|
||||
<StockDetailPreview
|
||||
holding={realtimeSelectedHolding}
|
||||
totalAmount={mergedSummary?.totalAmount ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 xl:grid-cols-[1.25fr_0.75fr]">
|
||||
<div className="space-y-4">
|
||||
<ActivitySection
|
||||
activity={activity}
|
||||
isLoading={isLoading}
|
||||
error={activityError}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ========== 하단 섹션: 최근 매매/충전 활동 내역 ========== */}
|
||||
<ActivitySection
|
||||
activity={activity}
|
||||
isLoading={isLoading}
|
||||
error={activityError}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
/>
|
||||
<HoldingsList
|
||||
holdings={mergedHoldings}
|
||||
selectedSymbol={selectedSymbol}
|
||||
isLoading={isLoading}
|
||||
error={balanceError}
|
||||
onRetry={() => {
|
||||
void handleRefreshAll();
|
||||
}}
|
||||
onSelect={setSelectedSymbol}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:sticky xl:top-5 xl:self-start">
|
||||
<StockDetailPreview
|
||||
holding={realtimeSelectedHolding}
|
||||
totalAmount={mergedSummary?.totalAmount ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TopStatusPill({
|
||||
label,
|
||||
value,
|
||||
ok,
|
||||
warn = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
ok: boolean;
|
||||
warn?: boolean;
|
||||
}) {
|
||||
const toneClass = ok
|
||||
? warn
|
||||
? "border-amber-300/70 bg-amber-50/70 text-amber-700 dark:border-amber-700/50 dark:bg-amber-950/30 dark:text-amber-300"
|
||||
: "border-emerald-300/70 bg-emerald-50/70 text-emerald-700 dark:border-emerald-700/50 dark:bg-emerald-950/30 dark:text-emerald-300"
|
||||
: "border-red-300/70 bg-red-50/70 text-red-700 dark:border-red-700/50 dark:bg-red-950/30 dark:text-red-300";
|
||||
|
||||
return (
|
||||
<div className={`rounded-xl border px-3 py-2 ${toneClass}`}>
|
||||
<p className="text-[11px] font-medium opacity-80">{label}</p>
|
||||
<p className="text-xs font-semibold">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description REST 지수 데이터가 없을 때 실시간 수신값만으로 코스피/코스닥 표시 데이터를 구성합니다.
|
||||
* @param realtimeIndices 실시간 지수 맵
|
||||
|
||||
Reference in New Issue
Block a user