주소검색 api 및 컴포넌트 생성(인터넷용)

This commit is contained in:
leedj312
2025-08-26 15:54:25 +09:00
parent 94833e54d3
commit 632f1b0fd9
3 changed files with 195 additions and 0 deletions
@@ -14,6 +14,7 @@ export * from "./search_form_left";
export * from "./search_form_right";
export * from "./search_input";
export * from "./search_input_select";
export * from "./search_input_address";
export * from "./select";
export * from "./tab";
export * from "./table";
@@ -0,0 +1,86 @@
const addresssKey = 'devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM='; //인증키 만료일은 2025-11-23
/**
* 주소 API 응답 타입
* https://business.juso.go.kr/addrlink/main.do 해당 사이트 참조
*/
export type AddressApiResponse ={
results: {
common: {
errorMessage: string;
countPerPage: string;
totalCount: string;
errorCode: string;
currentPage: string;
};
juso: addressType[];
};
}
export type addressType ={
detBdNmList: string;
engAddr: string;
rn: string;
emdNm: string;
zipNo: string;
roadAddrPart2: string;
emdNo: string;
sggNm: string;
jibunAddr: string;
siNm: string;
roadAddrPart1: string;
bdNm: string;
admCd: string;
udrtYn: string;
lnbrMnnm: string;
roadAddr: string;
lnbrSlno: string;
buldMnnm: string;
bdKdcd: string;
liNm: string;
rnMgtSn: string;
mtYn: string;
bdMgtSn: string;
buldSlno: string;
}
/**
* 주소에 따른 검색
* @param search : 검색어
* @returns addressType[] 주소 배열
*/
export const findAddress = async (search: string) => {
if (!search) {
return [];
}
const params = new URLSearchParams({
currentPage: "1", // 원하는 페이지
countPerPage: "5", // 페이지당 결과 수
keyword: search, // 검색어(주소)
confmKey: addresssKey, // 인증키 인터넷용 패쇄망용은 따로 발급 필요 즉 middleware에서 치환해서 사용해야할듯
resultType: "json", // 결과 형식 기본은 xml
});
const url = `https://business.juso.go.kr/addrlink/addrLinkApi.do?${params.toString()}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
if (data.results && data.results.juso) {
return data.results.juso as addressType[];
} else {
return [];
}
} catch (error) {
console.error('Error fetching address data:', error);
return [];
}
}
@@ -0,0 +1,108 @@
import { Input } from "@common/components/ui";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { findAddress, type addressType } from "./findAddress";
type thisProps = {
initValue: string | undefined;
setRtnData: (rtnData: addressType) => void;
};
/**
*
* @param initValue : 초기값 ?
* @returns setRtnData => 선택된 주소 데이터 반환
*/
export default function SearchInputAddress({
initValue = undefined,
setRtnData = () => {},
}: thisProps) {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || "");
const [searchList, setSearchList] = useState<addressType[]>([]);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextValue(value);
findAddress(value).then((data) => {
setSearchList(data);
})
if (value) {
setIsOpen(true);
} else {
setIsOpen(false);
}
},
[setSearchList]
);
const handleSuggestionClick = useCallback(
(roadAddrPart1: string) => {
setTextValue(roadAddrPart1);
setIsOpen(false);
setRtnData(searchList?.find((item) => item.roadAddrPart1 === roadAddrPart1) || ({} as addressType));
},
[searchList, setRtnData]
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div ref={wrapperRef} className="relative">
<Input
type="text"
value={textValue}
onChange={handleInputChange}
/* onFocus={() => setIsOpen(true)} */
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`주소를 입력하세요`}
/>
{isOpen && (
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-300 rounded-md shadow-lg max-h-72 overflow-auto z-50">
<div className="max-h-72">
{searchList && searchList.length > 0 ? (
searchList.map((item: addressType, index: number) => (
<div
key={index}
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
onClick={() => handleSuggestionClick(item.roadAddrPart1)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleSuggestionClick(item.roadAddrPart1);
}
}}
>
{item.zipNo}&nbsp;
<span className="float-right">{item.roadAddrPart1}</span>
</div>
))
) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
);
}