주소검색 api 및 컴포넌트 생성(인터넷용)
This commit is contained in:
@@ -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}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user