dm_new 우편번호 검색
This commit is contained in:
+3
-1
@@ -25,4 +25,6 @@ apps/fo/.next/**/*
|
|||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
settings.json
|
settings.json
|
||||||
.npmrc
|
.npmrc
|
||||||
.cursor
|
.cursor
|
||||||
|
|
||||||
|
apps/**/tsconfig.tsbuildinfo
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import SubPageHeader from "@/components/SubPageHeader";
|
import SubPageHeader from "@/components/SubPageHeader";
|
||||||
|
import PostcodeModal from "@/components/PostcodeModal";
|
||||||
|
import type { AddrItem } from "@/app/store/sdmMng/postcode-service";
|
||||||
import { useDmStore } from "@/app/store";
|
import { useDmStore } from "@/app/store";
|
||||||
import { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api";
|
import { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api";
|
||||||
import { getCookie } from "@common/components/lib";
|
import { getCookie } from "@common/components/lib";
|
||||||
@@ -21,6 +23,12 @@ type SessionUser = {
|
|||||||
|
|
||||||
const TOKEN_ID_COOKIE = "token-id";
|
const TOKEN_ID_COOKIE = "token-id";
|
||||||
const FALLBACK_UUID = "c475824c-0e82-4a06-a282-2b55a20df265";
|
const FALLBACK_UUID = "c475824c-0e82-4a06-a282-2b55a20df265";
|
||||||
|
const normalizeRegionName = (value: string) => value.replace(/\s+/g, "").trim();
|
||||||
|
const isSameRegionName = (left: string, right: string) => {
|
||||||
|
const a = normalizeRegionName(left);
|
||||||
|
const b = normalizeRegionName(right);
|
||||||
|
return Boolean(a && b && (a === b || a.includes(b) || b.includes(a)));
|
||||||
|
};
|
||||||
|
|
||||||
function Divider() {
|
function Divider() {
|
||||||
return <div className="h-px w-full bg-neutral-200" />;
|
return <div className="h-px w-full bg-neutral-200" />;
|
||||||
@@ -129,6 +137,7 @@ export default function DemandPlaceRegisterPage() {
|
|||||||
const [zip, setZip] = useState("");
|
const [zip, setZip] = useState("");
|
||||||
const [addr1, setAddr1] = useState("");
|
const [addr1, setAddr1] = useState("");
|
||||||
const [addr2, setAddr2] = useState("");
|
const [addr2, setAddr2] = useState("");
|
||||||
|
const [isPostcodeModalOpen, setIsPostcodeModalOpen] = useState(false);
|
||||||
|
|
||||||
const [tel, setTel] = useState("");
|
const [tel, setTel] = useState("");
|
||||||
const [pw, setPw] = useState("");
|
const [pw, setPw] = useState("");
|
||||||
@@ -311,6 +320,29 @@ export default function DemandPlaceRegisterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectAddress = (item: AddrItem) => {
|
||||||
|
setZip(item.zipNo);
|
||||||
|
setAddr1(item.roadAddr);
|
||||||
|
const matchedSido = selectSidoData.find(
|
||||||
|
(region) =>
|
||||||
|
region.val !== "0" &&
|
||||||
|
isSameRegionName(region.text ?? "", item.ctpvNm ?? ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchedSido) {
|
||||||
|
setSido(matchedSido.val);
|
||||||
|
const matchedSigungu = selectSigunData.find(
|
||||||
|
(region) =>
|
||||||
|
region.val !== "0" &&
|
||||||
|
region.val.startsWith(matchedSido.val) &&
|
||||||
|
isSameRegionName(region.text ?? "", item.sggNm ?? ""),
|
||||||
|
);
|
||||||
|
setSigungu(matchedSigungu?.val ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPostcodeModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const pageTitle = editMode ? "수요처수정" : "수요처등록";
|
const pageTitle = editMode ? "수요처수정" : "수요처등록";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -414,7 +446,7 @@ export default function DemandPlaceRegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => alert("우편번호 검색(샘플)")}
|
onClick={() => setIsPostcodeModalOpen(true)}
|
||||||
className="h-11 rounded bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
className="h-11 rounded bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
||||||
>
|
>
|
||||||
우편번호 검색
|
우편번호 검색
|
||||||
@@ -478,6 +510,12 @@ export default function DemandPlaceRegisterPage() {
|
|||||||
{editMode ? "수요처 수정" : "수요처 등록"}
|
{editMode ? "수요처 수정" : "수요처 등록"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PostcodeModal
|
||||||
|
open={isPostcodeModalOpen}
|
||||||
|
onClose={() => setIsPostcodeModalOpen(false)}
|
||||||
|
onSelect={handleSelectAddress}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
export type AddrItem = {
|
||||||
|
zipNo: string;
|
||||||
|
roadAddr: string;
|
||||||
|
jibunAddr: string;
|
||||||
|
ctpvNm: string;
|
||||||
|
sggNm: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ZipSearchParams {
|
||||||
|
roadNm?: string;
|
||||||
|
bldgNoMno?: number | null;
|
||||||
|
bldgNoSno?: number | null;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Zip {
|
||||||
|
zoneNo: string;
|
||||||
|
ctpvNm: string;
|
||||||
|
sggNm: string;
|
||||||
|
lotno: string;
|
||||||
|
zipAddr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ZipListResponse {
|
||||||
|
content?: Zip[];
|
||||||
|
totalElements?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostcodeSearchParams = {
|
||||||
|
roadNm: string;
|
||||||
|
bldgNoMno: number;
|
||||||
|
bldgNoSno: number | null;
|
||||||
|
page: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsePostcodeInputResult =
|
||||||
|
| { ok: true; value: PostcodeSearchParams; keyword: string }
|
||||||
|
| { ok: false; message: string };
|
||||||
|
|
||||||
|
export type PostcodeSearchResult = {
|
||||||
|
totalCount: number;
|
||||||
|
items: AddrItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function findAddress(searchQuery: ZipSearchParams): Promise<ZipListResponse> {
|
||||||
|
if (!searchQuery) return {};
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (searchQuery.roadNm) params.append("roadNm", searchQuery.roadNm);
|
||||||
|
if (searchQuery.bldgNoMno != null) {
|
||||||
|
params.append("bldgNoMno", searchQuery.bldgNoMno.toString());
|
||||||
|
}
|
||||||
|
if (searchQuery.bldgNoSno != null) {
|
||||||
|
params.append("bldgNoSno", searchQuery.bldgNoSno.toString());
|
||||||
|
}
|
||||||
|
if (searchQuery.page != null) params.append("page", searchQuery.page.toString());
|
||||||
|
if (searchQuery.size != null) params.append("size", searchQuery.size.toString());
|
||||||
|
|
||||||
|
const url = `/cms-service/api/v1/zips?${params.toString()}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error("Network response was not ok");
|
||||||
|
return (await response.json()) as ZipListResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchFilter(search: string): boolean {
|
||||||
|
if (search.length === 0) return true;
|
||||||
|
|
||||||
|
const expText = /[%=><]/;
|
||||||
|
if (expText.test(search)) {
|
||||||
|
alert("특수문자를 입력 할수 없습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlArray = [
|
||||||
|
"OR",
|
||||||
|
"SELECT",
|
||||||
|
"INSERT",
|
||||||
|
"DELETE",
|
||||||
|
"UPDATE",
|
||||||
|
"CREATE",
|
||||||
|
"DROP",
|
||||||
|
"EXEC",
|
||||||
|
"UNION",
|
||||||
|
"FETCH",
|
||||||
|
"DECLARE",
|
||||||
|
"TRUNCATE",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const sqlWord of sqlArray) {
|
||||||
|
const regex = new RegExp(sqlWord, "gi");
|
||||||
|
if (regex.test(search)) {
|
||||||
|
alert(`"${sqlWord}"와(과) 같은 특정문자로 검색할 수 없습니다.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePostcodeInput(
|
||||||
|
input: string,
|
||||||
|
page = 1,
|
||||||
|
): ParsePostcodeInputResult {
|
||||||
|
const normalized = input.replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "도로명과 건물번호를 입력해 주세요. 예) 테헤란로 152-1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = normalized.split(" ");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "도로명과 건물번호를 공백으로 구분해 주세요. 예) 테헤란로 152-1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bldgToken = parts.at(-1) ?? "";
|
||||||
|
const roadNm = parts.slice(0, -1).join(" ").trim();
|
||||||
|
|
||||||
|
if (!roadNm) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "도로명을 입력해 주세요. 예) 테헤란로 152-1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+(?:-\d+)?$/.test(bldgToken)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"건물번호는 숫자 또는 숫자-숫자 형식으로 입력해 주세요. 예) 152 또는 152-1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [mnoStr, snoStr] = bldgToken.split("-");
|
||||||
|
const bldgNoMno = Number(mnoStr);
|
||||||
|
const bldgNoSno = snoStr ? Number(snoStr) : null;
|
||||||
|
|
||||||
|
if (!Number.isInteger(bldgNoMno) || bldgNoMno <= 0) {
|
||||||
|
return { ok: false, message: "건물본번은 1 이상의 숫자로 입력해 주세요." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bldgNoSno !== null && (!Number.isInteger(bldgNoSno) || bldgNoSno <= 0)) {
|
||||||
|
return { ok: false, message: "건물부번은 1 이상의 숫자로 입력해 주세요." };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
roadNm,
|
||||||
|
bldgNoMno,
|
||||||
|
bldgNoSno,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
keyword: `${roadNm} ${bldgNoMno}${bldgNoSno ? `-${bldgNoSno}` : ""}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSearchInputFromRoadAddr(roadAddr: string): string {
|
||||||
|
const firstSegment = roadAddr.split(",")[0]?.trim() ?? "";
|
||||||
|
if (!firstSegment) return "";
|
||||||
|
|
||||||
|
const parts = firstSegment.split(/\s+/);
|
||||||
|
const bldgToken = parts.at(-1) ?? "";
|
||||||
|
if (!/^\d+(?:-\d+)?$/.test(bldgToken)) return "";
|
||||||
|
|
||||||
|
return `${parts.slice(0, -1).join(" ")} ${bldgToken}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPostcode(
|
||||||
|
params: PostcodeSearchParams,
|
||||||
|
): Promise<PostcodeSearchResult> {
|
||||||
|
const keywordForCheck = `${params.roadNm} ${params.bldgNoMno}${
|
||||||
|
params.bldgNoSno ? `-${params.bldgNoSno}` : ""
|
||||||
|
}`;
|
||||||
|
if (!searchFilter(keywordForCheck)) {
|
||||||
|
return {
|
||||||
|
totalCount: 0,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: ZipSearchParams = {
|
||||||
|
roadNm: params.roadNm,
|
||||||
|
bldgNoMno: params.bldgNoMno,
|
||||||
|
bldgNoSno: params.bldgNoSno,
|
||||||
|
page: Math.max(params.page - 1, 0),
|
||||||
|
size: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await findAddress(query);
|
||||||
|
const list = Array.isArray(data?.content) ? (data.content as Zip[]) : [];
|
||||||
|
const items: AddrItem[] = list.map((item) => ({
|
||||||
|
zipNo: item.zoneNo ?? "",
|
||||||
|
roadAddr: item.zipAddr ?? "",
|
||||||
|
jibunAddr: item.lotno ?? item.zipAddr ?? "",
|
||||||
|
ctpvNm: item.ctpvNm ?? "",
|
||||||
|
sggNm: item.sggNm ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCount: Number(data?.totalElements ?? 0),
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import {
|
||||||
|
fetchPostcode,
|
||||||
|
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<T>(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<AddrItem[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchMessage, setSearchMessage] = useState("");
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, [dq, open]);
|
||||||
|
|
||||||
|
if (!mounted || !open) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed inset-0 z-[9999]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-0 bg-black/40"
|
||||||
|
aria-label="닫기"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center p-6">
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-[520px] rounded-2xl bg-white shadow-[0_20px_60px_rgba(0,0,0,0.18)]"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="우편번호 검색"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-7 pt-6 pb-4">
|
||||||
|
<h2 className="text-[22px] font-extrabold tracking-tight text-slate-900">
|
||||||
|
우편번호 검색
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="grid h-9 w-9 place-items-center rounded-full hover:bg-slate-100"
|
||||||
|
aria-label="닫기"
|
||||||
|
>
|
||||||
|
<span className="text-[18px] leading-none">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-7 pb-4">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
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(" ")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 grid h-9 w-9 place-items-center rounded-full hover:bg-slate-100 text-[#01ACE5]"
|
||||||
|
aria-label="검색"
|
||||||
|
onClick={() => setQ((v) => v.trim())}
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path
|
||||||
|
d="M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 21l-4.3-4.3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* suggestions dropdown
|
||||||
|
<div className="absolute left-0 right-0 top-[calc(100%+6px)] z-10 overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg">
|
||||||
|
이후 다른 API 연동 예정
|
||||||
|
</div>
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-7 pb-7">
|
||||||
|
<div className="mb-3 text-[13px] text-slate-500">
|
||||||
|
{q.trim()
|
||||||
|
? loading
|
||||||
|
? "검색 중..."
|
||||||
|
: searchMessage || `총 ${total.toLocaleString()}개의 검색결과`
|
||||||
|
: "도로명과 건물번호를 입력하세요. 예) 테헤란로 152-1"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[420px] overflow-y-auto pr-1">
|
||||||
|
{!loading && q.trim() && items.length === 0 && !searchMessage && (
|
||||||
|
<div className="rounded-lg border border-slate-200 p-4 text-[14px] text-slate-600">
|
||||||
|
검색 결과가 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((it) => (
|
||||||
|
<button
|
||||||
|
key={`${it.zipNo}-${it.roadAddr}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(it)}
|
||||||
|
className={[
|
||||||
|
"w-full rounded-lg border p-4 text-left transition",
|
||||||
|
"border-slate-200 hover:border-sky-300 hover:bg-sky-50/30",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<div className="text-[20px] font-extrabold tracking-tight text-slate-900">
|
||||||
|
{it.zipNo}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-start gap-2">
|
||||||
|
<span className="w-[52px] shrink-0 mt-[2px] inline-flex items-center justify-center rounded-full border border-sky-400 px-2 py-[2px] text-[12px] font-semibold text-sky-600">
|
||||||
|
도로명
|
||||||
|
</span>
|
||||||
|
<p className="text-[14px] leading-6 text-slate-700">
|
||||||
|
{it.roadAddr}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1 flex items-start gap-2">
|
||||||
|
<span className="w-[52px] shrink-0 mt-[2px] inline-flex items-center justify-center rounded-full border border-[#6DBB4F] px-2 py-[2px] text-[12px] font-semibold text-[#6DBB4F]">
|
||||||
|
지번
|
||||||
|
</span>
|
||||||
|
<p className="text-[14px] leading-6 text-slate-700">
|
||||||
|
{it.jibunAddr}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user