Files
stock_chart_site/web/components/MarketsOverview.tsx
claude-owner 645e93e7bf feat(market): 거래대금 상위 + 52주 위치 게이지
- /api/markets/movers: by_trading_value (close*volume DESC) 카테고리 추가
- MarketsOverview: 4 컬럼 그리드 (등락 상위/하위/거래대금/거래량), 거래대금은 억·조 표기
- SymbolSidebar: 52주 범위 내 현재가 위치 게이지 (그라데이션 바 + 화이트 점)
2026-05-28 15:21:27 +09:00

162 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// 시장 overview: 등락 상위 / 등락 하위 / 거래대금 상위 / 거래량 상위 4 컬럼.
// /api/markets/movers 응답을 그대로 표 형태로 렌더.
// 거래대금 = close × volume, 단위 억원. 토스 톤의 핵심 랭킹.
import Link from "next/link";
import { useEffect, useState } from "react";
import { api, type Mover, type MoversResponse } from "../lib/api";
export function MarketsOverview() {
const [data, setData] = useState<MoversResponse | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let alive = true;
api
.movers("ALL", 10)
.then((r) => {
if (alive) setData(r);
})
.catch((e) => {
if (alive) setErr(e instanceof Error ? e.message : String(e));
});
return () => {
alive = false;
};
}, []);
if (err)
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-xs text-zinc-500">
overview : {err}
</div>
);
if (!data)
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-xs text-zinc-500">
overview
</div>
);
// 아직 ohlcv_daily 가 비어 있으면 모든 리스트가 빈 배열로 옴 — 안내만.
const empty =
!data.gainers.length &&
!data.losers.length &&
!data.by_volume.length &&
!data.by_trading_value.length;
if (empty)
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-xs text-zinc-500">
. ohlcv_daily .
</div>
);
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<MoverList title="등락 상위" items={data.gainers} />
<MoverList title="등락 하위" items={data.losers} />
<MoverList
title="거래대금 상위"
items={data.by_trading_value}
metric="trading_value"
/>
<MoverList title="거래량 상위" items={data.by_volume} metric="volume" />
</div>
);
}
type Metric = "price" | "volume" | "trading_value";
function MoverList({
title,
items,
metric = "price",
}: {
title: string;
items: Mover[];
metric?: Metric;
}) {
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/30 p-3">
<div className="mb-2 text-xs font-medium text-zinc-300">{title}</div>
{items.length === 0 ? (
<div className="text-[11px] text-zinc-500"> </div>
) : (
<ul className="divide-y divide-zinc-800">
{items.map((m, i) => (
<MoverRow key={m.code} rank={i + 1} m={m} metric={metric} />
))}
</ul>
)}
</div>
);
}
function MoverRow({
rank,
m,
metric,
}: {
rank: number;
m: Mover;
metric: Metric;
}) {
const pct = m.pct_change;
const tone =
pct == null || pct === 0
? "text-zinc-400"
: pct > 0
? "text-rose-400"
: "text-sky-400";
let primary: string;
if (metric === "volume") {
primary = fmtVol(m.volume);
} else if (metric === "trading_value") {
primary = fmtKrw(m.trading_value ?? null);
} else {
primary =
m.close != null
? m.close.toLocaleString(undefined, { maximumFractionDigits: 0 })
: "—";
}
return (
<li>
<Link
href={`/${m.code}`}
className="flex items-center justify-between gap-2 py-1.5 text-xs hover:bg-zinc-800/40"
>
<div className="flex min-w-0 items-center gap-2">
<span className="w-4 shrink-0 text-zinc-500">{rank}</span>
<span className="min-w-0 truncate text-zinc-100">{m.name}</span>
</div>
<div className="shrink-0 text-right tabular-nums">
<div className="text-zinc-100">{primary}</div>
<div className={`text-[10px] ${tone}`}>
{pct != null ? `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%` : ""}
</div>
</div>
</Link>
</li>
);
}
function fmtVol(n: number | null): string {
if (n == null) return "—";
if (n >= 1e8) return `${(n / 1e8).toFixed(1)}`;
if (n >= 1e4) return `${(n / 1e4).toFixed(1)}`;
return n.toLocaleString();
}
// 거래대금 원 → 억/조 표기. 토스에서 가장 흔한 단위.
function fmtKrw(n: number | null): string {
if (n == null) return "—";
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}`;
if (n >= 1e8) return `${Math.round(n / 1e8).toLocaleString()}`;
if (n >= 1e4) return `${(n / 1e4).toFixed(0)}`;
return n.toLocaleString();
}