refactor(returns): extract shared ReturnBucket from Intraday/PeriodReturns

IntradayReturns(1D 10분봉 모드)와 PeriodReturns(일봉 이상)는 페이지의
같은 슬롯에서 모드 전환으로 갈아끼우는 한 쌍이다. 두 컴포넌트의 Bucket
서브컴포넌트는 className 문자열까지 완전히 동일했지만 각 파일에 별도
정의돼 있었다 — 한쪽 톤만 바꾸면 모드 전환 시 시각적 점프가 생기는
드리프트 리스크.

신규 web/components/ReturnBucket.tsx 로 단일 source of truth 분리:
- 색 규칙(rose/sky/zinc), 카드 톤(rounded-md border-zinc-800
  bg-zinc-900/30 px-2 py-2 text-center), 라벨/값 타이포 일괄 캡슐화.
- 두 패널은 각자 horizon 라벨/계산 로직만 유지하고 렌더는 ReturnBucket
  으로 위임.

기존 외부 동작/시각 결과 동일 — 동일한 className 을 한 곳으로 묶은
순수 리팩토링.

검증:
- npx tsc --noEmit clean
- npx next lint "✔ No ESLint warnings or errors"
This commit is contained in:
claude-owner
2026-06-01 10:47:44 +09:00
parent 3b06b95586
commit 232b94755a
3 changed files with 34 additions and 41 deletions

View File

@@ -13,6 +13,7 @@
// 추가 백엔드 호출 없음 — chart.ohlcv 슬라이싱만. // 추가 백엔드 호출 없음 — chart.ohlcv 슬라이싱만.
import type { OhlcvPoint } from "../lib/api"; import type { OhlcvPoint } from "../lib/api";
import { ReturnBucket } from "./ReturnBucket";
const BUCKETS: { label: string; bars: number }[] = [ const BUCKETS: { label: string; bars: number }[] = [
{ label: "10분", bars: 1 }, { label: "10분", bars: 1 },
@@ -55,27 +56,9 @@ export function IntradayReturns({ ohlcv }: { ohlcv: OhlcvPoint[] }) {
</div> </div>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-5"> <div className="grid grid-cols-3 gap-2 sm:grid-cols-5">
{buckets.map((b) => ( {buckets.map((b) => (
<Bucket key={b.label} label={b.label} pct={b.pct} /> <ReturnBucket key={b.label} label={b.label} pct={b.pct} />
))} ))}
<Bucket label="시가대비" pct={todayPct} /> <ReturnBucket label="시가대비" pct={todayPct} />
</div>
</div>
);
}
function Bucket({ label, pct }: { label: string; pct: number | null }) {
// 한국식 컬러 — 상승=rose, 하락=sky, 0/null=zinc.
const tone =
pct == null || pct === 0
? "text-zinc-400"
: pct > 0
? "text-rose-400"
: "text-sky-400";
return (
<div className="rounded-md border border-zinc-800 bg-zinc-900/30 px-2 py-2 text-center">
<div className="text-[11px] text-zinc-500">{label}</div>
<div className={`mt-1 text-sm font-medium tabular-nums ${tone}`}>
{pct == null ? "—" : `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`}
</div> </div>
</div> </div>
); );

View File

@@ -5,6 +5,7 @@
// 거래일 기준 근사: 1주≈5, 1개월≈21, 3개월≈63, 6개월≈126, 1년≈252. // 거래일 기준 근사: 1주≈5, 1개월≈21, 3개월≈63, 6개월≈126, 1년≈252.
import type { OhlcvPoint } from "../lib/api"; import type { OhlcvPoint } from "../lib/api";
import { ReturnBucket } from "./ReturnBucket";
const BUCKETS: { label: string; days: number }[] = [ const BUCKETS: { label: string; days: number }[] = [
{ label: "1주", days: 5 }, { label: "1주", days: 5 },
@@ -45,28 +46,9 @@ export function PeriodReturns({ ohlcv }: { ohlcv: OhlcvPoint[] }) {
</div> </div>
<div className="grid grid-cols-3 gap-2 sm:grid-cols-6"> <div className="grid grid-cols-3 gap-2 sm:grid-cols-6">
{buckets.map((b) => ( {buckets.map((b) => (
<Bucket key={b.label} label={b.label} pct={b.pct} /> <ReturnBucket key={b.label} label={b.label} pct={b.pct} />
))} ))}
<Bucket label="전체" pct={all} /> <ReturnBucket label="전체" pct={all} />
</div>
</div>
);
}
function Bucket({ label, pct }: { label: string; pct: number | null }) {
const tone =
pct == null || pct === 0
? "text-zinc-400"
: pct > 0
? "text-rose-400"
: "text-sky-400";
return (
<div className="rounded-md border border-zinc-800 bg-zinc-900/30 px-2 py-2 text-center">
<div className="text-[11px] text-zinc-500">{label}</div>
<div className={`mt-1 text-sm font-medium tabular-nums ${tone}`}>
{pct == null
? "—"
: `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`}
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,28 @@
// 수익률 미니 셀 — IntradayReturns(1D 10분봉 모드) 와 PeriodReturns(일봉 이상) 가 같은
// 슬롯에서 모드 전환으로 갈아끼우므로 두 패널의 Bucket 톤/색 규칙은 반드시 동일해야 한다.
// 예전엔 각 파일에 동일한 Bucket 이 중복 정의돼 있어 한쪽만 톤을 바꾸면 모드 전환 시
// 시각적 점프가 생길 위험이 있었다. 한 곳으로 묶어 단일 source of truth 로 둔다.
//
// 한국식 컬러 규칙: 상승 = rose, 하락 = sky, 0/null = zinc (Toss 톤).
type Props = {
label: string;
pct: number | null;
};
export function ReturnBucket({ label, pct }: Props) {
const tone =
pct == null || pct === 0
? "text-zinc-400"
: pct > 0
? "text-rose-400"
: "text-sky-400";
return (
<div className="rounded-md border border-zinc-800 bg-zinc-900/30 px-2 py-2 text-center">
<div className="text-[11px] text-zinc-500">{label}</div>
<div className={`mt-1 text-sm font-medium tabular-nums ${tone}`}>
{pct == null ? "—" : `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`}
</div>
</div>
);
}