Merge branch 'iipbs/sn/dev'

This commit is contained in:
swcho
2025-11-13 15:39:59 +09:00
12 changed files with 673 additions and 406 deletions
@@ -58,7 +58,7 @@ export default function MyForm() {
dataList={demandCompList}
rtnValue={callback}
rtnInputData={handleInsertSdmData}
openNetwork={false}
openNetwork={true}
/>
<Toaster position="top-center" />
</div>
+16 -29
View File
@@ -42,9 +42,8 @@ export function middleware(req: NextRequest) {
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : ""
}`;
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${decodedParams ? `?${decodedParams}` : ""
}`;
return NextResponse.rewrite(new URL(newUrl));
}
@@ -55,33 +54,25 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
// const token =
// "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww";
// const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0";
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
//wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
//http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
console.log(
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`
);
console.log(`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
if (pathname.startsWith("/user-service")) {
if (pathname.startsWith("/user-service")
|| pathname.startsWith("/cms-service")) {
let queryParams = searchParams.toString();
const requestHeaders = new Headers(req.headers);
const newUrl = `${serverApiUrl}${pathname}`;
const newUrl = `${serverApiUrl}${pathname}${queryParams ? `?${queryParams}` : ""}`;
console.log(
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`
);
console.log(`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
@@ -130,27 +121,24 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/reuse-service")) {
const queryParams = searchParams.toString();
const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${
queryParams ? `?${queryParams}` : ""
}`;
const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/operation-service")) {
const queryParams = searchParams.toString();
const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${
queryParams ? `?${queryParams}` : ""
}`;
const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/sewerage-service")) {
const queryParams = searchParams.toString();
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${
queryParams ? `?${queryParams}` : ""
}`;
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/sewerage-service' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
@@ -174,9 +162,7 @@ export function middleware(req: NextRequest) {
const newUrl = `${BUSINESS_JUSO_API_URL}?${params.toString()}`;
console.log(`[Middleware] Request to business.juso.go.kr`);
console.log(req.url);
console.log(
`[Middleware] Rewriting from '/business.juso.go.kr' path to: ${newUrl}`
);
console.log(`[Middleware] Rewriting from '/business.juso.go.kr' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
@@ -187,6 +173,7 @@ export const config = {
matcher: [
"/server/:path*",
"/user-service/:path*",
"/cms-service/:path*",
"/attach-service/:path*",
"/geoserver/:path*",
"/operation-service/:path*",
@@ -302,7 +302,7 @@ export default function CoSttn() {
</div>
<UiModal
bodyClass={
"w-[40vw] max-w-none h-[83vh] " // +updateData?.dmdInfoId?'h-[60vh]':'h-[90vh]'
"w-[40vw] max-w-none h-[90vh] overflow-y-auto" // +updateData?.dmdInfoId?'h-[60vh]':'h-[90vh]'
}
open={isDmdModal.isMoal}
onOpenChange={() => {
@@ -350,7 +350,7 @@ export default function CoSttn() {
}}
bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"}
updateList={updateData}
openNetwork={false}
openNetwork={true}
></SdmInsertForm>
}
></UiModal>
@@ -766,7 +766,7 @@ export function PbnstBuseForm({
></UiModal>
<UiModal
bodyClass={"w-[40vw] max-w-none h-[90vh]"}
bodyClass={"w-[40vw] max-w-none h-[100vh] overflow-y-auto"}
open={isDmdModal.isMoal}
onOpenChange={() => {
setIsDmdModal({ isMoal: false, data: {} as DemandCompType });
@@ -787,9 +787,10 @@ export function PbnstBuseForm({
});
}}
bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"}
openNetwork={false}
openNetwork={true}
></SdmInsertForm>
}
noFooterYn={"Y"}
></UiModal>
</div>
) : (
+2 -1
View File
@@ -60,7 +60,8 @@ export function middleware(req: NextRequest) {
}
if (pathname.startsWith("/cms-service")) {
const newUrl = `${serverApiUrl}${pathname}`;
let queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname}${queryParams ? `?${queryParams}` : ""}`;
console.log(`[Middleware] Rewriting from '/cms-service' path to: ${newUrl}`);
const rewriteRequest = rewriteWith(newUrl, req, true);
return rewriteRequest;
@@ -31,19 +31,21 @@ export function UiModal({
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={bodyClass}>
{noHeaderYn === 'N' && (
<DialogHeader>
<DialogDescription className={headerTitleClass}>
{headerTitle}
</DialogDescription>
</DialogHeader>
)}
{(open && body) ?
useDirectRender
? body
: <>{body}</>
: <></>
}
{noHeaderYn === 'N' && (
<DialogHeader>
<DialogDescription className={headerTitleClass}>
{headerTitle}
</DialogDescription>
</DialogHeader>
)}
<div>
{(open && body) ?
useDirectRender
? body
: <>{body}</>
: <></>
}
</div>
{noFooterYn === 'N' && (
<DialogFooter className="sm:justify-start">
{footerContent}
@@ -14,7 +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 "./search_input_address";
export * from "./select";
export * from "./tab";
export * from "./table";
@@ -1,4 +1,4 @@
import { Input } from "@common/components";
import { Button, Input, Label } from "@common/components";
import { AsYouType } from "libphonenumber-js";
import React, { useCallback, useEffect, useState } from "react";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
@@ -6,8 +6,10 @@ import { FormField } from "../form";
import { FormUi } from "../form_ui";
import { SelectBox } from "../select";
import { SdmSearchForm } from "./sdmNameBox";
import SearchInputAddress from "../search_input_address";
import type { addressType } from "../search_input_address/findAddress";
import { UiModal, UiTable } from '../../ui';
import type { Zip, ZipSearchParams } from "../search_input_address/zipService";
import { findAddress } from "../search_input_address/findAddress";
import { cn } from "@common/components";
// 수요처 정보
export type DemandCompType = {
@@ -18,7 +20,7 @@ export type DemandCompType = {
daddr: string | "";
telno: string | "";
rgtr: string | "";
mdfr?:string ;
mdfr?: string;
useUsgCd: string | "";
ctpvCd: string | "";
sggCd: string | "";
@@ -89,6 +91,7 @@ export function SdmRegForm({
readOnly,
openNetwork,
}: SdmRegFormProps) {
const PAGE_SIZE = 10;
const [sidoValue, setSidoValue] = useState<string>("");
const [sigunValue, setSigunValue] = useState<string>("");
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
@@ -97,6 +100,34 @@ export function SdmRegForm({
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
const [orgBznmNo, setOrgBznmNo] = useState<string>("");
const [updateCoNm, setUpdateNm] = useState<string>("");
const [isZipModalOpen, setIsZipModalOpen] = useState<boolean>(false);
const [zipId, setZipId] = useState<number | undefined>(undefined);
const [address, setAddress] = useState<string | undefined>("");
const [detailedAddress, setDetailedAddress] = useState<string>("");
const [curPage, setCurPage] = useState<number>(1);
// 우편번호 검색 관련 상태
const [zipSearchResults, setZipSearchResults] = useState<Zip[]>([]);
const [zipTotalElements, setZipTotalElements] = useState<number>(-1);
const [searchQuery, setSearchQuery] = useState<ZipSearchParams>({
roadNm: "",
bldgNoMno: null,
bldgNoSno: null,
page: curPage,
size: PAGE_SIZE
});
const [isResetPage, setIsResetPage] = useState<boolean>(false);
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined);
const [selectedZip, setSelectedZip] = useState<Zip>();
const [isSelectedZip, setIsSelectedZip] = useState<boolean>(false);
const handleCellClick = (index: any, headId: any) => {
console.log('index', index);
console.log('headId', headId);
setSelectedZip(zipSearchResults[index]);
setIsSelectedZip(true);
console.log('selectedZip', zipSearchResults[index]);
};
const formatPhoneNumber = (value: string) => {
const input = value;
@@ -262,6 +293,17 @@ export function SdmRegForm({
[orgBznmNo, setIsDuplicate],
);
// 우편번호 선택 함수
const handleSelectAddress = () => {
setZipId(selectedZip?.zipId);
setAddress(selectedZip?.zipAddr || "");
setDetailedAddress("");
setIsZipModalOpen(false);
setSelectedZip(undefined);
setIsSelectedZip(false);
setSelectedRowIndex(undefined);
};
useEffect(() => {
if (updateList?.dmdInfoId) {
form.reset(updateList);
@@ -283,199 +325,344 @@ export function SdmRegForm({
console.log("form.formState.errors ", form.formState.errors);
}, [form.formState.errors]);
useEffect(() => {
console.log("curPage ", curPage);
searchQuery.page = curPage - 1;
const fetchAddress = async () => {
const result = await findAddress(searchQuery);
setZipSearchResults(result?.content);
};
fetchAddress();
}, [curPage]);
// 폼 제출 전에 주소 합치기
useEffect(() => {
// address와 detailedAddress를 합쳐서 daddr에 설정
if (address) {
const fullAddress = `${address || ""} ${detailedAddress || ""}`.trim();
form.setValue("daddr", fullAddress, {shouldValidate: true });
}
}, [address, detailedAddress, form]);
const renderBody = () => (
<>
<form
onSubmit={async (e) => {
e.preventDefault();
e.stopPropagation();
const result = await findAddress(searchQuery);
setZipSearchResults(result?.content);
setZipTotalElements(result?.totalElements);
}}
className="mb-4 space-y-2"
>
{/* 검색 조건 부분 */}
<div className="card card-border !m-0 flex flex-col gap-4 p-4">
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col">
<Label className="mb-1 font-bold text-base">도로명</Label>
<Input
value={searchQuery.roadNm || ""}
onChange={(e) =>
setSearchQuery({
...searchQuery,
roadNm: e.target.value,
})
}
placeholder="예: 테헤란로"
className="!text-[1.3rem]"
/>
</div>
<div className="flex flex-col">
<Label className="mb-1 text-base">건물본번</Label>
<Input
type="number"
value={searchQuery.bldgNoMno ?? ""}
onChange={(e) => {
const value = e.target.value
? parseInt(e.target.value)
: null;
setSearchQuery({
...searchQuery,
bldgNoMno: value,
});
}}
placeholder="예: 123"
className="!text-[1.3rem]"
/>
</div>
<div className="flex flex-col">
<Label className="mb-1 text-base">건물부번</Label>
<Input
type="number"
value={searchQuery.bldgNoSno ?? ""}
onChange={(e) => {
const value = e.target.value
? parseInt(e.target.value)
: null;
setSearchQuery({
...searchQuery,
bldgNoSno: value,
});
}}
placeholder="예: 45"
className="!text-[1.3rem]"
/>
</div>
</div>
<div className="flex justify-end">
<Button className="rounded-md" type="submit">검색</Button>
</div>
</div>
</form>
{/* 검색 결과 부분 */}
{zipTotalElements > 0 && (
<UiTable
headName=''
tableData={zipSearchResults}
headlist={[{ id: 'zoneNo', title: '우편번호' }, { id: 'zipAddr', title: '주소' }]}
pageSize={PAGE_SIZE}
total={zipTotalElements}
cellClick={handleCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
index={selectedRowIndex}
/>
)}
{zipTotalElements === 0 && (
<div className={cn("flex h-96 flex-col items-center justify-center gap-4")}>
<i className="diveicon di-info-circle text-3xl" />
조회된 데이터가 없습니다.
</div>
)}
</>
);
const formFields: Array<{
name: keyof FormSchema;
label: string;
component: FieldComponent;
}> = [
{
name: "ctpvCd",
label: "시도",
component: (field) =>
updateList?.dmdInfoId ? (
<Input
placeholder="시도"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => {
field.onChange(e.target.value);
}}
value={
selectSidoData.find((item) => item.val === field.value?.trim())
?.text ?? ""
}
readOnly={!!updateList?.dmdInfoId}
/>
) : (
<SelectBox
selectClass={undefined}
label=""
selectArray={selectSidoData}
onSelectValue={(val) =>
onSelectValue(val.toString(), "ctpvCd", field ?? "")
}
className=""
value={sidoValue}
{
name: "ctpvCd",
label: "시도",
component: (field) =>
updateList?.dmdInfoId ? (
<Input
placeholder="시도"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => {
field.onChange(e.target.value);
}}
value={
selectSidoData.find((item) => item.val === field.value?.trim())
?.text ?? ""
}
readOnly={!!updateList?.dmdInfoId}
/>
) : (
<SelectBox
selectClass={undefined}
label=""
selectArray={selectSidoData}
onSelectValue={(val) =>
onSelectValue(val.toString(), "ctpvCd", field ?? "")
}
className=""
value={sidoValue}
/>
),
},
{
name: "sggCd",
label: "시군구",
component: (field) =>
updateList?.dmdInfoId ? (
<Input
placeholder="시도"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => {
field.onChange(e.target.value);
}}
value={
selectSigunData.find((item) => item.val === field.value?.trim())
?.text ?? ""
}
readOnly={!!updateList?.dmdInfoId}
/>
) : (
<SelectBox
selectClass={"!text-xl text-gray-600"}
label=""
selectArray={filteredSigunData}
onSelectValue={(val) =>
onSelectValue(val.toString(), "sggCd", field)
}
className=""
value={sigunValue}
/>
),
},
{
name: "dmdCoNm",
label: "회사명",
component: (field) => (
<SdmSearchForm
rtnData={setRtnData}
label="수요처명"
initValue={updateCoNm}
dataList={dataList}
rtnValue={setRtnValue}
rtnText={setRtnText}
readOnly={readOnly}
{...field}
/>
),
},
{
name: "sggCd",
label: "시군구",
component: (field) =>
updateList?.dmdInfoId ? (
},
{
name: "dmdInfoId",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="시도"
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => {
field.onChange(e.target.value);
}}
value={
selectSigunData.find((item) => item.val === field.value?.trim())
?.text ?? ""
}
onChange={(e) => bznNoChange(e, "dmdInfoId", field)}
value={formatBusinessNumber(field.value ?? "")}
readOnly={!!updateList?.dmdInfoId}
/>
) : (
<SelectBox
selectClass={"!text-xl text-gray-600"}
label=""
selectArray={filteredSigunData}
onSelectValue={(val) =>
onSelectValue(val.toString(), "sggCd", field)
}
className=""
value={sigunValue}
/>
),
},
{
name: "dmdCoNm",
label: "회사명",
component: (field) => (
<SdmSearchForm
rtnData={setRtnData}
label="수요처명"
initValue={updateCoNm}
dataList={dataList}
rtnValue={setRtnValue}
rtnText={setRtnText}
readOnly={readOnly}
{...field}
/>
),
},
{
name: "dmdInfoId",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => bznNoChange(e, "dmdInfoId", field)}
value={formatBusinessNumber(field.value ?? "")}
readOnly={!!updateList?.dmdInfoId}
/>
),
},
{
name: "bznmTotNo",
label: "",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
},
{
name: "bznmTotNo",
label: "",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
onChange={(e) => bznNoChange(e, "bznmTotNo", field)}
// {...field}
value={field.value ?? ""}
readOnly={!!updateList?.dmdInfoId}
/>
</FormUi>
),
},
{
name: "daddr",
label: "사업자주소",
component: (field) =>
openNetwork ? (
<div>
<div className="flex items-center gap-2">
{/* 우편번호 */}
<Input
type="text"
className="!text-xl text-gray-600 w-64"
readOnly={true}
value={zipId?.toString() ?? ""}
placeholder="우편번호"
/>
<Button className="px-4 py-2 rounded-md" onClick={() => setIsZipModalOpen(true)}>우편번호 검색</Button>
</div>
{/* 주소 */}
<Input
type="text"
className="!text-xl text-gray-600"
readOnly={true}
value={address ?? ""}
placeholder="주소"
/>
{/* 상세 주소 */}
<Input
placeholder="상세 주소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
readOnly={readOnly}
onChange={(e) => {
const value = e.target.value;
setDetailedAddress(value);
}}
value={detailedAddress ?? ""}
/>
</div>
) : (
<Input
placeholder="주소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
readOnly={readOnly}
{...field}
value={field.value ?? ""}
/>
),
},
{
name: "telno",
label: "전화번호",
component: (field) => {
const formattedValue = formatPhoneNumber(field.value ?? ""); // 초기 렌더링 시에도 포맷 적용
return (
<Input
placeholder="전화번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={formattedValue}
readOnly={readOnly}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ""); // 숫자만 저장
field.onChange(value);
}}
/>
);
},
},
{
name: "dmdAplyPswd",
label: "패스워드",
component: (field) => (
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
onChange={(e) => bznNoChange(e, "bznmTotNo", field)}
// {...field}
value={field.value ?? ""}
readOnly={!!updateList?.dmdInfoId}
/>
</FormUi>
),
},
{
name: "daddr",
label: "사업자주소",
component: (field) =>
openNetwork ? (
<SearchInputAddress
initValue={updateList?.daddr || ""}
readonly={readOnly ?? false}
setRtnData={(rtnData: addressType) => {
field.onChange(rtnData.roadAddrPart1);
}}
/>
) : (
<Input
placeholder="주소를 입력하세요"
type="text"
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
readOnly={readOnly}
{...field}
value={field.value ?? ""}
/>
),
},
{
name: "telno",
label: "전화번호",
component: (field) => {
const formattedValue = formatPhoneNumber(field.value ?? ""); // 초기 렌더링 시에도 포맷 적용
return (
<Input
placeholder="전화번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={formattedValue}
readOnly={readOnly}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ""); // 숫자만 저장
field.onChange(value);
}}
/>
);
},
},
{
name: "dmdAplyPswd",
label: "패스워드",
component: (field) => (
<Input
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
readOnly={readOnly}
{...field}
value={field.value ?? ""}
/>
),
},
{
name: "dmdAplyPswdConfirm",
label: "패스워드확인",
component: (field) => (
<Input
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
readOnly={readOnly}
{...field}
value={field.value ?? ""}
/>
),
},
];
{
name: "dmdAplyPswdConfirm",
label: "패스워드확인",
component: (field) => (
<Input
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
readOnly={readOnly}
{...field}
value={field.value ?? ""}
/>
),
},
];
return (
<>
@@ -538,6 +725,36 @@ export function SdmRegForm({
/>
);
})}
<UiModal
headerTitle="주소 검색"
headerTitleClass="text-2xl font-bold text-gray-900"
open={isZipModalOpen}
onOpenChange={setIsZipModalOpen}
bodyClass={`
${zipTotalElements > -1 ? 'h-[80vh]' : 'h-[30vh]'}
w-[30vw]
min-w-[500px]
max-w-[90vw]
max-h-[85vh]
overflow-y-auto
!fixed
!top-[50%]
!left-[50%]
!-translate-x-1/2
!-translate-y-1/2
z-[101]
flex
flex-col
`}
body={renderBody()}
footerContent={
<div className="w-full flex justify-center items-center gap-4">
<Button className="rounded-md" size="lg" onClick={handleSelectAddress} disabled={!isSelectedZip}>선택</Button>
<Button className="rounded-md" size="lg" onClick={() => setIsZipModalOpen(false)}>취소</Button>
</div>
}
useDirectRender={true}
/>
</>
);
}
@@ -39,7 +39,7 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up
.refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"),
daddr: z.string()
.min(1, "주소를 입력해주세요")
.min(6, "주소는 6자 이상 입력해주세요")
// .min(6, "주소는 6자 이상 입력해주세요")
.max(40, "주소는 40자 이내로 입력해주세요"),
telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
@@ -2,11 +2,13 @@
// const Key = 'U01TX0FVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA'
// const addresssKey = 'devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM='; //인증키 만료일은 2025-11-23
import type { Zip, ZipSearchParams } from "./zipService";
/**
* 주소 API 응답 타입
* https://business.juso.go.kr/addrlink/main.do 해당 사이트 참조
*/
export type AddressApiResponse ={
export type AddressApiResponse = {
results: {
common: {
errorMessage: string;
@@ -19,7 +21,7 @@ export type AddressApiResponse ={
};
}
export type addressType ={
export type addressType = {
detBdNmList: string;
engAddr: string;
rn: string;
@@ -46,79 +48,70 @@ export type addressType ={
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: '', // 인증키 인터넷용 패쇄망용은 따로 발급 필요 즉 middleware에서 치환해서 사용해야할듯
resultType: "json", // 결과 형식 기본은 xml
});
// const url = `https://business.juso.go.kr/addrlink/addrLinkApi.do?${params.toString()}`;
const url = `/bussiness-juso-service/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 [];
}
export const findAddress = async (searchQuery: ZipSearchParams): Promise<any> => {
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()}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
if (data) return data
else return [];
} catch (error) {
console.error('Error fetching address data:', error);
return [];
console.error('Error fetching address data:', error);
return [];
}
}
export const searchFilter=(search:string)=>{
export const searchFilter = (search: string) => {
if(search.length >0){
//특수문자 제거
const expText = /[%=><]/ ;
if(expText.test(search) == true){
alert("특수문자를 입력 할수 없습니다.") ;
search = search.split(expText).join("");
return false;
}
//특정문자열(sql예약어의 앞뒤공백포함) 제거
const sqlArray = [
//sql 예약어
"OR", "SELECT", "INSERT", "DELETE", "UPDATE", "CREATE", "DROP", "EXEC",
"UNION", "FETCH", "DECLARE", "TRUNCATE"
if (search.length > 0) {
//특수문자 제거
const expText = /[%=><]/;
if (expText.test(search) == true) {
alert("특수문자를 입력 할수 없습니다.");
search = search.split(expText).join("");
return false;
}
//특정문자열(sql예약어의 앞뒤공백포함) 제거
const sqlArray = [
//sql 예약어
"OR", "SELECT", "INSERT", "DELETE", "UPDATE", "CREATE", "DROP", "EXEC",
"UNION", "FETCH", "DECLARE", "TRUNCATE"
]
let regex;
for(let i=0; i<sqlArray.length; i++){
regex = new RegExp( sqlArray[i] as string,"gi") ;
if (regex.test(search) ) {
alert("\"" + sqlArray[i]+"\"와(과) 같은 특정문자로 검색할 수 없습니다.");
search =search.replace(regex, "");
return false;
}
}
}
return true ;
let regex;
for (let i = 0; i < sqlArray.length; i++) {
regex = new RegExp(sqlArray[i] as string, "gi");
if (regex.test(search)) {
alert("\"" + sqlArray[i] + "\"와(과) 같은 특정문자로 검색할 수 없습니다.");
search = search.replace(regex, "");
return false;
}
}
}
return true;
@@ -1,129 +1,129 @@
import { Input } from "@common/components/ui";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { findAddress, searchFilter, type addressType } from "./findAddress";
// import { Input } from "@common/components/ui";
// import React, { useCallback, useEffect, useRef, useState } from "react";
// import { findAddress, searchFilter, type addressType } from "./findAddress";
type thisProps = {
initValue: string | undefined;
setRtnData: (rtnData: addressType) => void;
readonly?: boolean;
};
// type thisProps = {
// initValue: string | undefined;
// setRtnData: (rtnData: addressType) => void;
// readonly?: boolean;
// };
/**
*
* @param initValue : 초기값 ?
* @returns setRtnData => 선택된 주소 데이터 반환
*/
export default function SearchInputAddress({
initValue,
setRtnData = () => {},
readonly = false,
}: thisProps) {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || "");
const [searchList, setSearchList] = useState<addressType[]>([]);
// /**
// *
// * @param initValue : 초기값 ?
// * @returns setRtnData => 선택된 주소 데이터 반환
// */
// export default function SearchInputAddress({
// initValue,
// setRtnData = () => {},
// readonly = false,
// }: thisProps) {
// const [isOpen, setIsOpen] = useState(false);
// const [textValue, setTextValue] = useState(initValue || "");
// const [searchList, setSearchList] = useState<addressType[]>([]);
useEffect(() => {
if (
initValue !== textValue &&
initValue !== undefined &&
initValue !== null &&
initValue !== ""
) {
setTextValue(initValue);
}
}, [initValue]);
const wrapperRef = useRef<HTMLDivElement>(null);
// useEffect(() => {
// if (
// initValue !== textValue &&
// initValue !== undefined &&
// initValue !== null &&
// initValue !== ""
// ) {
// setTextValue(initValue);
// }
// }, [initValue]);
// const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// const handleInputChange = useCallback(
// (e: React.ChangeEvent<HTMLInputElement>) => {
// const value = e.target.value;
if (searchFilter(value)) {
setTextValue(value);
findAddress(value).then((data) => {
setSearchList(data);
});
}else{
setTextValue("");
setSearchList([]);
}
if (value) {
setIsOpen(true);
} else {
setIsOpen(false);
}
},
[setSearchList],
);
// if (searchFilter(value)) {
// setTextValue(value);
// findAddress(value).then((data) => {
// setSearchList(data);
// });
// }else{
// setTextValue("");
// setSearchList([]);
// }
// 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],
);
// 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);
}
};
// 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);
};
}, []);
// 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="h-[3.2rem] w-full rounded-md border border-gray-300 pl-3 !text-xl text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`주소를 입력하세요`}
readOnly={readonly}
/>
{isOpen && (
<div className="absolute left-0 right-0 z-50 mt-2 max-h-72 overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
<div className="max-h-72">
{searchList && searchList.length > 0 ? (
searchList.map((item: addressType, index: number) => (
<div
key={index}
className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100"
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="w-full cursor-pointer overflow-auto px-5 py-3 !text-xl hover:bg-gray-100">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
);
}
// return (
// <div ref={wrapperRef} className="relative">
// <Input
// type="text"
// value={textValue}
// onChange={handleInputChange}
// /* onFocus={() => setIsOpen(true)} */
// className="h-[3.2rem] w-full rounded-md border border-gray-300 pl-3 !text-xl text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
// placeholder={`주소를 입력하세요`}
// readOnly={readonly}
// />
// {isOpen && (
// <div className="absolute left-0 right-0 z-50 mt-2 max-h-72 overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
// <div className="max-h-72">
// {searchList && searchList.length > 0 ? (
// searchList.map((item: addressType, index: number) => (
// <div
// key={index}
// className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100"
// 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="w-full cursor-pointer overflow-auto px-5 py-3 !text-xl hover:bg-gray-100">
// 검색 결과가 없습니다.
// </div>
// )}
// </div>
// </div>
// )}
// </div>
// );
// }
@@ -0,0 +1,66 @@
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
export interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PageableParams {
page?: number;
size?: number;
sort?: string;
}
export interface ZipSearchParams extends PageableParams {
zoneNo?: string; // 구역번호
ctpvNm?: string; // 시도명
sggNm?: string; // 시군구명
emdNm?: string; // 읍면동명
roadNm?: string; // 도로명
stdgNm?: string; // 법정동명
dongNm?: string; // 행정동명
bldgNoMno?: number | null; // 빌딩본번
bldgNoSno?: number | null; // 빌딩딩부번
lotnoMno?: number | null; // 지번본번
lotnoSno?: number | null; // 지번부번
}
export interface Zip {
zipId: number;
zoneNo: string;
ctpvNm: string;
sggNm: string;
emdNm: string;
roadNm: string;
bldgNoMno: number;
bldgNoSno: number;
lotnoMno: number;
lotnoSno: number;
lotno: string;
zipAddr: string;
}
export type ZipList = PageableList<Zip>;