"use client"; // 목표가 / 손절가 시뮬레이터 (Toss "내 목표가" read-only 클론). // - 현재가 기준 변동률 미리 계산해서 표시 // - 저장 시 localStorage 에만 — 차트 오버레이는 부모 page 가 같은 상태를 읽어 StockChart 에 전달 // - 알림은 별도 AlertsPanel 담당, 여기는 시각화 + 손익 프리뷰만 import { useEffect, useMemo, useState } from "react"; import { readTargets, writeTargets, type Targets } from "../lib/targets"; import { Collapsible } from "./Collapsible"; type Props = { code: string; current: number | null; // 저장/해제 시 부모에 변경을 전달 — 차트에 즉시 반영하도록. onChange?: (t: Targets) => void; }; function parseNum(s: string): number | null { const cleaned = s.replace(/,/g, "").trim(); if (!cleaned) return null; const n = Number(cleaned); return Number.isFinite(n) && n > 0 ? n : null; } function pct(price: number | null, current: number | null): number | null { if (price == null || current == null || current <= 0) return null; return ((price - current) / current) * 100; } export function PriceTargets({ code, current, onChange }: Props) { const [target, setTarget] = useState(""); const [stop, setStop] = useState(""); const [saved, setSaved] = useState({}); // 종목 바뀌면 새로 로드. 입력창도 저장값으로 초기화. useEffect(() => { const t = readTargets(code); setSaved(t); setTarget(t.target != null ? String(t.target) : ""); setStop(t.stop != null ? String(t.stop) : ""); }, [code]); const targetN = useMemo(() => parseNum(target), [target]); const stopN = useMemo(() => parseNum(stop), [stop]); const targetPct = pct(targetN, current); const stopPct = pct(stopN, current); const dirty = (targetN ?? null) !== (saved.target ?? null) || (stopN ?? null) !== (saved.stop ?? null); const save = () => { const next: Targets = { target: targetN, stop: stopN }; writeTargets(code, next); setSaved(next); onChange?.(next); }; const clear = () => { setTarget(""); setStop(""); const next: Targets = {}; writeTargets(code, next); setSaved(next); onChange?.(next); }; const fmtPct = (p: number | null): string => p == null ? "" : `${p >= 0 ? "+" : ""}${p.toFixed(2)}%`; const pctCls = (p: number | null): string => p == null ? "text-zinc-500" : p >= 0 ? "text-rose-400" : "text-sky-400"; // 접힌 상태에서도 유저가 어떤 값을 저장해뒀는지 한눈에 — 둘 다 없으면 InvestmentNote 와 // 같은 톤의 "비공개 · 브라우저 저장" 안내. 저장값(savedN)이 아닌 dirty 입력값을 보여주면 // 토글 사이클 사이에 안내가 깜빡여 혼란. saved 기준만 표시. const fmtNum = (n: number): string => n.toLocaleString(undefined, { maximumFractionDigits: 0 }); const subtitle = saved.target != null && saved.stop != null ? `목표 ${fmtNum(saved.target)} · 손절 ${fmtNum(saved.stop)}` : saved.target != null ? `목표 ${fmtNum(saved.target)}` : saved.stop != null ? `손절 ${fmtNum(saved.stop)}` : "비공개 · 브라우저 저장"; return ( // defaultOpen=true — 입력 중인 텍스트는 Collapsible 의 hidden 본문 덕에 토글로 사라지지 // 않지만, 처음 보는 사용자에겐 input 이 펴져 있어야 "여기서 목표가를 적는다" 가 자명.
{current == null && (
현재가 로딩 중 — 변동률은 잠시 후 표시
)}
); } function Row({ label, color, value, onChange, deltaText, deltaCls, }: { label: string; color: string; value: string; onChange: (s: string) => void; deltaText: string; deltaCls: string; }) { return ( ); }