- New ShareButton in the page header tries navigator.share() first
(native sheet on mobile), then falls back to navigator.clipboard, then
to a hidden textarea + execCommand("copy") for non-secure-context LAN.
Shows a brief "링크 복사됨" / "복사 안됨" toast; native share success
shows none (the OS sheet is the feedback).
- Mounted between ⇄ 비교 and the Star button.
Fixes reviewer concern on 442cc38: writeNote() used to return null for
both "user emptied the textarea (intentional delete)" and "localStorage
quota failure," so clearing a note showed "저장 안됨." It now returns a
discriminated { status: "saved" | "deleted" | "failed" } and the panel
shows "저장됨" / "삭제됨" / "저장 안됨" accordingly. Failure keeps the
user's text so they can retry or shorten it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
// 종목별 사용자 메모 (브라우저 한정 — 비로그인 앱).
|
|
//
|
|
// 키: `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 type WriteResult =
|
|
| { status: "saved"; note: Note }
|
|
| { status: "deleted" }
|
|
| { status: "failed" };
|
|
|
|
export function writeNote(code: string, text: string): WriteResult {
|
|
if (typeof window === "undefined") return { status: "failed" };
|
|
const trimmed = text.slice(0, MAX_LEN);
|
|
// 공백만이면 키 자체 제거 → 빈 잔여물 방지. 의도된 삭제이므로 failed 와 구분.
|
|
if (!trimmed.trim()) {
|
|
clearNote(code);
|
|
return { status: "deleted" };
|
|
}
|
|
const next: Note = { text: trimmed, updatedAt: new Date().toISOString() };
|
|
try {
|
|
window.localStorage.setItem(`${PREFIX}${code}`, JSON.stringify(next));
|
|
return { status: "saved", note: next };
|
|
} catch {
|
|
return { status: "failed" }; // 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;
|