-
학습 대상 10종목
-
- 삼성전자, SK하이닉스, 에코프로비엠, 한미반도체, 두산에너빌리티,
- 한화에어로스페이스, HD현대중공업, NAVER, KT&G, 한국가스공사
-
-
-
매칭/재학습
-
- 평일 16:30 KST 예측↔실제 매칭, 일요일 02:00 KST LGBM 재학습.
- 앙상블은 Chronos(zero-shot) + LightGBM.
-
+
+
+
+
+
+
매칭 · 재학습
+
+ 평일 16:30 KST 예측↔실제 매칭, 일요일 02:00 KST LGBM 재학습.
+ 앙상블은 Chronos(zero-shot) + LightGBM.
diff --git a/web/components/PriceHero.tsx b/web/components/PriceHero.tsx
index c282227..b25a8db 100644
--- a/web/components/PriceHero.tsx
+++ b/web/components/PriceHero.tsx
@@ -1,7 +1,7 @@
"use client";
// 종목 페이지 상단 가격 헤더.
-// 표시: 종목명 / (코드·시장) / 현재가 / 전일대비 (절대값 + 퍼센트).
+// 표시: 종목명 / (코드·시장) / 현재가 / 전일대비 + 우측에 sparkline.
// 한국 거래소 관행대로 상승=빨강, 하락=파랑.
type Props = {
@@ -11,9 +11,11 @@ type Props = {
current: number | null;
prev: number | null;
asOf?: string | null;
+ // 헤더 우측 sparkline 용 종가 시계열 (오래된 → 최신 순). 비어 있으면 미표시.
+ series?: number[];
};
-export function PriceHero({ name, code, market, current, prev, asOf }: Props) {
+export function PriceHero({ name, code, market, current, prev, asOf, series }: Props) {
const change = current != null && prev != null ? current - prev : null;
const pct = change != null && prev ? (change / prev) * 100 : null;
const tone =
@@ -25,28 +27,70 @@ export function PriceHero({ name, code, market, current, prev, asOf }: Props) {
const arrow = change == null || change === 0 ? "—" : change > 0 ? "▲" : "▼";
return (
-
-
- {code} · {market}
-
-
- {name}
-
-
-
- {current != null ? current.toLocaleString() : "-"}
-
- {change != null && pct != null && (
-
- {arrow} {Math.abs(change).toLocaleString()}{" "}
- ({pct >= 0 ? "+" : ""}
- {pct.toFixed(2)}%)
+
+
+
+ {code} · {market}
+
+
+ {name}
+
+
+
+ {current != null ? current.toLocaleString() : "-"}
- )}
+ {change != null && pct != null && (
+
+ {arrow} {Math.abs(change).toLocaleString()}{" "}
+ ({pct >= 0 ? "+" : ""}
+ {pct.toFixed(2)}%)
+
+ )}
+
+ {asOf &&
기준 {asOf}
}
- {asOf && (
-
기준 {asOf}
+
+ {series && series.length >= 2 && (
+
)}
);
}
+
+// 가벼운 SVG sparkline. lightweight-charts 인스턴스를 또 만들 필요 없음.
+function Sparkline({ values, stroke }: { values: number[]; stroke: string }) {
+ const W = 140;
+ const H = 44;
+ const min = Math.min(...values);
+ const max = Math.max(...values);
+ const span = max - min || 1;
+ const stepX = W / (values.length - 1);
+ const points = values
+ .map((v, i) => {
+ const x = (i * stepX).toFixed(1);
+ const y = (H - ((v - min) / span) * H).toFixed(1);
+ return `${x},${y}`;
+ })
+ .join(" ");
+ return (
+
+ );
+}
diff --git a/web/components/SeedTiles.tsx b/web/components/SeedTiles.tsx
new file mode 100644
index 0000000..41e959f
--- /dev/null
+++ b/web/components/SeedTiles.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+// 학습 대상 SEED 10종목 quick-access 타일.
+//
+// 토스 홈의 '인기 종목' 카드 톤. 각 타일이 자체적으로 /api/chart 를 2일치 받아
+// 현재가/전일대비 를 계산해서 표시. 10개 병렬 호출 — 백엔드는 캐시 친화적이므로 OK.
+//
+// 백엔드 신규 엔드포인트가 추가되면 (예: /api/markets/top-volume) 그쪽으로 갈아끼움.
+
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { api } from "../lib/api";
+
+type Tile = {
+ code: string;
+ name: string;
+ market: string;
+ current: number | null;
+ prev: number | null;
+};
+
+const SEED_TICKERS: Array> = [
+ { code: "005930", name: "삼성전자", market: "KOSPI" },
+ { code: "000660", name: "SK하이닉스", market: "KOSPI" },
+ { code: "247540", name: "에코프로비엠", market: "KOSDAQ" },
+ { code: "042700", name: "한미반도체", market: "KOSPI" },
+ { code: "034020", name: "두산에너빌리티", market: "KOSPI" },
+ { code: "012450", name: "한화에어로스페이스", market: "KOSPI" },
+ { code: "329180", name: "HD현대중공업", market: "KOSPI" },
+ { code: "035420", name: "NAVER", market: "KOSPI" },
+ { code: "033780", name: "KT&G", market: "KOSPI" },
+ { code: "036460", name: "한국가스공사", market: "KOSPI" },
+];
+
+export function SeedTiles() {
+ const [tiles, setTiles] = useState(
+ SEED_TICKERS.map((t) => ({ ...t, current: null, prev: null })),
+ );
+
+ useEffect(() => {
+ let alive = true;
+ // 10 병렬 fetch. 한 종목 실패해도 다른 타일은 살린다.
+ Promise.allSettled(
+ SEED_TICKERS.map(async (t) => {
+ const c = await api.getChart(t.code, 5, "1d");
+ const valid = c.ohlcv.filter((p) => p.close != null);
+ const last = valid[valid.length - 1] ?? null;
+ const prev = valid.length >= 2 ? valid[valid.length - 2] : null;
+ return {
+ code: t.code,
+ name: t.name,
+ market: t.market,
+ current: last?.close ?? null,
+ prev: prev?.close ?? null,
+ };
+ }),
+ ).then((settled) => {
+ if (!alive) return;
+ const next: Tile[] = settled.map((r, i) => {
+ if (r.status === "fulfilled") return r.value;
+ return { ...SEED_TICKERS[i], current: null, prev: null };
+ });
+ setTiles(next);
+ });
+ return () => {
+ alive = false;
+ };
+ }, []);
+
+ return (
+
+
+
학습 대상 10종목
+
현재가 · 전일대비
+
+
+ {tiles.map((t) => (
+
+ ))}
+
+
+ );
+}
+
+function TileCard({ t }: { t: Tile }) {
+ const change =
+ t.current != null && t.prev != null ? t.current - t.prev : null;
+ const pct =
+ change != null && t.prev != null && t.prev !== 0
+ ? (change / t.prev) * 100
+ : null;
+ const tone =
+ change == null || change === 0
+ ? "text-zinc-400"
+ : change > 0
+ ? "text-rose-400"
+ : "text-sky-400";
+ return (
+
+ {t.name}
+
+ {t.code} · {t.market}
+
+
+ {t.current != null
+ ? t.current.toLocaleString(undefined, { maximumFractionDigits: 0 })
+ : "—"}
+
+
+ {change == null
+ ? " "
+ : `${change > 0 ? "+" : ""}${change.toLocaleString(undefined, {
+ maximumFractionDigits: 0,
+ })}${pct != null ? ` (${pct > 0 ? "+" : ""}${pct.toFixed(2)}%)` : ""}`}
+
+
+ );
+}