Files
stock_chart_site/web/app/[code]/page.tsx
claude-owner 0a5c634680 feat(chart): 10m 실시간 / 일·주·월 토글 / 오늘 마커 / 예측 거래일 선택
- backend/app/api/chart.py: interval=10m|1d|1w|1mo. 10m 은 ohlcv_1m 을
  time_bucket(10min) 으로 집계, stale(>10분) 이면 KIS 분봉 fetch 후 재조회.
  1w/1mo 는 ohlcv_daily 를 date_trunc 로 집계. today 필드 추가.
- backend/app/fetch/kis.py: fetch_minute_price() 추가 (tr_id FHKST03010200).
  KIS 응답 KST 시각을 tz-aware datetime 으로 변환, 오름차순 정렬.
- web/lib/api.ts: ChartInterval 타입, getChart(interval), predict(horizons[]).
- web/components/StockChart.tsx: 10m 이면 timeVisible. 일·주·월에서 오늘
  화살표 마커 표시. ISO datetime 도 파싱.
- web/components/PredictionPanel.tsx: 단기/중기/장기 프리셋 + 사용자 직접
  지정 (예: 1,2,3,7). API 에 horizons 배열 전달.
- web/app/[code]/page.tsx: interval 칩 (10분/일/주/월). 10m 일 때 60초마다
  폴링. interval 별 기본 lookback (10m=1, 1d=180, 1w=730, 1mo=1825).
2026-05-23 01:34:29 +09:00

159 lines
5.1 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { MetricsPanel } from "../../components/MetricsPanel";
import { NewsList } from "../../components/NewsList";
import { PredictionPanel } from "../../components/PredictionPanel";
import { StockChart } from "../../components/StockChart";
import {
api,
type ChartInterval,
type ChartPayload,
type LatestPredictionResponse,
} from "../../lib/api";
const INTERVALS: { label: string; value: ChartInterval; defaultDays: number }[] = [
{ label: "10분", value: "10m", defaultDays: 1 },
{ label: "일", value: "1d", defaultDays: 180 },
{ label: "주", value: "1w", defaultDays: 365 * 2 },
{ label: "월", value: "1mo", defaultDays: 365 * 5 },
];
export default function CodePage({ params }: { params: { code: string } }) {
const { code } = params;
const [chart, setChart] = useState<ChartPayload | null>(null);
const [prediction, setPrediction] = useState<LatestPredictionResponse | null>(null);
const [err, setErr] = useState<string | null>(null);
const [interval, setIntervalKind] = useState<ChartInterval>("1d");
const [days, setDays] = useState(180);
// interval 바꾸면 days 도 그 interval 에 맞는 기본값으로 (사용자가 명시적으로 다시 고를 수 있게).
function pickInterval(v: ChartInterval) {
const meta = INTERVALS.find((i) => i.value === v)!;
setIntervalKind(v);
setDays(meta.defaultDays);
}
// 초기/주기적 차트 로드. 10분봉이면 60초마다 폴링 — 백엔드가 캐시-then-fetch 로
// 10분 이내면 DB 만 읽고, 넘었으면 KIS 호출. 폴링 부담은 낮음.
useEffect(() => {
let alive = true;
setErr(null);
setChart(null);
const load = () => {
api
.getChart(code, days, interval)
.then((c) => {
if (alive) setChart(c);
})
.catch((e) => {
if (alive) setErr(e instanceof Error ? e.message : String(e));
});
};
load();
if (interval === "10m") {
const h = window.setInterval(load, 60_000);
return () => {
alive = false;
window.clearInterval(h);
};
}
return () => {
alive = false;
};
}, [code, days, interval]);
useEffect(() => {
let alive = true;
api
.latestPrediction(code)
.then((r) => {
if (alive && r.found) setPrediction(r);
})
.catch(() => {
// 예측 이력 없는 경우는 무시.
});
return () => {
alive = false;
};
}, [code]);
return (
<main className="mx-auto max-w-5xl px-6 py-10">
<div className="mb-4 flex items-center justify-between">
<Link href="/" className="text-xs text-zinc-500 hover:text-zinc-300">
</Link>
<div className="flex items-center gap-2">
<div className="flex overflow-hidden rounded-md border border-zinc-700 text-xs">
{INTERVALS.map((it) => (
<button
key={it.value}
onClick={() => pickInterval(it.value)}
className={
interval === it.value
? "bg-emerald-700 px-3 py-1 text-white"
: "bg-zinc-900 px-3 py-1 text-zinc-300 hover:bg-zinc-800"
}
>
{it.label}
</button>
))}
</div>
{interval !== "10m" && (
<select
value={days}
onChange={(e) => setDays(Number(e.target.value))}
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs"
>
<option value={60}> 3</option>
<option value={180}> 6</option>
<option value={365}> 1</option>
<option value={365 * 2}> 2</option>
<option value={365 * 5}> 5</option>
<option value={365 * 10}> 10</option>
</select>
)}
</div>
</div>
{chart && (
<div className="mb-4 flex items-baseline justify-between">
<h1 className="text-2xl font-semibold text-zinc-100">
{chart.name}{" "}
<span className="text-sm font-normal text-zinc-500">
{chart.code} · {chart.market}
</span>
</h1>
{interval === "10m" && (
<div className="text-xs text-zinc-500">
10 · 60
{chart.intraday_status && (
<span className="ml-2 text-zinc-600">[{chart.intraday_status}]</span>
)}
</div>
)}
</div>
)}
{err && <div className="mb-4 text-sm text-red-400"> : {err}</div>}
{chart && (
<>
<StockChart chart={chart} prediction={prediction} />
<div className="mt-6">
<PredictionPanel code={code} initial={prediction} onResult={setPrediction} />
</div>
<div className="mt-6 grid gap-6 md:grid-cols-2">
<MetricsPanel code={code} />
<NewsList code={code} />
</div>
</>
)}
</main>
);
}