// 가격 알람 — localStorage 기반. 단순 임계값 알람. // // 스키마 (Alert): // id 랜덤 문자열 // code 종목코드 // name 등록 시점의 종목명 (표시용) // op "gte" = 현재가 ≥ target, "lte" = 현재가 ≤ target // target 임계값 (원) // triggered 한 번 발사되면 true. 사용자가 명시적으로 다시 켜기 전까지 침묵. // createdAt epoch ms // // 발사는 AlertsToaster (글로벌 폴러) 가 처리. 이 모듈은 저장소+pub/sub 만 담당. const KEY = "stockchart.alerts"; export type AlertOp = "gte" | "lte"; export type Alert = { id: string; code: string; name?: string; op: AlertOp; target: number; triggered: boolean; createdAt: number; }; function read(): Alert[] { if (typeof window === "undefined") return []; try { const raw = window.localStorage.getItem(KEY); if (!raw) return []; const v = JSON.parse(raw); if (!Array.isArray(v)) return []; return v.filter( (x): x is Alert => x && typeof x.id === "string" && typeof x.code === "string" && (x.op === "gte" || x.op === "lte") && typeof x.target === "number" && typeof x.triggered === "boolean", ); } catch { return []; } } function write(items: Alert[]) { if (typeof window === "undefined") return; window.localStorage.setItem(KEY, JSON.stringify(items)); window.dispatchEvent(new CustomEvent("alerts:change")); } function genId(): string { return Math.random().toString(36).slice(2, 10) + Date.now().toString(36); } export const alerts = { list(): Alert[] { return read(); }, listForCode(code: string): Alert[] { return read().filter((a) => a.code === code); }, add(input: Omit & { triggered?: boolean }) { const cur = read(); cur.unshift({ id: genId(), triggered: input.triggered ?? false, createdAt: Date.now(), code: input.code, name: input.name, op: input.op, target: input.target, }); write(cur); }, remove(id: string) { write(read().filter((a) => a.id !== id)); }, markTriggered(id: string) { write(read().map((a) => (a.id === id ? { ...a, triggered: true } : a))); }, reset(id: string) { write(read().map((a) => (a.id === id ? { ...a, triggered: false } : a))); }, clear() { write([]); }, subscribe(cb: () => void): () => void { if (typeof window === "undefined") return () => {}; const onStorage = (e: StorageEvent) => { if (e.key === KEY) cb(); }; const onCustom = () => cb(); window.addEventListener("storage", onStorage); window.addEventListener("alerts:change", onCustom as EventListener); return () => { window.removeEventListener("storage", onStorage); window.removeEventListener("alerts:change", onCustom as EventListener); }; }, };