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}
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ ★ 관심
+
+
+
+ );
+}