// 종목 조회 카운터 클라이언트 dedupe. // // 서버는 받은 POST 횟수만큼 +1. 같은 사용자가 페이지를 새로고침할 때마다 카운트되면 // 인플레됨 → 클라이언트가 localStorage 에 (code, KST today) 마크를 남겨 하루 1회만 POST. // 새 탭/시크릿 모드는 새 사용자로 간주 (브라우저 한정 dedupe — 비로그인 앱이라 한계). const STORAGE_KEY = "views:logged"; type LoggedMap = Record; // code → "YYYY-MM-DD" function kstToday(): string { // KST = UTC+9. naive: Date 가 시스템 TZ 영향을 받으므로 UTC 기준 +9 로 직접 계산. const now = new Date(); const utcMs = now.getTime() + now.getTimezoneOffset() * 60_000; const kst = new Date(utcMs + 9 * 60 * 60_000); const y = kst.getFullYear(); const m = String(kst.getMonth() + 1).padStart(2, "0"); const d = String(kst.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function readLogged(): LoggedMap { if (typeof window === "undefined") return {}; try { const raw = window.localStorage.getItem(STORAGE_KEY); return raw ? (JSON.parse(raw) as LoggedMap) : {}; } catch { return {}; } } function writeLogged(m: LoggedMap): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(m)); } catch { /* quota 초과 등 — 무시. dedupe 실패 시 최악으로 한 사용자가 N번 카운트될 뿐. */ } } /** 오늘 이미 카운트된 종목인지. POST 호출 전 dedupe 게이트. */ export function wasRecordedToday(code: string): boolean { const today = kstToday(); const logged = readLogged(); return logged[code] === today; } /** POST 성공 이후 호출. 실패 시 마크하지 않아 다음 방문에 재시도 가능. */ export function markRecorded(code: string): void { const today = kstToday(); const logged = readLogged(); logged[code] = today; // 한도: 너무 커지면 오래된 항목 제거. 단순히 100개로 잘라냄. const entries = Object.entries(logged); if (entries.length > 100) { const trimmed: LoggedMap = Object.fromEntries(entries.slice(-100)); writeLogged(trimmed); } else { writeLogged(logged); } }