From 66126bbe621ffbed124f4f4531ade58855f32eeb Mon Sep 17 00:00:00 2001 From: swcho Date: Thu, 12 Jun 2025 18:57:49 +0900 Subject: [PATCH 01/22] =?UTF-8?q?etc:=20=EC=A4=91=EA=B0=84=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/components/lib/api/index.ts | 2 +- apps/sn/app/components/ui/modal.tsx | 10 +- apps/sn/app/components/ui/verticalTable.tsx | 87 ++++- apps/sn/app/map/components/common/index.ts | 32 +- apps/sn/app/map/components/common/table.ts | 93 ++++- .../app/map/components/popupComponent/Ssc.tsx | 173 ++++++--- .../components/popupComponent/SwtrmtFclt.tsx | 70 ++-- .../app/map/components/popupComponent/smh.tsx | 2 +- apps/wr/app/map/featureClickEventPopup.tsx | 201 ++++++++++ apps/wr/app/map/govSidoSigunguSearchForm.tsx | 114 ++++++ apps/wr/app/map/htdflSttn/page.tsx | 113 ++++++ apps/wr/app/map/mapSearchBar.tsx | 154 ++++++++ apps/wr/app/map/mapSearchForm.tsx | 347 ++++++++++++++++++ apps/wr/app/map/searchModal.tsx | 82 +++++ apps/wr/app/map/service/index.ts | 1 + apps/wr/app/map/service/map.ts | 47 +++ apps/wr/app/page.tsx | 12 + .../images/map/layericon/BML_CBAS_PS.png | Bin 0 -> 1547 bytes .../images/map/layericon/BML_ELEV_PS.png | Bin 0 -> 1030 bytes .../images/map/layericon/BML_ETCP_PS.png | Bin 0 -> 961 bytes .../images/map/layericon/BML_STMS_PS.png | Bin 0 -> 1542 bytes .../images/map/layericon/BML_SYMB_PS.png | Bin 0 -> 970 bytes .../images/map/layericon/RDL_BYST_PS.png | Bin 0 -> 1196 bytes .../images/map/layericon/RDL_CROS_PS.png | Bin 0 -> 1083 bytes .../images/map/layericon/RDL_ETCT_PS.png | Bin 0 -> 1016 bytes .../images/map/layericon/RDL_OCUP_PS.png | Bin 0 -> 1061 bytes .../images/map/layericon/RDL_RDSN_PS.png | Bin 0 -> 1313 bytes .../images/map/layericon/RDL_SCLT_PS.png | Bin 0 -> 1072 bytes .../images/map/layericon/RDL_SEPC_PS.png | Bin 0 -> 1068 bytes .../images/map/layericon/RDL_STAT_PS.png | Bin 0 -> 1075 bytes .../images/map/layericon/RDL_STLT_PS.png | Bin 0 -> 1089 bytes .../images/map/layericon/RDL_TFSN_PS.png | Bin 0 -> 1291 bytes .../images/map/layericon/RDL_TRCT_PS.png | Bin 0 -> 1412 bytes .../images/map/layericon/RDL_TREE_PS.png | Bin 0 -> 1136 bytes .../images/map/layericon/RDL_TRSN_PS.png | Bin 0 -> 1375 bytes .../images/map/layericon/RDL_TSCT_PS.png | Bin 0 -> 1700 bytes .../images/map/layericon/SWL_CLAY_PS.png | Bin 0 -> 1146 bytes .../images/map/layericon/SWL_CLAY_PS_RED.png | Bin 0 -> 468 bytes .../images/map/layericon/SWL_DEPT_PS.png | Bin 0 -> 1079 bytes .../images/map/layericon/SWL_MANH_PS.png | Bin 0 -> 1559 bytes .../map/layericon/SWL_MANH_PS_BLACK.png | Bin 0 -> 757 bytes .../images/map/layericon/SWL_MANH_PS_BLUE.png | Bin 0 -> 785 bytes .../images/map/layericon/SWL_MANH_PS_GRAY.png | Bin 0 -> 1054 bytes .../map/layericon/SWL_MANH_PS_GREEN.png | Bin 0 -> 791 bytes .../images/map/layericon/SWL_MANH_PS_RED.png | Bin 0 -> 782 bytes .../map/layericon/SWL_MANH_PS_YELLOW.png | Bin 0 -> 782 bytes .../images/map/layericon/SWL_SPEW_PS.png | Bin 0 -> 1217 bytes .../images/map/layericon/SWL_SPOT_PS.png | Bin 0 -> 1067 bytes .../map/layericon/SWL_SPOT_PS_BLACK.png | Bin 0 -> 217 bytes .../images/map/layericon/SWL_SPOT_PS_BLUE.png | Bin 0 -> 217 bytes .../map/layericon/SWL_SPOT_PS_GREEN.png | Bin 0 -> 217 bytes .../images/map/layericon/SWL_SPOT_PS_GREY.png | Bin 0 -> 217 bytes .../images/map/layericon/SWL_SPOT_PS_RED.png | Bin 0 -> 217 bytes .../images/map/layericon/SWL_VENT_PS.png | Bin 0 -> 1033 bytes .../images/map/layericon/TL_SPBD_ENTRC.png | Bin 0 -> 1260 bytes .../images/map/layericon/TL_SPGF_LOC.png | Bin 0 -> 1383 bytes .../images/map/layericon/TL_SPOT_NOIN.png | Bin 0 -> 1009 bytes .../images/map/layericon/TL_SPRD_BANO.png | Bin 0 -> 1044 bytes .../images/map/layericon/TL_SPRD_CRSRD.png | Bin 0 -> 1083 bytes .../images/map/layericon/TL_SPRD_NONM.png | Bin 0 -> 1049 bytes .../images/map/layericon/TL_SPRD_RENM.png | Bin 0 -> 1009 bytes .../images/map/layericon/TL_SPRD_RWNM.png | Bin 0 -> 1049 bytes .../images/map/layericon/UFL_BMAN_PS.png | Bin 0 -> 1552 bytes .../images/map/layericon/UFL_BOPC_PS.png | Bin 0 -> 1132 bytes .../images/map/layericon/UFL_BPIP_PS.png | Bin 0 -> 1017 bytes .../images/map/layericon/UFL_BPOL_PS.png | Bin 0 -> 1193 bytes .../images/map/layericon/UFL_BTRS_PS.png | Bin 0 -> 1097 bytes .../images/map/layericon/UFL_GMNT_PS.png | Bin 0 -> 1166 bytes .../images/map/layericon/UFL_GPIP_PS.png | Bin 0 -> 1103 bytes .../images/map/layericon/UFL_GPRS_PS.png | Bin 0 -> 1408 bytes .../images/map/layericon/UFL_GVAL_PS.png | Bin 0 -> 1426 bytes .../images/map/layericon/UFL_KMAN_PS.png | Bin 0 -> 1572 bytes .../images/map/layericon/UFL_KPOL_PS.png | Bin 0 -> 1122 bytes .../images/map/layericon/UFL_VALV_PS.png | Bin 0 -> 1058 bytes .../images/map/layericon/WTL_FIRE_PS.png | Bin 0 -> 1072 bytes .../images/map/layericon/WTL_FLOW_PS.png | Bin 0 -> 1137 bytes .../images/map/layericon/WTL_MANH_PS.png | Bin 0 -> 1557 bytes .../images/map/layericon/WTL_META_PS.png | Bin 0 -> 1100 bytes .../images/map/layericon/WTL_PIPE_PS.png | Bin 0 -> 1075 bytes .../images/map/layericon/WTL_RSRV_PS.png | Bin 0 -> 1057 bytes .../images/map/layericon/WTL_RSRV_PS_GRAY.png | Bin 0 -> 259 bytes .../images/map/layericon/WTL_VALV_PS.png | Bin 0 -> 1077 bytes .../public/images/map/layericon/default.png | Bin 0 -> 160 bytes apps/wr/public/images/map/layericon/reset.png | Bin 0 -> 108 bytes .../business_components/src/ui/map/Gis.js | 50 ++- .../map/{MapToggleBtn.js => GisMapToggle.js} | 0 .../src/ui/map/GisSelect.js | 55 +-- .../business_components/src/ui/map/GisTree.js | 110 ++++-- .../src/ui/map/lib/gis.core.js | 21 +- 89 files changed, 1584 insertions(+), 192 deletions(-) create mode 100644 apps/wr/app/map/featureClickEventPopup.tsx create mode 100644 apps/wr/app/map/govSidoSigunguSearchForm.tsx create mode 100644 apps/wr/app/map/htdflSttn/page.tsx create mode 100644 apps/wr/app/map/mapSearchBar.tsx create mode 100644 apps/wr/app/map/mapSearchForm.tsx create mode 100644 apps/wr/app/map/searchModal.tsx create mode 100644 apps/wr/app/map/service/index.ts create mode 100644 apps/wr/app/map/service/map.ts create mode 100644 apps/wr/public/images/map/layericon/BML_CBAS_PS.png create mode 100644 apps/wr/public/images/map/layericon/BML_ELEV_PS.png create mode 100644 apps/wr/public/images/map/layericon/BML_ETCP_PS.png create mode 100644 apps/wr/public/images/map/layericon/BML_STMS_PS.png create mode 100644 apps/wr/public/images/map/layericon/BML_SYMB_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_BYST_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_CROS_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_ETCT_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_OCUP_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_RDSN_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_SCLT_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_SEPC_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_STAT_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_STLT_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_TFSN_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_TRCT_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_TREE_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_TRSN_PS.png create mode 100644 apps/wr/public/images/map/layericon/RDL_TSCT_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_CLAY_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png create mode 100644 apps/wr/public/images/map/layericon/SWL_DEPT_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_BLACK.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_BLUE.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_RED.png create mode 100644 apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPEW_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLACK.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS_BLUE.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREEN.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS_GREY.png create mode 100644 apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png create mode 100644 apps/wr/public/images/map/layericon/SWL_VENT_PS.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPGF_LOC.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPOT_NOIN.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPRD_BANO.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPRD_CRSRD.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPRD_NONM.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPRD_RENM.png create mode 100644 apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png create mode 100644 apps/wr/public/images/map/layericon/UFL_BMAN_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_BOPC_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_BPIP_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_BPOL_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_BTRS_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_GMNT_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_GPIP_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_GPRS_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_GVAL_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_KMAN_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_KPOL_PS.png create mode 100644 apps/wr/public/images/map/layericon/UFL_VALV_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_FIRE_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_FLOW_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_MANH_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_META_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_PIPE_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_RSRV_PS.png create mode 100644 apps/wr/public/images/map/layericon/WTL_RSRV_PS_GRAY.png create mode 100644 apps/wr/public/images/map/layericon/WTL_VALV_PS.png create mode 100644 apps/wr/public/images/map/layericon/default.png create mode 100644 apps/wr/public/images/map/layericon/reset.png rename packages/common/business_components/src/ui/map/{MapToggleBtn.js => GisMapToggle.js} (100%) diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index a171844a1..24f15ed10 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -1,4 +1,4 @@ -const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; +const BASE_URL = 'http://con.mind-one.co.kr:11083/sewerage-service/api'; export const postData = async (url: string, data: object): Promise => { const options: RequestInit = { diff --git a/apps/sn/app/components/ui/modal.tsx b/apps/sn/app/components/ui/modal.tsx index bc8085f71..f1dda48a6 100644 --- a/apps/sn/app/components/ui/modal.tsx +++ b/apps/sn/app/components/ui/modal.tsx @@ -7,20 +7,24 @@ interface ModalProps { title?: string; children: React.ReactNode; closeModal: () => void; + saveModal?: () => void } -const Modal: React.FC = ({ isModalOpen, setIsModalOpen, title, children, closeModal }) => { +const Modal: React.FC = ({ isModalOpen, setIsModalOpen, title, children, closeModal,saveModal }) => { return ( - - + + {title ? {title} : null} {children} +
+ {saveModal ? : null} +
diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index 36451bfbd..e5e506796 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -4,16 +4,29 @@ import { useEffect, useState } from "react"; type UiTableProps = { headName: React.ReactNode | undefined; + tableName1?: React.ReactNode | undefined; tableData: { [key: string]: string | number | boolean | object }[]; headlist: UiHeadProps[]; pageSize: number; total: number; - children: React.ReactNode | undefined; + children: Record | undefined; cellClick: ((index: number | undefined, headId: string) => void) | undefined; onPageChange: (page: number) => void; className?: string; resetPageNo?: boolean; isEditable?: boolean; + tableName2?: React.ReactNode | undefined; + tableData2?: { [key: string]: string | number | boolean | object }[]; + headlist2?: UiHeadProps[]; + pageSize2?: number; + total2?: number; + children2?: Record | undefined; + cellClick2?: ((index: number | undefined, headId: string) => void) | undefined; + onPageChange2?: (page: number) => void; + className2?: string; + resetPageNo2?: boolean; + isEditable2?: boolean; + showSecondTable?: boolean; }; type UiHeadProps = { @@ -25,17 +38,30 @@ type UiHeadProps = { }; export function VerticalTable({ + headName, + tableName1, tableData, headlist, pageSize, total, children, - headName, cellClick, onPageChange, className = "", resetPageNo = false, isEditable = false, + tableName2, + tableData2, + headlist2, + pageSize2, + total2, + children2, + cellClick2, + onPageChange2, + className2 = "", + resetPageNo2 = false, + isEditable2 = false, + showSecondTable = false, }: UiTableProps) { const [cur, setCur] = useState(1); const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({}); @@ -45,7 +71,7 @@ export function VerticalTable({ onPageChange(1); } }, [onPageChange, resetPageNo]); - + useEffect(() => { if (tableData.length > 0 && tableData[0]) { setFormData(tableData[0] as { [key: string]: string | number | boolean }); @@ -72,6 +98,9 @@ export function VerticalTable({ ) : null}
+ {tableName1 && ( // tableName prop이 있으면 표시 +
{tableName1}
+ )} {headlist?.map((head) => ( @@ -82,9 +111,10 @@ export function VerticalTable({ {/* 데이터 값 또는 입력 필드 */} - {isEditable ? ( + {children && children[head.id] ? children[head.id] : isEditable ? ( head.type === "select" ? ( ) : head.type === "input" ? ( handleInputChange(head.id, e.target.value)} @@ -113,6 +144,54 @@ export function VerticalTable({ ))}
+ {tableName2 && ( // tableName prop이 있으면 표시 +
{tableName2}
+ )} + {showSecondTable && ( + + + {headlist2?.map((head) => ( + + {/* 컬럼 제목 */} + + {head.title} + + {/* 데이터 값 또는 입력 필드 */} + + {children && children[head.id] ? children[head.id] : isEditable2 ? ( + head.type === "select" ? ( + + ) : head.type === "input" ? ( + handleInputChange(head.id, e.target.value)} + className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1" + /> + ) : ( + {formData[head.id] ?? "-"} + ) + ) : ( + {formData[head.id] ?? "-"} + )} + + + ))} + +
+ )} {total > 0 && ( ; + props?: any; + }; +}; // 레이어 이름과 컴포넌트를 매핑 -const layerContentMap: { [key: string]: { component?: React.FC ; props?: any; } } = { +// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 +const layerContentMap: LayerContentMap = { // TODO: gis데이터 추가되면 추가 작업 필요 - "하수처리시설": { - component: SwtrmtFclt, - }, + "하수처리시설": { component: SwtrmtFclt, }, "하수처리구역": { }, - "중점우수토실":{component: Ssc,}, - "하수관로":{component: SwtrmtFclt,}, + "우수토실":{component: Ssc,}, + "중점우수토실":{component: Ssc, }, // 중점우수토실 여부 설정 + // "하수관로":{component: MsSwagePpl,}, "하수맨홀":{component: smh,}, + // "빗물펌프":{component:StormWaterPump}, + // "오수펌프":{component:sewagePump}, }; // 동적 컴포넌트 반환 함수 -export const getLayerContentComponent = (layerNm: string): React.FC => { +export const getLayerContentComponent = (layerNm: string): React.FC | null => { const component = layerContentMap[layerNm]; if (!component || !component.component) { // component가 없으면 null 반환 return () => null; } - return component.component; + return component.component }; // 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단) export const handleInputChange = (section: string, diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/map/components/common/table.ts index a64e610d3..6c641f9be 100644 --- a/apps/sn/app/map/components/common/table.ts +++ b/apps/sn/app/map/components/common/table.ts @@ -7,11 +7,11 @@ export const SwtrmtFcltCol = [ { id: 'oprtngStrtYmd', title: '가동개시일자' }, { id: 'clsgYmd', title: '폐쇄일자' }, { id: 'prcsMsrcCd', title: '처리공법코드' }, - { id: 'laeCd', title: '지자체코드' }, + // { id: 'laeCd', title: '지자체코드' }, { id: 'laeNm', title: '지자체명' }, { id: 'ctpvNm', title: '시도명' }, { id: 'sggNm', title: '시군구명' }, - { id: 'sareaCd', title: '권역코드' }, + // { id: 'sareaCd', title: '권역코드' }, { id: 'sareaNm', title: '권역명' }, { id: 'lrgeSareaNm', title: '대권역명' }, { id: 'rvrNm', title: '하천명' }, @@ -26,7 +26,7 @@ export const SwtrmtFcltCol = [ { id: 'prcsZoneAreaSum', title: '처리구역면적합계' }, { id: 'xcrd', title: 'X좌표' }, { id: 'ycrd', title: 'Y좌표' }, - { id: 'oprtngSttsCd', title: '가동상태코드' }, + // { id: 'oprtngSttsCd', title: '가동상태코드' }, { id: 'rprsMsrcCn', title: '대표공법내용' }, { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량' }, { id: 'sttyCrtrToc', title: '법정기준TOC' }, @@ -35,17 +35,94 @@ export const SwtrmtFcltCol = [ { id: 'sttyCrtrTph', title: '법정기준총인' }, { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수' }, { id: 'bsnNm', title: '유역명' }, - { id: 'obsrvnSptCd', title: '관측지점코드' }, + // { id: 'obsrvnSptCd', title: '관측지점코드' }, ]; // 하수처리구역 export const SwtrmtPrcsZoneCol = [ { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, { id: 'swtrmtZoneNm', title: '하수처리구역명' }, - { id: 'swtrmtFcltCd', title: '하수처리시설코드' }, - { id: 'ctpvCd', title: '시도코드' }, + // { id: 'swtrmtFcltCd', title: '하수처리시설코드' }, + // { id: 'ctpvCd', title: '시도코드' }, { id: 'ctpvNm', title: '시도명' }, - { id: 'sggCd', title: '시군구코드' }, + // { id: 'sggCd', title: '시군구코드' }, { id: 'sggNm', title: '시군구명' }, { id: 'swtrmtFcltNm', title: '하수처리시설명' }, -] +]; + +// 하수맨홀 +export const MsSwageMhlCol = [ + { id: 'fid', title: 'FID' }, + { id: 'geom', title: 'geometry' }, + { id: 'ftreSmblCd', title: '지형지물부호코드' }, + { id: 'mngNo', title: '관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + { id: 'lastDrdgnYmd', title: '최종준설일자' }, + { id: 'usgCd', title: '용도코드' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + { id: 'mhlKndCd', title: '맨홀종류코드' }, + { id: 'swageMhDiam', title: '하수맨홀구경' }, + { id: 'wdthLen', title: '가로길이' }, + { id: 'vrtcLen', title: '세로길이' }, + // { id: 'mqltCd', title: '재질코드' }, + { id: 'altdVl', title: '표고값' }, + { id: 'dpthlVl', title: '심도값' }, + { id: 'instMngNo', title:'기관관리번호'} +]; + +// 중점우수토실 측정지점 +export const untrtSwgRainCol1 = [ + { id: 'manage_no', title: '관리번호' }, + { id: 'swr_ppl_eqp_nm', title: '처리구역' }, + { id: 'swr_ppl_trt_nm', title: '처리분구' }, + { id: 'addr', title: '인근주소' }, + { id: 'chmbr_xcnts', title: '토실 위도' }, + { id: 'chmbr_ydnts', title: '토실 경도' } +]; + +// 중점우수토실 측정지점 시설현황 +export const untrtSwgRainCol2 = [ + { id: 'flwmtr_mesure_tp_etc', title: '구분' }, + { id: 'discharge_pipe_spec', title: '방류관로 규격' }, + { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, + { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, + { id: 'screen_instl_at', title: '스크린설치 여부' }, + { id: 'weir_hg', title: '위어 높이(m)' }, + { id: 'weir_lt', title: '위어 길이(m)' } +]; + +// 중점우수토실 강우정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol3 = [ + { id: 'rain_begin_dt', title: '강우시작일시' }, + { id: 'rain_end_dt', title: '강우종료일시' }, + { id: 'precdng_dry', title: '선행건기(h)' }, + { id: 'rain_info', title: '강우정보' }, + { id: 'rain_fall', title: '누적강우량(mm)' }, + { id: 'cntnc_time', title: '지속시간(h)' }, + { id: 'rain_in', title: '강우강도(mm/h)' } +]; + +// 중점우수토실 측정정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol4 = [ + { id: 'mesure_begin_dt', title: '측정시작일시' }, + { id: 'mesure_end_dt', title: '측정종료일시' }, + { id: 'mesure_intrvl', title: '측정간격(분)' }, + { id: 'mesure_mhrls_instl_lc', title: '측정기기 설치위치' }, + { id: 'flwmtr_combined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 + { id: 'qltwtr_begin_dt', title: '채수시작일시' }, + { id: 'qltwtr_end_dt', title: '채수종료일시' }, + { id: 'qltwtr_co', title: '채수횟수' } +]; + +// 중점우수토실 수량/수질측정 정보 +export const untrtSwgRainCol5 = [ + { id: 'odr', title: '회차' }, + { id: 'mesureDt', title: '측정일자' }, + { id: 'mesureQy', title: '수량(m)' }, + { id: 'mesureBod', title: 'BOD(mg/L)' }, + { id: 'mesureToc', title: 'TOC(mg/L)' }, + { id: 'mesureLoad', title: '오염부하량(kg)' } +]; \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx index a6e783ac9..2ca7c174d 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -1,18 +1,16 @@ // 우수토실 -import Modal from '@/components/ui/modal'; -import { VerticalTable } from '@/components/ui/verticalTable'; -import { UiTable } from '@common/business_components/ui'; +import { UiModal, UiTable } from '@common/business_components/ui'; import { Button, TableHead, TableRow } from '@common/components/ui'; -import React from 'react' -const inputColumn = [ - { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, - { id: "mainSscYn", title: "중점우수토실 대상여부", type: "select", options: ['Y', 'N'] }, -]; -const headlist = [ - { id: 'startrain', title: '강우시작일시' }, { id: 'endrain', title: '강우종료일시' }, { id: '선행건기', title: '선행건기(h)' }, { id: '강우정보', title: '강우정보' }, { id: '누적강우량', title: '누적강우량(mm)' }, { id: '지속시간', title: '지속시간(h)' }, { id: '강우강도', title: '강우강도(mm/h)' }, -] -const headlist3 = [{ id: 'nm', title: '회차' }, { id: 'date', title: '측정일자' }, { id: 'measure', title: '수량(m)' }, { id: 'bod', title: 'BOD(mg/L)' }, { id: 'toc', title: 'TOC(mg/L)' }, { id: '오염부하량', title: '오염부하량(kg)' }] -const Ssc = () => { +import React from 'react'; +import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; + +const Ssc = ({ featureData, chkMainSsc, additionalData }: { + featureData: any, + chkMainSsc?: boolean, + additionalData?: any +}) => { + console.log('Additional data received:', additionalData); // 추가 데이터 확인용 + const [isModalOpen, setIsModalOpen] = React.useState(false); const openModal = () => { setIsModalOpen(true); @@ -20,45 +18,120 @@ const Ssc = () => { const closeModal = () => { setIsModalOpen(false); }; + + // 데이터 전처리: 합쳐진 필드 추가 + const preprocessedData = featureData ? [{ + ...featureData, + flwmtr_combined: `${featureData.flwmtr_mesure_tp_etc || '-'} / ${featureData.flwmtr_knd || '-'}` + }] : []; + + const renderModalBody = () => ( +
+ {/* 강우정보 테이블 */} + + + {/* 측정정보 테이블 */} + + {/* 커스텀 헤더 */} + + 수량 + 수질 + + + 측정시작일시 + 측정종료일시 + 측정간격(분) + 측정기기설치위치 + 유량계 종류 및 모델명 + 채수시작일시 + 채수종료일시 + 채수횟수 + + + + {/* 수량/수질측정 정보 테이블 */} + +
+ ); + return ( -
-
-
-

중점우수토실 운영정보

+
+ {/* */} + + {!chkMainSsc ? null : +
+
+

중점우수토실 운영정보

+
+ + +
- - -
- {/* 커스텀 헤더 */} - - - 수량 - - - 수질 - - - - 측정시작일시 - 측정종료일시 - 측정간격(분) - 측정기기설치위치 - 유량계 종류 및 모델명 - 채수시작일시 - 채수종료일시 - 채수횟수 - -
-
-
+ }
) } diff --git a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx index b6d785414..0d1c3ef2e 100644 --- a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx +++ b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx @@ -1,44 +1,48 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; +import Modal from '../../../components/ui/modal'; +import { VerticalTable } from '../../../components/ui/verticalTable'; +import { UiTable } from '@common/business_components/ui'; +import { Button, TableCell } from '@common/components/ui'; import React, { useState } from 'react' -import InputSection from '../InputSection'; -import { handleInputChange } from '../common'; + // 하수처리시설 -// TODO: 컬럼에 보고서 엉 ? 이거 버튼누르면 팝업으로 상세화면 띄워주는 건지 확인 필요 -const inputColumn = [ - { id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, - { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, - { id: "basu", title: "배수구역", type: "input" }, - { id: "bungu", title: "배수분구", type: "input" }, - { id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] }, - { id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, - { id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] }, -]; +const headList = [ + { id: "title", title: "하수도정비 기본계획보고서" }, + { id: "cad", title: "하수도대장 평면도" },] +const modalHeadList = [ + { id: "no", title: "번호" }, + { id: "year", title: "승인연도" }, + { id: "cad", title: '평면도'}, + { id: "fileNm", title: "파일명" }, + { id: "date", title: "입력시간" }, +] + const SwtrmtFclt = () => { - const [repairData, setRepairData] = useState([ - { year: "2025", type: "개대체" }, - { year: "2025", type: "보수" }, - ]); - const [cleaningData, setCleaningData] = useState([ - { year: "2025", type: "청소" }, - { year: "2025", type: "준설" }, - ]); - const [inspectionData, setInspectionData] = useState([ - { year: "2025", type: "관로기술진단" }, - { year: "2025", type: "관로기술진단" }, - ]); - const dataMap = { - repair: { data: repairData, setData: setRepairData }, - cleaning: { data: cleaningData, setData: setCleaningData }, - inspection: { data: inspectionData, setData: setInspectionData }, + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalTitle, setModalTitle] = useState(''); + const openModal = (title:string) => { + setIsModalOpen(true); + setModalTitle(title) + }; + const closeModal = () => { + setIsModalOpen(false); }; return (
- openModal('하수도정비 기본계획보고서')}>보고서, + cad: , + }} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { throw new Error("Function not implemented."); }}> - handleInputChange(section, index, field, value, dataMap)} /> - handleInputChange(section, index, field, value, dataMap)} /> - handleInputChange(section, index, field, value, dataMap)} />
+ +
+ +
+
+
) } diff --git a/apps/sn/app/map/components/popupComponent/smh.tsx b/apps/sn/app/map/components/popupComponent/smh.tsx index e693a28ce..39920232b 100644 --- a/apps/sn/app/map/components/popupComponent/smh.tsx +++ b/apps/sn/app/map/components/popupComponent/smh.tsx @@ -1,5 +1,5 @@ // 하수맨홀 -import { VerticalTable } from '@/components/ui/verticalTable'; +import { VerticalTable } from '../../../components/ui/verticalTable'; import React from 'react' const inputColumn = [ { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, diff --git a/apps/wr/app/map/featureClickEventPopup.tsx b/apps/wr/app/map/featureClickEventPopup.tsx new file mode 100644 index 000000000..8048d0997 --- /dev/null +++ b/apps/wr/app/map/featureClickEventPopup.tsx @@ -0,0 +1,201 @@ +'use client' +import { getData, putData } from "../../../sn/app/components/lib/api"; +import Modal from "../../../sn/app/components/ui/modal"; +import { VerticalTable } from "../../../sn/app/components/ui/verticalTable"; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import { Button } from "@common/components/ui"; +import { useEffect, useRef, useState } from "react"; +import { getLayerContentComponent } from "../../../sn/app/map/components/common"; +// TODO: +const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { + const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; + // const featureId = featureData.getId(); // 피처의 id 가져오기 + const featureId = typeof featureData.getId === 'function' + ? featureData.getId() + : featureData.fid; + const [layerNm, setLayerNm] = useState(""); // 레이어 이름 상태 + const [tableData, setTableData] = useState(null); // 피처 데이터 + const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보 + const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [buttonChk, setButtonChk] = useState(true) + const [fetchData, setfetchData] = useState(); // api 호출여부 + const contentRef = useRef(null); + const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 + const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부 + + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 + const getSelectedLayers = () => { + let result = null; + const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기 + let featureLayer: { get: (key: string) => any } = { get: (key: string) => null }; // 피처가 속한 레이어를 찾기 위한 변수 + layers.forEach((layer: any) => { + const source = layer.getSource(); + if (source && source.hasFeature && featureData + && typeof featureData.getId === 'function' && source.hasFeature(featureData)) { + featureLayer = layer; // 피처가 속한 레이어 찾기 + } + }); + // SearchModal 클릭 이벤트와 Gis 클릭 이벤트 구분하기 + if (featureLayer.get('lyrNm') === null) { + if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; + else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; + else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; + } + else result = featureLayer.get('lyrNm'); + + return result; + // return featureLayer.get('lyrNm'); // 레이어 이름 출력 + }; + + useEffect(() => { + const fetchData = async () => { + if (featureId) { + const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 + setLayerNm(selectedLayerName); // 상태 업데이트 + const layerId = featureId.split('.')[0] + let data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); + setfetchData(data) + switch (selectedLayerName) { + case '하수처리시설': + setButtonChk(false) + break; + case '하수처리구역': + setButtonChk(false) + break; + case '중점우수토실': + setButtonChk(false) + setChkMainSsc(true) + + // 데이터 전처리: 합쳐진 필드 추가 + data = data ? { + ...data, + discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}` + } : {}; + + // 수량/수질측정 정보 데이터 호출 + const additionalResponse = await getData(`v1/onsemiro/swgRain/${data.adst_dstr_cd}/${data.manage_no}/mntrngs`); + setMntrngsData(additionalResponse.content); + break; + case '우수토실': + setButtonChk(true) + break; + case '하수관로': + setButtonChk(true) + break; + case '오수펌프': + setButtonChk(true) + break; + case '빗물펌프': + setButtonChk(true) + break; + case '하수맨홀': + setButtonChk(true) + break; + default: + break; + } + setTableData(data); + }; + } + fetchData(); + }, [featureId]); + + // 저장 시 모달 띄우기 + const handleSaveClick = () => { + setIsModalOpen(true); // 모달 열기 + }; + + const handleModalSave = () => { + setIsModalOpen(false); // 모달 닫기 + btnClickEvent(); // 저장 이벤트 실행 + }; + + const handleModalClose = () => { + setIsModalOpen(false); // 모달 닫기 + }; + + const btnClickEvent = async () => { + const bodyData: Record = {} + if (buttonChk && contentRef.current) { + const inputElements = contentRef.current.querySelectorAll('input'); + const selectElements = contentRef.current.querySelectorAll('select'); + // 카멜케이스 변환 함수 + const toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + // select 태그의 값 가져오기 + selectElements.forEach((select) => { + const camelCaseId = toCamelCase(select.id); + bodyData[camelCaseId] = select.value; + }); + // input 태그의 값 가져오기 + inputElements.forEach((input) => { + const camelCaseId = toCamelCase(input.id); + bodyData[camelCaseId] = input.value; + }); + if (layerNm === '하수관로') bodyData['lyrTypeNm'] = 'tb_ms_swage_ppl' + else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr' + else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' + else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' + else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl' + bodyData['fid'] = featureId + const data = await putData(`v1/fclts/${featureId}`, bodyData); + if (data.status == 200) { + alert('저장되었습니다') + setTableData(data) + setfetchData(data) + } + } + } + type ContentComponentProps = { + featureData: any; + chkMainSsc: boolean; + additionalData: any; + }; + const ContentComponent = layerNm ? getLayerContentComponent(layerNm) as React.ComponentType : null; + return ( + + ) +} +export default FeatureClickEventPopup; \ No newline at end of file diff --git a/apps/wr/app/map/govSidoSigunguSearchForm.tsx b/apps/wr/app/map/govSidoSigunguSearchForm.tsx new file mode 100644 index 000000000..127d02f78 --- /dev/null +++ b/apps/wr/app/map/govSidoSigunguSearchForm.tsx @@ -0,0 +1,114 @@ +import { useWrStore } from "@/app/store"; +import { getCityOrSgg } from "@/components/lib/func/commom"; +import { + type ReturnType +} from "@common/business_components/ui"; +import { SelectBox } from '@common/business_components/ui/select'; +import React, { useEffect, useRef, useState } from 'react'; + +export type comCodeState = { + code: { + isInit: boolean; + chBuCd: SelectDataType[] + rainFcltCd: SelectDataType[]; + bdstUsgCd: SelectDataType[]; + cityCd: SelectDataType[]; + sggCd: SelectDataType[]; + rvbsnCd: SelectDataType[]; + mdmbsnCd: SelectDataType[]; + lgbsnCd: SelectDataType[]; + lclgvrnmntCd: EnvPartSelectDataType[]; + orgwtKndCd: SelectDataType[]; + yn: SelectDataType[]; + buildingType: { + RST: string, + RNB: string, + HTB: string + } + }; +}; + +type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) +} + +type EnvPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +type SearchFormLeftProps = { + isShowPart?: boolean | undefined; + children: React.ReactNode; + selectSidoData?: SelectDataType[] | []; + selectSigunData?: SelectDataType[] | []; + selectRegionData?: SelectDataType[] | []; + selectUnitWaterShedData?: SelectDataType[] | []; + selectMiddleRegionData?: SelectDataType[] | []; + selectEnvPartData?: EnvPartSelectDataType[] | []; + selectEnvPartSidoData?: SelectDataType[] | []; + selectEnvPartSigunData?: SelectDataType[] | []; + + setRtnPartValue?: (value: string) => void; + setRtnSidoValue?: (value: string) => void; + setRtnSigunValue?: (value: string) => void; + setRtnRegionValue?: (value: string) => void; + setRtnUnitWaterShedValue?: (value: string) => void; + setRtnMiddleRegionValue?: (value: string) => void; + setRtnEnvPartValue?: (value: string) => void; + setRtnEnvPartSidoValue?: (value: string) => void; + setRtnEnvPartSigunValue?: (value: { val: string, text: string }) => void; // setRtnEnvPartSigunValue prop 타입을 객체로 변경 + resetTrigger?: boolean; +} + + + +function GovSidoSigunguSearchForm({ + isShowPart = false, + children, + selectSidoData = [], + selectSigunData = [], + selectRegionData = [], + selectUnitWaterShedData = [], + selectMiddleRegionData = [], + selectEnvPartData = [], + setRtnPartValue, + setRtnSidoValue, + setRtnSigunValue, + setRtnRegionValue, + setRtnUnitWaterShedValue, + setRtnMiddleRegionValue, + setRtnEnvPartValue, + setRtnEnvPartSidoValue, + setRtnEnvPartSigunValue, + resetTrigger = false +}: SearchFormLeftProps) { + const { code, pbnstSttn, pbnstSttnSearch } = useWrStore((state) => state); + + const ref = useRef({} as ReturnType); + const codeData = getCityOrSgg(ref); + //환경청별 시도 시군구 + const [envPartValue, setEnvPartValue] = useState('0'); + // 환경청 + const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); + + return ( + isShowPart ? ( +
+ <> + onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={'rounded-lg bg-white truncate'} /> + onSelectValue(val.toString(), 'envPartSido')} value={envPartSidoValue} selectClass={'rounded-lg bg-white truncate'} /> + onSelectValue(val.toString(), 'envPartSigun')} value={envPartSigunValue} selectClass={'rounded-lg bg-white truncate'} /> + + {children} +
+ ) : ( +
+ {children} +
+ ) + ) +} + +export { GovSidoSigunguSearchForm }; diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx new file mode 100644 index 000000000..f6b967c05 --- /dev/null +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { useEffect, useRef, useState } from "react"; +import SearchModal from "../searchModal"; +import FeatureClickEventPopup from "../featureClickEventPopup"; +import { mapService } from "../service"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // SearchModal, GisContent 등에서 호출 + // const onFeatureSelect = (data: any) => { + // let map = gisMaps.current; + // if (!map) { + // console.warn("지도가 초기화되지 않았습니다."); + // return; + // } + + // // 항상 같은 select 객체 사용 + // const select = GisApp.Module.select as any; + // if (!select) return; + + // // 1. 기존 선택 초기화 + // select.resetSelectedFeature(); + + // // 2. data가 OpenLayers Feature 인스턴스인지 확인 + // let foundFeature = null; + // if ( + // data && + // typeof data.getGeometry === "function" && + // typeof data.setStyle === "function" + // ) { + // // 이미 Feature 인스턴스라면 그대로 사용 + // foundFeature = data; + // } else { + // // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + // const featureId = data.fid || data.ftrIdn || data.id; + // map + // .getLayers() + // .getArray() + // .forEach((layer: any) => { + // const source = layer.getSource?.(); + // if (source?.getFeatureById) { + // const feature = source.getFeatureById(String(featureId).trim()); + // if (feature) foundFeature = feature; + // } + // }); + // } + + // // 3. 피처 선택 + // if (foundFeature) { + // let geometry = foundFeature.getGeometry(); + // if (geometry) { + // let extent = geometry.getExtent(); + + // map.getView().fit(extent, { + // duration: 300, + // padding: [50, 50, 50, 50], + // maxZoom: 16, + // constrainResolution: true, + // nearest: true, + // }); + // } + // select.selectFeature(foundFeature, true); + // } else { + // console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + // } + + // setFeatureData(data); + // setIsOpen(true); + // setIsSearchModalOpen(false); + // }; + return ( +
+ +
+ ); +}; + +export default Map; diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx new file mode 100644 index 000000000..a578dfac2 --- /dev/null +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -0,0 +1,154 @@ +"use client" + +import { Button } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import type { EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; +import { SearchFormRight } from "@common/business_components/ui/search_form_right"; +import type { SelectDataType } from "@common/business_components/ui/search_input_select"; +import type { ReturnType } from "@common/business_components/ui/search_base_form"; +import { MapSearchForm } from "./mapSearchForm"; + +type SearchBaseType = { + selectSidoData?: SelectDataType[] | []; + selectSigunData?: SelectDataType[] | []; + selectRegionData?: SelectDataType[] | []; + selectUnitWaterShedData?: SelectDataType[] | []; + selectMiddleRegionData?: SelectDataType[] | []; + selectEnvPartData?: EnvPartSelectDataType[] | []; + queryClick: (returnType: ReturnType) => void; + excelName?: string; + excelClick: (returnType: ReturnType) => void; + initClick: () => void; + btnClass: string; + ref: React.RefObject; + isAdmin: boolean; + leftClassName?: string; + rightClassName?: string; + showQueryButton?: boolean; + showInitButton?: boolean; + showApplyButton?: boolean; +} + +export function MapSearchBar({ + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + btnClass, + excelName, + queryClick, + excelClick, + initClick, + ref, + isAdmin = false, + leftClassName, + rightClassName, + showQueryButton = true, + showInitButton = true, + showApplyButton = false +}: SearchBaseType) { + const [partValue, setPartValue] = useState("A"); + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [sidoText, setSidoText] = useState(""); + const [sigunText, setSigunText] = useState(""); + const [regionValue, setRegionValue] = useState("0"); + const [untitWaterShedValue, setUntitWaterShedValue] = useState("0"); + const [middleRegionValue, setMiddleRegionValue] = useState("0"); + const [envPartValue, setEnvPartValue] = useState("0"); + const [resetTrigger, setResetTrigger] = useState(false); + + const queryVal = () => { + console.log("조회버튼 클릭"); + queryClick({ + partValue: partValue, + sidoValue: sidoValue, + sigunValue: sigunValue, + regionValue: regionValue, + unitWaterShedValue: untitWaterShedValue, + middleRegionValue: middleRegionValue, + envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, + }); + } + + const initVal = useCallback(() => { + setResetTrigger(true); + initClick(); + }, [setResetTrigger, initClick]); + + useEffect(() => { + if (resetTrigger) { + setResetTrigger(false); + } + }, [resetTrigger]); + + const excelDownload = () => { + excelClick({ + partValue: partValue, + sidoValue: sidoValue, + sigunValue: sigunValue, + regionValue: regionValue, + unitWaterShedValue: untitWaterShedValue, + middleRegionValue: middleRegionValue, + envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, + }) + }; + + useEffect(() => { + if (ref && ref.current) { + const customRef = ref.current; + customRef.partValue = partValue; + customRef.sidoValue = sidoValue; + customRef.sigunValue = sigunValue; + customRef.regionValue = regionValue; + customRef.unitWaterShedValue = untitWaterShedValue; + customRef.middleRegionValue = middleRegionValue; + customRef.envPartValue = envPartValue; + customRef.sidoText = sidoText; + customRef.sigunText = sigunText; + } + }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]); + + return ( +
+ {/* 검색 조건 부분 */} + + {/* 버튼 부분 */} +
+
+ {showQueryButton && } + {showInitButton && } + {showApplyButton && } +
+
+
+ ) +} diff --git a/apps/wr/app/map/mapSearchForm.tsx b/apps/wr/app/map/mapSearchForm.tsx new file mode 100644 index 000000000..00ba8e6f0 --- /dev/null +++ b/apps/wr/app/map/mapSearchForm.tsx @@ -0,0 +1,347 @@ +"use client" + +import React, { useEffect, useState } from 'react'; +import type { SelectDataType } from "@common/business_components/ui/search_input_select"; +import { SelectBox } from '@common/business_components/ui/select'; + +/** + * 검색 폼 왼쪽 컴포넌트 타입 + * + * @param isShowPart 구분 조회 여부 + * @param children 자식 컴포넌트 + * @param selectSidoData 시도 데이터 + * @param selectSigunData 시군구 데이터 + * @param selectRegionData 유역 데이터 + * @param selectUnitWaterShedData 단위유역 데이터 + * @param selectMiddleRegionData 중권역 데이터 + * @param selectEnvPartData 환경청 데이터 + * @param selectEnvPartSidoData 환경청 시도 데이터 + * @param selectEnvPartSigunData 환경청 시군구 데이터 + * @param setRtnPartValue 부분 값 변경 핸들러 + */ +type SearchFormLeftProps = { + isShowPart?: boolean | undefined; + children: React.ReactNode; + selectSidoData?: SelectDataType[] | []; + selectSigunData?: SelectDataType[] | []; + selectRegionData?: SelectDataType[] | []; + selectUnitWaterShedData?: SelectDataType[] | []; + selectMiddleRegionData?: SelectDataType[] | []; + selectEnvPartData?: EnvPartSelectDataType[] | []; + selectEnvPartSidoData?: SelectDataType[] | []; + selectEnvPartSigunData?: SelectDataType[] | []; + + setRtnPartValue?: (value: string) => void; + setRtnSidoValue?: (value: string) => void; + setRtnSigunValue?: (value: string) => void; + setRtnRegionValue?: (value: string) => void; + setRtnUnitWaterShedValue?: (value: string) => void; + setRtnMiddleRegionValue?: (value: string) => void; + setRtnEnvPartValue?: (value: string) => void; + setRtnEnvPartSidoValue?: (value: string) => void; + setRtnEnvPartSigunValue?: (value: string) => void; + setRtnSidoText?: (value: string) => void; + setRtnSigunText?: (value: string) => void; + resetTrigger?: boolean; + isAdmin?: boolean; +} + +export type EnvPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +const selectPartData: SelectDataType[] = [ + { text: "행정별", val: 'A' }, + { text: "유역별", val: 'R' }, + { text: "환경청별", val: 'G' }, +]; + +const selectType: SelectDataType[] = [ + { text: "행정별", val: 'A' }, + { text: "유역별", val: 'R' }, + { text: "환경청별", val: 'G' }, +]; + +function MapSearchForm({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { + /******************선택된 데이터******************/ + const [partValue, setPartValue] = useState('A'); + //행정별 시도 시군구 + const [sidoValue, setSidoValue] = useState('0'); + const [sigunValue, setSigunValue] = useState('0'); + const [sidoText, setSidoText] = useState(''); + const [sigunText, setSigunText] = useState(''); + //유역별 + const [regionValue, setRegionValue] = useState('0'); + const [unitWaterShedValue, setUnitWaterShedValue] = useState('0'); + const [middleRegionValue, setMiddleRegionValue] = useState('0'); + //환경청별 시도 시군구 + const [envPartValue, setEnvPartValue] = useState('0'); + // 환경청 + const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); + + /****************** 셀렉트 리스트 데이터. ******************/ + // 조회된 조건에 맞는 시도 시군 유역 + const [filteredSigunData, setFilteredSigunData] = useState([{ text: "전체", val: '0' }]); + const [filteredUnitWaterShedData, setFilteredUnitWaterShedData] = useState([{ text: "전체", val: '0' }]); + const [filteredMiddleRegionData, setFilteredMiddleRegionData] = useState([{ text: "전체", val: '0' }]); + // 조회된 조건에 맞는 환경청 시도 시군 + const [filteredEnvPartData, setFilteredEnvPartData] = useState([{ text: "전체", val: '0', adstDliStrCd: '' }]); + const [filteredEnvPartSidoData, setFilteredEnvPartSidoData] = useState([{ text: "전체", val: '0', adstDliStrCd: '' }]); + const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState([{ text: "전체", val: '0' }]); + + // 시군구 세팅 + const initializeSigunData = (sidoCd: string) => { + if (sidoCd === '0') { + setSigunValue('0'); + setFilteredSigunData([{ text: "전체", val: '0' }]); + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...selectSigunData.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd) + ]; + setSigunValue('0'); + setFilteredSigunData(newFilteredData); + } + } + + // 단위유역 세팅 + const initializeUnitWaterShedData = (regionCd: string) => { + if (regionCd === '0') { + setUnitWaterShedValue('0'); + setFilteredUnitWaterShedData([{ text: "전체", val: '0' }]); + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...selectUnitWaterShedData.filter((item: SelectDataType) => item.val.substring(0, 1) === regionCd) + ]; + setUnitWaterShedValue('0'); + setFilteredUnitWaterShedData(newFilteredData); + } + } + + // 중권역 세팅 + const initializeMiddleRegionData = (unitWaterShedCd: string) => { + if (unitWaterShedCd === '0') { + setMiddleRegionValue('0'); + setFilteredMiddleRegionData([{ text: "전체", val: '0' }]); + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...selectMiddleRegionData.filter((item: SelectDataType) => item.val.substring(0, 2) === unitWaterShedCd) + ]; + setMiddleRegionValue('0'); + setFilteredMiddleRegionData(newFilteredData); + } + } + + // 환경청 세팅 - 시도 + const initializeCityDataByEnvPartSido = (envPartCd: string) => { + const foundEnvPart = selectEnvPartData.find((item: EnvPartSelectDataType) => item.val === envPartCd); + const cityArray = foundEnvPart?.adstDliStrCd.toString().split('|').join('').split(','); + + // 시군구 데이터 + const envSigunData = selectSigunData.filter((item: SelectDataType) => cityArray?.includes(item.val)); + + // 환경청 시도 데이터 + const sidoArray = [...new Set(envSigunData.map((item: SelectDataType) => item.val.substring(0, 2)))]; + const envSidoData = selectSidoData.filter((item: SelectDataType) => sidoArray?.includes(item.val)); + setFilteredEnvPartSigunDataSelected(envSigunData); + + setFilteredEnvPartSidoData([ + { text: "전체", val: '0', adstDliStrCd: '' }, + ...envSidoData.map((item: SelectDataType) => ({ text: item.text, val: item.val, adstDliStrCd: '' })) + ]); + } + + // 환경청 세팅 - 시군구 + const initializeCityDataByEnvPartSigun = (sidoCd: string) => { + const envSigunData = filteredEnvPartSigunDataSelected.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd); + setFilteredEnvPartSigunData([ + { text: "전체", val: '0' }, + ...envSigunData.map((item: SelectDataType) => ({ text: item.text, val: item.val })) + ]); + } + + const onSelectValue = (val: string, type: string) => { + let selectedSido; + let selectedSigun; + let selectedEnvPartSido; + let selectedEnvPartSigun; + + switch (type) { + case 'part': + setPartValue(val); + if (setRtnPartValue) { + setRtnPartValue(val); + } + // 구분 + if (val == 'R') { + initializeMiddleRegionData('0'); + initializeUnitWaterShedData('0'); + } else if (val == 'G') { + setFilteredEnvPartData([ + { text: "전체", val: '0', adstDliStrCd: '' }, + ...selectEnvPartData.filter((item: EnvPartSelectDataType) => item.val !== null && item.adstDliStrCd !== '') + ]); + setEnvPartValue('0'); + initializeCityDataByEnvPartSido('0'); + } else { + initializeSigunData('0'); + onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); + } + + // 다른 selectbox 전체로 초기화 + onSelectValue(selectSidoData[0]?.val || '0', 'sido'); + onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); + break; + case 'sido': + setSidoValue(val); + selectedSido = selectSidoData.find(item => item.val === val); + setSidoText(selectedSido?.text || ''); + if (setRtnSidoValue) { + setRtnSidoValue(val); + } + if (setRtnSidoText) { + setRtnSidoText(selectedSido?.text || ''); + } + initializeSigunData(val); + onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); + break; + case 'sigun': + if (setRtnSigunValue) { + setRtnSigunValue(val); + } + setSigunValue(val); + selectedSigun = filteredSigunData.find(item => item.val === val); + if (setRtnSigunText) { + setRtnSigunText(selectedSigun?.text || ''); + } + setSigunText(selectedSigun?.text || ''); + break; + + // 유역 세팅 + case 'region': + setRegionValue(val); + if (setRtnRegionValue) { + setRtnRegionValue(val); + } + initializeUnitWaterShedData(val); + onSelectValue(selectUnitWaterShedData[0]?.val || '0', 'unitWaterShed'); + break; + case 'unitWaterShed': + setUnitWaterShedValue(val); + if (setRtnUnitWaterShedValue) { + setRtnUnitWaterShedValue(val); + } + initializeMiddleRegionData(val); + onSelectValue(selectMiddleRegionData[0]?.val || '0', 'middleRegion'); + break; + case 'middleRegion': + setMiddleRegionValue(val); + if (setRtnMiddleRegionValue) { + setRtnMiddleRegionValue(val); + } + break; + + // 환경청 세팅 + case 'envPart': + setEnvPartValue(val); + if (setRtnEnvPartValue) { + setRtnEnvPartValue(val); + } + initializeCityDataByEnvPartSido(val); + onSelectValue(filteredEnvPartSidoData[0]?.val || '0', 'envPartSido'); + break; + case 'envPartSido': + setSidoValue(val); + selectedEnvPartSido = filteredEnvPartSidoData.find(item => item.val === val); + setSidoText(selectedEnvPartSido?.text || ''); + if (setRtnEnvPartSidoValue) { + setRtnEnvPartSidoValue(val); + } + initializeCityDataByEnvPartSigun(val); + onSelectValue(filteredEnvPartSigunData[0]?.val || '0', 'envPartSigun'); + break; + case 'envPartSigun': + setSigunValue(val); + selectedEnvPartSigun = filteredEnvPartSigunData.find(item => item.val === val); + setSigunText(selectedEnvPartSigun?.text || ''); + if (setRtnEnvPartSigunValue) { + setRtnEnvPartSigunValue(val); + } + break; + } + } + + // 리셋 함수 + const resetAllValues = () => { + setPartValue('A'); + setSidoValue('0'); + setSigunValue('0'); + setRegionValue('0'); + setUnitWaterShedValue('0'); + setMiddleRegionValue('0'); + setEnvPartValue('0'); + + setFilteredSigunData([{ text: "전체", val: '0' }]); + setFilteredUnitWaterShedData([{ text: "전체", val: '0' }]); + setFilteredMiddleRegionData([{ text: "전체", val: '0' }]); + setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd: '' }]); + setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd: '' }]); + setFilteredEnvPartSigunData([{ text: "전체", val: '0' }]); + + if (setRtnPartValue) setRtnPartValue('A'); + if (setRtnSidoValue) setRtnSidoValue('0'); + if (setRtnSigunValue) setRtnSigunValue('0'); + if (setRtnRegionValue) setRtnRegionValue('0'); + if (setRtnUnitWaterShedValue) setRtnUnitWaterShedValue('0'); + if (setRtnMiddleRegionValue) setRtnMiddleRegionValue('0'); + if (setRtnEnvPartValue) setRtnEnvPartValue('0'); + if (setRtnEnvPartSidoValue) setRtnEnvPartSidoValue('0'); + if (setRtnEnvPartSigunValue) setRtnEnvPartSigunValue('0'); + }; + + // 리셋 트리거 변경 감지 + useEffect(() => { + if (resetTrigger) { + resetAllValues(); + // 리셋 후 트리거를 false로 설정 + if (setRtnPartValue) { + setRtnPartValue('0'); + } + } + }, [resetTrigger]); + + return ( + isShowPart ? ( +
+ onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined} /> + {partValue === 'R' ? ( + <> + onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={undefined} /> + + ) : partValue === 'G' ? ( + <> + onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={undefined} /> + + ) : ( + <> + onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined} /> + + )} + {children} +
+ ) : ( +
+ {children} +
+ ) + ) +} + +export { MapSearchForm }; diff --git a/apps/wr/app/map/searchModal.tsx b/apps/wr/app/map/searchModal.tsx new file mode 100644 index 000000000..d5dfaccc2 --- /dev/null +++ b/apps/wr/app/map/searchModal.tsx @@ -0,0 +1,82 @@ +"use client" + +import { useWrStore } from "@/app/store"; +import { btnClass } from "@/app/store/common/code"; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { MapSearchBar, type ReturnType } from "./mapSearchBar"; +import { Button } from "@common/components/ui"; +import { SearchFormLeft, type EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; +import { SearchFormRight } from "@common/business_components/ui/search_form_right"; +import type { SelectDataType } from "@common/business_components/ui/search_input_select"; + +const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; + +const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); + +interface SearchModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFeatureSelect: (row: any) => void; +} + +// 날짜 조합 함수 +const getDateString = ( + year: string, + month: string, + day: string, + defaultDate: string, +) => { + if (!year && !month && !day) return defaultDate; + const y = year || defaultDate.slice(0, 4); + const m = month || defaultDate.slice(4, 6); + const d = day || defaultDate.slice(6, 8); + return `${y}${m}${d}`; +}; + +const SearchModal: React.FC = ({ + open, + onOpenChange, + onFeatureSelect, +}) => { + + // 로딩바 + const [isLoading, setIsLoading] = useState(false); + // 상세 정보 조회를 위한 상태 추가 + const [popupFeature, setPopupFeature] = useState(null); + const { code, pbnstSttn, pbnstSttnSearch } = useWrStore((state) => state); + const [isChHead, setIsChHead] = useState(false); + const ref = useRef({} as ReturnType); + + return ( +
+ +
+ ); +}; + +export default React.memo(SearchModal); diff --git a/apps/wr/app/map/service/index.ts b/apps/wr/app/map/service/index.ts new file mode 100644 index 000000000..c6bfcff72 --- /dev/null +++ b/apps/wr/app/map/service/index.ts @@ -0,0 +1 @@ +export * from './map' diff --git a/apps/wr/app/map/service/map.ts b/apps/wr/app/map/service/map.ts new file mode 100644 index 000000000..bf0c8a694 --- /dev/null +++ b/apps/wr/app/map/service/map.ts @@ -0,0 +1,47 @@ + +/** + * 관망도 API + */ +// const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` +const API_URL = "http://con.mind-one.co.kr:11083/sewerage-service/api/v1/map"; + +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(`${API_URL}/lyrStngs/WR_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index ff6ef558d..c24d49500 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -136,6 +136,18 @@ export default function Home() {
+ + {/* 시설 현황(지도) */} +
+

시설 현황(지도)

+
+
+ +
+
+
); diff --git a/apps/wr/public/images/map/layericon/BML_CBAS_PS.png b/apps/wr/public/images/map/layericon/BML_CBAS_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..5ff4add3ef67899ca56ef065952b919b8426d086 GIT binary patch literal 1547 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`$o& z6x?pH#Hm*w=oo!a#3DsBObD2IKumbD1#;jCKQ#}S+KYh6+H&5{UIqrHdQTU}kcwM# zCdYbA4wN|lzxuh^-KyftM`9}$b|eHj#ydx=Nojj7R_fibWOhtktBRwzr;AGL6JLcX zP9asojgm^Nvyv>YoZNEAS66w8U5Los;>%~ht$SX1yNy%n%YpY#=RBYD{eR7zVv*&a zHg7yw)KPTEN8<3l%tLM_g0*X7YcHPdDrJgV)VE?sNS)uzi4)u2J~Hz-$nY#Z{b5O+ zn|F2jglp2BSwbJzieC`a_ZJb3;`&^C;m;P%xwd6GyY5cNP>d5PEO@UsWuC6v_uqA2 ztlg|m)V8_oF?{%<)YbL4?(SgT9XV^8jz4d=GUMYsCQ?~=c0bqc2kI7fy9%~0;^Ao3 z`RnX=Zd=fT{>}|zGDZ)d-L3kexg^7*Ma_@fQ6Qz_jGbq|BgbUpPoIh+uUG9-IKJeJ zkN3)jv-tO9Wgb5Agg?Wzc8g~tflT*nqwc24D_r^eK(M07myPQdht9O1E(DK!JCiZNB zzeiH|raMOx65h_&*cMm)<@t$<#hNS+^COS+R2N!$F45U>`_2aQz0QG*+fLXNDYaDy zde2LpcWy(^SyL;u`NavPsi);W%v-$krbKb?sgs)yJid0_cHw8$;{vP29>hoX2aQPz!58&o`d My85}Sb4q9e0Czo2EdT%j literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/BML_ELEV_PS.png b/apps/wr/public/images/map/layericon/BML_ELEV_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..83970a3311924d9ae070ef863066a89d337b9ced GIT binary patch literal 1030 zcmaJ=O=#0#7!J-&H=Oh5qB14R5EX3xnr3ZS99`47f|;c&tQ+Xel5cI;k}pfX*{mSq zK(HP}$G{1C5cQ}>FT&gof)g+5%^y4ndKjXhh)2J4ZRf!nl6*fr&-1?D`#xVeJ#@0Y z?LZqr5bdc!C4}dC39#PPNJgb0wQj}2|%VSEe|q4Gv;R2KrcZw zQD!!Wa_T8bhZd~`7}~cS%qEE5KHt&wDS*fckT-3a`u5~0MVf|8jYL#dbrPUp4whUn zTpG&ir71mTP<Ae$+H~Ku=o{2oFGL+iSHq+4~3(-#-x-{2CA{}N~Q`3ITFJZi$%H^ zrlFf>xL7O}Xz+XpBSPMsjWj=Gd)+k!1$eq^I>>}J87OKKa2ClF&UE7lmQ$Cty=s|o z!5Ck27>;IxBh`Sa`hTcp)zKcxfX#gWDePtE9Kd9N2WMR!H*T^!2<1o#7ib8&SqP_V zRZJHkf?feSWMWt(kB*qO0gK+T3SL#El@H^S~Hzp+#1XESZgS>1n7} z|c$pZDIEW)4FZ(qHcU(W4F85)>pg6^Y}`XQc67Qzg=8-*GhEl z*!szpb4$(T=Fz1y@8_M4z4tyJX?=NxZrN=~p3x%oe+21+&n literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/BML_ETCP_PS.png b/apps/wr/public/images/map/layericon/BML_ETCP_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..df9e4957ba7412d8684ee344371ee72728a40899 GIT binary patch literal 961 zcmaJ=zi-n(7&Q$8tyBrYQkYIo2at$=#dZ@brfPi&jfATxv6PH;>}z5r_8I$voeqV< z0NsF*m7R$NF@O*YLnS1{!o@n9pC$mDh!ijJ*z`H=3ULf zK4)hbF7-poW|+!m8rseP5zs|FFVNXv@7}Y(b9J_@n1UG^sPF9@M5uMJYdHr4r|hzu zw?QS}gURvjGeoA)s)@f+L6`P*qNp6;%~) z0Z9-gUZAg95M@nKHE9D(AC^Xo+B;k@Ghoc@ZmdoW#Ly`&< zQHaL@vC~2j=VuBK#ZKgf#KQr|6zwh^5uK%(o*uyu=dwXOEfXympV}cWazb{b8PGKU z5B2>yIwnnYmhZX3u{916-$XGUMGkG;em)BoYDR=?f+Gv#;jD^{J|;Ns;}94v72Ifh zfs2#)<^*q=T0Mw~9XO~Cb(Sh{p66;Mp|&Ln5|owVhA39$vLM4!8J22>3^!y!m~kO? zMm`G2jO(6ps|&fA75tD!hA8sxBexb|A54y{d5d$|UZ^+ax{Gr$7IJx-44*ajpGMEN zsC%;M+_rQvH$Doe+aqf0C*Rb^^hzjoxMii^yT3P&j@Cacefo0+KDAEX{8+u3JASx) t!h9UQuyPmjU%tLNTw6ZeTUW-pi)+l=m;BEM?Vg!ks(N)7zTVz@^cR&XC_w-K literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/BML_STMS_PS.png b/apps/wr/public/images/map/layericon/BML_STMS_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..423f2e8fcb87d6c1b9192d1547a4cf97c3bb7292 GIT binary patch literal 1542 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDkc?Br}XH{644~kf%h=vIPQxAv!Z9mxW?6};@ zEo2|D%REmdk7w!IEnAsXx3Fee%g-}U76loozD!P z&)&Lq>vfBCkG(7`X&G_Lf*+s096UWhW9E%h;$|vw%~()q&4|I^Q@J~*G4D0Fh-ibWa?D}1IL z51*v6Bq*7&YTk!CH|nJ>%zXC!xsRIdY>y*~lO3j=&^@SJFq1VhVC9tnohe>l^nO*X zPnWL4S3e!)m$$clI?+9yv+1q9 z*{4e3_h$DN<(4m4J!?M0E|$|ij}FdN)#zfs_*bZgY4=sDBp2q})uC5rm1=ByboJdl z4oOayi!VzOt?$;mghbZQ`F8Kc3kw;yU3SLpZofGuJ+6AWS}R$@h)0*f*HnPlo#mZa zVdSe1!6_edtg0)s3{(zoGqu^X_eG->i}{RD4GRY41&6okY|)*q%AvciH|O7ve@!<; z9XA-woavCdymN6H^VUyh{3qjfojsf!BVmxh!!o0{)g_C`O{3{bg_KIf2h)PO>--B| zvZ<+0iiof8uQ}MVj8kwWqkQ*7VP(TL&+-z!A2_srZf)b%KiyAuP3k&0q0y_9{rFox zrd@uHp_4S`nU?gb-qe_7I&I14c^MYBSwknL=H!MamP>c=x+`vEeq{D5w?u~hVSvEz zuU4gr?rloTWkpWE+rB>DZ+(f4+~>6Yfm-4_#s0os7x9nFfMHhRx5p)mI~zeIv!|<{ J%Q~loCIHG1Mo<6% literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/BML_SYMB_PS.png b/apps/wr/public/images/map/layericon/BML_SYMB_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..497589bea2f5a6aa1ee6c8f4756a96a4ec8b6bf7 GIT binary patch literal 970 zcmaJ=O>5LZ7!DMr6beE;XfKYl2o>CXZ8mm?rfZvI+ZD5v?Lya!G@DF!Lz~PrnY!76 zSiSi(`~md`sL&NW)RR~7CVKPW!K+Xw+uf~)t|7^M@I24^&ig*|YNNI`J2O8c2*PZ2 z-Kz8PTJ}v}<^Nm1&s;v-WOkEnk{#A{V=S!rq=i8>blbR&UB9>g2`>r46bu?o)^r{r zkA$L|VZ=0~oGl1T<&?VKE@q&G+d*W&U++#J2z&!>6&=~3Chi35hcVtftTnvDU9aTB z@-kRT5f=zCb3q#JMF~m`IM79Wo^4AI3?OXRfaggyoef};7=xl%$a}J)09_Xqt*GnD zJ)p{pD#`rS^NNOwI#Tb0;RAWJ*xx~Qt1^tmR|f1bMv)|SyIrwc5J}vYlv1gbX{c(R zBl1ZvVs4s`lH5qa!ig6Llm#RLnWEbw2h4yx)AJ*QbSxVs!!q%LNvTUEMU=B6jew5x ze`pwv(Fv>Li+nE?P8vOmr8-W?LG1Cy?c}mh6qzx08HpQ&?2W3p(IJc^9YTS*se^^B zAo58!xjVo+4yr~8b0ZH|Edz1|F$jF5n59+Iv}LWRX^LWNEB0!^UepR^MJ;K%x;Wxm z#5)La#712Ig3H@9fty(&q&%{P%BV~>K zr_rM=?w)Kqwk=yNSre)!A4}Si+a6FgYuBu(L-mGrF F{0katFXsRN literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_BYST_PS.png b/apps/wr/public/images/map/layericon/RDL_BYST_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..1e78cba6874b6dd9f8f3d1ce95a120e0ec52dd5e GIT binary patch literal 1196 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`Vr*gN=xSkTV&ZIRXy|HU;pXCG>E>i^;OJuN zH{644~kf%h=vIPQxAvIJCflpNy)(IT{C~%tb#?nlypKc4j32kqDx;@2v0ppbYTLR2w=B#nrQhxMN z0~6ou_h|hk2c&?`pw(~ZH5-Rb%% z>W_l_=KW`mR7zYRop6!M=Ak)1)?9S%C(mwwxC`_SR{9qWy( z>`Nzha87l7^inrrRmIZD+()1IyuQV;=ZLbNYoCeyqW;5N`DTH>;l?gXf-{%Me3a78 zU#$Fl&YwHVmoH8<&Uno@^U2T=Lp>FVdQ I&MBb@01j)NZvX%Q literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_CROS_PS.png b/apps/wr/public/images/map/layericon/RDL_CROS_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..57b5f2ca51d03eb08728f311091e33ba37747c4f GIT binary patch literal 1083 zcmaJ=T}TvB7#$Kz{443Dg6K98CA2#;yX)vqZlXK8?nWcFyP!dV>bp*LIYk zh<*rs2`mW0iU@0a5zcpcp&Y>KL`aZFiBLkwDByJm#wuMfv)ps69csX}5UGYmVl zSdRshJq;0-3wp3=I|_I31!FlWLY4K^R#O zRCxU!h2=QF^Ik4c!SmcM;A1%-!;+WxZ~>u$7krgq>7hw9OKlSpVst4MS%v5hj7@=I zGMS7wdaQLu>)abcOVmllRVhjq!}v8*yRg&NfP3Qjb%fDaWO;_1+S*50vq-P{FPN= zAP}zQxJV!v;H#?GfUhnb^>ds*nB|H{>C&Nrvs`tB8_CIat)QDEvIs5h2vnmM(!s)! z1ub_jbvgBxxN7cP!a2DNNrrJ7`&Xm0Tf{x?blJ9Kv21*35Vu>z)(>9$zL6_&J1*9y zoar~onUV=mJbd%$z2fu~Y$<&TipFc!4;2^PABhd`+`e~uZeYiOyRUwHIs?kC-a3%p zesbtE7p<-S@Yuj*1NoQi%SA8k`9o{o{gnIfT+tKhUvuXZsr2>4i&)=4|J%G%qhrsM zgV*xQpG@rU9m$+yKTf{ypV}~yoV*y_{kfisK8hTldOR~xcY*hxZ0tQX8kN!<`(?J^ lhPG<1=9s*;mmi$nI!?vb^=)?A=1bk{9FH`JgLN$@{s3DZXz>65 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_ETCT_PS.png b/apps/wr/public/images/map/layericon/RDL_ETCT_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..29c0b77b4ee74276df8de9416fe284e4c8bc15ec GIT binary patch literal 1016 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 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_STAT_PS.png b/apps/wr/public/images/map/layericon/RDL_STAT_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..13d8ea6899c02aa0d0917b630ca9f697783fd6ff GIT binary patch literal 1075 zcmaJ=&rj1(9512<`H{#)kqa+fOw_P--MZ=;1{vKZiMjYDVVY{1~JIMQOyX^pH}bFKvP5XU_kOprU zru$RM^q3M<>7H)TRS<}P4zUah`nX{Wg%G`?E0Dc&%+O#5g2zJiZcu3{0Yqd$5b*fj zikD>p&wE%d!1HV;@OfDu<0UWeW;r3i3%=7}@1cn|OC1rCk=ULuVuk1|#-_k9`F!4! z_j`~v%CNy;(9!Vu+yvparwl9?+=lI{C`6#GSel77WB^A|&Y&C)(InE{6m+vHYuI~b zA_Zd#vdOR>uai;*C`tc^>UtGz<0SkO?>~j@)RYOCB(zb^Qb^-QT#hSK5G^QUWTg-q zuT(LSMHtyxWCC%32PXzKLq&P}GZk(GDLEY)xWZNcaQT{CXB2dkc#c3zn}ll2LOR$|c$p9Fgg9w$-sEMs@I^L8jdzV}1OY-y&DyRy-0;6<%ew%H^JV zzVupXjlM;>ZWo{HyvOCIS2I^-|6J?Z0Wkk`VB^ijLzBFB=EJsl<;(3=cId&}xjPGs zpsi_Q`Qf|pQtxK_1*XV#|N6FYsjW08nU6nin%`GDdOo)o{msL(3qL#dFVEMxR;76pH}7 QeD7T2c(gyV)H{6nH~m6mg#Z8m literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_STLT_PS.png b/apps/wr/public/images/map/layericon/RDL_STLT_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..489524b5c4cd57a22db674f7ad8897be209f617b GIT binary patch literal 1089 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 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/RDL_TRSN_PS.png b/apps/wr/public/images/map/layericon/RDL_TRSN_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..15675cb315243b6ee8ea7977c337e3387ddcb6d5 GIT binary patch literal 1375 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 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png b/apps/wr/public/images/map/layericon/SWL_CLAY_PS_RED.png new file mode 100644 index 0000000000000000000000000000000000000000..0e5450688f9dda207fc2f3679b15d7b2a3e3c982 GIT binary patch literal 468 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 Ao&W#< literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GRAY.png new file mode 100644 index 0000000000000000000000000000000000000000..e19f3d8b36c0d5c05f2bd0156cde3a42819d0733 GIT binary patch literal 1054 zcmX|9dq`7p6uxS{W{M1_SrG(+mJc#5qoUBvC8e2)FwNA|d{&~SNl+mv^+)tjs1Qpb zQ6$ZUt_RM?bZ$PcZnNp;eCwuOXD(BlufBDo1NYo>&+nY`ec$=rCn3RXYsOv%K@iq~ z7yLunJhM52Yy@7!N znkotahnOVEv9U2=p$3zY2O@}}7@)|Y1$zecf0=Y8Es!pzR|G`N1To;^3NV>Qq_smP z2tzw=Q>9V?bbNdq3kP#t7IdlA>V<^`tyT-|*h2}+wg%UqpB)+;MwpwMGnsaFES86d zM?e6ZeGQhBl$?^1T3A?ER#skKSI6ZxL8q_-+(jh$U7=8o0>A+Acl@kct#m> z7PU)DKYjoQKtB!kYjt&Pef_t=ut2?j1BQif{j=?D>5EJa*jQ#<;A9bmO`G8{65Xy! zz@K^`(BC)QB`m(h=C!F^P^vGpus$ zSbd(DC`fm>!^>G&;eGb;Q9IoTI!Z7&zs_gm1hvLUF$KKK{)#73_zbO5o->0`*PraP$y&H0OnXz(n98C)DhF|>VT6Enx zr(R!jQvSZ%s9=x(N_AxX)mPa*iKpJ=rTol)bV8V&>Y|oyXP%S!up87^TGbLezEy~00DNQ ACIA2c literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_GREEN.png new file mode 100644 index 0000000000000000000000000000000000000000..4503c13c63f182dc291aaa8184b523c09d5363ea GIT binary patch literal 791 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%Gqe0X`wFK=GhSDbcfYJD<#+{eNwEZ|bc7>mvG6X8+%izqPLHL}%)X;?@VV zLVFU!CnV4KzX3?j{J%bKZf@o2-dX?G1I0u8l453Ommlv+T~!)6C9UFQSHi;l@QJC} zo2p9>w}thmL`+GW`F}&g{M@XKRoyQaL`+FfSzcUvxIKPeZr=8~%G2F77bb-EC#9__ z4V#bxR2efXD|=&A{;r0C-HkK;t`F*nid)x-6t8A$@fj(5~27Sxq;lhV>@{ zwWKU73hIoHn3S5oqhZ>wHNiazQPVTx=H|rB%k^)L_HT&}?1%#z64VJq@xfj3!QBZ! zb3%F(LqRAB=zg#{$>9@I!Y8Ey&54+t3iL?i)byyS>CrPXV`gRnofJDOJ7Ioa;-Z42 z#f3>r3X_)>r7SB>U0DKjPv-i{+^w~_+v@Um)aUPPDBRmra~6*gPoKYd@d^muym|NTJrI2Q{Q1k5Z{NQC`0?xa@4rCs4+u)H?VJlVm9xMj zvY3H^?=T269?xHq0u;RI>Ealo5uAL0?FI|)s#U9|{Fu40hEMSFr8!@EL~J?r4Cec~ zTvfSt`@H*2!_Z06Dhe;Mbh1N4gx2Iv7STGwA!4B6ZE$@h2aom%-%pz#Fmr`GZD~NS%GpH0X`wFK=DZq4znE`o5Y+y6N^bDf+{hlT>VKyjer*^Z9KLqb-$yHByRI~f|f(AjySt?ec+ufst> z{Wdm0L7>X{&dwXXyMOQ3>IE2}Omt8ObRpdr0rXZP9IKuiF-7N{L)3(##-?d@kc zILvf#06GI`3((n%TwE3dk*n(xSJ$PkuFKrqR=T?bow452bE}WfHXolIzP>yC{Pz0$ z9}EmU5)^bSIQV*0)Q#xqJ25ej;^UttBmljyCL^)~7}j1TL4LsuGC-iFW^3!}>gn0o z*f@Fe&Yin<@7}ZL(6wuK?%#j#`|;z~ z@85rc;2#i_UfVerXewucM`SSr1K(i~W;~w1A_XXT)6>NvC1))a~o;Hw^I^E9WE9-F zk@1}#s{*4cfA(&h`x|a_Jh<>eVgked)S4=%2LFH+ixNu~*abAWTAVuVWo&)O-#hBA zFN4N`1Im(*76ggCoxE64`4pE>!UK`(zS=!JyavL_?4KniW>knCI&|m@tHLLZhDJt) Y#s0d7PA`?`1A3Cd)78&qol`;+05Lm9=Kufz literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png b/apps/wr/public/images/map/layericon/SWL_MANH_PS_YELLOW.png new file mode 100644 index 0000000000000000000000000000000000000000..b7de1fbfdfdb1b373ac6c53c3541dbf14a53cc43 GIT binary patch literal 782 zcmW+ydrVSc6u*>KQl^!)thOx7(aYJ~np@chgBh0Q&_Albw8v~cmg})K*B{XPfj~&h zG<#@IZI(4lq?Sq33KcHOD=?5i;oi%=m*-GZJM`Q4o%4O?obPvj=et=_T#(FM#Y7M! zS+JWY1fLq;3+94lZ%ru$$2co2;392E#pJnk1G6V zLp181ot?$?HDEDR3YH%22>ZSV>@Q;hC$5*^1~~)?ecbv@jS-&~4!BN&>!f~TL)6Vq2E_98r{AU0orUdUj?S8@xdnYO%qa^hhhFlhF27=tUaqW0Zx^%isxReGXZ| zpa{7-XQn4Xl7ILfX=((1U;kxa{}pWD29Q7vHh2ru-Nxe!WY@s3xL%6uWjHDa!N35G zKrJXFhVJ@@>-@(1eq%i-rz}mBwVAR$rL51WU(adVE07WR{U+$_2#$UTj&_Dzs?eX$ zVb9md__xS-Z;Uj9yb0Pq8FEdAJuv=u1Ns2|y6~K^xD;6f$jZvk=ko;uu~=MNt5S8T z)t^4~m`rwu!|8UrJs!`v*NXuNf+Q(GFcb=hqtR$AHaRsl4VVF(H>nzdHK}U%fwKs* zSQFojzSA#{K+r1Samv`Ghr8!l5|V#vwRdCppIltLWT>fMIn2vX%FcOI#4~38vgrz} z+XRZ$nHk>WTTUp~E`5EfHg)~C1!*~KX|8!=@}hx(Li)XPE>l@~?MhbW^{W$8He2;3 zQG%lJ0gJ0)vA9O}o*vE|f$&T^n%A*@GMAP+)67MiCCr66%MQ;E nrLU~Ju)Ig3i6mwO*&;Cm>D!;3^kS^F7=}ay{9;}kxBSvSe_-5< literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_SPEW_PS.png b/apps/wr/public/images/map/layericon/SWL_SPEW_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..d260ea3a668f2d7fba866de62fb10c4220409ca9 GIT binary patch literal 1217 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDkcWM*z=V&ZCSVrt}UXy|HUX=-NdXzJu@Y3^d? z=H_Aq)9aF-T$-DjR|3Y4 z5WOk5-C~4OuRhQ*`k;tKifEV+F!g|#@MH_*z!QFI9x$~R0h6`2pt1!61EZ;@i(^Q| zt)xHy|JyUGHn7fo{OXzCg)Mh?ml-9QGA!A-f4#Wt$M^R258ssM-#>6wQThy@e{k1zgC6apd*5wu4cAOPFNvKfD zApKrVTPwG?(S}V1#v)>(&9RK-j6R=#`_C_wVwlw5-=Dr>O}-27k<;7XHCZa0^`CFs z?7gq@XOp8#4AV&tO@o!Im-T#TS~t(m=9SKxH4mgNGM-r=e7x`AQN7qb2Tn|yIMp@i zkfDKrgRh6UUQ9wkNohOxoBMl(eT21-F%=7W2&r_uaY;HdLqdZ^MEZcm)07wgV;W6) z{-^&&T#n)DpWjO{an^LB{Ts5>LH|o literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..84c7992e41cc3c681f1b6ea44d7059de78fa2a9a GIT binary patch literal 1067 zcmaJ=O-$2Z7_NeZAS8HlkQi!_D8}_?KZO;>&(`5)ERr#Wi-xZKHX3NZ+J3mPi-3s{ zNIa+qO-wW#^g=Y=NQ}{hEQ%)+Pa0!L1;jG|HLte`-hmK_Y8)lJMMi0r|IMdxD=w?~g zu#08F1!JB5038lMD~@*Xix|L|8uJ3w8Ltom{xY6`?Yv0|S+~>K@lyk?UANH*sVUSlSh!4p>kpcaAJ* zm2+V$>J_%=rIjQcW&Qo>3Unc)bwn6pL*j$in+GD#MPE9}4+= zzVPMNW8D>vMz@cjJldAt7(4&Tf%U#)>ifmRjBBfzeRyv^Sw-xxC*B-Hr>^D$ QPo3)NS%G|s0G|+7AT1;$q@$x_YisN8@1K^IR#H;Z+uOU`V#QvN0VP3x!3+-1ZlnP@ zoCO|{#S9F5he4R}c>anMprEa%i(`mJ@YEhlJ_ZF27DmB;|C4zmKX$bGZ8bi6NqUmP zgc-Z9PLfOg>^NS%G|s0G|+7AZ-}A$iHYqa^ud5Nrxt{IJbD~)%|B4Z8@v20y3Z^$S;_|;n|He zAcwQSBeIx*f$uN~Gak=hkpdL7^>lFz(FmT}W68&$z`?>O`0sx*Z{)|0R==&rM=wcF zQkXDf_ti;qsh_NS%G|s0G|+7ARRTqtY|@S)9Uoe+p1R_ox1hx(ld7tn!Zc302xpcNS%G|s0G|+7AYETyKXKy3dGqG2UcGwHo;^p89=(43`o{-TdO!x01o;IsI6S+N z2IO!SctjR6Fz_7)VaDV6D^h@hww^AIAsWF`do1}F6gXHI1^@j|=8gQ=(dxI=_~<3+ zNeUBY?7lilF7>nbgm>8;@jeTB3@wzopr E07Dc;WdHyG literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png b/apps/wr/public/images/map/layericon/SWL_SPOT_PS_RED.png new file mode 100644 index 0000000000000000000000000000000000000000..a2da36f9bd0c68654350c5ed8840a5a513e05c5b GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G|s0G|+7AT7?tWho)yuc(-&t=(#2G2PyNyRWbE`itTq14@GYf*Bm1-ADs+ zI14-?iy0XB4ude`@%$AjKtWqi7sn8d;Hf>9d<+U4ER2Hx{wMQBe(Y%V+iHCDlJq2n z2{U$Iog|m~*?Ypf?2dS!1wDoq%;y&@fxHa7-4%z4m{Oi^LYPn%|KSU0+4KLt~^wz7fAwAdM zd!m=3sQ%Qvk|E<@@Ez?T|5Lwe8W~3LOde-Z5mz-AQb_|9AV^u-3d}&wSiABDPEb@Q zZRYYguU?RJWHDNRVSLLWY>Jwg@*PbtLktRV#k3Rj{>xW1FpUJg6jeFZkzvW4Uw2`4 zJ)P6n%X-|PrzXLKFA)I?Vh#9K#r7mWLDzL9G7q*{8q^`UoS+X(<<$isBNu`w6A9@Y z&jV3pcp)l^{1^yxe3<3PD~5PMii%Qr95f!9M01Uzlu>3IvB)YxmoRoDmaSH+Of|wF zcZKES@pzyS4u=RLWQ7fQToS=!*gA^>MC2M<)VT!-1gh%)p_bJ`dpH9R{r*$f%dI((%|H*Wx;iObaWn|!NU{qxjNBYTmF6if zln_Q<2{}N{is0OmX&b2Oov-6nRZ7_&)@&W7lmtx_7}GQ)IVlT*m>d@bd79^EgjhVn z%h4DwipiJ~my=Ddg7j4j+PKLz4!N9vv`M$Sq^<957f+HaaV@1JbN*i8cda(nd1j;g z!TT*^Wbx6(9fjZCy~7P}2$w$Y4#FSLN9R7@_A|IO3kE|r_;+f{s2UANB{r; literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png b/apps/wr/public/images/map/layericon/TL_SPBD_ENTRC.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7de8593acb28bd70cc17bc07f51fe41104a3d4 GIT binary patch literal 1260 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`Vrt-GY+`0%VrpV;Xy|I1JVR zVPRv4~Pj*wm=R%;iu*SQ+p9GSzA2OwPs*oOz?DZ45_#^ zCD_*ch=V}w?%4$eVnq+wJEkUNK9=9Gb@S%yr~J6S1xR`N| zJvQ5C?o3u`PthfQ5-al93`?KC+#KK4)4bnrwtm;#UOhuS$xiNBH3#lG?~)07@@xLf z#2-q(E=e`?ujg1?p%eG;x!ZZrDK;@68tDnm{r-UW|S17>0 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPGF_LOC.png b/apps/wr/public/images/map/layericon/TL_SPGF_LOC.png new file mode 100644 index 0000000000000000000000000000000000000000..74b0adac361882af61b2f8d1a29c7a3f4c8063ad GIT binary patch literal 1383 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`Y~<|b;_T>bVrt}UXy|HU;p}W+;cDb!Vq)a# zW@79F)9aF-T$-DjR|3v4~Pj*wm=R%;iu*SQ+p9GS-(v#`NF`!c-zy(F{I+w zlF6sNr2|Edoj-f#$;Ca(9JefR6p#;PcHZT2l>INWj@BaXi5gdYx8E%dytG!^wII^9 z;Q6Ai0P%Mr0$G}wZ`C-QF0}G!?F%r@RGVL(=OMwJEN)q_&H28UHD=1T`s$be@TJYmI(%^28qHs(l4VPW8$&Ht~udsoCUlg3EBf%aQk< za+8_=3w>f;RDC(J(RRk{aIKoHR-!w579Do=_EN3?e(=nfB(E-2ls8_x|9s z?Gjfu@T|SeZF`sJvA?p~y*EU-oi*rrdt!T>V>egLkY4Wy)EqO6{v)NAK zVJLX;ASi<1P4OTeM3EutO)p+NO+>wU5k)sIz_C?!K5Ynwuc4|ly%C_< zjY7%VSg{nFoj3u;BS-~YL`)F5t6m5rm96VSI*+$`7SthRMP+xBDjPFE!vO+mE}5_d zQ3SHgiK(mD(qgclAq6?7C@f#VYgdmvUcD|n5lEX{QH2(I6f^}>$cD#DWbi6|0x`n)_uemQHa+9i#Bd)C=TUAEkGv0K?&p4W))|u znBcIAeW1KX>xUJt+~h}O|HGimD+M+E4V(5tfRm=kL+B4T~I$VblT>U&b8HRaP9WF z2yMAMO@@yf`&XlzThu-Av}IemXc-@Q)a?Pa_2WyMkX{K|y~$Gaw(@IdXQE3Il1Xjg zWapj2t%`oodU5Z;_tfT%g{|Q^ZNc9@@Z@;s>$685bzYa=b$@)LU+d^Kra!}=_pSv# z?0flp>cUpJZ?ST_|H_~+();}8)i3X#o_g59baXS{J`Ef`^K))HzJ&Q~LBF3_y!Zzu Cr9T`1 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_BANO.png b/apps/wr/public/images/map/layericon/TL_SPRD_BANO.png new file mode 100644 index 0000000000000000000000000000000000000000..8d08dca2996d0ea7bc0c52c6296323f9721077a4 GIT binary patch literal 1044 zcmaJ=PiWIn9F8j+b2uD$*;MqAq0G7FU)yXAtJ~VFu42pRit9nJY4WyaTk>M^X45gD z4h9Y;cb2r@l+n;-~+c#SjoqNvWqgq$X0m-n@|k^k`@y9yb4@OTzap&49MY)Fl1C=WrxP-bBoDq89C8#qi+ zK3dOYaW;8gQjx(Z9)@uZi?Atbc*M07bsl1nhqJmFp}%fEr-80T=;=_BOIk5l&?lB` zIJK0@s7v!|SffYIfMHi60tUniaE%4ik=zJf)s@KH+h%D{h2Z%Jy_ZxrISFFOh9JZQ z{3^%uKol817!pN(00lW6L2|Pt5j}~!+CYkOX!LS;#rc)~u zDH!W27Rxi7cceNnnfyQ0FdAqFr{SM`|0(QbN)}|(&_Ro~N*XuQ>xHtUm<<(->;vR3>uq|0Mj1NuXcAMDxacX&)T#4%mc`W0;&+qK+ zj`-T;c>LF;V^3DDb$31OUr~O3|GW;wL%!8=>xQ~*p1XAt!lP~F(OeG1AUtv5Mx|0Y z@a5futxAiws+IP&^@r{~-250nqWDHr7vGjo4{U!L9PJ6?*WX@#*etHCx5AxA<%4&( oT3+qfySraR`)*DM>bp*LIYk zh<*rs2`mW0iU@0a5zcpcp&Y>KL`aZFiBLkwDByJm#wuMfv)ps69csX}5UGYmVl zSdRshJq;0-3wp3=I|_I31!FlWLY4K^R#O zRCxU!h2=QF^Ik4c!SmcM;A1%-!;+WxZ~>u$7krgq>7hw9OKlSpVst4MS%v5hj7@=I zGMS7wdaQLu>)abcOVmllRVhjq!}v8*yRg&NfP3Qjb%fDaWO;_1+S*50vq-P{FPN= zAP}zQxJV!v;H#?GfUhnb^>ds*nB|H{>C&Nrvs`tB8_CIat)QDEvIs5h2vnmM(!s)! z1ub_jbvgBxxN7cP!a2DNNrrJ7`&Xm0Tf{x?blJ9Kv21*35Vu>z)(>9$zL6_&J1*9y zoar~onUV=mJbd%$z2fu~Y$<&TipFc!4;2^PABhd`+`e~uZeYiOyRUwHIs?kC-a3%p zesbtE7p<-S@Yuj*1NoQi%SA8k`9o{o{gnIfT+tKhUvuXZsr2>4i&)=4|J%G%qhrsM zgV*xQpG@rU9m$+yKTf{ypV}~yoV*y_{kfisK8hTldOR~xcY*hxZ0tQX8kN!<`(?J^ lhPG<1=9s*;mmi$nI!?vb^=)?A=1bk{9FH`JgLN$@{s3DZXz>65 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_NONM.png b/apps/wr/public/images/map/layericon/TL_SPRD_NONM.png new file mode 100644 index 0000000000000000000000000000000000000000..e68c05ab606783899526d999e99e088f96d1fa5e GIT binary patch literal 1049 zcmaJ=&rj1}7%o{D1R>Fr;)O5e0@1Z=*A~_=eyy7X5z(gFvb}F0F{V|2TivvH3b>*=kebj@9)g6a7GVae`r^VzI6_fA z+Q{Z`E_qqjkjbbXhN+r1VN=w|Sk+dwIfy|X77Z&x@4bFY14EC{Gh#BBw4<9J8TQk98-39$;Q=Dg*~)d*eJmC4-OW@%7|;JFBWWGa_TgD7$!5SdUw z3vwKgB!&}2N#ZU7KFINGki1fW6J$}6`5|!p&?K6p7vzi*KaNFK5xRu2EwgN;Qei3~ z202BR3x~s=2G0iwBH%7sSgi&ux4)sFKv#1N8ym<1o}!vZWgMZ2)1xDpc2m}JkJCgF z##U9E<(Qy%qy{jV{6Ew*n`jqj;ECUV3cJ}w8?qVbqOzlr!WH_xP_`U(po)=`MQFZ} z#dHZ_PqSF!>m@zCJRouZkKADsgmWx$Og9#-<69vXFbeWq}h9y205=36&xR?;; zL*j5q9Fk&TNg9rZ8e9cwWfNMs!PQT=LQAgK3Z_jWE6_2nLp|;w6V#6^8?C*NvbXYb z#MN7S;ahT9Vutk!`&XeGTcmruX|rv~qS^S+BHiwgwqAJN`<+~g)r2yUt$xiP93GDO zq^ULtlzqGv8aphab)BS@{E|puN^3+s_ zS>2(&JX!7=;&}e&EdkNb&ODv~(MRV7+QHeiQ{%O(v;Ot%b~~5;;d|l#wEJu2^N0D3 q9x&K__lB^tIlk_*VKLt}ezT3bGx}!tT6|ORE^8u|QeIBZuKWSyOihgd literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_RENM.png b/apps/wr/public/images/map/layericon/TL_SPRD_RENM.png new file mode 100644 index 0000000000000000000000000000000000000000..4800beaeb5d42b54c5c28451bcecc023bbbf7506 GIT binary patch literal 1009 zcmaJ=PiWIn9L_czGB(&{iUakLU7W1>*EU-oi*rrdt!T>V>egLkY4Wy)EqO6{v)NAK zVJLX;ASi<1P4OTeM3EutO)p+NO+>wU5k)sIz_C?!K5Ynwuc4|ly%C_< zjY7%VSg{nFoj3u;BS-~YL`)F5t6m5rm96VSI*+$`7SthRMP+xBDjPFE!vO+mE}5_d zQ3SHgiK(mD(qgclAq6?7C@f#VYgdmvUcD|n5lEX{QH2(I6f^}>$cD#DWbi6|0x`n)_uemQHa+9i#Bd)C=TUAEkGv0K?&p4W))|u znBcIAeW1KX>xUJt+~h}O|HGimD+M+E4V(5tfRm=kL+B4T~I$VblT>U&b8HRaP9WF z2yMAMO@@yf`&XlzThu-Av}IemXc-@Q)a?Pa_2WyMkX{K|y~$Gaw(@IdXQE3Il1Xjg zWapj2t%`oodU5Z;_tfT%g{|Q^ZNc9@@Z@;s>$685bzYa=b$@)LU+d^Kra!}=_pSv# z?0flp>cUpJZ?ST_|H_~+();}8)i3X#o_g59baXS{J`Ef`^K))HzJ&Q~LBF3_y!Zzu Cr9T`1 literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png b/apps/wr/public/images/map/layericon/TL_SPRD_RWNM.png new file mode 100644 index 0000000000000000000000000000000000000000..e68c05ab606783899526d999e99e088f96d1fa5e GIT binary patch literal 1049 zcmaJ=&rj1}7%o{D1R>Fr;)O5e0@1Z=*A~_=eyy7X5z(gFvb}F0F{V|2TivvH3b>*=kebj@9)g6a7GVae`r^VzI6_fA z+Q{Z`E_qqjkjbbXhN+r1VN=w|Sk+dwIfy|X77Z&x@4bFY14EC{Gh#BBw4<9J8TQk98-39$;Q=Dg*~)d*eJmC4-OW@%7|;JFBWWGa_TgD7$!5SdUw z3vwKgB!&}2N#ZU7KFINGki1fW6J$}6`5|!p&?K6p7vzi*KaNFK5xRu2EwgN;Qei3~ z202BR3x~s=2G0iwBH%7sSgi&ux4)sFKv#1N8ym<1o}!vZWgMZ2)1xDpc2m}JkJCgF z##U9E<(Qy%qy{jV{6Ew*n`jqj;ECUV3cJ}w8?qVbqOzlr!WH_xP_`U(po)=`MQFZ} z#dHZ_PqSF!>m@zCJRouZkKADsgmWx$Og9#-<69vXFbeWq}h9y205=36&xR?;; zL*j5q9Fk&TNg9rZ8e9cwWfNMs!PQT=LQAgK3Z_jWE6_2nLp|;w6V#6^8?C*NvbXYb z#MN7S;ahT9Vutk!`&XeGTcmruX|rv~qS^S+BHiwgwqAJN`<+~g)r2yUt$xiP93GDO zq^ULtlzqGv8aphab)BS@{E|puN^3+s_ zS>2(&JX!7=;&}e&EdkNb&ODv~(MRV7+QHeiQ{%O(v;Ot%b~~5;;d|l#wEJu2^N0D3 q9x&K__lB^tIlk_*VKLt}ezT3bGx}!tT6|ORE^8u|QeIBZuKWSyOihgd literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_BMAN_PS.png b/apps/wr/public/images/map/layericon/UFL_BMAN_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..f2f64a641080ceee0479ecc735869a2d146d2513 GIT binary patch literal 1552 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`U~b~(=3?e-V&V)8bypKdCpQZhM*~xHL!gkQ zs~JqMOMY@`Zfaf$Om7N8uM1ASprnvn0JPa9wJ5VJHN~wcKUV?lWvfiwZgIqE9#n4% zZnrq$)TknMZjcjY$NByz`)ev>EaktaVsa- zJ6kwV!nRzla-zv(6WwjgwuYFqF7;R`7OvFs-{DB>gH<~MjndRf` z8>)5!_Q!hHZrtpfBeZ@_lgl^c3alKiw!qDJTCl(e+cotnRx`{wgL(9y>Vk6&s_@7d|kjXWkvY@3-Zsea-q_pjo!R&$M&xmSp+iFZ8p;1CbbH`P92 zo~r^Yh0?7oc${x)hJX$z{qB|hbo5x@QV*jS;TNsM#Lx+ey4d~5DZlhm9f;x6qf#Huy< z=z-Gyg>A3qT{~#^@!Nw3;zenUOM@&gH|DK+CcW~+tY!DLr&{Hht-t<0Yg5>x>l1~_ zAFD*Y{&uU|@9IN|(-(GKP;vCk4$RbCt})+X_VyrBK7@LqWSUvzOU+aNBgp&6P&Bbs=8@=Wpjmv0Z%; zBZ?jh%K{-Rqn@M7Lt#WkP$WTxLC{N^6j@;wMFpC6MqTZpZJ3#J;rqV-KmY%ob$9n^AoWYK9q)c%PB$@|AZnsHQ&KtrB4yB~84+sg_Fan9)CjddC^DiM1MONv zj|G~0n$k*7hZ0t)=q|D*$72Bo{JG=pEx!v^>u$NNI$;zQwRmYU&HLVPh6UPY*OgiM~M zv)QaS>-R#djb_8)u%qGgc`(9bcN<8`c?`R%pdbKSu{0BD&>$T}Nrqi0Lg7poj-Z=M zvWC4_CR{K&Cz&+sWt<}wfTH++sID)eZIlAb`TkSbPIsGtP5~QsSqg4krpgIr@-YiY z2wG_f4;HGJY=;Qi?a(A+%^bOHzh9NC*)MD|j_cwxQcUwM9fTsCdnwWhJL(U*5Uy%1e7cKgtz{OiYLWwrb5yAAhB zFBAP+l7m;tHT|xn+&45d)Hg7&{z(P^kZ*locB6M@X7tPSPsmJ9|D1A1duNY)lH}pF htwPmy;!IUZshi-wpAg^66XVYPPt-RFSL#|1{{cK~fpGu; literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_BPIP_PS.png b/apps/wr/public/images/map/layericon/UFL_BPIP_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc3622a76eeccb7c555f8fd9f30f76ce8591f08 GIT binary patch literal 1017 zcmaJ=U2D@&7|tBbF*i^Ybl!N#5EN{_+N3nBt6j6Xf+?dbtRjA-$ypoL-_i7Wh(Q4sO*>A1d;Nw6rV*!SMU_*X6fBuj3oe{l z$mH~ec|B&(i7_zh%S6C}SOdOQwmsR8(@kBO%!6%~22BW_kJI}}<<)7BLM{X%6AtMd z&jU$f_=qS;`~@Izyufngl|p<(7A0ATg4RQmXs$6QXO+oTEV7EzC5#=JWvkUHQw=l7 zEwX$p77H{4Aw&=%uV!P-582*GTS0-I?wSrZkqrVxt$->xPLoXck6<|+S=(!si4=_W zHHYOHE;v#fsH*>mT2=?`;VeAJ_n*REuI4~C3q4eEb<()Gksy>Kr(CFE(nS_%9$7ZK=Mw9x*Ww!8 zb4hmPvLqQcXzX8&Zf_Cy1k;Xf$)aO?XcM=)#MbYA(#Oe_u+mC0=YKBjHX4cE!52$Q zjp+NAaN|R`uu-q~EH2l!A8&nHDh{$w6VU(h>+I^Q#RtQ8e($dL*H<5w?=COzHEw*l zvQoZ2*2C@X)DQO=Z`XF7Y;CUG|2TB2vhwW6ZDZ54zNH<*u#Rk>P(9XCg5894sLM$cg6$rAb zI0TDOlt)ke1mhUif-9x6UCzDENxDjiK8A2r1F>taQq_JTsCgaI{AntNVK!vslT=lqSac%-%)#cFK-zIUe3bBV1 z__L(SxdPyI6M`tw6P75N1}sa^ktoa3A;3^HLsIBv!*qm;vK+G;tUWl2X3B$HQOK;t zLaPK`wQYkVNyl*rr-#tZA(D>8V!j5$gb^ZajcT^&hBd3Zp&&p@G8MyCbPf25VnrXZ z6FAEBSqZAKE~{B<$Ak`ybVY-t3Cb_20nFw8AF8VBXv;3b=lTApuvHp0AX$W#K4MC! zaf97{D1+loDB8MN()HtwQ!G?>N-uS$C{gSMj+Vm(?s=)FhY{5;#&I z6h-FJDL$3qX+9E-Ff^Tx>|yzgAjBAoN-+YxXLo}u=+cM^HM_x;pL2z#T;B?+fg%gg zR8B%UW9lkcEtylA=aOowx5ky5=W?Jamqf{seq;aD=*AXuk3U_vEn2J_A8N?$CbIRf z`Q9ZA3v9~@sgnCk<&ozlTUtN5k@a4v1liM{eB3b=3eE<)H+e6e{VZ^+uj z-%G({`6v}1ot`Yz7H-tvOSfOVmn6u^4X4HVcb(wG?3+EGXOD>P^5V9>o|f9+r`V6b zZ*V*FcNX7XSbAgom*);19`77KmIf_PpDxeTA3oUIdE&gZ@WFHTj9{rL6yx4f>>1uxL?sQ0S?_~Jr( z{G3OfQ7|fa=r8YPePud+x5hC0^S5VfB!jh5*vQm3mCMO1bN=O#P3MIx2adh-4>{eP A^#A|> literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_BTRS_PS.png b/apps/wr/public/images/map/layericon/UFL_BTRS_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..557c17089a8e791685cbd7fed39d5418549c6531 GIT binary patch literal 1097 zcmaJ=O-K}B7#=mH{OONMvxCD(v4uOcv+j=0uu&^FA>_rvo%@B6*)^Ihv` zJ6%#(UPur`Nw`&v;Bmk6?JmIoiXUki4@XgR40Xa@l$0$%G^#KT$gnQ=fe4V*k-_&M zKoEJ97LB2pbWTv9PRkC4PU$9Q6GWgsWy(rFKx7>BX-1G*pPr>iO$}0Ae#s-5A&}5o zM=j7f+7?wt`xRcL>g&irO27g-Kr)%q2Mk+C1*r{P0neRnh9Wm0s6R+;CKZ!9$Plyu z>8GpRiic%Mj-y$hpX1mn((7Tpj0eA*oAn8PPVm-{TMvb!S!%Bk5nHxm;Z=}IAY=** zlT0S*WHk+~K8EFa-qG-S-5BAvM+_vV+=gA5Q4oQxSel76XpoMg9EU?FNa0L3kD!~| zvWC4?CR{KkC7TROdz>R>fRglosIG6LZ4?1J`TkSbj*ggsi2xf8Sqg4kZ>1B;6han| z5wxNZ4rHp>k$?!=322g`PL6bSX@&}u_Q?&rBne@|MzW!Ruo$GU0FO@h27&!w{43T+r|e5cDsdbtqfh7!B=7?EH*|{ z%kf|7bbaXT$lO@LQjQ{h$t^fi*4`SslX$Sc^5*J``(>_ORbO+LyjJY6G5PY)#QN&D z;_qDL!ra`=Ds_B?4ERDxyVKI7CrCo xxeoQ?r#_5*PUp`zlwR>vp6PB_lpho0L>>{GGM>WaPig0Jhnw5PsivOGe*l2=X6*m~ literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_GMNT_PS.png b/apps/wr/public/images/map/layericon/UFL_GMNT_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..6081d8b00f9a9d5a9e9ee32b353bd1ad1042a5b5 GIT binary patch literal 1166 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`?qX)?WMJuRVrpV+Xy|I<v4~Pj*wm=R%;iu*SQ+p9GS>M`mbt(e`Ba^3#V@SoV zHIr?*4mk+4syjM<5SS*XkX!Y3?Vg4kw>T^tm#Tkc2s0~t#h7E$u%JV+cT&&ivUG)i zOQP8AJno&TKDlZ4T4Tf75Vf9`eHTL43iKWFin!9gILbjwrTudz? z7MwhvG>c2qpIvHB&$xE|=9StQ=CX`CZCP2XE}hRfagNDoVN=Gl<843f68C+vsc1`6 zlw$C2YuNjD+sySRZ@;%%5b2R?S literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_GPIP_PS.png b/apps/wr/public/images/map/layericon/UFL_GPIP_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..5d7e86fd28d67d9256ec0e6f5b6c5a70b5e3038f GIT binary patch literal 1103 zcmaJ=Pe>F|93HbIBuS&fBJweU6}0nb-JP0gH{IQFYoQfe7u?XI*_r1$+02_}-nOer zsnx;i&>_lBNd$JP=p+egWnee2b*S(lRD^*A9fIwRx!R#^n3?y7@B4ng_xpWsq^kF(WJBO* z8a*n<^FR<7-scwten0SXyqD$3D|mRH$DL$j-35D2lFz*Xtn&kJD#jCGRnv`l5mi9o5z?tRoY+ib@(~agZjN{yBnSt;(9t zN|{K(*t}w~Jj1z1Dgxv2|3eL9746^zT+8>L!cMZ!f@}ghC~K>vaozQ9C`*diP{GJf zB6PM`#kL-Vk<)`L5NQ{{-VWW=P|n%6jE~2qnCW1}RAEdG(nNvLbxq=b;w0kpY&EEa|0l zIapF}g{zg$MJ&l>NiwY4*uNTG+#>FAr>nLli&f)8lepa`wtg_}NaRYqipim5{$2WK zq0m%b{p;S?*tddm;%(va9cE}f7;YBBqa$Ti)~CQmTedLpY+`Uj zM=j^|&S%%nUA>X28WigmwrQ6`>Dldrn_h4qkN1hE_FSKt8JpQUTl++x#?5n6LyBTW zCVqSe@0%;X7Orsp>GDq-E|i@)R4;TcEq=(1K79DIPMhq?>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`X=vzX>1bkVV&dd%Xy|I<v4~Pj*wm=R%;iu*SQ+p9GSu@w9+cPjQe(`j145_%4 z^dv1Q)q#EY&yXbzAyNYEPOV|gA9b$#m(NpN{rl5*ac1t0hvKud&z?E6<;~U8&u#eM z-LfTkNLNsleevy_Vd1VN%Z=EzbnNuUhAkR ziV7TLdMtZqQx)@sUF=6<)FbS_6>s?PBzseVM8lJFe}C5HKRy5bdgB@03kO=6#Z0=b z9FHu#AMfx(VL~SNkz)ZnH#^iw+)dTt*}#oXD@?|_rvpU1_F9>umrf8@oU^o38j z_CECbrITrCkAEE4@Ks?M<0+#xJo$YmmdyR0w1e$Zk4)>f_xrWpTr!z;=9ucC56`}G z`-tDE{q&n*LK#Ehy+7#_Qw|9IUz4+jXOYk(oHPAIbBZ|UOqpmbtRc{t zxzBx0=TTdC1zP@JP6>&yTnds^2=d#Wzp$Pz-{}=QC literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_GVAL_PS.png b/apps/wr/public/images/map/layericon/UFL_GVAL_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f7cde2e6b5547f88f9d06f44527a8fdf956cda GIT binary patch literal 1426 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`Vrgt-X5?&PVrt}KXy|HU>Fna_V&rOS;p%MW z=4xyV)9aF-T$-DjR|3v4~Pj*wm=R%;iu*SQ+p9GS=UH!EoWe0V)Jxy45_%4 z^ymM7duFR02hX=_axnHt$=fgF*l;J>SpMA4^Cyn{u-5!8UtOwq_^q>hpvbCQk;Z>= z=Kh)g@L{1^+y46h#tv%}YHG{mC!FPYwx{&VZ~jYvl7D*ntp5KuJ)yskLGlno3G3sf zSBn+&ejGk@{J@zb97oP1FgjLdwgq1@;wfrBzfj`CiEAAPc{IK=othqSG2sXI7rWD& z=i5uh+3h{Apr~*lfx|@dNxkKYMQZ7?2C>_}bKN)~?!dE2R^{8P`|)gcj*4u{>>Yyd z)l{7?`1sCrN4DdSzxQO%sXgQ4@jCU2N8e1pxPeVU+2`WxetF4#|9>7nz#_zGIlpyg z>%(Vfl?&AOR~8*-KKAeP*Ujpi%FoTY!*}H{%f^>qPA=vNUqA2J&Wdjr(!VrJo*0_` zz`u+X=F%7fIZ{M(9pi%Mjld-c`mE7TFyq_hn>BXO0 zkehI9!OEp)7`peBu6|b6<#pm?Lge0>PkH|@@U(4TpJ#AqZ?SmZf$Q=;8fwihCdvJ8~^Mu=p4~|a{=Q+RM;l`v(n|KZ-NjY3&WE0Cd zDe58;{&my$ZKD4fHeTMGZSiLA-nyn6u9gQiPRbl^*r%DDcxL|TYqH5H3JfzOG*}YE sb7E|pReTq0XnL7=WZGO$Mg~SU2FAd1S2y18F$0y7p00i_>zopr0Fdb%6951J literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_KMAN_PS.png b/apps/wr/public/images/map/layericon/UFL_KMAN_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..764ce8ba5dcfbccd8ca87ccd6a270597c2d441a0 GIT binary patch literal 1572 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`XyR($o& z6x?pHz^PXs=oo!a#3DsBObD2IKumbD1#;jCKQ#}S+KYh6`qIybz?{W2!_&nvq~cai zuywXbqKNH%bK~Dhljq(|xt_9EDJ#*zQ?MyXwdqD=XGg0-m{LasN0?&o*0~%>s$z!q ziYikDgr}64bqQ>8+`Rexn=@(8pYC)Pn*73iPHOtIdDZ7_zxU0Wxvwqxxl&>O4#zo4 zA{#w*gx`1S&%Zv6uj%SS7BNG2o6X%zx8#Zox(Z%esk3Jv_x9_qiyyMo{BNu9+cD{c zjmtlSm<{>iTFDwy-5yrz-FnM5r7uu#x?um5z?C6qdwb{iSMPntdhGrrmET-(n;vxw zCkt~NX4t)3?~ullvbZS4zNCBZ8n=`KP95ogfB(Y!>h~wASNt^AoOEpUrM#ZGaomiP zCE6I*>PDnV-OS-DlzI65n^?_#=bb@Q7x8zTzI5pCC6AjIq7Ii52Y45#-e@tZ3rERx&D||?l)ZDhQJH$#^(RHtRdf2kaCv1*C%8%6; ze~1i~T$AB+S^Pe~K&#**gN3TecNQ&PeA9=o@x9thmw#{WT&}lSan&qwhE}HOl9e{O zo*QoDndyuA-F|UQCU(unZM#0j^Xpvp5=o0V^Sb&|M(nqQuKIoR?9EqRy2&%kPxeyx zBKe6w99Fdh_2j&d{onU46u4r@WNb z!9|sG=Wd%Iej?gPZB@F$X^*i`<=X`KL{ae#??A6LmrpcX+5WM)|VWo{v?1 z&Uf*?{{vxeM}Z#e-d`f>i*&SIzo$-}sFJmPmP>Q?yR*jM^e$TcGkrII^%3QySrT`p li<8%f{uC1DXa6sl!0;kmk>gzTt=FJ(+tbz0Wt~$(69BpkPN4t* literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/UFL_KPOL_PS.png b/apps/wr/public/images/map/layericon/UFL_KPOL_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..d9fa712cf6c3528d5673c19c6c3cc6aaa42fe604 GIT binary patch literal 1122 zcmaJ=TWHfz7|xtjom2NBIzc_lKoo4!)HbuBb#}?BMO#K&aOydsF z6J(%1i6{!9h$y~@^8(Hx-awoeHWV*UKBxngDT0XgWUkW(Ye;e~eBbx~=l{QRpdq|z*4Hwl}f!nKYmD{T;iENsz?2>tQ;O&VxwgkBd+_!6cFTea2Q z7EE_1GfH=x5>n~f6(H;gL_mjF29Dlt*n$(G$8-fUcehy@j6rZ)g#MjWW1<0w$bulq zEb}TpjsrZ;aDgDtb4!8W$N5z_Q~t?COGo1Z~CAOspXTxQcQXb>Ik1GW~r7-JFm$ z?C~;@g0YTlvK-@ck5m9A690$l`UKj>DflPfe+t`~E)%jTXrm5GA&qOUc0-whXh9hx zD}zvbp^6Qy2qU`{nLteQVDUQ5P*KiaGKNnigt%d2*-&6yiqJ%X(KJ;E1*K>#Aaa3# z=;yflK!^|WVzkaD)`X;(CqG;o2d{^9zIa$PIvCW$OTOY4Mc%tAUCJF=h^ z&803@RBxQC7SF|3l*^K2Shul%HM+1x+~ZCsY)cjs#)k%RyG3mMF*#gKuEbDWie{V_ z*&q3QZ7JW7-ZZ*d6pzb~PDILIhQsCQvq^aSOz*yuS%ZVmCO@q~uLjQTpLT3ZXD9!1 zJ(zQ2+qd4Tsue$LAA3%g>iYaAO*?14zPWA9(UL3Z&7$X&({sJA&v);^{Zl=MMkZ~i z&X@Erzgw~Kl37(6f4B0>UB=V9YF1@);KJUiftn)=dMe*mJUq3-(>Ht9VL#t~Vc`3l zE7@pKD3{fhAENU1j3f%eFk6Cwc({*PzH(XnG7xdEEnWH=E%$a76?J8kt zq6rojArweP5K&MU-2|2u84I(`Yuw+v;kP3CuR+Lh35cw&b>BbQ>qbRFe z`7+^xF=EHg{|a}0hlBF$48?=M7NNn%OE29DXc)Nsv@{KJ{S&p!)}io zS3TrzPeu4O{kIUM;6r5 zxrin8@?524F5XZ{E`yU{?8g4p=)x9uk3B8g7B7m%2Re4UiEaJMq_*M9u^ncLGeHr>)p%vD@!;U``# z#gk%1;(Kl((_k##n3z3teeLa=n$a&0_rF`&E~f8~tkl&$seXE(aqmEP>n9@ja(Q`t z{4gE3Sv?<{yu8J`?>4#3e9(QcqCGHPdG=hINX%@$SUGk46tSt2m{>hMKY8!ibNkjt LLLK6r*6y+26oFXm literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_FIRE_PS.png b/apps/wr/public/images/map/layericon/WTL_FIRE_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..4137a3f74526c367650a0bf2b998b01a3fe31e12 GIT binary patch literal 1072 zcmaJ=TWHfz7>=xLV>qt}I&g;UMP*Hzu1(jlI@@NKf~m8st`91v$ypn=!U=7x6k}n~cG{AWnTzaD(m17bo*Y5a-FZZa!E;l5^qvzW+b}|DFD} zXiH^9RRuv1m628kzr^8 z(o5GlWf#km97nSrFUPU9q}#>185e#zC+p$89Pjp#s}F^vnMx-g6Pj0J;Z=Z2BV_Ol zlgVW0OdSo)6vO)cep|!sc4CCn>eZ2yb?R14K|uhPY^nxQp-$S0QWEx{0EIJMIf7=a z$?Dc>nQ+0FtYk1O?Xr(l0E*)Op_;aawonYL=lf4#E8c4WCI&3nW6HR3oi%nSgAbcP zLePvu*j=b%TN)y0rJ+HF+c|P?hpH2(Jg4*z1+KErZ7#~St)Lk=vH(o=3{aX)sFBM@ z=GEf4gp2B}a+Tt_2t~OJPKL1?`&Xk2Ti8AJbj`MSv1WXrW4D{w)=&Gl-@sSmc0>rq zv!5nn?4~@q(UF>&gTJo*o-Es0HZwLc-xpVF?;PD$I&eJQ{iHgjm5<+LKabRx4n0U* zCg{!Ie{3C!#B+-C zZxL_8C6$Z$s>tgPfrXyxdB<2qBKhThwB;)`{_5e5U~b_2+|t>F=Pz4t4x7<~nZfCa a_WTAyAH6Vm<>PMAzQd8GsBkZIs_zd&Kw|U& literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_FLOW_PS.png b/apps/wr/public/images/map/layericon/WTL_FLOW_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..285f7edfa81fce7e403067fa779164e206500b58 GIT binary patch literal 1137 zcmaJ>TWHi+7)~t?T3gu_#S1Pyr0!O6XOc6Q*u=)}?4+Z!I(3|mwm#WRa@xi=IWak% znbw!O%Ibn3!ipj&7D4u*%C0P|s}EL0t>S}XABvz9K~O=e)Z2qtPim+7;A}{8{&V=g z@4qI$r<&GRRV=Gu7^W(*K}pl~8}FIpqwng8NtG^3Nnioj&@|DmQigLX+8eUA65!fvM5MqC=XxX00G$V!MM9v#Z)II*zLqNh&2mfWs70yIPb2S!Y7kb!g7gfX(*w{EL8{?hAxG| zaZ!MLI2?g72phvuYKTIe2*WTQ7eh*st6;6iL>4J>_0QZ$Nv<~vrcFI7$T4zAk2}}| zQz=VE=~#j#^`^Oc=~zM~xf~6K^BVg_ql-s$dOUMxZ0TlZ@R3EQ-JxT>ZQq>%`bj)a zDA7#ee)j$3WJ9?ybo~Av_T}!}>>8ox;AkadG;RE4-^9_Amv6jy{XRPQ+h6<`b8m1( zYSEd0w5|K^^$%*?kZ_e5o~`WO^9SF?EbX5+u~~jo{p8i;yAd9y=GfO~3D;I!oAvL2 zTv6Qug5THHo*wl*+OfJSTlbu;TwMNlG&?$SerUbren?a^<$r2hB=_J^&5Pnm_Mm@*%;YWbnPrw70R?c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvv ztM~P_^2{qPNz6-5^>ndS0-B(gnVDi`VQ%K?Vrt-QVruAYXy|HUY3X8N>11x~Z0PJ} zU~1_G)9aF-T$-DjR|3v4~Pj*wm=R%;iu*SQ+p9GS^s~$av=i)Q>UkkV@SoV zCBeS_!hs@t-=Cd*-EX;r_i_O*(bR?9_DyP9oQa7Yf7qR4B*Ts!Int^vw(HVdPU~Lp z$NmR8)(P~e{b;;D%faP9(2TNMm&)w!e=cK?EnKc|d2-L5Io0QPe&?AObm+g#uIF_e zhMgLUDJ>P?0zRbO6 z@_2e*sf4rSvU`(S{N7KBSn~Nu(Bi_Bs$|_6Yr_^^SmPX%cwy6x^)Iv6O^H2!>gdE4 zr^-ofdseP!JzXZyV`IYIIrGx8(va(Gxqmhv*l4ZJbWW6Gv5;w1H~&xt;H zvY7SQvux2{r?i$g2tD4hu3CL^t^fn4uE;FEZ5OtfYSw;WZB0Ec?R2i9+oNooLcU&I zYvt)9=NWiR+1~aXG{4Qada+f`QinMW)6;wkT0bs&y~b9;V)Fm3t&_5ue~HW~3>0LV z_U0|)sws`@0s=M`^aK^}FbxutmQ)}j&`ZOcI;!o7cQg*F*v!hpp9=6QT zVe2>VR%m!3v1a?6P0}7eH+_$9a9~P{`Kh5QdpsiE<;@zeq%~7NZOgIR>D|Zna{hUL aMmC1RZ)#d6L=Wu;71W-telF{r5}E*KPgV5* literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_META_PS.png b/apps/wr/public/images/map/layericon/WTL_META_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..22c7af31b32386e501fbc0948bcf4bdf44d58332 GIT binary patch literal 1100 zcmaJ=TWHfz7>-oSm~Ko^)Cqc)sheP%OWSC()wwmDE7~!-$_j!uP0rT1B_}3lHmgI2 zV;fT+L=;4P5?_4Lf#8b|-f%Eb@J$p%MCOY(ydd7^$+k`(tRcy{@O|I^pa1{Pct`ur zit_4milQpwZBmkqRqj{jCI9swXJj(0!Lc;%MBO+qTaap2Q5J%@F89DBl+}TQQ?QYu zJhYZd<8)%Ts36@hyBNQtn}khKjZKazE4>hdEbP&Y2>tHHO&VxwgzgF@*n}B{IjyZ= z!OlW^N-6XzVU=#$4jLVi2w(A(MnQ>bDfqxb9jvlqp6nC}U)$ z5b7&du_K2tvUA7;(M|!>cWH)-^7iIgd?F#n4I9gb0^?GICJKH{Q$;qw@`A867z}X| z$HjupEj$};4e?S-fR9CET#+jwrC)~zE^^gB+)zobYX#jTktJwphoIVOAsx&fS=37B z!k5&W=c=W136$hABpJqS>|c#8ZV~sm(*@g-#e(snLELT;Tfg#5?I2g;R$OXMIZv`b zW@ef^8;6!ZrQQq$FKpWPls&Qh>LT#q`lPr`Zy5YsK3ZdXGUspp{5HHNuxjnn^xN0d zYwJfRGM!G>WaV9Ix^8Tu`lL*?UAufdI6ShV?(TTcv*({QZ-aO0@OO4u=Faqf^#Q0} zmAU;gbxH^3w@S|i@A%Q$Now%dJ?})JdGDFwntPAdjcl5tb}h9>U(~fAicuN=xS0Z>&&g AEdT%j literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_PIPE_PS.png b/apps/wr/public/images/map/layericon/WTL_PIPE_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c6863f68f8ce9b39d7758ae2bc1d0814d46948 GIT binary patch literal 1075 zcmaJ=OK8+k6n(8)wNlV3s9k7CD=0e2WI7W&Vbb4Zq9dl1=|HDa#58$rV@+O~ymr!$ zQi@g-E3QK2{g=0waKAdyzefOSsrnO~X zMR`>@0H7kdUrLd2z5lH#BmeE+aw-|N;zS0wp>CX29SE9q)CH-esrJAWRP~|$=P(Aq z3ff3#a7H;OXvkz#AH#U2P1pd$8a!Lo`XHvdV2@#m^q2cHG-c=_-5ybbiXDf&#{OXk zwhgzWwc$Q3s?!biRLm2IfC;fmdFC<86+Ds7>k4G>+0E;xq^!E`=ds)_U zm&!y6#(JvFa!k-aQURza|A(69GTOx{_$S|g3cKkc8?q_rq5(%Ejq9%VL)k*yfhtB$ z8lhu_Dz^3_jND#iQ}H&Q+SzVcI?B4c@_0oNl9r2AOM^*Cq=^D!7`o70$2Ui0IUbI1 zagIxb>*S_LBE<8u6qiGCG+5wDNE%p4(iBF9TP|r^o zxm<4aemu|@!+!)pRYV9y)9peIrC#+9iRB|V*xxFn|{A$ z(~E`Csj{)@a`F3}tuNm`I#p6#)xV>%6Lg3h4=j`_r{4mXIVWGde)PZ%>+rQhR?RA~ Zatrt{J2@{+9zW|}<7A>my48GS{1?p)VJH9q literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_RSRV_PS.png b/apps/wr/public/images/map/layericon/WTL_RSRV_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b307a61bcaa6059fe5565713ae6055c29d209b GIT binary patch literal 1057 zcmaJ=O=#0#7>?|RF*leyZM!UFtW31|=}%_EYU!GGE80=o!is_{O}?#RO}?0Xv)vFK zia+zyB!Hw$pL`Am z2%?TsQ)!fr9TjA#(UOg!3z~u11ThdO7?L~*5SalJs_v)0E-X@{s`#l(M|u=wSjoJa8Tf~%h_KNOB;D&s;zjFe;Hm7mHYWC#qC z&*$m9n}+5D!})wZTf^mYVuaJ0){#_j>Q+xhK?Ig;ss>V_PTGo622LSAg)`kLK{Kkd zx>Y_Vd|*sLG8m3#?UE|MSnU5$O{=0UlmNT={!`dWO&fqo01Hl;GH%>>j~&VoLMD(9 zG*b}fDyJCFLIkZWG{{hrC;LWKU4eOPe+eIp2~pibk}iX&=%=s(t*VOPW?4SSdt4qb zCvsfa6LdwmLwwll@kK)3aKEp@6`?$(0UcGi$}ZPklWSW+GjL=PnCdB@L`1jXD^d3}ZL;uSQq4uzT!j)wXz1H9pX>+f8ii?@j3=_)1)hiosOj zZRTgO7^v%=ZJ57pJZo=BZmeCFR_nDQTHIAGDY zWXc1lPdC`srZos0TDLEC?t;)gN!Fb+Bv)nKtA18_%+mdO>6NIHmdpWOiIHMnnpdJ` zF3X(yChSMs{s-bLm%jhK&0eB%Ok;Wck14mcZ5S1f`OHr}BH0XdBZH@_pUXO@geCxp CKw=aC literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/WTL_VALV_PS.png b/apps/wr/public/images/map/layericon/WTL_VALV_PS.png new file mode 100644 index 0000000000000000000000000000000000000000..ca2b4788b74c2c48d2c6a71fee593031ab421a82 GIT binary patch literal 1077 zcmaJ=TSyd97#=k&P0b(>Lvk2}GTfP+buaGVD(lRu8;vgOf*bVE?95RIojKFYv0Z&p zQ$&j(Gcw6udJuXjdI^Fal7cQq?-3$hjGijGJP0LrMt9d9+J>1q7ryWN|MUOfnTd3^ zmlhu`CJ3T5+#y8qSZ;s&3h}@C$CiYLN)(EtZrF>Gk_m{Q0uz7?Yf>MG0!bM@`vNo( zL;JC~_Nu1_IPhQgJat^3VjN zhi-7lPL?ITUYd1#yk54RbU9fU7oyb4hL2pJs1 zB$G)x*+4_Hk750OzpdeNIWWRu4eLlsIdrQgqaXlFHdOX( zvbvQn6D}B&k_?8Wo%WG3KvDcZRMWC(3q`?hzW)@qV#5YtqQHVfri>fcTVsbZINk&j zf@TcD!Aun+{SZN`9~vaz?In-(sJa4^R^2vU6uGc&AxW1(SO`#9fmT(83%Pv0#-OLs z?O}PA4Y~axC*RoW6Ik9AbOzg6Gh6}6LmJRghO6vy{W-a|6*L1!7J#Xq14^3-HFEpN zoSHiqZ%(~5SIM1=FDIA5$uM?f|7vt*3%kdjW^IcXS>pp8yWPaLzWJ)_6TT9+!$L5Y zdXw1L+G;5%xv{XY*}gP0EAKh@e0g=Xs^0j%9&RgikBMda*u=!fBr7i5*;tR{udJ8IEDy!;YEe%*XruYA&OU*vG8Q@GoD=KLQ_Vqx_F literal 0 HcmV?d00001 diff --git a/apps/wr/public/images/map/layericon/default.png b/apps/wr/public/images/map/layericon/default.png new file mode 100644 index 0000000000000000000000000000000000000000..92bf1f587e549c99cabe90492d4421dd2f8a8dbc GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&j3?%D+y-WjAJOMr-t{fa3iV^}#7cKqw|Nnm$ z%?m(rmXaX9U?7jdL2Km~AfL0qBeIx*f$sEak7F*Vu1jg5_``2YX^v17+v zG)>uiB)-dvm@BaA1RpYY5rpXW7lzz`{*&wxTB^!gCocK!zUrl|Wb_P#ZKbLh* G2~7Ysgdv9j literal 0 HcmV?d00001 diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index f68d0798a..42ef97f70 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,25 +1,38 @@ -import './lib/css/gis.common.css'; -import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { Search } from 'lucide-react'; import OlMap from 'ol/Map'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; import { defaults } from 'ol/interaction'; import * as olProj from 'ol/proj'; -import proj4 from 'proj4'; import { register } from 'ol/proj/proj4'; -import GisApp from './lib/gis.core'; -import GisContent from './GisContent'; -import GisTree from './GisTree'; +import proj4 from 'proj4'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import GisLonLat from './GisLonLat'; +import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; -import GisMapToggle from './GisMapToggle'; -import { Search } from 'lucide-react'; +import GisTree from './GisTree'; +import './lib/css/gis.common.css'; +import GisApp from './lib/gis.core'; +import GisContent from './GisContent'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { +function Gis({ + isOpen , + gisMaps, + isSearchModalOpen, + treeData = [], + isSearch = false, + isMiniMap = false, + setIsOpen, + setFeatureData, + featureData, + searchView : SearchView, + detailView: DetailView, + onFeatureSelect = null, + setIsSearchModalOpen }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); @@ -92,6 +105,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa setIsSearchModalOpen(false); }, [mapInstanceRef]); + // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 @@ -105,7 +119,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa ]); register(proj4); - //맵 생성 + // OpenLayers 지도 객체 생성 const map = new OlMap({ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], renderer: 'webgl', @@ -133,7 +147,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa gisMaps.current = map - //미니맵 + // 미니맵 추가 if(isMiniMap){ const overviewMap = new OverviewMap({ layers: [GisApp.Module.core.baseMiniMapLayers['base']], @@ -148,6 +162,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa map.addControl(overviewMap); } + // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer('base'); @@ -155,20 +170,25 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa mapInstanceRef.current = map; // 생성 완료 플래그 설정 }, []); + // treeData 변경 시 레이어 코드 생성 useEffect(() => { if (!treeData || treeData.length === 0) return; + // 기존 오버레이 및 레이어 모두 삭제 gisCore.deleteAllOverlay(); gisCore.deleteAllLayer(); + // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); - GisApp.layerCode = layerCode; + GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 //setTreeData(data); + // 편집 가능한 레이어 목록 생성 const layerItems = getLayerItems(layerCode); //setEditLayerItems(prev => [...layerItems]) }, [treeData]); + // 레이어 코드에서 편집 가능한 레이어만 추출 const getLayerItems = (layerCode) => { const layerItems = Object.entries(layerCode) .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') @@ -213,12 +233,12 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa - + {isSearch && ( @@ -243,4 +263,4 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa ); } -export default React.memo(Gis); +export default React.memo(Gis); \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/MapToggleBtn.js b/packages/common/business_components/src/ui/map/GisMapToggle.js similarity index 100% rename from packages/common/business_components/src/ui/map/MapToggleBtn.js rename to packages/common/business_components/src/ui/map/GisMapToggle.js diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index 4aa8f3049..e9deb72f7 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,37 +1,42 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; +import { GisContext } from './Gis'; import GisApp from './lib/gis.core'; import GisOlSelect from './lib/gis.select'; -import { GisContext } from './Gis'; export default React.memo(function GisSelect({ onFeatureSelect }) { const { gisMap } = useContext(GisContext); - - - // useEffect(() => { - // if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 - - // const select = new GisOlSelect(gisMap); - // GisApp.Module.select = select; - - // select.getSelectedInfo(function (features) { - // if (features?.[0]) { - // // onFeatureClick?.(features[0]); // props로 넘긴 함수 실행 - // onFeatureSelect(layer.getProperties()); //피쳐 속성데이터 상위컴포넌트로 전달 - // } - // }); - // }, [gisMap, onFeatureSelect]); - const select = new GisOlSelect(gisMap); - GisApp.Module.select = select; - select.getSelectedInfo(function (features) { - //clickFeature(features[0]); - if(features.length > 0) { - const layer = features[0]; - onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 + useEffect(() => { + if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + + // 이미 select가 있으면 재생성하지 않음 + if (!GisApp.Module.select) { + const select = new GisOlSelect(gisMap); + GisApp.Module.select = select; + + // 피처 선택 시 콜백 등록 + select.getSelectedInfo(function (features) { + if (features?.[0]) { + console.log('GisSelect features', features); + onFeatureSelect(features[0]); + } + }); } - }); + }, [gisMap, onFeatureSelect]); + + // const select = new GisOlSelect(gisMap); + // GisApp.Module.select = select; + + // select.getSelectedInfo(function (features) { + // //clickFeature(features[0]); + // if(features.length > 0) { + // const layer = features[0]; + // // console.log('GisSelect layer', layer); + // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 + // } + // }); return ( <> diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index d01786187..dec618926 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,21 +1,20 @@ -import GisApp from './lib/gis.core'; -import { util } from './lib/util'; -import React, { useEffect, useState } from 'react'; -import { useGisContext } from './Gis'; -import { DndProvider } from "react-dnd"; import { - Tree, MultiBackend, + Tree, getBackendOptions } from "@minoru/react-dnd-treeview"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components/ui/tabs"; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { DndProvider } from "react-dnd"; +import { useGisContext } from './Gis'; +import GisApp from './lib/gis.core'; -export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => {} }) { +export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); const [nodes, setNodes] = useState([]); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [activeTab, setActiveTab] = useState('layers'); useEffect(() => { if (treeData && treeData.length > 0) { @@ -25,19 +24,27 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes } }, [treeData]); + // 트리 노드 체크 시 레이어 표시/숨김 useEffect(() => { if (nodes.length > 0) { + // 기본으로 체크된 노드들 찾기 const defaultChecked = getDefaultCheckedNodes(nodes); setChecked(defaultChecked); + console.log('defaultChecked : ', defaultChecked); + + // 기본으로 확장된 노드들 설정 const defaultExpanded = Array.from(new Set([ ...defaultChecked, ...defaultChecked.flatMap((id) => getAncestors(id, nodes)) ])); setExpanded(defaultExpanded); + + // 기본 체크된 레이어들을 지도에 표시 gisCore.setLayerOnMap(defaultChecked, true); } }, [nodes]); + // 초기 표시 여부가 'Y'인 레이어들 찾기 const getDefaultCheckedNodes = (nodes, values = []) => { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; @@ -115,7 +122,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes }; const makeLegendSrc = layerinfo => { - if(!layerinfo.gobjTy) return null; + if (!layerinfo.gobjTy) return null; let src; if (layerinfo.iconNm) { @@ -176,34 +183,61 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes return ( <> - - ( -
- {node.droppable && ( - - - )} - handleCheck(node.id)} className="cursor-pointer"/> - {node.icon && {node.icon}} - {node.text} -
- )} - sort={(a, b) => { - const aOrder = parseInt(a.data?.sortOrdr ?? '0'); - const bOrder = parseInt(b.data?.sortOrdr ?? '0'); - return aOrder - bOrder; - }} - /> -
+
+
+

레이어

+
+ + ( +
+ {node.droppable && ( + + + )} + handleCheck(node.id)} className="cursor-pointer" /> + {node.icon && {node.icon}} + {node.text} +
+ )} + sort={(a, b) => { + const aOrder = parseInt(a.data?.sortOrdr ?? '0'); + const bOrder = parseInt(b.data?.sortOrdr ?? '0'); + return aOrder - bOrder; + }} + /> +
+
+ {/* 접기 버튼 */} + +
+ {/* 열기 버튼 */} + {!visible && ( + + )} ); }); diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index faebb90d3..309012a44 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -26,10 +26,13 @@ const GisApp = {}; //레이어 정보 GisApp.layerInfos = []; //기본정보 -//GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows'; -//GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms'; -GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows'; -GisApp.GEOSERVER_WMS_URL = '/sn/geoserver/hasudo/wms'; +GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows'; +GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms'; +// GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows'; +// GisApp.GEOSERVER_WMS_URL = '/sn/geoserver/hasudo/wms'; +// GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; +// GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry'; +// GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; //맵 정보 GisApp.EMAP_URL = '/emap/'; @@ -47,7 +50,8 @@ GisApp.BASE_MAP_TYPES = []; GisApp.FROM_PROJ = 'EPSG:4326'; GisApp.TO_PROJ = 'EPSG:3857'; GisApp.TO_DATA_PROJ = 'EPSG:4326'; -GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/'; +// GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/'; +GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/'; GisApp.layerCode = {}; GisApp.hlLayerId = 'LightFeatureList'; GisApp.hLayerId = 'highLightFeature'; @@ -507,6 +511,7 @@ GisApp.core.prototype = { * * Returns: */ + // 레이어를 지도에 표시/숨김 setLayerOnMap: function (layerIds, visible, addQurryFlter) { for (let i in layerIds) { let id = layerIds[i]; @@ -517,10 +522,14 @@ GisApp.core.prototype = { const layerCode = GisApp.layerCode[id]; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); + //layer = this.addWmsLayer(layerCode); + + // WFS 레이어 생성 및 지도에 추가 layer = this.addWfsLayer(layerCode); } } + + // 선택 인터랙션 및 스냅 기능 설정 if (GisApp.Module.select) { this.setSelectInteraction(); } From 6b2b5b56d4bbf067b2fabccc3845cfac224e625b Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 13 Jun 2025 11:26:08 +0900 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20merge=20=ED=9B=84=20import=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx | 2 +- apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx | 4 ++-- .../sn/app/map/components/popupComponent/chart/infilChart.tsx | 2 +- apps/sn/app/map/components/popupComponent/sewagePump.tsx | 2 +- apps/sn/app/map/components/popupComponent/stormWaterPump.tsx | 2 +- packages/common/business_components/src/ui/map/Gis.js | 4 ---- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx index 1db284c8e..1fc0cef80 100644 --- a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx +++ b/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx @@ -1,4 +1,4 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; +import { VerticalTable } from '../../../components/ui/verticalTable'; import React, { useState } from 'react' import InputSection from '../InputSection'; import { handleInputChange } from '../common'; diff --git a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx index eef14ea5e..0d1c3ef2e 100644 --- a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx +++ b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx @@ -1,5 +1,5 @@ -import Modal from '@/components/ui/modal'; -import { VerticalTable } from '@/components/ui/verticalTable'; +import Modal from '../../../components/ui/modal'; +import { VerticalTable } from '../../../components/ui/verticalTable'; import { UiTable } from '@common/business_components/ui'; import { Button, TableCell } from '@common/components/ui'; import React, { useState } from 'react' diff --git a/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx index ba0b70455..06c5b5fec 100644 --- a/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx +++ b/apps/sn/app/map/components/popupComponent/chart/infilChart.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { type ChartConfig, MainChart, CHART_FORM } from "@common/business_components"; -import Modal from '@/components/ui/modal'; +import Modal from '../../../../components/ui/modal'; import { Button } from '@common/components/ui'; type ModalOpenProp = { isModalOpen: boolean, diff --git a/apps/sn/app/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/map/components/popupComponent/sewagePump.tsx index 5776df5b2..79103a40e 100644 --- a/apps/sn/app/map/components/popupComponent/sewagePump.tsx +++ b/apps/sn/app/map/components/popupComponent/sewagePump.tsx @@ -1,4 +1,4 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; +import { VerticalTable } from '../../../components/ui/verticalTable'; import React from 'react' const inputColumn = [ { id: "swtrmt_zone_cd", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, diff --git a/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx index eefef8b3b..172022f31 100644 --- a/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx +++ b/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx @@ -1,4 +1,4 @@ -import { VerticalTable } from '@/components/ui/verticalTable'; +import { VerticalTable } from '../../../components/ui/verticalTable'; import React from 'react' const inputColumn = [ { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 3d498f40c..48dce9284 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -15,10 +15,6 @@ import GisLonLat from './GisLonLat'; import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; -import GisTree from './GisTree'; -import './lib/css/gis.common.css'; -import GisApp from './lib/gis.core'; -import GisContent from './GisContent'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); From 0ebe5e8ab8efd6f7fd2b54c5df2bf9a42f8961e0 Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 13 Jun 2025 12:50:43 +0900 Subject: [PATCH 03/22] =?UTF-8?q?-=20refactor:=20=EC=95=88=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20import=20=EC=A0=95=EB=A6=AC=20-=20feat:=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EC=97=90=EC=84=9C=20Search=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80,=20searchModal.tsx=20props=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/htdflSttn/page.tsx | 2 +- apps/wr/app/map/searchModal.tsx | 2 -- .../business_components/src/ui/map/GisContent.js | 12 ++++-------- .../business_components/src/ui/map/GisMapToggle.js | 2 -- .../business_components/src/ui/map/GisTree.js | 8 ++++---- .../business_components/src/ui/map/lib/gis.core.js | 14 ++++++-------- 6 files changed, 15 insertions(+), 25 deletions(-) diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx index f6b967c05..f3ff3da1c 100644 --- a/apps/wr/app/map/htdflSttn/page.tsx +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -94,7 +94,7 @@ const Map = () => {
= ({ selectUnitWaterShedData={code.lgbsnCd} selectMiddleRegionData={code.mdmbsnCd} selectEnvPartData={code.lclgvrnmntCd} - rightChildren={undefined} - leftChildren={undefined} queryClick={function (returnType: ReturnType): void { // '{partValue: 'A', sidoValue: '0', sigunValue: '0', regionValue: '0', unitWaterShedValue: '0', …}' let con; diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index a8ac408e8..4133beed0 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -1,12 +1,8 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import GisApp from './lib/gis.core'; -import { useGisContext } from './Gis'; -import GisLonLat from './GisLonLat'; -import GisSelect from './GisSelect'; -import GisToolBar from './GisToolBar'; -import './lib/css/gis.edit.css'; -import GisMapToggle from './GisMapToggle'; import { Search } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useGisContext } from './Gis'; +import './lib/css/gis.edit.css'; +import GisApp from './lib/gis.core'; export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { const { gisMap, gisCore } = useGisContext(); diff --git a/packages/common/business_components/src/ui/map/GisMapToggle.js b/packages/common/business_components/src/ui/map/GisMapToggle.js index b157dc141..c890f75f8 100644 --- a/packages/common/business_components/src/ui/map/GisMapToggle.js +++ b/packages/common/business_components/src/ui/map/GisMapToggle.js @@ -1,7 +1,5 @@ import React, { useContext, useState } from 'react'; -import { Button } from "@common/components"; import { GisContext } from './Gis'; -import GisApp from './lib/gis.core'; export default React.memo(function MapToggleBtn() { const { gisCore } = useContext(GisContext); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index babdf69b2..dec618926 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,13 +1,13 @@ -import GisApp from './lib/gis.core'; -import React, { useEffect, useState } from 'react'; -import { useGisContext } from './Gis'; -import { DndProvider } from "react-dnd"; import { MultiBackend, Tree, getBackendOptions } from "@minoru/react-dnd-treeview"; import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { DndProvider } from "react-dnd"; +import { useGisContext } from './Gis'; +import GisApp from './lib/gis.core'; export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) { const { gisCore, treeData } = useGisContext(); diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index 693fbfa0c..72a584970 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -1,26 +1,24 @@ import MousePosition from 'ol/control/MousePosition'; import ScaleLine from 'ol/control/ScaleLine'; +import { getTopLeft } from 'ol/extent'; import Feature from 'ol/Feature'; +import GeoSJON from 'ol/format/GeoJSON'; import WKT from 'ol/format/WKT'; +import { Point } from 'ol/geom'; import LinearRing from 'ol/geom/LinearRing'; import Draw from 'ol/interaction/Draw'; import TileLayer from 'ol/layer/Tile'; +import VectorLayer from 'ol/layer/Vector'; import * as olProj from 'ol/proj'; import VectorSource from 'ol/source/Vector'; +import TileWMTS from 'ol/source/WMTS'; import XYZ from 'ol/source/XYZ'; -import GeoSJON from 'ol/format/GeoJSON'; import { Fill, Stroke, Style } from 'ol/style'; import CircleStyle from 'ol/style/Circle'; -import VectorLayer from 'ol/layer/Vector'; -import { Point } from 'ol/geom'; -import { getTopLeft } from 'ol/extent'; -import TileWMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; // import { get as getProjection } from 'ol/proj'; -import proj4 from 'proj4'; import { register } from 'ol/proj/proj4'; -import olSourceImageWMS from 'ol/source/ImageWMS'; -import { GeoJSON } from 'ol/format'; +import proj4 from 'proj4'; import GisOlEditLayer from './gis.edit'; import GisOlLayer from './gis.layer'; From 16bed417723b53a399df51bdd829b9aed25d06ee Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 17 Jun 2025 18:17:21 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20-=20wr=20middleware.ts=EC=97=90?= =?UTF-8?q?=20geoserver=20path=20=EC=B6=94=EA=B0=80=20-=20mapSearchBar.tsx?= =?UTF-8?q?=20UI=20=EA=B5=AC=ED=98=84=20(API,=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=EB=84=88=20=EC=9E=91=EC=97=85=20=ED=95=84?= =?UTF-8?q?=EC=9A=94)=20-=20mapSearchForm.tsx=20css=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20=EC=A7=80=EB=8F=84=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20searchView=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/govSidoSigunguSearchForm.tsx | 114 ------- apps/wr/app/map/htdflSttn/page.tsx | 17 +- apps/wr/app/map/mapSearchBar.tsx | 328 +++++++++++++++---- apps/wr/app/map/mapSearchForm.tsx | 33 +- apps/wr/app/map/searchModal.tsx | 80 ----- apps/wr/middleware.ts | 13 +- 6 files changed, 304 insertions(+), 281 deletions(-) delete mode 100644 apps/wr/app/map/govSidoSigunguSearchForm.tsx delete mode 100644 apps/wr/app/map/searchModal.tsx diff --git a/apps/wr/app/map/govSidoSigunguSearchForm.tsx b/apps/wr/app/map/govSidoSigunguSearchForm.tsx deleted file mode 100644 index 127d02f78..000000000 --- a/apps/wr/app/map/govSidoSigunguSearchForm.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useWrStore } from "@/app/store"; -import { getCityOrSgg } from "@/components/lib/func/commom"; -import { - type ReturnType -} from "@common/business_components/ui"; -import { SelectBox } from '@common/business_components/ui/select'; -import React, { useEffect, useRef, useState } from 'react'; - -export type comCodeState = { - code: { - isInit: boolean; - chBuCd: SelectDataType[] - rainFcltCd: SelectDataType[]; - bdstUsgCd: SelectDataType[]; - cityCd: SelectDataType[]; - sggCd: SelectDataType[]; - rvbsnCd: SelectDataType[]; - mdmbsnCd: SelectDataType[]; - lgbsnCd: SelectDataType[]; - lclgvrnmntCd: EnvPartSelectDataType[]; - orgwtKndCd: SelectDataType[]; - yn: SelectDataType[]; - buildingType: { - RST: string, - RNB: string, - HTB: string - } - }; -}; - -type SelectDataType = { - text: string; - val: string; - sido?: string; // 시도 코드 (선택 사항) -} - -type EnvPartSelectDataType = SelectDataType & { - adstDliStrCd: string, -} - -type SearchFormLeftProps = { - isShowPart?: boolean | undefined; - children: React.ReactNode; - selectSidoData?: SelectDataType[] | []; - selectSigunData?: SelectDataType[] | []; - selectRegionData?: SelectDataType[] | []; - selectUnitWaterShedData?: SelectDataType[] | []; - selectMiddleRegionData?: SelectDataType[] | []; - selectEnvPartData?: EnvPartSelectDataType[] | []; - selectEnvPartSidoData?: SelectDataType[] | []; - selectEnvPartSigunData?: SelectDataType[] | []; - - setRtnPartValue?: (value: string) => void; - setRtnSidoValue?: (value: string) => void; - setRtnSigunValue?: (value: string) => void; - setRtnRegionValue?: (value: string) => void; - setRtnUnitWaterShedValue?: (value: string) => void; - setRtnMiddleRegionValue?: (value: string) => void; - setRtnEnvPartValue?: (value: string) => void; - setRtnEnvPartSidoValue?: (value: string) => void; - setRtnEnvPartSigunValue?: (value: { val: string, text: string }) => void; // setRtnEnvPartSigunValue prop 타입을 객체로 변경 - resetTrigger?: boolean; -} - - - -function GovSidoSigunguSearchForm({ - isShowPart = false, - children, - selectSidoData = [], - selectSigunData = [], - selectRegionData = [], - selectUnitWaterShedData = [], - selectMiddleRegionData = [], - selectEnvPartData = [], - setRtnPartValue, - setRtnSidoValue, - setRtnSigunValue, - setRtnRegionValue, - setRtnUnitWaterShedValue, - setRtnMiddleRegionValue, - setRtnEnvPartValue, - setRtnEnvPartSidoValue, - setRtnEnvPartSigunValue, - resetTrigger = false -}: SearchFormLeftProps) { - const { code, pbnstSttn, pbnstSttnSearch } = useWrStore((state) => state); - - const ref = useRef({} as ReturnType); - const codeData = getCityOrSgg(ref); - //환경청별 시도 시군구 - const [envPartValue, setEnvPartValue] = useState('0'); - // 환경청 - const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); - - return ( - isShowPart ? ( -
- <> - onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={'rounded-lg bg-white truncate'} /> - onSelectValue(val.toString(), 'envPartSido')} value={envPartSidoValue} selectClass={'rounded-lg bg-white truncate'} /> - onSelectValue(val.toString(), 'envPartSigun')} value={envPartSigunValue} selectClass={'rounded-lg bg-white truncate'} /> - - {children} -
- ) : ( -
- {children} -
- ) - ) -} - -export { GovSidoSigunguSearchForm }; diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx index f3ff3da1c..6c75edb92 100644 --- a/apps/wr/app/map/htdflSttn/page.tsx +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -1,11 +1,10 @@ "use client"; import Gis from "@common/business_components/ui/map/Gis"; -import GisApp from "@common/business_components/ui/map/lib/gis.core"; import { useEffect, useRef, useState } from "react"; -import SearchModal from "../searchModal"; import FeatureClickEventPopup from "../featureClickEventPopup"; import { mapService } from "../service"; +import { MapSearchBar } from "../mapSearchBar"; const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 @@ -90,6 +89,18 @@ const Map = () => { // setIsOpen(true); // setIsSearchModalOpen(false); // }; + + // MapSearchBar 래퍼 컴포넌트 + const CustomMapSearchBar = (props: any) => { + return ( + + ); + }; + return (
{ isSearch={false} gisMaps={gisMaps} isMiniMap={true} - searchView={SearchModal} + searchView={CustomMapSearchBar} detailView={FeatureClickEventPopup} setIsOpen={setIsOpen} setFeatureData={setFeatureData} diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx index a578dfac2..ca746ccce 100644 --- a/apps/wr/app/map/mapSearchBar.tsx +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -1,14 +1,19 @@ "use client" -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useState } from "react"; -import type { EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; +import { Button, Input, Label } from "@common/components/ui"; +import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"; +import { SearchFormLeft, type EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; import { SearchFormRight } from "@common/business_components/ui/search_form_right"; import type { SelectDataType } from "@common/business_components/ui/search_input_select"; import type { ReturnType } from "@common/business_components/ui/search_base_form"; -import { MapSearchForm } from "./mapSearchForm"; +import { MapSearchFormLeft } from "./mapSearchForm"; +import { useWrStore } from "@/app/store"; +import { btnClass } from "@/app/store/common/code"; +import { SelectBox } from "@common/business_components/ui/select"; +import { SearchBaseForm } from "@common/business_components"; type SearchBaseType = { + searchType: string; selectSidoData?: SelectDataType[] | []; selectSigunData?: SelectDataType[] | []; selectRegionData?: SelectDataType[] | []; @@ -16,12 +21,11 @@ type SearchBaseType = { selectMiddleRegionData?: SelectDataType[] | []; selectEnvPartData?: EnvPartSelectDataType[] | []; queryClick: (returnType: ReturnType) => void; - excelName?: string; - excelClick: (returnType: ReturnType) => void; initClick: () => void; - btnClass: string; - ref: React.RefObject; - isAdmin: boolean; + applyClick: () => void; + btnClass?: string; + ref?: React.RefObject; + isAdmin?: boolean; leftClassName?: string; rightClassName?: string; showQueryButton?: boolean; @@ -29,7 +33,24 @@ type SearchBaseType = { showApplyButton?: boolean; } +const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; + +const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); + +interface SearchModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onFeatureSelect: (row: any) => void; +} + +interface SelectedDataTypeWithSggNm { + text: string; + val: string; + sggNm: string; +} + export function MapSearchBar({ + searchType, selectSidoData, selectSigunData, selectRegionData, @@ -37,28 +58,77 @@ export function MapSearchBar({ selectMiddleRegionData, selectEnvPartData, btnClass, - excelName, queryClick, - excelClick, initClick, - ref, + applyClick, isAdmin = false, leftClassName, rightClassName, showQueryButton = true, showInitButton = true, - showApplyButton = false + showApplyButton = true }: SearchBaseType) { + + // 로딩바 + const [isLoading, setIsLoading] = useState(false); + // 상세 정보 조회를 위한 상태 추가 + const [popupFeature, setPopupFeature] = useState(null); + const { code } = useWrStore((state) => state); + const [isChHead, setIsChHead] = useState(false); + const ref = useRef({} as ReturnType); + + // 전체 목록 const [partValue, setPartValue] = useState("A"); const [sidoValue, setSidoValue] = useState("0"); const [sigunValue, setSigunValue] = useState("0"); - const [sidoText, setSidoText] = useState(""); - const [sigunText, setSigunText] = useState(""); const [regionValue, setRegionValue] = useState("0"); const [untitWaterShedValue, setUntitWaterShedValue] = useState("0"); const [middleRegionValue, setMiddleRegionValue] = useState("0"); const [envPartValue, setEnvPartValue] = useState("0"); const [resetTrigger, setResetTrigger] = useState(false); + const [sidoText, setSidoText] = useState("0"); + const [sigunText, setSigunText] = useState("0"); + // 공급처 후보지 조회 필터 목록 + const [selectedSido, setSelectedSido] = useState({ text: "전체", val: '0' }); + const [selectedSigun, setSelectedSigun] = useState({ text: "전체", val: '0' }); + const [selectedFclt, setSelectedFclt] = useState({ text: "전체", val: '0' }); + // 전체 시설 + let allFclt: SelectedDataTypeWithSggNm[] = [{ text: "전체", val: '0', sggNm: '전체' }]; + // 우리동네가뭄지도 필터 목록 + const [selectedDong, setSelectedDong] = useState({ text: "전체", val: '0' }); // 행정동 + + const [allSido, setAllSido] = useState([ + { text: "전체", val: '0' }, + ...(code?.cityCd || []) + ]); + const [allRegion, setAllRegion] = useState([ + { text: "전체", val: '0' }, + ...(code?.rvbsnCd || []) + ]); + + const filteredSigunData = useMemo(() => { + if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }]; + return [ + { text: "전체", val: '0' }, + ...code.sggCd.filter(item => item.val.substring(0, 2) === selectedSido.val.substring(0, 2)) + ]; + }, [selectedSido]); + + const filteredFcltData = useMemo(() => { + if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; + return [ + { text: "전체", val: '0', sggNm: '전체' }, + ...allFclt.filter(item => item.sggNm === selectedSido.text) + ]; + }, [selectedSigun]); + + const filteredDong = useMemo(() => { + if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; + return [ + { text: "전체", val: '0', sggNm: '전체' }, + // ...code.dong.filter(item => item.sggNm === selectedSido.text) + ]; + }, [selectedSigun]); const queryVal = () => { console.log("조회버튼 클릭"); @@ -70,8 +140,8 @@ export function MapSearchBar({ unitWaterShedValue: untitWaterShedValue, middleRegionValue: middleRegionValue, envPartValue: envPartValue, - sidoText: sidoText, - sigunText: sigunText, + sidoText: selectedSido.text, + sigunText: selectedSigun.text, }); } @@ -80,25 +150,68 @@ export function MapSearchBar({ initClick(); }, [setResetTrigger, initClick]); + const applyVal = useCallback(() => { + if (searchType == '공급처 후보지 조회') { + } else if (searchType == '우리동네가뭄지도보기') { + } else if (searchType == '지도보기') { + + } + }, [setResetTrigger, initClick]); + useEffect(() => { if (resetTrigger) { setResetTrigger(false); } }, [resetTrigger]); - const excelDownload = () => { - excelClick({ - partValue: partValue, - sidoValue: sidoValue, - sigunValue: sigunValue, - regionValue: regionValue, - unitWaterShedValue: untitWaterShedValue, - middleRegionValue: middleRegionValue, - envPartValue: envPartValue, - sidoText: sidoText, - sigunText: sigunText, - }) - }; + // 시설 조회 + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) { + allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); + setIsLoading(false); + return; + } + + try { + const queryParamsFacility = new URLSearchParams({ + lyrTypeNm: 'tb_il_swage_prcs_fclt', + page: '0', + size: '100000', + sort: 'string', + }); + const cqlFiltersFacility: string[] = []; + if (selectedSigun.text && selectedSigun.val !== '0') { + cqlFiltersFacility.push(`sgg_nm ILIKE '%${selectedSigun.text}%'`); + } + if (cqlFiltersFacility.length > 0) { + queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); + } + console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`); + + const facilityRes = await fetch(`${API_URL}/v1/fclts?${queryParamsFacility.toString()}`, { + method: "GET", + headers: { accept: "application/json" } + }).then(res => res.json()); + + const facilities = (facilityRes.data || []).map((item: any) => ({ + text: item.swtrmtFcltNm, + val: item.fid, + sggNm: item.sggNm + })); + allFclt = (facilities); + + } catch (error) { + console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error); + allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [selectedSigun]); useEffect(() => { if (ref && ref.current) { @@ -110,43 +223,132 @@ export function MapSearchBar({ customRef.unitWaterShedValue = untitWaterShedValue; customRef.middleRegionValue = middleRegionValue; customRef.envPartValue = envPartValue; - customRef.sidoText = sidoText; - customRef.sigunText = sigunText; + customRef.sidoText = selectedSido.text; + customRef.sigunText = selectedSigun.text; } - }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]); - + }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); + return ( -
- {/* 검색 조건 부분 */} - - {/* 버튼 부분 */} -
-
+
+
+
+ {searchType == "공급처 후보지 조회" && ( + <> + { + const selected = code.cityCd.find(item => (item.val === val)); + setSelectedSido(selected || { text: "전체", val: '0' }); + }} + value={selectedSido.val} + selectClass={'w-32'} + /> + { + const selected = filteredSigunData.find(item => (item.val === val)); + setSelectedSigun(selected || { text: "전체", val: '0' }); + }} + value={selectedSigun.val} + selectClass={'w-32'} + /> + { + const selected = allFclt.find(item => (item.val === val)); + setSelectedFclt(selected || { text: "전체", val: '0', sggNm: '전체' }); + }} + value={selectedFclt.val} + selectClass={'w-32'} + /> + +
+ +
+ + )} + {searchType == "우리동네가뭄지도보기" && ( + <> + { + const selected = code.cityCd.find(item => (item.val === val)); + setSelectedSido(selected || { text: "전체", val: '0' }); + }} + value={selectedSido.val} + selectClass={'w-32'} + /> + { + const selected = filteredSigunData.find(item => (item.val === val)); + setSelectedSigun(selected || { text: "전체", val: '0' }); + }} + value={selectedSigun.val} + selectClass={'w-32'} + /> + { + const selected = filteredDong.find(item => (item.val === val)); + setSelectedDong(selected || { text: "전체", val: '0', sggNm: '전체' }); + }} + value={selectedDong.val} + selectClass={'w-32'} + /> + + )} + {searchType == "지도보기" && ( +
+
{/* SearchFormLeft 래퍼 */} + +
+ +
+ +
+
+ )} +
+ {/* 버튼 부분 */} +
{showQueryButton && } {showInitButton && } - {showApplyButton && } + {showApplyButton && }
diff --git a/apps/wr/app/map/mapSearchForm.tsx b/apps/wr/app/map/mapSearchForm.tsx index 00ba8e6f0..a148fdf67 100644 --- a/apps/wr/app/map/mapSearchForm.tsx +++ b/apps/wr/app/map/mapSearchForm.tsx @@ -1,7 +1,7 @@ "use client" import React, { useEffect, useState } from 'react'; -import type { SelectDataType } from "@common/business_components/ui/search_input_select"; +import type { SelectDataType } from '@common/business_components/ui/search_input_select'; import { SelectBox } from '@common/business_components/ui/select'; /** @@ -56,13 +56,7 @@ const selectPartData: SelectDataType[] = [ { text: "환경청별", val: 'G' }, ]; -const selectType: SelectDataType[] = [ - { text: "행정별", val: 'A' }, - { text: "유역별", val: 'R' }, - { text: "환경청별", val: 'G' }, -]; - -function MapSearchForm({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { +function MapSearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { /******************선택된 데이터******************/ const [partValue, setPartValue] = useState('A'); //행정별 시도 시군구 @@ -314,27 +308,26 @@ function MapSearchForm({ isShowPart = false, children, selectSidoData = [], sele return ( isShowPart ? ( -
- onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined} /> +
+ onSelectValue(val.toString(), 'part')} value={partValue} selectClass={'w-32'} /> {partValue === 'R' ? ( <> - onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined} /> - onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={undefined} /> - onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={'w-44'} /> + onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={'w-44'} /> + onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={'w-44'} /> ) : partValue === 'G' ? ( <> - onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={undefined} /> - onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={undefined} /> - onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={'w-44'} /> + onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={'w-44'} /> + onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={'w-44'} /> ) : ( <> - onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={undefined} /> - onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={undefined} /> + onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={'w-40'} /> + onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={'w-40'} /> )} - {children}
) : (
@@ -344,4 +337,4 @@ function MapSearchForm({ isShowPart = false, children, selectSidoData = [], sele ) } -export { MapSearchForm }; +export { MapSearchFormLeft }; diff --git a/apps/wr/app/map/searchModal.tsx b/apps/wr/app/map/searchModal.tsx deleted file mode 100644 index 658c9545f..000000000 --- a/apps/wr/app/map/searchModal.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client" - -import { useWrStore } from "@/app/store"; -import { btnClass } from "@/app/store/common/code"; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { MapSearchBar, type ReturnType } from "./mapSearchBar"; -import { Button } from "@common/components/ui"; -import { SearchFormLeft, type EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; -import { SearchFormRight } from "@common/business_components/ui/search_form_right"; -import type { SelectDataType } from "@common/business_components/ui/search_input_select"; - -const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; - -const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); - -interface SearchModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onFeatureSelect: (row: any) => void; -} - -// 날짜 조합 함수 -const getDateString = ( - year: string, - month: string, - day: string, - defaultDate: string, -) => { - if (!year && !month && !day) return defaultDate; - const y = year || defaultDate.slice(0, 4); - const m = month || defaultDate.slice(4, 6); - const d = day || defaultDate.slice(6, 8); - return `${y}${m}${d}`; -}; - -const SearchModal: React.FC = ({ - open, - onOpenChange, - onFeatureSelect, -}) => { - - // 로딩바 - const [isLoading, setIsLoading] = useState(false); - // 상세 정보 조회를 위한 상태 추가 - const [popupFeature, setPopupFeature] = useState(null); - const { code, pbnstSttn, pbnstSttnSearch } = useWrStore((state) => state); - const [isChHead, setIsChHead] = useState(false); - const ref = useRef({} as ReturnType); - - return ( -
- -
- ); -}; - -export default React.memo(SearchModal); diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 330252074..3fabc529d 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -5,6 +5,8 @@ import { CS_URL } from "./components/lib/config"; const CLAIM_NAME = "Authorization"; const AUTH_USER_ID = "token-id"; const serverApiUrl = CS_URL; +const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; + export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; console.log(`[Middleware] Request URL: ${req.url}`); @@ -44,9 +46,18 @@ export function middleware(req: NextRequest) { }); } + if (pathname.startsWith("/geoserver")) { + const queryParams = searchParams.toString(); + const newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + return NextResponse.next(); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*"], + matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*"], }; From b095dda5adca7512e1928120d950e647d7a510e0 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 17 Jun 2025 19:01:02 +0900 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20=EC=A7=80=EB=8F=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=94=BC=EC=B2=98=20=EC=84=A0=ED=83=9D=EC=8B=9C=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=9C=20=ED=99=94=EB=A9=B4=EC=97=90=20?= =?UTF-8?q?=ED=94=BC=EC=B2=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/map/Gis.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 48dce9284..887207530 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -103,6 +103,7 @@ function Gis({ setFeatureData(data); setIsOpen(true); setIsSearchModalOpen(false); + onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef]); // 지도 생성 및 초기화 From 3208b02fb2379920407a5f8f54db34b1abac8524 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 17 Jun 2025 19:14:10 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=ED=99=94,=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B2=84=ED=8A=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/mapSearchBar.tsx | 66 +++++++++++++++++++------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx index ca746ccce..86e9f939a 100644 --- a/apps/wr/app/map/mapSearchBar.tsx +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -22,7 +22,7 @@ type SearchBaseType = { selectEnvPartData?: EnvPartSelectDataType[] | []; queryClick: (returnType: ReturnType) => void; initClick: () => void; - applyClick: () => void; + onFeatureSelect: () => void; btnClass?: string; ref?: React.RefObject; isAdmin?: boolean; @@ -60,7 +60,7 @@ export function MapSearchBar({ btnClass, queryClick, initClick, - applyClick, + onFeatureSelect, isAdmin = false, leftClassName, rightClassName, @@ -91,11 +91,11 @@ export function MapSearchBar({ // 공급처 후보지 조회 필터 목록 const [selectedSido, setSelectedSido] = useState({ text: "전체", val: '0' }); const [selectedSigun, setSelectedSigun] = useState({ text: "전체", val: '0' }); - const [selectedFclt, setSelectedFclt] = useState({ text: "전체", val: '0' }); + const [selectedFclt, setSelectedFclt] = useState({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 전체 시설 let allFclt: SelectedDataTypeWithSggNm[] = [{ text: "전체", val: '0', sggNm: '전체' }]; // 우리동네가뭄지도 필터 목록 - const [selectedDong, setSelectedDong] = useState({ text: "전체", val: '0' }); // 행정동 + const [selectedDong, setSelectedDong] = useState({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동 const [allSido, setAllSido] = useState([ { text: "전체", val: '0' }, @@ -131,32 +131,46 @@ export function MapSearchBar({ }, [selectedSigun]); const queryVal = () => { - console.log("조회버튼 클릭"); - queryClick({ - partValue: partValue, - sidoValue: sidoValue, - sigunValue: sigunValue, - regionValue: regionValue, - unitWaterShedValue: untitWaterShedValue, - middleRegionValue: middleRegionValue, - envPartValue: envPartValue, - sidoText: selectedSido.text, - sigunText: selectedSigun.text, - }); - } - - const initVal = useCallback(() => { - setResetTrigger(true); - initClick(); - }, [setResetTrigger, initClick]); - - const applyVal = useCallback(() => { if (searchType == '공급처 후보지 조회') { } else if (searchType == '우리동네가뭄지도보기') { } else if (searchType == '지도보기') { - + console.log("조회버튼 클릭"); + queryClick({ + partValue: partValue, + sidoValue: sidoValue, + sigunValue: sigunValue, + regionValue: regionValue, + unitWaterShedValue: untitWaterShedValue, + middleRegionValue: middleRegionValue, + envPartValue: envPartValue, + sidoText: selectedSido.text, + sigunText: selectedSigun.text, + }); } - }, [setResetTrigger, initClick]); + } + + const initVal = useCallback(() => { + setPartValue("A"); + setSidoValue("0"); + setSigunValue("0"); + setRegionValue("0"); + setUntitWaterShedValue("0"); + setMiddleRegionValue("0"); + setEnvPartValue("0"); + setSidoText("전체"); + setSigunText("전체"); + setSelectedSido({ text: "전체", val: '0' }); + setSelectedSigun({ text: "전체", val: '0' }); + setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); + setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); + setResetTrigger(true); + + initClick(); // 부모로부터 받은 초기화 함수 호출 + }, [initClick]); + + const applyVal = useCallback(() => { + onFeatureSelect + }, [onFeatureSelect]); useEffect(() => { if (resetTrigger) { From 04d43408f7ae71f3d37747b95f508f48e2e869f2 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 24 Jun 2025 12:14:54 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20-=20=ED=95=98=EC=88=98=EC=9E=AC?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=EC=8B=9C=EC=84=A4,=20=EB=B9=97=EB=AC=BC?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=EC=8B=9C=EC=84=A4,=20=EC=A4=91=EC=88=98?= =?UTF-8?q?=EB=8F=84=EC=8B=9C=EC=84=A4,=20=EC=98=A8=EB=B0=B0=EC=88=98?= =?UTF-8?q?=EC=8B=9C=EC=84=A4=20=ED=99=94=EB=A9=B4=EC=9D=98=20=ED=98=84?= =?UTF-8?q?=ED=99=A9(=EC=A7=80=EB=8F=84),=20=EB=AA=A9=EB=A1=9D,=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A0=95=EB=B3=B4=20UI=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(=EC=A7=80=EB=8F=84=EC=A0=90=EA=B2=80=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=20=EB=AF=B8=EA=B5=AC=ED=98=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/htdflPrfmncSttnPopup.tsx | 621 ++++++++++++++++++ apps/wr/app/map/htdflSttn/page.tsx | 142 ++-- apps/wr/app/map/mapSearchBar.tsx | 192 ++++-- apps/wr/app/map/mapSearchForm.tsx | 4 +- apps/wr/app/map/newWindowPopup.tsx | 380 +++++++++++ apps/wr/app/map/newWindowPortal.tsx | 90 +++ apps/wr/app/map/pbnstPrfmncSttnPopup.tsx | 596 +++++++++++++++++ apps/wr/app/map/rainPrfmncSttnPopup.tsx | 421 ++++++++++++ apps/wr/app/map/rswtrPrfmncSttnPopup.tsx | 404 ++++++++++++ apps/wr/app/page.tsx | 2 +- .../ruseFcltMng/fcltSttn/rainSttn/page.tsx | 34 +- .../sttn/pbnstSttnModal/pbnstSttnModal.tsx | 18 +- .../business_components/src/ui/map/Gis.js | 182 ++++- .../business_components/src/ui/map/GisTree.js | 7 +- 14 files changed, 2921 insertions(+), 172 deletions(-) create mode 100644 apps/wr/app/map/htdflPrfmncSttnPopup.tsx create mode 100644 apps/wr/app/map/newWindowPopup.tsx create mode 100644 apps/wr/app/map/newWindowPortal.tsx create mode 100644 apps/wr/app/map/pbnstPrfmncSttnPopup.tsx create mode 100644 apps/wr/app/map/rainPrfmncSttnPopup.tsx create mode 100644 apps/wr/app/map/rswtrPrfmncSttnPopup.tsx diff --git a/apps/wr/app/map/htdflPrfmncSttnPopup.tsx b/apps/wr/app/map/htdflPrfmncSttnPopup.tsx new file mode 100644 index 000000000..8d1bec486 --- /dev/null +++ b/apps/wr/app/map/htdflPrfmncSttnPopup.tsx @@ -0,0 +1,621 @@ +"use client"; + +import { + Form, + FormField, + FormUi, + SelectBox, +} from "@common/business_components/ui"; +import { Button, Input } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { useWrStore } from "@/app/store"; +import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { + hdtflZod, + type customHtdflBdst, +} from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; +import { useEffect } from "react"; +import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg"; +import { + formatCommaNumber, + handleCommaNumberChange, +} from "@/components/lib/func/commom"; +import { formH1Class } from "@/components/lib/class/comClass"; + +export default function HtdflPrfmncSttnPopup({ + htdflForm, + isModal, +}: { + htdflForm: customHtdflBdst; + isModal: boolean; +}) { + const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state); + + const defaultValues: z.infer = hdtflZod.parse({}); + + const form = useForm>({ + resolver: zodResolver(hdtflZod), + defaultValues: defaultValues, + }); + + const onSubmit = (values: z.infer) => { + if (isModal) { + htdflBdstReg(values, '41'); + } else { + htdflBdstUpdate(values, '41'); + } + }; + + + const cleanHtdflData = (raw: any): z.infer => { + return { + ...raw, + frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null), + archChgPrmsnDt: raw?.archChgPrmsnDt + ? new Date(raw?.archChgPrmsnDt) + : null, + chgBuilding: (raw?.chgBuilding || []).map((item: any) => ({ + ...item, + archChgPrmsnDt: + item?.archChgPrmsnDt === null + ? new Date() + : new Date(item?.archChgPrmsnDt), + bdstChgNm: item?.bdstChgNm ?? "", // null → "" + })), + reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({ + ...item, + instlCmptnDt: new Date(item?.instlCmptnDt), + htdflObj: { + ...item?.htdflObj, + }, + })), + }; + }; + + useEffect(() => { + console.log("form errors:", form.formState.errors); + }, [form.formState.errors]); + + useEffect(() => { + if (hdtflZod) form.reset(cleanHtdflData(htdflForm)); + }, [htdflForm]); + + return ( +
+
+ + {/*
*/} +
+
+

온배수 시설 정보

+
+
+ + {isModal && htdflForm != undefined ? : <>} + {isModal ? ( +

+ 온배수 시설 정보{" "} +

+ ) : ( + <> + )} + +
+
+ ( + + + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+

+ 온배수 시설 운영현황 +

+
+ +
+
+ ( + + { + field.onChange(Number(e.target.value)); + }} + readOnly={true} + /> + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ +
+

+ 요구수질 +

+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ +
+
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+ +
+ ( + + { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + + )} + /> +
+
+ + +
+ ); +} diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx index 6c75edb92..5ba199c8c 100644 --- a/apps/wr/app/map/htdflSttn/page.tsx +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -1,18 +1,23 @@ "use client"; import Gis from "@common/business_components/ui/map/Gis"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useCallback } from "react"; import FeatureClickEventPopup from "../featureClickEventPopup"; import { mapService } from "../service"; import { MapSearchBar } from "../mapSearchBar"; +import NewWindowPopup from "../newWindowPopup"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 - const gisMaps = useRef(null); + const gisMaps = useRef(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); + const fetchData = async () => { try { const response = await mapService.getLyrStngs(); @@ -22,80 +27,92 @@ const Map = () => { } }; + // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 + const handlePopupClose = useCallback(() => { + setIsOpen(false); + setFeatureData(null); + }, []); + // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); + // 체크된 레이어 정보를 받는 핸들러 + const handleCheckedLayersChange = (checkedIds: string[], checkedNodesData: any[]) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + }; + // SearchModal, GisContent 등에서 호출 - // const onFeatureSelect = (data: any) => { - // let map = gisMaps.current; - // if (!map) { - // console.warn("지도가 초기화되지 않았습니다."); - // return; - // } + const onFeatureSelect = (data: any) => { + let map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } - // // 항상 같은 select 객체 사용 - // const select = GisApp.Module.select as any; - // if (!select) return; + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; - // // 1. 기존 선택 초기화 - // select.resetSelectedFeature(); + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); - // // 2. data가 OpenLayers Feature 인스턴스인지 확인 - // let foundFeature = null; - // if ( - // data && - // typeof data.getGeometry === "function" && - // typeof data.setStyle === "function" - // ) { - // // 이미 Feature 인스턴스라면 그대로 사용 - // foundFeature = data; - // } else { - // // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - // const featureId = data.fid || data.ftrIdn || data.id; - // map - // .getLayers() - // .getArray() - // .forEach((layer: any) => { - // const source = layer.getSource?.(); - // if (source?.getFeatureById) { - // const feature = source.getFeatureById(String(featureId).trim()); - // if (feature) foundFeature = feature; - // } - // }); - // } + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } - // // 3. 피처 선택 - // if (foundFeature) { - // let geometry = foundFeature.getGeometry(); - // if (geometry) { - // let extent = geometry.getExtent(); + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); - // map.getView().fit(extent, { - // duration: 300, - // padding: [50, 50, 50, 50], - // maxZoom: 16, - // constrainResolution: true, - // nearest: true, - // }); - // } - // select.selectFeature(foundFeature, true); - // } else { - // console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); - // } + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } - // setFeatureData(data); - // setIsOpen(true); - // setIsSearchModalOpen(false); - // }; + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; // MapSearchBar 래퍼 컴포넌트 const CustomMapSearchBar = (props: any) => { return ( ); @@ -109,13 +126,20 @@ const Map = () => { gisMaps={gisMaps} isMiniMap={true} searchView={CustomMapSearchBar} - detailView={FeatureClickEventPopup} + detailView={(props: any) => ( + + )} + onFeatureSelect={undefined} setIsOpen={setIsOpen} setFeatureData={setFeatureData} featureData={featureData} setIsSearchModalOpen={setIsSearchModalOpen} isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={handleCheckedLayersChange} />
); diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx index 86e9f939a..f5d693ee3 100644 --- a/apps/wr/app/map/mapSearchBar.tsx +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -2,15 +2,35 @@ import { Button, Input, Label } from "@common/components/ui"; import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"; -import { SearchFormLeft, type EnvPartSelectDataType } from "@common/business_components/ui/search_form_left"; -import { SearchFormRight } from "@common/business_components/ui/search_form_right"; -import type { SelectDataType } from "@common/business_components/ui/search_input_select"; -import type { ReturnType } from "@common/business_components/ui/search_base_form"; +import { useForm } from "react-hook-form"; import { MapSearchFormLeft } from "./mapSearchForm"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; -import { SelectBox } from "@common/business_components/ui/select"; import { SearchBaseForm } from "@common/business_components"; +import { FormUi, SelectBox, SearchFormLeft, SearchFormRight, Form, FormField } from "@common/business_components/ui"; +import type { ReturnType, SelectDataType, EnvPartSelectDataType } from "@common/business_components/ui"; +import SearchInputText from "@/components/ui/searchInput/searchInput"; +import { zodResolver } from "@hookform/resolvers/zod"; + + +// 건물 검색 타입 정의 +interface BuildingInfo { + bdstInfoPk: number; + ctpvCd: number; + sggCd: number; + daddr: string; + bdstNm: string; + frstArchPrmsnDt: string; + archArea: number; + ctpvNm: string | null; + sggNm: string | null; + lat: number | null; + lot: number | null; + regDt: string; + rgtr: string | null; + mdfcnDt: string; + mdfr: string | null; +} type SearchBaseType = { searchType: string; @@ -20,8 +40,6 @@ type SearchBaseType = { selectUnitWaterShedData?: SelectDataType[] | []; selectMiddleRegionData?: SelectDataType[] | []; selectEnvPartData?: EnvPartSelectDataType[] | []; - queryClick: (returnType: ReturnType) => void; - initClick: () => void; onFeatureSelect: () => void; btnClass?: string; ref?: React.RefObject; @@ -58,8 +76,6 @@ export function MapSearchBar({ selectMiddleRegionData, selectEnvPartData, btnClass, - queryClick, - initClick, onFeatureSelect, isAdmin = false, leftClassName, @@ -77,6 +93,10 @@ export function MapSearchBar({ const [isChHead, setIsChHead] = useState(false); const ref = useRef({} as ReturnType); + const [selectedBuilding, setSelectedBuilding] = useState(null); + const [buildingSearchText, setBuildingSearchText] = useState(""); + const [buildingList, setBuildingList] = useState([]); + // 전체 목록 const [partValue, setPartValue] = useState("A"); const [sidoValue, setSidoValue] = useState("0"); @@ -97,15 +117,6 @@ export function MapSearchBar({ // 우리동네가뭄지도 필터 목록 const [selectedDong, setSelectedDong] = useState({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동 - const [allSido, setAllSido] = useState([ - { text: "전체", val: '0' }, - ...(code?.cityCd || []) - ]); - const [allRegion, setAllRegion] = useState([ - { text: "전체", val: '0' }, - ...(code?.rvbsnCd || []) - ]); - const filteredSigunData = useMemo(() => { if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }]; return [ @@ -130,26 +141,82 @@ export function MapSearchBar({ ]; }, [selectedSigun]); + // 건물 검색 API 호출 함수 + const searchBuildings = async (searchText: string) => { + if (!searchText.trim()) { + setBuildingList([]); + return; + } + + try { + const flg = "41"; // 적절한 플래그 값으로 설정 + const sigunCode = selectedSigun.val !== '0' ? selectedSigun.val : ''; + const apiEndpoint = `${API_URL}/v1/reuseFc/building/${flg}/${sigunCode}/${encodeURIComponent(searchText)}`; + + const response = await fetch(apiEndpoint, { + method: "GET", + headers: { + accept: "application/json", + "Content-Type": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`API 호출 실패: ${response.status}`); + } + + const result = await response.json(); + + if (result.status === 200 && Array.isArray(result.data)) { + // 건물 검색 결과를 demandCompList 형태로 변환 + const convertedList = result.data.map((building: BuildingInfo) => ({ + dmdInfoPk: String(building.bdstInfoPk), + dmdCoNm: building.bdstNm, + daddr: building.daddr, + useUsgCd: "", + splyCpct: 0, + splyFmYn: "Y", + // 추가 필드들... + })); + setBuildingList(result.data); + // 글로벌 상태에도 업데이트 (SearchInputText가 사용할 수 있도록) + // getDemandCompList를 직접 호출하는 대신 상태를 직접 설정 + const { setBuildingSearchResults } = useWrStore.getState(); + if (setBuildingSearchResults) { + setBuildingSearchResults(convertedList); + } + } else { + setBuildingList([]); + } + } catch (error) { + console.error("건물 검색 중 오류 발생:", error); + setBuildingList([]); + } + }; + + // 조회 함수 const queryVal = () => { + console.log("조회버튼 클릭"); if (searchType == '공급처 후보지 조회') { + // queryClick({ + // partValue: partValue, + // sidoValue: sidoValue, + // sigunValue: sigunValue, + // regionValue: regionValue, + // unitWaterShedValue: untitWaterShedValue, + // middleRegionValue: middleRegionValue, + // envPartValue: envPartValue, + // sidoText: selectedSido.text, + // sigunText: selectedSigun.text, + // }); } else if (searchType == '우리동네가뭄지도보기') { } else if (searchType == '지도보기') { - console.log("조회버튼 클릭"); - queryClick({ - partValue: partValue, - sidoValue: sidoValue, - sigunValue: sigunValue, - regionValue: regionValue, - unitWaterShedValue: untitWaterShedValue, - middleRegionValue: middleRegionValue, - envPartValue: envPartValue, - sidoText: selectedSido.text, - sigunText: selectedSigun.text, - }); + } } - const initVal = useCallback(() => { + // 초기화 함수 + const initVal = () => { setPartValue("A"); setSidoValue("0"); setSigunValue("0"); @@ -164,13 +231,12 @@ export function MapSearchBar({ setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); setResetTrigger(true); - - initClick(); // 부모로부터 받은 초기화 함수 호출 - }, [initClick]); + }; - const applyVal = useCallback(() => { - onFeatureSelect - }, [onFeatureSelect]); + // 적용 버튼 + const applyVal = () => { + onFeatureSelect(); + }; useEffect(() => { if (resetTrigger) { @@ -251,10 +317,11 @@ export function MapSearchBar({ { const selected = code.cityCd.find(item => (item.val === val)); setSelectedSido(selected || { text: "전체", val: '0' }); + setSelectedSigun({ text: "전체", val: '0' }); }} value={selectedSido.val} selectClass={'w-32'} @@ -266,24 +333,39 @@ export function MapSearchBar({ onSelectValue={(val) => { const selected = filteredSigunData.find(item => (item.val === val)); setSelectedSigun(selected || { text: "전체", val: '0' }); + setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); }} value={selectedSigun.val} selectClass={'w-32'} /> - { - const selected = allFclt.find(item => (item.val === val)); - setSelectedFclt(selected || { text: "전체", val: '0', sggNm: '전체' }); - }} - value={selectedFclt.val} - selectClass={'w-32'} - /> - + {/*
+
*/} +
+ +
+ { + setBuildingSearchText(searchText || ""); + if (searchText) { + searchBuildings(searchText); + } + }} + setRtnData={(buildingData) => { + if (buildingData && buildingData.length > 0 && buildingData[0]) { + // 선택된 건물 정보를 찾아서 설정 + const selectedBuildingInfo = buildingList.find(building => + building.bdstNm === buildingData[0].dmdCoNm + ); + setSelectedBuilding(selectedBuildingInfo || null); + console.log("선택된 시설:", selectedBuildingInfo); + } + }} + /> +
)} @@ -292,13 +374,14 @@ export function MapSearchBar({ { const selected = code.cityCd.find(item => (item.val === val)); setSelectedSido(selected || { text: "전체", val: '0' }); + setSelectedSigun({ text: "전체", val: '0' }); }} value={selectedSido.val} - selectClass={'w-32'} + selectClass={'w-44'} /> { const selected = filteredSigunData.find(item => (item.val === val)); setSelectedSigun(selected || { text: "전체", val: '0' }); + setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); }} value={selectedSigun.val} selectClass={'w-32'} @@ -329,9 +413,9 @@ export function MapSearchBar({
{/* SearchFormLeft 래퍼 */} ) : ( <> - onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={'w-40'} /> - onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={'w-40'} /> + onSelectValue(val.toString(), 'sido')} value={sidoValue} selectClass={'w-44'} /> + onSelectValue(val.toString(), 'sigun')} value={sigunValue} selectClass={'w-44'} /> )}
diff --git a/apps/wr/app/map/newWindowPopup.tsx b/apps/wr/app/map/newWindowPopup.tsx new file mode 100644 index 000000000..e67b2b0fb --- /dev/null +++ b/apps/wr/app/map/newWindowPopup.tsx @@ -0,0 +1,380 @@ +import { useWrStore } from "@/app/store"; +import { btnClass } from "@/app/store/common/code"; +import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; +import { excelDownload } from "@/components/lib/func/commom"; +import { Title, UiTable, UITab, ContentsDiv } from '@common/business_components'; +import { HeadMakeColSpan } from "@common/business_components/ui/table/HeadMakeColSpan"; +import { Button } from "@common/components/ui"; +import React, { useCallback, useState, useEffect } from 'react'; +import NewWindowPortal from '@/app/map/newWindowPortal'; +import PbnstPrfmncSttnPopup from "@/app/map/pbnstPrfmncSttnPopup"; +import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal"; +import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type"; +import type { chgBuilding, customRainBdst } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import RainPrfmncSttnPopup from "@/app/map/rainPrfmncSttnPopup"; +import RswtrPrfmncsSttnPopup from "@/app/map/rswtrPrfmncSttnPopup"; +import type { customRswtBdst } from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; +import HtdflPrfmncSttnPopup from "@/app/map/htdflPrfmncSttnPopup"; +import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg"; + +interface NewWindowPopupProps { + featureData: any; + checkedLayers?: string[]; + checkedLayersData?: any[]; + onClose: () => void; +} + +// 탭 +const TABS = [ + { val: "0", text: "재이용시설1" }, + { val: "1", text: "재이용시설2" }, + { val: "2", text: "재이용시설n" }, +]; + +// 데이터의 속성(key)을 사용자에게 보여줄 한글 이름으로 변환하기 위한 맵 +// 실제 데이터에 맞게 이 부분을 확장해야 합니다. +const propertyNameMap: { [key: string]: string } = { + fid: '고유 ID', + fcltNm: '시설명', + opertDe: '운영일자', + addr: '주소', + // ... 필요한 만큼 속성 매핑 추가 +}; + +const NewWindowPopup: React.FC = ({ + featureData, + checkedLayers = [], + checkedLayersData = [], + onClose +}) => { + const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "0"); + const [showDetailView, setShowDetailView] = useState(false); // 상세정보 화면 표시 상태 추가 + + // onClose 함수를 useCallback으로 메모이제이션 + const memoizedOnClose = useCallback(() => { + onClose(); + }, [onClose]); + + // 하수재이용시설 변수 + const { + code, + pbnstSttn, + pbnstSttnSearch, + pbnSttnInit, + getPbnStnDetail, + pbnNmSearch, + } = useWrStore((state) => state); + const [bdstTable, setBdstTable] = useState(); + const [pageNation, setPageNation] = useState(1); + const [pbnDetaileData, setPbnDetaileData] = useState<{ + isModal: boolean; + data: customPbnst; + }>({ isModal: false, data: {} as customPbnst }); + + // 빗물재이용시설 변수 + const { rainSttn, rainSttnSearch, rainSttnInit } = useWrStore( + (state) => state + ); + const { rainSttnRes } = rainSttn; + const [rainTable, setRainTable] = useState(); + const [rainBdstChgProps, setRainBdstChgProps] = useState<{ + isOpen: boolean; + chBuilding: chgBuilding[]; + }>({ isOpen: false, chBuilding: [] }); + const [rainDetailModal, setRainDetailModal] = useState<{ isOpen: boolean, data: customRainBdst | undefined }>({ data: undefined, isOpen: false }); + + // 중수도시설 변수 + const { rswtSttn, rswtSttnSearch, rswtSttnInit } = useWrStore( + (state) => state + ); + const { rswtSttnRes } = rswtSttn; + const [rswtrTable, setRswtrTable] = useState(); + const [rswtrDetailModal, setRswtrDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false }); + const [rswtrBdstChgProps, setRswtrBdstChgProps] = useState<{ + isOpen: boolean; + chBuilding: chgBuilding[]; + }>({ isOpen: false, chBuilding: [] }); + + // 온배수시설 변수 + const { htdflSttn, htdflSttnSearch, initHtdfl } = useWrStore((state) => state); + const { htdflSttnRes } = htdflSttn; + const [htdflTable, setHtdflTable] = useState(); + const [htdflDetailModal, setHtdflDetailModal] = useState<{ isOpen: boolean, data: customHtdflBdst | undefined }>({ data: undefined, isOpen: false }); + const [bdstChgProps, setBdstChgProps] = useState<{ isOpen: boolean, chBuilding: chgBuilding[] }>({ isOpen: false, chBuilding: [] }); + + if (!featureData) { + return null; + } + + // 2. RuseMngTable에 필요한 tableData를 생성합니다. (단일 행 데이터) + const tableData = [featureData]; + console.log("tableData", tableData); + + // 물재이용시설 cellClick 이벤트 (상세정보) + const pbnstCellClick = (index: number | undefined) => { + if ( + index !== undefined && + pbnstSttn.pbnstSttnRes && + pbnstSttn.pbnstSttnRes[index] + ) { + const pbnstRuseFcltPk = pbnstSttn.pbnstSttnRes[index].pbnstRuseFcltPk; + getPbnStnDetail(pbnstRuseFcltPk); + } + }; + + useEffect(() => { + setBdstTable(pbnstSttn.pbnstSttnRes); + }, [pbnstSttn.pbnstSttnRes]); + + useEffect(() => { + if (pbnstSttn.pbnstDetail.pbnstRuseFcltPk) setShowDetailView(true); + }, [pbnstSttn.pbnstDetail]); + + + // 빗물재이용시설 cellClick 이벤트 (상세정보) + const rainCellClick = (index: number | undefined, headId: string) => { + if ( + index !== undefined && + rainTable !== undefined && + headId !== 'chBuildingYn') { + setRainDetailModal({ isOpen: true, data: rainTable[index] }) + } else if (headId === 'chBuildingYn' && index !== undefined && rainTable !== undefined) { + if (rainTable[index] && rainTable[index]?.chgBuilding[0]?.bdstChgNm !== null) + setRainBdstChgProps({ isOpen: true, chBuilding: rainTable[index].chgBuilding }); + } + } + + // 중수도시설 cellClick 이벤트 (상세정보) + const rswtCellClick = (index: number | undefined, headId: string) => { + if (headId !== 'chBuildingYn' && index !== undefined && rswtrTable !== undefined) { + setRswtrDetailModal({ isOpen: true, data: rswtrTable[index] }) + } else if (headId === 'chBuildingYn' && index !== undefined && rswtrTable !== undefined) { + if (rswtrTable[index] && rswtrTable[index]?.chBuildingYn === 'Y') { + setRswtrBdstChgProps({ isOpen: true, chBuilding: rswtrTable[index].chgBuilding }); + } + } + } + + // 온배수시설 cellClick 이벤트 (상세정보) + const htdflCellClick = (index: number | undefined, headId: string) => { + if ( + headId !== "chBuildingYn" && + index !== undefined && + htdflTable !== undefined + ) { + setHtdflDetailModal({ isOpen: true, data: htdflTable[index] }); + } + }; + + const excelClick = useCallback(() => { + // if (codeData) pbnstSttnSearch(1, "10000", codeData); + excelDownload( + [[]], + "하수재이용시설 현황", + "sample_pbnstSttn.xlsx", + "A1:U2" + ); + }, [pbnstSttnSearch]); + + // 체크된 레이어에 따른 조건부 컴포넌트 렌더링 함수 + const renderConditionalContent = () => { + // 특정 레이어가 체크되었는지 확인 + const hasWaterTreatmentLayer = checkedLayersData.some(layer => + layer.data?.lyrNm?.includes('하수처리') || layer.data?.lyrNm?.includes('재이용시설') + ); + + // 재이용시설 + const isPbnst = false; + // 빗물이용시설 + const isRain = false; + // 중수도시설 + const isRswt = false; + // 온배수시설 + const isHtdfl = true; + + + return ( +
+ {/* 체크된 레이어가 없는 경우 (재이용시설 목록) */} + {isPbnst && ( + <> + {showDetailView ? ( + // 재이용시설 상세 + <> +
+ + </div> + <div className="flex justify-between items-center mb-8"> + <UITab + defaultValue={TABS[0]?.val} + tabList={TABS.map((tab) => ({ + value: tab.val, + label: tab.text, + onClick: () => setActiveTab(tab.val), + }))} + children={undefined} + /> + </div> + <PbnstSttnModal + data={pbnDetaileData.data || ({} as customPbnst)} + titleVisible={false} + /> + <PbnstPrfmncSttnPopup isVisible={false} /> + </> + ) : ( + // 재이용시설 목록 + <> + <div className="flex justify-between items-center"> + <Title title='하수재이용시설 현황' /> + <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button> + </div> + </div> + <UiTable + headName={[]} + tableData={bdstTable || []} + children={ + <HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} /> + } + headlist={pbnstSttn.pbnstSttHeadList} + pageSize={20} + total={100000} + cellClick={(index: number | undefined, headId: string) => { + pbnstCellClick(index); + }} + onPageChange={setPageNation} + /> + </> + )} + </> + )} + {/* 빗물이용시설 */} + {isRain && ( + <> + {showDetailView ? ( + // 빗물이용시설 상세 + <> + <div className="flex justify-between items-center mb-4"> + <Title title='빗물이용시설 상세정보' /> + </div> + <RainPrfmncSttnPopup rainForm={rainDetailModal.data as customRainBdst} isModal={false} /> + {/* TODO: 지도 점검 결과 현황 추가 할 것 */} + </> + ) : ( + // 빗물이용시설 목록 + <> + <div className="flex justify-between items-center"> + <Title title='빗물이용시설 현황' /> + <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button> + </div> + </div> + <UiTable + headName={[]} + tableData={rainTable || []} + children={ + <HeadMakeColSpan upHeadList={rainSttn.rainSttNodeList} /> + } + headlist={rainSttn.rainSttHeadList} + pageSize={20} + total={100000} + cellClick={(index: number | undefined, headId: string) => { + rainCellClick(index ?? undefined, headId) + }} + onPageChange={setPageNation} + /> + </> + )} + </> + )} + {/* 중수도시설 */} + {isRswt && ( + <> + {showDetailView ? ( + // 재이용시설 상세 + <> + <div className="flex justify-between items-center mb-4"> + <Title title='중수도시설 상세정보' /> + </div> + <RswtrPrfmncsSttnPopup rswtForm={rswtrDetailModal.data as customRswtBdst} isModal={false} /> + {/* TODO: 지도 점검 결과 현황 추가 할 것 */} + </> + ) : ( + // 재이용시설 목록 + <> + <div className="flex justify-between items-center"> + <Title title='중수도시설 현황' /> + <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button> + </div> + </div> + <UiTable + headName={[]} + tableData={rswtrTable || []} + children={ + <HeadMakeColSpan upHeadList={rswtSttn.rswtSttNodeList} /> + } + headlist={rswtSttn.rswtSttHeadList} + pageSize={20} + total={100000} + cellClick={(index: number | undefined, headId: string) => { + rswtCellClick(index, headId); + }} + onPageChange={setPageNation} + /> + </> + )} + </> + )} + {/* 온배수시설 */} + {isHtdfl && ( + <> + {showDetailView ? ( + // 재이용시설 상세 + <> + <div className="flex justify-between items-center mb-4"> + <Title title='온배수시설 상세정보' /> + </div> + {/* <HtdflPrfmncSttnPopup htdflForm={htdflDetailModal.data as customHtdflBdst} isModal={false} /> */} + <HtdflPrfmncSttnPopup htdflForm={htdflDetailModal.data as customHtdflBdst} isModal={false} /> + </> + ) : ( + // 재이용시설 목록 + <> + <div className="flex justify-between items-center"> + <Title title='온배수시설 현황' /> + <div className="flex gap-2 mt-8"> + <Button className={btnClass} color="primary" onClick={excelClick}>엑셀다운로드</Button> + </div> + </div> + <UiTable + headName={[]} + tableData={bdstTable || []} + children={ + <HeadMakeColSpan upHeadList={htdflSttn.htdflSttNodeList} /> + } + headlist={htdflSttn.htdflSttHeadList} + pageSize={20} + total={100000} + cellClick={(index: number | undefined, headId: string) => { + htdflCellClick(index ?? undefined, headId); + }} + onPageChange={setPageNation} + /> + </> + )} + </> + )} + </div> + ); + }; + + return ( + <NewWindowPortal onClose={memoizedOnClose} title="시설 상세 정보"> + {/* 체크된 레이어에 따른 조건부 렌더링 */} + {renderConditionalContent()} + </NewWindowPortal> + ); +}; + +export default NewWindowPopup; \ No newline at end of file diff --git a/apps/wr/app/map/newWindowPortal.tsx b/apps/wr/app/map/newWindowPortal.tsx new file mode 100644 index 000000000..49949e0f4 --- /dev/null +++ b/apps/wr/app/map/newWindowPortal.tsx @@ -0,0 +1,90 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { createPortal } from 'react-dom'; + +// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수 +const copyStyles = (sourceDoc: Document, targetDoc: Document) => { + Array.from(sourceDoc.styleSheets).forEach(styleSheet => { + try { + if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS + const newStyleEl = sourceDoc.createElement('style'); + Array.from(styleSheet.cssRules).forEach(cssRule => { + newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText)); + }); + targetDoc.head.appendChild(newStyleEl); + } else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일 + const newLinkEl = sourceDoc.createElement('link'); + newLinkEl.rel = 'stylesheet'; + newLinkEl.href = styleSheet.href; + targetDoc.head.appendChild(newLinkEl); + } + } catch (e) { + console.warn('스타일시트를 복사할 수 없습니다:', e); + } + }); +}; + +interface NewWindowPortalProps { + children: React.ReactNode; + onClose: () => void; + title?: string; +} + +const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "Detail" }) => { + const [container, setContainer] = useState<HTMLElement | null>(null); + const newWindowRef = useRef<Window | null>(null); + const onCloseRef = useRef(onClose); + + // onClose 함수를 ref로 저장하여 의존성 문제 해결 + useEffect(() => { + onCloseRef.current = onClose; + }, [onClose]); + + const handleUnload = useCallback(() => { + onCloseRef.current(); + }, []); + + useEffect(() => { + // 이미 창이 열려있으면 새로 열지 않음 + if (newWindowRef.current && !newWindowRef.current.closed) return; + + // 1. 새 창을 열고 참조를 저장합니다. + newWindowRef.current = window.open('', title, 'width=1400,height=1024,resizable=yes,scrollbars=yes'); + + if (newWindowRef.current) { + newWindowRef.current.document.title = title; + + // 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다. + const div = newWindowRef.current.document.createElement('div'); + newWindowRef.current.document.body.appendChild(div); + setContainer(div); + + // 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요) + copyStyles(document, newWindowRef.current.document); + + // 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다. + // const handleUnload = () => { + // onClose(); + // }; + newWindowRef.current.addEventListener('unload', handleUnload); + + // 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다. + return () => { + if (newWindowRef.current) { + newWindowRef.current.removeEventListener('unload', handleUnload); + if (!newWindowRef.current.closed) { + newWindowRef.current.close(); + } + } + }; + } else { + alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.'); + onCloseRef.current(); + // onClose(); + } + }, [onClose, title]); + + // `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다. + return container ? createPortal(children, container) : null; +}; + +export default NewWindowPortal; \ No newline at end of file diff --git a/apps/wr/app/map/pbnstPrfmncSttnPopup.tsx b/apps/wr/app/map/pbnstPrfmncSttnPopup.tsx new file mode 100644 index 000000000..f6d870092 --- /dev/null +++ b/apps/wr/app/map/pbnstPrfmncSttnPopup.tsx @@ -0,0 +1,596 @@ +"use client"; + +import { useWrStore } from "@/app/store"; +import { initValInUsage, initValOutUsage, subTotalObj, totalObj, type FcPrfType, type PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; +import { CHART_FORM, ContentsDiv, MainChart, ScrollArea, SelectBox, UiTable, type ChartConfig, type ReturnType } from "@common/business_components"; +import ListDiv from "@common/business_components/ui/list_div"; +import { useEffect, useRef, useState } from "react"; +import { Toaster } from "react-hot-toast"; + +interface PbnstPrfmncSttnPopupProps { + isVisible?: boolean; +} + +export default function PbnstPrfmncSttnPopup({ isVisible = true }: PbnstPrfmncSttnPopupProps) { + const { code, tableHeadMaker, pbnstPrfmncSttn, pbnstPrfmncInit, pbnstPrfmncSearch, prfmnc } = useWrStore((state) => state); + const { monthHeadList } = prfmnc + const { pbnstPrfHeadList, pbnstPrfmncRes, prfmncRes, rawData, selectYearList } = pbnstPrfmncSttn; + const [isChHead, setIsChHead] = useState(false); + const [fromDate, setFromDate] = useState<string>(String(new Date().getFullYear())); + const [toDate, setToDate] = useState<string>(String(new Date().getFullYear())); + const [year, setYear] = useState<string>(String(new Date().getFullYear())); + const [bdstTable, setbdstTable] = useState<FcPrfType[]>(); + const ref = useRef<ReturnType>({} as ReturnType); + const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(); + const [chartConfig, setChartConfig] = useState<ChartConfig>({}); + const [chartData, setChartData] = useState<dnChartForm[]>([]); + const [dnInUsage, setDnInUsage] = useState<dnChartForm[]>([]); + const [dnInChartConfig, setDnInChartConfig] = useState<ChartConfig>({}); + const [dnOutUsage, setDnOutUsage] = useState<dnChartForm[]>([]); + const [dnOutChartConfig, setDnOutChartConfig] = useState<ChartConfig>({}); + const [barData, setBarData] = useState<barChartForm[]>([]); + const [barChartConfig, setBarChartConfig] = useState<ChartConfig>({}); + const [prfData, setPrfData] = useState<any[]>(); + const [lnInChartConfig, setLnInChartConfig] = useState<ChartConfig>({}); + const [lnOutChartConfig, setLnOutChartConfig] = useState<ChartConfig>({}); + const [inLineData, setInLineData] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + chartData3: lineChartDataProps[]; + chartData4: lineChartDataProps[]; + chartData5: lineChartDataProps[]; + chartData6: lineChartDataProps[]; + chartData7: lineChartDataProps[]; + chartData8: lineChartDataProps[]; + chartData9: lineChartDataProps[]; + }>(); + // const [lnInChartConfig, setLnInChartConfig] = useState<ChartConfig>({}); + const [outLineData, setOutLineData] = useState<{ + chartData1: lineChartDataProps[]; + chartData2: lineChartDataProps[]; + chartData3: lineChartDataProps[]; + chartData4: lineChartDataProps[]; + chartData5: lineChartDataProps[]; + chartData6: lineChartDataProps[]; + }>(); + + + + type dnChartForm = { label: string, val: number, fill?: string }; + type barChartForm = { label: string, "장내용수": number, "장외용수": number }; + type lineChartDataProps = { label: string, val: number, color: string }; + + const prfTableData = (prfList: PrfType[], year: string) => { + const inUsageData = initValInUsage; + const outUsageData = initValOutUsage; + + const inSubtotal = subTotalObj; + const outSubtotal = subTotalObj; + + console.log(' prfList ', prfList) + const prfFilter = prfList?.filter((e) => e.ym.slice(0, 4) === year) + if (prfList && prfFilter.length > 0) { + prfList?.forEach((prfItem) => { + const month = Number(prfItem?.ym?.slice(4)); // "MM" + const monthKey = `${month}월`; + + inUsageData.forEach((inData: any) => { + const usageKey = inData.usage; + const value = (prfItem as any)[usageKey] ?? 0; + + inData[monthKey] = value; + inData.total += value; + + if (inSubtotal && usageKey !== "subtotal") { + (inSubtotal as any)[monthKey] = + ((inSubtotal as any)[monthKey] ?? 0) + value; + inSubtotal.total += value; + } + }); + + outUsageData.forEach((outData: any) => { + const usageKey = outData.usage; + const value = (prfItem as any)[usageKey] ?? 0; + + outData[monthKey] = value; + outData.total += value; + + if (usageKey !== "subtotal") { + (outSubtotal as any)[monthKey] = + ((outSubtotal as any)[monthKey] ?? 0) + value; + outSubtotal.total += value; + } + }); + }); + inUsageData.push(inSubtotal); + outUsageData.push(outSubtotal); + const usageTotalObj: { [key: string]: any } = totalObj; + for (const key in usageTotalObj) { + if (["usage", "gubun", "edit"].includes(key)) continue; + const val = + (Number((inSubtotal as any)[key]) || 0) + + (Number((outSubtotal as any)[key]) || 0); + + (usageTotalObj as any)[key] = val; + } + const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; + setPrfData(tableData); + + } else { + setPrfData([]); + } + } + useEffect(() => { + let wshwtr = 0; + let coolUswtr = 0; + let cleanUswtr = 0; + let drwtUswtr = 0; + let dltnUswtr = 0; + let ithlRswtr = 0; + let ithlEtcUswtr = 0; + let cleanToilUswtr = 0; + let resn = 0; + let lndwtr = 0; + let hydUswtr = 0; + let ugwtChr = 0; + let farmUswtr = 0; + let rvrKeepUswtr = 0; + let indstUswtr = 0; + if (rawData) { + rawData.forEach((item) => { + prfTableData(item.prfList, year); + item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + + wshwtr += element.wshwtr; //세척수 + coolUswtr += element.coolUswtr; //냉각수 + cleanUswtr += element.cleanUswtr; //청소수 + drwtUswtr += element.drwtUswtr; //식수대 + dltnUswtr += element.dltnUswtr //희석용수 + ithlRswtr += element.ithlRswtr //중수도 + ithlEtcUswtr += element.ithlEtcUswtr //기타 + cleanToilUswtr += element.cleanToilUswtr //청소화장실용수 + resn += element.resn //세척살수용수 + + lndwtr += element.lndwtr; //조경용수 + hydUswtr += element.hydUswtr; //친수용수 + ugwtChr += element.ugwtChr; //지하수충전 + farmUswtr += element.farmUswtr //농업용수 + rvrKeepUswtr += element.rvrKeepUswtr //하천등유지용수 + indstUswtr += element.indstUswtr //공업용수 + + }); + }) + const data: dnChartForm[] = [] + data?.push({ label: "세척수", val: wshwtr, fill: "red" }) + data?.push({ label: "냉각수", val: coolUswtr, fill: "orange" }) + data?.push({ label: "청소수", val: cleanUswtr, fill: "yellow" }) + data?.push({ label: "식수대", val: drwtUswtr, fill: "green" }) + data?.push({ label: "희석용수", val: dltnUswtr, fill: "skyblue" }) + data?.push({ label: "중수도", val: ithlRswtr, fill: "blue" }) + data?.push({ label: "기타", val: ithlEtcUswtr, fill: "purple" }) + data?.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "red" }) + data?.push({ label: "세척살수용수", val: resn, fill: "orange" }) + data?.push({ label: "조경용수", val: lndwtr, fill: "yellow" }) + data?.push({ label: "친수용수", val: hydUswtr, fill: "green" }) + data?.push({ label: "지하수충전", val: ugwtChr, fill: "skyblue" }) + data?.push({ label: "농업용수", val: farmUswtr, fill: "blue" }) + data?.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "purple" }) + data?.push({ label: "공업용수", val: indstUswtr, fill: "black" }) + setChartData(data); + } + }, [rawData, year]) + + + useEffect(() => { + const newChartConfig = chartData.reduce((config, element) => { + config[element.label] = { + label: element.label, + fill: element.fill + }; + return config; + }, {} as Record<string, { label: string; fill?: string }>) satisfies ChartConfig; + setChartConfig(newChartConfig); + }, [chartData]); + + // 장내용수 도넛차트 + useEffect(() => { + let wshwtr = 0; + let coolUswtr = 0; + let cleanUswtr = 0; + let drwtUswtr = 0; + let dltnUswtr = 0; + let ithlRswtr = 0; + let ithlEtcUswtr = 0; + let cleanToilUswtr = 0; + let resn = 0; + if (rawData) { + rawData.forEach((item) => { + item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + + wshwtr += element.wshwtr; //세척수 + coolUswtr += element.coolUswtr; //냉각수 + cleanUswtr += element.cleanUswtr; //청소수 + drwtUswtr += element.drwtUswtr; //식수대 + dltnUswtr += element.dltnUswtr //희석용수 + ithlRswtr += element.ithlRswtr //중수도 + ithlEtcUswtr += element.ithlEtcUswtr //기타 + cleanToilUswtr += element.cleanToilUswtr //청소화장실용수 + resn += element.resn //세척살수용수 + }); + }) + const data: dnChartForm[] = [] + data?.push({ label: "세척수", val: wshwtr, fill: "yellow" }) + data?.push({ label: "냉각수", val: coolUswtr, fill: "green" }) + data?.push({ label: "청소수", val: cleanUswtr, fill: "yellow" }) + data?.push({ label: "식수대", val: drwtUswtr, fill: "green" }) + data?.push({ label: "희석용수", val: dltnUswtr, fill: "yellow" }) + data?.push({ label: "중수도", val: ithlRswtr, fill: "green" }) + data?.push({ label: "기타", val: ithlEtcUswtr, fill: "yellow" }) + data?.push({ label: "청소화장실용수", val: cleanToilUswtr, fill: "green" }) + data?.push({ label: "세척살수용수", val: resn, fill: "yellow" }) + setDnInUsage(data); + } + }, [rawData, year]) + + useEffect(() => { + const newChartConfig = dnInUsage.reduce((config, element) => { + config[element.label] = { + label: element.label, + fill: element.fill + }; + return config; + }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; + setDnInChartConfig(newChartConfig); + }, [dnInUsage]); + + // 장외용수 도넛차트 + useEffect(() => { + let lndwtr = 0; + let hydUswtr = 0; + let ugwtChr = 0; + let farmUswtr = 0; + let rvrKeepUswtr = 0; + let indstUswtr = 0; + if (rawData) { + rawData.forEach((item) => { + item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + lndwtr += element.lndwtr; //조경용수 + hydUswtr += element.hydUswtr; //친수용수 + ugwtChr += element.ugwtChr; //지하수충전 + farmUswtr += element.farmUswtr //농업용수 + rvrKeepUswtr += element.rvrKeepUswtr //하천등유지용수 + indstUswtr += element.indstUswtr //공업용수 + + }); + }) + const data: dnChartForm[] = [] + data?.push({ label: "조경용수", val: lndwtr, fill: "yellow" }) + data?.push({ label: "친수용수", val: hydUswtr, fill: "black" }) + data?.push({ label: "지하수충전", val: ugwtChr, fill: "yellow" }) + data?.push({ label: "농업용수", val: farmUswtr, fill: "black" }) + data?.push({ label: "하천등유지용수", val: rvrKeepUswtr, fill: "yellow" }) + data?.push({ label: "공업용수", val: indstUswtr, fill: "black" }) + setDnOutUsage(data); + } + }, [rawData, year]) + + useEffect(() => { + const newChartConfig = dnOutUsage.reduce((config, element) => { + config[element.label] = { + label: element.label, + fill: element.fill + }; + return config; + }, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig; + setDnOutChartConfig(newChartConfig); + }, [dnOutUsage]); + + //월별 장내외 용수 총 실적 막대차트 + useEffect(() => { + let wshwtr = 0; + let coolUswtr = 0; + let cleanUswtr = 0; + let drwtUswtr = 0; + let dltnUswtr = 0; + let ithlRswtr = 0; + let ithlEtcUswtr = 0; + let cleanToilUswtr = 0; + let resn = 0; + let lndwtr = 0; + let hydUswtr = 0; + let ugwtChr = 0; + let farmUswtr = 0; + let rvrKeepUswtr = 0; + let indstUswtr = 0; + let inUsageSum = 0; + let outUsageSum = 0; + let inUsageArr = []; + let outUsageArr = []; + + + if (rawData) { + const data: barChartForm[] = [] + rawData.forEach((item) => { + item.prfList + .filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + + wshwtr = element.wshwtr; //세척수 + coolUswtr = element.coolUswtr; //냉각수 + cleanUswtr = element.cleanUswtr; //청소수 + drwtUswtr = element.drwtUswtr; //식수대 + dltnUswtr = element.dltnUswtr //희석용수 + ithlRswtr = element.ithlRswtr //중수도 + ithlEtcUswtr = element.ithlEtcUswtr //기타 + cleanToilUswtr = element.cleanToilUswtr //청소화장실용수 + resn = element.resn //세척살수용수 + + lndwtr = element.lndwtr; //조경용수 + hydUswtr = element.hydUswtr; //친수용수 + ugwtChr = element.ugwtChr; //지하수충전 + farmUswtr = element.farmUswtr //농업용수 + rvrKeepUswtr = element.rvrKeepUswtr //하천등유지용수 + indstUswtr = element.indstUswtr //공업용수 + + inUsageSum = wshwtr + coolUswtr + cleanUswtr + drwtUswtr + dltnUswtr + ithlRswtr + ithlEtcUswtr + cleanToilUswtr + resn; + inUsageArr.push(inUsageSum) + outUsageSum = lndwtr + hydUswtr + ugwtChr + farmUswtr + rvrKeepUswtr + indstUswtr; + outUsageArr.push(outUsageSum) + + const month = Number(element.ym.slice(4, 6)) + data.push({ label: `${month}월`, "장내용수": inUsageSum, "장외용수": outUsageSum }) + + }, + ); + + }) + + setBarData(data); + } + }, [rawData, year]) + + useEffect(() => { + const newChartConfig = barData.reduce((config, element) => { + config[element.label] = { + label: element.label, + }; + return config; + + }, {} as Record<string, { label: string }>) satisfies ChartConfig; + setBarChartConfig(newChartConfig); + }, [barData]); + + //월별 장내 용수 실적 라인차트 + useEffect(() => { + + if (rawData) { + const wshwtrData: lineChartDataProps[] = [] + const coolUswtrData: lineChartDataProps[] = [] + const cleanUswtrData: lineChartDataProps[] = [] + const drwtUswtrData: lineChartDataProps[] = [] + const dltnUswtrData: lineChartDataProps[] = [] + const ithlRswtrData: lineChartDataProps[] = [] + const ithlEtcUswtrData: lineChartDataProps[] = [] + const cleanToilUswtrData: lineChartDataProps[] = [] + const resnData: lineChartDataProps[] = [] + rawData.forEach((item) => { + item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + const month = Number(element.ym.slice(4, 6)) + wshwtrData.push({ label: `${month}월`, val: element.wshwtr, color: "red" }) + coolUswtrData.push({ label: `${month}월`, val: element.coolUswtr, color: "red" }) + cleanUswtrData.push({ label: `${month}월`, val: element.cleanUswtr, color: "red" }) + drwtUswtrData.push({ label: `${month}월`, val: element.drwtUswtr, color: "red" }) + dltnUswtrData.push({ label: `${month}월`, val: element.dltnUswtr, color: "red" }) + ithlRswtrData.push({ label: `${month}월`, val: element.ithlRswtr, color: "red" }) + ithlEtcUswtrData.push({ label: `${month}월`, val: element.ithlEtcUswtr, color: "red" }) + cleanToilUswtrData.push({ label: `${month}월`, val: element.cleanToilUswtr, color: "red" }) + resnData.push({ label: `${month}월`, val: element.resn, color: "red" }) + + }, + ); + }) + const rtndata = { + chartData1: wshwtrData, + chartData2: coolUswtrData, + chartData3: cleanUswtrData, + chartData4: drwtUswtrData, + chartData5: dltnUswtrData, + chartData6: ithlRswtrData, + chartData7: ithlEtcUswtrData, + chartData8: cleanToilUswtrData, + chartData9: resnData + } + + setInLineData(rtndata) + } + }, [rawData, year]) + + useEffect(() => { + if (inLineData?.chartData1) { + const newChartConfig = { + chartData1: { + label: '세척수', + color: "red", + }, + chartData2: { + label: '냉각수', + color: "orange", + }, + chartData3: { + label: '청소수', + color: "yellow", + }, + chartData4: { + label: '식수대', + color: "pink", + }, + chartData5: { + label: '희석용수', + color: "purple", + }, + chartData6: { + label: '중수도', + color: "green", + }, + chartData7: { + label: '기타', + color: "blue", + }, + chartData8: { + label: '청소화장실용수', + color: "magenta", + }, + chartData9: { + label: '세척살수용수', + color: "skyblue", + }, + } satisfies ChartConfig; + setLnInChartConfig(newChartConfig); + } + }, [inLineData]); + + //월별 장외 용수 실적 라인차트 + useEffect(() => { + if (rawData) { + const lndwtrData: lineChartDataProps[] = [] + const hydUswtrData: lineChartDataProps[] = [] + const ugwtChrData: lineChartDataProps[] = [] + const farmUswtrData: lineChartDataProps[] = [] + const rvrKeepUswtrData: lineChartDataProps[] = [] + const indstUswtrData: lineChartDataProps[] = [] + rawData.forEach((item) => { + item.prfList.filter((e) => e.ym.slice(0, 4) === year).forEach((element) => { + const month = Number(element.ym.slice(4, 6)) + lndwtrData.push({ label: `${month}월`, val: element.lndwtr, color: "red" }) + hydUswtrData.push({ label: `${month}월`, val: element.hydUswtr, color: "red" }) + ugwtChrData.push({ label: `${month}월`, val: element.ugwtChr, color: "red" }) + farmUswtrData.push({ label: `${month}월`, val: element.farmUswtr, color: "red" }) + rvrKeepUswtrData.push({ label: `${month}월`, val: element.rvrKeepUswtr, color: "red" }) + indstUswtrData.push({ label: `${month}월`, val: element.indstUswtr, color: "red" }) + }, + ); + }) + setOutLineData({ + chartData1: lndwtrData, + chartData2: hydUswtrData, + chartData3: ugwtChrData, + chartData4: farmUswtrData, + chartData5: rvrKeepUswtrData, + chartData6: indstUswtrData, + }) + } + }, [rawData, year]) + + useEffect(() => { + if (outLineData?.chartData1) { + const newChartConfig = { + chartData1: { + label: '조경용수', + color: "red", + }, + chartData2: { + label: '친수용수', + color: "orange", + }, + chartData3: { + label: '지하수충전', + color: "yellow", + }, + chartData4: { + label: '농업용수', + color: "green", + }, + chartData5: { + label: '하천등유지용수', + color: "blue", + }, + chartData6: { + label: '공업용수', + color: "purple", + }, + } satisfies ChartConfig; + setLnOutChartConfig(newChartConfig); + } + }, [outLineData]); + + useEffect(() => { + if (!pbnstPrfmncSttn.isInit) { + pbnstPrfmncInit("1", "11"); + } + }, [pbnstPrfmncSttn.isInit, pbnstPrfmncInit]); + + useEffect(() => { + setbdstTable(prfmncRes); + }, [prfmncRes]); + + return ( + <> + <ContentsDiv> + <ListDiv> + <hr /> + <h3 className="mt-4 ml-2 text-2xl font-medium text-label">하수재이용실적 상세현황</h3> + + <SelectBox + className="w-1/12 text-2xl mt-4 ml-2" + label="연도" + selectClass={""} + selectArray={selectYearList} + value={year} + onSelectValue={(val: string | number) => setYear(String(val))} /> + <h1 className="mt-4 ml-2">장내·외 용도별 하수재이용실적 현황(계)</h1> + + <div className="flex"> + <div className="w-1/3"> + <MainChart chartConfig={chartConfig} chartData={chartData} chartTitle={"장내·외 용수 총 실적"} chartForm={CHART_FORM.DN} /> + </div> + <div className="w-1/3"> + <MainChart chartConfig={dnInChartConfig} chartData={dnInUsage} chartTitle={"장내용수 실적"} chartForm={CHART_FORM.DN} /> + </div> + <div className="w-1/3"> + <MainChart chartConfig={dnOutChartConfig} chartData={dnOutUsage} chartTitle={"장외용수 실적"} chartForm={CHART_FORM.DN} /> + </div> + </div> + <h1 className="">장내·외 월별 하수재이용실적 현황</h1> + <div className="flex"> + <div className="w-1/3"> + <MainChart chartConfig={barChartConfig} chartData={barData} chartTitle={"월별 장내·외 용수 총 실적"} chartForm={CHART_FORM.DOUBLEBAR} /> + </div> + <div className="w-1/3"> + <MainChart chartConfig={lnInChartConfig} chartData={inLineData} chartTitle={"월별 장내 용수 실적"} chartForm={CHART_FORM.LINE} /> + </div> + <div className="w-1/3"> + <MainChart chartConfig={lnOutChartConfig} chartData={outLineData} chartTitle={"월별 장외 용수 총 실적"} chartForm={CHART_FORM.LINE} /> + </div> + </div> + {/* 하수재이용실적 현황 - 조건부 렌더링 */} + {isVisible && ( + <> + <h1 className="">하수재이용실적 현황</h1> + <ListDiv className="col-span-8 pr-6 "> + <ScrollArea className="h-[60vh]"> + <div className="card-header"> + <h3 className="text-2xl font-medium text-label"> + {"하수처리수재이용 실적(m3) 등록결과"} + </h3> + </div> + + <UiTable + headName={undefined} + tableData={prfData || []} + headlist={monthHeadList} + pageSize={0} + total={0} + cellClick={undefined} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }}> + </UiTable> + </ScrollArea> + </ListDiv> + </> + )} + </ListDiv> + </ContentsDiv> + <Toaster position="top-center" /> + </> + ) +} diff --git a/apps/wr/app/map/rainPrfmncSttnPopup.tsx b/apps/wr/app/map/rainPrfmncSttnPopup.tsx new file mode 100644 index 000000000..18cdac8e6 --- /dev/null +++ b/apps/wr/app/map/rainPrfmncSttnPopup.tsx @@ -0,0 +1,421 @@ +"use client"; + +import { useWrStore } from "@/app/store"; +import { + rainZod, + selectClass, + TEST_UUID, + type customRainBdst, +} from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { DatePicker, FormUi } from "@common/business_components"; +import { Form, FormField, SelectBox } from "@common/business_components/ui"; +import { Button, Input } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm, useWatch, type UseFormReturn } from "react-hook-form"; +import { z } from "zod"; +import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg"; +import { + formatCommaNumber, + handleCommaNumberChange, +} from "@/components/lib/func/commom"; +import { formH1Class, formH2Class } from "@/components/lib/class/comClass"; +import { Label } from "@common/business_components/ui/form/lib/label"; + +export default function RainPrfmncSttnPopup({ + rainForm, + isModal, +}: { + rainForm: customRainBdst; + isModal: boolean; +}) { + const { code, rainBdstReg, rainBdstUpdate, rain } = useWrStore( + (state) => state + ); + const { rainFcltCd } = code; + const defaultValues: z.infer<typeof rainZod> = rainZod.parse({}); + + const form = useForm<z.infer<typeof rainZod>>({ + resolver: zodResolver(rainZod), + defaultValues: defaultValues, + }); + + const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn] = form.watch([ + "reuseBuilding.0.grntchBdstCertYn", + "reuseBuilding.0.instlCstSprtYn", + "reuseBuilding.0.rainyObj.filtrTrtmfctYn", + ]); + + const cleanRainData = (raw: any): z.infer<typeof rainZod> => { + return { + ...raw, + frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null), + archChgPrmsnDt: raw?.archChgPrmsnDt + ? new Date(raw?.archChgPrmsnDt) + : null, + reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({ + ...item, + instlCmptnDt: new Date(item?.instlCmptnDt), + operMngCd: item?.operMngCd ?? "", // null → "" + grntchBdstCertYr: + typeof item?.grntchBdstCertYr === "string" + ? item?.grntchBdstCertYr + : "", // null → "" + rainObj: { + ...item?.rainObj, + }, + })), + }; + }; + + useEffect(() => { + if (grntchBdstCertYn === "N") { + form.setValue("reuseBuilding.0.grntchBdstCertYr", ""); + form.setValue("reuseBuilding.0.grntchBdstCertNo", ""); + form.setValue("reuseBuilding.0.grntchBdstCertGrd", ""); + } else if (instlCstSprtYn === "N") { + form.setValue("reuseBuilding.0.instlCstSprtAmt", 0); + } else if (filtrTrtmfctYn === "N") { + form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctCpct", 0); + form.setValue("reuseBuilding.0.rainyObj.filtrTrtmfctPrs", ""); + } + }, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctYn]); + + // form 값이 변경될 때마다 로그 출력 + useEffect(() => { + const subscription = form.watch((value) => { + }); + + + return () => subscription.unsubscribe(); + }, [form]); + + useEffect(() => { + console.log("form errors:", form.formState.errors); + }, [form.formState.errors]); + + const onSubmit = (values: z.infer<typeof rainZod>) => { + if (isModal) { + rainBdstReg(values, "41"); + } else { + rainBdstUpdate(values, "41"); + } + }; + + useEffect(() => { + if (rainZod) form.reset(cleanRainData(rainForm)); + }, [rainForm]); + + + return ( + <div className="!mx-[1.6rem] !p-0r"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div> + <div className="grid grid-cols-12 flex items-center card-header"> + <h1 className="text-2xl font-medium text-label col-span-10"> + {isModal ? "건축물 운영정보" : "빗물이용시설 정보"} + </h1> + </div> + </div> + {isModal && rainForm != undefined ? <BdstReg form={form} /> : <></>} + {isModal ? ( + <h1 className="pl-3 text-xl col-span-10 mt-4"> + 빗물이용시설 정보{" "} + </h1> + ) : ( + <></> + )} + <h1 className={formH1Class}> + 기본정보 + </h1> + + <div className="grid grid-cols-4 gap-2 mt-4"> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCmptnDt" + render={({ field }) => ( + <FormUi label="설치완료일"> + <DatePicker + mode="single" + field={{ + value: + field.value && !isNaN(new Date(field.value).getTime()) + ? new Date(field.value) + : new Date(), // Provide a default Date value + onChange: (date) => field.onChange(date), + }} + selectClass={"w-full"} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCst" + render={({ field }) => ( + <FormUi label="설치비"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + </div> + <div className=""> + <h1 className={formH1Class}> + 빗물이용시설 정보 + </h1> + </div> + <div> + <h2 className={formH2Class}>집수면</h2> + </div> + <div className="grid grid-cols-10 gap-4 mt-4 "> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.pntfColKnd" + render={({ field }) => ( + <FormUi label="종류"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={rainFcltCd} + onSelectValue={field.onChange} + value={field.value ?? rainFcltCd[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.pntfColArea" + render={({ field }) => ( + <FormUi label="면적(m³)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + /> + </FormUi> + )} + /> + </div> + </div> + + <div> + <h2 className={formH2Class}>여과 등 처리시설</h2> + </div> + <div className="grid grid-cols-12 gap-2 mt-4"> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctYn" + render={({ field }) => ( + <FormUi label="유무"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctCpct" + render={({ field }) => ( + <FormUi label="용량(m³/일)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={filtrTrtmfctYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rainyObj.filtrTrtmfctPrs" + render={({ field }) => ( + <FormUi label="처리공정"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={filtrTrtmfctYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + </div> + <div className=" my-2"> + <h1 className={formH1Class}> + 인센티브 정보 + </h1> + </div> + <div> + <h2 className={formH2Class}>설치비 지원</h2> + </div> + <div className="grid grid-cols-10 gap-2 mt-4"> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtYn" + render={({ field }) => ( + <FormUi label="지원 여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-5"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtAmt" + render={({ field }) => ( + <FormUi label="지원금액(원)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={instlCstSprtYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + </div> + + <div> + <h2 className={formH2Class}>녹색건축물 인증</h2> + </div> + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYn" + render={({ field }) => ( + <FormUi label="인증여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYr" + render={({ field }) => ( + <FormUi label="인증년도"> + <Input + className={'w-full !text-xl'} + placeholder="2024" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={grntchBdstCertYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3 "> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertNo" + render={({ field }) => ( + <FormUi label="인증번호"> + <Input + className={'w-full !text-xl'} + placeholder="ab1234678" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={grntchBdstCertYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertGrd" + render={({ field }) => ( + <FormUi label="인증등급"> + <Input + className={'w-full !text-xl'} + placeholder="A" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={grntchBdstCertYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + </div> + </form> + </Form> + </div> + ); +} diff --git a/apps/wr/app/map/rswtrPrfmncSttnPopup.tsx b/apps/wr/app/map/rswtrPrfmncSttnPopup.tsx new file mode 100644 index 000000000..e7f94a3b5 --- /dev/null +++ b/apps/wr/app/map/rswtrPrfmncSttnPopup.tsx @@ -0,0 +1,404 @@ +"use client"; + +import { + DatePicker, + Form, + FormField, + FormUi, + SelectBox, +} from "@common/business_components/ui"; +import { Button, Input } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { useWrStore } from "@/app/store"; +import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; +import { + rswtZod, + type customRswtBdst, +} from "@/app/store/ruseFcltMng/fcltReg/rswtReg"; +import { useEffect } from "react"; +import BdstReg from "@/components/ui/form/fcltReg/bdstReg/bdstReg"; +import { + formatCommaNumber, + handleCommaNumberChange, +} from "@/components/lib/func/commom"; +import { formH1Class, formH2Class } from "@/components/lib/class/comClass"; +import { Label } from "@common/business_components/ui/form/lib/label"; + +export default function RswtrPrfmncsSttnPopup({ + rswtForm, + isModal, +}: { + rswtForm: customRswtBdst; + isModal: boolean; +}) { + const { rswt, code, rswtBdstUpdate, rswtBdstReg } = useWrStore( + (state) => state + ); + + const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({}); + + const form = useForm<z.infer<typeof rswtZod>>({ + resolver: zodResolver(rswtZod), + defaultValues: defaultValues, + }); + + const [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs] = form.watch([ + "reuseBuilding.0.grntchBdstCertYn", + "reuseBuilding.0.instlCstSprtYn", + "reuseBuilding.0.rswtrObj.filtrTrtmfctProcs", + ]); + + const onSubmit = (values: z.infer<typeof rswtZod>) => { + if (isModal) { + rswtBdstReg(values, "41"); + } else { + rswtBdstUpdate(values, "41"); + } + }; + const cleanRswtData = (raw: any): z.infer<typeof rswtZod> => { + return { + ...raw, + frstArchPrmsnDt: new Date(raw?.frstArchPrmsnDt || null), + archChgPrmsnDt: raw?.archChgPrmsnDt + ? new Date(raw?.archChgPrmsnDt) + : null, + reuseBuilding: (raw?.reuseBuilding || []).map((item: any) => ({ + ...item, + instlCmptnDt: new Date(item?.instlCmptnDt), + operMngCd: item?.operMngCd ?? "", // null → "" + grntchBdstCertYr: + typeof item?.grntchBdstCertYr === "string" + ? item?.grntchBdstCertYr + : "", // null → "" + rswtrObj: { + ...item?.rswtrObj, + }, + })), + }; + }; + + useEffect(() => { + console.log("form errors:", form.formState.errors); + }, [form.formState.errors]); + + useEffect(() => { + console.log("filtrTrtmfctProcs ", filtrTrtmfctProcs); + if (grntchBdstCertYn === "N") { + form.setValue("reuseBuilding.0.grntchBdstCertYr", ""); + form.setValue("reuseBuilding.0.grntchBdstCertNo", ""); + form.setValue("reuseBuilding.0.grntchBdstCertGrd", ""); + } else if (instlCstSprtYn === "N") { + form.setValue("reuseBuilding.0.instlCstSprtAmt", 0); + } + }, [grntchBdstCertYn, instlCstSprtYn, filtrTrtmfctProcs]); + + useEffect(() => { + if (rswtZod) form.reset(cleanRswtData(rswtForm)); + }, [rswtForm]); + + return ( + <div className="!mx-[1.6rem] !p-0r"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + {/* <div className="flex justify-between items-center mx-3"> */} + <div> + <div className="grid grid-cols-12 flex items-center card-header"> + <h1 className="text-2xl font-medium text-label col-span-10"> + 건축물 운영정보 + </h1> + </div> + </div> + + {isModal && rswtForm != undefined ? <BdstReg form={form} /> : <></>} + {isModal ? ( + <h1 className="pl-3 text-xl col-span-10 mt-4 "> + 중수도 시설 정보{" "} + </h1> + ) : ( + <></> + )} + <h1 className={formH1Class}>기본정보</h1> + <div className="grid grid-cols-4 gap-2 mt-4"> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCmptnDt" + render={({ field }) => ( + <FormUi label="설치완료일"> + <DatePicker + mode="single" + field={{ + value: + field.value && !isNaN(new Date(field.value).getTime()) + ? new Date(field.value) + : new Date(), // Provide a default Date value + onChange: (date) => field.onChange(date), + }} + selectClass={"w-full"} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCst" + render={({ field }) => ( + <FormUi label="설치비"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + </div> + <div className="grid grid-cols-4 gap-2 mt-4"> + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.wtrUsqty" + render={({ field }) => ( + <FormUi label="물사용량(m³)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + + <div className="col-span-2"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.oblgTrgtWtrUsqty" + render={({ field }) => ( + <FormUi label="의무대상 물사용량(m³)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + </div> + <div className=" my-4"> + <h1 className={formH1Class}>중수도 시설 정보</h1> + </div> + + <div className="grid grid-cols-12 gap-1 mt-4"> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.filtrTrtmfctCpct" + render={({ field }) => ( + <FormUi label="처리시설 용량"> + <Input + className={'w-full !text-xl'} + placeholder="0" + type="number" + value={field.value ?? ""} + onChange={(e)=>{field.onChange(Number(e.target.value))} } + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.orgwtKndCd" + render={({ field }) => ( + <FormUi label="원수 종류"> + <SelectBox + className={""} + label={""} + selectClass={selectClass} + selectArray={code.orgwtKndCd} + onSelectValue={field.onChange} + value={field.value ?? code.orgwtKndCd[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-4"> + <FormField + control={form.control} + name="reuseBuilding.0.rswtrObj.filtrTrtmfctProcs" + render={({ field }) => ( + <FormUi label="주요처리공정"> + <Input + className={'w-full !text-xl'} + placeholder="주요처리공정" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + </div> + + <div className=" my-2"> + <h1 className={formH1Class}>인센티브 정보</h1> + </div> + <div> + <h2 className={formH2Class}>설치비 지원</h2> + </div> + <div className="grid grid-cols-2 gap-2 mt-4"> + <div className="col-span-1"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtYn" + render={({ field }) => ( + <FormUi label="설치비 지원여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-1"> + <FormField + control={form.control} + name="reuseBuilding.0.instlCstSprtAmt" + render={({ field }) => ( + <FormUi label="지원금액(원)"> + <Input + className={'w-full !text-xl'} + placeholder="1,000,000" + type="text" + onChange={(e) => { + handleCommaNumberChange(e, field.onChange); + }} + value={formatCommaNumber(field.value)} + readOnly={instlCstSprtYn !== "Y"} + /> + </FormUi> + )} + /> + </div> + </div> + + <div> + <h2 className={formH2Class}>녹색건축물 인증</h2> + </div> + <div className="grid grid-cols-6 gap-2 mt-4"> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYn" + render={({ field }) => ( + <FormUi label="인증여부"> + <SelectBox + className={""} + selectClass={selectClass} + label={""} + selectArray={code.yn} + onSelectValue={field.onChange} + value={field.value ?? code.yn[0]?.val} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertYr" + render={({ field }) => ( + <FormUi label="인증년도"> + <Input + className={'w-full !text-xl'} + placeholder="2024" + type="text" + {...field} + value={field.value ?? ""} + onChange={field.onChange} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertNo" + render={({ field }) => ( + <FormUi label="인증번호"> + <Input + className={'w-full !text-xl'} + placeholder="ab1234678" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + <div className="col-span-3"> + <FormField + control={form.control} + name="reuseBuilding.0.grntchBdstCertGrd" + render={({ field }) => ( + <FormUi label="인증등급"> + <Input + className={'w-full !text-xl'} + placeholder="A" + type="" + value={field.value ?? ""} + onChange={field.onChange} + readOnly={true} + /> + </FormUi> + )} + /> + </div> + </div> + </form> + </Form> + </div> + ); +} diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index 960971d6b..70261c4c6 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -160,7 +160,7 @@ export default function Home() { <div className="grid grid-cols-12 gap-4"> <div className="col-span-2"> <Button className="w-full"> - <Link href="map/htdflSttn">온배수시설 현황(지도)</Link> + <Link href="map/htdflSttn">시설 현황(지도)</Link> </Button> </div> </div> diff --git a/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx b/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx index 88b3c8649..0af6072de 100644 --- a/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx +++ b/apps/wr/app/ruseFcltMng/fcltSttn/rainSttn/page.tsx @@ -37,10 +37,7 @@ export default function RainSttn() { const [pageNation, setPageNation] = useState(1); const [isChHead, setIsChHead] = useState(false); const [bdstTable, setBdstTable] = useState<customRainBdst[]>(); - const [detailModal, setDetailModal] = useState<{ - isOpen: boolean; - data: customRainBdst | undefined; - }>({ data: undefined, isOpen: false }); + const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRainBdst | undefined }>({ data: undefined, isOpen: false }) const [bdstChgProps, setBdstChgProps] = useState<{ isOpen: boolean; chBuilding: chgBuilding[]; @@ -49,25 +46,12 @@ export default function RainSttn() { const codeData = getCityOrSgg(ref); const cellClick = (index: number | undefined, headId: string) => { - if ( - headId !== "chBuildingYn" && - index !== undefined && - bdstTable !== undefined - ) { - setDetailModal({ isOpen: true, data: bdstTable[index] }); - } else if ( - headId === "chBuildingYn" && - index !== undefined && - bdstTable !== undefined - ) { - if ( - bdstTable[index] && - bdstTable[index]?.chgBuilding[0]?.bdstChgNm !== null - ) { - setBdstChgProps({ - isOpen: true, - chBuilding: bdstTable[index].chgBuilding, - }); + + if (headId !== 'chBuildingYn' && index !== undefined && bdstTable !== undefined) { + setDetailModal({ isOpen: true, data: bdstTable[index] }) + } else if (headId === 'chBuildingYn' && index !== undefined && bdstTable !== undefined) { + if (bdstTable[index] && bdstTable[index]?.chgBuilding[0]?.bdstChgNm !== null) { + setBdstChgProps({ isOpen: true, chBuilding: bdstTable[index].chgBuilding }); } } }; @@ -168,9 +152,7 @@ export default function RainSttn() { headlist={rainSttn.rainSttHeadList} pageSize={20} total={100000} - cellClick={(index: number | undefined, headId: string) => { - cellClick(index ?? undefined, headId); - }} + cellClick={(index: number | undefined, headId: string) => { cellClick(index ?? undefined, headId) }} onPageChange={setPageNation} ></UiTable> </ListDiv> diff --git a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx index 438ab439c..0d3c2ca63 100644 --- a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx +++ b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx @@ -28,7 +28,7 @@ import { useEffect, useState } from "react"; import { set, useForm } from "react-hook-form"; import { z } from "zod"; -export function PbnstSttnModal({ data }: { data: customPbnst }) { +export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) { const { code, pbnstSttn, prfmnc } = useWrStore((state) => state); const [fcltTab, setFcltTab] = useState<string>("fclt"); const [ruseTab, setRuseTab] = useState<string>("inoutTotal"); @@ -233,20 +233,24 @@ export function PbnstSttnModal({ data }: { data: customPbnst }) { } }, [pbnstSttn.pbnstSttnRes, data]); - useEffect(() => {}, [ruseBizData?.head]); + useEffect(() => { }, [ruseBizData?.head]); - useEffect(()=>{ + useEffect(() => { - if(data){ - form.reset({items:[data]}) + if (data) { + form.reset({ items: [data] }) } - },[data, form]) + }, [data, form]) return ( <> <div> - <Title title={"하수재이용 시설 상세정보"} /> + {titleVisible && ( + <> + <Title title={"하수재이용 시설 상세정보"} /> + </> + )} <ScrollArea className={"h-[70vh]"}> <PbnstMngForm form={form} diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index bb169c3ff..c09247f2f 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,5 +1,6 @@ import { Search } from 'lucide-react'; import OlMap from 'ol/Map'; +import Overlay from 'ol/Overlay'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; import { defaults } from 'ol/interaction'; @@ -7,31 +8,35 @@ import * as olProj from 'ol/proj'; import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; import GisLonLat from './GisLonLat'; import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; import GisTree from './GisTree'; +import FacilityInfoPopup from './facilityInfoPopup'; import './lib/css/gis.common.css'; import GisApp from './lib/gis.core'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ - isOpen , - gisMaps, - isSearchModalOpen, - treeData = [], - isSearch = false, - isMiniMap = false, - setIsOpen, - setFeatureData, - featureData, - searchView : SearchView, - detailView: DetailView, - onFeatureSelect = null, - setIsSearchModalOpen }) { +function Gis({ + isOpen, + gisMaps, + isSearchModalOpen, + treeData = [], + isSearch = false, + isMiniMap = false, + setIsOpen, + setFeatureData, + featureData, + searchView: SearchView, + detailView: DetailView, + onFeatureSelect, + setIsSearchModalOpen, + onCheckedLayersChange +}) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); @@ -41,13 +46,21 @@ function Gis({ const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); const mapInstanceRef = useRef(null); const checkedLayerIds = useRef([]); + + // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 + const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); + const facilityOverlaysRef = useRef([]); + // 체크된 레이어 상태 관리 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); + let gisMap = null; const closePopup = () => { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; - // SearchModal, GisContent 등에서 호출 + // Feature Click Event const handleFeatureSelect = useCallback((data) => { gisMap = mapInstanceRef.current; @@ -148,7 +161,7 @@ function Gis({ gisMaps.current = map // 미니맵 추가 - if(isMiniMap){ + if (isMiniMap) { const overviewMap = new OverviewMap({ layers: [GisApp.Module.core.baseMiniMapLayers['base']], view: new View({ @@ -213,15 +226,140 @@ function Gis({ } } + // 데이터 로드 및 팝업 생성 로직 + useEffect(() => { + if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 + + // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) + const fetchPopupData = async () => { + // 실제 API 호출 로직으로 대체해야 합니다. + // 예시: const response = await yourApiService.getFcltyWtCnt(); + // setFacilityInfoPopupData(response.data); + + // 임시 목업 데이터 + const mockData = [ + { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, + { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } + ]; + setFacilityInfoPopupData(mockData); + }; + + fetchPopupData(); + + }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 + + // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 + useEffect(() => { + const map = mapInstanceRef.current; + if (!map || facilityInfoPopupData.length === 0) return; + + // 기존 오버레이 정리 + facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); + facilityOverlaysRef.current = []; + + facilityInfoPopupData.forEach(data => { + const popupId = `${data.ftrIdn}_${data.ftrCde}`; + + // 팝업을 담을 div 엘리먼트 생성 + const popupElement = document.createElement('div'); + popupElement.id = popupId; + + // OpenLayers Overlay 생성 + const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); + const overlay = new Overlay({ + element: popupElement, + position: position, + offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 + positioning: 'bottom-center', + className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 + }); + + map.addOverlay(overlay); + facilityOverlaysRef.current.push(overlay); + + // React 컴포넌트를 popupElement에 렌더링 + const root = createRoot(popupElement); + root.render(<FacilityInfoPopup data={data} />); + }); + + // 초기 가시성 업데이트 + updatePopupVisibility(); + + }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 + + // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 + const updatePopupVisibility = useCallback(() => { + const map = mapInstanceRef.current; + if (!map) return; + + const visibleLayers = new Set(); + map.getLayers().forEach(layer => { + if (layer.getVisible()) { + // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 + const typeName = layer.get('typeName'); + if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 + visibleLayers.add(layer.get('ftrCde')); + } + } + }); + + facilityOverlaysRef.current.forEach(overlay => { + const element = overlay.getElement(); + const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 + + if (visibleLayers.has(ftrCde)) { + element.style.display = ''; + } else { + element.style.display = 'none'; + } + }); + }, []); + + // GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다. + // 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출 + useEffect(() => { + // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 + const map = mapInstanceRef.current; + if (!map) return; + + const handleLayerVisibilityChange = () => { + updatePopupVisibility(); + }; + + map.getLayers().on('change:length', handleLayerVisibilityChange); + map.getLayers().forEach(layer => { + layer.on('change:visible', handleLayerVisibilityChange); + }); + + return () => { + // 클린업 + map.getLayers().un('change:length', handleLayerVisibilityChange); + map.getLayers().forEach(layer => { + layer.un('change:visible', handleLayerVisibilityChange); + }); + }; + }, [updatePopupVisibility]); + + // 체크된 레이어 정보를 처리하는 함수 + const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); + }, [onCheckedLayersChange]); + return ( <> <GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}> <div className="h-[calc(100vh-10rem)] flex relative"> <div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} /> - <GisTree visible={tabVisible} setVisible={setTabVisible} /> - + <GisTree + visible={tabVisible} + setVisible={setTabVisible} + onCheckedChange={handleCheckedLayersChange} + /> + {/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */} - + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} {isOpen && featureData && !isSearchModalOpen && ( <DetailView @@ -234,9 +372,9 @@ function Gis({ onOpenChange={setIsSearchModalOpen} onFeatureSelect={handleFeatureSelect} /> - <GisLonLat/> - <GisToolBar/> - <GisMapToggle/> + <GisLonLat /> + <GisToolBar /> + <GisMapToggle /> <GisSelect onFeatureSelect={handleFeatureSelect} /> </div> diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index dec618926..e36569143 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -9,7 +9,7 @@ import { DndProvider } from "react-dnd"; import { useGisContext } from './Gis'; import GisApp from './lib/gis.core'; -export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) { +export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); @@ -102,6 +102,11 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); gisCore.setLayerOnMap(newChecked, true); gisCore.setLayerOnMap(unchecked, false); + + // 체크된 노드 정보와 함께 노드 데이터를 상위로 전달 + const checkedNodesData = nodes.filter(node => newChecked.includes(node.id)); + onCheckedChange?.(newChecked, checkedNodesData); + return newChecked; }); }; From 5579ab677d6a352c65ab5118882d4fa3aed09fed Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 1 Jul 2025 10:26:40 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20=EC=9E=AC=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=EC=8B=9C=EC=84=A4=ED=98=84=ED=99=A9=20=EC=83=81=EB=8B=A8=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EB=B0=94=20=EC=8B=9C=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=EC=A1=B0=ED=9A=8C=20=EB=B2=84=ED=8A=BC,?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B2=84=ED=8A=BC=20=EB=AF=B8?= =?UTF-8?q?=EA=B5=AC=ED=98=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/page.tsx | 1 + apps/wr/app/map/htdflSttn/page.tsx | 31 ++-- apps/wr/app/map/mapSearchBar.tsx | 157 +++++++----------- apps/wr/app/page.tsx | 4 +- .../business_components/src/ui/map/GisTree.js | 6 +- 5 files changed, 88 insertions(+), 111 deletions(-) diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx index e1a90d5f1..d8bab06bb 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/map/page.tsx @@ -106,6 +106,7 @@ const Map = () => { setIsSearchModalOpen={setIsSearchModalOpen} isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} + onCheckedLayersChange={undefined} /> </div> ); diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx index 5ba199c8c..937490fe2 100644 --- a/apps/wr/app/map/htdflSttn/page.tsx +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -108,15 +108,15 @@ const Map = () => { }; // MapSearchBar 래퍼 컴포넌트 - const CustomMapSearchBar = (props: any) => { - return ( - <MapSearchBar - {...props} - searchType="공급처 후보지 조회" - showApplyButton={false} - /> - ); - }; + // const CustomMapSearchBar = (props: any) => { + // return ( + // <MapSearchBar + // {...props} + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // /> + // ); + // }; return ( <div className="bg-container"> @@ -125,10 +125,21 @@ const Map = () => { isSearch={false} gisMaps={gisMaps} isMiniMap={true} - searchView={CustomMapSearchBar} + searchView={(props: any) => ( + <MapSearchBar + searchType="공급처 후보지 조회" + showApplyButton={false} + checkedLayers={checkedLayers} + checkedLayersData={checkedLayersData} + {...props} + /> + ) + } detailView={(props: any) => ( <NewWindowPopup onClose={handlePopupClose} // 메모이제이션된 함수 사용 + checkedLayers={checkedLayers} + checkedLayersData={checkedLayersData} {...props} /> )} diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx index f5d693ee3..eabc6b892 100644 --- a/apps/wr/app/map/mapSearchBar.tsx +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -1,16 +1,12 @@ "use client" -import { Button, Input, Label } from "@common/components/ui"; -import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"; -import { useForm } from "react-hook-form"; -import { MapSearchFormLeft } from "./mapSearchForm"; import { useWrStore } from "@/app/store"; -import { btnClass } from "@/app/store/common/code"; -import { SearchBaseForm } from "@common/business_components"; -import { FormUi, SelectBox, SearchFormLeft, SearchFormRight, Form, FormField } from "@common/business_components/ui"; -import type { ReturnType, SelectDataType, EnvPartSelectDataType } from "@common/business_components/ui"; import SearchInputText from "@/components/ui/searchInput/searchInput"; -import { zodResolver } from "@hookform/resolvers/zod"; +import type { EnvPartSelectDataType, ReturnType, SelectDataType } from "@common/business_components/ui"; +import { SelectBox } from "@common/business_components/ui"; +import { Button, Input, Label } from "@common/components/ui"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { MapSearchFormLeft } from "./mapSearchForm"; // 건물 검색 타입 정의 @@ -49,6 +45,8 @@ type SearchBaseType = { showQueryButton?: boolean; showInitButton?: boolean; showApplyButton?: boolean; + checkedLayers?: string[]; + checkedLayersData?: any[]; } const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; @@ -82,17 +80,29 @@ export function MapSearchBar({ rightClassName, showQueryButton = true, showInitButton = true, - showApplyButton = true + showApplyButton = true, + checkedLayers = [], + checkedLayersData = [], }: SearchBaseType) { // 로딩바 const [isLoading, setIsLoading] = useState(false); // 상세 정보 조회를 위한 상태 추가 const [popupFeature, setPopupFeature] = useState<any>(null); - const { code } = useWrStore((state) => state); const [isChHead, setIsChHead] = useState(false); const ref = useRef<ReturnType>({} as ReturnType); + // zustand store에서 건물 검색 함수와 데이터 가져오기 + const { + code, + bdst, + findBdst, + wrMap, + // buildingSearchRes, + buildingNmSearch, + buildingNmSearchMultiple + } = useWrStore((state) => state); + const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>(null); const [buildingSearchText, setBuildingSearchText] = useState(""); const [buildingList, setBuildingList] = useState<BuildingInfo[]>([]); @@ -141,77 +151,16 @@ export function MapSearchBar({ ]; }, [selectedSigun]); - // 건물 검색 API 호출 함수 - const searchBuildings = async (searchText: string) => { - if (!searchText.trim()) { - setBuildingList([]); - return; - } - - try { - const flg = "41"; // 적절한 플래그 값으로 설정 - const sigunCode = selectedSigun.val !== '0' ? selectedSigun.val : ''; - const apiEndpoint = `${API_URL}/v1/reuseFc/building/${flg}/${sigunCode}/${encodeURIComponent(searchText)}`; - - const response = await fetch(apiEndpoint, { - method: "GET", - headers: { - accept: "application/json", - "Content-Type": "application/json" - } - }); - - if (!response.ok) { - throw new Error(`API 호출 실패: ${response.status}`); - } - - const result = await response.json(); - - if (result.status === 200 && Array.isArray(result.data)) { - // 건물 검색 결과를 demandCompList 형태로 변환 - const convertedList = result.data.map((building: BuildingInfo) => ({ - dmdInfoPk: String(building.bdstInfoPk), - dmdCoNm: building.bdstNm, - daddr: building.daddr, - useUsgCd: "", - splyCpct: 0, - splyFmYn: "Y", - // 추가 필드들... - })); - setBuildingList(result.data); - // 글로벌 상태에도 업데이트 (SearchInputText가 사용할 수 있도록) - // getDemandCompList를 직접 호출하는 대신 상태를 직접 설정 - const { setBuildingSearchResults } = useWrStore.getState(); - if (setBuildingSearchResults) { - setBuildingSearchResults(convertedList); - } - } else { - setBuildingList([]); - } - } catch (error) { - console.error("건물 검색 중 오류 발생:", error); - setBuildingList([]); - } - }; - // 조회 함수 const queryVal = () => { - console.log("조회버튼 클릭"); - if (searchType == '공급처 후보지 조회') { - // queryClick({ - // partValue: partValue, - // sidoValue: sidoValue, - // sigunValue: sigunValue, - // regionValue: regionValue, - // unitWaterShedValue: untitWaterShedValue, - // middleRegionValue: middleRegionValue, - // envPartValue: envPartValue, - // sidoText: selectedSido.text, - // sigunText: selectedSigun.text, - // }); - } else if (searchType == '우리동네가뭄지도보기') { - } else if (searchType == '지도보기') { - + // 46 : 하수처리시설 + // 47 : 하수재이용시설 + // 48 : 빗물이용시설 + // 49 : 중수도시설 + // 50 : 온배수시설 + console.log("조회버튼 클릭: ", checkedLayers); + if (checkedLayers && checkedLayers.includes('48')) { + // searchBuildings } } @@ -308,6 +257,27 @@ export function MapSearchBar({ } }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); + // buildingSearchText가 변경될 때 API 호출 + useEffect(() => { + if (buildingSearchText && buildingSearchText.length > 1) { + if (checkedLayers && checkedLayers.length > 0) { + // 다중 레이어 검색 + // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", buildingSearchText); + buildingNmSearch("RNB", selectedSigun.val, buildingSearchText); + } else { + // 단일 레이어 검색 + // buildingNmSearch(checkedLayers[0], selectedSigun.val, buildingSearchText); + } + } else { + // 검색 텍스트가 짧으면 리스트 초기화 + if (checkedLayers && checkedLayers.length > 0) { + // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", ""); + } else { + buildingNmSearch("RNB", selectedSigun.val || "", ""); + } + } + }, [buildingSearchText]); + return ( <div className="absolute left-1/2 -translate-x-1/2 bg-white flex items-center justify-center p-4 rounded-md shadow-lg w-auto min-w-fit"> <div className={leftClassName ? leftClassName : `flex items-center justify-center space-x-2 `}> @@ -338,32 +308,23 @@ export function MapSearchBar({ value={selectedSigun.val} selectClass={'w-32'} /> - {/* <Label className="w-auto text-xl text-gray-600 whitespace-nowrap">시설명</Label> - <div className="w-44"> - <Input className="mr-4" /> - </div> */} <div className="flex items-center"> <Label className="w-20 text-xl text-gray-600">시설명</Label> <div className="w-64"> <SearchInputText label="시설명" initValue={buildingSearchText} - setValue={(searchText) => { - setBuildingSearchText(searchText || ""); - if (searchText) { - searchBuildings(searchText); - } + setValue={(searchNm?: string) => { + setBuildingSearchText(searchNm || ""); }} - setRtnData={(buildingData) => { - if (buildingData && buildingData.length > 0 && buildingData[0]) { - // 선택된 건물 정보를 찾아서 설정 - const selectedBuildingInfo = buildingList.find(building => - building.bdstNm === buildingData[0].dmdCoNm - ); - setSelectedBuilding(selectedBuildingInfo || null); - console.log("선택된 시설:", selectedBuildingInfo); - } + setRtnData={function (rtnData: any): void { + // 선택된 건물 데이터 처리 + // if (rtnData && rtnData.bdstInfoPk) { + // setSelectedBuilding(rtnData); + // setBuildingSearchText(rtnData.bdstNm || ""); + // } }} + searchList={wrMap.buildingSearchRes || []} /> </div> </div> diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index 70261c4c6..c2be341cf 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -159,8 +159,8 @@ export default function Home() { <h2 className="text-xl font-semibold mb-4">시설 현황(지도)</h2> <div className="grid grid-cols-12 gap-4"> <div className="col-span-2"> - <Button className="w-full"> - <Link href="map/htdflSttn">시설 현황(지도)</Link> + <Button className="w-full" onClick={() => {router.push('/map/htdflSttn')}}> + 시설 현황(지도) </Button> </div> </div> diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index e36569143..3c2cb2819 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -41,8 +41,12 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes // 기본 체크된 레이어들을 지도에 표시 gisCore.setLayerOnMap(defaultChecked, true); + + // 초기 체크된 레이어 정보를 상위로 전달 + const defaultCheckedNodesData = nodes.filter(node => defaultChecked.includes(node.id)); + onCheckedChange?.(defaultChecked, defaultCheckedNodesData); } - }, [nodes]); + }, [nodes]); // onCheckedChange를 의존성에 추가 // 초기 표시 여부가 'Y'인 레이어들 찾기 const getDefaultCheckedNodes = (nodes, values = []) => { From ecc11720acbf2795439c46d69dad7af262213c61 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 1 Jul 2025 10:30:38 +0900 Subject: [PATCH 09/22] =?UTF-8?q?fix:=20Gis.js=20import=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/map/Gis.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 39cfdbe44..e85b8d162 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -14,7 +14,6 @@ import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; import GisTree from './GisTree'; -import FacilityInfoPopup from './facilityInfoPopup'; import './lib/css/gis.common.css'; import GisApp from './lib/gis.core'; From f4b0af5edea5013b3b9204e69c4637b04be913f6 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 1 Jul 2025 11:01:20 +0900 Subject: [PATCH 10/22] =?UTF-8?q?fix:=20mapSearchBar=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/htdflSttn/page.tsx | 21 +- apps/wr/app/map/mapSearchBar.tsx | 771 +++++++++--------- apps/wr/app/map/newWindowPopup.tsx | 8 +- .../business_components/src/ui/map/Gis.js | 200 ++--- 4 files changed, 503 insertions(+), 497 deletions(-) diff --git a/apps/wr/app/map/htdflSttn/page.tsx b/apps/wr/app/map/htdflSttn/page.tsx index 937490fe2..e74d055d8 100644 --- a/apps/wr/app/map/htdflSttn/page.tsx +++ b/apps/wr/app/map/htdflSttn/page.tsx @@ -125,16 +125,17 @@ const Map = () => { isSearch={false} gisMaps={gisMaps} isMiniMap={true} - searchView={(props: any) => ( - <MapSearchBar - searchType="공급처 후보지 조회" - showApplyButton={false} - checkedLayers={checkedLayers} - checkedLayersData={checkedLayersData} - {...props} - /> - ) - } + searchView={undefined} + // searchView={(props: any) => ( + // <MapSearchBar + // searchType="공급처 후보지 조회" + // showApplyButton={false} + // checkedLayers={checkedLayers} + // checkedLayersData={checkedLayersData} + // {...props} + // /> + // ) + // } detailView={(props: any) => ( <NewWindowPopup onClose={handlePopupClose} // 메모이제이션된 함수 사용 diff --git a/apps/wr/app/map/mapSearchBar.tsx b/apps/wr/app/map/mapSearchBar.tsx index eabc6b892..b24b5d83a 100644 --- a/apps/wr/app/map/mapSearchBar.tsx +++ b/apps/wr/app/map/mapSearchBar.tsx @@ -1,415 +1,414 @@ -"use client" +// "use client" -import { useWrStore } from "@/app/store"; -import SearchInputText from "@/components/ui/searchInput/searchInput"; -import type { EnvPartSelectDataType, ReturnType, SelectDataType } from "@common/business_components/ui"; -import { SelectBox } from "@common/business_components/ui"; -import { Button, Input, Label } from "@common/components/ui"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { MapSearchFormLeft } from "./mapSearchForm"; +// import { useWrStore } from "@/app/store"; +// import SearchInputText from "@/components/ui/searchInput/searchInput"; +// import type { EnvPartSelectDataType, ReturnType, SelectDataType } from "@common/business_components/ui"; +// import { SelectBox } from "@common/business_components/ui"; +// import { Button, Input, Label } from "@common/components/ui"; +// import React, { useEffect, useMemo, useRef, useState } from "react"; +// import { MapSearchFormLeft } from "./mapSearchForm"; -// 건물 검색 타입 정의 -interface BuildingInfo { - bdstInfoPk: number; - ctpvCd: number; - sggCd: number; - daddr: string; - bdstNm: string; - frstArchPrmsnDt: string; - archArea: number; - ctpvNm: string | null; - sggNm: string | null; - lat: number | null; - lot: number | null; - regDt: string; - rgtr: string | null; - mdfcnDt: string; - mdfr: string | null; -} +// // 건물 검색 타입 정의 +// interface BuildingInfo { +// bdstInfoPk: number; +// ctpvCd: number; +// sggCd: number; +// daddr: string; +// bdstNm: string; +// frstArchPrmsnDt: string; +// archArea: number; +// ctpvNm: string | null; +// sggNm: string | null; +// lat: number | null; +// lot: number | null; +// regDt: string; +// rgtr: string | null; +// mdfcnDt: string; +// mdfr: string | null; +// } -type SearchBaseType = { - searchType: string; - selectSidoData?: SelectDataType[] | []; - selectSigunData?: SelectDataType[] | []; - selectRegionData?: SelectDataType[] | []; - selectUnitWaterShedData?: SelectDataType[] | []; - selectMiddleRegionData?: SelectDataType[] | []; - selectEnvPartData?: EnvPartSelectDataType[] | []; - onFeatureSelect: () => void; - btnClass?: string; - ref?: React.RefObject<ReturnType>; - isAdmin?: boolean; - leftClassName?: string; - rightClassName?: string; - showQueryButton?: boolean; - showInitButton?: boolean; - showApplyButton?: boolean; - checkedLayers?: string[]; - checkedLayersData?: any[]; -} +// type SearchBaseType = { +// searchType: string; +// selectSidoData?: SelectDataType[] | []; +// selectSigunData?: SelectDataType[] | []; +// selectRegionData?: SelectDataType[] | []; +// selectUnitWaterShedData?: SelectDataType[] | []; +// selectMiddleRegionData?: SelectDataType[] | []; +// selectEnvPartData?: EnvPartSelectDataType[] | []; +// onFeatureSelect: () => void; +// btnClass?: string; +// ref?: React.RefObject<ReturnType>; +// isAdmin?: boolean; +// leftClassName?: string; +// rightClassName?: string; +// showQueryButton?: boolean; +// showInitButton?: boolean; +// showApplyButton?: boolean; +// checkedLayers?: string[]; +// checkedLayersData?: any[]; +// } -const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; +// const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; -const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); +// const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`); -interface SearchModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onFeatureSelect: (row: any) => void; -} +// interface SearchModalProps { +// open: boolean; +// onOpenChange: (open: boolean) => void; +// onFeatureSelect: (row: any) => void; +// } -interface SelectedDataTypeWithSggNm { - text: string; - val: string; - sggNm: string; -} +// interface SelectedDataTypeWithSggNm { +// text: string; +// val: string; +// sggNm: string; +// } -export function MapSearchBar({ - searchType, - selectSidoData, - selectSigunData, - selectRegionData, - selectUnitWaterShedData, - selectMiddleRegionData, - selectEnvPartData, - btnClass, - onFeatureSelect, - isAdmin = false, - leftClassName, - rightClassName, - showQueryButton = true, - showInitButton = true, - showApplyButton = true, - checkedLayers = [], - checkedLayersData = [], -}: SearchBaseType) { +// export function MapSearchBar({ +// searchType, +// selectSidoData, +// selectSigunData, +// selectRegionData, +// selectUnitWaterShedData, +// selectMiddleRegionData, +// selectEnvPartData, +// btnClass, +// onFeatureSelect, +// isAdmin = false, +// leftClassName, +// rightClassName, +// showQueryButton = true, +// showInitButton = true, +// showApplyButton = true, +// checkedLayers = [], +// checkedLayersData = [], +// }: SearchBaseType) { - // 로딩바 - const [isLoading, setIsLoading] = useState(false); - // 상세 정보 조회를 위한 상태 추가 - const [popupFeature, setPopupFeature] = useState<any>(null); - const [isChHead, setIsChHead] = useState(false); - const ref = useRef<ReturnType>({} as ReturnType); +// // 로딩바 +// const [isLoading, setIsLoading] = useState(false); +// // 상세 정보 조회를 위한 상태 추가 +// const [popupFeature, setPopupFeature] = useState<any>(null); +// const [isChHead, setIsChHead] = useState(false); +// const ref = useRef<ReturnType>({} as ReturnType); - // zustand store에서 건물 검색 함수와 데이터 가져오기 - const { - code, - bdst, - findBdst, - wrMap, - // buildingSearchRes, - buildingNmSearch, - buildingNmSearchMultiple - } = useWrStore((state) => state); +// // zustand store에서 건물 검색 함수와 데이터 가져오기 +// const { +// code, +// bdst, +// findBdst, +// wrMap, +// buildingSearchRes, +// buildingNmSearch, +// buildingNmSearchMultiple +// } = useWrStore((state) => state); - const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>(null); - const [buildingSearchText, setBuildingSearchText] = useState(""); - const [buildingList, setBuildingList] = useState<BuildingInfo[]>([]); +// const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>(null); +// const [buildingSearchText, setBuildingSearchText] = useState(""); +// const [buildingList, setBuildingList] = useState<BuildingInfo[]>([]); - // 전체 목록 - const [partValue, setPartValue] = useState<string>("A"); - const [sidoValue, setSidoValue] = useState<string>("0"); - const [sigunValue, setSigunValue] = useState<string>("0"); - const [regionValue, setRegionValue] = useState<string>("0"); - const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0"); - const [middleRegionValue, setMiddleRegionValue] = useState<string>("0"); - const [envPartValue, setEnvPartValue] = useState<string>("0"); - const [resetTrigger, setResetTrigger] = useState(false); - const [sidoText, setSidoText] = useState<string>("0"); - const [sigunText, setSigunText] = useState<string>("0"); - // 공급처 후보지 조회 필터 목록 - const [selectedSido, setSelectedSido] = useState<SelectDataType>({ text: "전체", val: '0' }); - const [selectedSigun, setSelectedSigun] = useState<SelectDataType>({ text: "전체", val: '0' }); - const [selectedFclt, setSelectedFclt] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 - // 전체 시설 - let allFclt: SelectedDataTypeWithSggNm[] = [{ text: "전체", val: '0', sggNm: '전체' }]; - // 우리동네가뭄지도 필터 목록 - const [selectedDong, setSelectedDong] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동 +// // 전체 목록 +// const [partValue, setPartValue] = useState<string>("A"); +// const [sidoValue, setSidoValue] = useState<string>("0"); +// const [sigunValue, setSigunValue] = useState<string>("0"); +// const [regionValue, setRegionValue] = useState<string>("0"); +// const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0"); +// const [middleRegionValue, setMiddleRegionValue] = useState<string>("0"); +// const [envPartValue, setEnvPartValue] = useState<string>("0"); +// const [resetTrigger, setResetTrigger] = useState(false); +// const [sidoText, setSidoText] = useState<string>("0"); +// const [sigunText, setSigunText] = useState<string>("0"); +// // 공급처 후보지 조회 필터 목록 +// const [selectedSido, setSelectedSido] = useState<SelectDataType>({ text: "전체", val: '0' }); +// const [selectedSigun, setSelectedSigun] = useState<SelectDataType>({ text: "전체", val: '0' }); +// const [selectedFclt, setSelectedFclt] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 +// // 전체 시설 +// let allFclt: SelectedDataTypeWithSggNm[] = [{ text: "전체", val: '0', sggNm: '전체' }]; +// const [selectedDong, setSelectedDong] = useState<SelectedDataTypeWithSggNm>({ text: "전체", val: '0', sggNm: '전체' }); // 시설명 선택 상태 // 행정동 - const filteredSigunData = useMemo(() => { - if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }]; - return [ - { text: "전체", val: '0' }, - ...code.sggCd.filter(item => item.val.substring(0, 2) === selectedSido.val.substring(0, 2)) - ]; - }, [selectedSido]); +// const filteredSigunData = useMemo(() => { +// if (!selectedSido || selectedSido.val === '0' || !selectedSido.val) return [{ text: "전체", val: '0' }]; +// return [ +// { text: "전체", val: '0' }, +// ...code.sggCd.filter((item: { val: string; }) => item.val.substring(0, 2) === selectedSido.val.substring(0, 2)) +// ]; +// }, [selectedSido]); - const filteredFcltData = useMemo(() => { - if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; - return [ - { text: "전체", val: '0', sggNm: '전체' }, - ...allFclt.filter(item => item.sggNm === selectedSido.text) - ]; - }, [selectedSigun]); +// const filteredFcltData = useMemo(() => { +// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; +// return [ +// { text: "전체", val: '0', sggNm: '전체' }, +// ...allFclt.filter(item => item.sggNm === selectedSido.text) +// ]; +// }, [selectedSigun]); - const filteredDong = useMemo(() => { - if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; - return [ - { text: "전체", val: '0', sggNm: '전체' }, - // ...code.dong.filter(item => item.sggNm === selectedSido.text) - ]; - }, [selectedSigun]); +// const filteredDong = useMemo(() => { +// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) return [{ text: "전체", val: '0', sggNm: '전체' }]; +// return [ +// { text: "전체", val: '0', sggNm: '전체' }, +// // ...code.dong.filter(item => item.sggNm === selectedSido.text) +// ]; +// }, [selectedSigun]); - // 조회 함수 - const queryVal = () => { - // 46 : 하수처리시설 - // 47 : 하수재이용시설 - // 48 : 빗물이용시설 - // 49 : 중수도시설 - // 50 : 온배수시설 - console.log("조회버튼 클릭: ", checkedLayers); - if (checkedLayers && checkedLayers.includes('48')) { - // searchBuildings - } - } +// // 조회 함수 +// const queryVal = () => { +// // 46 : 하수처리시설 +// // 47 : 하수재이용시설 +// // 48 : 빗물이용시설 +// // 49 : 중수도시설 +// // 50 : 온배수시설 +// console.log("조회버튼 클릭: ", checkedLayers); +// if (checkedLayers && checkedLayers.includes('48')) { +// // searchBuildings +// } +// } - // 초기화 함수 - const initVal = () => { - setPartValue("A"); - setSidoValue("0"); - setSigunValue("0"); - setRegionValue("0"); - setUntitWaterShedValue("0"); - setMiddleRegionValue("0"); - setEnvPartValue("0"); - setSidoText("전체"); - setSigunText("전체"); - setSelectedSido({ text: "전체", val: '0' }); - setSelectedSigun({ text: "전체", val: '0' }); - setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); - setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); - setResetTrigger(true); - }; +// // 초기화 함수 +// const initVal = () => { +// setPartValue("A"); +// setSidoValue("0"); +// setSigunValue("0"); +// setRegionValue("0"); +// setUntitWaterShedValue("0"); +// setMiddleRegionValue("0"); +// setEnvPartValue("0"); +// setSidoText("전체"); +// setSigunText("전체"); +// setSelectedSido({ text: "전체", val: '0' }); +// setSelectedSigun({ text: "전체", val: '0' }); +// setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); +// setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); +// setResetTrigger(true); +// }; - // 적용 버튼 - const applyVal = () => { - onFeatureSelect(); - }; +// // 적용 버튼 +// const applyVal = () => { +// onFeatureSelect(); +// }; - useEffect(() => { - if (resetTrigger) { - setResetTrigger(false); - } - }, [resetTrigger]); +// useEffect(() => { +// if (resetTrigger) { +// setResetTrigger(false); +// } +// }, [resetTrigger]); - // 시설 조회 - useEffect(() => { - const fetchData = async () => { - setIsLoading(true); - if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) { - allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); - setIsLoading(false); - return; - } +// // 시설 조회 +// useEffect(() => { +// const fetchData = async () => { +// setIsLoading(true); +// if (!selectedSigun || selectedSigun.val === '0' || !selectedSigun.val) { +// allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); +// setIsLoading(false); +// return; +// } - try { - const queryParamsFacility = new URLSearchParams({ - lyrTypeNm: 'tb_il_swage_prcs_fclt', - page: '0', - size: '100000', - sort: 'string', - }); - const cqlFiltersFacility: string[] = []; - if (selectedSigun.text && selectedSigun.val !== '0') { - cqlFiltersFacility.push(`sgg_nm ILIKE '%${selectedSigun.text}%'`); - } - if (cqlFiltersFacility.length > 0) { - queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); - } - console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`); +// try { +// const queryParamsFacility = new URLSearchParams({ +// lyrTypeNm: 'tb_il_swage_prcs_fclt', +// page: '0', +// size: '100000', +// sort: 'string', +// }); +// const cqlFiltersFacility: string[] = []; +// if (selectedSigun.text && selectedSigun.val !== '0') { +// cqlFiltersFacility.push(`sgg_nm ILIKE '%${selectedSigun.text}%'`); +// } +// if (cqlFiltersFacility.length > 0) { +// queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); +// } +// console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`); - const facilityRes = await fetch(`${API_URL}/v1/fclts?${queryParamsFacility.toString()}`, { - method: "GET", - headers: { accept: "application/json" } - }).then(res => res.json()); +// const facilityRes = await fetch(`${API_URL}/v1/fclts?${queryParamsFacility.toString()}`, { +// method: "GET", +// headers: { accept: "application/json" } +// }).then(res => res.json()); - const facilities = (facilityRes.data || []).map((item: any) => ({ - text: item.swtrmtFcltNm, - val: item.fid, - sggNm: item.sggNm - })); - allFclt = (facilities); +// const facilities = (facilityRes.data || []).map((item: any) => ({ +// text: item.swtrmtFcltNm, +// val: item.fid, +// sggNm: item.sggNm +// })); +// allFclt = (facilities); - } catch (error) { - console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error); - allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); - } finally { - setIsLoading(false); - } - }; +// } catch (error) { +// console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error); +// allFclt = ([{ text: "전체", val: '0', sggNm: '전체' }]); +// } finally { +// setIsLoading(false); +// } +// }; - fetchData(); - }, [selectedSigun]); +// fetchData(); +// }, [selectedSigun]); - useEffect(() => { - if (ref && ref.current) { - const customRef = ref.current; - customRef.partValue = partValue; - customRef.sidoValue = sidoValue; - customRef.sigunValue = sigunValue; - customRef.regionValue = regionValue; - customRef.unitWaterShedValue = untitWaterShedValue; - customRef.middleRegionValue = middleRegionValue; - customRef.envPartValue = envPartValue; - customRef.sidoText = selectedSido.text; - customRef.sigunText = selectedSigun.text; - } - }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); +// useEffect(() => { +// if (ref && ref.current) { +// const customRef = ref.current; +// customRef.partValue = partValue; +// customRef.sidoValue = sidoValue; +// customRef.sigunValue = sigunValue; +// customRef.regionValue = regionValue; +// customRef.unitWaterShedValue = untitWaterShedValue; +// customRef.middleRegionValue = middleRegionValue; +// customRef.envPartValue = envPartValue; +// customRef.sidoText = selectedSido.text; +// customRef.sigunText = selectedSigun.text; +// } +// }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); - // buildingSearchText가 변경될 때 API 호출 - useEffect(() => { - if (buildingSearchText && buildingSearchText.length > 1) { - if (checkedLayers && checkedLayers.length > 0) { - // 다중 레이어 검색 - // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", buildingSearchText); - buildingNmSearch("RNB", selectedSigun.val, buildingSearchText); - } else { - // 단일 레이어 검색 - // buildingNmSearch(checkedLayers[0], selectedSigun.val, buildingSearchText); - } - } else { - // 검색 텍스트가 짧으면 리스트 초기화 - if (checkedLayers && checkedLayers.length > 0) { - // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", ""); - } else { - buildingNmSearch("RNB", selectedSigun.val || "", ""); - } - } - }, [buildingSearchText]); +// // buildingSearchText가 변경될 때 API 호출 +// useEffect(() => { +// if (buildingSearchText && buildingSearchText.length > 1) { +// if (checkedLayers && checkedLayers.length > 0) { +// // 다중 레이어 검색 +// // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", buildingSearchText); +// buildingNmSearch("RNB", selectedSigun.val, buildingSearchText); +// } else { +// // 단일 레이어 검색 +// // buildingNmSearch(checkedLayers[0], selectedSigun.val, buildingSearchText); +// } +// } else { +// // 검색 텍스트가 짧으면 리스트 초기화 +// if (checkedLayers && checkedLayers.length > 0) { +// // buildingNmSearchMultiple(checkedLayers, selectedSigun.val || "", ""); +// } else { +// buildingNmSearch("RNB", selectedSigun.val || "", ""); +// } +// } +// }, [buildingSearchText]); - return ( - <div className="absolute left-1/2 -translate-x-1/2 bg-white flex items-center justify-center p-4 rounded-md shadow-lg w-auto min-w-fit"> - <div className={leftClassName ? leftClassName : `flex items-center justify-center space-x-2 `}> - <div className="flex gap-8 items-center"> - {searchType == "공급처 후보지 조회" && ( - <> - <SelectBox - className="flex items-center" - label={'시도'} - selectArray={code.cityCd} - onSelectValue={(val) => { - const selected = code.cityCd.find(item => (item.val === val)); - setSelectedSido(selected || { text: "전체", val: '0' }); - setSelectedSigun({ text: "전체", val: '0' }); - }} - value={selectedSido.val} - selectClass={'w-32'} - /> - <SelectBox - className="flex items-center" - label={'시군구'} - selectArray={filteredSigunData} - onSelectValue={(val) => { - const selected = filteredSigunData.find(item => (item.val === val)); - setSelectedSigun(selected || { text: "전체", val: '0' }); - setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); - }} - value={selectedSigun.val} - selectClass={'w-32'} - /> - <div className="flex items-center"> - <Label className="w-20 text-xl text-gray-600">시설명</Label> - <div className="w-64"> - <SearchInputText - label="시설명" - initValue={buildingSearchText} - setValue={(searchNm?: string) => { - setBuildingSearchText(searchNm || ""); - }} - setRtnData={function (rtnData: any): void { - // 선택된 건물 데이터 처리 - // if (rtnData && rtnData.bdstInfoPk) { - // setSelectedBuilding(rtnData); - // setBuildingSearchText(rtnData.bdstNm || ""); - // } - }} - searchList={wrMap.buildingSearchRes || []} - /> - </div> - </div> - </> - )} - {searchType == "우리동네가뭄지도보기" && ( - <> - <SelectBox - className="flex items-center" - label={'시도'} - selectArray={code.cityCd} - onSelectValue={(val) => { - const selected = code.cityCd.find(item => (item.val === val)); - setSelectedSido(selected || { text: "전체", val: '0' }); - setSelectedSigun({ text: "전체", val: '0' }); - }} - value={selectedSido.val} - selectClass={'w-44'} - /> - <SelectBox - className="flex items-center" - label={'시군구'} - selectArray={filteredSigunData} - onSelectValue={(val) => { - const selected = filteredSigunData.find(item => (item.val === val)); - setSelectedSigun(selected || { text: "전체", val: '0' }); - setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); - }} - value={selectedSigun.val} - selectClass={'w-32'} - /> - <SelectBox - className="flex items-center mr-8" - label={'행정동'} - selectArray={filteredDong} - onSelectValue={(val) => { - const selected = filteredDong.find(item => (item.val === val)); - setSelectedDong(selected || { text: "전체", val: '0', sggNm: '전체' }); - }} - value={selectedDong.val} - selectClass={'w-32'} - /> - </> - )} - {searchType == "지도보기" && ( - <div className="flex items-center gap-2 w-[800px]"> - <div className="flex min-w-0"> {/* SearchFormLeft 래퍼 */} - <MapSearchFormLeft - isShowPart={true} - selectSidoData={code.cityCd} - selectSigunData={filteredSigunData} - selectRegionData={code.rvbsnCd} - selectUnitWaterShedData={selectUnitWaterShedData} - selectMiddleRegionData={selectMiddleRegionData} - selectEnvPartData={selectEnvPartData} - setRtnPartValue={setPartValue} - setRtnSidoValue={setSidoValue} - setRtnSigunValue={setSigunValue} - setRtnRegionValue={setRegionValue} - setRtnUnitWaterShedValue={setUntitWaterShedValue} - setRtnMiddleRegionValue={setMiddleRegionValue} - setRtnEnvPartValue={setEnvPartValue} - setRtnEnvPartSidoValue={setSidoValue} - setRtnEnvPartSigunValue={setSigunValue} - setRtnSidoText={setSidoText} - setRtnSigunText={setSigunText} - resetTrigger={resetTrigger} - isAdmin={isAdmin} - children={undefined} - /> - </div> - <Label className="w-auto text-xl text-gray-600 whitespace-nowrap">시설명</Label> - <div className="w-44"> - <Input className="mr-4" /> - </div> - </div> - )} - </div> - {/* 버튼 부분 */} - <div className="flex items-center justify-center gap-4"> - {showQueryButton && <Button className={btnClass} onClick={queryVal}>조회</Button>} - {showInitButton && <Button className={btnClass} onClick={initVal}>초기화</Button>} - {showApplyButton && <Button className={btnClass} onClick={applyVal}>적용</Button>} - </div> - </div> - </div> - ) -} +// return ( +// <div className="absolute left-1/2 -translate-x-1/2 bg-white flex items-center justify-center p-4 rounded-md shadow-lg w-auto min-w-fit"> +// <div className={leftClassName ? leftClassName : `flex items-center justify-center space-x-2 `}> +// <div className="flex gap-8 items-center"> +// {searchType == "공급처 후보지 조회" && ( +// <> +// <SelectBox +// className="flex items-center" +// label={'시도'} +// selectArray={code.cityCd} +// onSelectValue={(val) => { +// const selected = code.cityCd.find((item: { val: string | number; }) => (item.val === val)); +// setSelectedSido(selected || { text: "전체", val: '0' }); +// setSelectedSigun({ text: "전체", val: '0' }); +// }} +// value={selectedSido.val} +// selectClass={'w-32'} +// /> +// <SelectBox +// className="flex items-center" +// label={'시군구'} +// selectArray={filteredSigunData} +// onSelectValue={(val) => { +// const selected = filteredSigunData.find(item => (item.val === val)); +// setSelectedSigun(selected || { text: "전체", val: '0' }); +// setSelectedFclt({ text: "전체", val: '0', sggNm: '전체' }); +// }} +// value={selectedSigun.val} +// selectClass={'w-32'} +// /> +// <div className="flex items-center"> +// <Label className="w-20 text-xl text-gray-600">시설명</Label> +// <div className="w-64"> +// <SearchInputText +// label="시설명" +// initValue={buildingSearchText} +// setValue={(searchNm?: string) => { +// setBuildingSearchText(searchNm || ""); +// }} +// setRtnData={function (rtnData: any): void { +// // 선택된 건물 데이터 처리 +// // if (rtnData && rtnData.bdstInfoPk) { +// // setSelectedBuilding(rtnData); +// // setBuildingSearchText(rtnData.bdstNm || ""); +// // } +// }} +// searchList={buildingSearchRes || []} +// /> +// </div> +// </div> +// </> +// )} +// {searchType == "우리동네가뭄지도보기" && ( +// <> +// <SelectBox +// className="flex items-center" +// label={'시도'} +// selectArray={code.cityCd} +// onSelectValue={(val) => { +// const selected = code.cityCd.find((item: { val: string | number; }) => (item.val === val)); +// setSelectedSido(selected || { text: "전체", val: '0' }); +// setSelectedSigun({ text: "전체", val: '0' }); +// }} +// value={selectedSido.val} +// selectClass={'w-44'} +// /> +// <SelectBox +// className="flex items-center" +// label={'시군구'} +// selectArray={filteredSigunData} +// onSelectValue={(val) => { +// const selected = filteredSigunData.find(item => (item.val === val)); +// setSelectedSigun(selected || { text: "전체", val: '0' }); +// setSelectedDong({ text: "전체", val: '0', sggNm: '전체' }); +// }} +// value={selectedSigun.val} +// selectClass={'w-32'} +// /> +// <SelectBox +// className="flex items-center mr-8" +// label={'행정동'} +// selectArray={filteredDong} +// onSelectValue={(val) => { +// const selected = filteredDong.find(item => (item.val === val)); +// setSelectedDong(selected || { text: "전체", val: '0', sggNm: '전체' }); +// }} +// value={selectedDong.val} +// selectClass={'w-32'} +// /> +// </> +// )} +// {searchType == "지도보기" && ( +// <div className="flex items-center gap-2 w-[800px]"> +// <div className="flex min-w-0"> {/* SearchFormLeft 래퍼 */} +// <MapSearchFormLeft +// isShowPart={true} +// selectSidoData={code.cityCd} +// selectSigunData={filteredSigunData} +// selectRegionData={code.rvbsnCd} +// selectUnitWaterShedData={selectUnitWaterShedData} +// selectMiddleRegionData={selectMiddleRegionData} +// selectEnvPartData={selectEnvPartData} +// setRtnPartValue={setPartValue} +// setRtnSidoValue={setSidoValue} +// setRtnSigunValue={setSigunValue} +// setRtnRegionValue={setRegionValue} +// setRtnUnitWaterShedValue={setUntitWaterShedValue} +// setRtnMiddleRegionValue={setMiddleRegionValue} +// setRtnEnvPartValue={setEnvPartValue} +// setRtnEnvPartSidoValue={setSidoValue} +// setRtnEnvPartSigunValue={setSigunValue} +// setRtnSidoText={setSidoText} +// setRtnSigunText={setSigunText} +// resetTrigger={resetTrigger} +// isAdmin={isAdmin} +// children={undefined} +// /> +// </div> +// <Label className="w-auto text-xl text-gray-600 whitespace-nowrap">시설명</Label> +// <div className="w-44"> +// <Input className="mr-4" /> +// </div> +// </div> +// )} +// </div> +// {/* 버튼 부분 */} +// <div className="flex items-center justify-center gap-4"> +// {showQueryButton && <Button className={btnClass} onClick={queryVal}>조회</Button>} +// {showInitButton && <Button className={btnClass} onClick={initVal}>초기화</Button>} +// {showApplyButton && <Button className={btnClass} onClick={applyVal}>적용</Button>} +// </div> +// </div> +// </div> +// ) +// } diff --git a/apps/wr/app/map/newWindowPopup.tsx b/apps/wr/app/map/newWindowPopup.tsx index e67b2b0fb..dfbb1dc24 100644 --- a/apps/wr/app/map/newWindowPopup.tsx +++ b/apps/wr/app/map/newWindowPopup.tsx @@ -48,7 +48,8 @@ const NewWindowPopup: React.FC<NewWindowPopupProps> = ({ onClose }) => { const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "0"); - const [showDetailView, setShowDetailView] = useState(false); // 상세정보 화면 표시 상태 추가 + // swcho : useState의 true, false 값 변경하여 목록, 상태 테스트 하였음 + const [showDetailView, setShowDetailView] = useState(true); // 상세정보 화면 표시 상태 추가 // onClose 함수를 useCallback으로 메모이제이션 const memoizedOnClose = useCallback(() => { @@ -183,14 +184,15 @@ const NewWindowPopup: React.FC<NewWindowPopupProps> = ({ layer.data?.lyrNm?.includes('하수처리') || layer.data?.lyrNm?.includes('재이용시설') ); + // swcho : true, false 값 변경하여 시설 변경하여 테스트하였음 // 재이용시설 const isPbnst = false; // 빗물이용시설 const isRain = false; // 중수도시설 - const isRswt = false; + const isRswt = true; // 온배수시설 - const isHtdfl = true; + const isHtdfl = false; return ( diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index e85b8d162..5ac98f27f 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -16,6 +16,7 @@ import GisToolBar from './GisToolBar'; import GisTree from './GisTree'; import './lib/css/gis.common.css'; import GisApp from './lib/gis.core'; +// import FacilityInfoPopup from './FacilityInfoPopup'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); @@ -47,8 +48,8 @@ function Gis({ const checkedLayerIds = useRef([]); // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 - const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); - const facilityOverlaysRef = useRef([]); + // const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); + // const facilityOverlaysRef = useRef([]); // 체크된 레이어 상태 관리 const [checkedLayers, setCheckedLayers] = useState([]); const [checkedLayersData, setCheckedLayersData] = useState([]); @@ -225,119 +226,119 @@ function Gis({ } } - // 데이터 로드 및 팝업 생성 로직 - useEffect(() => { - if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 + // // 데이터 로드 및 팝업 생성 로직 + // useEffect(() => { + // if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 - // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) - const fetchPopupData = async () => { - // 실제 API 호출 로직으로 대체해야 합니다. - // 예시: const response = await yourApiService.getFcltyWtCnt(); - // setFacilityInfoPopupData(response.data); + // // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) + // const fetchPopupData = async () => { + // // 실제 API 호출 로직으로 대체해야 합니다. + // // 예시: const response = await yourApiService.getFcltyWtCnt(); + // // setFacilityInfoPopupData(response.data); - // 임시 목업 데이터 - const mockData = [ - { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, - { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } - ]; - setFacilityInfoPopupData(mockData); - }; + // // 임시 목업 데이터 + // const mockData = [ + // { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, + // { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } + // ]; + // setFacilityInfoPopupData(mockData); + // }; - fetchPopupData(); + // fetchPopupData(); - }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 + // }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 - // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 - useEffect(() => { - const map = mapInstanceRef.current; - if (!map || facilityInfoPopupData.length === 0) return; + // // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 + // useEffect(() => { + // const map = mapInstanceRef.current; + // if (!map || facilityInfoPopupData.length === 0) return; - // 기존 오버레이 정리 - facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); - facilityOverlaysRef.current = []; + // // 기존 오버레이 정리 + // facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); + // facilityOverlaysRef.current = []; - facilityInfoPopupData.forEach(data => { - const popupId = `${data.ftrIdn}_${data.ftrCde}`; + // facilityInfoPopupData.forEach(data => { + // const popupId = `${data.ftrIdn}_${data.ftrCde}`; - // 팝업을 담을 div 엘리먼트 생성 - const popupElement = document.createElement('div'); - popupElement.id = popupId; + // // 팝업을 담을 div 엘리먼트 생성 + // const popupElement = document.createElement('div'); + // popupElement.id = popupId; - // OpenLayers Overlay 생성 - const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); - const overlay = new Overlay({ - element: popupElement, - position: position, - offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 - positioning: 'bottom-center', - className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 - }); + // // OpenLayers Overlay 생성 + // const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); + // const overlay = new Overlay({ + // element: popupElement, + // position: position, + // offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 + // positioning: 'bottom-center', + // className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 + // }); - map.addOverlay(overlay); - facilityOverlaysRef.current.push(overlay); + // map.addOverlay(overlay); + // facilityOverlaysRef.current.push(overlay); - // React 컴포넌트를 popupElement에 렌더링 - const root = createRoot(popupElement); - root.render(<FacilityInfoPopup data={data} />); - }); + // // React 컴포넌트를 popupElement에 렌더링 + // const root = createRoot(popupElement); + // root.render(<FacilityInfoPopup data={data} />); + // }); - // 초기 가시성 업데이트 - updatePopupVisibility(); + // // 초기 가시성 업데이트 + // updatePopupVisibility(); - }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 + // }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 - // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 - const updatePopupVisibility = useCallback(() => { - const map = mapInstanceRef.current; - if (!map) return; + // // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 + // const updatePopupVisibility = useCallback(() => { + // const map = mapInstanceRef.current; + // if (!map) return; - const visibleLayers = new Set(); - map.getLayers().forEach(layer => { - if (layer.getVisible()) { - // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 - const typeName = layer.get('typeName'); - if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 - visibleLayers.add(layer.get('ftrCde')); - } - } - }); + // const visibleLayers = new Set(); + // map.getLayers().forEach(layer => { + // if (layer.getVisible()) { + // // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 + // const typeName = layer.get('typeName'); + // if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 + // visibleLayers.add(layer.get('ftrCde')); + // } + // } + // }); - facilityOverlaysRef.current.forEach(overlay => { - const element = overlay.getElement(); - const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 + // facilityOverlaysRef.current.forEach(overlay => { + // const element = overlay.getElement(); + // const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 - if (visibleLayers.has(ftrCde)) { - element.style.display = ''; - } else { - element.style.display = 'none'; - } - }); - }, []); + // if (visibleLayers.has(ftrCde)) { + // element.style.display = ''; + // } else { + // element.style.display = 'none'; + // } + // }); + // }, []); // GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다. // 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출 - useEffect(() => { - // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 - const map = mapInstanceRef.current; - if (!map) return; + // useEffect(() => { + // // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 + // const map = mapInstanceRef.current; + // if (!map) return; - const handleLayerVisibilityChange = () => { - updatePopupVisibility(); - }; + // const handleLayerVisibilityChange = () => { + // updatePopupVisibility(); + // }; - map.getLayers().on('change:length', handleLayerVisibilityChange); - map.getLayers().forEach(layer => { - layer.on('change:visible', handleLayerVisibilityChange); - }); + // map.getLayers().on('change:length', handleLayerVisibilityChange); + // map.getLayers().forEach(layer => { + // layer.on('change:visible', handleLayerVisibilityChange); + // }); - return () => { - // 클린업 - map.getLayers().un('change:length', handleLayerVisibilityChange); - map.getLayers().forEach(layer => { - layer.un('change:visible', handleLayerVisibilityChange); - }); - }; - }, [updatePopupVisibility]); + // return () => { + // // 클린업 + // map.getLayers().un('change:length', handleLayerVisibilityChange); + // map.getLayers().forEach(layer => { + // layer.un('change:visible', handleLayerVisibilityChange); + // }); + // }; + // }, [updatePopupVisibility]); // 체크된 레이어 정보를 처리하는 함수 const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { @@ -359,18 +360,21 @@ function Gis({ {/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */} - {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} {isOpen && featureData && !isSearchModalOpen && ( <DetailView featureData={featureData} onClose={closePopup} // 닫기 콜백 전달 /> )} - <SearchView - open={isSearchModalOpen} - onOpenChange={setIsSearchModalOpen} - onFeatureSelect={handleFeatureSelect} - /> + {/* SearchView가 존재할 때만 렌더링 */} + {SearchView && ( + <SearchView + open={isSearchModalOpen} + onOpenChange={setIsSearchModalOpen} + onFeatureSelect={handleFeatureSelect} + /> + )} <GisLonLat /> <GisToolBar /> <GisMapToggle /> From ea20ceacb3de830ec0748ff008d9e65f443251d7 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 1 Jul 2025 11:01:22 +0900 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20mapSearchBar=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/store/map/index.tsx | 185 ++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 apps/wr/app/store/map/index.tsx diff --git a/apps/wr/app/store/map/index.tsx b/apps/wr/app/store/map/index.tsx new file mode 100644 index 000000000..6a1e76c9b --- /dev/null +++ b/apps/wr/app/store/map/index.tsx @@ -0,0 +1,185 @@ +import { getFetch } from "@/components/lib/api"; + +export type wrMapType = wrMapState & wrMapActions; + +export type wrMapState = { + wrMap: { + buildingSearchRes: any[] + } // 건물 검색 결과 데이터 +}; + +type wrMapActions = { + buildingNmSearch: ( + flg: string, + sigunCode: string, + searchNm: string | undefined + ) => void; + buildingNmSearchMultiple: ( + checkedLayers: string[], + sigunCode: string, + searchNm: string | undefined + ) => void; // 여러 레이어 동시 검색 함수 추가 +}; + +export const wrMapStateExport: wrMapState = { + wrMap: { + buildingSearchRes: [] + } +}; + +// 레이어 타입과 플래그 매핑 +const LAYER_FLAG_MAP: { [key: string]: string } = { + // '46': '???', // 하수처리시설 + // '47': '???', // 하수재이용시설 + '48': 'RNB', // 빗물이용시설 + '49': 'RST', // 중수도시설 + '50': 'HTB', // 온배수시설 +}; + +const findBdst = async (flg: string, sggCd: string, searchTxt: string | undefined) => { + return await getFetch('wr', `/reuseFc/building/${flg}/${sggCd}` + (searchTxt ? `/${searchTxt}` : '')) +} + +export const wrMapActionsExport: (set: any) => wrMapActions = ( + set: any +) => { + return { + buildingNmSearch: async ( + flg: string, + sigunCode: string, + searchNm: string | undefined + ) => { + try { + // if (!searchNm || searchNm.length < 2) { + // set((state: wrMapState) => ({ + // ...state, + // buildingSearchData: [] + // })); + // return; + // } + console.log("건축물 코드, 시군구 코드, 검색어: ", flg, sigunCode, searchNm); + + const response = await findBdst(flg, sigunCode, searchNm); + console.log("response: ", response); + + // API 응답을 SearchInputText 형식으로 변환 + const searchListFormatted = (response || []).map((building: any) => ({ + searchNm: building.bdstNm, + searchPk: String(building.bdstInfoPk), + dmdCoNm: building.bdstNm, + layerType: flg, + ...building + })); + + // findBdst와 유사한 API 호출 + // findBdst(flg, sigunCode || "", searchNm).then(() => { + // console.log("건물 검색 API 호출: ", sigunCode, searchNm, flg); + // // bdst.bdstBase를 SearchInputText 형식으로 변환 + // const searchListFormatted = (state.bdst.bdstBase || []).map((building: any) => ({ + // searchNm: building.bdstNm, + // searchPk: String(building.bdstInfoPk), + // dmdCoNm: building.bdstNm, + // layerType: flg, + // ...building + // })); + + // set((state: any) => ({ + // ...state, + // wrMap: { + // ...state.wrMap, + // buildingSearchData: searchListFormatted + // } + // })); + // }); + set((state: wrMapState) => ({ + ...state, + wrMap: { + ...state.wrMap, + buildingSearchRes: searchListFormatted + } + })); + } catch (error) { + console.error("건물 검색 중 오류:", error); + } + }, + + buildingNmSearchMultiple: async ( + checkedLayers: string[], + sigunCode: string, + searchNm: string | undefined + ) => { + try { + if (!searchNm || searchNm.length < 2) { + set((state: any) => ({ + ...state.wrMap, + buildingSearchData: [] + })); + return; + } + + // 체크된 레이어들에 대해 병렬로 API 호출 + const searchPromises = checkedLayers + .filter(layer => LAYER_FLAG_MAP[layer]) // 매핑된 레이어만 필터링 + .map(layer => { + const flg = LAYER_FLAG_MAP[layer]; + + return findBdst(flg, sigunCode, searchNm) + .then(response => { + console.log(`레이어 ${layer}(${flg}) API 응답:`, response); + + const searchListFormatted = (state.bdst.bdstBase || []).map((building: any) => ({ + searchNm: building.bdstNm, + searchPk: String(building.bdstInfoPk), + dmdCoNm: building.bdstNm, + layerType: flg, + ...building + })); + + return { + layerType: layer, + flg: flg, + data: bdstNames + }; + }) + .catch(error => { + console.error(`레이어 ${layer}(${flg}) 검색 중 오류:`, error); + return { layerType: layer, flg: flg, data: [] }; + }); + }); + + const results = await Promise.all(searchPromises); + console.log("다중 레이어 검색 결과:", results); + + // 모든 결과를 하나의 배열로 합치기 + const allSearchResults = results.reduce((acc, result) => { + const formatted = result.data.map((building: any) => ({ + searchNm: building.bdstNm, + searchPk: String(building.bdstInfoPk), + dmdCoNm: building.bdstNm, + layerType: result.layerType, + flg: result.flg, + ...building.originalData + })); + return [...acc, ...formatted]; + }, [] as any[]); + + console.log("최종 합쳐진 검색 결과:", { + totalCount: allSearchResults.length, + sampleData: allSearchResults.slice(0, 3) + }); + + set((state: any) => ({ + ...state.wrMap, + buildingSearchData: allSearchResults + })); + + } catch (error) { + console.error("다중 레이어 검색 중 오류:", error); + set((state: any) => ({ + ...state, + buildingSearchData: [] + })); + } + } + }; +}; \ No newline at end of file From 547244872d6320772dd8237e15ada51852cd5557 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 1 Jul 2025 11:06:15 +0900 Subject: [PATCH 12/22] =?UTF-8?q?etc:=20=ED=95=98=EC=88=98=EC=9E=AC?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=EC=8B=9C=EC=84=A4=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A1=9C=20=ED=8C=9D=EC=97=85=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/map/newWindowPopup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wr/app/map/newWindowPopup.tsx b/apps/wr/app/map/newWindowPopup.tsx index dfbb1dc24..ebfcac1ca 100644 --- a/apps/wr/app/map/newWindowPopup.tsx +++ b/apps/wr/app/map/newWindowPopup.tsx @@ -186,11 +186,11 @@ const NewWindowPopup: React.FC<NewWindowPopupProps> = ({ // swcho : true, false 값 변경하여 시설 변경하여 테스트하였음 // 재이용시설 - const isPbnst = false; + const isPbnst = true; // 빗물이용시설 const isRain = false; // 중수도시설 - const isRswt = true; + const isRswt = false; // 온배수시설 const isHtdfl = false; From b0dfec9825022ffc2ab85ea10b8c1f75171d916e Mon Sep 17 00:00:00 2001 From: jyha00 <jyha@mind-one.co.kr> Date: Tue, 1 Jul 2025 11:27:09 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=EC=9A=B0=EB=A6=AC=EB=8F=99=EB=84=A4?= =?UTF-8?q?=EA=B0=80=EB=AD=84=20=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/matMng/coMng/drgtSttn/page.tsx | 84 ++++++---- apps/wr/app/store/matDmd/drgtSttn/index.tsx | 148 +++++++++++------- .../ui/modal/coMat/drgtDetailModal.tsx | 56 ++++--- 3 files changed, 183 insertions(+), 105 deletions(-) diff --git a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx index d7c5170b2..2f07dd6de 100644 --- a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx @@ -6,15 +6,15 @@ import { drgtHeadListExport } from "@/app/store/matDmd/drgtSttn/headList"; import { excelDownload, getCity } from "@/components/lib/func/commom"; import DrgtDetailModal from "@/components/ui/modal/coMat/drgtDetailModal"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; -import { ContentsDiv, DateMonthPicker, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; +import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; import { MonthPicker } from "@common/business_components/ui/date_month_picker/mothPicker"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, DatePicker, Label } from "@common/components/ui"; +import { Button, DatePicker, Label, ScrollArea } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; import { toast, Toaster } from "react-hot-toast"; export default function DrgtSttn() { - const { code, drgt, drgtSearch, drgtModalSearch } = useWrStore((state) => state); + const { code, drgt, drgtSearch, drgtModalSearch, drgtInit } = useWrStore((state) => state); const { drgtRes, drgtMoRes } = drgt; const [fromDate, setFromDate] = useState<string>(); const [toDate, setToDate] = useState<string>(); @@ -22,6 +22,8 @@ export default function DrgtSttn() { const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); const [tableData, setTableData] = useState<customDrgt[]>(); const [selectedSgg, setSelectedSgg] = useState<string>(''); + const [excelData, setExcelData] = useState<any[]>(); + const [num, setNum] = useState<string>(); const [drgtModal, setDrgtModal] = useState<{ isOpen: boolean; data: customDrgt | undefined; @@ -35,6 +37,39 @@ export default function DrgtSttn() { } } + const excelClick = () => { + downloadExcel({ + data: excelData ?? [], + fileName: "우리동네가뭄 현황", + sheetNameArray: ["Sheet1"], + headerRange: "A1:G1" + }) + } + + useEffect(() => { + if (!drgt.isInit) { + drgtInit(); + } + }, [drgt.isInit, drgtInit]); + + useEffect(() => { + const resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes]; + const data = Array.isArray(resData) + ? resData.map((item: customDrgt) => { + return { + "시도": item.ctpvCd, + "시군구": item.sggNm, + "정상(횟수)": item.normal, + "관심(횟수)": item.attention, + "주의(횟수)": item.caution, + "관심+주의(횟수)": item.warning, + "가뭄순위": item.ranking, + } + }) : []; + setExcelData(data) + setTableData(drgtRes) + }, [drgtRes]) + useEffect(() => { if (drgtMoRes && drgtMoRes.length > 0) { setDrgtModal({ isOpen: true, data: drgtMoRes }); @@ -65,15 +100,6 @@ export default function DrgtSttn() { setObjSearch({ ...objSearch, sgg: String(val) }); }; - const excelClick = useCallback(() => { - excelDownload( - [[]], - "우리동네가뭄", - "sample_drgtSttn.xlsx", - "A1:H1" - ); - }, []); - useEffect(() => { if (fromDate === undefined && toDate === undefined) { const today = new Date(); @@ -101,7 +127,7 @@ export default function DrgtSttn() { onSelectValue={getSelCity} /> <SelectBox - className="col-span-2 w-1/6 h-full text-2xl" + className="col-span-2 w-1/6 h-full text-2xl ml-8" label="시군구" selectClass={""} selectArray={selectSgg || []} @@ -110,7 +136,7 @@ export default function DrgtSttn() { /> <div className="col-span-5 w-auto"> <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600">조회기간</Label> + <Label className="text-xl text-gray-600 ml-8">조회기간</Label> <div className="flex flex-row w-1/4"> <DateMonthPicker field={{ @@ -125,8 +151,9 @@ export default function DrgtSttn() { setFromDate(formatted); } }} + selectClass="ml-4 text-2xl" /> - <span>~</span> + <span className="ml-2 text-center">~</span> <DateMonthPicker field={{ value: toDate, @@ -140,6 +167,7 @@ export default function DrgtSttn() { setToDate(formatted); } }} + selectClass="ml-4" /> </div> </div> @@ -148,7 +176,7 @@ export default function DrgtSttn() { </SearchFormLeft> <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> - <div className="flex flex-row space-x-2"> + <div className="flex flex-row space-x-2 items-center"> <Button size={"sm"} onClick={(e) => { @@ -199,17 +227,19 @@ export default function DrgtSttn() { <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> - <UiTable - headName={[]} - tableData={drgtRes || []} - headlist={drgtHeadListExport} - pageSize={20} - total={10000} - cellClick={(index: number | undefined, headId: string) => { - cellClick(index ?? undefined, headId); - }} - onPageChange={() => { }} - ></UiTable> + <ScrollArea className="h-[66vh]"> + <UiTable + headName={undefined} + tableData={tableData || []} + headlist={drgtHeadListExport} + pageSize={14} + total={0} + cellClick={(index: number | undefined, headId: string) => { + cellClick(index ?? undefined, headId); + }} + onPageChange={() => { }} + ></UiTable> + </ScrollArea> </ListDiv> </div> </div> diff --git a/apps/wr/app/store/matDmd/drgtSttn/index.tsx b/apps/wr/app/store/matDmd/drgtSttn/index.tsx index 698ba5984..053bb12fc 100644 --- a/apps/wr/app/store/matDmd/drgtSttn/index.tsx +++ b/apps/wr/app/store/matDmd/drgtSttn/index.tsx @@ -14,10 +14,12 @@ export type drgtState = { drgtHeadList: CommonTableProps[]; drgtRes: customDrgt[]; drgtMoRes: customDrgt[]; + isInit: boolean; } } type drgtActions = { + drgtInit: () => void drgtSearch: (fromDate: string, toDate: string, sido: string, sgg: string) => void drgtModalSearch: (fromDate: string, toDate: string, sgg: string) => void }; @@ -29,72 +31,105 @@ export const drgtStateExport: drgtState = { drgtHeadList: drgtHeadList, drgtRes: drgtRes, drgtMoRes: drgtRes, + isInit: false, } } +const initFetch = async (fromDate: string, toDate: string, sido: string, sgg: string) => { + + let searchUrl = ""; + + if (sido === "" || sido === "전체") { + searchUrl = `/il/getDataCount/${fromDate}/${toDate}`; + } else { + searchUrl = `/il/getDataCount/${fromDate}/${toDate}/${sido}/${sgg}`; + } + + const drgtData = await getFetch( + fetchType.WR, + searchUrl + ); + + //주의, 관심, 정상 추출 + const statusData = Array.from( + new Set(drgtData.map((e) => e.lvngMfgUswtrDrgtInfo)) + ); + + const statusMap = drgtData.reduce((acc: any, { sggNm, lvngMfgUswtrDrgtInfo: status, countDrgt }: { sggNm: string; lvngMfgUswtrDrgtInfo: string; countDrgt: number }) => { + if (!acc[sggNm]) { + acc[sggNm] = { sggNm, ...Object.fromEntries(statusData.map((e) => [e, 0])) }; + } + acc[sggNm][status] = Number(countDrgt); + return acc; + }, {}); + const data = Object.values(statusMap); + + const formatStatus = data.map((item) => { + const attention = item['관심'] || 0; + const caution = item['주의'] || 0; + return { + sggNm: item.sggNm, + normal: String(item['정상'] || 0), + caution: String(caution), + attention: String(attention), + warning: String(attention + caution), + }; + }); + + const ranking = Array.from( + new Set(formatStatus.map(e => Number(e.warning))) + ).sort((a, b) => b - a); + + const rankData = ranking.reduce((acc, val, idx) => { + acc[val] = idx + 1; + return acc; + }, {}); + + const drgtRes = formatStatus.map(item => ({ + ...item, + warning: String(item.warning), + ranking: rankData[Number(item.warning)], + })); + + return drgtRes; + +} + + export const drgtActionsExport: (set: any) => drgtActions = ( set: any ) => { return { + drgtInit: async () => { + try { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const formatted = `${year}${month}`; + const fromDate = formatted; + const toDate = formatted; + + //FIXME: 페이지 처음 로딩될때 데이터 보려고 6월로 박아놓음 이거 fromDate로 고치기 + const drgtRes = await initFetch("202506", toDate, "", ""); + set((state: drgtState) => { + return { + ...state, + drgt: { + ...state.drgt, + drgtRes: drgtRes, + isInit: false, + }, + } + }); + } catch (e) { + console.log(e) + } + }, drgtSearch: async (fromDate: string, toDate: string, sido: string, sgg: string) => { try { - let searchUrl = ""; - if (sido === "" || sido === "전체") { - searchUrl = `/il/getDataCount/${fromDate}/${toDate}`; - } else { - searchUrl = `/il/getDataCount/${fromDate}/${toDate}/${sido}/${sgg}`; - } + const drgtRes = await initFetch(fromDate, toDate, sido, sgg); - console.log("searchUrl", searchUrl); - - const drgtData = await getFetch( - fetchType.WR, - searchUrl - ); - - console.log("drgtRes", drgtData) - - //주의, 관심, 정상 추출 - const statusData = Array.from( - new Set(drgtData.map((e) => e.lvngMfgUswtrDrgtInfo)) - ); - - const statusMap = drgtData.reduce((acc: any, { sggNm, lvngMfgUswtrDrgtInfo: status, countDrgt }: { sggNm: string; lvngMfgUswtrDrgtInfo: string; countDrgt: number }) => { - if (!acc[sggNm]) { - acc[sggNm] = { sggNm, ...Object.fromEntries(statusData.map((e) => [e, 0])) }; - } - acc[sggNm][status] = Number(countDrgt); - return acc; - }, {}); - const data = Object.values(statusMap); - - const formatStatus = data.map((item) => { - const attention = item['관심'] || 0; - const caution = item['주의'] || 0; - return { - sggNm: item.sggNm, - normal: String(item['정상'] || 0), - caution: String(caution), - attention: String(attention), - warning: String(attention + caution), - }; - }); - - const ranking = Array.from( - new Set(formatStatus.map(e => Number(e.warning))) - ).sort((a, b) => b - a); - - const rankData = ranking.reduce((acc, val, idx) => { - acc[val] = idx + 1; - return acc; - }, {}); - - const drgtRes = formatStatus.map(item => ({ - ...item, - warning: String(item.warning), - ranking: rankData[Number(item.warning)], - })); set((state: drgtState) => { return { ...state, @@ -115,10 +150,7 @@ export const drgtActionsExport: (set: any) => drgtActions = ( `/il/getData/${fromDate}/${toDate}/${sgg}/${sgg}` ); - console.log("drgtMoData", drgtMoData) - const drgtMoRes = drgtMoData.map((item) => { - console.log("item", item) const year = item.anlsYmd.slice(0, 4); const month = item.anlsYmd.slice(4, 6); const drgtState = item.lvngMfgUswtrDrgtInfo; diff --git a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx index 1be95fae6..102292ccf 100644 --- a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx +++ b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx @@ -4,24 +4,38 @@ import { btnClass } from "@/app/store/common/code" import type { customDrgt } from "@/app/store/matDmd/drgtSttn" import { drgtDetailHeadListExport } from "@/app/store/matDmd/drgtSttn/headList" import { excelDownload } from "@/components/lib/func/commom" -import { Title, UiTable } from "@common/business_components/ui" +import { downloadExcel, Title, UiTable } from "@common/business_components/ui" import ListDiv from "@common/business_components/ui/list_div" -import { Button } from "@common/components/ui" -import { useCallback, useEffect } from "react" +import { Button, ScrollArea } from "@common/components/ui" +import { useCallback, useEffect, useState } from "react" export default function DrgtDetailModal({ data, }: { data: customDrgt; }) { - const excelClick = useCallback(() => { - excelDownload( - [[]], - "우리동네가뭄", - "sample_drgtSttn.xlsx", - "A1:H1" - ); - }, []); + const [excelData, setExcelData] = useState<any[]>(); + const excelClick = () => { + downloadExcel({ + data: excelData ?? [], + fileName: "우리동네가뭄 상세현황", + sheetNameArray: ["Sheet1"], + headerRange: "A1:C1" + }) + } + + useEffect(() => { + const resData = Array.isArray(data) ? data : [data]; + const exData = Array.isArray(resData) + ? resData.map((item: customDrgt) => { + return { + "년": item.year, + "월": item.month, + "가뭄상태": item.drgtState, + } + }) : []; + setExcelData(exData) + }, [data]) return ( <> @@ -43,15 +57,17 @@ export default function DrgtDetailModal({ <div className="grid grid-cols-6 gap-4 "> <div className="col-span-6"> <ListDiv> - <UiTable - headName={[]} - tableData={data} - headlist={drgtDetailHeadListExport} - pageSize={10} - total={10000} - cellClick={(index: number | undefined, headId: string) => { }} - onPageChange={() => { }} - ></UiTable> + <ScrollArea className="h-[63vh]"> + <UiTable + headName={undefined} + tableData={data} + headlist={drgtDetailHeadListExport} + pageSize={10} + total={10000} + cellClick={(index: number | undefined, headId: string) => { }} + onPageChange={() => { }} + ></UiTable> + </ScrollArea> </ListDiv> </div> </div> From d16645d0aa7b56319ab25832d52377f40bfede92 Mon Sep 17 00:00:00 2001 From: jyha00 <jyha@mind-one.co.kr> Date: Tue, 1 Jul 2025 16:34:43 +0900 Subject: [PATCH 14/22] =?UTF-8?q?=ED=95=98=EC=88=98=EC=9E=AC=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=EC=8B=A4=EC=A0=81=20=EB=93=B1=EB=A1=9D=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=ED=99=94=EB=A9=B4=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20merge=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EC=86=8C=EA=B3=84=20=EB=B0=8F=20=EC=B4=9D=ED=95=A9=EA=B3=84=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prfmncReg/pbnstPrfmncReg/page.tsx | 38 +++-- .../prfmncReg/pbnstPrfmncReg/headList.tsx | 27 ++-- apps/wr/components/lib/func/TableData.tsx | 151 ++++++++++++------ .../pbnPrfExcelModal/pbnPrfExcelModal.tsx | 123 +++++++------- apps/wr/public/sample_pbnstPrfmncReg.xlsx | Bin 13777 -> 13806 bytes 5 files changed, 208 insertions(+), 131 deletions(-) diff --git a/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx b/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx index 6be50a8c5..10dd7ccb6 100644 --- a/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx +++ b/apps/wr/app/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx @@ -25,7 +25,7 @@ import { } from "@common/components/ui"; import { useState, useEffect, useCallback } from "react"; import { set } from "react-hook-form"; -import { Toaster } from "react-hot-toast"; +import toast, { Toaster } from "react-hot-toast"; import { headMakeprfmnc } from "../chkgdnReg/TableHead"; import { InputTableBody } from "@/components/lib/func/TableData"; import { @@ -51,8 +51,8 @@ export default function prfmncPrfmncReg() { }>({ isModal: false, data: {} as FcPrfType }); const [inputData, setInputData] = useState<any[]>(); const [pbnstRuseFcltPk, setPbnstRuseFcltPk] = useState<string>(); - const [pageNation , setPageNation] = useState<number>(1); - + const [pageNation, setPageNation] = useState<number>(1); + const [cellNum, setCellNum] = useState<number>(); const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) ); @@ -76,7 +76,7 @@ export default function prfmncPrfmncReg() { ); prfmncHasuInit(String(pageNation), "41"); } - }, [inputData, searchYear, pbnstRuseFcltPk,prfmncHasuInit,pageNation]); + }, [inputData, searchYear, pbnstRuseFcltPk, prfmncHasuInit, pageNation]); @@ -88,10 +88,16 @@ export default function prfmncPrfmncReg() { isModal: false, data: prfmncRes[index] || ({} as FcPrfType), }); - const inUsageData = structuredClone(initValInUsage); - const outUsageData = structuredClone(initValOutUsage); - const inSubtotal = structuredClone(subTotalObj); - const outSubtotal = structuredClone(subTotalObj); + const inUsageData = structuredClone(initValInUsage).map(row => ({ + ...row, + 구분: "장내용수", + })); + const outUsageData = structuredClone(initValOutUsage).map(row => ({ + ...row, + 구분: "장외용수", + })); + const inSubtotal = { ...structuredClone(subTotalObj), 구분: "장내용수" }; + const outSubtotal = { ...structuredClone(subTotalObj), 구분: "장외용수" }; if (prfmncRes[index]?.prfList) { prfmncRes[index].prfList?.forEach((prfItem) => { const month = Number(prfItem?.ym?.slice(4)); // "MM" @@ -127,7 +133,7 @@ export default function prfmncPrfmncReg() { }); inUsageData.push(inSubtotal); outUsageData.push(outSubtotal); - const usageTotalObj: { [key: string]: any } = structuredClone(totalObj); + const usageTotalObj: { [key: string]: any } = { ...structuredClone(totalObj), 구분: "총합계", 용수명: "" }; for (const key in usageTotalObj) { if (["usage", "gubun", "edit"].includes(key)) continue; const val = @@ -162,7 +168,7 @@ export default function prfmncPrfmncReg() { <ContentsDiv> <div className="grid grid-cols-4 gap-4"> <div className="col-span-4"> - <Title title={"하수재이용 시설 등록"} /> + <Title title={"하수재이용 실적 등록"} /> <SearchDiv> <SearchFormLeft> <div className="col-span-2 flex flex-row space-x-2"> @@ -197,7 +203,7 @@ export default function prfmncPrfmncReg() { <SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2 "> <div className="space-y"> <div className="flex flex-row space-x-2"> - <Button size={"sm"} onClick={(e) => {}}> + <Button size={"sm"} onClick={(e) => { }}> 조회 </Button> <Button @@ -211,7 +217,12 @@ export default function prfmncPrfmncReg() { <Button size={"sm"} onClick={() => { - setIsExcelOPen({ ...isExcelOpen, isModal: true }); + if (cellNum === undefined) { + toast.error("하수처리재이용시설을 선택해주세요.") + return; + } else { + setIsExcelOPen({ ...isExcelOpen, isModal: true }); + } }} > 파일등록 @@ -238,6 +249,7 @@ export default function prfmncPrfmncReg() { headName={"하수재이용시설 목록"} cellClick={(index: number | undefined, headId: string) => { cellClick(index, headId); + setCellNum(index ?? 0); }} onPageChange={setPageNation} children={undefined} @@ -266,7 +278,7 @@ export default function prfmncPrfmncReg() { </div> </ContentsDiv> <UiModal - bodyClass={""} + bodyClass={"w-[30vw] max-w-none"} body={<PbnPrfExcelModal data={isExcelOpen.data}></PbnPrfExcelModal>} open={isExcelOpen.isModal} onOpenChange={() => { diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx index 544f07516..abde1778f 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/headList.tsx @@ -4,7 +4,7 @@ import type { TableUpperProps } from "../../common/type/header"; export const pbnPrfRegExcelZod = z.object({ ym: z.string().default(""), bscFctNm: z.string().default(""), - pbnstRuseFcltFk:z.string(), + pbnstRuseFcltFk: z.string(), pbnstRuseFcltNm: z.string().default(""), file: z.instanceof(File).optional(), }); @@ -18,20 +18,21 @@ export const pbnstPrfmncHeadListExport: TableUpperProps[] = [ ]; export const monthHeadListExport: TableUpperProps[] = [ + { id: "구분", title: "구분", mergeCell: true }, { id: "용수명", title: "용수명" }, { id: "total", title: "계" }, - { id: "1월", title: "1월" ,edit: true }, - { id: "2월", title: "2월" ,edit: true }, - { id: "3월", title: "3월" ,edit: true }, - { id: "4월", title: "4월" ,edit: true }, - { id: "5월", title: "5월" ,edit: true }, - { id: "6월", title: "6월" ,edit: true }, - { id: "7월", title: "7월" ,edit: true }, - { id: "8월", title: "8월" ,edit: true }, - { id: "9월", title: "9월" ,edit: true }, - { id: "10월", title: "10월" ,edit: true }, - { id: "11월", title: "11월" ,edit: true }, - { id: "12월", title: "12월" ,edit: true }, + { id: "1월", title: "1월", edit: true }, + { id: "2월", title: "2월", edit: true }, + { id: "3월", title: "3월", edit: true }, + { id: "4월", title: "4월", edit: true }, + { id: "5월", title: "5월", edit: true }, + { id: "6월", title: "6월", edit: true }, + { id: "7월", title: "7월", edit: true }, + { id: "8월", title: "8월", edit: true }, + { id: "9월", title: "9월", edit: true }, + { id: "10월", title: "10월", edit: true }, + { id: "11월", title: "11월", edit: true }, + { id: "12월", title: "12월", edit: true }, ]; diff --git a/apps/wr/components/lib/func/TableData.tsx b/apps/wr/components/lib/func/TableData.tsx index dbb3aa7a6..39c8b7739 100644 --- a/apps/wr/components/lib/func/TableData.tsx +++ b/apps/wr/components/lib/func/TableData.tsx @@ -174,8 +174,6 @@ export const chRowspanTable = ( ); }; - - export const headVisibleChange = (headList: TableUpperProps[], isEdit: boolean) => { return headList.forEach((head) => { if (head.edit !== undefined) @@ -187,63 +185,124 @@ export const headVisibleChange = (headList: TableUpperProps[], isEdit: boolean) }); } - const handleChange = ( rowIndex: number, field: any, value: string | number, - setInputData: (data:any) => void + setInputData: (data: any) => void ) => { - if(Number(value) < 0 ||! Number(value)){ - value = 0; - }else{ + if (Number(value) < 0 || !Number(value)) { + value = 0; + } else { value = Number(value); } - setInputData((prevData:any) => - prevData?.map((row:any, index: number) => { - if (index === rowIndex) { - return { - ...row, - [field ]: value, - }; - } - return row; - }) - ); + setInputData((prevData: any) => + prevData?.map((row: any, index: number) => { + if (index === rowIndex) { + return { + ...row, + [field]: value, + }; + } + return row; + }) + ); }; ; +export const InputTableBody = ( + chgRes: any[] | [], + headList: TableUpperProps[], + setInputData: (data: any[]) => void +) => { + // mergeCell 처리용 + const getRowSpan = (data: any[], key: string, rowIndex: number) => { + if (rowIndex > 0 && data[rowIndex][key] === data[rowIndex - 1][key]) { + 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; + }; - -export const InputTableBody = (chgRes: any[] | [] ,headList:TableUpperProps[], setInputData: (data: any[]) => void) => { - return ( + return ( <> - {chgRes.map((data, dataIndex) => ( - <TableRow key={dataIndex + "row"}> - {headList.map((head, headIndex) => ( - <TableCell key={head.id + headIndex} className="w-36" > - {head.edit ===true && data.edit !==false? - <Input - className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`} - onChange={(e) => - handleChange(dataIndex, head.id, e.target.value,setInputData) - } - value={data[head.id as keyof any] ?? ""} - /> : ( - <div - className={` - ${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} - !text-xl w-full h-10 - `} + {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) => { + // 총합계 행이면 "구분"에서 colSpan=2로 병합 + if ( + data["구분"] === 0 && + head.id === "구분" + ) { + return ( + <TableCell + key={head.id + headIndex} + colSpan={2} + className="font-bold text-center bg-[#d5e1eb]" > - {data[head.id as keyof any] ?? ""} - </div> - ) + 총합계 + </TableCell> + ); + } + // 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음 + if ( + data["구분"] === 0 && + head.id === "용수명" + ) { + return null; + } + // mergeCell 처리 + if (head.mergeCell) { + 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}`}> + {data[head.id as keyof any] ?? ""} + </TableCell> + ); } - </TableCell> - ))} - </TableRow> - ))} + // 기본 셀 + return 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`} + onChange={(e) => + handleChange(dataIndex, head.id, e.target.value, setInputData) + } + value={data[head.id as keyof any] ?? ""} + /> + </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 + `} + > + {data[head.id as keyof any] ?? ""} + </div> + </TableCell> + ); + })} + </TableRow> + ); + })} </> ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx index 28dc882c3..fb6ba9b22 100644 --- a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx @@ -52,6 +52,7 @@ export default function PbnPrfExcelModal({ data }: { data: FcPrfType }) { useEffect(() => { if (files) { + console.log("이런") const file = files[0]; if (form.getValues("ym").length < 1) { ToastFormUi({ @@ -62,11 +63,12 @@ export default function PbnPrfExcelModal({ data }: { data: FcPrfType }) { return; } - if (file) + if (file) { filePicked(file, 0).then((res) => { if (res !== undefined) - setExcelData(excelToPbnPrfData(res as unknown as JSON[], form.getValues("ym"),form.getValues('pbnstRuseFcltFk') ) as PrfType[]) + setExcelData(excelToPbnPrfData(res as unknown as JSON[], form.getValues("ym"), form.getValues('pbnstRuseFcltFk')) as PrfType[]) }); + } } }, [files]); @@ -75,82 +77,85 @@ export default function PbnPrfExcelModal({ data }: { data: FcPrfType }) { <div> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> - <div> + <div className="flex justify-between items-center"> <Title title={"하수재이용 실적등록"}> -
-
-

- {"점검정보 입력"} -

- -
+

+ {"점검정보 입력"} +

-
+
( field.onChange(val)} /> )} /> - - ( - - - - )} - /> - ( - - - - )} - /> - - ( - - { - if (files) setFiles(files); - }} - fileTypes="xlsx" - maxFiles={1} - > - - )} - /> +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+
+ ( + + { + if (files) setFiles(files); + }} + fileTypes="xlsx" + maxFiles={1} + > + + )} + /> +
diff --git a/apps/wr/public/sample_pbnstPrfmncReg.xlsx b/apps/wr/public/sample_pbnstPrfmncReg.xlsx index 67f0d559e096443366eeff0f951b5648e422a972..1f2ff81aa24e1dce6ca8e8bce7fd7d9f54ed91ad 100644 GIT binary patch delta 5703 zcmZ8lWmFVUw;ejAyBRR0lvY|mTDofhhZboVQW~T|QbKa*l#Y?^4oT_f_ug7> z`QH6??^^qub$*<4?%sRS;l!av7z>L6jPM9T1_0JzQdm?_5~WD>eQTvfm64f=i$QTS#AMS_lT|vu?>V+=u_$x9fsEmQ}y= zw(*M&N=I&w6Nv&<0d|Wm7vI7BkioFehB0w?rwx$7N%Uw`5B3i+8fA*cJ&R4~IAuF; z*mv;&n6!n^(R84@i8}um%XUT_j`yr790TaZ_B#i%+F5%sIxr8Bv$nWQ%a>ZA1Z7h2 z1iN9(<&>|ZWuO<41vJ!o)dG5Zq(&M~s5}Gqrexf}3uK1k7y%D^nM4@ugUidW-EZ*B zc^|K_DI-MkJQR~>_=Tte+rrE)y@&qaPDL2;G1xbum(7XGa0NH|-78KSF&YN}jr4q# z7;#Vv&fQ1|M|5_Q+rm{3=qv&JVBXi^PV{9XDN_z)j)so`&fmSiQo*#(q%KDl_GAu6 zdeK?&Tn#Zi(tZi2-3@xhl~IsD^4+lqsUcn7xBRAdC`;!zMQp)PV#JvfSS4jUNlh9{ z(y$b&qOBpu1QQzituFN}>#26IXM)+8zYqgWvk)yMzDu7Be`2?n^h#w_V|FKGVvC>t z{=1tex2D&M4;!+~b0p&6!=7o4-f(m0GLc;X&OHD-WjQ3i@dzUPjBz^QT>0#muSuQ|gSQJ*2a>fFC|B=3zbEaoLrqjF@ zf?nTfC`^~)Qsp6kCGI1kD!SdB^d9QOaJUHS(TW?38j)Htd4!lhUhTT1Nj-g~vu0!U zbfQDI2O2VHhg`&Tz0cceIOM<*Ly{|3(U)oaocG*(@IZYRhbnWaXEZ^QlIBt)w9&kEGEV+l27 z#W|#0oN*^cVh#&2f_fL0Opp^crB1hB8iPcWZQ!K*dZUd>_X(%Pp}xv;1eO zWP(d^KU0(dmJV75?~Lxb>^^i&M^=_TUMFNHi?O`Uu)VOIc*XUMKiaV+C`~)tph5sd zTiV%QocvbYdny0-2T`yyU`^51@+i(Fz-;` zK+3x>GAqkq49fZ$eW`v~BWVc|dG@-xs)0C$g+GL}ozXW53#@*+kLE!S;Ic{bvpQI~ zwd|gHlv|$~Ulq1jHMgeOIHWA2bU$V|Z5fxc?cJ3Y+u099T!rAAlO4@$v@a7azW3~! zO9{^Z=8RiuKOa>boIk7|o<{}Gu@RgsYomnHSRKYn_feWsdL2-syfW)}%Ntz|@=kGR zB0P8{BD?Ovgt}Y#%AEkZ52kOykgzIGYCcrE%E`oe8__OVbpi``%VNDMbtTQa@i{vb z4wGx+Zm*ylhN+!?GL|Krdb$9j+CAG5h2Y;TzQz`f&J|`d;g$_t;CNF_T9#=srBl^1 z_;kO_tKVtXU@(>%rrNFMRcgwSNy{K$BE(Ydtf4%+Fw=LYh?5TG>7z8#@oV&>K#V<- zi?WQ+gftmlk&5l=L`NR&Jf++@Uwb`cl$YwW1nhLr8etP5R&&(wFs5Omu6HY6Pd&k+ zp6KgV2Vu*bL!MOlZC&7}Z?oo)kx%6(=yF9nKA%{^c`8c>_0}{qTLzt9;08R#;?*oJ zV91Pvu>;3HgLA;Ap8ln$m;TukpXH>|uYbT`AUxxR9!cSQUY~ z{-u&Q8X@bn%uBzBou}>TpHb=!3F2|EP=eO=^)LVcG1zbF*U%B*QO654KO_s~8trOF8^l?VrEp&b0*+ZspX)n5H(%03 zs{SR+6}4`*FUa*)L9B%lc~JgyJ*;DmYzfJv7b*^QVNg-Z>|BLJsp6rCQT86{E(igb zDy5|0kH(ms){%^ouB9SabxPcyg$EJz!!{UlWi~D1BsZ|rVOu-4+U)?mwM7^v>7Lh> zJ#(xeCrMU2?EP4ML|xK~iWs@^*C`^P-fKJ5YOWpDYB7`D`W4Kt>Y8U;iXzJb!jE(4IT)FX0*blDOup_+ zAM7JgVC8-SzK!VKr(77JbC@}8X&>jsLtY>4P916KXfV#)lg75~%UjZ?Vmp@(zLDPOL*cP1~TqAJnZ(FDPgG)VbEiOEZ zV@EDBBEG28G8P`6AXa=e7G-r7Z3PB)4ArUiV`6t31C^aCe4beRyccE3q`3rPgX`uc zvC-3G>n-Zk7@Ut`uD+0o{%zv!^?ey?jCv1`x&K_!@M8u>2sgZ*P}W^H>@1(-?&6_* ztWS!-C${jH_je+kBY`@sFm1y&ulHpb*A*tRB6xEX;@=@%buwP3rTixAF?a#E90t>- ztk;79;no~Zx*d%i(wC-vEs0+5icuIPN(ZTK)Z{!v0whYQa;@ld$|uu=0(3X)D+x=f z8d)mBl-?4sP7;zTT@%r3%P{58Y0E(6a%i<>EOY+8LrrP4Tb4V&Vfoc-(qXP|56PwM zzm!wUK^mqjMwxcXrTmnWN#dE&&%L!`fBwhmda+&d-9l&lD5-k?NBnKVX*$7-s6oiY zphWEG{2+2H*UTU=mTPXXmCJG;V zF-5xh`?%n6fbwO}Lvz;0a<1udg2azFi97tH;n04u8KP?9k$!mrYT@~s9*pdXX{^9( z&;*uTHfRdVGTXJB_@`%g3sj?7lrb3@|FE9K*XR;!%uNN4wT`~>3Q1|^a+U`1qodiSDqpgP1*uS@rJRnG<%dxj=6Dy>XbjlVZnk7+4$c& zYXQoRaOW#IM{RHhcZBq-Q>jz3Unk9K+f0gU`W)CoRv1X!P1jJ%q$}= z*|GH3d#dg@H_$s=jbpO(x-Odkm)c`&eNE}Sk}sLKDxA4Ic{qknF)kKLOwC&%;<@hF zfPfgHFMdR6tbvs5Io^}}c}w685EsT1?Db;I(W3bpTv^Po|wl%$p+0}F=d11uu8<&)mXC*!=j00#jf>9 zt1dA~18a=?A60$6=Yy1jFiyoa>{*k!QyNgqT{`-Zl|$t1tw+q z#;l*DeX%z>Z#y z^<^LWLBHa1ZcjerTMERetU3kV$gVU+(a5ecMdSNahMxGmDtXCQcVIvRrqi|I2b2L- z5kA&4M%=|5T13|*EYo{BpG!iO@z-_WLZaV41xI6A#+g9tUq@vnkB=$%&8kF`Lo%Ft z3{U8ss*jG>m${d<%mYAemio}!9r(CagK-Zp+W}WN=cR6K)&7-)B<$lVd>p4S+}Lq3 z*IX;Ubt_{u>0E44FZ+d|P};s0QR-0#t+NumW?_=k8`66BcE;81=d*E7JQrpFV($uk z`F7?ZckYy_%*;F6q>gFZ@|sxu6EKfWpt;WFqXM6>@R>(E2b!3t=qPASXv7uKJF)b< zXL&C5mwx}{?>ShB3b2+ zo&G;hnZ<6hh!K7;;mKfUaD)O0jS-A*N@X;Pnz?LEpSLi+iO;=MapQ{}p$S}U84f<^ zfnpF2eYc5dCXPDXauPkcpDX-ePCr_wLCOhQrQ<*&(twU6)9@4 zA&9ZiOVvWFc6p!7(tlXl0mhh#v@9ho*HApUWTQ_krAm~ria3Y+n3ii$3$2`-53@*} zUYsKy6=0d1-o|J7J^l&DLt!=}Yb)Rz=vp0b@Pe^tUIz9`_ZdOG+K8gJyBU14lnW2P zv5+jk-g?|gPC$|?zvzX42ucqf16JDGZ8Lk1{M*AwK(`D=4V@=I6B!rqq-NfW5zAuO z8yClVvQA`?8afiGile1>WqsV>)rV8HJv7WK<)=s=a0?+{Xm!ZbPd9Cb!K+E3OU*66 zzNO{n$w#vW)pWvCaDWu( zKmo0*D(TrK6kRA)DMT4h?-fa9fGC?^Y0WWDD93e)uB{H}jTTd*Z6WIA1i4$fG^n5j z7niE{*Z0BY_du#W+0##{s}zs6C{o;nqki;JJk(?h3dC+mwbcWzp}wu7EAs@HaAecx zL5Oaz#=E>?H1Y-Dp=Y4S(CA<7nk~0pXu45_Otg&%46|B!VE0U=($uUrv_2A$b9}e6 znW(86pnP4$7TNifN~Gp`-~y;WWT8Mo3j$8hNx?AL1)<}b#4;pLs*{*cF|6^u#A&i4 zns&{}WTQ!qt=>56Yw)7%?g(q2`quAgmfgXvpOv1`@#!R7FHWHEyUjKfIy0QZ$qEWs z>|KQgE_RL=MONDaPY;DL=#lAUDQHmSmCb1_2Q8!KC2ZLB*<;OVyha(L(Pl)Q&)E?~ zJwt~BzED@zTO>I-3V3FwfXIRJ8joApBC*7L%Q|nzc&^7TUK-#kYtx3w+DO|Iyi>QL7 z2Rmc4xjR6!tdh4TN#~tO8l2YkwTWmz9>`n zj5{Ni&Y0mkLpHM7onOJ2MOFqKm}>rJ3?iXPrHguAnK<>=x*5P6lg}bzc&ubS5X; z#`-BN6uhB=|H4Sa^;lc#Gudg&mSVilAoPl)-S#X}=gVne(km2e!ZNoCB@ak0?Mago zeNy=0Fs$D;FeG67I{eVnXch~EM3fy{uB#ZS;l zXQ*L80Qo?RM~LYH^wrPD+sPr@405nUU@`A-8sVlOVt@YP_Esdh4zIQOzJ7Cj7pe%& zVmZMZcG_?jn9io3Gmrj00aR)*&?GJ|uwHh_3o96&MdNR;|)(@_g;1Q*x+PK zmaHOX=B41P-9i=o*TeH-4Aqh4)RJjgQ4tuh+x41xip+{7vfD}C&iieb&ScGI<`Hb- zO6p96t^3k$`Cw&TDeSId_Q!1@AK6{dbLitp;sbWQ3zsi4nw-9xykY$jk026KBa9J5 z0#$C^#LNm};Y`iK7Dd&Vig?Sw=(p)NzS>49Mp><}E*T>1$Nq){9o3J)aLA)YZ8AXk zw`PJZS4tLHQ5cC+SDu@%O|2SR@V#SRk=WXN9x3E-k(Cd{pYFn`C-hmh@z zW5iB*L@hZdXWCIWjqg;Ff^U9pW!ILM=TQmry?Y{9)!?np)P8(7*J=?|7gnEcm)AIh z7`UI_JzSh0@d)vsg@mFBc^{;<4+iHt)wLjHudRAvIMg-CujRZZZbJABkF7me2=mGq z$`>SRMtme08)Wmgb%p3RE&H35IN(?;KA3IPYIc7S^^~7tIsq8M{GXMqR$*3rh=1Hu zMSj7T|7%x~0|0*wt$)re4glcm$m`+h=V%Sn79@pz=chulhfVNnAvwVq1+|U+}=} Zg@FIlFR(-*Qlvky5+P>vNWTBX{udqJuh;+p delta 5698 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 Date: Tue, 1 Jul 2025 17:18:59 +0900 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20SN(=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C)=20GIS=20=EA=B3=B5=ED=86=B5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 19 +++++++++---------- .../src/ui/map/lib/gis.core.js | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 8b8acf1cb..fa1cf7ab4 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,20 +1,19 @@ -import './lib/css/gis.common.css'; -import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { Search } from 'lucide-react'; import OlMap from 'ol/Map'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; import { defaults } from 'ol/interaction'; import * as olProj from 'ol/proj'; -import proj4 from 'proj4'; import { register } from 'ol/proj/proj4'; -import GisApp from './lib/gis.core'; -import GisContent from './GisContent'; -import GisTree from './GisTree'; +import proj4 from 'proj4'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import GisLonLat from './GisLonLat'; +import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; -import GisMapToggle from './GisMapToggle'; -import { Search } from 'lucide-react'; +import GisTree from './GisTree'; +import './lib/css/gis.common.css'; +import GisApp from './lib/gis.core'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); @@ -122,7 +121,7 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa projection: GisApp.TO_PROJ, zoom: gisCore.defaults.zoom, center: olProj.transform( - [126.6757, 37.5585], + [127.4875, 37.4916], GisApp.FROM_PROJ, GisApp.TO_PROJ ), @@ -242,4 +241,4 @@ function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = fa ); } -export default React.memo(Gis); \ No newline at end of file +export default React.memo(Gis); diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index db58c898f..ddfcec3a1 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -94,7 +94,7 @@ GisApp.core.prototype = { target: '', centerX: 37.2806094, centerY: 127.0032502, - zoom: 12, + zoom: 15, minZoom: 6, maxZoom: 20, }, From 9b07952340e11f275d7aa12321c74b09bcb7613a Mon Sep 17 00:00:00 2001 From: yschoi Date: Tue, 1 Jul 2025 19:09:45 +0900 Subject: [PATCH 16/22] =?UTF-8?q?=ED=95=98=EC=88=98=EA=B4=80=EB=A1=9C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9D=EC=8B=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/middleware.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 3fabc529d..5acc41863 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -6,6 +6,7 @@ const CLAIM_NAME = "Authorization"; const AUTH_USER_ID = "token-id"; const serverApiUrl = CS_URL; const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; @@ -55,9 +56,17 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + 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)); + } + return NextResponse.next(); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*"], + matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"], }; From 9cf281db7e9c139a21a9383caa99f31621e99c1e Mon Sep 17 00:00:00 2001 From: yschoi Date: Tue, 1 Jul 2025 19:32:25 +0900 Subject: [PATCH 17/22] =?UTF-8?q?gis=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 67 ++++--------------- .../src/ui/map/lib/gis.core.js | 21 +++--- .../src/ui/map/lib/gis.layer.js | 10 ++- 3 files changed, 31 insertions(+), 67 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 8929a3124..6d46e77aa 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,6 +1,5 @@ import { Search } from 'lucide-react'; import OlMap from 'ol/Map'; -import Overlay from 'ol/Overlay'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; import { defaults } from 'ol/interaction'; @@ -19,22 +18,7 @@ import GisApp from './lib/gis.core'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ - isOpen, - gisMaps, - isSearchModalOpen, - treeData = [], - isSearch = false, - isMiniMap = false, - setIsOpen, - setFeatureData, - featureData, - searchView: SearchView, - detailView: DetailView, - onFeatureSelect, - setIsSearchModalOpen, - onCheckedLayersChange -}) { +function Gis({ isOpen, gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView: SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); @@ -44,17 +28,13 @@ function Gis({ const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); const mapInstanceRef = useRef(null); const checkedLayerIds = useRef([]); - // 체크된 레이어 상태 관리 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); - let gisMap = null; const closePopup = () => { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; - // Feature Click Event + // SearchModal, GisContent 등에서 호출 const handleFeatureSelect = useCallback((data) => { gisMap = mapInstanceRef.current; @@ -109,10 +89,8 @@ function Gis({ setFeatureData(data); setIsOpen(true); setIsSearchModalOpen(false); - onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef]); - // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 @@ -126,7 +104,7 @@ function Gis({ ]); register(proj4); - // OpenLayers 지도 객체 생성 + //맵 생성 const map = new OlMap({ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], renderer: 'webgl', @@ -154,7 +132,7 @@ function Gis({ gisMaps.current = map - // 미니맵 추가 + //미니맵 if (isMiniMap) { const overviewMap = new OverviewMap({ layers: [GisApp.Module.core.baseMiniMapLayers['base']], @@ -169,7 +147,6 @@ function Gis({ map.addControl(overviewMap); } - // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer('base'); @@ -177,20 +154,16 @@ function Gis({ mapInstanceRef.current = map; // 생성 완료 플래그 설정 }, []); - // treeData 변경 시 레이어 코드 생성 useEffect(() => { if (!treeData || treeData.length === 0) return; - // 기존 오버레이 및 레이어 모두 삭제 gisCore.deleteAllOverlay(); gisCore.deleteAllLayer(); - // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); - GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 + GisApp.layerCode = layerCode; //setTreeData(data); - // 편집 가능한 레이어 목록 생성 const layerItems = getLayerItems(layerCode); //setEditLayerItems(prev => [...layerItems]) }, [treeData]); @@ -220,45 +193,31 @@ function Gis({ } } - // 체크된 레이어 정보를 처리하는 함수 - const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); - }, [onCheckedLayersChange]); - return ( <>
- + {/* */} - {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} {isOpen && featureData && !isSearchModalOpen && ( )} - {/* SearchView가 존재할 때만 렌더링 */} - {SearchView && ( - - )} + - +
{isSearch && ( diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index 7488e4b72..ddfcec3a1 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -1,24 +1,26 @@ import MousePosition from 'ol/control/MousePosition'; import ScaleLine from 'ol/control/ScaleLine'; -import { getTopLeft } from 'ol/extent'; import Feature from 'ol/Feature'; -import GeoSJON from 'ol/format/GeoJSON'; import WKT from 'ol/format/WKT'; -import { Point } from 'ol/geom'; import LinearRing from 'ol/geom/LinearRing'; import Draw from 'ol/interaction/Draw'; import TileLayer from 'ol/layer/Tile'; -import VectorLayer from 'ol/layer/Vector'; import * as olProj from 'ol/proj'; import VectorSource from 'ol/source/Vector'; -import TileWMTS from 'ol/source/WMTS'; import XYZ from 'ol/source/XYZ'; +import GeoSJON from 'ol/format/GeoJSON'; import { Fill, Stroke, Style } from 'ol/style'; import CircleStyle from 'ol/style/Circle'; +import VectorLayer from 'ol/layer/Vector'; +import { Point } from 'ol/geom'; +import { getTopLeft } from 'ol/extent'; +import TileWMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; // import { get as getProjection } from 'ol/proj'; -import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; +import { register } from 'ol/proj/proj4'; +import olSourceImageWMS from 'ol/source/ImageWMS'; +import { GeoJSON } from 'ol/format'; import GisOlEditLayer from './gis.edit'; import GisOlLayer from './gis.layer'; @@ -560,7 +562,6 @@ GisApp.core.prototype = { * * Returns: */ - // 레이어를 지도에 표시/숨김 setLayerOnMap: function (layerIds, visible, addQurryFlter) { for (let i in layerIds) { let id = layerIds[i]; @@ -571,14 +572,10 @@ GisApp.core.prototype = { const layerCode = GisApp.layerCode[id]; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); - - // WFS 레이어 생성 및 지도에 추가 + //layer = this.addWmsLayer(layerCode); layer = this.addWfsLayer(layerCode); } } - - // 선택 인터랙션 및 스냅 기능 설정 if (GisApp.Module.select) { this.setSelectInteraction(); } diff --git a/packages/common/business_components/src/ui/map/lib/gis.layer.js b/packages/common/business_components/src/ui/map/lib/gis.layer.js index d9657c313..097c90ba5 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.layer.js +++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js @@ -77,7 +77,15 @@ GisOlLayer.prototype = { let lyrTyNm = options.lyrTyNm; let typename = this.storageNm + lyrTyNm; - if(options.lyrTyNm == "om_dt_untrt_swg_rain"){ //중점우수토실실 + if(options.lyrTyNm == "om_dt_untrt_swg_rain" //중점우수토실 + || options.lyrTyNm == "om_wqlt_station" //관측소 + || options.lyrTyNm == "tb_wr_bdst_ruse_fclt" //재이용시설 + || options.lyrTyNm == "tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || options.lyrTyNm == "tb_wr_rswtr_fclt" //중수도시설 + || options.lyrTyNm == "tb_wr_htdfl_fclt" //온배수시설 + || options.lyrTyNm == "tb_wr_tmpr_dmd_co" //잠재수요처 + ) + { url = GisApp.SN_GEOSERVER_URL; typename = lyrTyNm; } From 54140dbad5cbf7bc4cf9afc36ccc3d41fecee79e Mon Sep 17 00:00:00 2001 From: jyha00 Date: Tue, 1 Jul 2025 20:11:29 +0900 Subject: [PATCH 18/22] =?UTF-8?q?=EC=9A=B0=EB=A6=AC=EB=8F=99=EB=84=A4?= =?UTF-8?q?=EA=B0=80=EB=AD=84=20=EC=8B=9C=EB=8F=84,=20=EC=8B=9C=EA=B5=B0?= =?UTF-8?q?=EA=B5=AC=20=EC=A1=B0=ED=9A=8C=EC=A1=B0=EA=B1=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20=EC=8B=9C?= =?UTF-8?q?=EB=8F=84,=20=EC=8B=9C=EA=B5=B0=EA=B5=AC=20=ED=91=9C=EC=B6=9C?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/matMng/coMng/drgtSttn/page.tsx | 58 ++++++++++--------- .../wr/app/store/matDmd/drgtSttn/headList.tsx | 4 +- apps/wr/app/store/matDmd/drgtSttn/index.tsx | 36 +++++------- .../ui/modal/coMat/drgtDetailModal.tsx | 3 +- 4 files changed, 48 insertions(+), 53 deletions(-) diff --git a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx index 2f07dd6de..af49f02b1 100644 --- a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx @@ -3,14 +3,13 @@ import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; import type { customDrgt } from "@/app/store/matDmd/drgtSttn"; import { drgtHeadListExport } from "@/app/store/matDmd/drgtSttn/headList"; -import { excelDownload, getCity } from "@/components/lib/func/commom"; +import { getCity } from "@/components/lib/func/commom"; import DrgtDetailModal from "@/components/ui/modal/coMat/drgtDetailModal"; import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal"; import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; -import { MonthPicker } from "@common/business_components/ui/date_month_picker/mothPicker"; import ListDiv from "@common/business_components/ui/list_div"; -import { Button, DatePicker, Label, ScrollArea } from "@common/components/ui"; -import { useCallback, useEffect, useState } from "react"; +import { Button, Label, ScrollArea } from "@common/components/ui"; +import { useEffect, useState } from "react"; import { toast, Toaster } from "react-hot-toast"; export default function DrgtSttn() { @@ -23,7 +22,6 @@ export default function DrgtSttn() { const [tableData, setTableData] = useState(); const [selectedSgg, setSelectedSgg] = useState(''); const [excelData, setExcelData] = useState(); - const [num, setNum] = useState(); const [drgtModal, setDrgtModal] = useState<{ isOpen: boolean; data: customDrgt | undefined; @@ -53,7 +51,23 @@ export default function DrgtSttn() { }, [drgt.isInit, drgtInit]); useEffect(() => { - const resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes]; + let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes]; + resData = resData.map((item) => { + console.log("item", item); + if(item.sggNm === '세종시'){ + return { + ...item, + ctpvCd: '세종특별자치시', + sggNm: '' + } + } + + return { + ...item, + ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0,2) == ct.val)))?.text, + sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text + } + }) const data = Array.isArray(resData) ? resData.map((item: customDrgt) => { return { @@ -67,7 +81,7 @@ export default function DrgtSttn() { } }) : []; setExcelData(data) - setTableData(drgtRes) + setTableData(resData) }, [drgtRes]) useEffect(() => { @@ -76,27 +90,14 @@ export default function DrgtSttn() { } }, [drgtMoRes]); - const getCityVal = (val: string, sggCd: SelectDataType[]) => { - const data = sggCd - .filter((item) => String(item.val).substring(0, 2) === val) - .map((item) => ({ - val: item.val, - text: item.text, // 한국어 텍스트 - })); - return data; - }; - const getSelCity = (val: string | number) => { - const cityValue = String(val); - const data = getCityVal(cityValue, code.sggCd); + const cityValue = String(val); // Ensure val is treated as a string + const data = getCity(cityValue, code.sggCd); setSelectSgg(data); - - const sggVal = data.length > 0 ? data[0].val : ''; - setSelectedSgg(sggVal); // 시군구 value 세팅 - setObjSearch({ city: cityValue, sgg: sggVal }); + setObjSearch({ city: cityValue, sgg: "" }); }; + const getSgg = (val: string | number) => { - setSelectedSgg(String(val)); setObjSearch({ ...objSearch, sgg: String(val) }); }; @@ -181,19 +182,22 @@ export default function DrgtSttn() { size={"sm"} onClick={(e) => { let sidoText = ''; - let sggText = ''; + let sggText = []; if (objSearch?.city.toString().length > 0) { sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? ''; } if (objSearch?.sgg.toString().length > 1) { sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? ''; + } else { + sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text); + } if (sidoText === '세종특별자치시') { sidoText = '세종시'; sggText = sidoText; } - drgtSearch(fromDate ?? "", toDate ?? "", sidoText, sggText) + drgtSearch(fromDate ?? "", toDate ?? "", [sidoText, ...sggText]) }} > 조회 @@ -249,7 +253,7 @@ export default function DrgtSttn() { setDrgtModal({ ...drgtModal, isOpen: !drgtModal.isOpen }); }} bodyClass={"w-[50vw] max-w-none h-[80vh]"} - body={} + body={} /> diff --git a/apps/wr/app/store/matDmd/drgtSttn/headList.tsx b/apps/wr/app/store/matDmd/drgtSttn/headList.tsx index 1741b7456..dcdc655b5 100644 --- a/apps/wr/app/store/matDmd/drgtSttn/headList.tsx +++ b/apps/wr/app/store/matDmd/drgtSttn/headList.tsx @@ -2,8 +2,8 @@ import { z } from "zod"; import type { CommonTableProps } from "../../common/type/header"; export const drgtZod = z.object({ - ctpvCd: z.string().default(''), - sggNm: z.string().default(''), + ctpvCd: z.string().default('').nullish(), + sggNm: z.string().default('').nullish(), normal: z.string().default(''), attention: z.string().default(''), caution: z.string().default(''), diff --git a/apps/wr/app/store/matDmd/drgtSttn/index.tsx b/apps/wr/app/store/matDmd/drgtSttn/index.tsx index 053bb12fc..4211d2d08 100644 --- a/apps/wr/app/store/matDmd/drgtSttn/index.tsx +++ b/apps/wr/app/store/matDmd/drgtSttn/index.tsx @@ -1,4 +1,4 @@ -import { fetchType, getFetch } from "@/components/lib/api"; +import { fetchType, getFetch, postFectch } from "@/components/lib/api"; import type { CommonTableProps } from "../../common/type/header"; import { drgtHeadListExport, drgtZod } from "./headList"; import type { z } from "zod"; @@ -20,7 +20,7 @@ export type drgtState = { type drgtActions = { drgtInit: () => void - drgtSearch: (fromDate: string, toDate: string, sido: string, sgg: string) => void + drgtSearch: (fromDate: string, toDate: string, sido: string[]) => void drgtModalSearch: (fromDate: string, toDate: string, sgg: string) => void }; @@ -35,27 +35,20 @@ export const drgtStateExport: drgtState = { } } -const initFetch = async (fromDate: string, toDate: string, sido: string, sgg: string) => { - - let searchUrl = ""; - - if (sido === "" || sido === "전체") { - searchUrl = `/il/getDataCount/${fromDate}/${toDate}`; - } else { - searchUrl = `/il/getDataCount/${fromDate}/${toDate}/${sido}/${sgg}`; - } - - const drgtData = await getFetch( - fetchType.WR, - searchUrl - ); +const initFetch = async (fromDate: string, toDate: string, sido:string[]) => { +console.log("sido",sido) + const drgtData = await postFectch( + `/il/getDataCount/${fromDate}/${toDate}`, + sido, + fetchType.WR + ); //주의, 관심, 정상 추출 const statusData = Array.from( - new Set(drgtData.map((e) => e.lvngMfgUswtrDrgtInfo)) + new Set(drgtData.data.map((e) => e.lvngMfgUswtrDrgtInfo)) ); - const statusMap = drgtData.reduce((acc: any, { sggNm, lvngMfgUswtrDrgtInfo: status, countDrgt }: { sggNm: string; lvngMfgUswtrDrgtInfo: string; countDrgt: number }) => { + const statusMap = drgtData.data.reduce((acc: any, { sggNm, lvngMfgUswtrDrgtInfo: status, countDrgt }: { sggNm: string; lvngMfgUswtrDrgtInfo: string; countDrgt: number }) => { if (!acc[sggNm]) { acc[sggNm] = { sggNm, ...Object.fromEntries(statusData.map((e) => [e, 0])) }; } @@ -110,7 +103,7 @@ export const drgtActionsExport: (set: any) => drgtActions = ( const toDate = formatted; //FIXME: 페이지 처음 로딩될때 데이터 보려고 6월로 박아놓음 이거 fromDate로 고치기 - const drgtRes = await initFetch("202506", toDate, "", ""); + const drgtRes = await initFetch("202506", toDate, []); set((state: drgtState) => { return { ...state, @@ -125,10 +118,9 @@ export const drgtActionsExport: (set: any) => drgtActions = ( console.log(e) } }, - drgtSearch: async (fromDate: string, toDate: string, sido: string, sgg: string) => { + drgtSearch: async (fromDate: string, toDate: string, sido: string[]) => { try { - - const drgtRes = await initFetch(fromDate, toDate, sido, sgg); + const drgtRes = await initFetch(fromDate, toDate, sido); set((state: drgtState) => { return { diff --git a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx index 102292ccf..880c789c3 100644 --- a/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx +++ b/apps/wr/components/ui/modal/coMat/drgtDetailModal.tsx @@ -3,11 +3,10 @@ import { btnClass } from "@/app/store/common/code" import type { customDrgt } from "@/app/store/matDmd/drgtSttn" import { drgtDetailHeadListExport } from "@/app/store/matDmd/drgtSttn/headList" -import { excelDownload } from "@/components/lib/func/commom" import { downloadExcel, Title, UiTable } from "@common/business_components/ui" import ListDiv from "@common/business_components/ui/list_div" import { Button, ScrollArea } from "@common/components/ui" -import { useCallback, useEffect, useState } from "react" +import { useEffect, useState } from "react" export default function DrgtDetailModal({ data, From 08adf380e678d3b2ec16a83a958a966d3ba93abf Mon Sep 17 00:00:00 2001 From: jyha00 Date: Tue, 1 Jul 2025 20:17:38 +0900 Subject: [PATCH 19/22] =?UTF-8?q?=EC=83=81=EC=84=B8=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/matMng/coMng/drgtSttn/page.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx index af49f02b1..317e05dc6 100644 --- a/apps/wr/app/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/drgtSttn/page.tsx @@ -202,13 +202,6 @@ export default function DrgtSttn() { > 조회 -

하수재이용실적 현황

- -
+ {/* */} + {/*

{"하수처리수재이용 실적(m3) 등록결과"}

-
+
*/} -
+ + {/* + +
+

+ {"하수처리수재이용 실적(m3) 등록결과"} +

+
+ + + + {headMakeprfmnc(monthHeadList, [])} + + + {InputTableBody(prfData ?? [], monthHeadList, setPrfData)} + +
+
+
*/} + {/* */}
From c50c2685c1df40e19b1b0f32534f72eb23fcfe71 Mon Sep 17 00:00:00 2001 From: jyha00 Date: Wed, 2 Jul 2025 14:30:18 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=EC=9E=A0=EC=9E=AC=EC=88=98=EC=9A=94?= =?UTF-8?q?=EC=B2=98=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wr/app/matMng/coMng/ldrmntCoSttn/page.tsx | 73 ++++++++++++------- .../app/store/matDmd/ldrmntCoSttn/excel.tsx | 26 ++----- .../app/store/matDmd/ldrmntCoSttn/index.tsx | 67 ++++++++++------- .../coReg/coRegExcel/coRegExcelModal.tsx | 24 +++--- 4 files changed, 104 insertions(+), 86 deletions(-) diff --git a/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx b/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx index c5daee29c..eb43b29bf 100644 --- a/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx +++ b/apps/wr/app/matMng/coMng/ldrmntCoSttn/page.tsx @@ -44,7 +44,7 @@ export default function LdrmntCoSttn() { const [searchTxt, setSearchTxt] = useState(); const ref = useRef({} as ReturnType); const [excelData, setExcelData] = useState(); - + const [isExcelDownload, setIsExcelDownload] = useState(false); const excelClick = useCallback(() => { excelDownload( @@ -57,14 +57,22 @@ export default function LdrmntCoSttn() { }, []); const excelDownloadClick = () => { - downloadExcel({ - data: excelData ?? [], - fileName: "잠재수요처 관리", - sheetNameArray: ["Sheet1"], - headerRange: "A1:F1" - }) + ldrmntSearch(1, "10000", "0", ""); + setIsExcelDownload(true); } + useEffect(() => { + if (isExcelDownload && excelData && excelData.length > 0) { + downloadExcel({ + data: excelData, + fileName: "잠재수요처 관리", + sheetNameArray: ["Sheet1"], + headerRange: "A1:F1" + }); + setIsExcelDownload(false); + } + }, [excelData, isExcelDownload]); + const [isExcelOpen, setIsExcelOpen] = useState<{ isModal: boolean; }>({ isModal: false }); @@ -73,14 +81,21 @@ export default function LdrmntCoSttn() { if (index !== undefined) { setDetailModal({ isOpen: true, data: ldrmntRes[index] }); } - } const getSelCity = (val: string | number) => { - const cityValue = String(val); // Ensure val is treated as a string - const data = getCity(cityValue, code.sggCd); - setSelectSgg(data); - setObjSearch({ city: cityValue, sgg: '' }); + const cityValue = String(val); + + if (cityValue === "0") { + setSelectSgg([]); + setObjSearch({ city: cityValue, sgg: "" }); + } else { + const data = getCity(cityValue, code.sggCd); + setSelectSgg(data); + + const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? ""); + setObjSearch({ city: cityValue, sgg: firstSggVal }); + } }; const getSgg = (val: string | number) => { @@ -125,10 +140,9 @@ export default function LdrmntCoSttn() { setExcelData(data) }, [excelList]) - useEffect(() => { - let cityTxt = ''; - let sggTxt = ''; + let cityTxt = '' + let sggTxt = '' if (objSearch?.city.toString().length > 0) { cityTxt = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? ''; @@ -138,21 +152,23 @@ export default function LdrmntCoSttn() { sggTxt = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? ''; } - let cityNm = ""; + if (cityTxt === '전체' || cityTxt === '') { + cityTxt = '0' + } + + let cityNm = "" if (sggTxt !== "0") { - cityNm = cityTxt + " " + sggTxt; + cityNm = cityTxt + " " + sggTxt } else { - cityNm = cityTxt; + cityNm = cityTxt } - if (cityNm) { - if (cityTxt === '') cityNm = "0"; - ldrmntSearch(pageNation, cityNm, searchTxt || ""); + ldrmntSearch(pageNation, '20', cityNm, searchTxt || ""); } else { - ldrmntSearch(pageNation, "", searchTxt || ""); + ldrmntSearch(pageNation, '20', '0', searchTxt || ""); } - }, [pageNation, objSearch, code, searchTxt, ldrmntSearch]) + }, [pageNation]); return ( <> @@ -175,6 +191,7 @@ export default function LdrmntCoSttn() { label="시군구" selectClass={""} selectArray={selectSgg || []} + value={objSearch.sgg} // value props 추가 onSelectValue={getSgg} /> -
+