feat(chart): horizontal line drawing tool + modal-open shortcut guards
리뷰어 093ac86 지적 처리:
- 종목 페이지 단축키 핸들러 (←/→/S) 에 modal 가드 추가.
`document.querySelector('[role="dialog"][aria-modal="true"]')` 가 매치되면
return — ShortcutsHelp 같은 모달이 열린 상태에서 close 버튼이 포커스를
잡고 있어도 페이지 단축키가 뒤에서 동작하지 않도록.
- StockChart 의 F 단축키에도 동일한 modal 가드 — 동일 문제 (모달 위에서
F 가 차트 전체화면 토글) 차단.
신규 슬라이스 — 차트 수평선 그리기 도구:
- `web/lib/lines.ts` — `hlines:<code>` 키 localStorage. id 단위 add/remove/clear,
최대 10 개, 같은 가격 중복 추가 방지(부동소수 4 자리 비교), crypto.randomUUID
폴백 포함.
- StockChart 툴바: `─ 수평선 (N/10)` 버튼 — hover 가격 또는 마지막 봉 종가에
추가. 한도 도달 / 가격 없음이면 disabled.
- 추가된 라인은 chips 형태로 toolbar 아래 한 줄에 표시 — 각각 ✕ 로 제거,
`전체 해제` 버튼 별도.
- H 키 단축키 — modifier/editable/modal 가드 일관 적용. 가격은 ref 로 들고
있어 keydown effect 가 재구독되지 않음.
- 차트엔 `createPriceLine` 으로 zinc-400 solid 라인 (목표/손절의 점선과 톤
분리). 종목 전환 시 자동 reload.
- ShortcutsHelp 에 H 단축키 등록.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
104
web/lib/lines.ts
Normal file
104
web/lib/lines.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// 종목별 사용자 수평선(horizontal price lines) 저장소.
|
||||
// TradingView/Toss 식 그리기 도구의 가장 가벼운 형태 — 가격 라인만, per-code, localStorage.
|
||||
// 차트 자체는 라인 ID 를 모르고 좌표만 받음. 추가/삭제는 이 모듈을 통해서만.
|
||||
|
||||
const KEY = (code: string) => `hlines:${code}`;
|
||||
export const HLINE_MAX = 10;
|
||||
|
||||
export type HLine = {
|
||||
id: string;
|
||||
price: number;
|
||||
// 작성 시각 — 디버그/정렬용. UI 에선 노출 안 해도 됨.
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
function safeStorage(): Storage | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
return window.localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function readLines(code: string): HLine[] {
|
||||
const s = safeStorage();
|
||||
if (!s) return [];
|
||||
try {
|
||||
const raw = s.getItem(KEY(code));
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
const out: HLine[] = [];
|
||||
for (const item of parsed) {
|
||||
if (
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
typeof (item as HLine).id === "string" &&
|
||||
typeof (item as HLine).price === "number" &&
|
||||
Number.isFinite((item as HLine).price) &&
|
||||
(item as HLine).price > 0
|
||||
) {
|
||||
out.push({
|
||||
id: (item as HLine).id,
|
||||
price: (item as HLine).price,
|
||||
createdAt:
|
||||
typeof (item as HLine).createdAt === "string"
|
||||
? (item as HLine).createdAt
|
||||
: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return out.slice(0, HLINE_MAX);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeRaw(code: string, lines: HLine[]): boolean {
|
||||
const s = safeStorage();
|
||||
if (!s) return false;
|
||||
try {
|
||||
if (lines.length === 0) s.removeItem(KEY(code));
|
||||
else s.setItem(KEY(code), JSON.stringify(lines.slice(0, HLINE_MAX)));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// id 충돌 가능성이 거의 없는 short id. crypto.randomUUID 미지원 환경 폴백.
|
||||
function newId(): string {
|
||||
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
// price 같음 검사용 — 유효숫자 4 자리 정도 비교로 부동소수 오차 무시.
|
||||
function nearlyEqual(a: number, b: number): boolean {
|
||||
const denom = Math.max(Math.abs(a), Math.abs(b), 1);
|
||||
return Math.abs(a - b) / denom < 1e-4;
|
||||
}
|
||||
|
||||
export function addLine(code: string, price: number): HLine[] {
|
||||
if (!Number.isFinite(price) || price <= 0) return readLines(code);
|
||||
const cur = readLines(code);
|
||||
// 같은 가격이 이미 있으면 중복 추가 안 함 — 라인이 안 보일 뿐 헷갈림 유발.
|
||||
if (cur.some((l) => nearlyEqual(l.price, price))) return cur;
|
||||
const next = [...cur, { id: newId(), price, createdAt: new Date().toISOString() }];
|
||||
writeRaw(code, next);
|
||||
return next.slice(0, HLINE_MAX);
|
||||
}
|
||||
|
||||
export function removeLine(code: string, id: string): HLine[] {
|
||||
const cur = readLines(code);
|
||||
const next = cur.filter((l) => l.id !== id);
|
||||
writeRaw(code, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function clearLines(code: string): HLine[] {
|
||||
writeRaw(code, []);
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user