feat(notes): per-code private investment note panel
- New lib/notes.ts: localStorage key `notes:<code>` storing { text, updatedAt },
2000-char cap, blank/whitespace removes the key.
- New InvestmentNote sidebar panel: textarea + character counter, save button
enabled only when dirty, "방금 전 / N분 전 / 오늘 HH:MM / YYYY-MM-DD"
relative timestamp on the last save, short save-confirmation toast.
- Mounted in [code]/page.tsx sidebar after AlertsPanel.
Also tightens writeTargets() to require strictly positive finite values,
matching the UI's parseNum() guard so the lib is safe under direct external
calls (reviewer's non-blocking note on a4a4a7f).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
53
web/lib/notes.ts
Normal file
53
web/lib/notes.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// 종목별 사용자 메모 (브라우저 한정 — 비로그인 앱).
|
||||
//
|
||||
// 키: `notes:<code>` → { text, updatedAt }
|
||||
// 투자 결정의 근거/관전 포인트를 짧게 적어두는 용도. 다른 사용자에게는 보이지 않음.
|
||||
|
||||
const PREFIX = "notes:";
|
||||
const MAX_LEN = 2000; // 너무 큰 입력 방어 — localStorage quota 보호.
|
||||
|
||||
export type Note = {
|
||||
text: string;
|
||||
updatedAt: string; // ISO
|
||||
};
|
||||
|
||||
export function readNote(code: string): Note | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(`${PREFIX}${code}`);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as Partial<Note>;
|
||||
if (typeof parsed.text !== "string" || typeof parsed.updatedAt !== "string") return null;
|
||||
return { text: parsed.text, updatedAt: parsed.updatedAt };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeNote(code: string, text: string): Note | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
const trimmed = text.slice(0, MAX_LEN);
|
||||
// 공백만이면 키 자체 제거 → 빈 잔여물 방지.
|
||||
if (!trimmed.trim()) {
|
||||
clearNote(code);
|
||||
return null;
|
||||
}
|
||||
const next: Note = { text: trimmed, updatedAt: new Date().toISOString() };
|
||||
try {
|
||||
window.localStorage.setItem(`${PREFIX}${code}`, JSON.stringify(next));
|
||||
return next;
|
||||
} catch {
|
||||
return null; // quota 등 — UI 가 저장 실패 처리
|
||||
}
|
||||
}
|
||||
|
||||
export function clearNote(code: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.removeItem(`${PREFIX}${code}`);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export const NOTE_MAX_LEN = MAX_LEN;
|
||||
@@ -24,9 +24,10 @@ export function readTargets(code: string): Targets {
|
||||
export function writeTargets(code: string, t: Targets): void {
|
||||
if (typeof window === "undefined") return;
|
||||
// 둘 다 비어 있으면 키 자체를 지움 → 빈 객체 잔여물 제거.
|
||||
// 양수 유한값만 통과. UI 가 이미 가드하지만 lib 자체도 방어 — 외부에서 직접 호출돼도 안전.
|
||||
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;
|
||||
if (t.target != null && Number.isFinite(t.target) && t.target > 0) cleaned.target = t.target;
|
||||
if (t.stop != null && Number.isFinite(t.stop) && t.stop > 0) cleaned.stop = t.stop;
|
||||
try {
|
||||
if (cleaned.target == null && cleaned.stop == null) {
|
||||
window.localStorage.removeItem(`${PREFIX}${code}`);
|
||||
|
||||
Reference in New Issue
Block a user