feat(targets): price target/stop simulator with chart overlay lines
- New lib/targets.ts: per-code localStorage (key `targets:<code>`) with read/write/clear; rejects non-positive/non-finite values. - New PriceTargets sidebar panel: 목표가/손절가 inputs, live %-from-current preview (rose=up, sky=down), save/clear actions. - StockChart accepts optional `targets` prop and draws dashed horizontal price lines on the candle series (rose=target, sky=stop) with axis labels. - Page loads saved targets on mount/code-change and re-renders chart on save. Also fixes view-counter dedupe caveat from reviewer: split into wasRecordedToday() + markRecorded(); the page now marks localStorage only after POST /api/views succeeds, so a failed POST stays retryable on the next visit instead of being silently swallowed for the day. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { PeriodReturns } from "../../components/PeriodReturns";
|
||||
import { PeriodTabs, periodSpec, type Period } from "../../components/PeriodTabs";
|
||||
import { PredictionPanel } from "../../components/PredictionPanel";
|
||||
import { PriceHero } from "../../components/PriceHero";
|
||||
import { PriceTargets } from "../../components/PriceTargets";
|
||||
import { RelatedStocks } from "../../components/RelatedStocks";
|
||||
import { StarButton } from "../../components/StarButton";
|
||||
import { StockChart } from "../../components/StockChart";
|
||||
@@ -21,7 +22,8 @@ import { SymbolSidebar } from "../../components/SymbolSidebar";
|
||||
import { TradingValuePanel } from "../../components/TradingValuePanel";
|
||||
import { api, type ChartPayload, type LatestPredictionResponse } from "../../lib/api";
|
||||
import { recent } from "../../lib/recent";
|
||||
import { shouldRecordView } from "../../lib/views";
|
||||
import { readTargets, type Targets } from "../../lib/targets";
|
||||
import { markRecorded, wasRecordedToday } from "../../lib/views";
|
||||
|
||||
export default function CodePage({ params }: { params: { code: string } }) {
|
||||
const { code } = params;
|
||||
@@ -30,6 +32,7 @@ export default function CodePage({ params }: { params: { code: string } }) {
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const [period, setPeriod] = useState<Period>("3M");
|
||||
const [viewsToday, setViewsToday] = useState<number | null>(null);
|
||||
const [targets, setTargets] = useState<Targets>({});
|
||||
|
||||
const spec = periodSpec(period);
|
||||
const isIntraday = spec.interval === "10m";
|
||||
@@ -49,11 +52,17 @@ export default function CodePage({ params }: { params: { code: string } }) {
|
||||
const apply = (n: number) => {
|
||||
if (alive) setViewsToday(n);
|
||||
};
|
||||
if (shouldRecordView(code)) {
|
||||
api.recordView(code).then((r) => apply(r.today_views)).catch(() => {
|
||||
// POST 실패 시 GET 한 번 시도 (혹시 다른 사용자 카운트라도 보여줌).
|
||||
api.views(code).then((r) => apply(r.today_views)).catch(() => {});
|
||||
});
|
||||
if (!wasRecordedToday(code)) {
|
||||
api.recordView(code)
|
||||
.then((r) => {
|
||||
// POST 성공 후에만 mark — 실패 시 다음 방문에 다시 시도 가능.
|
||||
markRecorded(code);
|
||||
apply(r.today_views);
|
||||
})
|
||||
.catch(() => {
|
||||
// POST 실패 시 GET 한 번 시도 (혹시 다른 사용자 카운트라도 보여줌).
|
||||
api.views(code).then((r) => apply(r.today_views)).catch(() => {});
|
||||
});
|
||||
} else {
|
||||
api.views(code).then((r) => apply(r.today_views)).catch(() => {});
|
||||
}
|
||||
@@ -62,6 +71,11 @@ export default function CodePage({ params }: { params: { code: string } }) {
|
||||
};
|
||||
}, [code]);
|
||||
|
||||
// 저장된 목표가/손절가 — 마운트 + 종목 전환 시 localStorage 에서 로드.
|
||||
useEffect(() => {
|
||||
setTargets(readTargets(code));
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setErr(null);
|
||||
@@ -169,7 +183,7 @@ export default function CodePage({ params }: { params: { code: string } }) {
|
||||
/>
|
||||
<div className="grid gap-6 lg:grid-cols-[1fr_280px]">
|
||||
<div>
|
||||
<StockChart chart={chart} prediction={prediction} />
|
||||
<StockChart chart={chart} prediction={prediction} targets={targets} />
|
||||
{isIntraday && chart.intraday_status && (
|
||||
<div className="mt-2 text-right text-[11px] text-zinc-500">
|
||||
실시간 10분봉 · 60초 갱신 · [{chart.intraday_status}]
|
||||
@@ -182,6 +196,7 @@ export default function CodePage({ params }: { params: { code: string } }) {
|
||||
<div className="space-y-4">
|
||||
<SymbolSidebar code={code} />
|
||||
<OrderbookPanel code={code} />
|
||||
<PriceTargets code={code} current={current} onChange={setTargets} />
|
||||
<AlertsPanel code={code} name={chart?.name} current={current} />
|
||||
<RelatedStocks code={code} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user