PriceHero gains an optional flow={open,high,low,volume} prop. Rendered
as a compact 4-KPI inline strip (시/고/저/거래량) below the change
line. Wraps naturally on narrow widths via flex-wrap.
page.tsx computes flow from chart.ohlcv tail inside the existing
useMemo:
- 1D (10m): groups bars by same KST date as last bar, aggregates as
session OHLV (open=first, high=max, low=min, volume=sum).
- 1d/1w/1mo: just the last valid bar's OHLV (each bar already
represents one unit).
Volume formatted with the same 억/만 rule as StockChart fmtKVolume —
small enough to inline at use site instead of carving out a shared
format module. High/low labels get rose-300/sky-300 ink as a soft
directional hint while the numbers themselves stay neutral (zinc-200)
to avoid implying a misleading +/- sign.
129 lines
4.6 KiB
TypeScript
129 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
// 종목 페이지 상단 가격 헤더.
|
|
// 표시: 종목명 / (코드·시장) / 현재가 / 전일대비 + 우측에 sparkline.
|
|
// 한국 거래소 관행대로 상승=빨강, 하락=파랑.
|
|
|
|
import { Sparkline } from "./Sparkline";
|
|
|
|
type Props = {
|
|
name: string;
|
|
code: string;
|
|
market: string;
|
|
current: number | null;
|
|
prev: number | null;
|
|
asOf?: string | null;
|
|
// 헤더 우측 sparkline 용 종가 시계열 (오래된 → 최신 순). 비어 있으면 미표시.
|
|
series?: number[];
|
|
// social-proof — "오늘 N명이 봤어요" 배지. null/0 이면 숨김.
|
|
viewsToday?: number | null;
|
|
// 오늘(또는 마지막 봉) 흐름 요약. page.tsx 에서 ohlcv 끝부분으로 계산해 전달.
|
|
// null 이면 strip 자체를 숨김 (데이터 없음 + 레이아웃 jitter 방지).
|
|
flow?: { open: number; high: number; low: number; volume: number } | null;
|
|
};
|
|
|
|
// 거래량 한국식 단위 (억/만) — StockChart 의 fmtKVolume 과 같은 규칙. 둘 다 작아서
|
|
// 별도 모듈로 빼지 않고 사용처에 인라인 유지.
|
|
function fmtVolume(n: number): string {
|
|
if (!Number.isFinite(n) || n <= 0) return "-";
|
|
if (n >= 1e8) return `${(n / 1e8).toFixed(1)}억`;
|
|
if (n >= 1e4) return `${(n / 1e4).toFixed(1)}만`;
|
|
return n.toLocaleString();
|
|
}
|
|
|
|
export function PriceHero({
|
|
name,
|
|
code,
|
|
market,
|
|
current,
|
|
prev,
|
|
asOf,
|
|
series,
|
|
viewsToday,
|
|
flow,
|
|
}: Props) {
|
|
const change = current != null && prev != null ? current - prev : null;
|
|
const pct = change != null && prev ? (change / prev) * 100 : null;
|
|
const tone =
|
|
change == null || change === 0
|
|
? "text-zinc-400"
|
|
: change > 0
|
|
? "text-rose-400"
|
|
: "text-sky-400";
|
|
const arrow = change == null || change === 0 ? "—" : change > 0 ? "▲" : "▼";
|
|
|
|
return (
|
|
<div className="mb-5 flex items-end justify-between gap-4">
|
|
<div>
|
|
<div className="text-xs text-zinc-500">
|
|
{code} · {market}
|
|
</div>
|
|
<h1 className="mt-0.5 text-2xl font-bold tracking-tight text-zinc-50">
|
|
{name}
|
|
</h1>
|
|
<div className="mt-3 flex items-baseline gap-3">
|
|
<span className="text-4xl font-semibold tabular-nums text-zinc-50">
|
|
{current != null ? current.toLocaleString() : "-"}
|
|
</span>
|
|
{change != null && pct != null && (
|
|
<span className={`text-base font-medium tabular-nums ${tone}`}>
|
|
{arrow} {Math.abs(change).toLocaleString()}{" "}
|
|
({pct >= 0 ? "+" : ""}
|
|
{pct.toFixed(2)}%)
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-2 text-xs text-zinc-500">
|
|
{asOf && <span>기준 {asOf}</span>}
|
|
{viewsToday != null && viewsToday > 0 && (
|
|
<span className="rounded-full border border-zinc-700/70 bg-zinc-900/60 px-2 py-0.5 text-[11px] text-zinc-300">
|
|
오늘 <span className="tabular-nums text-zinc-100">
|
|
{viewsToday.toLocaleString()}
|
|
</span>
|
|
명이 봤어요
|
|
</span>
|
|
)}
|
|
</div>
|
|
{flow && (
|
|
// 오늘의 흐름 요약 — 시/고/저/거래량 4-KPI mini strip. 차트 hover 없이도 진입 즉시 보임.
|
|
// 고/저 가격 자체에는 색을 칠하지 않음 (양수/음수 의미가 없으므로 톤이 거짓 신호가 됨).
|
|
// 대신 라벨만 sky-300(저) / rose-300(고) 으로 살짝 칠해 직관 보조.
|
|
<div className="mt-3 flex flex-wrap items-baseline gap-x-4 gap-y-1 text-[11px] text-zinc-400">
|
|
<span>
|
|
시{" "}
|
|
<span className="tabular-nums text-zinc-200">
|
|
{flow.open.toLocaleString()}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
<span className="text-rose-300">고</span>{" "}
|
|
<span className="tabular-nums text-zinc-200">
|
|
{flow.high.toLocaleString()}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
<span className="text-sky-300">저</span>{" "}
|
|
<span className="tabular-nums text-zinc-200">
|
|
{flow.low.toLocaleString()}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
거래량{" "}
|
|
<span className="tabular-nums text-zinc-200">
|
|
{fmtVolume(flow.volume)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{series && series.length >= 2 && (
|
|
<Sparkline
|
|
values={series}
|
|
stroke={change != null && change < 0 ? "#3b82f6" : "#ef4444"}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|