diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 88b6f8067..234d496d9 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -19,7 +19,7 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 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 # 박이정 -NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = / NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do diff --git a/apps/dm/.env.mindone b/apps/dm/.env.mindone index da6f80177..64b982cdc 100644 --- a/apps/dm/.env.mindone +++ b/apps/dm/.env.mindone @@ -17,7 +17,7 @@ NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 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 # 박이정 -NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = / NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do diff --git a/apps/dm_new/app/(landing)/reuse/apply/modal/SimilarCaseModal.tsx b/apps/dm_new/app/(landing)/reuse/apply/modal/SimilarCaseModal.tsx index f997d88e1..774ab81b8 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/modal/SimilarCaseModal.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/modal/SimilarCaseModal.tsx @@ -71,16 +71,18 @@ export default function SimilarCaseModal({ onClose, rows, detailsByCaseId, + onSearch }: { open: boolean; onClose: () => void; rows: SimilarCaseRow[]; detailsByCaseId: Record; + onSearch :(criterion: Criterion) => void; }) { const titleId = useId(); const [mounted, setMounted] = useState(false); - const [criterion, setCriterion] = useState(""); + const [criterion, setCriterion] = useState("supplyAmt"); const [selectedCaseId, setSelectedCaseId] = useState(""); useEffect(() => setMounted(true), []); @@ -88,7 +90,8 @@ export default function SimilarCaseModal({ // 모달 열릴 때 초기화 useEffect(() => { if (!open) return; - setCriterion(""); + setCriterion("supplyAmt"); + onSearch("supplyAmt"); setSelectedCaseId(""); }, [open]); @@ -192,12 +195,14 @@ export default function SimilarCaseModal({ const v = e.target.value as Criterion; setCriterion(v); setSelectedCaseId(""); + + if(v){ + onSearch(v); + } }} className="h-10 w-[180px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500" > - - diff --git a/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx index 2a9bbaf9a..9350f59f4 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx @@ -633,6 +633,9 @@ export default function DemandRegisterPage() { onClose={() => setOpenSimilar(false)} rows={similarRows} detailsByCaseId={similarDetailsByCaseId} + onSearch={async (criterion) => { + console.log(criterion); + }} /> ); diff --git a/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx index b69b0fdd9..3282f54a5 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import SubPageHeader from "../../_components/SubPageHeader"; +//import { useDmStore } from "@/app/store"; type WaterDetail = { toc: string; @@ -114,12 +115,17 @@ type PairField = { label: string; value: string }; export default function DemandStatusPage() { const rows = useMemo(() => MOCK, []); - // ✅ rows가 빈 경우도 대비 (런타임 오류 방지) + // rows가 빈 경우도 대비 (런타임 오류 방지) const [selectedId, setSelectedId] = useState(rows[0]?.id ?? null); - // ✅ 상세영역 스크롤 이동용 + // 상세영역 스크롤 이동용 const detailRef = useRef(null); + // update + /* const updateSdmInfoData = useDmStore( + (state) => state.updateSdmInfoData + ); */ + // ✅ selected 계산 안정화 const selected = useMemo(() => { if (!rows.length) return null; diff --git a/apps/dm_new/app/(popup-layout)/layout.tsx b/apps/dm_new/app/(popup-layout)/layout.tsx new file mode 100644 index 000000000..742519a66 --- /dev/null +++ b/apps/dm_new/app/(popup-layout)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; +import "../globals.css"; +import { StoreProvider } from "../store"; + +export default function PopupLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/apps/dm_new/app/(popup-layout)/map/popup/page.tsx b/apps/dm_new/app/(popup-layout)/map/popup/page.tsx new file mode 100644 index 000000000..1c625bcac --- /dev/null +++ b/apps/dm_new/app/(popup-layout)/map/popup/page.tsx @@ -0,0 +1,352 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState } from "react"; +import { mapService } from "../service"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { SearchBar } from "@/components/ui/map/searchBar/searchBar"; +import SelectedFcltList from "./selectedFcltList"; +import type { SubsInfo } from "@/app/store/map/type"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [selectDataPk, setSelectDataPk] = useState>({}); + const [selectedFclts, setSelectedFclts] = useState([]); + + //적용 버튼 클릭 이벤트 + const onApply = () => { + if (selectedFclts.length > 3) { + alert("재이용시설 후보지는 최대 3개까지만 선택할 수 있습니다."); + return; + } + + const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({ + ruseUtztnFcltNm: fclt.ruseUtztnFcltNm, + ruseUtztnFcltCd: fclt.pbnstRuseFcltId, + })); + + if (!window.opener || typeof window.opener.postMessage !== "function") { + alert("부모 창을 찾을 수 없습니다. 팝업이 'noopener'로 열렸거나, 보안 정책으로 차단되었을 수 있어요."); + return; + } + + window.opener.postMessage( + { + type: "SET_SUBS_INFO", + payload: subsInfoList, // 배열로 전송 (최대 3개) + }, + "*" + ); + + window.close(); + }; + + //트리 레이어 데이터 조회 + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + //하수처리시설코드로 재이용시설 목록 조회 + const reuseData = async (ruseFcltPk: any) => { + if (!ruseFcltPk) { + console.warn("[reuseData] ruseFcltPk가 없습니다."); + return; + } + try { + console.log("[reuseData] API 호출 - ruseFcltPk:", ruseFcltPk); + const response = await mapService.getPbnstRuseFclts(ruseFcltPk); + console.log("[reuseData] API 응답 전체:", response); + // 응답이 배열이면 바로 사용, 객체면 .data 추출 + const data = Array.isArray(response) + ? response + : Array.isArray(response?.data) + ? response.data + : response?.data + ? [response.data] + : []; + console.log("[reuseData] 파싱된 data:", data); + setSelectedFclts((prev) => { + const newItems = data.filter( + (item: any) => + item && + Object.keys(item).length > 0 && + !prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId) + ); + console.log("[reuseData] 추가될 newItems:", newItems); + return [...prev, ...newItems]; + }); + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // 부모창에 초기 선택값 요청 (팝업 열릴 때 기존 선택 목록 받아오기) + useEffect(() => { + const origin = window.location.origin; + if (window.opener) { + window.opener.postMessage({ type: "REQUEST_INIT_SUBS_INFO" }, origin); + } + }, []); + + // 부모창에서 초기 선택값 수신 + useEffect(() => { + const origin = window.location.origin; + + const handler = (event: MessageEvent) => { + if (event.origin !== origin) return; + + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "INIT_SUBS_INFO") { + const list = Array.isArray(data.payload) ? data.payload : []; + setSelectedFclts( + list.map((x) => ({ + ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "", + pbnstRuseFcltId: x.ruseUtztnFcltCd ?? "", + })) + ); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); + }, []); + + // 레이어 생성 후 초기화 처리 + const onCheckedLayersChange = () => { + //수요처 ID + const urlParams = new URLSearchParams(window.location.search); + const dmdInfoId = urlParams.get("dmdInfoId"); + console.log("[하이라이트] dmdInfoId:", dmdInfoId); + + //수요처 위치로 화면 이동 + const maps = gisMaps.current; + const layer = GisApp.Module.layer as any; + const layerNm = "tb_wr_ruse_fclt_dmd_co"; + const featureId = `${layerNm}.${dmdInfoId}`; + let foundFeature: any = null; + + // 현재 로드된 레이어 목록 출력 + const allLayerNames = maps + .getLayers() + .getArray() + .map((l: any) => l.get("lyrTyNm") || l.get("name") || "(unnamed)"); + console.log("[하이라이트] 전체 레이어 목록:", allLayerNames); + console.log("[하이라이트] 탐색 대상 레이어:", layerNm); + console.log("[하이라이트] 탐색 대상 featureId:", featureId); + + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + console.log("[하이라이트] 레이어 발견:", layerNm); + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + console.log("[하이라이트] feature 탐색결과:", feature); + if (feature) { + foundFeature = feature; + return; + } + // feature 못 찾을 때 소스 내 전체 feature ID 출력 + if (source?.getFeatures) { + const allIds = source.getFeatures().map((f: any) => f.getId()); + console.log("[하이라이트] 소스 내 전체 feature ID:", allIds); + } + } + } + }); + + if (!foundFeature) { + console.warn("[하이라이트] feature를 찾지 못했습니다. 위 레이어 목록/ID를 확인하세요."); + } + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + + if (geometry) { + const extent = geometry.getExtent(); + + if (geometry.getType() === "Point") { + maps.getView().setCenter(geometry.getCoordinates()); + maps.getView().setZoom(11); + } else { + maps.getView().fit(extent, { + duration: 300, + padding: [30, 30, 30, 30], + maxZoom: 16, + }); + } + } + + layer.highLightFeature( + [foundFeature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + } else { + console.warn("수요처의 좌표정보를 찾을 수 없습니다. dmdInfoId:", dmdInfoId); + } + + const formatNumber = (val: any) => { + const num = Number(val); + return isNaN(num) ? "-" : num.toLocaleString(); + }; + + // 재이용시설 오버레이 + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + + features.forEach((feature: any) => { + // 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId()) + const featureId = + feature.getId() || feature.get("ftrIdn") || Math.random(); + + // HTML 또는 React 오버레이 컴포넌트 구성 + const overlayContent = ` +
+ +
+ ${feature.get("swtrmt_fclt_nm") || "-"} +
+ + + + + + + + + +
용량(m³/일)${formatNumber(feature.get("fclt_cpct"))}
+
+ `; + feature.set("html", overlayContent); + feature.set("overLayId", `overLay.${featureId}`); + }); + } + } + }); + }; + + // 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리 + useEffect(() => { + console.warn(selectDataPk); + if (!selectDataPk || Object.keys(selectDataPk).length === 0) return; + const maps = gisMaps.current; + let foundFeature: any = null; + const layerNm = "tb_wr_bdst_ruse_fclt"; + const featureId = `${layerNm}.${selectDataPk.bscFctCd}`; + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + maps.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk); + } + }, [selectDataPk]); + + // 재이용시설 클릭 시 선택 시설목록에 추가 + const onFeatureSelect = (data: any) => { + // feature의 모든 속성 출력 (어떤 키가 있는지 확인) + const properties = data.getProperties?.() ?? {}; + console.log("[onFeatureSelect] feature 속성 전체:", properties); + // 처리시설코드 + const swtrmtFcltCd = data.get("swtrmt_fclt_cd"); + console.log("[onFeatureSelect] swtrmt_fclt_cd:", swtrmtFcltCd); + reuseData(swtrmtFcltCd); + }; + + return ( + <> +
+ { + if (hasuPk) setSelectDataPk(hasuPk); + }} + onApply={onApply} + /> + } + waterDepth={undefined} + /> +
+ setSelectedFclts(updatedList)} + /> + + ); +}; + +export default Map; diff --git a/apps/dm_new/app/(popup-layout)/map/popup/selectedFcltList.tsx b/apps/dm_new/app/(popup-layout)/map/popup/selectedFcltList.tsx new file mode 100644 index 000000000..5c742684f --- /dev/null +++ b/apps/dm_new/app/(popup-layout)/map/popup/selectedFcltList.tsx @@ -0,0 +1,75 @@ +import React from "react"; + +type FcltItem = { + pbnstRuseFcltId: string; + ruseUtztnFcltNm: string; + splyExtraCpct?: number; + ruseCpct?: number; + [key: string]: any; +}; + +export default function SelectedFcltList({ + data = [], + onRemove, +}: { + data: FcltItem[]; + onRemove: (updatedList: FcltItem[]) => void; +}) { + const handleDelete = (code: string) => { + const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code); + onRemove(updatedList); + }; + + const TableContent = ( + <> + + + 시설명 + + 처리량(㎥/일) + + + 재이용량(㎥/일) + + 삭제 + + + + {data.map((item) => ( + + {item.ruseUtztnFcltNm} + + {Number(item.splyExtraCpct || 0).toLocaleString()} + + + {Number(item.ruseCpct || 0).toLocaleString()} + + + + + + ))} + + + ); + + return ( +
+

+ 선택된 시설 +

+
+ + {TableContent} +
+
+
+ ); +} diff --git a/apps/dm_new/app/(popup-layout)/map/service/index.ts b/apps/dm_new/app/(popup-layout)/map/service/index.ts new file mode 100644 index 000000000..c6bfcff72 --- /dev/null +++ b/apps/dm_new/app/(popup-layout)/map/service/index.ts @@ -0,0 +1 @@ +export * from './map' diff --git a/apps/dm_new/app/(popup-layout)/map/service/map.ts b/apps/dm_new/app/(popup-layout)/map/service/map.ts new file mode 100644 index 000000000..dea14f0d0 --- /dev/null +++ b/apps/dm_new/app/(popup-layout)/map/service/map.ts @@ -0,0 +1,59 @@ +/** + * 관망도 API + */ +import { DEV_URL_WR, DEV_URL_SN } from "@/components/ui/config"; + +export type LyrStngProps = { + lyr_sn: string + lyr_nm: string + upper_lyr_sn: string + drawOrdr: string + flterSql: string + gobjTy: string + lyrTpNm: string + useYn: string + minViewSize: string + iconNm: string + lblAtrbNm: string + fontNm: string + fontIntnColor: string + fontExtnColor: string + fontExtnWdth: string + lnWdth: string + lnColor: string + polgnColor: string + initEprssYn: string + clickPsblYn: string +} + +export const mapService = { + //레이어 설정 조회 + getLyrStngs: async () => { + const response = await fetch(`${DEV_URL_SN}/map/lyrStngs/DM_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + }, + getPbnstRuseFclts: async (ruseFcltPk: any) => { + const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} diff --git a/apps/dm_new/app/(with-layout)/apply/register/page.tsx b/apps/dm_new/app/(with-layout)/apply/register/page.tsx index 3c084bf73..db1c47cb5 100644 --- a/apps/dm_new/app/(with-layout)/apply/register/page.tsx +++ b/apps/dm_new/app/(with-layout)/apply/register/page.tsx @@ -8,14 +8,20 @@ */ "use client"; -import { useMemo, useState, useRef } from "react"; +import { useMemo, useState, useRef, useEffect } from "react"; +import { useRouter } from "next/navigation"; import SubPageHeader from "@/components/SubPageHeader"; import WaterQualityRefModal from "../modal/WaterQualityRefModal"; import CandidateSiteModal, { type CandidateSite } from "@/components/CandidateSiteModal"; +import { useDmStore } from "@/app/store"; +import type { SdmInfoFormData } from "@/app/store/sdmMng"; +import SimilarCaseModal, { type SimilarCaseDetail, type SimilarCaseRow } from "@/app/(landing)/reuse/apply/modal/SimilarCaseModal"; +//import type { SubsInfo } from "@/app/store/map/type"; type PHRange = { min: string; max: string }; export default function DemandRegisterPage() { + const router = useRouter(); const today = useMemo(() => { const d = new Date(); const yyyy = d.getFullYear(); @@ -36,7 +42,7 @@ export default function DemandRegisterPage() { const [hopePrice, setHopePrice] = useState(""); // 요구수질 - const [coliform, setColiform] = useState(""); + const [coliform, setColiform] = useState(""); // 총대장균군수 const [noDetect, setNoDetect] = useState(false); const [tn, setTn] = useState(""); @@ -55,265 +61,654 @@ export default function DemandRegisterPage() { const [cand1, setCand1] = useState(""); const [cand2, setCand2] = useState(""); const [cand3, setCand3] = useState(""); + //const [candidateList, setCandidateList] = useState([]); + + const useTypesData = useDmStore((state) => state.Sdm.useTypesData); + const getUseTypesData = useDmStore((state) => state.getUseTypesData); + const [openSimilar, setOpenSimilar] = useState(false); + + // 후보지 팝업 메시지 핸들러 + const candsRef = useRef<{ name: string; code: string }[]>([]); + + useEffect(() => { + const origin = window.location.origin; + + const handleMessage = (event: MessageEvent) => { + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "SET_SUBS_INFO") { + // 팝업에서 배열로 전송 (최대 3개) + const list = Array.isArray(data.payload) ? data.payload : []; + setCand1(list[0]?.ruseUtztnFcltNm ?? ""); + setCand2(list[1]?.ruseUtztnFcltNm ?? ""); + setCand3(list[2]?.ruseUtztnFcltNm ?? ""); + // 추후 REQUEST_INIT_SUBS_INFO 응답용 ref 갱신 + candsRef.current = list.map((x: any) => ({ + name: x.ruseUtztnFcltNm ?? "", + code: x.ruseUtztnFcltCd ?? "", + })); + } + + if (data.type === "REQUEST_INIT_SUBS_INFO") { + // 팝업이 현재 선택된 후보지 초기값을 요청하면 응답 + const initList = candsRef.current + .filter((x) => x.name) + .map((x) => ({ + ruseUtztnFcltNm: x.name, + ruseUtztnFcltCd: x.code, + })); + const target = event.source as Window | null; + target?.postMessage( + { type: "INIT_SUBS_INFO", payload: initList }, + { targetOrigin: origin } + ); + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, []); + + // 유사사례 + const similarCaseList = useDmStore((state) => state.Sdm.similarCaseList); + const getSimilarCaseList = useDmStore((state) => state.getSimilarCaseList); + const [farmText, setFarmText] = useState("이하"); + + const insertSdmInfoData = useDmStore( + (state) => state.insertSdmInfoData + ); + + useEffect(() => { + getUseTypesData(); + }, [getUseTypesData]); + + useEffect(() => { + const handler = (event: MessageEvent) => { + if (!event.data) return; + + if (event.data.type === "SET_SUBS_INFO") { + const list = event.data.payload || []; + + // 최대 3개 + setCand1(list[0]?.ruseUtztnFcltNm || ""); + setCand2(list[1]?.ruseUtztnFcltNm || ""); + setCand3(list[2]?.ruseUtztnFcltNm || ""); + //setCandidateList(list); + } + }; + + window.addEventListener("message", handler); + + return () => { + window.removeEventListener("message", handler); + }; + }, []); + + // option + // 'req':필수 / 'num' : 숫자 / 'num+[숫자] : 자릿수 + const validation = (val, text, option: string[] = []) => { + if (option.includes('req')) { + if (val.trim() === "" || val === undefined) { + alert(`${text} 항목은 필수 입력값입니다.`); + return false; + } + } else { + if (val.trim() === "") { + return true; + } + } + + const numOption = option.find((opt) => opt.startsWith("num")); + if (numOption) { + const isDot = numOption.includes("Dot"); + + // 3. 숫자 체크 + const regex = isDot ? /^\d+(\.\d+)?$/ : /^\d+$/; + + if (!regex.test(val)) { + alert( + `${text}은(는) ${isDot ? "숫자 또는 소수" : "숫자" + }만 입력 가능합니다.` + ); + return false; + } + + // 4. 자리수 체크 + const lengthStr = numOption.replace("numDot", "").replace("num", ""); + + if (lengthStr) { + const length = Number(lengthStr); + + if (isDot) { + // 🔥 소수 자리수 체크 (소수점 기준) + const decimalPart = val.split(".")[1]; + + if (decimalPart && decimalPart.length > length) { + alert(`${text}은(는) 소수점 ${length}자리까지 가능합니다.`); + return false; + } + } else { + // 정수 자리수 체크 + if (val.length !== length) { + alert(`${text}은(는) ${length}자리여야 합니다.`); + return false; + } + } + } + } + return true; + } + + const rows: SimilarCaseRow[] = similarCaseList.map((el, idx) => ({ + caseId: String(idx), + projectName: el.ruseBizNm, + period: `${el.bizBgngDate} ~ ${el.bizEndDate}`, + + supplyFacilityType: el.splyFcltKnd, + supplyName: el.splyFcltNm, + supplyCapacity: Number(el.splyFcltCpct), + + demandName: el.dmdFcltNm, + demandAddress: el.dmdFcltAddr, + + facilityCapacity: Number(el.ruseFcltCpct), + pipeKm: Number(el.ruseFcltPplLen), + purpose: el.ruseFcltUsg, + + supplyAmt: Number(el.ruseFcltSplyCpct), + treatment: el.ruseFcltPrcsMsrc, + usePurpose: el.ruseFcltUsg, + })); + + const detailsByCaseId: Record = {}; + similarCaseList.forEach((el, idx) => { + detailsByCaseId[String(idx)] = { + coliform: el.grnteWqGrssClfg, + residualCl: el.grnteWqCombResdch, + turbidity: el.grnteWqTrbt, + bod: el.grnteWqBod, + odor: el.grnteWqSmll, + color: el.grnteWqCrmty, + tn: el.grnteWqTn, + tp: el.grnteWqTp, + ph: el.grnteWqPh, + chloride: el.grnteWqClIonQnt, + ec: el.grnteWqCndctStat, + note: el.grnteWqRmrk, + + costTotal: Number(el.reqBizGrssCst), + costFacility: Number(el.reqBizReuseFcltCst), + costPipe: Number(el.reqBizPplCst), + costDesign: Number(el.reqBizDesignCst), + costSupervision: Number(el.reqBizSpvsCst), + costOther: Number(el.reqBizEtc), + + fundingTotal: Number(el.prcmPlanGrssCst), + fundingNational: Number(el.prcmPlanNlep), + fundingLocal: Number(el.prcmPlanLcep), + fundingSelf: Number(el.prcmPlanBrdnCst), + fundingRate: Number(el.prcmPlanEcqrRt), + }; + }); + + // 데이터 저장 + const saveInfo = async () => { + if (!validation(reqQty, '요구수량', ['req', 'numDot'])) return false; + if (!validation(usePurpose, '사용용도', ['req'])) return false; + if (!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false; + /* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false; + if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false; + if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false; + if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false; + if(!validation(odor, '냄새', ['req'])) return false; */ + + // 필수는 아니지만 숫자로는 들어와야함. + /* if(!validation(tn, '총질소', ['numDot'])) return false; + if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false; + if(!validation(tp, '총인', ['numDot'])) return false; + if(!validation(ph.min, 'ph', ['numDot'])) return false; + if(!validation(ph.max, 'ph', ['numDot'])) return false; + if(!validation(chloride, '염화물', ['numDot'])) return false; + if(!validation(ec, '전기전도도', ['numDot'])) return false; */ + + const userStr = sessionStorage.getItem("user"); + const user = userStr ? JSON.parse(userStr) : null; + + if (user) { + // 후보지 목록 (candsRef에 코드까지 보관됨) + const splyCndtList = candsRef.current + .filter((x) => x.name) + .map((x) => ({ + ruseUtztnFcltNm: x.name, + ruseUtztnFcltCd: x.code, + })); + + const payload: SdmInfoFormData = { + dmndTmng: new Date(reqDate), + dmdInfoId: user.dmdInfoId, + dmndQty: Number(reqQty), + wntUntprc: Number(hopePrice), + useUsgCd: usePurpose, + // 수질 + grssClfg: Number(coliform), + tn: Number(tn) || null, + combResdch: Number(residualCl) || null, + tph: Number(tp) || null, + trbt: Number(turbidity), + ph: ph.min || '' + ((ph.min || ph.max) ? '~' : '') + ph.max || '', + blgcOxygDmndQnt: Number(bod), + clIonQnt: Number(chloride) || null, + smll: odor, + cdtnv: Number(ec) || null, + crmty: Number(color) || null, + rmrk: remark, + splyCndtList, // 후보지 목록 추가 + } + const success = await insertSdmInfoData(payload); + if (success) { + router.push("/apply/status"); + } + } + } + + const searchCandidate = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const dmdInfoId = JSON.parse( + sessionStorage.getItem("user") || "{}" + )?.dmdInfoId?.toString(); + + const url = `/map/popup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`; + const width = window.screen.availWidth; + const height = window.screen.availHeight; + const left = 0; + const top = 0; + const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`; + window.open(url, "candidatePopup", popupOptions); + }; + + const getCriterionCode = (criterion: string) => { + switch (criterion) { + case "supplyAmt": + return "S"; + case "money": + return "M"; + case "usePurpose": + return "U"; + default: + return ""; + } + }; + + const searchSimilarCase = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setOpenSimilar(true); + }; + + const initQualityForm = () => { + setColiform(""); + setNoDetect(false); + setTn(""); + setResidualCl(""); + setTp(""); + setTurbidity(""); + setPh({ min: "", max: "" }); + setBod(""); + setChloride(""); + setOdor("불쾌하지 않을 것"); + setEc(""); + setColor(""); + setRemark(""); + setFarmText("이하"); + } + + const fncSetOptionValue = (e: React.ChangeEvent) => { + const value = e.target.value; + initQualityForm(); + + switch (value) { + // 청소화장실용도 + case '01': + setTp('0.2'); + setTurbidity('2'); + setPh({ min: '5.8', max: '8.5' }); + setBod('5'); + setColor('20'); + break; + // 세척살수용도 + case '02': + setTp('0.2'); + setTurbidity('2'); + setPh({ min: '5.8', max: '8.5' }); + setBod('5'); + break; + // 조경용도 + case '03': + setTp('0.2'); + setTurbidity('2'); + setPh({ min: '5.8', max: '8.5' }); + setBod('5'); + setChloride('250'); + break; + // 지하수 + case '05': + break; + // 농업용도 + case '06': + setTurbidity('5'); + setPh({ min: '5.8', max: '8.5' }); + setBod('8'); + setEc('2000'); + setFarmText("이하 (** 간접:2000,직접:700 )"); + break; + // 하천 등 유지보수 + case '07': + setColiform('1000'); + setTn('10'); + setTp('0.5'); + setPh({ min: '5.8', max: '8.5' }); + setBod('5'); + break; + // 공업 + case '08': + setPh({ min: '5.8', max: '8.5' }); + break; + default: + console.log('잘못된 선택입니다.'); + } + + setUsePurpose(value); + } return ( <> - -
- {/* 서브 타이틀 + 브레드크럼 */} - +
- {/* 컨텐츠 영역 */} -
- - {/* 기본정보 테이블 */} -
- - - - - - - - - - - - - - - - - - - - - -
- - {/* 요구수질 섹션 타이틀 바 */} -
-
- 요구수질 - - {/* 수질기준참고 모달 */} - setOpenRef(false)} - imageSrc="/images/landing/water_quality_ref_1.png" - /> -
-
- - {/* 요구수질 테이블 */} -
- {/* 1행 */} - - - -
- } - rightLabel="총질소(T-N)(mg/L)" - rightControl={ - - } - /> - - - {/* 2행 */} - } - rightLabel="총인(T-P)(mg/L)" - rightControl={ - - } - /> - - - {/* 3행 */} - - } - rightLabel="수소이온농도(pH)" - rightControl={ -
- setPh((p) => ({ ...p, min: v }))} - className="w-32" - /> - ~ - setPh((p) => ({ ...p, max: v }))} - className="w-32" - /> -
- } - /> - - - {/* 4행 */} - - 생물화학적산소요구량 -
- (BOD)(mg/L) - - } - leftRequired - leftControl={} - rightLabel="염화물(mgCL/L)" - rightControl={} - /> - - - {/* 5행 */} - setOdor(e.target.value)} - className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500" - > - - - - - } - rightLabel="전기전도도(µs/cm)" - rightControl={} - /> - - - {/* 6행 */} - } - rightLabel="비고" - rightControl={ -