dm_new 우편번호 검색
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
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 { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api";
|
||||
import { getCookie } from "@common/components/lib";
|
||||
@@ -21,6 +23,12 @@ type SessionUser = {
|
||||
|
||||
const TOKEN_ID_COOKIE = "token-id";
|
||||
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() {
|
||||
return <div className="h-px w-full bg-neutral-200" />;
|
||||
@@ -129,6 +137,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
const [zip, setZip] = useState("");
|
||||
const [addr1, setAddr1] = useState("");
|
||||
const [addr2, setAddr2] = useState("");
|
||||
const [isPostcodeModalOpen, setIsPostcodeModalOpen] = useState(false);
|
||||
|
||||
const [tel, setTel] = 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 ? "수요처수정" : "수요처등록";
|
||||
|
||||
return (
|
||||
@@ -414,7 +446,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => alert("우편번호 검색(샘플)")}
|
||||
onClick={() => setIsPostcodeModalOpen(true)}
|
||||
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 ? "수요처 수정" : "수요처 등록"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<PostcodeModal
|
||||
open={isPostcodeModalOpen}
|
||||
onClose={() => setIsPostcodeModalOpen(false)}
|
||||
onSelect={handleSelectAddress}
|
||||
/>
|
||||
</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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user