feat(ui): 최근 본 종목 (localStorage + 홈 카드 + TopNav popover)

- web/lib/recent.ts: stockchart.recent KEY, push/get/clear/subscribe (storage + custom event)
- [code]/page.tsx: 페이지 진입 시 recent.push(code) — 자동 트래킹
- web/components/RecentTiles.tsx: 홈 카드 그리드 (최대 10개, 2~5컬럼)
- web/app/page.tsx: SearchBox 아래에 RecentTiles 배치 (빈 리스트면 자동 숨김)
- TopNav: 검색어 비면서 포커스 시 최근 본 종목 popover 표시 + "지우기" 버튼

토스 "최근 본 종목" 톤 — 검색 빈 상태에서도 popover 가 비어 있지 않게.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-owner
2026-05-28 15:14:40 +09:00
parent 300f7beef0
commit aa58761176
5 changed files with 229 additions and 0 deletions

View File

@@ -9,14 +9,23 @@
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { api, type Symbol } from "../lib/api";
import { recent } from "../lib/recent";
export function TopNav() {
const [q, setQ] = useState("");
const [items, setItems] = useState<Symbol[]>([]);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [recentCodes, setRecentCodes] = useState<string[]>([]);
const wrapRef = useRef<HTMLDivElement | null>(null);
// 최근 본 종목 구독 — popover 빈 상태에서 보여줌.
useEffect(() => {
const refresh = () => setRecentCodes(recent.get());
refresh();
return recent.subscribe(refresh);
}, []);
// 디바운스 검색. 입력 비면 결과 클리어.
useEffect(() => {
const term = q.trim();
@@ -112,6 +121,34 @@ export function TopNav() {
</ul>
</div>
)}
{open && !q.trim() && recentCodes.length > 0 && (
<div className="absolute left-0 right-0 top-full mt-1 max-h-80 overflow-auto rounded-md border border-zinc-800 bg-zinc-950 shadow-xl">
<div className="flex items-center justify-between px-3 py-1.5 text-[10px] text-zinc-500">
<span> </span>
<button
type="button"
onClick={() => recent.clear()}
className="hover:text-zinc-300"
>
</button>
</div>
<ul className="divide-y divide-zinc-800">
{recentCodes.map((c) => (
<li key={c}>
<Link
href={`/${c}`}
onClick={() => setOpen(false)}
className="block px-3 py-2 text-xs text-zinc-200 hover:bg-zinc-800/60"
>
{c}
</Link>
</li>
))}
</ul>
</div>
)}
</div>
<Link