116 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
}
|