Files
auto-trade/features/dashboard/components/DashboardContainer.tsx
2026-02-12 14:20:07 +09:00

116 lines
3.6 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { DashboardAccessGate } from "@/features/dashboard/components/DashboardAccessGate";
import { DashboardSkeleton } from "@/features/dashboard/components/DashboardSkeleton";
import { HoldingsList } from "@/features/dashboard/components/HoldingsList";
import { MarketSummary } from "@/features/dashboard/components/MarketSummary";
import { StatusHeader } from "@/features/dashboard/components/StatusHeader";
import { StockDetailPreview } from "@/features/dashboard/components/StockDetailPreview";
import { useDashboardData } from "@/features/dashboard/hooks/use-dashboard-data";
import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
/**
* @description 대시보드 메인 컨테이너입니다.
* @remarks UI 흐름: 대시보드 진입 -> useDashboardData API 호출 -> StatusHeader/MarketSummary/HoldingsList/StockDetailPreview 순으로 렌더링
* @see app/(main)/dashboard/page.tsx 로그인 완료 후 이 컴포넌트를 렌더링합니다.
* @see features/dashboard/hooks/use-dashboard-data.ts 대시보드 데이터 조회/갱신 상태를 관리합니다.
*/
export function DashboardContainer() {
const {
verifiedCredentials,
isKisVerified,
_hasHydrated,
wsApprovalKey,
wsUrl,
} = useKisRuntimeStore(
useShallow((state) => ({
verifiedCredentials: state.verifiedCredentials,
isKisVerified: state.isKisVerified,
_hasHydrated: state._hasHydrated,
wsApprovalKey: state.wsApprovalKey,
wsUrl: state.wsUrl,
})),
);
const canAccess = isKisVerified && Boolean(verifiedCredentials);
const {
balance,
indices,
selectedHolding,
selectedSymbol,
setSelectedSymbol,
isLoading,
isRefreshing,
balanceError,
indicesError,
lastUpdatedAt,
refresh,
} = useDashboardData(canAccess ? verifiedCredentials : null);
const isKisRestConnected = useMemo(() => {
if (indices.length > 0) return true;
if (balance && !balanceError) return true;
return false;
}, [balance, balanceError, indices.length]);
if (!_hasHydrated) {
return (
<div className="flex h-full items-center justify-center p-6">
<LoadingSpinner />
</div>
);
}
if (!canAccess) {
return <DashboardAccessGate canAccess={canAccess} />;
}
if (isLoading && !balance && indices.length === 0) {
return <DashboardSkeleton />;
}
return (
<section className="mx-auto flex w-full max-w-7xl flex-col gap-4 p-4 md:p-6">
{/* ========== STATUS HEADER ========== */}
<StatusHeader
summary={balance?.summary ?? null}
isKisRestConnected={isKisRestConnected}
isWebSocketReady={Boolean(wsApprovalKey && wsUrl)}
isRefreshing={isRefreshing}
lastUpdatedAt={lastUpdatedAt}
onRefresh={() => {
void refresh();
}}
/>
{/* ========== MAIN CONTENT GRID ========== */}
<div className="grid gap-4 lg:grid-cols-[1.15fr_1fr]">
<HoldingsList
holdings={balance?.holdings ?? []}
selectedSymbol={selectedSymbol}
isLoading={isLoading}
error={balanceError}
onSelect={setSelectedSymbol}
/>
<div className="grid gap-4">
<MarketSummary
items={indices}
isLoading={isLoading}
error={indicesError}
/>
<StockDetailPreview
holding={selectedHolding}
totalAmount={balance?.summary.totalAmount ?? 0}
/>
</div>
</div>
</section>
);
}