Files
claude-owner 1e0c917f68 feat(chart): CSV export of current chart range + fix ShareButton comment
- New lib/csv.ts: RFC 4180 escaping, UTF-8 BOM prefix (한글 Excel 호환),
  Blob + temporary anchor download, filename
  `<name>_<code>_<interval>_<from>_<to>.csv` with filesystem-illegal
  characters replaced by `_`.
- StockChart toolbar now always renders (was hidden in 10m intraday mode
  because it gated on showSma) and includes a right-aligned `⬇ CSV`
  button next to the SMA/RSI/MACD chips. Button is disabled when ohlcv
  is empty.

Also addresses the reviewer's non-blocking note on f18493f:
ShareButton.tsx header comment claimed clipboard-first, but the code
calls navigator.share() first. Comment now matches the actual order:
share → clipboard → execCommand → "복사 안됨".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:11:57 +09:00

70 lines
2.1 KiB
TypeScript

// OHLCV → CSV 변환 + 다운로드 트리거.
// 차트 데이터를 분석가 친화 CSV (UTF-8 BOM, Excel 호환) 로 내려받게 해줌.
import type { ChartInterval, OhlcvPoint } from "./api";
function escapeField(v: string): string {
// 표준 RFC 4180 — 쉼표/큰따옴표/개행 포함 시 큰따옴표로 감싸고 내부 큰따옴표는 이중화.
if (v.includes(",") || v.includes('"') || v.includes("\n")) {
return `"${v.replace(/"/g, '""')}"`;
}
return v;
}
function num(n: number | null | undefined): string {
if (n == null || !Number.isFinite(n)) return "";
return String(n);
}
export function ohlcvToCsv(ohlcv: OhlcvPoint[]): string {
const header = ["date", "open", "high", "low", "close", "volume"];
const lines: string[] = [header.join(",")];
for (const p of ohlcv) {
lines.push(
[
escapeField(p.date),
num(p.open),
num(p.high),
num(p.low),
num(p.close),
num(p.volume),
].join(","),
);
}
return lines.join("\n");
}
export function downloadCsv(filename: string, csv: string): boolean {
if (typeof window === "undefined" || typeof document === "undefined") return false;
// Excel BOM — 한글 Excel 이 UTF-8 로 추론하도록.
const BOM = "\uFEFF";
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8" });
const url = URL.createObjectURL(blob);
try {
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
return true;
} catch {
return false;
} finally {
setTimeout(() => URL.revokeObjectURL(url), 0);
}
}
// `삼성전자_005930_1d_2025-08-01_2025-11-28.csv` — 파일시스템 금지 문자만 `_` 치환.
export function csvFilename(
name: string,
code: string,
interval: ChartInterval,
from: string,
to: string,
): string {
const safe = (s: string) => s.replace(/[\\/:*?"<>|]/g, "_").trim() || "data";
return `${safe(name)}_${code}_${interval}_${from}_${to}.csv`;
}