diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 2a89d00..adcc45e 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,5 +1,6 @@ import "./globals.css"; import type { Metadata } from "next"; +import { TopNav } from "../components/TopNav"; export const metadata: Metadata = { title: "Stock Chart Site", @@ -9,7 +10,10 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + + {children} + ); } diff --git a/web/components/TopNav.tsx b/web/components/TopNav.tsx new file mode 100644 index 0000000..2ec9370 --- /dev/null +++ b/web/components/TopNav.tsx @@ -0,0 +1,126 @@ +"use client"; + +// 글로벌 sticky 헤더 — 모든 페이지 상단에 고정. 토스 톱바 톤. +// 좌: 로고/타이틀, 중: 컴팩트 검색 (debounced + popover), 우: 관심종목 링크. +// +// 홈의 큰 SearchBox 와 별개로 동작. 결과 클릭 시 popover 가 닫히고 페이지 이동. +// Esc 또는 바깥 클릭으로 popover 닫힘. + +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { api, type Symbol } from "../lib/api"; + +export function TopNav() { + const [q, setQ] = useState(""); + const [items, setItems] = useState([]); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const wrapRef = useRef(null); + + // 디바운스 검색. 입력 비면 결과 클리어. + useEffect(() => { + const term = q.trim(); + if (!term) { + setItems([]); + return; + } + setLoading(true); + const h = setTimeout(async () => { + try { + const r = await api.search(term, false, 8, false); + setItems(r.items); + } catch { + setItems([]); + } finally { + setLoading(false); + } + }, 200); + return () => clearTimeout(h); + }, [q]); + + // 바깥 클릭으로 popover 닫기. + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (!wrapRef.current) return; + if (!wrapRef.current.contains(e.target as Node)) setOpen(false); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false); + }; + document.addEventListener("mousedown", onDoc); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onDoc); + document.removeEventListener("keydown", onKey); + }; + }, []); + + return ( +
+
+ + Stock Chart + + +
+ { + setQ(e.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + placeholder="종목 검색" + className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-xs text-zinc-100 outline-none focus:border-zinc-600" + /> + {open && q.trim() && ( +
+ {loading && ( +
검색중…
+ )} + {!loading && items.length === 0 && ( +
결과 없음
+ )} +
    + {items.map((it) => ( +
  • + { + setOpen(false); + setQ(""); + }} + className="flex items-center justify-between gap-2 px-3 py-2 text-xs hover:bg-zinc-800/60" + > + + {it.name} + {it.is_seed && ( + + SEED + + )} + + + {it.code} · {it.market} + + +
  • + ))} +
+
+ )} +
+ + + ★ 관심 + +
+
+ ); +}