"use client"; // 토스증권 우측 정보 사이드바를 흉내낸 카드 묶음. // // 표시 항목 (백엔드가 안정적으로 제공하는 값으로 한정): // - 오늘: 시가 / 고가 / 저가 / 거래량 // - 52주: 최고 / 최저 (1Y 일봉에서 계산) // // 백엔드에 PER/EPS/시총 데이터가 아직 없으므로 그 항목들은 의도적으로 제외. // 차트 폴링과 독립적으로 1Y 일봉을 한 번만 받아 52주 범위만 계산한다. import { useEffect, useMemo, useState } from "react"; import { api, type ChartPayload } from "../lib/api"; export function SymbolSidebar({ code }: { code: string }) { const [chart, setChart] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let alive = true; setErr(null); setChart(null); api .getChart(code, 365, "1d") .then((c) => { if (alive) setChart(c); }) .catch((e) => { if (alive) setErr(e instanceof Error ? e.message : String(e)); }); return () => { alive = false; }; }, [code]); const stats = useMemo(() => { if (!chart) return null; const points = chart.ohlcv.filter((p) => p.close != null); if (!points.length) return null; const last = points[points.length - 1]; let yearHigh = -Infinity; let yearLow = Infinity; for (const p of points) { if (p.high != null && p.high > yearHigh) yearHigh = p.high; if (p.low != null && p.low < yearLow) yearLow = p.low; } const yh = yearHigh === -Infinity ? null : yearHigh; const yl = yearLow === Infinity ? null : yearLow; // 52주 위치 (0~1). 종가 기준, 분모 0 방어. let pos: number | null = null; if (yh != null && yl != null && yh > yl && last.close != null) { pos = Math.min(1, Math.max(0, (last.close - yl) / (yh - yl))); } return { open: last.open, high: last.high, low: last.low, close: last.close, volume: last.volume, yearHigh: yh, yearLow: yl, yearPos: pos, }; }, [chart]); if (err) { return (
시세 정보 로딩 실패: {err}
); } if (!stats) { return (
시세 정보 로딩 중…
); } return (
시세 정보
{stats.yearPos != null && ( )}
); } // 52주 범위 내 현재가 위치 — 토스 종목 상세에 있는 가로 게이지를 흉내냄. function YearRangeBar({ low, high, pos, }: { low: number | null; high: number | null; pos: number; }) { const pct = pos * 100; return (
52주 위치 {pct.toFixed(0)}%
{fmt(low)} {fmt(high)}
); } function Row({ label, value, tone, }: { label: string; value: string; tone?: "up" | "down"; }) { const toneCls = tone === "up" ? "text-rose-300" : tone === "down" ? "text-sky-300" : "text-zinc-100"; return ( <>
{label}
{value}
); } function fmt(n: number | null | undefined): string { if (n == null || !Number.isFinite(n)) return "-"; return n.toLocaleString(undefined, { maximumFractionDigits: 0 }); } function fmtCompact(n: number | null | undefined): string { if (n == null || !Number.isFinite(n)) return "-"; if (n >= 1e8) return `${(n / 1e8).toFixed(1)}억`; if (n >= 1e4) return `${(n / 1e4).toFixed(1)}만`; return n.toLocaleString(); }