Files
stock_chart_site/web/components/RelatedStocks.tsx
claude-owner d53ff7a745 feat(sidebar): RelatedStocks defaults collapsed + readOpen garbage-value fallback
readOpen() (reviewer f9027f5 non-blocking note):
- Previously returned v === '1' which collapsed '0' AND any garbage
  value (legacy markers, manual edits) into the same false branch,
  contradicting the 'fallback to default' comment. Split into three
  explicit cases: '1' → true, '0' → false, anything else →
  defaultOpen. Behavior unchanged for the writeOpen-only path; only
  garbage input is corrected.

RelatedStocks defaultOpen=false:
- Of the three collapsible sidebar panels (AlertsPanel,
  InvestmentNote, RelatedStocks), RelatedStocks has the lowest
  information density per row and the weakest direct tie to 'this
  symbol' (it's other symbols in the same market). Starting collapsed
  cuts initial mobile scroll height for first-time visitors; once the
  user toggles, localStorage pins their preference.
2026-06-01 10:31:48 +09:00

96 lines
3.3 KiB
TypeScript

"use client";
// 같은 시장 내 거래량/유동성 기준 관련 종목 8개.
// /api/markets/related/{code} 응답을 좁은 list 카드로 렌더.
// Collapsible 로 감싸 — 모바일에서 사이드바 길이가 부담되면 사용자가 접어 둠.
import Link from "next/link";
import { useEffect, useState } from "react";
import { api, type RelatedResponse } from "../lib/api";
import { Collapsible } from "./Collapsible";
export function RelatedStocks({ code }: { code: string }) {
const [data, setData] = useState<RelatedResponse | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setErr(null);
setData(null);
api
.related(code, 8)
.then((r) => {
if (alive) setData(r);
})
.catch((e) => {
if (alive) setErr(e instanceof Error ? e.message : String(e));
});
return () => {
alive = false;
};
}, [code]);
const subtitle = data?.market ? `${data.market} · 거래량 기준` : undefined;
return (
// defaultOpen=false — 사이드바의 세 collapsible 중 정보 밀도가 가장 낮고 "이 종목 자체"
// 와의 직접 연관도 약함(같은 시장의 다른 종목 리스트). 진입 즉시 접혀 있다가 사용자가
// 필요할 때 펴는 게 자연스러움. 첫 토글 이후엔 localStorage 가 사용자 선호로 굳음.
<Collapsible
title="관련 종목"
subtitle={subtitle}
storageKey="related"
defaultOpen={false}
>
{err && (
<div className="text-xs text-zinc-500"> : {err}</div>
)}
{!err && !data && (
<div className="text-xs text-zinc-500"> </div>
)}
{!err && data && !data.items.length && (
<div className="text-xs text-zinc-500">
({data.market}) .
</div>
)}
{!err && data && data.items.length > 0 && (
<ul className="divide-y divide-zinc-800">
{data.items.map((m) => {
const pct = m.pct_change;
const tone =
pct == null || pct === 0
? "text-zinc-400"
: pct > 0
? "text-rose-400"
: "text-sky-400";
return (
<li key={m.code}>
<Link
href={`/${m.code}`}
className="flex items-center justify-between gap-2 py-1.5 text-xs hover:bg-zinc-800/40"
>
<span className="min-w-0 truncate text-zinc-100">{m.name}</span>
<span className="shrink-0 text-right tabular-nums">
<span className="text-zinc-100">
{m.close != null
? m.close.toLocaleString(undefined, {
maximumFractionDigits: 0,
})
: "—"}
</span>
<span className={`ml-2 ${tone}`}>
{pct != null
? `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`
: ""}
</span>
</span>
</Link>
</li>
);
})}
</ul>
)}
</Collapsible>
);
}