"use client"; import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { fetchPostcode, fetchPostcodeSuggestions, parsePostcodeInput, type AddrItem, type PostcodeSearchResult, } from "@/app/store/sdmMng/postcode-service"; export type { AddrItem } from "@/app/store/sdmMng/postcode-service"; type Props = { open: boolean; onClose: () => void; onSelect: (item: AddrItem) => void; }; 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; } export default function PostcodeModal({ open, onClose, onSelect }: Props) { const [mounted, setMounted] = useState(false); const [q, setQ] = useState(""); const dq = useDebouncedValue(q, 250); const [items, setItems] = useState([]); const [suggestions, setSuggestions] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [searchMessage, setSearchMessage] = useState(""); const [isInputFocused, setIsInputFocused] = useState(false); const inputRef = useRef(null); useEffect(() => setMounted(true), []); useEffect(() => { if (open) return; setQ(""); setItems([]); setSuggestions([]); setTotal(0); setLoading(false); setSearchMessage(""); setIsInputFocused(false); }, [open]); useEffect(() => { if (!open) return; const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; setTimeout(() => inputRef.current?.focus(), 50); // 주소입력란에 focus() 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([]); return; } let alive = true; (async () => { try { const nextSuggestions = await fetchPostcodeSuggestions(keyword); if (!alive) return; setSuggestions(nextSuggestions); } catch { if (!alive) return; setSuggestions([]); } })(); return () => { alive = false; }; }, [dq, open]); useEffect(() => { if (!open) return; const keyword = dq.trim(); if (!keyword) { setItems([]); setTotal(0); setLoading(false); setSearchMessage(""); return; } const parsed = parsePostcodeInput(keyword, 1); if (!parsed.ok) { setItems([]); setTotal(0); setLoading(false); setSearchMessage(parsed.message); return; } let alive = true; (async () => { setLoading(true); setSearchMessage(""); try { const result: PostcodeSearchResult = await fetchPostcode(parsed.value); if (!alive) return; setItems(result.items); setTotal(result.totalCount); } catch { if (!alive) return; setItems([]); setTotal(0); setSearchMessage("우편번호 검색 중 오류가 발생했습니다."); } finally { if (alive) { setLoading(false); setSuggestions([]); } } })(); return () => { alive = false; }; }, [dq, open]); const showSuggest = open && isInputFocused && q.trim().length >= 1 && suggestions.length > 0; if (!mounted || !open) return null; return createPortal(
setQ(e.target.value)} onFocus={() => setIsInputFocused(true)} onBlur={() => { setTimeout(() => setIsInputFocused(false), 100); }} placeholder="예) 테헤란로 152-1" 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) => ( ))}
)}
{q.trim() ? loading ? "검색 중..." : searchMessage || `총 ${total.toLocaleString()}개의 검색결과` : "도로명과 건물번호를 입력하세요. 예) 테헤란로 152-1"}
{!loading && q.trim() && items.length === 0 && !searchMessage && (
검색 결과가 없습니다.
)}
{items.map((it) => ( ))}
, document.body, ); }