diff --git a/web/app/[code]/page.tsx b/web/app/[code]/page.tsx
index 74a1b64..f40863f 100644
--- a/web/app/[code]/page.tsx
+++ b/web/app/[code]/page.tsx
@@ -8,6 +8,7 @@ import { PeriodTabs, periodSpec, type Period } from "../../components/PeriodTabs
import { PredictionPanel } from "../../components/PredictionPanel";
import { PriceHero } from "../../components/PriceHero";
import { StockChart } from "../../components/StockChart";
+import { SymbolSidebar } from "../../components/SymbolSidebar";
import { api, type ChartPayload, type LatestPredictionResponse } from "../../lib/api";
export default function CodePage({ params }: { params: { code: string } }) {
@@ -100,14 +101,19 @@ export default function CodePage({ params }: { params: { code: string } }) {
prev={prev}
asOf={asOf}
/>
-
- {isIntraday && chart.intraday_status && (
-
- 실시간 10분봉 · 60초 갱신 · [{chart.intraday_status}]
+
+
+
+ {isIntraday && chart.intraday_status && (
+
+ 실시간 10분봉 · 60초 갱신 · [{chart.intraday_status}]
+
+ )}
+
- )}
-
diff --git a/web/components/SymbolSidebar.tsx b/web/components/SymbolSidebar.tsx
new file mode 100644
index 0000000..d6d112e
--- /dev/null
+++ b/web/components/SymbolSidebar.tsx
@@ -0,0 +1,117 @@
+"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;
+ }
+ return {
+ open: last.open,
+ high: last.high,
+ low: last.low,
+ volume: last.volume,
+ yearHigh: yearHigh === -Infinity ? null : yearHigh,
+ yearLow: yearLow === Infinity ? null : yearLow,
+ };
+ }, [chart]);
+
+ if (err) {
+ return (
+
+ 시세 정보 로딩 실패: {err}
+
+ );
+ }
+ if (!stats) {
+ return (
+
+ 시세 정보 로딩 중…
+
+ );
+ }
+
+ return (
+
+
시세 정보
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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();
+}