diff --git a/web/app/[code]/page.tsx b/web/app/[code]/page.tsx index 1931eb7..091b65c 100644 --- a/web/app/[code]/page.tsx +++ b/web/app/[code]/page.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { DisclosuresPanel } from "../../components/DisclosuresPanel"; import { MetricsPanel } from "../../components/MetricsPanel"; import { NewsList } from "../../components/NewsList"; +import { PeriodReturns } from "../../components/PeriodReturns"; import { PeriodTabs, periodSpec, type Period } from "../../components/PeriodTabs"; import { PredictionPanel } from "../../components/PredictionPanel"; import { PriceHero } from "../../components/PriceHero"; @@ -12,6 +13,7 @@ import { RelatedStocks } from "../../components/RelatedStocks"; import { StarButton } from "../../components/StarButton"; import { StockChart } from "../../components/StockChart"; import { SymbolSidebar } from "../../components/SymbolSidebar"; +import { TradingValuePanel } from "../../components/TradingValuePanel"; import { api, type ChartPayload, type LatestPredictionResponse } from "../../lib/api"; export default function CodePage({ params }: { params: { code: string } }) { @@ -138,11 +140,15 @@ export default function CodePage({ params }: { params: { code: string } }) { -
- - -
+ +
+
+ + +
+
+
diff --git a/web/components/PeriodReturns.tsx b/web/components/PeriodReturns.tsx new file mode 100644 index 0000000..1b9d432 --- /dev/null +++ b/web/components/PeriodReturns.tsx @@ -0,0 +1,73 @@ +"use client"; + +// 기간별 수익률 패널 — chart.ohlcv 에서 직접 계산. +// 토스 종목 페이지의 "기간 수익률" 박스 모사. 1주/1개월/3개월/6개월/1년/전체. +// 거래일 기준 근사: 1주≈5, 1개월≈21, 3개월≈63, 6개월≈126, 1년≈252. + +import type { OhlcvPoint } from "../lib/api"; + +const BUCKETS: { label: string; days: number }[] = [ + { label: "1주", days: 5 }, + { label: "1개월", days: 21 }, + { label: "3개월", days: 63 }, + { label: "6개월", days: 126 }, + { label: "1년", days: 252 }, +]; + +export function PeriodReturns({ ohlcv }: { ohlcv: OhlcvPoint[] }) { + const closes = ohlcv + .map((p) => p.close) + .filter((c): c is number => c != null); + if (closes.length < 2) { + return ( +
+ 기간 수익률 데이터 없음 +
+ ); + } + const last = closes[closes.length - 1]; + const buckets = BUCKETS.map((b) => { + const idx = closes.length - 1 - b.days; + if (idx < 0) return { ...b, pct: null as number | null }; + const base = closes[idx]; + return { ...b, pct: base ? ((last - base) / base) * 100 : null }; + }); + const all = + closes[0] != null && closes[0] !== 0 + ? ((last - closes[0]) / closes[0]) * 100 + : null; + + return ( +
+
+
기간 수익률
+
차트 구간 기준
+
+
+ {buckets.map((b) => ( + + ))} + +
+
+ ); +} + +function Bucket({ label, pct }: { label: string; pct: number | null }) { + const tone = + pct == null || pct === 0 + ? "text-zinc-400" + : pct > 0 + ? "text-rose-400" + : "text-sky-400"; + return ( +
+
{label}
+
+ {pct == null + ? "—" + : `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`} +
+
+ ); +} diff --git a/web/components/TradingValuePanel.tsx b/web/components/TradingValuePanel.tsx new file mode 100644 index 0000000..46c49a2 --- /dev/null +++ b/web/components/TradingValuePanel.tsx @@ -0,0 +1,83 @@ +"use client"; + +// 거래주체 패널 — chart.trading_value (외국인/기관/개인 순매수) 를 최근 5일 표로. +// 별도 백엔드 호출 없음. ChartPayload 에 이미 들어있는 시리즈를 재사용. +// 단위: 백엔드는 원(KRW) 단위. UI 는 억원(억원=1e8)으로 환산. +// 색: KR 관행 — 매수 우위(+) 빨강, 매도 우위(-) 파랑. + +import type { TradingValuePoint } from "../lib/api"; + +export function TradingValuePanel({ + data, +}: { + data: TradingValuePoint[]; +}) { + if (!data || data.length === 0) { + return ( +
+ 거래주체 데이터 없음 +
+ ); + } + // 최근 5일만 (입력은 오래된→최신 순), 표시는 최신이 위. + const recent = data.slice(-5).reverse(); + + return ( +
+
+
거래주체 순매수
+
최근 {recent.length}일 · 단위: 억원
+
+
+ + + + + + + + + + + {recent.map((row) => ( + + + + + + + ))} + +
일자외국인기관개인
+ {shortDate(row.date)} +
+
+
+ ); +} + +function NetCell({ value }: { value: number | null }) { + if (value == null) { + return —; + } + const eok = value / 1e8; + const tone = + eok === 0 + ? "text-zinc-400" + : eok > 0 + ? "text-rose-400" + : "text-sky-400"; + const sign = eok > 0 ? "+" : ""; + return ( + + {sign} + {eok.toLocaleString(undefined, { maximumFractionDigits: 0 })} + + ); +} + +function shortDate(iso: string): string { + // 'YYYY-MM-DD' → 'MM-DD' + const m = /^\d{4}-(\d{2})-(\d{2})/.exec(iso); + return m ? `${m[1]}-${m[2]}` : iso; +}