"use client"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; export type AddrItem = { zipNo: string; // 우편번호 roadAddr: string; // 도로명주소(전체 문장) jibunAddr: string; // 지번주소 }; type Props = { open: boolean; onClose: () => void; onSelect: (item: AddrItem) => void; }; type PostcodeApiResponse = { totalCount?: number; items?: AddrItem[]; }; function useDebouncedValue(value: T, delay = 250) { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), delay); return () => clearTimeout(t); }, [value, delay]); return v; } // A: 추천은 roadAddr 전체 문장 const normalize = (s: string) => s.replace(/\s+/g, " ").trim(); const makeSuggestionsA = (list: AddrItem[], keyword: string, limit = 7) => { const key = normalize(keyword); const roadAddrs = list .map((x) => x.roadAddr) .filter((v): v is string => typeof v === "string" && v.trim().length > 0) .map(normalize); const uniq = Array.from(new Set(roadAddrs)); // 입력어 포함 우선 + 짧은 주소 우선 uniq.sort((a, b) => { const ai = a.includes(key) ? 0 : 1; const bi = b.includes(key) ? 0 : 1; if (ai !== bi) return ai - bi; return a.length - b.length; }); return uniq.slice(0, limit); }; export default function PostcodeModal({ open, onClose, onSelect }: Props) { const [mounted, setMounted] = useState(false); const [q, setQ] = useState(""); const dq = useDebouncedValue(q, 250); const [suggestions, setSuggestions] = useState([]); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const inputRef = useRef(null); useEffect(() => setMounted(true), []); // open 시: body scroll lock + focus + ESC useEffect(() => { if (!open) return; const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; setTimeout(() => inputRef.current?.focus(), 50); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKeyDown); return () => { document.body.style.overflow = prevOverflow; window.removeEventListener("keydown", onKeyDown); }; }, [open, onClose]); // 검색/추천/리스트 갱신 useEffect(() => { if (!open) return; const keyword = dq.trim(); if (!keyword) { setSuggestions([]); setItems([]); setTotal(0); setLoading(false); return; } let alive = true; (async () => { setLoading(true); try { const res = await fetch(`/api/postcode?query=${encodeURIComponent(keyword)}&page=1`, { cache: "no-store", }); const data = (await res.json()) as PostcodeApiResponse; if (!alive) return; const list: AddrItem[] = data.items ?? []; const totalCount: number = data.totalCount ?? 0; setItems(list); setTotal(totalCount); // ✅ A: roadAddr 전체 문장 추천 const sug = makeSuggestionsA(list, keyword, 7); setSuggestions(sug); } catch { if (!alive) return; setItems([]); setTotal(0); setSuggestions([]); } finally { if (alive) setLoading(false); } })(); return () => { alive = false; }; }, [dq, open]); const showSuggest = open && q.trim().length > 0 && suggestions.length > 0; if (!mounted || !open) return null; return createPortal(
{/* overlay */}
{/* search input */}
setQ(e.target.value)} placeholder="주소검색" className={[ "w-full rounded-md border px-4 py-3 pr-12 text-[14px] outline-none", "border-slate-300 focus:border-sky-500 focus:ring-2 focus:ring-sky-200", ].join(" ")} /> {/* suggestions dropdown */} {showSuggest && (
{suggestions.map((s) => ( ))}
)}
{/* results */}
{q.trim() ? loading ? "검색 중…" : `총 ${total.toLocaleString()}개의 검색결과` : "주소를 입력하면 검색결과가 표시됩니다."}
{!loading && q.trim() && items.length === 0 && (
검색 결과가 없습니다.
)}
{items.map((it) => ( ))}
, document.body ); }