This commit is contained in:
2025-09-02 13:25:55 +09:00
42 changed files with 3239 additions and 497 deletions
+3
View File
@@ -17,3 +17,6 @@ NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/ap
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
+3
View File
@@ -20,3 +20,6 @@ NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/ap
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
+3
View File
@@ -16,3 +16,6 @@ NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BE
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
+37 -1
View File
@@ -15,4 +15,40 @@ export const sanitizeInput = (val: string, allowDecimal = false) => {
// 정수만 허용
return val.replace(/[^0-9]/g, "");
};
};
// utils: format Date <-> "YYYY-MM-DD" without moment
export const formatDateToYMD=(value: Date): string => {
if (!value) return "";
// value can be Date | string ("YYYY-MM-DD")
if (typeof value === "string") {
// assume already in yyyy-mm-dd
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
const d = new Date(value);
if (isNaN(d.getTime())) return "";
return toYMD(d);
}
if (value instanceof Date) {
if (isNaN(value.getTime())) return "";
return toYMD(value);
}
return "";
}
const toYMD=(date: Date): string => {
// avoid timezone shift by normalizing to local date parts
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
// const parseYMDToDate=(ymd: string): Date | null => {
// if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) return null;
// const [y, m, d] = ymd.split("-").map(Number);
// const date = new Date(y, (m - 1), d);
// // validate round-trip
// return (date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d) ? date : null;
// }
@@ -2,7 +2,7 @@ import ModalSimilarPage from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/mod
import { useDmStore } from "@/app/store";
import {
CANDIDATE,
DatePicker,
// DatePicker,
FormField,
FormLabel,
FormUi,
@@ -10,7 +10,7 @@ import {
UiModal,
} from "@common/business_components";
import { Input } from "@common/components";
import { Button, Label } from "@common/components/ui";
import { Button, Label } from "@common/components/ui";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { AccessibleFormField } from "./AccessibleFormField";
@@ -22,7 +22,7 @@ import {
} from "react-hook-form";
import SearchInputText from "../common/searhDemandInput/component";
import React from "react";
import { sanitizeInput } from "../common/utils/utils";
import { formatDateToYMD, sanitizeInput } from "../common/utils/utils";
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
import type { SdmInfoFormData } from "@/app/store/sdmMng";
@@ -161,7 +161,9 @@ export function SdmInfoRegForm({
if (defaults) {
Object.entries(defaults).forEach(([field, value]) => {
form.setValue(field as keyof SdmInfoFormData, value, { shouldDirty: true });
form.setValue(field as keyof SdmInfoFormData, value, {
shouldDirty: true,
});
});
}
}, [form.getValues("usePurpose")]);
@@ -208,8 +210,8 @@ export function SdmInfoRegForm({
formIndex: 0,
required: true,
component: (field) => (
<div className="">
<DatePicker
<div className="relative w-64">
{/* <DatePicker
mode="single"
selected={field.value as Date}
onSelect={(date) => {
@@ -224,8 +226,18 @@ export function SdmInfoRegForm({
},
}}
showOutsideDays={true}
selectClass={undefined}
/>
selectClass={undefined}
/> */}
{/* 네이티브 input */}
<Input
className="w-full !text-xl text-right"
type="date"
onChange= {(e) => {
const selectedDate = e.target.value ? new Date(e.target.value) : null;
field.onChange(selectedDate);
}}
value={ formatDateToYMD(field.value as Date) }
/>
</div>
),
},
@@ -363,13 +375,11 @@ export function SdmInfoRegForm({
render={({ field }) =>
name === "money" ? (
<div>
<Label className="text-black" >
희망단가(원)*
</Label>
{ component(field)}
<Label className="text-black">희망단가(원)*</Label>
{component(field)}
</div>
// <FormUi label={label + "*"}>{component(field)}</FormUi>
) : (
// <FormUi label={label + "*"}>{component(field)}</FormUi>
<AccessibleFormField
field={field}
label={label}
@@ -773,7 +783,7 @@ export function SdmInfoRegForm({
onSelectValue={(val: string | number) => {
field.onChange(val.toString());
}}
value={field.value }
value={field.value}
readOnly={isDisabled}
onBlur={field.onBlur}
ref={field.ref}
+54 -29
View File
@@ -7,6 +7,8 @@ const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL;
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const BUSINESS_JUSO_API_URL = process.env.NEXT_PUBLIC_FINDADRESS_URL || "";
const AdressKey = process.env.NEXT_PUBLIC_WR_ADDRESS_KEY || "";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
@@ -24,8 +26,8 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/server")) {
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : ""
@@ -38,7 +40,6 @@ export function middleware(req: NextRequest) {
return NextResponse.redirect(new URL("/pages/wrStatus", req.url));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
// const token =
@@ -69,7 +70,7 @@ export function middleware(req: NextRequest) {
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders
headers: requestHeaders,
});
}
@@ -81,33 +82,31 @@ export function middleware(req: NextRequest) {
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
if (
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
typename == "hasudo:tb_il_swage_prcs_fclt" || //하수처리시설
typename == "hasudo:tb_ms_swage_prcs_zone" || //하수처리구역
typename == "hasudo:tb_ms_sbmrg_dngr_map" || //침수위험지도
typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
) {
newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
}
else if (
typename == "hasudo:tb_ms_swage_ppl" //관로
|| typename == "hasudo:tb_ms_swage_mhl" //맨홀
|| typename == "hasudo:tb_ms_swage_pmstn" //펌프장
|| typename == "hasudo:tb_ms_stofcbr" //우수토실
} else if (
typename == "hasudo:tb_ms_swage_ppl" || //관로
typename == "hasudo:tb_ms_swage_mhl" || //맨홀
typename == "hasudo:tb_ms_swage_pmstn" || //펌프장
typename == "hasudo:tb_ms_stofcbr" //우수토실
) {
newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
}
else if (
typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실
|| typename == "hasudo:om_wqlt_station" //관측소
|| typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설
|| typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설
|| typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설
|| typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설
|| typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처
|| typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
){
} else if (
typename == "hasudo:om_dt_untrt_swg_rain" || //중점우수토실
typename == "hasudo:om_wqlt_station" || //관측소
typename == "hasudo:tb_wr_bdst_ruse_fclt" || //재이용시설
typename == "hasudo:tb_wr_rnwtr_utztn_fclt" || //빗물이용시설
typename == "hasudo:tb_wr_rswtr_fclt" || //중수도시설
typename == "hasudo:tb_wr_htdfl_fclt" || //온배수시설
typename == "hasudo:tb_wr_ruse_fclt_dmd_co" || //수요처
typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
) {
const url = req.nextUrl.clone();
url.searchParams.set('TYPENAME', typename.replace("hasudo:",""));
url.searchParams.set("TYPENAME", typename.replace("hasudo:", ""));
queryParams = url.searchParams.toString();
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
}
@@ -118,15 +117,41 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/sewerage-service")) {
const queryParams = searchParams.toString();
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : ""
}`;
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/bussiness-juso-service")) {
console.log("AdressKey ", AdressKey);
const params = new URLSearchParams({
currentPage: "1", // 원하는 페이지
countPerPage: "5", // 페이지당 결과 수
keyword: searchParams.get("keyword") || "", // 검색어(주소)
confmKey: AdressKey, // 인증키 인터넷용 패쇄망용은 따로 발급 필요 즉 middleware에서 치환해서 사용해야할듯
resultType: "json", // 결과 형식 기본은 xml
});
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}`
);
return NextResponse.rewrite(new URL(newUrl));
}
return NextResponse.next();
}
export const config = {
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*", "/"],
matcher: [
"/server/:path*",
"/user-service/:path*",
"/attach-service/:path*",
"/geoserver/:path*",
"/sewerage-service/:path*",
"/bussiness-juso-service/:path*",
"/",
],
};
@@ -12,8 +12,8 @@ import RdiiChart from './chart/rdiiChart';
import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { codeMapper } from "../common/codeMapper";
import { searchUpperPpls } from '@common/business_components/ui/map/Gis';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { searchUpperPpls } from "./smh";
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
@@ -58,7 +58,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const getSelectedLayers = () => {
let result = null;
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수
let featureLayer: any = null; // 피처가 속한 레이어를 찾기 위한 변수
layers.some((layer: any) => {
const source = layer.getSource();
// if (source && featureData && featureData.fid) {
@@ -77,98 +77,119 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
setIsClicked(true);
console.log('gis data', featureData.values_);
const lyrTyNm = featureData.id_.split('.')[0];
if (lyrTyNm === 'tb_ms_swage_ppl') {
result = '하수관로';
setHeadlist(MsSwagePplCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'tb_ms_swage_mhl') {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'tb_ms_swage_pmstn') { // 하수펌프장
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol);
switch (lyrTyNm) {
case 'tb_ms_swage_ppl': {
result = '하수관로';
setHeadlist(MsSwagePplCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
else {
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol);
case 'tb_ms_swage_mhl': {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
case 'tb_ms_swage_pmstn': {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol);
}
else {
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol);
}
setLayerSpecificNm(featureData.values_.fclt_mng_no);
result = '하수펌프장';
break;
}
case 'tb_il_swage_prcs_fclt': {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
break;
}
case 'tb_ms_swage_prcs_zone': {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
break;
}
case 'tb_ms_stofcbr': {
result = '우수토실';
setHeadlist(stofcbrCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
break;
}
case 'om_dt_untrt_swg_rain': {
result = '중점우수토실';
setHeadlist(untrtSwgRainCol1);
setHeadlist2(untrtSwgRainCol2);
setLayerSpecificNm(featureData.values_.manage_no);
break;
}
default: {
result = featureLayer ? featureLayer.get('lyrNm') : null;
break;
}
setLayerSpecificNm(featureData.values_.fclt_mng_no);
result = '하수펌프장';
}
else if (lyrTyNm === 'tb_il_swage_prcs_fclt') {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
}
else if (lyrTyNm === 'tb_ms_swage_prcs_zone') {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
}
else if (lyrTyNm === 'tb_ms_stofcbr') {
result = '우수토실';
setHeadlist(stofcbrCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'om_dt_untrt_swg_rain') {
result = '중점우수토실';
setHeadlist(untrtSwgRainCol1);
setHeadlist2(untrtSwgRainCol2);
setLayerSpecificNm(featureData.values_.manage_no);
}
else result = featureLayer.get('lyrNm');
}
else {
// SearchModal 클릭 이벤트
setIsClicked(false);
console.log('SearchModal data', featureData);
const lyrTyNm = featureData.lyrTypeNm;
if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') {
result = '하수관로';
setHeadlist(MsSwagePplCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol2)
setLayerSpecificNm(featureData.fcltMngNo);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') { // 하수펌프장
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) {
console.log('오수펌프장');
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol2)
switch (lyrTyNm) {
case 'tb_ms_swage_ppl': {
result = '하수관로';
setHeadlist(MsSwagePplCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
break;
}
else {
console.log('빗물펌프장');
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol2)
case 'tb_ms_swage_mhl': {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol2)
setLayerSpecificNm(featureData.fcltMngNo);
break;
}
case 'tb_ms_swage_pmstn': {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) {
console.log('오수펌프장');
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol2)
}
else {
console.log('빗물펌프장');
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol2)
}
setLayerSpecificNm(featureData.fcltMngNo);
result = '하수펌프장';
break;
}
case 'tb_il_swage_prcs_fclt': {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
break;
}
case 'tb_ms_swage_prcs_zone': {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol2);
setLayerSpecificNm(featureData.swtrmtFcltCd);
break;
}
case 'tb_ms_stofcbr': {
result = '우수토실';
setHeadlist(stofcbrCol2)
setLayerSpecificNm(featureData.fcltMngNo);
break;
}
setLayerSpecificNm(featureData.fcltMngNo);
result = '하수펌프장';
}
else if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol2);
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') {
result = '우수토실';
setHeadlist(stofcbrCol2)
setLayerSpecificNm(featureData.fcltMngNo);
}
// else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
}
return result;
@@ -424,7 +445,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
className="px-2 py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
onClick={() => {
setIsLoading(true);
searchUpperPpls(layerSpecificNm).finally(() => setIsLoading(false));
searchUpperPpls(gisMap, layerSpecificNm).finally(() => setIsLoading(false));
}}
>
상류관로 보기
@@ -1,6 +1,84 @@
// 하수맨홀
import { VerticalTable } from '@/components/ui/verticalTable';
import { useEffect, useState } from 'react';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data: any = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// JSON 파싱 시도
data = JSON.parse(text);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
} else {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
const select: any = GisApp.Module.select;
let foundFeature: any = null;
(data || []).forEach((item: any) => {
const pplMngNo = item.ppl_mng_no?.trim?.();
gisMap
.getLayers()
.getArray()
.filter((e: any) => e.get("lyrTyNm") == "tb_ms_swage_ppl")
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
for (const f of features) {
if (f.values_.fclt_mng_no?.trim?.() === pplMngNo) {
foundFeature = f;
break;
}
}
}
});
if (foundFeature) select?.selectMultiFeature?.(foundFeature, true);
});
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
const geometry = foundFeature?.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 14,
constrainResolution: true,
nearest: true,
});
}
}
const inputColumn = [
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
+1 -2
View File
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./searchModal/searchModal";
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
import { WaterDepth, WATER_DEPTH_LAYER_TYPES, ORDERED_LAYERS } from "./waterDepth/waterDepth";
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
// 타입 정의 추가
@@ -160,7 +160,6 @@ const Map = () => {
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
orderedLayers={ORDERED_LAYERS as any}
checkInterval={2500}
/>
</div>
@@ -149,7 +149,6 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
// if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠)
// filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null);
// }
console.log('report count:', filterReportData.length);
setTableCount(filterReportData.length || 0);
@@ -80,12 +80,9 @@ const SearchModal: React.FC<SearchModalProps> = ({
const [end2, setEnd2] = useState("");
const [var1, setVar1] = useState("");
const [var2, setVar2] = useState("");
const [var3, setVar3] = useState("");
const [var4, setVar4] = useState("");
// 시작, 종료
const [yearFilter, setYearFilter] = useState({ start: "", end: "" });
const { ctpv, sgg, BIZ_TY_CD, BIZ_SE_CD } = useStore();
const { ctpv, sgg, BIZ_TY_CD } = useStore();
// 오늘 날짜 구하기
@@ -128,8 +125,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
setEnd2("");
setVar1("");
setVar2("");
setVar3("");
setVar4("");
setCtpvCd("0");
setSggCd("0");
setYearFilter({
@@ -196,7 +191,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('api:', `${API_URL}/v1/fclts?${queryParams.toString()}`);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => {
@@ -231,7 +225,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
};
});
console.log("dataWithRowNumber: ", dataWithRowNumber)
setTableData(dataWithRowNumber);
setTableCount(res.data.count || []);
@@ -264,7 +257,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
console.log('운영정보 사업 데이터:', res.content);
const mappedContent = (res.content || []).map((item: any, index: number) => {
const formattedItem = formatDisplayData(item);
@@ -512,7 +504,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
console.log('운영정보 cellClick 데이터:', res.data);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
@@ -527,7 +518,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
console.log('운영정보 cellClick 데이터:', mappedData);
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
@@ -565,7 +555,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
console.log('운영정보 PageNation 데이터:', mappedData);
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
@@ -667,7 +656,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('res.data.data', res.data.data);
@@ -785,7 +773,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
console.log('excelData', excelData);
// 파일명 생성 (분석번호 기반)
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
@@ -929,8 +916,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
const apiStartTime = performance.now();
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const apiEndTime = performance.now();
console.log(`🚀 1. API 호출 시간: ${(apiEndTime - apiStartTime).toFixed(2)}ms`);
console.log('운영정보 결과 다운로드.data:', res.data);
const mappingStartTime = performance.now();
// 헤더 매핑 정의
@@ -974,13 +959,10 @@ const SearchModal: React.FC<SearchModalProps> = ({
};
});
const mappingEndTime = performance.now();
console.log(`🔄 2. 데이터 매핑 시간: ${(mappingEndTime - mappingStartTime).toFixed(2)}ms`);
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedData; // 배열로 감싸서 전달
console.log('excelData', excelData);
const excelStartTime = performance.now();
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
@@ -994,7 +976,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
headerMapping: headerMapping
});
const excelEndTime = performance.now();
console.log(`📊 3. Excel 다운로드 시간: ${(excelEndTime - excelStartTime).toFixed(2)}ms`);
setIsLoading(false);
} catch (error) {
@@ -1023,7 +1004,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
setIsLoading(true);
const row = detailTableData[0];
console.log('row', row);
const layerNm = row.lyrTypeNm;
const fids = detailTableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
@@ -1116,6 +1096,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
@@ -1147,6 +1128,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
curPage={curPage}
headButton={
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-sm font-medium green" size="sm" onClick={operationExcelDownload}>파일 다운로드</Button>
}
@@ -1173,6 +1155,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
onPageChange={setDetailCurPage}
resetPageNo={isResetDetailPage}
resetSelection={isResetDetailSelection}
curPage={detailCurPage}
headButton={
<div className='flex'>
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
@@ -7,16 +7,6 @@ export const WATER_DEPTH_LAYER_TYPES: string[] = [
'tb_ms_flood_dngr_map',
'tb_ms_sbmrg_dngr_map'
];
// 지정된 순서에 따른 lyr_sn 배열
export const ORDERED_LAYERS: string[] = [
'27', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도
'28', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도
'3', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도
'4', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도
'59', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도
'60', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도
'58' // 도시침수지도(tb_ms_flood_dngr_map) - 80년 빈도
];
interface WaterDepthProps {
onClose?: () => void;
@@ -0,0 +1,76 @@
import { TableHead, TableRow } from "@common/components/ui";
export type TableUpperProps = {
id: string;
title: string;
upName?: string;
upSequnce?: number;
edit?: boolean;
visible?: boolean;
rowSpan?: number;
colSpan?: number;
type?: string;
selectData?: string; // type 이 seelct 일경우 selectarray 정하기 위해
row?: number;
mergeCell?: boolean // 셀을 합칠경우
maxLength?: number
readonly?: boolean
};
type chHeadType = {
headNm: string;
text: string;
length: number;
rows?: number;
};
export const HeadMakeFloodPre = (
upHeadList: TableUpperProps[],
makeHeadNm: chHeadType[]
) => {
return (
<>
<TableRow>
{upHeadList
.filter((data) => data.visible != false)
.map((head: TableUpperProps) =>
!head.upSequnce ? (
<TableHead
className={`border border-gray-600 px-2 py-1 `}
rowSpan={2}
key={head.id}
>
{head.title}
</TableHead>
) : head.upSequnce === 1 ? (
<TableHead
className={`border border-gray-600 px-2 py-1 `}
key={head.id}
rowSpan={1}
colSpan={
makeHeadNm?.find((ch) => ch.headNm === head.upName)?.length
}
>
{makeHeadNm?.find((ch) => ch.headNm === head.upName)?.text}
</TableHead>
) : null
)}
</TableRow>
<TableRow>
{upHeadList
.filter((data) => data.visible != false)
.map((head: TableUpperProps) =>
makeHeadNm?.find((ch) => ch.headNm === head.upName) ? (
<TableHead
className={`border border-gray-600 px-2 py-1 `}
key={head.id}
>
{head.title}
</TableHead>
) : null
)}
</TableRow>
</>
);
};
@@ -0,0 +1,200 @@
import type { TableUpperProps } from "./headMakeFloodPre";
import { Input, TableCell, TableRow } from "@common/components/ui";
// 숫자입력시 , 삽입
const formatCommaNumber = (
value: number | string | undefined | null
): string => {
if (value === undefined || value === null || value === "") return "";
const num = Number(value);
// 숫자로 변환할 수 없는 값이 들어올 경우 빈 문자열을 반환하여 예상치 못한 "NaN" 출력을 방지합니다.
if (isNaN(num)) {
return "";
}
return num.toLocaleString();
};
// onchange 텍스트를 숫자로 and ,제거
const handleCommaNumberChange = (
e: React.ChangeEvent<HTMLInputElement>,
onChange: (value: number | "") => void
) => {
const raw = e.target.value.replace(/,/g, "");
if (!/^\d*$/.test(raw)) return;
onChange(raw === "" ? "" : Number(raw));
};
const handleChange = (
rowIndex: number,
field: any,
value: string,
setInputData: (data: any) => void
) => {
if (Number(value) < 0 || !Number(value)) {
value = "0";
}
setInputData((prevData: any) =>
prevData?.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...row,
[field]: Number(value.replace(/,/g, "")),
};
}
return row;
})
);
};
export const InputTableBody = (
chgRes: any[] | [],
headList: TableUpperProps[],
setInputData: (data: any[]) => void,
isEditable: boolean = true
) => {
// mergeCell 처리용
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
if (rowIndex > 0 && data[rowIndex - 1][key] && String(data[rowIndex][key]).trim() === "") {
return 0; // 이전과 같으면 셀을 그리지 않음
}
let span = 1;
for (let i = rowIndex + 1; i < data.length; i++) {
if (data[i][key] === data[rowIndex][key]) span++;
else break;
}
return span;
};
return (
<>
{chgRes.map((data, dataIndex) => {
// 소계/총합계 행 판별
const isSubTotal = data["용수명"] === "소계";
const isTotal = data["구분"] === 0;
const bgClass = isTotal
? "bg-[#d5e1eb] font-bold"
: isSubTotal
? "bg-[#e6eef5] font-semibold"
: "";
return (
<TableRow key={dataIndex + "row"}>
{headList.map((head, headIndex) => {
// console.log('head, head.id', head, head.id);
const isTextColumn = head.id === "__EMPTY_3";
const textAlignClass = isTextColumn
? "text-center"
: "text-right";
// // 총합계 행이면 "구분"에서 colSpan=2로 병합
// if (
// (data["구분"] === 0 || data["용수명"] === 0) &&
// head.id === "구분"
// ) {
// return (
// <TableCell
// key={head.id + headIndex}
// colSpan={2}
// className="font-bold text-center bg-[#d5e1eb]"
// >
// 총합계
// </TableCell>
// );
// }
// // 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음
// if (
// (data["구분"] === 0 || data["용수명"] === 0) &&
// head.id === "용수명"
// ) {
// return null;
// }
// // 총합계 행의 나머지 셀에도 색상 적용
// if (data["구분"] === 0 || data["용수명"] === 0) {
// return (
// <TableCell
// key={head.id + headIndex}
// className="bg-[#d5e1eb] font-bold text-right"
// >
// {Number(data[head.id])
// ? formatCommaNumber(data[head.id as keyof any])
// : (data[head.id as keyof any] ?? "")}
// </TableCell>
// );
// }
// mergeCell 처리
if (head.mergeCell === true) {
const rowSpan = getRowSpan(chgRes, head.id, dataIndex);
if (rowSpan === 0) return null;
return (
<TableCell
key={head.id + headIndex}
rowSpan={rowSpan}
className={`w-36 align-middle text-center whitespace-nowrap ${bgClass}`}
>
{Number(data[head.id])
? formatCommaNumber(data[head.id as keyof any])
: (data[head.id as keyof any] ?? "")}
</TableCell>
);
}
// 기본 셀
// return head.edit === true && data.edit !== false ? (
return isEditable && head.edit === true && data.edit !== false ? (
<TableCell
key={head.id + headIndex}
className={`w-36 ${bgClass}`}
>
<Input
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
className={`${textAlignClass} !text-xl h-10 w-full`}
onChange={(e) =>
handleCommaNumberChange(e, (val) =>
handleChange(
dataIndex,
head.id,
String(val),
setInputData
)
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
maxLength={12}
/>
</TableCell>
) : (
<TableCell
key={head.id + headIndex}
className={`w-36 ${bgClass}`}
>
<div
// className={`
// ${head.id !== "utztnUsgCd" ? "text-right" : "text-center"}
// !text-xl w-full h-10
// `}
className={`${textAlignClass} !text-xl w-full h-10`}
>
{Number(data[head.id])
? formatCommaNumber(data[head.id as keyof any])
: (data[head.id as keyof any] ?? "")}
</div>
</TableCell>
);
})}
</TableRow>
);
})}
{chgRes.length === 0 && (
<TableRow>
<TableCell
colSpan={headList.length}
className="h-24 text-center text-gray-500"
>
조회된 결과가 없습니다.
</TableCell>
</TableRow>
)}
</>
);
};
@@ -2,7 +2,10 @@
import { useState, useEffect } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { UITab, UiTable } from '@common/business_components/ui'
import { Table, TableBody, TableHeader } from "@common/components/ui";
import * as XLSX from 'xlsx';
import { HeadMakeFloodPre } from './headMakeFloodPre';
import { InputTableBody } from './inputTableBody';
const TABS = [
{ val: "management", text: "관리SHEET" },
@@ -19,7 +22,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "management");
const [managementTableHeader, setManagementTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더
const [managementTableData, setManagementTableData] = useState<any>([{}]) //테이블 데이터
const [resultTableHeader, setResultTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더
const [resultTableHeader, setResultTableHeader] = useState<{ id: string; title: string, mergeCell?: boolean }[]>([]) //테이블 헤더
const [resultTableData, setResultTableData] = useState<any>([{}]) //테이블 데이터
const [isLoading, setIsLoading] = useState(false) //로딩 상태
@@ -53,13 +56,12 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
// 관리 SHEET
const managementHeader = jsonDataBySheet['관리SHEET']?.[7] || [];
// 1번째~47번째, 74번째~80번째 데이터만 필터링
const filteredManagementHeader = Object.entries(managementHeader)
.filter(([key, value]) => {
const match = key.match(/^__EMPTY_(\d+)$/);
if (match) {
const index = parseInt(match[1] ?? '', 10);
return (index > 0 && index < 47) || (index > 73);
return (0 < index && index < 47) || (73 < index);
}
return false;
})
@@ -81,17 +83,20 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
// 운영관리현황 점검결과
const resultHeader = jsonDataBySheet['운영관리현황 점검결과']?.[2] || [];
const dynamicInspectionHeader = Object.entries(resultHeader)
const dynamicResultHeader = Object.entries(resultHeader)
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
title: String(value).replace(/\r\n/g, '\n'),
mergeCell: false,
...(key === '__EMPTY_3' && { mergeCell: true }) // __EMPTY_3인 경우 mergeCell: true 추가
}));
dynamicInspectionHeader.splice(1, 0, {
dynamicResultHeader.splice(1, 0, {
id: "__EMPTY_2",
title: ''
title: '',
mergeCell: false
});
setResultTableHeader(dynamicInspectionHeader);
console.log('resultTableHeader', dynamicInspectionHeader);
setResultTableHeader(dynamicResultHeader);
console.log('resultTableHeader', dynamicResultHeader);
const resultData = Array.isArray(jsonDataBySheet['운영관리현황 점검결과'])
? jsonDataBySheet['운영관리현황 점검결과'].slice(3)
@@ -225,7 +230,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
)}
{/* 운영관리현황 점검결과 탭 */}
{activeTab === 'Inspection results' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="h-[79vh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
@@ -238,6 +243,14 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
cellClick={undefined}
onPageChange={() => { }}
/>
{/* <Table variant="secondary">
<TableHeader className="font-bold">
{HeadMakeFloodPre(resultTableHeader, [])}
</TableHeader>
<TableBody>
{InputTableBody(resultTableData ?? [], resultTableHeader, setResultTableData, false)}
</TableBody>
</Table> */}
</div>
</div>
)}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
'use client'
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
import { searchData } from '@/components/lib/common/apiHelpers'
import { formatDate, handleDownload, yearOptions } from '@/components/lib/utils'
import { formatDate, handleDownload } from '@/components/lib/utils'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable, UiModal } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
@@ -14,6 +14,7 @@ import { AUTH_USER_ID } from "@/shared/config";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@common/components/config";
import { getUserInfo } from "@common/components/api";
import TableModal from './tableModal';
const Page = () => {
@@ -30,7 +31,8 @@ const Page = () => {
const [curPage, setCurPage] = useState(1)
const [totalData, setTotalData] = useState(0) //총 데이터 수
const [isLoading, setIsLoading] = useState(true) //로딩 상태
const [isModalOpen, setIsModalOpen] = useState(false) // 모달 상태
const [isModalOpen, setIsModalOpen] = useState(false) // 등록 모달 상태
const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
const [isResetPage, setIsResetPage] = useState<boolean>(false);
@@ -223,6 +225,7 @@ const Page = () => {
setFileNm(fileName);
setAtflId(id);
setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn);
setIsTableModalOpen(true);
}
}}
onPageChange={setCurPage}
@@ -245,6 +248,21 @@ const Page = () => {
/>
}
/>
{/* 엑셀 데이터 테이블 모달 */}
<UiModal
open={isTableModalOpen}
onOpenChange={setIsTableModalOpen}
bodyClass={`
h-[100dvh]
w-[90dvw]
max-w-[100dvw]
max-h-[100dvh]
overflow-hidden
z-[101]
`}
body={<TableModal fileNm={fileNm} atflId={atflId} />}
useDirectRender={true}
/>
</>
)
}
@@ -0,0 +1,465 @@
'use client'
import { useState, useEffect } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList';
const TABS = [
{ val: "1", text: "row data" },
{ val: "2", text: "1.정밀조사" },
{ val: "3", text: "2.정밀조사_정비사업연계" },
{ val: "4", text: " 3.정비사업" },
{ val: "5", text: "4.정비사업요약" },
{ val: "6", text: "4.정비사업최종" },
];
interface TableModalProps {
fileNm: string;
atflId: string;
}
const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "1");
const [tableHeader1, setTableHeader1] = useState<{ id: string; title: string }[]>([])
const [tableHeader2, setTableHeader2] = useState<{ id: string; title: string }[]>([])
const [tableHeader4, setTableHeader4] = useState<{ id: string; title: string }[]>([])
const [tableHeader5, setTableHeader5] = useState<{ id: string; title: string }[]>([])
const [tableData1, setTableData1] = useState<any>([{}])
const [tableData2, setTableData2] = useState<any>([{}])
const [tableData3, setTableData3] = useState<any>([{}])
const [tableData4, setTableData4] = useState<any>([{}])
const [tableData5, setTableData5] = useState<any>([{}])
const [tableData6, setTableData6] = useState<any>([{}])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
isChckFile();
}, []);
// 파일 다운로드
const isChckFile = async () => {
setIsLoading(true);
try {
if (fileNm === '' || fileNm === undefined) {
alert('파일을 선택해주세요');
setIsLoading(false);
return false;
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const targetSheets = ['row data', '1.정밀조사', '2.정밀조사_정비사업연계', ' 3.정비사업', '4.정비사업요약', '4.정비사업최종'];
const jsonDataBySheet = await filePickedSelectedSheets(file, 0, targetSheets);
const header1 = jsonDataBySheet['row data']?.[1] || [];
const header2 = jsonDataBySheet['1.정밀조사']?.[1] || [];
const header4 = jsonDataBySheet[' 3.정비사업']?.[1] || [];
const header5 = jsonDataBySheet['4.정비사업요약']?.[2] || [];
// row data
const allEntries1 = Object.entries(header1);
const filteredHeader1 = [
allEntries1[0],
allEntries1[1],
allEntries1[2],
allEntries1[3],
allEntries1[5],
allEntries1[6],
allEntries1[7],
allEntries1[8],
allEntries1[9],
allEntries1[10],
allEntries1[11],
allEntries1[12],
allEntries1[13],
allEntries1[14],
allEntries1[15]
].filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader1.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader1(filteredHeader1);
const data1 = Array.isArray(jsonDataBySheet['row data'])
? jsonDataBySheet['row data'].slice(2)
: [];
const filteredData1 = filterEmptyRows(data1).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
__EMPTY_1: item.__EMPTY_1 || '-'
}));
setTableData1(filteredData1);
// 1.정밀조사
const allEntries2 = Object.entries(header2);
let filteredHeader2 = [
allEntries2[0],
allEntries2[1],
allEntries2[2],
allEntries2[3],
allEntries2[5],
allEntries2[6],
allEntries2[7],
allEntries2[8],
allEntries2[9],
allEntries2[10],
allEntries2[11],
allEntries2[12],
allEntries2[13],
allEntries2[14],
allEntries2[15]
].filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader2.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader2(filteredHeader2);
const data2 = Array.isArray(jsonDataBySheet['1.정밀조사'])
? jsonDataBySheet['1.정밀조사'].slice(2)
: [];
const filteredData2 = filterEmptyRows(data2).map((item: any, index: number) => ({
...item,
rowNumber: index + 1
}));
setTableData2(filteredData2);
// 2.정밀조사_정비사업연계 (헤더는 import)
const data3 = Array.isArray(jsonDataBySheet['2.정밀조사_정비사업연계'])
? jsonDataBySheet['2.정밀조사_정비사업연계'].slice(5)
: [];
const filteredData3 = filterEmptyRows(data3).map((item: any, index: number) => {
const newItem = { ...item };
// 매핑 에러로 인해 특정 키를 "__EMPTY_0"으로 변경
const targetKey = "*1차 출처: 공단자료\r\n*2차 출처: 정산서, 정밀조사보고서 등.\r\n (파랑) 2024년말 기준 실적 제출 자료. 준공 후 재확인 필요\r\n (노랑셀) 준공(또는 정산) 후에 재확인 필요";
if (newItem[targetKey] !== undefined) {
newItem["__EMPTY_0"] = newItem[targetKey];
delete newItem[targetKey];
}
return {
...newItem,
__EMPTY_4: newItem?.__EMPTY_4 || '-',
rowNumber: index + 1
};
});
setTableData3(filteredData3);
// 3.정비사업
const allEntries4 = Object.entries(header4);
const filteredHeader4 = allEntries4.filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader4.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader4(filteredHeader4);
const data4 = Array.isArray(jsonDataBySheet[' 3.정비사업'])
? jsonDataBySheet[' 3.정비사업'].slice(2)
: [];
const filteredData4 = filterEmptyRows(data4).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
__EMPTY_2: item?.__EMPTY_2 || '-',
__EMPTY_11: item?.__EMPTY_11 || '-',
}));
setTableData4(filteredData4);
// 4.정비사업요약
const allEntries5 = Object.entries(header5);
const excludeKeys5 = [
": \"정비사업\"sheet에서 사업코드 ctrl+c, Ctrl+v 한 다음 중복된 데이터 제거실시",
"__EMPTY",
"__EMPTY_8",
"__EMPTY_15"
];
const filteredHeader5 = allEntries5
.filter(([key]) => !excludeKeys5.includes(key))
.filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n'),
}));
filteredHeader5.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader5(filteredHeader5);
const data5 = Array.isArray(jsonDataBySheet['4.정비사업요약'])
? jsonDataBySheet['4.정비사업요약'].slice(3)
: [];
const filteredData5 =
filterEmptyRows(data5).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
}));
setTableData5(filteredData5);
// 4.정비사업최종 (헤더는 import)
const data6 = Array.isArray(jsonDataBySheet['4.정비사업최종'])
? jsonDataBySheet['4.정비사업최종'].slice(6)
: [];
const filteredData6 =
filterEmptyRows(data6).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
}));
setTableData6(filteredData6);
} catch (error) {
console.error('파일 처리 중 오류 발생:', error);
alert('파일 처리 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
function filterEmptyRows(data: any[]) {
return data
.filter(row => {
return Object.entries(row).some(([key, value]) => {
const match = key.match(/^__EMPTY_(\d+)$/);
if (!match || parseInt(match[1] ?? '', 10) < 2) return false;
return value !== undefined &&
value !== null &&
String(value).trim() !== '' &&
String(value).trim() !== '-' &&
String(value).trim() !== '';
});
})
.map((row) => {
const processedRow = { ...row };
const keys = Object.keys(processedRow);
keys.forEach((key, keyIndex) => {
// 마지막 속성(사업 근거)이 아닌 경우에만 빈 값을 '-'으로 변경
if (keyIndex !== keys.length - 1) {
const value = processedRow[key];
if (value === undefined ||
value === null ||
value === '' ||
String(value).trim() === '') {
processedRow[key] = '-';
}
}
});
return processedRow;
});
};
// 특정 시트만 선택적으로 JSON으로 변환하는 함수
function filePickedSelectedSheets(file: File, headerNum: number = 0, sheetNames: string[]): Promise<Record<string, any[]>> {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = function (e) {
const data = e.target?.result;
if (!data) {
console.error("Failed to read file: e.target.result is null");
reject(new Error("Failed to read file"));
return;
}
// Excel 파일 읽기
const wb = XLSX.read(data, { type: 'binary', cellDates: true });
const result: Record<string, any[]> = {};
// 지정된 시트 이름만 찾아서 처리
sheetNames.forEach(targetSheetName => {
// 대소문자 구분 없이 시트 이름 찾기
const sheetName = wb.SheetNames.find(name =>
name.toLowerCase() === targetSheetName.toLowerCase()
);
if (sheetName) {
const sheet = wb.Sheets[sheetName];
if (sheet) {
const jsonData = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw: false });
result[targetSheetName] = jsonData;
}
} else {
console.warn(`Sheet "${targetSheetName}" not found in workbook`);
result[targetSheetName] = [];
}
});
resolve(result);
};
reader.onerror = function () {
console.error("Error reading file");
reject(new Error("Error reading file"));
};
// 파일 읽기 시작
reader.readAsBinaryString(file);
});
};
return (
<>
{/* 탭 영역 */}
<UITab
defaultValue={activeTab}
tabList={TABS.map((tab) => ({
value: tab.val,
label: tab.text
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
{/* row data 탭 */}
{activeTab === '1' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[150%]">
<UiTable
headName={'row data'}
tableData={tableData1}
headlist={tableHeader1}
pageSize={10}
total={0}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 1.정밀조사 탭 */}
{activeTab === '2' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[150%]">
<UiTable
headName={'1.정밀조사'}
tableData={tableData2}
headlist={tableHeader2}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 2.정밀조사_정비사업연계 탭 */}
{activeTab === '3' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[550%]">
<UiTable
headName={'2.정밀조사_정비사업연계'}
tableData={tableData3}
headlist={oldPpTable2HeadList}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
>
<HeadMakeColSpan upHeadList={oldPpTable2NodeList} />
</UiTable>
</div>
</div>
</div>
)}
{/* 3.정비사업 탭 */}
{activeTab === '4' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[160%]">
<UiTable
headName={'3.정비사업'}
tableData={tableData4}
headlist={tableHeader4}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 4.정비사업요약 탭 */}
{activeTab === '5' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[130%]">
<UiTable
headName={'4.정비사업요약'}
tableData={tableData5}
headlist={tableHeader5}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 4.정비사업최종 탭 */}
{activeTab === '6' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[200%]">
<UiTable
headName={'4.정비사업최종'}
tableData={tableData6}
headlist={oldPpTable6HeadList}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
>
<HeadMakeColSpan upHeadList={oldPpTable6NodeList} />
</UiTable>
</div>
</div>
</div>
)}
</>
)
};
export default TableModal;
+2
View File
@@ -27,3 +27,5 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
+6
View File
@@ -16,6 +16,12 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+3
View File
@@ -16,6 +16,9 @@ NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BE
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
+60 -60
View File
@@ -52,7 +52,7 @@ export const ruseBizMthdCd: SelectDataType[] = [
{ val: "자체", text: "자체" },
];
//공통 건축물 용도
// 공통 건축물 용도
export const bdstUsgCd: SelectDataType[] = [
{ val: "-1", text: "확인불가" },
{ val: "1", text: "운동장" },
@@ -71,67 +71,67 @@ export const bdstUsgCd: SelectDataType[] = [
{ val: "14", text: "골프장" },
{ val: "15", text: "대규모점포" },
{ val: "16", text: "기타" },
{ text: "숙박업", val: "17" },
{ text: "목욕장업", val: "18" },
{ text: "공장", val: "19" },
{ text: "발전시설", val: "20" },
{ text: "관광단지개발사업", val: "21" },
{ text: "도시개발사업", val: "22" },
{ text: "산업단지개발사업", val: "23" },
{ text: "택지개발사업", val: "24" },
{ text: "대규모점포(대형마트)", val: "25" },
{ text: "대규모점포(전문점)", val: "26" },
{ text: "대규모점포(백화점)", val: "27" },
{ text: "대규모점포(쇼핑센터)", val: "28" },
{ text: "대규모점포(복합쇼핑몰)", val: "29" },
{ text: "물류시설", val: "30" },
{ text: "운수시설(여객자동차터미널)", val: "31" },
{ text: "운수시설(철도시설)", val: "32" },
{ text: "운수시설(공항시설)", val: "33" },
{ text: "운수시설(항만시설)", val: "34" },
{ text: "업무시설(공공업무시설)", val: "35" },
{ text: "업무시설(금융업소)", val: "36" },
{ text: "업무시설(사무소)", val: "37" },
{ text: "업무시설(결혼상담소 등 소개업소)", val: "38" },
{ text: "업무시설(출판사)", val: "39" },
{ text: "업무시설(신문사)", val: "40" },
{ text: "업무시설(오피스텔)", val: "41" },
{ text: "교정시설", val: "42" },
{ text: "방송국", val: "43" },
{ text: "전신전화국", val: "44" },
{ text: "기타", val: "45" },
{ val: "17", text: "숙박업" },
{ val: "18", text: "목욕장업" },
{ val: "19", text: "공장" },
{ val: "20", text: "발전시설" },
{ val: "21", text: "관광단지개발사업" },
{ val: "22", text: "도시개발사업" },
{ val: "23", text: "산업단지개발사업" },
{ val: "24", text: "택지개발사업" },
{ val: "25", text: "대규모점포(대형마트)" },
{ val: "26", text: "대규모점포(전문점)" },
{ val: "27", text: "대규모점포(백화점)" },
{ val: "28", text: "대규모점포(쇼핑센터)" },
{ val: "29", text: "대규모점포(복합쇼핑몰)" },
{ val: "30", text: "물류시설" },
{ val: "31", text: "운수시설(여객자동차터미널)" },
{ val: "32", text: "운수시설(철도시설)" },
{ val: "33", text: "운수시설(공항시설)" },
{ val: "34", text: "운수시설(항만시설)" },
{ val: "35", text: "업무시설(공공업무시설)" },
{ val: "36", text: "업무시설(금융업소)" },
{ val: "37", text: "업무시설(사무소)" },
{ val: "38", text: "업무시설(결혼상담소 등 소개업소)" },
{ val: "39", text: "업무시설(출판사)" },
{ val: "40", text: "업무시설(신문사)" },
{ val: "41", text: "업무시설(오피스텔)" },
{ val: "42", text: "교정시설" },
{ val: "43", text: "방송국" },
{ val: "44", text: "전신전화국" },
{ val: "45", text: "기타" },
];
export const rswtBdstType: SelectDataType[] = [
{ text: "숙박업", val: "17" },
{ text: "목욕장업", val: "18" },
{ text: "공장", val: "19" },
{ text: "발전시설", val: "20" },
{ text: "관광단지개발사업", val: "21" },
{ text: "도시개발사업", val: "22" },
{ text: "산업단지개발사업", val: "23" },
{ text: "택지개발사업", val: "24" },
{ text: "대규모점포(대형마트)", val: "25" },
{ text: "대규모점포(전문점)", val: "26" },
{ text: "대규모점포(백화점)", val: "27" },
{ text: "대규모점포(쇼핑센터)", val: "28" },
{ text: "대규모점포(복합쇼핑몰)", val: "29" },
{ text: "물류시설", val: "30" },
{ text: "운수시설(여객자동차터미널)", val: "31" },
{ text: "운수시설(철도시설)", val: "32" },
{ text: "운수시설(공항시설)", val: "33" },
{ text: "운수시설(항만시설)", val: "34" },
{ text: "업무시설(공공업무시설)", val: "35" },
{ text: "업무시설(금융업소)", val: "36" },
{ text: "업무시설(사무소)", val: "37" },
{ text: "업무시설(결혼상담소 등 소개업소)", val: "38" },
{ text: "업무시설(출판사)", val: "39" },
{ text: "업무시설(신문사)", val: "40" },
{ text: "업무시설(오피스텔)", val: "41" },
{ text: "교정시설", val: "42" },
{ text: "방송국", val: "43" },
{ text: "전신전화국", val: "44" },
{ text: "기타", val: "45" },
{ val: "17", text: "숙박업" },
{ val: "18", text: "목욕장업" },
{ val: "19", text: "공장" },
{ val: "20", text: "발전시설" },
{ val: "21", text: "관광단지개발사업" },
{ val: "22", text: "도시개발사업" },
{ val: "23", text: "산업단지개발사업" },
{ val: "24", text: "택지개발사업" },
{ val: "25", text: "대규모점포(대형마트)" },
{ val: "26", text: "대규모점포(전문점)" },
{ val: "27", text: "대규모점포(백화점)" },
{ val: "28", text: "대규모점포(쇼핑센터)" },
{ val: "29", text: "대규모점포(복합쇼핑몰)" },
{ val: "30", text: "물류시설" },
{ val: "31", text: "운수시설(여객자동차터미널)" },
{ val: "32", text: "운수시설(철도시설)" },
{ val: "33", text: "운수시설(공항시설)" },
{ val: "34", text: "운수시설(항만시설)" },
{ val: "35", text: "업무시설(공공업무시설)" },
{ val: "36", text: "업무시설(금융업소)" },
{ val: "37", text: "업무시설(사무소)" },
{ val: "38", text: "업무시설(결혼상담소 등 소개업소)" },
{ val: "39", text: "업무시설(출판사)" },
{ val: "40", text: "업무시설(신문사)" },
{ val: "41", text: "업무시설(오피스텔)" },
{ val: "42", text: "교정시설" },
{ val: "43", text: "방송국" },
{ val: "44", text: "전신전화국" },
{ val: "45", text: "기타" },
];
export const orgwtKndCd = [
@@ -186,6 +186,7 @@ export const pplLenght = [
];
export const rainFcltCd = [
{ val: "10", text: "확인불가" },
{ val: "01", text: "지붕" },
{ val: "02", text: "건물옥상" },
{ val: "03", text: "골프장 연못" },
@@ -195,7 +196,6 @@ export const rainFcltCd = [
{ val: "07", text: "태양광패널" },
{ val: "08", text: "연못" },
{ val: "09", text: "기타" },
{ val: "10", text: "확인불가" },
];
@@ -17,15 +17,15 @@ const hdtflObjSchema = z.object({
.string()
.min(1, { message: validationMessages.required("에너지원 종류") }),
fclCpct: z
.number()
.positive({ message: validationMessages.required("시설 용량") }),
.number().nullish(),
//.positive({ message: validationMessages.required("시설 용량") }),
plwtYn: z.enum(["Y", "N"]).default("Y"),
orgwtWtritSpt: z
.string()
.min(1, { message: validationMessages.required("원수 취수 지점") }),
.string().nullish(),
// .min(1, { message: validationMessages.required("원수 취수 지점") }),
orgwtOcrnProcs: z
.string()
.min(1, { message: validationMessages.required("원수 발생 공정") }),
.string().nullish(),
// .min(1, { message: validationMessages.required("원수 발생 공정") }),
wtritEntrcTp: z
.number()
.max(100, { message: "온도는 100이상이 될 수 없습니다." })
@@ -33,12 +33,12 @@ const hdtflObjSchema = z.object({
amtgnr: z.number().default(0),
prcsProcsYn: z.enum(["Y", "N"]).default("Y"),
prcsFcltCpct: z
.number()
.positive({ message: validationMessages.required("처리시설 용량") }),
.number().nullish(),
// .positive({ message: validationMessages.required("처리시설 용량") }),
fmYn: z.string().default(""),
utztnCpct: z
.number()
.positive({ message: validationMessages.required("이용 용량") }),
.number().nullish(),
// .positive({ message: validationMessages.required("이용 용량") }),
utztnUsg: z
.string()
.min(1, { message: validationMessages.required("이용 용도") }),
@@ -19,14 +19,9 @@ export const baseReuseBuildingSchema = z.object({
instlCstSprtYn: z.string().default("Y"),
instlCstSprtAmt: z.number().nullish(),
grntchBdstCertYn: z.string().default("Y"),
grntchBdstCertYr: z
.string()
.max(4, { message: validationMessages.yearFormat })
.refine(
(year) => year.length === 0 || /^\d{4}$/.test(year),
{
message: validationMessages.yearFormat,
}
grntchBdstCertYr: z.string().refine(
(val) => val === "" || /^\d{4}$/.test(val),
{ message: "연도는 빈값 또는 4자리 숫자여야 합니다." }
),
grntchBdstCertNo: z.string().default(""),
grntchBdstCertGrd: z.string().default(""),
@@ -59,7 +54,7 @@ export const chgBuildingSchema = z.object({
export const htdflReuseBuildingSchema = z.object({
bdstRuseFcltSn: z.number().nullish(),
bdstInfoSn: z.number().nullish(),
instlCmptnDt: z.coerce.date().default(() => new Date()),
//instlCmptnDt: z.coerce.date().default(() => new Date()),
instlCst: z.number().nullish(),
operMngCd: z.string().optional().nullish(),
anlOperCst: z.number().default(0),
@@ -14,7 +14,7 @@ const rswtrObjSchema = z.object({
//wtrUsqty: z.number({ required_error: validationMessages.required("물사용량(m³)") }).positive({ message: validationMessages.required("물사용량(m³)") }),
//oblgTrgtWtrUsqty: z.number().nullish().default(0),
orgwtKndCd: z.string({ required_error: validationMessages.requiredSelect("원수 종류") }).min(1, { message: validationMessages.requiredSelect("원수 종류") }),
filtrTrtmfctCpct: z.number({ required_error: validationMessages.required("처리시설 용량") }),
filtrTrtmfctCpct: z.number().nullish(),
filtrTrtmfctProcs: z.string({ required_error: validationMessages.required("주요처리공정") }).min(1, { message: validationMessages.required("주요처리공정") }),
filtrTrtmfctUsgCd: z.string().default("").nullish(),
rswtrUsqty: z.number().default(0),
@@ -21,7 +21,7 @@ export default function BdstReg({ form, type }: { form: any; type?: string }) {
const { userSiSggCd } = code;
const [useCityCd, setUserCityCd] = useState<SelectDataType[]>([]);
const [useSggCd, setUserSggCd] = useState<SelectDataType[]>([]);
const [initDaddr, setInitDaddr] = useState<string>();
useEffect(() => {
if (userSiSggCd.length === 2 && code.cityCd.length > 0) {
@@ -56,6 +56,7 @@ export default function BdstReg({ form, type }: { form: any; type?: string }) {
form.setValue("ctpvCd", Number(city[0]?.val));
form.setValue("sggCd", Number(sgg[0]?.val));
}
}, [form, userSiSggCd, code.cityCd, code.sggCd]);
@@ -67,9 +68,16 @@ export default function BdstReg({ form, type }: { form: any; type?: string }) {
return () => subscription?.unsubscribe();
}, [form.form]);
// useEffect(() => {
// console.log("form ", form.watch());
// }, [form.formState]);
useEffect(() => {
console.log("form.formState ", form.watch("archArea"));
}, [form.formState]);
const bdstInfoSn = form.getValues("bdstInfoSn");
if(bdstInfoSn&&bdstInfoSn>0)
setInitDaddr(form.getValues("daddr"));
}, [form.getValues("bdstInfoSn")]);
return (
<>
@@ -166,7 +174,7 @@ export default function BdstReg({ form, type }: { form: any; type?: string }) {
/> */}
<div>
<SearchInputAddress
initValue={undefined}
initValue={initDaddr}
setRtnData={(rtnData: addressType) => {
console.log("rtnData", rtnData);
field.onChange(rtnData.roadAddrPart1);
@@ -78,6 +78,10 @@ export default function HtdflRegForm({
};
const cleanHtdflData = (raw: any): z.infer<typeof hdtflZod> => {
const buildings =
raw?.reuseBuilding && raw.reuseBuilding.length > 0
? raw.reuseBuilding
: [{}];
return {
...raw,
frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null),
@@ -92,7 +96,7 @@ export default function HtdflRegForm({
: new Date(item?.archChgPrmsnDt),
bdstChgNm: item?.bdstChgNm ?? "", // null → ""
})),
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
reuseBuilding:buildings.map((item: any) => ({
...item,
instlCmptnDt: new Date(item?.instlCmptnDt),
htdflObj: {
@@ -84,13 +84,14 @@ export default function RainRegForm({
typeof item?.grntchBdstCertYr === "string"
? item?.grntchBdstCertYr
: "", // null → ""
bdstUsgCd: item?.bdstUsgCd ?? Number("-1"), // null → "-1"
instlCstSprtYn: item?.instlCstSprtYn ?? "Y", // null → "Y"
grntchBdstCertYn: item?.grntchBdstCertYn ?? "Y",
grntchBdstCertYn: item?.grntchBdstCertYn ?? "D",
rainyObj: {
...item?.rainyObj,
pntfColKnd:item.rainyObj?.pntfColKnd??'확인불가',
pntfColKnd:item.rainyObj?.pntfColKnd??rainFcltCd[0]?.val??'',
filtrTrtmfctPrs:item.rainyObj?.filtrTrtmfctPrs??'',
filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn ?? "Y",
filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn ?? "D",
},
})),
};
@@ -98,7 +99,7 @@ export default function RainRegForm({
useEffect(() => {
if (grntchBdstCertYn === "N") {
form.setValue("reuseBuilding.0.grntchBdstCertYr", "");
form.setValue("reuseBuilding.0.grntchBdstCertYr", '');
form.setValue("reuseBuilding.0.grntchBdstCertNo", "");
form.setValue("reuseBuilding.0.grntchBdstCertGrd", "");
} else if (instlCstSprtYn === "N") {
@@ -298,7 +299,10 @@ export default function RainRegForm({
selectClass={selectClass}
label={""}
selectArray={rainFcltCd}
onSelectValue={readonly ? () => { } : field.onChange}
onSelectValue={ (item) =>{
if(readonly) return;
field.onChange(item)
} }
value={field.value ?? rainFcltCd[0]?.val}
readOnly={readonly}
/>
@@ -521,8 +525,7 @@ export default function RainRegForm({
control={form.control}
name="reuseBuilding.0.grntchBdstCertYr"
render={({ field }) => {
const isUnknown =
field.value === null || field.value === "0000"; // 또는 Number(value) === -1
const isUnknown = field.value === null || field.value === "0000"; // 또는 Number(value) === -1
return (
<FormUi label="인증년도">
<Input
@@ -535,7 +538,7 @@ export default function RainRegForm({
field.onChange(""); // 초기화
}
}}
value={isUnknown ? "확인불가" : (field.value ?? "")}
value={isUnknown ? "확인불가" : (field.value ?? '9999')}
onChange={(e) => {
if (readonly) return;
if (e.target.value === "확인불가") {
@@ -83,8 +83,10 @@ export default function RswtrRegForm({
const filtrTrtmfctCpct = form.watch('reuseBuilding.0.rswtrObj.filtrTrtmfctCpct')
const cleanRswtData = (raw: any): z.infer<typeof rswtZod> => {
console.log('raw ', raw)
const buildings =
raw?.reuseBuilding && raw.reuseBuilding.length > 0
? raw.reuseBuilding
: [{}];
return {
...raw,
frstArchPrmsnDt: raw?.frstArchPrmsnDt
@@ -93,14 +95,15 @@ export default function RswtrRegForm({
archChgPrmsnDt: raw?.archChgPrmsnDt
? new Date(raw?.archChgPrmsnDt)
: null,
reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({
reuseBuilding:buildings.map((item: any) => ({
...item,
instlCmptnDt: item?.instlCmptnDt
? new Date(item.instlCmptnDt)
: new Date(), // item?.instlCmptnDt가 유효하지 않으면 현재 날짜로 초기화
operMngCd: item?.operMngCd || "", // null → ""
instlCstSprtYn: item?.instlCstSprtYn || code.yn[0]?.val, // null → "Y"
grntchBdstCertYn: item?.grntchBdstCertYn || code.yn[0]?.val,
instlCstSprtYn: item?.instlCstSprtYn || 'N', // null → "Y"
grntchBdstCertYn: item?.grntchBdstCertYn ||'N',
bdstUsgCd: item?.bdstUsgCd ?? Number("-1"), // null → "-1"
rswtrObj: {
...item?.rswtrObj,
filtrTrtmfctYn: item?.rainyObj?.filtrTrtmfctYn || code.yn[0]?.val,
@@ -145,6 +148,9 @@ export default function RswtrRegForm({
if (rswtZod) form.reset(cleanRswtData(rswtForm));
}, [rswtForm, form]);
return (
<div className="!mx-[1.6rem] !p-0r">
<Form {...form}>
@@ -291,7 +297,7 @@ export default function RswtrRegForm({
selectClass={selectClass}
selectArray={code.orgwtKndCd}
onSelectValue={readonly ? () => { } : field.onChange}
value={field.value || code.orgwtKndCd[0]?.val}
value={field.value ?? code.orgwtKndCd[0]?.val}
readOnly={readonly}
/>
</FormUi>
@@ -49,8 +49,8 @@ export function BdstReg<T extends MoType>({
const { userSiSggCd } = code;
const [bdstTable, setbdstTable] = useState<BdstTypeMap[T][]>();
const [rainBdstForm, setRainBdstForm] = useState<BdstTypeMap[T]>(
const [bdstTable, setBdstTable] = useState<BdstTypeMap[T][]>();
const [bdstForm, setBdstForm] = useState<BdstTypeMap[T]>(
{} as BdstTypeMap[T]
);
const [searchTxt, setSearchTxt] = useState<string>();
@@ -83,7 +83,7 @@ export function BdstReg<T extends MoType>({
archArea: archArea,
};
});
setbdstTable(tableData(bdstData, { code }));
setBdstTable(tableData(bdstData, { code }));
}
}, [bdst]);
@@ -118,13 +118,13 @@ export function BdstReg<T extends MoType>({
if (val !== undefined && bdst?.bdstBase.length > 0) {
const data: BdstTypeMap[T] = bdst?.bdstBase[val];
if (data) {
setRainBdstForm(data);
setBdstForm(data);
}
}
};
useEffect(() => {
setbdstTable([]); // 초기화 작업
setBdstTable([]); // 초기화 작업
}, []);
return (
<>
@@ -200,13 +200,13 @@ export function BdstReg<T extends MoType>({
<div>
{moType === "RNB" ? (
<RainRegForm
rainForm={rainBdstForm as customRainBdst}
rainForm={bdstForm as customRainBdst}
isModal={isModal}
setIsModal={setIsModal}
/>
) : moType === "RST" ? (
<RswtrRegForm
rswtForm={rainBdstForm as customRswtBdst}
rswtForm={bdstForm as customRswtBdst}
isModal={isModal}
setIsModal={(open?: boolean) => {
if (setIsModal && open) {
@@ -216,7 +216,7 @@ export function BdstReg<T extends MoType>({
></RswtrRegForm>
) : (
<HtdflRegForm
htdflForm={rainBdstForm as unknown as customHtdflBdst}
htdflForm={bdstForm as unknown as customHtdflBdst}
isModal={true}
setIsModal={(open?: boolean) => {
if (setIsModal && open) {
@@ -8,7 +8,7 @@ import {
} from "@/components/lib/func/commom";
import {
DatePicker,
// DatePicker,
FormField,
FormUi,
SelectBox,
@@ -24,6 +24,7 @@ import {
TableHeader,
TableRow,
} from "@common/components/ui";
import moment from "moment";
import React from "react";
type RuseMngTableProps = {
@@ -49,14 +50,17 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
form,
formNm,
indexBiz,
readonly= false
readonly = false,
},
ref
) => {
return (
<>
<Table key={`${formNm}-table-${indexMng}-${indexBiz}`} variant="secondary"
ref={ref}>
<Table
key={`${formNm}-table-${indexMng}-${indexBiz}`}
variant="secondary"
ref={ref}
>
<TableHeader className="font-bold">
{upHeadList ? (
headMakeprfmnc(headList, upHeadList)
@@ -76,20 +80,20 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
<TableBody>
{tableData.map((item, i) => (
<TableRow key={`${item.id }-${i}` } >
<TableRow key={`${item.id}-${i}`}>
{headList.map((head, j) => (
<TableCell
key={`${head.id}-${j}`}
className="text-center whitespace-nowrap"
onClick={() => { }}
onClick={() => {}}
>
<FormField
control={form.control}
name={
typeof indexMng === "number" &&
indexMng >= 0 &&
typeof indexBiz === "number" &&
indexBiz >= 0
indexMng >= 0 &&
typeof indexBiz === "number" &&
indexBiz >= 0
? `items.${indexMng}.fcBizs.${indexBiz}.${formNm}.${i}.${head.id}`
: typeof indexMng === "number" && indexMng >= 0
? `items.${indexMng}.${formNm}.${i}.${head.id}`
@@ -99,7 +103,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
<FormUi label="">
{head.edit === true ? (
<Input
className={`${head.type === "number" ? "text-right" : "text-center"} !text-xl`}
className={`${head.type === "number" ? "text-right" : "text-center"} text-gray-600`}
type="text"
onChange={(txt) => {
if (head.type === "number") {
@@ -113,8 +117,10 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
? formatCommaNumber(field.value)
: field.value || ""
}
readOnly={readonly|| head.readonly ? true : false}
maxLength={head.maxLength?head.maxLength:15}
readOnly={
readonly || head.readonly ? true : false
}
maxLength={head.maxLength ? head.maxLength : 15}
/>
) : head.type === "select" ? (
<SelectBox
@@ -134,23 +140,38 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
field.onChange(val);
}
}}
readOnly={ readonly||head.readonly ? true : false}
// Disabled prop removed as SelectBox does not support it
readOnly={
readonly || head.readonly ? true : false
}
// Disabled prop removed as SelectBox does not support it
></SelectBox>
) : head.type === "date" ? (
<DatePicker
mode="single"
field={{
value:
typeof field.value === "string"
? new Date(field.value)
: field.value,
onChange: (date: Date | undefined) => {
console.log("date", date);
field.onChange(date); // readonly 상태가 아닐 때만 값 변경
},
// <DatePicker
// mode="single"
// field={{
// value:
// typeof field.value === "string"
// ? new Date(field.value)
// : field.value,
// onChange: (date: Date | undefined) => {
// console.log("date", date);
// field.onChange(date); // readonly 상태가 아닐 때만 값 변경
// },
// }}
// selectClass={"w-full"}
// readOnly={readonly||head.readonly ? true : false}
// />
<Input
type="date"
onChange={(e) => {
// 브라우저에서 선택한 값은 "YYYY-MM-DD"
const date = new Date(e.target.value); // e.g. "2025-06-01"
field.onChange(date);
}}
selectClass={"w-full"}
value={
field.value? moment(field.value, "YYYYMMDD").format( "YYYY-MM-DD"): ""
}
className={"w-full text-gray-600"}
readOnly={readonly||head.readonly ? true : false}
/>
) : head.type === "checkbox" ? (
@@ -158,22 +179,19 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>(
className=""
checked={field.value || false}
onCheckedChange={(checked) => {
if(readonly || head.readonly) {
if (readonly || head.readonly) {
return;
}else{
} else {
field.onChange(checked);
}
}}
></Checkbox>
) : (
(
<span className="text-black">{
head.type === "number"
<span className="text-black">
{head.type === "number"
? formatCommaNumber(field.value)
: field.value || ""
}</span>
)
: field.value || ""}
</span>
)}
</FormUi>
)}
+21
View File
@@ -10,6 +10,8 @@ const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const REUSE_API_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL || "";
const OPERATION_API_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL || "";
const BUSINESS_JUSO_API_URL = process.env.NEXT_PUBLIC_FINDADRESS_URL || "";
const AdressKey = process.env.NEXT_PUBLIC_WR_ADDRESS_KEY || "";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
@@ -148,6 +150,24 @@ export function middleware(req: NextRequest) {
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/bussiness-juso-service")) {
console.log('AdressKey ', AdressKey)
const params = new URLSearchParams({
currentPage: "1", // 원하는 페이지
countPerPage: "5", // 페이지당 결과 수
keyword: searchParams.get("keyword") || "", // 검색어(주소)
confmKey: AdressKey, // 인증키 인터넷용 패쇄망용은 따로 발급 필요 즉 middleware에서 치환해서 사용해야할듯
resultType: "json", // 결과 형식 기본은 xml
});
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}`);
return NextResponse.rewrite(new URL(newUrl));
}
return NextResponse.next();
}
@@ -161,6 +181,7 @@ export const config = {
"/sewerage-service/:path*",
"/reuse-service/:path*",
"/operation-service/:path*",
"/bussiness-juso-service/:path*",
"/"
],
};
@@ -7,6 +7,7 @@ import {
} from "./lib/chart";
import type { ContentType } from "recharts/types/component/DefaultLegendContent";
import type { set } from "date-fns";
import React from "react";
// 툴크 커스텀 css
/** 차트 기본 스타일 */
@@ -126,6 +127,37 @@ export default function LineBarchart({
monthTicks = getTicks(chartData);
}
const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:string }> = ({ viewBox, fieldText, position }) => {
const { x, y, width, height } = viewBox; // 축 영역
const padding = 5; // 차트 바깥으로 띄우기
if('left' == position) {
return (
<text
x={x - padding} // 맨 좌측 바깥
y={y + height / 2} // 수직 중앙
textAnchor="middle"
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
>
{fieldText}
</text>
);
}else{
return (
<text
x={x + height / 2 } // 맨 좌측 바깥
y={y - width + padding } // 수직 중앙
textAnchor="middle"
transform={`rotate(90, ${x}, ${y})`}
className="text-[12px] font-bold"
>
{fieldText}
</text>
);
}
}
return (
<div className={chartClassName}>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
@@ -133,7 +165,7 @@ export default function LineBarchart({
{chartTitle}
</div>
) : null}
<div className="h-[calc(100%-42px)] flex justify-center">
<div className="h-[calc(100%-42px)] w-[calc(100%-40px)] ">
<ChartContainer config={chartConfig ?? {}} className="h-full w-full">
<ComposedChart
data={chartData}
@@ -151,18 +183,22 @@ export default function LineBarchart({
yAxisId="left"
className="text-[12px] font-bold"
tickCount={6}
orientation="left"
domain={[0, Math.ceil(lineMaxValue * 1)]}
label={{ value: yFieldText, angle: -90, position: 'insideLeft', offset: -8 }}
tickFormatter={(value) =>
typeof value === "number" ? value.toLocaleString() : value
}
orientation="left"
label={(props) => <CustomAxisText {...props} fieldText={yFieldText} position="left" />}
/>}
{/* 오른쪽 Y축: barVal */}
{yFieldText2 && <YAxis
yAxisId="right"
className="text-[12px] font-bold"
tickCount={6}
orientation="right"
domain={[0, Math.ceil(barMaxValue * 1)]}
label={{ value: yFieldText2, angle: -90, position: 'insideRight' }}
orientation="right"
tickFormatter={(value) =>
typeof value === "number" ? value.toLocaleString() : value
}
label={(props) => <CustomAxisText {...props} fieldText={yFieldText} position="right" />}
reversed={true}
/>}
<Tooltip content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />} />
@@ -65,11 +65,11 @@ const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?:
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
{payload.filter(item => item.value != null && String(item.value) != 'NaN' && item.value != undefined).map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(',','')}`}</span>
</div>
))}
</div>
@@ -129,6 +129,37 @@ const CustomLegend = (props: LegendProps) => {
);
};
const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:string }> = ({ viewBox, fieldText, position }) => {
const { x, y, width, height } = viewBox; // 축 영역
const padding = 5; // 차트 바깥으로 띄우기
if('left' == position) {
return (
<text
x={x - padding} // 맨 좌측 바깥
y={y + height / 2} // 수직 중앙
textAnchor="middle"
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
>
{fieldText}
</text>
);
}else{
return (
<text
x={x + height / 2 } // 맨 좌측 바깥
y={y - width + padding } // 수직 중앙
textAnchor="middle"
transform={`rotate(90, ${x}, ${y})`}
className="text-[12px] font-bold"
>
{fieldText}
</text>
);
}
}
type DataType = {label: string; val: string};
type ChartDataType = {
@@ -220,7 +251,10 @@ export default function Linechart({
yAxisId="left"
className="text-[12px] font-bold"
orientation="left"
label={{ value: yFieldText, className:"text-[12px] font-bold", angle: -90, position: 'insideLeft' }} />
tickFormatter={(value) =>
typeof value === "number" ? value.toLocaleString() : value
}
label={(props) => <CustomAxisText {...props} fieldText={yFieldText} position="left" />}/>
<Tooltip content={<CustomTooltip />} />
{showLegend && <Legend content={CustomLegend as ContentType}/>}
{Object.keys(chartData).map((key) => {
@@ -233,8 +267,10 @@ export default function Linechart({
type="linear"
strokeWidth={2}
dot={false}
connectNulls={true}
name={String(config?.label || "X축")}
stroke={config?.color}
stroke={config?.color?.includes('dash')?config?.color?.replace('dash',''):config?.color}
strokeDasharray={config?.color?.includes("dash")?"4 4":undefined}
/>
);
})}
@@ -30,75 +30,6 @@ export const GisContext = createContext({
checkedLayerIds: null,
});
export const useGisContext = () => useContext(GisContext);
export const searchUpperPpls = async (fcltMngNo) => {
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// JSON 파싱 시도
data = JSON.parse(text);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
}
}
const select = GisApp.Module.select;
const gisMap = GisApp.Module.core?.map;
let foundFeature = null;
data.forEach((item) => {
const pplMngNo = item.ppl_mng_no.trim();
gisMap
.getLayers()
.getArray()
.filter((e) => e.get("lyrTyNm") == "tb_ms_swage_ppl")
.forEach((layer) => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
for (const f of features) {
if (f.values_.fclt_mng_no.trim() === pplMngNo) {
foundFeature = f;
break;
}
}
}
});
if (foundFeature) select.selectMultiFeature(foundFeature, true);
});
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
let geometry = foundFeature?.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 14,
constrainResolution: true,
nearest: true,
});
}
};
function Gis({
isOpen, //우측 상세 레이어 팝업
@@ -121,7 +52,6 @@ function Gis({
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
height: height = "h-[calc(100vh-5rem)]",
onOpenPopup = (data) => { },
@@ -359,7 +289,6 @@ function Gis({
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
targetLayerTypes={targetLayerTypes}
orderedLayers={orderedLayers}
checkInterval={checkInterval}
onProgressChange={handleProgressChange}
/>
@@ -39,7 +39,6 @@ export default React.memo(function GisTree({
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
onProgressChange = () => { },
}) {
@@ -313,14 +312,15 @@ export default React.memo(function GisTree({
// orderedLayers 순서를 우선 적용하면서, 타입 필터에 맞는 타겟 노드만 선별
const getTargetNodesInOrder = () => {
const orderedNodes = [];
orderedLayers.forEach((lyrSn) => {
const node = nodes.find(
(n) => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm)
);
if (node) orderedNodes.push(node);
const target = nodes.filter((n) => n.data?.actlLyrYn === 'N' && layerTypes.includes(n.data?.lyrTyNm));
// drawOrdr 값을 기준으로 오름차순 정렬
target.sort((a, b) => {
const aOrder = a.data?.drawOrdr || 0;
const bOrder = b.data?.drawOrdr || 0;
return aOrder - bOrder;
});
return orderedNodes;
return target;
};
const targetNodes = getTargetNodesInOrder();
@@ -510,8 +510,8 @@ export default React.memo(function GisTree({
{/* 사이드 패널 */}
<div
className={`absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden border-r border-gray-300 bg-white transition-all duration-300 ${visible
? "translate-x-0 opacity-100"
: "pointer-events-none -translate-x-full opacity-0"
? "translate-x-0 opacity-100"
: "pointer-events-none -translate-x-full opacity-0"
}`}
>
<div
@@ -597,40 +597,63 @@ GisOlLayer.prototype = {
const tileSize = 256; // 기본 타일 크기
return Math.ceil(Math.log2(maxResolution / (cameraHeight / tileSize)));
},
ensureSearchLayer: function () {
ensureSearchLayer: function (option) {
let layer = this.searchLayer;
if (!layer) {
layer = new VectorLayer({
source: new VectorSource(),
let source;
try {
if (option.cmncNm == 'bbox') {
source = this.getVectorSourceBbox(option);
} else {
source = this.getVectorSourceAll(option);
}
} catch (error) {
console.warn(option);
}
layer = new VectorImageLayer({
...option,
id: 'search-layer',
source: source,
zIndex: 9999
});
layer.setStyle(this.gisStyle.getLayerStyleFunction(layer, option));
let map = GisApp.Module.core.map;
map.addLayer(layer);
this.searchLayer = layer;
}
return layer;
},
clearSearchLayer: function () {
const layer = this.ensureSearchLayer();
clearSearchLayer: function (option) {
const layer = this.ensureSearchLayer(option);
layer.getSource().clear(true);
if (this.searchLayer) {
GisApp.Module.core.map.removeLayer(this.searchLayer);
this.searchLayer = null;
}
},
showSearchResultsFromGeoJSON: function (lyrTyNm, featureId) {
const layer = this.ensureSearchLayer();
// 이전 검색결과는 깨끗이
const prev = layer.getSource();
if (prev) prev.clear(true);
//레이어 타입 설정(point, line 등. 이걸 style에서 사용)
const layerCode = GisApp.layerCode;
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
// 이전 검색 레이어 완전히 제거
this.clearSearchLayer(matched);
// 새로운 검색 레이어 생성
const layer = this.ensureSearchLayer(matched);
layer.set("gobjTy", matched.gobjTy);
// 새 소스 만들기
const source = this.getFeatureSourceFromGeoserver(lyrTyNm, featureId);
const source = this.getFeatureSourceFromGeoserver(lyrTyNm, featureId, matched);
// 1) 로드 완료시 extent 맞추기
const onLoadEnd = () => {
if (Object.keys(source.nullGeometryFeatures_).length > 0) alert('해당 시설의 위치 정보가 없습니다.');
const feats = source.getFeatures();
if (feats && feats.length > 0) {
const select = GisApp.Module.select;
@@ -665,12 +688,13 @@ GisOlLayer.prototype = {
* @param {*} featureId
* @returns
*/
getFeatureSourceFromGeoserver: function (lyrTyNm, featureId) {
getFeatureSourceFromGeoserver: function (lyrTyNm, featureId, option) {
const typeName = this.storageNm + lyrTyNm;
const srsName = GisApp.TO_PROJ;
const baseUrl = GisApp.GEOSERVER_URL;
const source = new VectorSource({
...option,
format: new GeoJSON(),
url: () => {
const params = new URLSearchParams({
@@ -695,13 +719,14 @@ GisOlLayer.prototype = {
* @param {Array|string} featureIds 피처 ID 배열 또는 단일 ID
*/
showMultipleSearchResultsFromGeoJSON: function (lyrTyNm, featureIds) {
const layer = this.ensureSearchLayer();
const prev = layer.getSource();
if (prev) prev.clear(true);
const layerCode = GisApp.layerCode;
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
// 이전 검색 레이어 완전히 제거
this.clearSearchLayer(matched);
// 새로운 검색 레이어 생성
const layer = this.ensureSearchLayer(matched);
layer.set("gobjTy", matched.gobjTy);
const source = this.getMultiFeatureSourceFromGeoserver(lyrTyNm, featureIds);
@@ -284,6 +284,7 @@ GisOlSelect.prototype = {
selectedFeatures.forEach(feature => {
selectedFeatures.remove(feature);
});
selectedFeatures.clear();
},
/**
* SELECT 이벤트 활성화
@@ -412,10 +412,10 @@ export function SdmRegForm({
// {...field}
// />
<SearchInputAddress
initValue={undefined}
initValue={updateList?.daddr || ""}
readonly={readOnly ??false}
setRtnData={(rtnData: addressType)=> {
field.onChange(rtnData.roadAddrPart1);
console.log("rtnData ", rtnData);
}}
/>
),
@@ -1,85 +1,88 @@
const addresssKey = 'devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM='; //인증키 만료일은 2025-11-23
// const Key = 'U01TX0FVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA'
// 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[];
results: {
common: {
errorMessage: string;
countPerPage: string;
totalCount: string;
errorCode: string;
currentPage: string;
};
}
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;
}
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[] 주소 배열
*/
/**
* 주소에 따른 검색
* @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 [];
}
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 [];
}
} catch (error) {
console.error('Error fetching address data:', error);
return [];
}
}
@@ -14,7 +14,7 @@ type thisProps = {
* @returns setRtnData => 선택된 주소 데이터 반환
*/
export default function SearchInputAddress({
initValue = undefined,
initValue ,
setRtnData = () => {},
readonly=false
}: thisProps) {
@@ -24,6 +24,13 @@ export default function SearchInputAddress({
const [searchList, setSearchList] = useState<addressType[]>([]);
useEffect(()=>{
if(initValue !== textValue && initValue !== undefined && initValue !== null && initValue !== ''){
setTextValue(initValue)
}
},[initValue])
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -30,6 +30,7 @@ type UiTableProps = {
boldArray?: number[];
resetSelection?: boolean;
index?: number;
curPage?: number;
headButton?: React.ReactNode | undefined;
};
@@ -54,9 +55,10 @@ export function UiTable({
boldArray = [],
resetSelection = false,
index = -1,
curPage = 1,
headButton = undefined,
}: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [cur, setCur] = useState<number>(curPage);
const [idx, setIdx] = useState<number>(index);
const [headerHeight, setHeaderHeight] = useState<number>(0);
const headerRef = useRef<HTMLTableSectionElement>(null);
@@ -84,6 +86,10 @@ export function UiTable({
setIdx(index);
}, [index])
useEffect(() => {
setCur(curPage);
}, [curPage])
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);