feat(targets): price target/stop simulator with chart overlay lines
- New lib/targets.ts: per-code localStorage (key `targets:<code>`) with read/write/clear; rejects non-positive/non-finite values. - New PriceTargets sidebar panel: 목표가/손절가 inputs, live %-from-current preview (rose=up, sky=down), save/clear actions. - StockChart accepts optional `targets` prop and draws dashed horizontal price lines on the candle series (rose=target, sky=stop) with axis labels. - Page loads saved targets on mount/code-change and re-renders chart on save. Also fixes view-counter dedupe caveat from reviewer: split into wasRecordedToday() + markRecorded(); the page now marks localStorage only after POST /api/views succeeds, so a failed POST stays retryable on the next visit instead of being silently swallowed for the day. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
49
web/lib/targets.ts
Normal file
49
web/lib/targets.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// 종목별 목표가/손절가 저장 (브라우저 한정 — 비로그인 앱).
|
||||
//
|
||||
// 키: `targets:<code>` → { target?: number, stop?: number, savedAt: ISO }
|
||||
// 차트 오버레이 라인과 손익 미리보기에 사용. 알림 없음 (그건 AlertsPanel 담당).
|
||||
|
||||
const PREFIX = "targets:";
|
||||
|
||||
export type Targets = {
|
||||
target?: number | null; // 익절가 (선택)
|
||||
stop?: number | null; // 손절가 (선택)
|
||||
savedAt?: string;
|
||||
};
|
||||
|
||||
export function readTargets(code: string): Targets {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem(`${PREFIX}${code}`);
|
||||
return raw ? (JSON.parse(raw) as Targets) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function writeTargets(code: string, t: Targets): void {
|
||||
if (typeof window === "undefined") return;
|
||||
// 둘 다 비어 있으면 키 자체를 지움 → 빈 객체 잔여물 제거.
|
||||
const cleaned: Targets = {};
|
||||
if (t.target != null && Number.isFinite(t.target)) cleaned.target = t.target;
|
||||
if (t.stop != null && Number.isFinite(t.stop)) cleaned.stop = t.stop;
|
||||
try {
|
||||
if (cleaned.target == null && cleaned.stop == null) {
|
||||
window.localStorage.removeItem(`${PREFIX}${code}`);
|
||||
return;
|
||||
}
|
||||
cleaned.savedAt = new Date().toISOString();
|
||||
window.localStorage.setItem(`${PREFIX}${code}`, JSON.stringify(cleaned));
|
||||
} catch {
|
||||
/* quota 초과 등 — 무시 */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearTargets(code: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.removeItem(`${PREFIX}${code}`);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user