From 0e1f8f5868873f3b0724fc465c8d881094a9530e Mon Sep 17 00:00:00 2001 From: leedj312 Date: Fri, 8 Aug 2025 14:43:38 +0900 Subject: [PATCH 01/42] =?UTF-8?q?=ED=92=8B=ED=84=B0=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/fo/app/pages/(map-layout)/layout.tsx b/apps/fo/app/pages/(map-layout)/layout.tsx index a9e3baa7f..a9a7a8e93 100644 --- a/apps/fo/app/pages/(map-layout)/layout.tsx +++ b/apps/fo/app/pages/(map-layout)/layout.tsx @@ -1,3 +1,5 @@ +'use client' +import { NoFooterLayout } from "@common/components"; export default function RootLayout({ children, @@ -5,6 +7,6 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( -
{children}
+ {children} ); } From 3116fe8bed466d67293ef750142e49cebb68bb2b Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 22:53:07 +0900 Subject: [PATCH 02/42] =?UTF-8?q?fix:=20mapService=20import=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=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/(popUp-layout)/mapDrgtPopUp/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx index f0ac6bcdd..c6b964632 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx @@ -3,7 +3,7 @@ import Gis from "@common/business_components/ui/map/Gis"; import { useEffect, useRef, useState, useCallback } from "react"; import { useSearchParams } from "next/navigation"; -import { mapService } from "../../(with-layout)/map/service"; +import { mapService } from "../../(map-layout)/service"; import { DrgtLegend, DRGT_LAYER_TYPES, EXPRS_ORDERED_LAYERS } from "./drgtLegend"; import { SearchBar } from "./searchBar"; From b871226f8edc07fbf5eba1fa53c09c7543dbcc76 Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 22:53:58 +0900 Subject: [PATCH 03/42] =?UTF-8?q?refactor:=20import=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx | 2 +- apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx | 3 --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx index 3bf97d677..9c558851c 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect} from "react"; +import React from "react"; import { Label } from "@common/components/ui/label"; export const DRGT_LAYER_TYPES: string[] = [ diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx index c6b964632..a2c843351 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx @@ -17,10 +17,7 @@ export default function MapDrgtPopUp() { const gisMaps = useRef(null); const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 const [showDrgtLegend, setShowDrgtLegend] = useState(false); // DrgtLegend 표시 여부 const [isPlaying, setIsPlaying] = useState(false); diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 10de11308..3dc02caa3 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { SelectBox } from '@common/business_components/ui' import { Label, Button } from '@common/components'; import { type SelectDataType } from "@common/business_components/ui"; From f644bd6b7e657ceda1afdf693e7ddf1d2a861b15 Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 23:43:37 +0900 Subject: [PATCH 04/42] =?UTF-8?q?refactor,=20remove,=20add:=20GIS=20main?= =?UTF-8?q?=20=EB=B8=8C=EB=9E=9C=EC=B9=98=EC=99=80=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 118 ++- .../src/ui/map/GisContent.js | 121 ++- .../src/ui/map/GisSelect.js | 70 +- .../business_components/src/ui/map/GisTab.js | 84 -- .../business_components/src/ui/map/GisTree.js | 871 +++++++++--------- .../src/ui/map/lib/gis.core.js | 21 +- .../src/ui/map/lib/gis.style.js | 22 +- .../src/ui/map/searchBar/SearchInpuText.tsx | 122 +++ .../src/ui/map/searchBar/index.tsx | 3 + .../src/ui/map/searchBar/searchBar.tsx | 344 +++++++ 10 files changed, 1115 insertions(+), 661 deletions(-) delete mode 100644 packages/common/business_components/src/ui/map/GisTab.js create mode 100644 packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx create mode 100644 packages/common/business_components/src/ui/map/searchBar/index.tsx create mode 100644 packages/common/business_components/src/ui/map/searchBar/searchBar.tsx diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index bce7b2ef0..3d810b48d 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -156,18 +156,30 @@ function Gis({ setFeatureData(null); }; - // Feature Click Event + // 피처 클릭시 const handleFeatureSelect = useCallback( (data) => { + console.log("handleFeatureSelect"); onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef], ); + //피처로 포커스 이동 + const handleFeatureFocus = useCallback( + (layerName, data) => { + console.log("handleFeatureFocus"); + console.log(layerName); + console.log(data); + // onFeatureFocus?.(data); + }, + [mapInstanceRef], + ); + // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; - if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + //if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 // EPSG:5179 등록 proj4.defs([ @@ -178,33 +190,53 @@ function Gis({ ]); register(proj4); - // OpenLayers 지도 객체 생성 - const map = new OlMap({ - layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], - renderer: "webgl", - target: mapRef.current, - interactions: defaults({ doubleClickZoom: false }), - controls: defaultControls({ - zoom: false, - rotate: false, - attributionOptions: { - collapsible: false, - }, - }).extend([]), - view: new View({ - projection: GisApp.TO_PROJ, - zoom: gisCore.defaults.zoom, - center: olProj.transform( - [127.4875, 37.4916], - GisApp.FROM_PROJ, - GisApp.TO_PROJ, - ), - minZoom: gisCore.defaults.minZoom, - maxZoom: gisCore.defaults.maxZoom, - }), - }); + let map = mapInstanceRef.current; - gisMaps.current = map; + // OpenLayers 지도 객체 생성 + if (!map) { + console.log("지도 객체 생성"); + map = new OlMap({ + layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], + renderer: "webgl", + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [127.4875, 37.4916], + GisApp.FROM_PROJ, + GisApp.TO_PROJ, + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + } else { + // 📌 재마운트 시 target 없으면 다시 지정 + if (!map.getTarget()) { + console.log("재마운트"); + map.setTarget(mapRef.current); + } + } + + const select = GisApp.Module.select; + if (select) { + console.log("select 바인딩"); + if (select.selectInteraction) { + map.removeInteraction(select.selectInteraction); + } + select.map = map; // map 바인딩 + select.setInteraction(); + select.pointerMoveEventHandler(); + } // 미니맵 추가 if (isMiniMap) { @@ -221,12 +253,21 @@ function Gis({ map.addControl(overviewMap); } + gisMaps.current = map; + // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer("base"); setMapObject({ map: map }); mapInstanceRef.current = map; // 생성 완료 플래그 설정 + + // 📌 언마운트 시 완전 제거 + return () => { + console.log("지도 제거"); + map.setTarget(null); // DOM 연결 해제 + mapInstanceRef.current = null; // 참조 초기화 + }; }, []); // treeData 변경 시 레이어 코드 생성 @@ -240,11 +281,6 @@ function Gis({ // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn"); GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 - - //setTreeData(data); - // 편집 가능한 레이어 목록 생성 - //const layerItems = getLayerItems(layerCode); - //setEditLayerItems(prev => [...layerItems]) }, [treeData]); // droughtDates를 전역(GisApp)에 반영 @@ -287,9 +323,9 @@ function Gis({ const handleCheckedLayersChange = useCallback( (checkedIds, checkedNodesData) => { setTimeout(() => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); }, 1000); }, [onCheckedLayersChange], @@ -328,7 +364,9 @@ function Gis({ const dummyFeature = { getProperties: () => ({}), // 최소 구조 }; - onFeatureSelect(dummyFeature); + console.log(layerNm) + onOpenPopup(layerNm) + // onFeatureSelect(dummyFeature); }} imageVisible={imgVisble} isPlaying={isPlaying} @@ -370,10 +408,8 @@ function Gis({ )} {/* 시도/시군구 조회 영역 */} {SearchBar && ( -
- +
+ {SearchBar}
)} diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index d2594ba95..45b44c882 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -1,74 +1,61 @@ -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'; +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(); - const [visibleComponents, setVisibleComponents] = useState({}); - const addressResultLayer = useRef(null); - const [playShow, setPlayShow] = useState({ show: false }); +export default function GisContent({ + edit = false, + onFeatureSelect, + isSearch, + setIsSearchModalOpen, +}) { + const { gisMap, gisCore } = useGisContext(); - //셀렉트 버튼 이벤트 - // const clickFeature = useCallback(feature => { - // if (feature) { - // const layer = feature.getLayer(); - // setVisibleComponents(prevState => ({ - // ...prevState, - // })); - // } - // }, []); + //셀렉트 버튼 이벤트 + const selectRslt = useCallback((feature) => { + GisApp.Module.layer.highLightFeature( + [feature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 }, + ); + clickFeature(feature); + }, []); - const selectRslt = useCallback(feature => { - GisApp.Module.layer.highLightFeature( - [feature], - { - id: GisApp.hlLayerId, - zIndex: 10, - opacity: 1, - selectable: true, - }, - { width: 30 } - ); - clickFeature(feature); - }, []); + useEffect(() => { + if (gisMap) { + setVisibleComponents((prevState) => ({ + ...prevState, + })); + } + }, [gisMap]); - useEffect(() => { - if (gisMap) { - setVisibleComponents(prevState => ({ - ...prevState, - })); - } - }, [gisMap]); + if (!gisMap) return null; - if (!gisMap) return null; + return ( + <> + + + + + - return ( - <> - {/**/} - {/* - - */} - {/* */} - - {/* 검색 모달 */} - {isSearch && ( -
- -
- )} - - ); + {/* 검색 모달 */} + {isSearch && ( +
+ +
+ )} + + ); } diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index e9deb72f7..d7d5b5b72 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,46 +1,42 @@ -import React, { useContext, useEffect } from 'react'; -import { GisContext } from './Gis'; -import GisApp from './lib/gis.core'; -import GisOlSelect from './lib/gis.select'; +import React, { useContext, useEffect } from "react"; +import { GisContext } from "./Gis"; +import GisApp from "./lib/gis.core"; +import GisOlSelect from "./lib/gis.select"; export default React.memo(function GisSelect({ onFeatureSelect }) { + const { gisMap } = useContext(GisContext); - const { gisMap } = useContext(GisContext); + useEffect(() => { + if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + // 이미 select가 있으면 재생성하지 않음 + const select = new GisOlSelect(gisMap); + GisApp.Module.select = select; - useEffect(() => { - if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + // 피처 선택 시 콜백 등록 + select.getSelectedInfo(function (features) { + if (features?.[0]) { + console.log("GisSelect features", features); + onFeatureSelect(features[0]); + } + }); + }, [gisMap, onFeatureSelect]); - // 이미 select가 있으면 재생성하지 않음 - if (!GisApp.Module.select) { - const select = new GisOlSelect(gisMap); - GisApp.Module.select = 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]); + // select.getSelectedInfo(function (features) { + // //clickFeature(features[0]); + // if(features.length > 0) { + // const layer = features[0]; + // // console.log('GisSelect layer', layer); + // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 + // } + // }); - // 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 ( - <> - {/*
+ return ( + <> + {/*

선,면,다각형을 이용한 시설물 선택 영역

  • @@ -55,6 +51,6 @@ export default React.memo(function GisSelect({ onFeatureSelect }) {
*/} - - ); + + ); }); diff --git a/packages/common/business_components/src/ui/map/GisTab.js b/packages/common/business_components/src/ui/map/GisTab.js deleted file mode 100644 index 57d8a48e8..000000000 --- a/packages/common/business_components/src/ui/map/GisTab.js +++ /dev/null @@ -1,84 +0,0 @@ -// import React, { useState } from 'react'; -// import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs'; -// import GisTree from './GisTree'; -// import GisSearch from './GisSearch'; -// import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; -// import SearchModal from '@/map/SearchModal'; - - -// function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen, onFeatureSelect }) { -// const [activeTab, setActiveTab] = useState('layers'); - -// return ( -// <> -//
-// -// -// 레이어 -// {/* {isSearch && ( -// -// // -// // -// // -// )} */} -// - -// {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */} -//
-// -//
- -// {/* GisSearch도 동일하게 처리 */} -// {/* {isSearch && ( -//
-// -//
-// )} */} - - -// {/* 🔍 검색 모달 */} -// {/* */} -//
- -// {/* 접기 버튼 */} -// -//
- -// {/* 열기 버튼 */} -// {!visible && ( -// -// )} -// -// ); -// } - -// export default React.memo(GisTab); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index ed6bb00fc..7285caa07 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,446 +1,493 @@ import { MultiBackend, Tree, - getBackendOptions -} from "@minoru/react-dnd-treeview"; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import React, { useEffect, useState, useRef, useCallback } from 'react'; - -import { useGisContext } from './Gis'; -import GisApp from './lib/gis.core'; - -export default React.memo(function GisTree({ + getBackendOptions, + DndProvider, + } from "@minoru/react-dnd-treeview"; + import { ChevronLeft, ChevronRight } from "lucide-react"; + import React, { useEffect, useState, useRef, useCallback } from "react"; + + import { useGisContext } from "./Gis"; + import GisApp from "./lib/gis.core"; + + export default React.memo(function GisTree({ checkedNodes = [], - setCheckedNodes = () => { }, + setCheckedNodes = () => {}, visible, setVisible, onCheckedChange, onLayerIconClick, imageVisible = false, isPlaying = false, - setIsPlaying = () => { }, + setIsPlaying = () => {}, targetLayerTypes = [], orderedLayers = [], checkInterval = 3000, - onProgressChange = () => { }, -}) { + onProgressChange = () => {}, + }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); const [nodes, setNodes] = useState([]); - const [activeTab, setActiveTab] = useState('layers'); - + const [activeTab, setActiveTab] = useState("layers"); + const currentAnimationIndexRef = useRef(0); - const targetNodesRef = useRef([]); // 대상 노드들을 저장할 ref - const isPausedRef = useRef(false); // 일시정지 상태를 저장할 ref - const timeoutIdRef = useRef(null); // setTimeout ID를 저장할 ref - - useEffect(() => { - if (treeData && treeData.length > 0) { - const nodeData = setNodeValues(treeData); - setNodes(nodeData); - gisCore.setAllLayersOnMap(false); - } - }, [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); - - // 초기 체크된 레이어 정보를 상위로 전달 - const defaultCheckedNodesData = nodes.filter(node => defaultChecked.includes(node.id)); - onCheckedChange?.(defaultChecked, defaultCheckedNodesData); - } - }, [nodes]); // onCheckedChange를 의존성에 추가 - - // 초기 표시 여부가 'Y'인 레이어들 찾기 - const getDefaultCheckedNodes = (nodes, values = []) => { - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (node.data.initEprssYn === 'Y') { - values.push(node.data.lyrSn); - } - } - return values; - }; - + const targetNodesRef = useRef([]); + const isPausedRef = useRef(false); + const timeoutIdRef = useRef(null); + + // ===== 유틸 ===== const getDescendants = (id, all) => { - const children = all.filter((n) => n.parent === id); - return children.reduce( - (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)), - [] - ); + const children = all.filter((n) => n.parent === id); + return children.reduce( + (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)), + [] + ); }; - + const getAncestors = (id, all) => { - const node = all.find((n) => n.id === id); - if (!node || node.parent == null) return []; - return [node.parent, ...getAncestors(node.parent, all)]; + const node = all.find((n) => n.id === id); + if (!node || node.parent == null) return []; + return [node.parent, ...getAncestors(node.parent, all)]; }; - + + const getChildren = (parentId, all) => + all.filter((n) => n.parent === parentId); + + const areAllChildrenChecked = (parentId, all, checkedIds) => { + const children = getChildren(parentId, all); + return children.length > 0 && children.every((c) => checkedIds.includes(c.id)); + }; + + const updateParentCheckState = (nodeId, all, checkedIds) => { + const parent = all.find((n) => n.id === nodeId)?.parent; + if (parent == null) return checkedIds; + + if (areAllChildrenChecked(parent, all, checkedIds)) { + if (!checkedIds.includes(parent)) { + checkedIds.push(parent); + } + } else { + checkedIds = checkedIds.filter((id) => id !== parent); + } + return updateParentCheckState(parent, all, checkedIds); + }; + const isEveryChildChecked = (parentId, all, checked) => { - const children = all.filter((n) => n.parent === parentId); - return children.every((c) => checked.includes(c.id)); + const children = getChildren(parentId, all); + return children.length > 0 && children.every((c) => checked.includes(c.id)); }; - - const handleCheck = (id) => { - setChecked((prevChecked) => { - const isChecked = prevChecked.includes(id); - let newChecked = [...prevChecked]; - if (isChecked) { - const descendants = getDescendants(id, nodes); - newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid)); - const ancestors = getAncestors(id, nodes); - ancestors.forEach((p) => { - if (newChecked.includes(p)) { - newChecked = newChecked.filter((cid) => cid !== p); - } - }); - } else { - const descendants = getDescendants(id, nodes); - newChecked = Array.from(new Set([...newChecked, id, ...descendants])); - const ancestors = getAncestors(id, nodes); - ancestors.forEach((p) => { - if (isEveryChildChecked(p, nodes, newChecked)) { - newChecked.push(p); - } - }); - // setExpanded((prev) => Array.from(new Set([...prev, ...ancestors]))); - } - const allIds = nodes.map((n) => n.id); - 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; - }); + + // ===== 기본 체크 노드 ===== + const getDefaultCheckedNodes = (nodes) => { + let checkedIds = nodes + .filter((n) => n.data.initEprssYn === "Y") + .map((n) => n.id); + + nodes.forEach((node) => { + checkedIds = updateParentCheckState(node.id, nodes, checkedIds); + }); + + return Array.from(new Set(checkedIds)); }; - - const setNodeValues = (result) => { - return result.map((item) => { - const src = makeLegendSrc(item); - let label = item.lyrNm; - return { - id: item.lyrSn, - text: label, - parent: item.upperLyrSn, - droppable: !!item.children, - data: item, - icon: src ? {item.lyrNm} : '', - }; - }); - }; - - const makeLegendSrc = layerinfo => { - if (!layerinfo.gobjTy) return null; - - let src; - if (layerinfo.iconNm) { - src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; - } else { - const ss = makeLegendImage(layerinfo); - src = ss.src; - } - return src; - }; - - const makeLegendImage = layerinfo => { - let image = document.createElement('img'); - let canvas = document.createElement('canvas'); - canvas.width = 16; - canvas.height = 16; - let ctx = canvas.getContext('2d'); - ctx.beginPath(); - - if (layerinfo.gobjTy === 'lineLabelPoligon') { - ctx.moveTo(1, 1); - ctx.lineTo(15, 1); - ctx.lineTo(15, 15); - ctx.lineTo(1, 15); - ctx.lineTo(1, 1); - } else if (layerinfo.gobjTy === 'line') { - ctx.moveTo(1, 15); - ctx.lineTo(5, 1); - ctx.lineTo(9, 15); - ctx.lineTo(15, 1); - } else { - ctx.moveTo(1, 1); - ctx.lineTo(15, 1); - ctx.lineTo(15, 15); - ctx.lineTo(1, 15); - ctx.lineTo(1, 1); - } - - if (layerinfo.gobjTy === 'lineLabelPoligon') { - ctx.fillStyle = layerinfo.polgnColor; - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = layerinfo.lnColor; - ctx.stroke(); - } - - if (layerinfo.gobjTy === 'line') { - ctx.lineWidth = 1; - ctx.strokeStyle = layerinfo.lnColor; - ctx.stroke(); - } - - image.src = canvas.toDataURL('image/png'); - return image; - }; - const handleLayerImgClick = (nodeNm, nodeId, layerNm) => { - if (!checked.includes(nodeId)) alert('시설을 선택해주세요') - else onLayerIconClick(nodeNm) - } - - // 특정 노드를 체크/언체크하는 함수 - const checkSpecificNode = useCallback((nodeId, shouldCheck = true) => { - setChecked((prevChecked) => { - let newChecked = [...prevChecked]; - const isCurrentlyChecked = prevChecked.includes(nodeId); - - if (shouldCheck && !isCurrentlyChecked) { - // 체크하기 - const descendants = getDescendants(nodeId, nodes); - newChecked = Array.from(new Set([...newChecked, nodeId, ...descendants])); - const ancestors = getAncestors(nodeId, nodes); - ancestors.forEach((p) => { - if (isEveryChildChecked(p, nodes, newChecked)) { - newChecked.push(p); - } - }); - } else if (!shouldCheck && isCurrentlyChecked) { - // 언체크하기 - const descendants = getDescendants(nodeId, nodes); - newChecked = newChecked.filter((cid) => cid !== nodeId && !descendants.includes(cid)); - const ancestors = getAncestors(nodeId, nodes); - ancestors.forEach((p) => { - if (newChecked.includes(p)) { - newChecked = newChecked.filter((cid) => cid !== p); - } - }); - } - - // 지도에 레이어 표시/숨김 적용 - const allIds = nodes.map((n) => n.id); - 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; - }); + + // ===== 초기 로딩 ===== + useEffect(() => { + if (treeData && treeData.length > 0) { + const nodeData = setNodeValues(treeData); + setNodes(nodeData); + gisCore.setAllLayersOnMap(false); + } + }, [treeData]); + + useEffect(() => { + if (nodes.length > 0) { + const defaultChecked = getDefaultCheckedNodes(nodes); + setChecked(defaultChecked); + + const defaultExpanded = Array.from( + new Set([ + ...defaultChecked, + ...defaultChecked.flatMap((id) => getAncestors(id, nodes)), + ]) + ); + setExpanded(defaultExpanded); + + gisCore.setLayerOnMap(defaultChecked, true); + + const defaultCheckedNodesData = nodes.filter((node) => + defaultChecked.includes(node.id) + ); + onCheckedChange?.(defaultChecked, defaultCheckedNodesData); + } }, [nodes]); - - // 침수위험지도 실행 함수 - const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => { - // 기존 타임아웃 정리 - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - timeoutIdRef.current = null; + + // ===== 체크 이벤트 ===== + const handleCheck = (id) => { + setChecked((prevChecked) => { + let newChecked = [...prevChecked]; + const isChecked = prevChecked.includes(id); + + if (isChecked) { + const descendants = getDescendants(id, nodes); + newChecked = newChecked.filter( + (cid) => cid !== id && !descendants.includes(cid) + ); + } else { + const descendants = getDescendants(id, nodes); + newChecked = Array.from(new Set([...newChecked, id, ...descendants])); } - + + newChecked = updateParentCheckState(id, nodes, newChecked); + + const allIds = nodes.map((n) => n.id); + 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; + }); + }; + + // ===== 특정 노드 체크/해제 ===== + const checkSpecificNode = useCallback( + (nodeId, shouldCheck = true) => { + setChecked((prevChecked) => { + let newChecked = [...prevChecked]; + const isCurrentlyChecked = prevChecked.includes(nodeId); + + if (shouldCheck && !isCurrentlyChecked) { + const descendants = getDescendants(nodeId, nodes); + newChecked = Array.from( + new Set([...newChecked, nodeId, ...descendants]) + ); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (isEveryChildChecked(p, nodes, newChecked)) { + newChecked.push(p); + } + }); + } else if (!shouldCheck && isCurrentlyChecked) { + const descendants = getDescendants(nodeId, nodes); + newChecked = newChecked.filter( + (cid) => cid !== nodeId && !descendants.includes(cid) + ); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (newChecked.includes(p)) { + newChecked = newChecked.filter((cid) => cid !== p); + } + }); + } + + const allIds = nodes.map((n) => n.id); + 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; + }); + }, + [nodes] + ); + + // ===== 자동 애니메이션 ===== + const handleAutoCheckAnimation = useCallback( + (isEnabled, layerTypes = [], interval = 3000) => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + if (!isEnabled || layerTypes.length === 0) { - // 애니메이션 비활성화 시 상태 초기화 + isPausedRef.current = false; + targetNodesRef.current = []; + currentAnimationIndexRef.current = 0; + onProgressChange(0, 0); + return; + } + + const getTargetNodesInOrder = () => { + const orderedNodes = []; + orderedLayers.forEach((lyrSn) => { + const node = nodes.find( + (n) => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm) + ); + if (node) { + orderedNodes.push(node); + } + }); + return orderedNodes; + }; + + const targetNodes = getTargetNodesInOrder(); + + if (targetNodes.length === 0) return; + + if (targetNodesRef.current.length === 0) { + targetNodesRef.current = targetNodes; + currentAnimationIndexRef.current = 0; + onProgressChange(targetNodes.length, 0); + targetNodes.forEach((node) => { + checkSpecificNode(node.id, false); + }); + } + + isPausedRef.current = false; + + const processNextLayer = () => { + if (isPausedRef.current || !isPlaying) return; + + const currentTargetNodes = targetNodesRef.current; + const currentIndex = currentAnimationIndexRef.current; + + if (currentIndex < currentTargetNodes.length) { + checkSpecificNode(currentTargetNodes[currentIndex].id, true); + if (currentIndex > 0) { + checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false); + } + + currentAnimationIndexRef.current++; + onProgressChange( + currentTargetNodes.length, + currentAnimationIndexRef.current + ); + timeoutIdRef.current = setTimeout(processNextLayer, interval); + } else { isPausedRef.current = false; targetNodesRef.current = []; currentAnimationIndexRef.current = 0; - onProgressChange(0, 0); - return; - } - - // 대상 노드들을 찾아서 특정 순서로 정렬 후 배열로 반환 - const getTargetNodesInOrder = () => { - const orderedNodes = []; - orderedLayers.forEach(lyrSn => { - const node = nodes.find(n => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm)); - if (node) { - orderedNodes.push(node); - } - }); - - return orderedNodes; - }; - - const targetNodes = getTargetNodesInOrder(); - - if (targetNodes.length === 0) { - console.warn('대상 레이어를 찾을 수 없습니다:', layerTypes); - return; - } - - // 처음 시작하는 경우에만 초기화 - if (targetNodesRef.current.length === 0) { - targetNodesRef.current = targetNodes; - currentAnimationIndexRef.current = 0; - - console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm }))); - - // 진행률 초기화 (전체 단계 수 설정) - onProgressChange(targetNodes.length, 0); - - // 모든 대상 레이어를 먼저 언체크 - targetNodes.forEach(node => { - checkSpecificNode(node.id, false); - }); - } - - // 애니메이션 재개/시작 - isPausedRef.current = false; - - const processNextLayer = () => { - // 일시정지 상태이거나 애니메이션이 비활성화되면 중단 - if (isPausedRef.current || !isPlaying) { - return; - } - - const currentTargetNodes = targetNodesRef.current; - const currentIndex = currentAnimationIndexRef.current; - - if (currentIndex < currentTargetNodes.length) { - checkSpecificNode(currentTargetNodes[currentIndex].id, true); - if (currentIndex > 0) { - checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false); - } - - currentAnimationIndexRef.current++; - - // 진행률 업데이트 - onProgressChange(currentTargetNodes.length, currentAnimationIndexRef.current); - - // 다음 레이어 처리를 위한 타이머 설정 - timeoutIdRef.current = setTimeout(processNextLayer, interval); - } else { - // 애니메이션 완료 - 상태 초기화 및 완료 신호 전송 - console.log('애니메이션 완료'); - isPausedRef.current = false; - targetNodesRef.current = []; - currentAnimationIndexRef.current = 0; - timeoutIdRef.current = null; - - // 진행률 100% 완료 - onProgressChange(currentTargetNodes.length, currentTargetNodes.length); - - // 잠시 후 진행률 초기화 - setTimeout(() => { - onProgressChange(0, 0); - }, 1000); - - setIsPlaying?.(false); - } - }; - - processNextLayer(); - - }, [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]); - - // 애니메이션 일시정지 함수 - const pauseAnimation = useCallback(() => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; - } - isPausedRef.current = true; - }, []); - - // 외부에서 전달받은 props로 자동 체크 제어 - useEffect(() => { - if (isPlaying) handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); // 재생 시작/재개 - else pauseAnimation(); // 일시정지 - }, [isPlaying, handleAutoCheckAnimation, targetLayerTypes, checkInterval, pauseAnimation]); - - // 컴포넌트 언마운트 시 정리 - useEffect(() => { - return () => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - } + onProgressChange( + currentTargetNodes.length, + currentTargetNodes.length + ); + setTimeout(() => { + onProgressChange(0, 0); + }, 1000); + setIsPlaying?.(false); + } }; - }, []); - - - if (!treeData || treeData.length === 0) return null; - - - return ( - <> -
-
-

레이어

-
- - ( -
- {node.droppable && ( - - - )} - handleCheck(node.id)} className="cursor-pointer" /> - {node.icon && {node.icon}} - {node.text} - {imageVisible ? depth > 0 && { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}> : null} -
- )} - sort={(a, b) => { - const aOrder = parseInt(a.data?.sortOrdr ?? '0'); - const bOrder = parseInt(b.data?.sortOrdr ?? '0'); - return aOrder - bOrder; - }} - /> -
-
- {/* 접기 버튼 */} - -
- {/* 열기 버튼 */} - {!visible && ( - - )} - + + processNextLayer(); + }, + [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange] ); -}); + + const pauseAnimation = useCallback(() => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + isPausedRef.current = true; + }, []); + + useEffect(() => { + if (isPlaying) + handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); + else pauseAnimation(); + }, [ + isPlaying, + handleAutoCheckAnimation, + targetLayerTypes, + checkInterval, + pauseAnimation, + ]); + + useEffect(() => { + return () => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + } + }; + }, []); + + // ===== 노드 세팅 ===== + const setNodeValues = (result) => { + return result.map((item) => { + const src = makeLegendSrc(item); + return { + id: item.lyrSn, // 반드시 고유값 + text: item.lyrNm, + parent: item.upperLyrSn || null, // 루트는 null + droppable: true, // children 여부 모를 땐 true로 둬야 트리 확장 가능 + data: item, + icon: src ? {item.lyrNm} : "", + }; + }); + }; + + + const makeLegendSrc = (layerinfo) => { + if (!layerinfo.gobjTy) return null; + if (layerinfo.iconNm) { + return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; + } else { + return makeLegendImage(layerinfo).src; + } + }; + + const makeLegendImage = (layerinfo) => { + let image = document.createElement("img"); + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + let ctx = canvas.getContext("2d"); + ctx.beginPath(); + + if (layerinfo.gobjTy === "lineLabelPoligon") { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } else if (layerinfo.gobjTy === "line") { + ctx.moveTo(1, 15); + ctx.lineTo(5, 1); + ctx.lineTo(9, 15); + ctx.lineTo(15, 1); + } else { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } + + if (layerinfo.gobjTy === "lineLabelPoligon") { + ctx.fillStyle = layerinfo.polgnColor; + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + if (layerinfo.gobjTy === "line") { + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + + image.src = canvas.toDataURL("image/png"); + return image; + }; + + const handleLayerImgClick = (nodeNm, nodeId) => { + if (!checked.includes(nodeId)) alert("시설을 선택해주세요"); + else onLayerIconClick(nodeNm); + }; + + if (!treeData || treeData.length === 0) return null; + + return ( + <> +
+
+

레이어

+
+ + { + const isVisible = node.data.exprsYn === "Y"; + const hasVisibleChildren = nodes.some( + (child) => + child.parent === node.id && child.data.exprsYn === "Y" + ); + + return ( +
+ {hasVisibleChildren ? ( + + ) : ( + + )} + handleCheck(node.id)} + className="cursor-pointer" + /> + {node.icon && {node.icon}} + + {node.text} + + {imageVisible && depth > 0 && ( + + + handleLayerImgClick( + node.data.lyrTyNm, + node.id + ) + } + alt={node.text} + /> + + )} +
+ ); + }} + sort={(a, b) => { + const aOrder = parseInt(a.data?.sortOrdr ?? "0"); + const bOrder = parseInt(b.data?.sortOrdr ?? "0"); + return aOrder - bOrder; + }} + /> +
+
+ +
+ {!visible && ( + + )} + + ); + }); + \ No newline at end of file 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 a5268ac44..5a6e705da 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 @@ -16,11 +16,8 @@ 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 GisOlEditLayer from './gis.edit'; import GisOlLayer from './gis.layer'; @@ -34,7 +31,6 @@ GisApp.layerInfos = []; GisApp.DROUGHT_DATES = { startYmd: '', endYmd: '' }; //기본정보 GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; -GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry'; GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; //맵 정보 @@ -44,9 +40,8 @@ GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; -// TODO: NGII 정보 -GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키 -GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; +// NGII 정보(배경지도) +GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL; GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; @@ -563,18 +558,24 @@ GisApp.core.prototype = { */ setLayerOnMap: function (layerIds, visible, addQurryFlter) { for (let i in layerIds) { - + let id = layerIds[i]; let layer = this.getLayerById(id); if (layer) { layer.setVisible(visible); } else if (visible) { const layerCode = GisApp.layerCode[id]; + const actlLyrYn = layerCode.actlLyrYn; + if(actlLyrYn == 'N') continue; if(!layerCode.lyrTyNm) continue; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); - layer = this.addWfsLayer(layerCode); + + if(layerCode.srvcNm == 'wms'){ + layer = this.addWmsLayer(layerCode); + }else { + layer = this.addWfsLayer(layerCode); + } } } if (GisApp.Module.select) { diff --git a/packages/common/business_components/src/ui/map/lib/gis.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js index 8cff08dca..f38853962 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.style.js +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -88,7 +88,7 @@ GisOlStyle.prototype = { } let baseResolution = 2; - let minScale = 0.4; + let minScale = 0.8; let maxScale = 1.5; let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution; scale = Math.max(minScale, Math.min(maxScale, scale)); @@ -176,7 +176,7 @@ GisOlStyle.prototype = { const properties = feature.getProperties(); const overLayId = feature?.get('overLayId'); - const overLayMinZoom = layer.get('overLayMinZoom') || '12'; + const overLayMinZoom = layer.get('overLayMinZoom') || '10'; const overLayOffset = layer?.get('overLayOffset'); let findoOverlay = this.overlays[overLayId]; if (findoOverlay) { @@ -193,7 +193,8 @@ GisOlStyle.prototype = { if (typeof overlayHtml === 'string') { //const sanitizedHtml = DOMPurify.sanitize(overlayHtml); - overlayContainer.innerHTML = sanitizedHtml; + //overlayContainer.innerHTML = sanitizedHtml; + overlayContainer.innerHTML = overlayHtml; } // React 컴포넌트일 경우 createPortal() 사용 else if (React.isValidElement(overlayHtml)) { @@ -205,7 +206,7 @@ GisOlStyle.prototype = { element: overlayContainer, positioning: 'bottom-center', stopEvent: false, - offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정) + offset: overLayOffset ? overLayOffset : [80, 0], // 오프셋 설정 (위치 조정) }); this.overlays[overLayId] = overlay; @@ -274,7 +275,8 @@ GisOlStyle.prototype = { let fontExtnColor = layer.get('fontExtnColor'); let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1'); let fontIntnColor = layer.get('fontIntnColor'); - let minViewSize = layer.get('minViewSize'); + //let minViewSize = layer.get('minViewSize') || 13; + let minViewSize = 14; if (feature == null) { return; } @@ -282,14 +284,14 @@ GisOlStyle.prototype = { let label = prop[layer.get('lblAtrbNm')]; let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가 const zoom = GisApp.Module.core.map.getView().getZoom(); - if (zoom < minViewSize - 3) { + if (zoom <= minViewSize) { label = ''; } let text = new Text({ - stroke: new Stroke({ - color: '#FFFFFF', // 흰색 테두리 - width: fontExtnWdth, - }), + // stroke: new Stroke({ + // color: '#FFFFFF', // 흰색 테두리 + // width: fontExtnWdth, + // }), fill: new Fill({ color: fontIntnColor, width: fontExtnWdth, diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx new file mode 100644 index 000000000..97ba51fb7 --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx @@ -0,0 +1,122 @@ +// SearchInputText.tsx +"use client"; +import { Input } from "@common/components/ui"; +import { useEffect, useRef, useState } from "react"; + +interface SearchInputTextProps { + initValue?: string; + setValue: (val: string) => void; + setRtnData: (val: { fnm: string; fid: string }) => void; + searchList: { fnm: string; fid: string }[]; + onEnter?: () => void; + className?: string; +} + +export default function SearchInputText({ + initValue, + setValue, + setRtnData, + searchList, + onEnter, + className, +}: SearchInputTextProps) { + const [showResults, setShowResults] = useState(false); + const [text, setText] = useState(initValue ?? ""); + const [highlightIndex, setHighlightIndex] = useState(-1); + const wrapperRef = useRef(null); + + useEffect(() => { + setText(initValue ?? ""); + }, [initValue]); + + useEffect(() => { + if (searchList.length > 0) { + setShowResults(true); + } else { + setShowResults(false); + } + setHighlightIndex(-1); + }, [searchList]); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + wrapperRef.current && + !(wrapperRef.current as any).contains(event.target) + ) { + setShowResults(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [wrapperRef]); + + const handleChange = (e: React.ChangeEvent) => { + setText(e.target.value); + setValue(e.target.value); + setHighlightIndex(-1); + }; + + const handleSelect = (item: { fnm: string; fid: string }) => { + setText(item.fnm); + setValue(item.fnm); + setRtnData(item); + setShowResults(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (highlightIndex >= 0 && highlightIndex < searchList.length) { + const selected = searchList[highlightIndex]; + if (selected) handleSelect(selected); + } else if (onEnter) { + onEnter(); + } + } else if (e.key === "ArrowDown") { + e.preventDefault(); + setHighlightIndex((prev) => + prev < searchList.length - 1 ? prev + 1 : prev + ); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHighlightIndex((prev) => (prev > 0 ? prev - 1 : -1)); + } + }; + + return ( +
+ + {showResults && ( +
+ {searchList.length > 0 ? ( + searchList.map((item, index) => ( +
handleSelect(item)} + > + {item.fnm} +
+ )) + ) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+ )} +
+ ); +} diff --git a/packages/common/business_components/src/ui/map/searchBar/index.tsx b/packages/common/business_components/src/ui/map/searchBar/index.tsx new file mode 100644 index 000000000..4ba24de3a --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/index.tsx @@ -0,0 +1,3 @@ +export * from "./comboSearchBar"; +export * from "./procSearchBar"; +export * from './searchBar' diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx new file mode 100644 index 000000000..edd2853f2 --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -0,0 +1,344 @@ +"use client"; +import { SelectBox, type SelectDataType } from "../../../ui"; +import { Button } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import SearchInputText from "./SearchInpuText"; +import GisApp from "../lib/gis.core"; + +const FACILITY_TYPE_URL_MAP: Record = { + tb_il_swage_prcs_fclt: + "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&propertyName=swtrmt_fclt_cd,swtrmt_fclt_nm&cql_filter=", + tb_wr_bdst_ruse_fclt: + "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000/", + tb_wr_rnwtr_utztn_fclt: + "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}/{fcltyNm}", + tb_wr_rswtr_fclt: + "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}/{fcltyNm}", + tb_wr_htdfl_fclt: + "/reuse-service/drain/facilityList/1/1000/{sggCd}/{fcltyNm}", + tb_wr_ruse_fclt_dmd_co: + "/reuse-service/reuseFc/request/1/1000/{sggCd}/{fcltyNm}", + tb_wr_tmpr_dmd_co: + "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}/{fcltyNm}", +}; + +interface SearchBarProps { + gisMaps: React.RefObject; + facilityTypes: { text: string; val: string }[]; + onFeatureFocus: any; +} + +export const SearchBar = ({ + gisMaps, + facilityTypes, + onFeatureFocus, +}: SearchBarProps) => { + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [facilityType, setFacilityType] = useState( + facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" + ); + const [fcltyNm, setFcltyNm] = useState(""); + const [searchList, setSearchList] = useState<{ fnm: string; fid: string }[]>( + [] + ); + + const [sidoData, setSidoData] = useState([ + { text: "시도", val: "0" }, + ]); + const [sigunData, setSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + + const fetchSido = async () => { + const res = await fetch("/cms-service/api/v1/code/cache/ADSTDSTRCD/Y"); + const json = await res.json(); + const sidoList = json + .filter((item: any) => item.lwpoCd?.length === 2) + .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); + const sigunList = json + .filter((item: any) => item.lwpoCd?.length > 2) + .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); + setSidoData([{ text: "시도", val: "0" }, ...sidoList]); + setSigunData(sigunList); + }; + + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setFilteredSigunData([{ text: "시도", val: "0" }]); + setSigunValue("0"); + return; + } + const prefix = sidoCd.substring(0, 2); + const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); + setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); + setSigunValue("0"); + }, + [sigunData] + ); + + useEffect(() => { + fetchSido(); + }, []); + + const onSelectValue = useCallback( + (val: string, type: string) => { + if (type === "ctpvCd") { + setSidoValue(val); + initializeSigunData(val); + setSearchList([]); + setFcltyNm(""); + } else if (type === "sggCd") { + setSigunValue(val); + setSearchList([]); + setFcltyNm(""); + } else if (type === "facilityType") { + setFacilityType(val); + setSearchList([]); + setFcltyNm(""); + } + }, + [initializeSigunData] + ); + + const handleSearch = async () => { + if (!fcltyNm) return alert("시설명을 입력하세요."); + + const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || ""; + + const type = facilityType; + const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; + if (!baseUrlTemplate) return; + + let url = baseUrlTemplate; + const year = new Date().getFullYear().toString(); + url = url + .replace("{year}", year) + .replace("{sggCd}", sigunValue) + .replace("{sidoNm}", encodeURIComponent(sidoNm)) + .replace("{sggNm}", encodeURIComponent(sggNm)) + .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); + + let list: SelectDataType[] = []; + + if (type === "tb_il_swage_prcs_fclt") { + const cqlFilter = `ctpv_nm='${sidoNm}' and sgg_nm='${sggNm}' and swtrmt_fclt_nm like '%${fcltyNm}%'`; + url += encodeURIComponent(cqlFilter); + const res = await fetch(url); + const json = await res.json(); + list = json.features.map((f: any) => ({ + text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + val: f.id, + })); + } else if ( + type === "tb_wr_bdst_ruse_fclt" // 재이용시설 + ) { + //재이용에서 참조하는 하수처리시설 목록 + const procRes = await fetch("/reuse-service/reuseFc/pubFc"); + const procJson = await procRes.json(); + const procData = procJson.data; + const procResult = procData + .filter((code: string) => code.slice(0, 5) === sigunValue) + .map((code: string) => { + return code; + }); + + //처리시설명으로 처리시설코드 조회 + let procUrl2 = + "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}"; + procUrl2 = procUrl2 + .replace("{sggCd}", sigunValue) + .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); + const procRes2 = await fetch(procUrl2, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(procResult), + }); + const procJson2 = await procRes2.json(); + const procResult2 = (procJson2.data || []).map((item: any) => item.fid); + + //최종 처리시설코드로 재이용시설조회 + const res = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(procResult2), + }); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.ruseUtztnFcltNm ?? "(이름없음)", + val: item.pbnstRuseFcltId, + })); + } else if ( + type === "tb_wr_ruse_fclt_dmd_co" //수요처 + ) { + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.dmdCoNm ?? "(이름없음)", + val: item.dmdInfoId, + })); + } else if ( + type === "tb_wr_tmpr_dmd_co" //잠재수요처 + ) { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.dmdCoNm ?? "(이름없음)", + val: item.fctryMngSn, + })); + } else { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.bdstNm ?? "(이름없음)", + val: item.bdstInfoSn, + })); + } + + if (list.length === 0) { + alert("검색 결과가 없습니다."); + setSearchList([]); + setFcltyNm(""); + return; + } + + const newList = list.map((item: any) => { + const code = item.val; + const name = item.text; + return { fnm: name, fid: code }; + }); + setSearchList(newList); + }; + + //해당 시설물로 이동 + const handleFeatureFocus = (data: any) => { + const 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: any = null; + let featureId = data.fid; + 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) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } else { + console.warn("geometry를 찾을 수 없습니다:", data); + } + } else { + console.warn("Feature를 찾을 수 없습니다:", data); + } + + onFeatureFocus(foundFeature); + }; + + return ( +
+
+
+
+ {facilityTypes.length > 1 && ( + + onSelectValue(val.toString(), "facilityType") + } + value={facilityType} + /> + )} + onSelectValue(val.toString(), "ctpvCd")} + value={sidoValue} + /> + onSelectValue(val.toString(), "sggCd")} + value={sigunValue} + /> + { + setFcltyNm(val); + }} + setRtnData={(selected: { fnm: string; fid: string }) => { + // 선택된 항목이 있을 경우 onFeatureSelect 실행 + const type = facilityType; + handleFeatureFocus({ + layerNm: type, + ...selected, + }); + }} + searchList={searchList} + onEnter={handleSearch} + className="mr-5 w-[150px]" + /> +
+
+ +
+
+
+
+ ); +} From 3cc8fcb2eb5bfa4d7a0665054b562854a80dffdf Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 23:46:20 +0900 Subject: [PATCH 05/42] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=80=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 --- .../map/searchBar/{SearchInpuText.tsx => SearchInputText.tsx} | 0 .../business_components/src/ui/map/searchBar/searchBar.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/common/business_components/src/ui/map/searchBar/{SearchInpuText.tsx => SearchInputText.tsx} (100%) diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx similarity index 100% rename from packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx rename to packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index edd2853f2..e750ab1a7 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -2,7 +2,7 @@ import { SelectBox, type SelectDataType } from "../../../ui"; import { Button } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; -import SearchInputText from "./SearchInpuText"; +import SearchInputText from "./SearchInputText"; import GisApp from "../lib/gis.core"; const FACILITY_TYPE_URL_MAP: Record = { From 57fc624fbc03fa77661eb41f0682793604e5e369 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:29:50 +0900 Subject: [PATCH 06/42] =?UTF-8?q?feat:=20WR(=EB=AC=BC=EC=9E=AC=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9)=20=EA=B0=80=EB=AD=84=EC=A7=80=EB=8F=84=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EA=B5=AC=ED=98=84=20(=EC=9E=A0=EC=9E=AC=EC=88=98?= =?UTF-8?q?=EC=9A=94=EC=B2=98,=20=EC=88=98=EC=9A=94=EC=B2=98=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 2 +- apps/wr/app/(map-layout)/service/map.ts | 4 +- .../mapDrgtPopUp/drgtLegend.tsx | 70 +++++++ .../app/(popUp-layout)/mapDrgtPopUp/median.ts | 24 +++ .../app/(popUp-layout)/mapDrgtPopUp/page.tsx | 88 +++++++++ .../(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 173 ++++++++++++++++++ .../matMng/coMng/drgtSttn/page.tsx | 28 ++- apps/wr/middleware.ts | 7 +- 8 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 331e487bd..7451ae4b5 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -24,7 +24,7 @@ const Map = () => { const fetchData = async () => { try { - const response = await mapService.getLyrStngs(); + const response = await mapService.getLyrStngs('WR_NETWORK'); setTreeData(response); // API 응답 저장 } catch (error) { console.error("데이터 불러오기 실패:", error); diff --git a/apps/wr/app/(map-layout)/service/map.ts b/apps/wr/app/(map-layout)/service/map.ts index 5002afef7..79ba7fe9e 100644 --- a/apps/wr/app/(map-layout)/service/map.ts +++ b/apps/wr/app/(map-layout)/service/map.ts @@ -30,8 +30,8 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 - getLyrStngs: async () => { - const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, { + getLyrStngs: async (lyrGrp : string) => { + const response = await fetch(`${API_URL}/lyrStngs/${lyrGrp}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx new file mode 100644 index 000000000..9c558851c --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx @@ -0,0 +1,70 @@ +"use client"; + +import React from "react"; +import { Label } from "@common/components/ui/label"; + +export const DRGT_LAYER_TYPES: string[] = [ + // 수정 필요 + 'drought1', + 'drought2', + 'drought3', + 'drought4', + 'drought5', + 'drought6', +]; +// 지정된 순서에 따른 lyr_sn 배열 +export const EXPRS_ORDERED_LAYERS: string[] = [ + // 수정 필요 + '106', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도 + '107', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도 + '108', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도 + '109', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도 + '110', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도 + '111', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도 +]; + +export function DrgtLegend() { + + + return ( + <> +
+
+ +
+
+
+
+
+
+
+
가 뭄 정 보
+
+ + {/* 관심 - 1회 미만 */} + {/* 관심 - 1회 이상 */} + + {/* 주의 - 1회 미만 */} + {/* 주의 - 1회 이상 */} + + {/* 관심+주의 - 1회 미만 */} + {/* 관심+주의 - 1회 이상 */} +
+
+
+
+ + ); +} + +function LegendItem({ color, label }: { color: string; label: string }) { + return ( +
+ + {label} +
+ ); +} \ No newline at end of file diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts new file mode 100644 index 000000000..ab63e7c91 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -0,0 +1,24 @@ +import { boundingExtent, getCenter } from 'ol/extent'; +import Feature from 'ol/Feature'; +import Geometry from 'ol/geom/Geometry'; + +export function medianCoordinate2D(feature: Feature) { + // Feature를 입력 받아 중심 좌표를 계산하는 함수 + if (!feature) { + console.warn("유효한 피처가 없습니다."); + return; + } + + console.log(feature) + + const geometry = feature.getGeometry(); + if (!geometry) { + console.warn("피처에 geometry가 없습니다."); + return; + } + + const extent = geometry.getExtent(); + const center = getCenter(extent); + const centerExtent = boundingExtent([center]); + return centerExtent; +} \ No newline at end of file diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx new file mode 100644 index 000000000..a2c843351 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; +import { mapService } from "../../(map-layout)/service"; +import { DrgtLegend, DRGT_LAYER_TYPES, EXPRS_ORDERED_LAYERS } from "./drgtLegend"; +import { SearchBar } from "./searchBar"; + + +export default function MapDrgtPopUp() { + const searchParams = useSearchParams(); + const ctpvCd = searchParams.get('ctpvCd') || ''; + const sggCd = searchParams.get('sggCd') || ''; + const fromDate = searchParams.get('fromDate') || ''; + const toDate = searchParams.get('toDate') || ''; + + const gisMaps = useRef(null); + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [showDrgtLegend, setShowDrgtLegend] = useState(false); // DrgtLegend 표시 여부 + const [isPlaying, setIsPlaying] = useState(false); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + function fetchData() { + (async () => { + try { + const response = await mapService.getLyrStngs('WR_DROUGHT'); + setTreeData(response); // API 응답 저장 + console.log("Drought layers fetched:", response); + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + })(); + } + + // WaterDepth 표시 여부를 결정하는 함수 + const handleDrgtLegndVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + const drgtLegndLayers = checkedNodesData.filter(node => + DRGT_LAYER_TYPES.includes(node.data?.lyrTyNm) + ); + + const hasDrgtLegndLayer = drgtLegndLayers.length > 0; + setShowDrgtLegend(hasDrgtLegndLayer); + }, []); + + // 체크된 레이어 변경 처리 함수 + const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + handleDrgtLegndVisibility(checkedIds, checkedNodesData); + }, [handleDrgtLegndVisibility]); + + + + return ( +
+ } + waterDepth={showDrgtLegend ? DrgtLegend : undefined} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying as any} + targetLayerTypes={DRGT_LAYER_TYPES as any} + orderedLayers={EXPRS_ORDERED_LAYERS as any} + checkInterval={2500} + height={`h-[100vh]`} + droughtDates={{ startYmd: fromDate, endYmd: toDate }} + /> +
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx new file mode 100644 index 000000000..21a98d696 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -0,0 +1,173 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { SelectBox } from '@common/business_components/ui' +import { Label, Button } from '@common/components'; +import { type SelectDataType } from "@common/business_components/ui"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import GeoJSON from "ol/format/GeoJSON"; +import Feature from "ol/Feature"; +import Geometry from "ol/geom/Geometry"; +import { medianCoordinate2D } from "./median"; + +interface CtpvSggPrcsFcltSelectBoxProps { + gisMaps: React.RefObject; + ctpvCode: string; + sggCode: string; + fromDate: string; + toDate: string; +} + +const DEFAULT_OPTION: SelectDataType = { val: '0', text: '전체' }; + +export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectBoxProps) { + const [ctpvCd, setCtpvCd] = useState(ctpvCode); + const [sggCd, setSggCd] = useState(sggCode); + const [ctpv, setCtpv] = useState([]); + const [sgg, setSgg] = useState([]); + + useEffect(() => { + (async () => { + await fetchSido(); + await fetchSigungu(); + setCtpvCd(ctpvCode); + setSggCd(sggCode); + })(); + }, []); + + function onViewMapClick() { + if (sggCd && sggCd !== '0') { + fetchSggData(); + return; + } + if (ctpvCd && ctpvCd !== '0') fetchCtpvData(); + } + + async function fetchCtpvData() { + try { + const ctpvData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_ctpv&outputFormat=application%2Fjson&cql_filter=mctpv_cd=%27${ctpvCd.trim()}%27&srsName=EPSG:5179`) + .then(res => res.json()); + console.log('시도 데이터', ctpvData.features[0]); + + const format = new GeoJSON(); + const feature: Feature = format.readFeature(ctpvData.features[0], { + dataProjection: "EPSG:5179", // GeoJSON 좌표계 + featureProjection: GisApp.TO_PROJ // 지도 좌표계 + }); + + console.log('feature', feature); + + const extent = medianCoordinate2D(feature); + console.log('GisApp.TO_PROJ', GisApp.TO_PROJ); + console.log('extent', extent) + + handleFeatureFocus(extent); + } + catch (error) { + console.error("시도 data fetching Error:", error); + } + } + + async function fetchSggData() { + try { + const sggData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_sgg&outputFormat=application%2Fjson&cql_filter=sgg_cd=%27${sggCd}%27&srsName=EPSG:5179`) + .then(res => res.json()); + console.log('시군구 데이터', sggData.features[0]); + + + const format = new GeoJSON(); + const feature: Feature = format.readFeature(sggData.features[0], { + dataProjection: "EPSG:5179", // GeoJSON 좌표계 + featureProjection: GisApp.TO_PROJ // 지도 좌표계 + }); + + const extent = medianCoordinate2D(feature); + + handleFeatureFocus(extent); + } + catch (error) { + console.error("시군구 data fetching Error:", error); + } + } + + function handleFeatureFocus(extent: any) { + console.log('ctpv', ctpv); + console.log('sgg', sgg); + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 11 + }); + }; + + const fetchSido = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.mctpv_korn_nm, + val: f.properties.mctpv_cd, + })); + + console.log('시도 데이터', list); + setCtpv(list); + }; + + const fetchSigungu = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.sgg_korn_nm, + val: f.properties.sgg_cd, + })); + + console.log('시군구 데이터', list); + setSgg(list); + }; + + + + return ( +
+
+ + { + setCtpvCd(String(val)); + setSggCd(String(0)); + }} + selectContentClass="z-[101]" + /> + + String(opt.val).slice(0, 2) === String(ctpvCd).trim()) + ] + } + value={sggCd} + onSelectValue={val => { setSggCd(String(val)) }} + selectContentClass="z-[101]" + /> +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx index be7210f3f..f6881688a 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx @@ -9,7 +9,7 @@ import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/b import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button, Label, ScrollArea } from "@common/components/ui"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { toast, Toaster } from "react-hot-toast"; export default function DrgtSttn() { @@ -26,6 +26,7 @@ export default function DrgtSttn() { isOpen: boolean; data: customDrgt[] | undefined; }>({ data: undefined, isOpen: false }); + const popupRef = useRef(null); const cellClick = (index: number) => { if (fromDate && toDate && drgtRes[index]) { @@ -121,6 +122,19 @@ export default function DrgtSttn() { setToDate(formatted); } }, [fromDate, toDate]) + + function onViewMapClick() { + const params = new URLSearchParams({ + ctpvCd: String(objSearch.city), + sggCd: String(objSearch.sgg), + fromDate: fromDate || '', + toDate: toDate || '' + }); + const popupOptions = `width=${window.screen.availWidth},height=${window.screen.availHeight},left=0,top=0,resizable=yes,scrollbars=yes`; + popupRef.current = window.open(`/mapDrgtPopUp?${params.toString()}`, "가뭄 지도 보기 팝업", popupOptions); + } + + return ( <> @@ -136,7 +150,7 @@ export default function DrgtSttn() { selectClass={""} selectArray={code.cityCd || []} onSelectValue={getSelCity} - value={objSearch?.city ||code.cityCd[0]?.val ||""} + value={objSearch?.city || code.cityCd[0]?.val || ""} />
@@ -156,7 +170,7 @@ export default function DrgtSttn() { onChange: (date: string | undefined) => { const year = date ? date.slice(0, 4) : ''; let month = date?.slice(4); - if (month&&month?.length < 2) { + if (month && month?.length < 2) { month = month?.padStart(2, "0"); } const formatted = `${year}${month}`; @@ -172,7 +186,7 @@ export default function DrgtSttn() { onChange: (date: string | undefined) => { const year = date ? date.slice(0, 4) : ''; let month = date?.slice(4); - if (month&&month.length < 2) { + if (month && month.length < 2) { month = month?.padStart(2, "0"); } const formatted = `${year}${month}`; @@ -211,7 +225,7 @@ export default function DrgtSttn() { sggText = sidoText; } - const searchArr:string[] = []; + const searchArr: string[] = []; if (sidoText) searchArr.push(sidoText); if (Array.isArray(sggText)) { searchArr.push(...sggText); @@ -234,7 +248,7 @@ export default function DrgtSttn() { diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index d068f6aa5..c5c7f8c01 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -91,6 +91,8 @@ export function middleware(req: NextRequest) { let queryParams = searchParams.toString(); const typename = searchParams.get("TYPENAME"); console.log("typename ", typename); + const startYmd = searchParams.get("START_YMD") ?? null; + const endYmd = searchParams.get("END_YMD") ?? null; let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; @@ -116,10 +118,13 @@ export function middleware(req: NextRequest) { typename == "hasudo:tb_wr_rswtr_fclt" || //중수도시설 typename == "hasudo:tb_wr_htdfl_fclt" || //온배수시설 typename == "hasudo:tb_wr_ruse_fclt_dmd_co" || //수요처 - typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + typename == "hasudo:tb_wr_tmpr_dmd_co" || //잠재수요처 + typename?.startsWith("hasudo:drought") //가뭄 지도 ) { const url = req.nextUrl.clone(); url.searchParams.set("TYPENAME", typename.replace("hasudo:", "")); + if (startYmd) url.searchParams.set("startYmd", startYmd); + if (endYmd) url.searchParams.set("endYmd", endYmd); queryParams = url.searchParams.toString(); newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; } From bd3015dec56d7b747b0a91319cd033a7640dc6e1 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:40:23 +0900 Subject: [PATCH 07/42] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts | 6 +++--- apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index ab63e7c91..6a216813e 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,6 +1,6 @@ -import { boundingExtent, getCenter } from 'ol/extent'; -import Feature from 'ol/Feature'; -import Geometry from 'ol/geom/Geometry'; +import { boundingExtent, getCenter } from 'ol/extent.js'; +import Feature from 'ol/Feature.js'; +import Geometry from 'ol/geom/Geometry.js'; export function medianCoordinate2D(feature: Feature) { // Feature를 입력 받아 중심 좌표를 계산하는 함수 diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 21a98d696..110dc377a 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -5,9 +5,9 @@ import { SelectBox } from '@common/business_components/ui' import { Label, Button } from '@common/components'; import { type SelectDataType } from "@common/business_components/ui"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import GeoJSON from "ol/format/GeoJSON"; -import Feature from "ol/Feature"; -import Geometry from "ol/geom/Geometry"; +import GeoJSON from "ol/format/GeoJSON.js"; +import Feature from "ol/Feature.js"; +import Geometry from "ol/geom/Geometry.js"; import { medianCoordinate2D } from "./median"; interface CtpvSggPrcsFcltSelectBoxProps { From 5b4b8fbd80a9804b9def7ff1580327e602f047f5 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:48:54 +0900 Subject: [PATCH 08/42] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=83=80=EC=9E=85=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts | 7 ++++--- apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index 6a216813e..6bb5f78c9 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,6 +1,7 @@ -import { boundingExtent, getCenter } from 'ol/extent.js'; -import Feature from 'ol/Feature.js'; -import Geometry from 'ol/geom/Geometry.js'; +import boundingExtent from 'ol/extent'; +import getCenter from 'ol/extent'; +import Feature from 'ol/Feature'; +import Geometry from 'ol/geom/Geometry'; export function medianCoordinate2D(feature: Feature) { // Feature를 입력 받아 중심 좌표를 계산하는 함수 diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 110dc377a..21a98d696 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -5,9 +5,9 @@ import { SelectBox } from '@common/business_components/ui' import { Label, Button } from '@common/components'; import { type SelectDataType } from "@common/business_components/ui"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import GeoJSON from "ol/format/GeoJSON.js"; -import Feature from "ol/Feature.js"; -import Geometry from "ol/geom/Geometry.js"; +import GeoJSON from "ol/format/GeoJSON"; +import Feature from "ol/Feature"; +import Geometry from "ol/geom/Geometry"; import { medianCoordinate2D } from "./median"; interface CtpvSggPrcsFcltSelectBoxProps { From b5c111fef07a3a8083331d2b411456673f405e11 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:52:35 +0900 Subject: [PATCH 09/42] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20import=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/(popUp-layout)/mapDrgtPopUp/median.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index 6bb5f78c9..ab63e7c91 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,5 +1,4 @@ -import boundingExtent from 'ol/extent'; -import getCenter from 'ol/extent'; +import { boundingExtent, getCenter } from 'ol/extent'; import Feature from 'ol/Feature'; import Geometry from 'ol/geom/Geometry'; From fe524f98b40547d7bb3ea51c284006be46e514a8 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:59:52 +0900 Subject: [PATCH 10/42] =?UTF-8?q?test:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B0=B0=ED=8F=AC=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20wr=EC=97=90=20ol=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wr/package.json b/apps/wr/package.json index a29dcd66b..bb170c820 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -21,6 +21,8 @@ "lucide-react": "^0.474.0", "moment": "^2.30.1", "next": "15.1.7", + "ol": "^7.3.0", + "ol-ext": "^4.0.11", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.2", "react-i18next": "^15.6.0", From 4903684bceecf4d430c78b1d3b55dea93400dd01 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 02:05:04 +0900 Subject: [PATCH 11/42] =?UTF-8?q?=EA=B3=B5=EA=B8=89=EC=B2=98=20=EC=84=A0?= =?UTF-8?q?=EC=A0=95=20=EC=A7=80=EB=8F=84=20=ED=8C=9D=EC=97=85=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 --- .../SearchInputText/SearchInputText.tsx | 139 ++++++++ apps/wr/app/(popUp-layout)/splyPopup/page.tsx | 315 ++++++++++++++++++ .../splyPopup/searchBar/searchBar.tsx | 118 +++++++ .../splyPopup/selectedFcltList.tsx | 81 +++++ .../(popUp-layout)/splyPopup/service/index.ts | 1 + .../(popUp-layout)/splyPopup/service/map.ts | 60 ++++ apps/wr/app/store/map/splyPop/index.tsx | 93 ++++++ apps/wr/app/store/map/splyPop/type.tsx | 39 +++ apps/wr/app/store/store.tsx | 10 + apps/wr/components/lib/config/index.ts | 2 + .../ui/form/coMat/conCostForm/conCostForm.tsx | 63 +++- 11 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/page.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/service/index.ts create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/service/map.ts create mode 100644 apps/wr/app/store/map/splyPop/index.tsx create mode 100644 apps/wr/app/store/map/splyPop/type.tsx diff --git a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx new file mode 100644 index 000000000..05f41d09b --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx @@ -0,0 +1,139 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +type SearchType = { + bscFctNm: string; + bscFctCd: string; +}; + +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setValue: (searchNm?: string) => void; + setRtnData: (rtnData: any) => void; + searchList?: SearchType[]; +}; + +export default function SearchInputText({ + label = undefined, + initValue = undefined, + setValue = () => {}, + setRtnData = () => {}, + searchList = [], +}: thisProps) { + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ""); + const [highlightIndex, setHighlightIndex] = useState(-1); + + const wrapperRef = useRef(null); + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setTextValue(value); + setValue(value); + setHighlightIndex(-1); + setIsOpen(value !== ""); + }, + [setValue] + ); + + const handleSuggestionClick = useCallback( + (searchNm: string) => { + setValue(searchNm); + setTextValue(searchNm); + setIsOpen(false); + setRtnData(searchList?.find((item) => item.bscFctNm === searchNm) || {}); + }, + [searchList, setRtnData, setValue] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!isOpen || searchList.length === 0) return; + + if (e.key === "ArrowDown") { + e.preventDefault(); + setHighlightIndex((prev) => (prev + 1) % searchList.length); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHighlightIndex((prev) => + prev <= 0 ? searchList.length - 1 : prev - 1 + ); + } else if (e.key === "Enter") { + e.preventDefault(); + if (searchList.length === 1 && searchList[0]) { + handleSuggestionClick(searchList[0].bscFctNm); + } else if (highlightIndex >= 0 && highlightIndex < searchList.length && searchList[highlightIndex]) { + handleSuggestionClick(searchList[highlightIndex].bscFctNm); + } + } + }, + [highlightIndex, isOpen, searchList, handleSuggestionClick] + ); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + return ( +
+ { + setTextValue(""); + setValue(""); + setIsOpen(false); + }} + /> + {isOpen && ( +
+
+ {searchList.length > 0 ? ( + searchList.map((item, index) => ( +
handleSuggestionClick(item.bscFctNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + handleSuggestionClick(item.bscFctNm); + } + }} + > + {item.bscFctNm} +
+ )) + ) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+
+ )} +
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx new file mode 100644 index 000000000..51cf73064 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx @@ -0,0 +1,315 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState } from "react"; +import { mapService } from "./service"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { SearchBar } from "./searchBar/searchBar"; +import SelectedFcltList from "./selectedFcltList"; + +export type SubsInfo = { + ruseUtztnFcltNm: string; // 재이용시설명 + ruseUtztnFcltCd: string; // 재이용시설 코드 +} + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [selectDataPk, setSelectDataPk] = useState>({}); + const [selectedFclts, setSelectedFclts] = useState([]); + + //적용 버튼 클릭 이벤트 + const onApply = () => { + if (selectedFclts.length > 1) { + alert("재이용시설 공급처는 1곳만 선택할 수 있습니다."); + return; + } + + const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({ + ruseUtztnFcltNm: fclt.ruseUtztnFcltNm, + ruseUtztnFcltCd: fclt.pbnstRuseFcltId, + })); + + if (!window.opener || typeof window.opener.postMessage !== "function") { + alert("부모 창을 찾을 수 없습니다. 팝업이 'noopener'로 열렸거나, 보안 정책으로 차단되었을 수 있어요."); + return; + } + + window.opener.postMessage( + { + type: "SET_SUBS_INFO", + payload: subsInfoList[0], // 또는 필요한 데이터 + }, + "*" + ); + + window.close(); + }; + + //트리 레이어 데이터 조회 + const fetchTreeData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + //하수처리시설코드로 재이용시설 목록 조회 + const reuseData = async (ruseFcltPk: any) => { + try { + const response = await mapService.getPbnstRuseFclts(ruseFcltPk); + const data = response.data || []; + setSelectedFclts((prev) => { + const newItems = data.filter( + (item: any) => + item && + Object.keys(item).length > 0 && + !prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId) + ); + console.log(newItems); + return [...prev, ...newItems]; + }); + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchTreeData(); + }, []); + + // 팝업 Map.tsx useEffect 안 + useEffect(() => { + const origin = window.location.origin; + if (window.opener) { + window.opener.postMessage({ type: "REQUEST_INIT_SUBS_INFO" }, origin); + } + }, []); + + useEffect(() => { + const origin = window.location.origin; + + const handler = (event: MessageEvent) => { + // 동일 출처만 허용 (개발 중엔 임시로 주석처리하고 *로 테스트해도 됨) + if (event.origin !== origin) return; + + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "INIT_SUBS_INFO") { + // payload: [{ ruseUtztnFcltNm, ruseUtztnFcltCd }, ...] + const list = Array.isArray(data.payload) ? data.payload : []; + + // selectedFclts 형태에 맞게 변환해서 반영 + // (지금 onApply에서 pbnstRuseFcltId를 쓰므로 매핑 맞춰줍니다) + setSelectedFclts(list.map((x) => ({ + ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "", + pbnstRuseFcltId: x.ruseUtztnFcltCd ?? "", // <- 기존 코드가 기대하는 키 + }))); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); + }, []); + + // 레이어 생성 후 초기화 처리 + const onCheckedLayersChange = () => { + //수요처 ID + const urlParams = new URLSearchParams(window.location.search); + const dmdInfoId = urlParams.get("dmdInfoId"); + + //수요처 위치로 화면 이동 + const maps = gisMaps.current; + const layer = GisApp.Module.layer as any; + const layerNm = "tb_wr_ruse_fclt_dmd_co"; + const featureId = `${layerNm}.${dmdInfoId}`; + let foundFeature: any = null; + + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + + if (geometry) { + const extent = geometry.getExtent(); + + if (geometry.getType() === "Point") { + maps.getView().setCenter(geometry.getCoordinates()); + maps.getView().setZoom(11); + } else { + maps.getView().fit(extent, { + duration: 300, + padding: [30, 30, 30, 30], + maxZoom: 16, + }); + } + } + + layer.highLightFeature( + [foundFeature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + } + + const formatNumber = (val: any) => { + const num = Number(val); + return isNaN(num) ? "-" : num.toLocaleString(); + }; + + // 재이용시설 오버레이 + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + + features.forEach((feature: any) => { + // 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId()) + const featureId = + feature.getId() || feature.get("ftrIdn") || Math.random(); + + // HTML 또는 React 오버레이 컴포넌트 구성 + const overlayContent = ` +
+ +
+ ${feature.get("swtrmt_fclt_nm") || "-"} +
+ + + + + + + + + +
용량(m³/일)${formatNumber(feature.get("fclt_cpct"))}
+
+ `; + feature.set("html", overlayContent); + feature.set("overLayId", `overLay.${featureId}`); + }); + } + } + }); + }; + + // 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리 + useEffect(() => { + if (!selectDataPk || Object.keys(selectDataPk).length === 0) return; + const maps = gisMaps.current; + let foundFeature: any = null; + const layerNm = "tb_wr_bdst_ruse_fclt"; + const featureId = `${layerNm}.${selectDataPk.bscFctCd}`; + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + maps.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk); + } + }, [selectDataPk]); + + // 재이용시설 클릭 시 선택 시설목록에 추가 + const onFeatureSelect = (data: any) => { + // 처리시설코드 + const swtrmtFcltCd = data.get("swtrmt_fclt_cd"); + reuseData(swtrmtFcltCd); + }; + + return ( + <> +
+ { + if (hasuPk) setSelectDataPk(hasuPk); + }} + onApply={onApply} + /> + } + waterDepth={undefined} + /> +
+ setSelectedFclts(updatedList)} + /> + + ); +}; + +export default Map; diff --git a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx new file mode 100644 index 000000000..ce5c6d1ae --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx @@ -0,0 +1,118 @@ +"use client"; +import { useWrStore } from "@/app/store"; +import { + SelectBox, + type SelectDataType, +} from "@common/business_components/ui"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import SearchInputText from "../SearchInputText/SearchInputText"; + +export function SearchBar({ + setHasuPk, + onApply, +}: { + setHasuPk: (hasuPk: any) => void; + onApply: () => void; +}) { + const { map, code, codeInit, findRuseFclt } = useWrStore( + (state) => state + ); + const [sidoValue, setSidoValue] = useState(""); + const [sigunValue, setSigunValue] = useState(""); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "전체", val: "0" }, + ]); + + useEffect(() => { + //codeInit(); + }, [codeInit]); + + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setSigunValue("0"); + setFilteredSigunData([{ text: "전체", val: "0" }]); + } else { + const newFilteredData = [ + { text: "전체", val: "0" }, + ...code.sggCd.filter( + (item: SelectDataType) => item.val.substring(0, 2) === sidoCd + ), + ]; + setSigunValue("0"); + setFilteredSigunData(newFilteredData); + } + }, + [code.sggCd] + ); + + const onSelectValue = useCallback( + (val: string, type: string) => { + switch (type) { + case "ctpvCd": + initializeSigunData(val); + setSidoValue(val); + break; + case "sggCd": + setSigunValue(val); + } + }, + [initializeSigunData] + ); + + return ( +
+
+ onSelectValue(val.toString(), "ctpvCd")} + value={sidoValue} + /> + onSelectValue(val.toString(), "sggCd")} + value={sigunValue} + /> + + { + let sggOrCity = ""; + if (searchNm) { + if (sigunValue === "0" || sigunValue === "") { + sggOrCity = sidoValue; + } else { + sggOrCity = sigunValue; + } + findRuseFclt(sggOrCity, searchNm); + } + }} + setRtnData={(rtnData: any) => { + setHasuPk(rtnData); + }} + searchList={map.ruseFcltList || []} + /> + +
+
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx new file mode 100644 index 000000000..bdf4d7e2c --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +type FcltItem = { + pbnstRuseFcltId: string; + ruseUtztnFcltNm: string; + splyExtraCpct?: number; + ruseCpct?: number; + [key: string]: any; +}; + +export default function SelectedFcltList({ + data = [], + onRemove, +}: { + data: FcltItem[]; + onRemove: (updatedList: FcltItem[]) => void; +}) { + const handleDelete = (code: string) => { + const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code); + onRemove(updatedList); + }; + + const TableContent = ( + <> + + + 시설명 + + 하수처리량 +
+ (㎥/일) + + + 재이용량 +
+ (㎥/일) + + 삭제 + + + + {data.map((item) => ( + + {item.ruseUtztnFcltNm} + + {Number(item.splyExtraCpct || 0).toLocaleString()} + + + {Number(item.ruseCpct || 0).toLocaleString()} + + + + + + ))} + + + ); + + if (data.length === 0) return null; + + return ( +
+

+ 선택된 시설 +

+
+ + {TableContent} +
+
+
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts new file mode 100644 index 000000000..c6bfcff72 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts @@ -0,0 +1 @@ +export * from './map' diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts new file mode 100644 index 000000000..b7afe1deb --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts @@ -0,0 +1,60 @@ + +/** + * 관망도 API + */ + import { DEV_URL_FO, DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config"; + +export type LyrStngProps = { + lyr_sn: string + lyr_nm: string + upper_lyr_sn: string + drawOrdr: string + flterSql: string + gobjTy: string + lyrTpNm: string + useYn: string + minViewSize: string + iconNm: string + lblAtrbNm: string + fontNm: string + fontIntnColor: string + fontExtnColor: string + fontExtnWdth: string + lnWdth: string + lnColor: string + polgnColor: string + initEprssYn: string + clickPsblYn: string +} + +export const mapService = { + //레이어 설정 조회 + getLyrStngs: async () => { + const response = await fetch(`${DEV_URL_SN}/v1/map/lyrStngs/DM_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + }, + getPbnstRuseFclts: async (ruseFcltPk:any) => { + const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} diff --git a/apps/wr/app/store/map/splyPop/index.tsx b/apps/wr/app/store/map/splyPop/index.tsx new file mode 100644 index 000000000..42dc850d3 --- /dev/null +++ b/apps/wr/app/store/map/splyPop/index.tsx @@ -0,0 +1,93 @@ +import { fetchType, getFetch } from "@/components/lib/api"; +import type { FacilityInfo, SubsInfo } from "./type"; +import { DEV_URL_FO } from "@/components/lib/config"; + +const checkWrRegList = async () => { + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); + return wrRegList; +}; + +const getRuseFclt = async ( + page: string, + sggOrCity: string, + searchTxt?: string +) => { + const checkWrRegListRes = await checkWrRegList(); + + let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`; + + const ruseList = checkWrRegListRes.filter((item: string) => item !== ""); + + if (searchTxt) { + url = url + `/${searchTxt}`; + } + + const response = await fetch(DEV_URL_FO + url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ruseList), + }); + + const hasuList = await response.json().then((res: any) => { + const data = res.data + .map((item: any) => { + return { + ...item, + fctCpc: Number(item.fctCpc) || 0, + searchNm: item.fctNm, + searchPk: item.bscFctNm, + }; + }) + .filter((hasu: any) => ruseList.find((ruse: any) => hasu.bscFctCd === ruse)); + return { data: data, count: res.count }; + }); + + return hasuList; +}; + +export type DmMapActions = { + findRuseFclt(sggOrCity: string, searchTxt: string): void; + setSubsInfo(subsInfo: SubsInfo[]): void; +}; + +export type DmMapType = { + map: { + ruseFcltList: FacilityInfo[]; + subsInfo: SubsInfo[]; + }; +}; + +export const dmMapState: DmMapType = { + map: { + ruseFcltList: [], + subsInfo: [] + }, +}; +export type DmMapStore = DmMapType & DmMapActions; + +export const DmMapReducer = (set: any): DmMapActions => { + return { + findRuseFclt: async (sggOrCity: string, searchTxt: string) => { + const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt); + + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + ruseFcltList: ruseFcltList.data, + }, + })); + }, + setSubsInfo: async (subsInfo: SubsInfo[]) => { + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + subsInfo: subsInfo, + }, + })); + }, + }; +}; diff --git a/apps/wr/app/store/map/splyPop/type.tsx b/apps/wr/app/store/map/splyPop/type.tsx new file mode 100644 index 000000000..adcaf7f37 --- /dev/null +++ b/apps/wr/app/store/map/splyPop/type.tsx @@ -0,0 +1,39 @@ +export type FacilityInfo= { + bscFctCd: string; // 기본시설 코드 + bscFctNm: string; // 기본시설명 + rvbsnCd: string; // 유역 코드 + rvbsn: string; // 유역명 + lgbsn: string; // 대권역명 + mdmbsn: string; // 중권역명 + rgnCd: string; // 관할청 코드 + rgnNm: string; // 관할청 명칭 + adstDstrCd: string; // 행정구역 코드 + sido: string; // 시도 + gugun: string; // 구군 + addr: string; // 주소 + fctCpc: string; // 시설용량 + efflWqltStndrdCd: string; // 방류수 수질기준 코드 + efflWqltStndrdNm: string; // 방류수 수질기준명 + trtFrfNm: string; // 처리공법명 + trtDstr: string; // 처리구역 + rnDt: string; // 사용개시일 (예: YYYYMMDD) + cmplDt: string; // 준공일자 (예: YYYYMMDD) + trstOprEtpNm: string; // 위탁운영기관명 + bscFctClsNm: string; // 기본시설 구분명 + sitArea: string; // 부지면적 + trtDstrArea: string; // 처리구역 면적 + swrTrtPopl: string; // 하수처리 인구 + mnstr: string; // 총괄 유역 + efflWtrrgn: string; // 방류수역 + rvNm: string; // 하천명 + bscFctClsCd: string; // 기본시설 구분 코드 + rvbsnNm: string; // 유역명 + tel: string; // 전화번호 + rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N") + ruse: boolean; // 재이용 여부 (boolean) + } + + export type SubsInfo={ + ruseUtztnFcltNm: string; // 재이용시설명 + ruseUtztnFcltCd: string; // 재이용시설 코드 + } \ No newline at end of file diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index 6577f2a8f..c7dcf1ca8 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -216,6 +216,12 @@ import { type ldrmntMapState, type ldrmntMapType, } from "./map/ldrmntCoMap"; +import { + DmMapReducer, + dmMapState, + type DmMapType, + type DmMapStore, +} from "./map/splyPop"; export type WrStore = comCodeType & pubFacType & @@ -251,6 +257,7 @@ export type WrStore = comCodeType & ruseMapType & coSttnMapType & ldrmntMapType & + DmMapStore & authType; export const StoreZus = ( @@ -288,6 +295,7 @@ export const StoreZus = ( ruseMapState & coSttnMapState & ldrmntMapState & + DmMapType & authState = { ...comCodeStateExport, ...pubFacStateExport, @@ -324,6 +332,7 @@ export const StoreZus = ( ...ruseMapStateExport, ...coSttnMapStateExport, ...ldrmntMapStateExport, + ...dmMapState, } ) => { return createStore()((set) => { @@ -371,6 +380,7 @@ export const StoreZus = ( ...ruseMapActionsExport(set), ...coSttnMapActionsExport(set), ...ldrmntMapActionsExport(set), + ...DmMapReducer(set), }; }); }; diff --git a/apps/wr/components/lib/config/index.ts b/apps/wr/components/lib/config/index.ts index ff44511b4..f05a38e62 100644 --- a/apps/wr/components/lib/config/index.ts +++ b/apps/wr/components/lib/config/index.ts @@ -4,6 +4,7 @@ import { SERVER_API_CS_URL, SERVER_API_FO_URL, SERVER_API_WR_URL, + SERVER_SEWERAGE_API_URL, } from "@/src/shared/config"; //const DEV_PORT = "11080"; @@ -19,6 +20,7 @@ import { export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`; export const DEV_URL_WR = SERVER_API_WR_URL; export const DEV_URL_FO = SERVER_API_FO_URL; +export const DEV_URL_SN = SERVER_SEWERAGE_API_URL; export const LOCAL_URL = PROXY_HOST; export const CS_URL = SERVER_API_CS_URL; export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL; diff --git a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx index 423581432..515121e4e 100644 --- a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx +++ b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx @@ -69,6 +69,18 @@ export default function ConCostForm({ setCheckFinish(false); }, [selectionVersion]); + //공급처 팝업 값 셋팅 + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === "SET_SUBS_INFO") { + setLastSub(event.data.payload); + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [setLastSub]); + const zipDowonLoad = () => { fetch(`/2024_constarctFeeProgram.zip`, {}).then(async (response) => { const blobRet = await response.blob(); @@ -219,15 +231,62 @@ export default function ConCostForm({ }, [form.formState]); useEffect(() => { - console.log("splyCndtList ", splyList); + const origin = window.location.origin; + + const handler = (event: MessageEvent) => { + // 보안: 동일 출처만 수신 + if (event.origin !== origin) return; + + // 구조 안전 체크 + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "REQUEST_INIT_SUBS_INFO") { + const initSubs = + (splyList ?? []) + .filter(Boolean) + .map((x: any) => ({ + ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "", + ruseUtztnFcltCd: x.ruseUtztnFcltCd ?? "", + })); + + // 응답: 옵션 객체 형태로 전송 (TS 에러 방지) + const target = event.source as Window | null; + target?.postMessage( + { type: "INIT_SUBS_INFO", payload: initSubs }, + { targetOrigin: origin } + ); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); }, [splyList]); + useEffect(() => { form.register("dmdInfoSn"); form.register("dmdInfoId"); form.register("ruseUtztnFcltNm"); }, []); + + const searchCandidate = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const dmdInfoId = form.getValues("dmdInfoId"); + console.log(dmdInfoId) + + const url = `/splyPopup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`; + const width = window.screen.availWidth; + const height = window.screen.availHeight; + const left = 0; + const top = 0; + const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`; + window.open(url, "candidatePopup", popupOptions); + }; + return ( <>
@@ -515,6 +574,8 @@ export default function ConCostForm({ onClick={(e) => { console.log(e); setLastSub(null) // 빌드오류때문에 넣은거 삭제필요 + + searchCandidate(e); //splyList => 해당객체가 대민에서 선택한 후보처 리스트 //setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트 From a64328ef892769a28ed8c983d5461d7b685992a3 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 02:18:11 +0900 Subject: [PATCH 12/42] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/splyPopup/service/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts index b7afe1deb..9d58f65a7 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts @@ -2,7 +2,7 @@ /** * 관망도 API */ - import { DEV_URL_FO, DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config"; + import { DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config"; export type LyrStngProps = { lyr_sn: string From 044c3a3cb911f605c62db27441e6217a72880c34 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 02:32:00 +0900 Subject: [PATCH 13/42] =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/dm/app/store/map/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dm/app/store/map/index.tsx b/apps/dm/app/store/map/index.tsx index 50741bec3..7b5eb0cca 100644 --- a/apps/dm/app/store/map/index.tsx +++ b/apps/dm/app/store/map/index.tsx @@ -40,7 +40,7 @@ const getRuseFclt = async ( searchPk: item.bscFctNm, }; }) - .filter((hasu) => ruseList.find((ruse) => hasu.bscFctCd === ruse)); + .filter((hasu:any) => ruseList.find((ruse:any) => hasu.bscFctCd === ruse)); return { data: data, count: res.count }; }); From 9dc1c88c8074f4389d23d352e51888064a989383 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 02:59:43 +0900 Subject: [PATCH 14/42] =?UTF-8?q?=EC=9E=84=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wr/next.config.ts b/apps/wr/next.config.ts index 35ea029a8..2ec36653c 100644 --- a/apps/wr/next.config.ts +++ b/apps/wr/next.config.ts @@ -4,7 +4,7 @@ const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13001"; const nextConfig: NextConfig = { - output: "standalone", + // output: "standalone", async rewrites() { return { beforeFiles: [ From 0c23c99ef616d89513ad7e9a042811746773c5ed Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 03:06:35 +0900 Subject: [PATCH 15/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d62db0b5..68fddf6a3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "packages/*" ], "scripts": { - "build": "turbo run build --filter=!dm", + "build": "turbo run build", "build:common": "turbo run build --filter=@common/*", "dev": "turbo run dev", "build:sn": "turbo run build --filter=sn", From 2207d7f61bc255a011eef44da400765bd68ead7c Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 03:20:15 +0900 Subject: [PATCH 16/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/wr/package.json b/apps/wr/package.json index bb170c820..6ed84bcd6 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -6,6 +6,7 @@ "dev": "next dev -p 3001", "build": "next build", "start": "next start -p 3001", + "start:sn": "next start -p 3001", "lint": "next lint" }, "dependencies": { From 7304eccf1bff9773e6bfce0d55bf4b9b21cbcc22 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 03:23:31 +0900 Subject: [PATCH 17/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 68fddf6a3..384fa686c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "build:common": "turbo run build --filter=@common/*", "dev": "turbo run dev", "build:sn": "turbo run build --filter=sn", + "build:wr": "turbo run build --filter=wr", + "build:fo": "turbo run build --filter=fo", + "build:dm": "turbo run build --filter=dm", "dev:cs": "turbo run dev --filter=cs", "dev:sn": "turbo run dev --filter=sn", "dev:wr": "turbo run dev --filter=wr", From 11396a92d69106c46bd5dbfeb3c549d314a8bb40 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 03:28:04 +0900 Subject: [PATCH 18/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/next.config.ts | 2 +- apps/wr/package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wr/next.config.ts b/apps/wr/next.config.ts index 2ec36653c..35ea029a8 100644 --- a/apps/wr/next.config.ts +++ b/apps/wr/next.config.ts @@ -4,7 +4,7 @@ const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13001"; const nextConfig: NextConfig = { - // output: "standalone", + output: "standalone", async rewrites() { return { beforeFiles: [ diff --git a/apps/wr/package.json b/apps/wr/package.json index 6ed84bcd6..28438b1d8 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -7,6 +7,7 @@ "build": "next build", "start": "next start -p 3001", "start:sn": "next start -p 3001", + "start:wr": "next start -p 3001", "lint": "next lint" }, "dependencies": { From 5aed78e228288315e85f89aca1070a89face6149 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 03:50:06 +0900 Subject: [PATCH 19/42] =?UTF-8?q?error=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/store/map/splyPop/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wr/app/store/map/splyPop/index.tsx b/apps/wr/app/store/map/splyPop/index.tsx index 42dc850d3..a3fcba03b 100644 --- a/apps/wr/app/store/map/splyPop/index.tsx +++ b/apps/wr/app/store/map/splyPop/index.tsx @@ -1,4 +1,4 @@ -import { fetchType, getFetch } from "@/components/lib/api"; +import { getFetch } from "@/components/lib/api"; import type { FacilityInfo, SubsInfo } from "./type"; import { DEV_URL_FO } from "@/components/lib/config"; From a2b081eb10c5f227a7d111b8ed79393c24e3d1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Sun, 10 Aug 2025 13:04:11 +0900 Subject: [PATCH 20/42] =?UTF-8?q?CM=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/src/shared/config/api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index d6065bfc7..36dfc69a3 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -25,6 +25,7 @@ export const ENDPOINT = { OTP_VERIFY: "/user-service/api/v1/auth/2fa/verify", TWOFA_SETUP: "/user-service/api/v1/auth/2fa/setup", TWOFA_ENABLE: "/user-service/api/v1/auth/2fa/enable", + USER_JOIN: "/user-service/api/v1/anonymouse/users/join", }, CMS_SERVICE: { DASHBOARD: "/cms-service/api/v1/dashboard", From 57e363e1a3ac80dc3ed5f00d33198fedbfb63baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Sun, 10 Aug 2025 13:04:42 +0900 Subject: [PATCH 21/42] =?UTF-8?q?CM=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/.env.local | 2 +- apps/cm/app/{api => }/auth/logout/route.ts | 0 .../cm/src/entities/auth/lib/auth-wrapper.tsx | 5 + apps/cm/src/entities/code/@x/posting.ts | 1 + apps/cm/src/entities/code/api/code-service.ts | 14 + .../src/entities/code/api/lwpocode-service.ts | 73 ++ .../src/entities/code/api/upcode-service.ts | 47 ++ apps/cm/src/entities/code/index.ts | 19 + .../src/entities/code/model/code-interface.ts | 10 + .../entities/code/model/lwpocode-interface.ts | 69 ++ .../entities/code/model/upcode-interface.ts | 52 ++ apps/cm/src/entities/code/ui/code-select.tsx | 47 ++ apps/cm/src/entities/user/api/user-service.ts | 225 +++++ apps/cm/src/entities/user/index.ts | 17 + .../src/entities/user/model/user-interface.ts | 362 ++++++++ .../user/ui/password-reset-dialog.tsx | 124 +++ .../src/pages/auth/login/ui/SignupModal.tsx | 701 ++++++++++++++++ .../pages/auth/login/ui/UserSignupModal.tsx | 776 ++++++++++++++++++ .../cm/src/pages/auth/login/ui/login-page.tsx | 45 + .../src/pages/auth/login/ui/signup-modal.css | 277 +++++++ .../pages/auth/login/ui/user-signup-modal.css | 436 ++++++++++ .../src/pages/dashboard/ui/dashboard-page.tsx | 6 +- apps/cm/src/pages/users/api/zip-service.ts | 41 + apps/cm/src/pages/users/index.ts | 1 + .../src/pages/users/model/table-columns.tsx | 88 ++ .../pages/users/ui/agency-search-dialog.tsx | 179 ++++ apps/cm/src/pages/users/ui/create-page.tsx | 45 + apps/cm/src/pages/users/ui/modify-page.tsx | 75 ++ apps/cm/src/pages/users/ui/user-detail.tsx | 187 +++++ apps/cm/src/pages/users/ui/users-form.tsx | 669 +++++++++++++++ apps/cm/src/pages/users/ui/users-page.tsx | 396 +++++++++ .../src/pages/users/ui/zip-search-dialog.tsx | 224 +++++ apps/cm/src/shared/api/signup-client.ts | 71 ++ 33 files changed, 5280 insertions(+), 4 deletions(-) rename apps/cm/app/{api => }/auth/logout/route.ts (100%) create mode 100644 apps/cm/src/entities/code/@x/posting.ts create mode 100644 apps/cm/src/entities/code/api/code-service.ts create mode 100644 apps/cm/src/entities/code/api/lwpocode-service.ts create mode 100644 apps/cm/src/entities/code/api/upcode-service.ts create mode 100644 apps/cm/src/entities/code/index.ts create mode 100644 apps/cm/src/entities/code/model/code-interface.ts create mode 100644 apps/cm/src/entities/code/model/lwpocode-interface.ts create mode 100644 apps/cm/src/entities/code/model/upcode-interface.ts create mode 100644 apps/cm/src/entities/code/ui/code-select.tsx create mode 100644 apps/cm/src/entities/user/api/user-service.ts create mode 100644 apps/cm/src/entities/user/index.ts create mode 100644 apps/cm/src/entities/user/model/user-interface.ts create mode 100644 apps/cm/src/entities/user/ui/password-reset-dialog.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/SignupModal.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/signup-modal.css create mode 100644 apps/cm/src/pages/auth/login/ui/user-signup-modal.css create mode 100644 apps/cm/src/pages/users/api/zip-service.ts create mode 100644 apps/cm/src/pages/users/index.ts create mode 100644 apps/cm/src/pages/users/model/table-columns.tsx create mode 100644 apps/cm/src/pages/users/ui/agency-search-dialog.tsx create mode 100644 apps/cm/src/pages/users/ui/create-page.tsx create mode 100644 apps/cm/src/pages/users/ui/modify-page.tsx create mode 100644 apps/cm/src/pages/users/ui/user-detail.tsx create mode 100644 apps/cm/src/pages/users/ui/users-form.tsx create mode 100644 apps/cm/src/pages/users/ui/users-page.tsx create mode 100644 apps/cm/src/pages/users/ui/zip-search-dialog.tsx create mode 100644 apps/cm/src/shared/api/signup-client.ts diff --git a/apps/cm/.env.local b/apps/cm/.env.local index 896025bd6..a544bdf7e 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -3,6 +3,6 @@ NEXT_PUBLIC_PORT=3000 NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 NEXT_PUBLIC_SITE_ID=CS NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 -NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 +NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file diff --git a/apps/cm/app/api/auth/logout/route.ts b/apps/cm/app/auth/logout/route.ts similarity index 100% rename from apps/cm/app/api/auth/logout/route.ts rename to apps/cm/app/auth/logout/route.ts diff --git a/apps/cm/src/entities/auth/lib/auth-wrapper.tsx b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx index c0be4c2dd..1aa4f8e61 100644 --- a/apps/cm/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx @@ -13,6 +13,11 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + // 로그인 페이지에서는 인증 체크를 건너뜀 + if (window.location.pathname === '/auth/login') { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); diff --git a/apps/cm/src/entities/code/@x/posting.ts b/apps/cm/src/entities/code/@x/posting.ts new file mode 100644 index 000000000..0525684d9 --- /dev/null +++ b/apps/cm/src/entities/code/@x/posting.ts @@ -0,0 +1 @@ +export { CodeSelect } from "../ui/code-select"; diff --git a/apps/cm/src/entities/code/api/code-service.ts b/apps/cm/src/entities/code/api/code-service.ts new file mode 100644 index 000000000..0d1055f6a --- /dev/null +++ b/apps/cm/src/entities/code/api/code-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { CodeService, ICode } from "../model/code-interface"; + +export class CodeApiService implements CodeService { + public async getCode(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`, + ); + return response.data; + } +} + +export const CodeApi = new CodeApiService(); diff --git a/apps/cm/src/entities/code/api/lwpocode-service.ts b/apps/cm/src/entities/code/api/lwpocode-service.ts new file mode 100644 index 000000000..818ddb120 --- /dev/null +++ b/apps/cm/src/entities/code/api/lwpocode-service.ts @@ -0,0 +1,73 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + LwpoCodeFormSchema, + type LwpoCodeService, + type LwpoCodeList, + type LwpoCodeDetail, + type LwpoCodeParams, + type LwpoCodeFormData, +} from "../model/lwpocode-interface"; + +export class LwpoCodeApiService implements LwpoCodeService { + public async getLwpoCodeList(params?: LwpoCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + { params }, + ); + return response.data; + } + + public async getUpCodeLwpoCodeList(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`, + ); + return response.data; + } + + public async lwpoCodeDetail( + upCd: string, + lwpoCd: string, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + ); + return response.data; + } + + public async createLwpoCode(data: LwpoCodeFormData): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + validatedData, + ); + return response.data; + } + + public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + ); + } + + public async codeRefreshAll(): Promise { + await client.post( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/cache/refresh-all`, + ); + } +} + +export const LwpoCodeApi = new LwpoCodeApiService(); diff --git a/apps/cm/src/entities/code/api/upcode-service.ts b/apps/cm/src/entities/code/api/upcode-service.ts new file mode 100644 index 000000000..dcccd9534 --- /dev/null +++ b/apps/cm/src/entities/code/api/upcode-service.ts @@ -0,0 +1,47 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + upCodeFormSchema, + type UpCodeService, + type UpCodeList, + type UpCodeDetail, + type UpCodeParams, + type UpCodeFormData, +} from "../model/upcode-interface"; + +export class UpCodeApiService implements UpCodeService { + public async getUpCodeList(params?: UpCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + { params }, + ); + return response.data; + } + + public async upCodeDetail(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + ); + return response.data; + } + + public async createUpCode(data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateUpCode(upCd: string, data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + validatedData, + ); + return response.data; + } +} + +export const UpCodeApi = new UpCodeApiService(); diff --git a/apps/cm/src/entities/code/index.ts b/apps/cm/src/entities/code/index.ts new file mode 100644 index 000000000..e166e2c29 --- /dev/null +++ b/apps/cm/src/entities/code/index.ts @@ -0,0 +1,19 @@ +export { CodeApi } from "./api/code-service"; +export type { ICode } from "./model/code-interface"; +export { CodeSelect } from "./ui/code-select"; + +export { UpCodeApi } from "./api/upcode-service"; +export { + upCodeFormSchema, + type UpCode, + type UpCodeFormData, + type UpCodeParams, +} from "./model/upcode-interface"; + +export { LwpoCodeApi } from "./api/lwpocode-service"; +export { + LwpoCodeFormSchema, + type LwpoCode, + type LwpoCodeFormData, + type LwpoCodeParams, +} from "./model/lwpocode-interface"; diff --git a/apps/cm/src/entities/code/model/code-interface.ts b/apps/cm/src/entities/code/model/code-interface.ts new file mode 100644 index 000000000..4562e2559 --- /dev/null +++ b/apps/cm/src/entities/code/model/code-interface.ts @@ -0,0 +1,10 @@ +export interface ICode { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + unit: string; +} + +export interface CodeService { + getCode(upCd: string): Promise; +} diff --git a/apps/cm/src/entities/code/model/lwpocode-interface.ts b/apps/cm/src/entities/code/model/lwpocode-interface.ts new file mode 100644 index 000000000..2538c1c30 --- /dev/null +++ b/apps/cm/src/entities/code/model/lwpocode-interface.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface LwpoCodeParams extends PageableParams { + upCd?: string; + lwpoCd?: string; + lwpoCdNm?: string; + useYn?: string; +} + +export interface LwpoCode { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: number; + regDt: string; + rgTr: string; + rgTrNm: string; + mdfcnDt: string; + mdFr: string; +} + +export interface LwpoCodeDetail extends LwpoCodeFormData { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: 0; +} + +export const LwpoCodeFormSchema = z.object({ + lwpoCd: z.string().max(10).nonempty(), + upCd: z.string().max(10).nonempty(), + lwpoCdNm: z.string().max(100).nonempty(), + lwpoCdExpln: z.string().max(1000).nullish(), + unit: z.string().max(20).nullish(), + prntLwpoCd: z.string().max(10).nullish(), + useYn: z.string(), + sortSeq: z.number(), + delYn: z.string(), +}); + +export type LwpoCodeFormData = z.infer; +export type LwpoCodeList = PageableList; + +export interface LwpoCodeService { + getLwpoCodeList(params: LwpoCodeParams): Promise; + getUpCodeLwpoCodeList(upCd: string): Promise; + lwpoCodeDetail(upCd: string, lwpoCd: string): Promise; + updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; + createLwpoCode(data: LwpoCodeFormData): Promise; + deleteLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; + codeRefreshAll(): Promise; +} diff --git a/apps/cm/src/entities/code/model/upcode-interface.ts b/apps/cm/src/entities/code/model/upcode-interface.ts new file mode 100644 index 000000000..73bf4f3ae --- /dev/null +++ b/apps/cm/src/entities/code/model/upcode-interface.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface UpCodeParams extends PageableParams { + keyword?: string; + upCd?: string; + cdSe?: string; + useYn?: string; + siteId?: string; +} + +export interface UpCode { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + regDt: string; +} + +export interface UpCodeDetail extends UpCodeFormData { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + delYn: string; + regDt: string; + rgTrNm: string; +} + +export const upCodeFormSchema = z.object({ + upCd: z.string().max(10).nonempty(), + upCdNm: z.string().max(100).nonempty(), + upCdExpln: z.string().max(1000).nullish(), + cdSe: z.string().max(10).nonempty(), + siteId: z.string().max(2).nullish(), + useYn: z.string(), + delYn: z.string(), +}); + +export type UpCodeFormData = z.infer; +export type UpCodeList = PageableList; + +export interface UpCodeService { + getUpCodeList(params: UpCodeParams): Promise; + upCodeDetail(upCodeId: string): Promise; + updateUpCode(upCodeId: string, data: UpCodeFormData): Promise; + createUpCode(data: UpCodeFormData): Promise; +} diff --git a/apps/cm/src/entities/code/ui/code-select.tsx b/apps/cm/src/entities/code/ui/code-select.tsx new file mode 100644 index 000000000..08df4ce51 --- /dev/null +++ b/apps/cm/src/entities/code/ui/code-select.tsx @@ -0,0 +1,47 @@ +import { useQuery } from "@tanstack/react-query"; +import { + cn, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "../api/code-service"; + +type CodeSelectProps = { + upCd: string; + placeholder?: string; + className?: string; +} & React.ComponentPropsWithoutRef; + +function CodeSelect({ + upCd, + placeholder = "선택", + className, + ...props +}: CodeSelectProps) { + const { data: code } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd], + queryFn: () => CodeApi.getCode(upCd), + enabled: !!upCd, + }); + + return ( + + ); +} + +export { CodeSelect }; diff --git a/apps/cm/src/entities/user/api/user-service.ts b/apps/cm/src/entities/user/api/user-service.ts new file mode 100644 index 000000000..482d51e16 --- /dev/null +++ b/apps/cm/src/entities/user/api/user-service.ts @@ -0,0 +1,225 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + UserService, + UserSearchParams, + UserList, + UserDetailResponse, + UserSttsStatsList, + UserUpdateData, + UserPasswordUpdateData, + UserFindPasswordUpdateData, + UserPasswordMatchData, + UserUpdateInfoData, + UserSaveData, + UserFindPasswordSaveData, + UserVerifyData, + UserEmailData, + UserIdData, + AdminUserList, +} from "../model/user-interface"; + +export class UserApiService implements UserService { + // 사용자 목록 조회 + public async findPageUser(params: UserSearchParams): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}`, + { params }, + ); + return response.data; + } + + // 사용자 단건 조회 + public async findByUserUnqId(userUnqId: string): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`, + ); + return response.data; + } + + // 로그인한 사용자 정보 조회 + public async findByUserUnqIdByLogin(): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + return response.data; + } + + // 사용자 회원상태 통계 조회 + public async selectUserSttsStats(): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/stts/stats`, + ); + return response.data; + } + + // 사용자 저장 + public async saveUser(data: UserSaveData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}`, + data, + ); + return response.data; + } + + // 사용자 수정 + public async updateUser( + userUnqId: string, + data: UserUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 삭제 + public async deleteUser(userUnqId: string): Promise { + const response = await client.delete( + `${ENDPOINT.USER_SERVICE.USERS}/delete/${userUnqId}`, + ); + return response.data; + } + + // 사용자 계정잠금 처리 + public async lockUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/lock/${userUnqId}`, + ); + return response.data; + } + + // 사용자 계정잠금 해제처리 + public async unlockUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/unlock/${userUnqId}`, + ); + return response.data; + } + + // 리프레시 토큰 정보 재발급 + public async refreshToken(): Promise { + await client.put(`${ENDPOINT.USER_SERVICE.USERS}/token/refresh`); + } + + // 사용자 비밀번호 변경 + public async updatePassword(data: UserPasswordUpdateData): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/update`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 초기화(관리자용) + public async initializePassword( + userUnqId: string, + data: UserPasswordUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/initialize/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 변경 + public async changePassword( + data: UserFindPasswordUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/change`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 확인 + public async matchPassword(data: UserPasswordMatchData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/password/match`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 + public async findPasswordUser( + data: UserFindPasswordSaveData, + ): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/password/find`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 유효성 확인 + public async validPassword(token: string): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/password/valid/${token}`, + ); + return response.data; + } + + // 사용자 회원정보 변경 + public async updateInfo( + userUnqId: string, + data: UserUpdateInfoData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/info/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 승인처리 + public async applyUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/apply/${userUnqId}`, + ); + return response.data; + } + + // 사용자 회원탈퇴 + public async leaveUser(data: UserVerifyData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/leave`, + data, + ); + return response.data; + } + + // 사용자 이메일 중복 체크 + public async existsEmail(data: UserEmailData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/exists`, + data, + ); + return response.data; + } + + // 사용자 아이디 중복 체크 + public async existsUserId(data: UserIdData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, + data, + ); + return response.data; + } + + // 관리자 목록 조회 + public async findPageAdminUser( + params: UserSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/admin/list`, + { params }, + ); + return response.data; + } +} + +export const UserApi = new UserApiService(); diff --git a/apps/cm/src/entities/user/index.ts b/apps/cm/src/entities/user/index.ts new file mode 100644 index 000000000..8927256f9 --- /dev/null +++ b/apps/cm/src/entities/user/index.ts @@ -0,0 +1,17 @@ +export { UserApi } from "./api/user-service"; +export { + USER_STATE_CODE, + userSaveSchema, + userPasswordUpdateSchema, + userUpdateSchema, + type UserStateCode, + type UserSearchParams, + type UserListResponse, + type UserSaveData, + type UserDetailResponse, + type UserPasswordUpdateData, + type UserUpdateData, + type AdminUserList, + type AdminUserListResponse, +} from "./model/user-interface"; +export { PasswordResetDialog } from "./ui/password-reset-dialog"; diff --git a/apps/cm/src/entities/user/model/user-interface.ts b/apps/cm/src/entities/user/model/user-interface.ts new file mode 100644 index 000000000..012fe9e92 --- /dev/null +++ b/apps/cm/src/entities/user/model/user-interface.ts @@ -0,0 +1,362 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const USER_STATE_CODE = { + 대기: "00", + 정상: "01", + 정지: "07", + 탈퇴: "08", + 삭제: "09", +} as const; + +export type UserStateCode = + (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE]; + +// 사용자 검색 파라미터 +export interface UserSearchParams extends PageableParams { + keywordType?: string; + keyword?: string; + userStateCode?: string; + aprvYn?: string; + grpId?: string; + frDateYmd?: string; + toDateYmd?: string; + userName?: string; +} + +// 사용자 목록 응답 인터페이스 +export interface UserListResponse { + userId: string; + userName: string; + email: string; + roleId: string; + roleName: string; + userStateCode: UserStateCode; + userStateCodeName: string; + lastLoginDate: string; + loginFailCount: number; + userUnqId: string; + mphn: string; + telno: string; + aprvYn: string; + regDt: string; + laeSeCd: string; + laeSeNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + instSeCd: string; + instSeNm: string; + aprvYmd: string; +} + +// 사용자 상세 응답 인터페이스 +export interface UserDetailResponse { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: UserStateCode; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; + twoFactorEnabled: boolean; + adminRoleYn: string; + instSeCd: string; + aprvYmd: string; + enoCd: string; + dtlInstCd: string; + laeSeNm: string; + ogdpSeNm: string; + instSeNm: string; + userStateNm: string; + pswdHntNm: string; + ecoNm: string; + dtlInstNm: string; +} + +// 사용자 상태 통계 응답 인터페이스 +export interface UserSttsStatsResponse { + sttsCd: string; + sttsNm: string; + cnt: number; +} + +// 관리자 목록 응답 인터페이스 +export interface AdminUserListResponse { + userUnqId: string; + userId: string; + userName: string; + groupId: string; + groupNm: string; + userStateCode: string; + userStateCodeName: string; + laeSeCd: string; + laeSeNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + instSeCd: string; + instSeNm: string; + dtlInstCd: string; + dtlInstNm: string; + mdfr: string; + mdfrNm: string; + mdfcnDt: string; +} + +// 사용자 수정 요청 스키마 +export const userUpdateSchema = z.object({ + userId: z.string().optional(), + userName: z.string(), + email: z.string().email().optional(), + password: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ) + .optional(), + roleId: z.string().optional(), + userStateCode: z.string(), + pswdHnt: z.string().max(10), + pswdAns: z.string().max(100).optional(), + mphn: z.string().max(11).optional(), + telno: z.string().max(11).optional(), + zip: z.string().max(5).optional(), + addr: z.string().max(200).optional(), + daddr: z.string().max(200).optional(), + laeSeCd: z.string().max(10), + ogdpSeCd: z.string().max(10), + aprvYn: z.string(), + autzrId: z.string().uuid().optional(), + expryYmd: z.string().max(8), + instSeCd: z.string().optional(), + dtlInstCd: z.string().optional(), + aprvYmd: z.string().max(8).optional(), + enoCd: z.string().optional(), +}); + +// 사용자 비밀번호 변경 스키마 +export const userPasswordUpdateSchema = z + .object({ + password: z.string().optional(), + provider: z.string().optional(), + token: z.string().optional(), + newPassword: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ), + newPasswordConfirm: z.string(), + }) + .refine((data) => data.newPassword === data.newPasswordConfirm, { + message: "비밀번호가 일치하지 않습니다.", + path: ["newPasswordConfirm"], + }); + +// 사용자 비밀번호 찾기 변경 스키마 +export const userFindPasswordUpdateSchema = z.object({ + tokenValue: z.string(), + password: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ), +}); + +// 사용자 비밀번호 확인 스키마 +export const userPasswordMatchSchema = z.object({ + password: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ), +}); + +// 사용자 회원정보 변경 스키마 +export const userUpdateInfoSchema = z.object({ + password: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ) + .optional(), + provider: z.string().optional(), + token: z.string().optional(), + userName: z.string(), + email: z.string().email().optional(), +}); + +// 사용자 저장 스키마 +export const userSaveSchema = z.object({ + userId: z.string(), + userName: z.string(), + email: z.string().email().optional(), + password: z + .string() + .regex( + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/, + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.", + ), + pswdHnt: z.string().max(10), + pswdAns: z.string().max(100).optional(), + mphn: z.string().max(11).optional(), + telno: z.string().max(11).optional(), + zip: z.string().max(5).optional(), + addr: z.string().max(200).optional(), + daddr: z.string().max(200).optional(), + laeSeCd: z.string().max(10), + ogdpSeCd: z.string().max(10), + expryYmd: z.string().max(8), + instSeCd: z.string().optional(), + dtlInstCd: z.string().optional(), + enoCd: z.string().optional(), +}); + +// 사용자 비밀번호 찾기 저장 스키마 +export const userFindPasswordSaveSchema = z.object({ + userName: z.string(), + emailAddr: z.string().email(), + mainUrl: z.string(), + changePasswordUrl: z.string(), +}); + +// 사용자 인증 요청 스키마 +export const userVerifySchema = z.object({ + password: z + .string() + .regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/) + .optional(), + provider: z.string().optional(), + token: z.string().optional(), +}); + +// 사용자 이메일 요청 스키마 +export const userEmailSchema = z.object({ + userId: z.string().optional(), + email: z.string().email(), +}); + +// 사용자 ID 요청 스키마 +export const userIdSchema = z.object({ + userId: z.string(), +}); + +// Zod 스키마 타입 정의 +export type UserUpdateData = z.infer; +export type UserPasswordUpdateData = z.infer; +export type UserFindPasswordUpdateData = z.infer< + typeof userFindPasswordUpdateSchema +>; +export type UserPasswordMatchData = z.infer; +export type UserUpdateInfoData = z.infer; +export type UserSaveData = z.infer; +export type UserFindPasswordSaveData = z.infer< + typeof userFindPasswordSaveSchema +>; +export type UserVerifyData = z.infer; +export type UserEmailData = z.infer; +export type UserIdData = z.infer; + +// 페이지네이션 타입 정의 +export type UserList = PageableList; +export type UserSttsStatsList = UserSttsStatsResponse[]; +export type AdminUserList = PageableList; + +// 사용자 서비스 인터페이스 +export interface UserService { + // 사용자 목록 조회 + findPageUser: (params: UserSearchParams) => Promise; + + // 사용자 단건 조회 + findByUserUnqId: (userUnqId: string) => Promise; + + // 로그인한 사용자 정보 조회 + findByUserUnqIdByLogin: () => Promise; + + // 사용자 회원상태 통계 조회 + selectUserSttsStats: () => Promise; + + // 사용자 저장 + saveUser: (data: UserSaveData) => Promise; + + // 사용자 수정 + updateUser: (userUnqId: string, data: UserUpdateData) => Promise; + + // 사용자 삭제 + deleteUser: (userUnqId: string) => Promise; + + // 사용자 계정잠금 처리 + lockUser: (userUnqId: string) => Promise; + + // 사용자 계정잠금 해제처리 + unlockUser: (userUnqId: string) => Promise; + + // 리프레시 토큰 정보 재발급 + refreshToken: () => Promise; + + // 사용자 비밀번호 변경 + updatePassword: (data: UserPasswordUpdateData) => Promise; + + // 사용자 비밀번호 초기화(관리자용) + initializePassword: ( + userUnqId: string, + data: UserPasswordUpdateData, + ) => Promise; + + // 사용자 비밀번호 찾기 변경 + changePassword: (data: UserFindPasswordUpdateData) => Promise; + + // 사용자 비밀번호 확인 + matchPassword: (data: UserPasswordMatchData) => Promise; + + // 사용자 비밀번호 찾기 + findPasswordUser: (data: UserFindPasswordSaveData) => Promise; + + // 사용자 비밀번호 찾기 유효성 확인 + validPassword: (token: string) => Promise; + + // 사용자 회원정보 변경 + updateInfo: (userUnqId: string, data: UserUpdateInfoData) => Promise; + + // 사용자 승인처리 + applyUser: (userUnqId: string) => Promise; + + // 사용자 회원탈퇴 + leaveUser: (data: UserVerifyData) => Promise; + + // 사용자 이메일 중복 체크 + existsEmail: (data: UserEmailData) => Promise; + + // 사용자 아이디 중복 체크 + existsUserId: (data: UserIdData) => Promise; + + // 관리자 목록 조회 + findPageAdminUser: (params: UserSearchParams) => Promise; +} diff --git a/apps/cm/src/entities/user/ui/password-reset-dialog.tsx b/apps/cm/src/entities/user/ui/password-reset-dialog.tsx new file mode 100644 index 000000000..0d6aaf94e --- /dev/null +++ b/apps/cm/src/entities/user/ui/password-reset-dialog.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, + Button, + Input, + DialogTrigger, +} from "@common/components"; +import { + UserApi, + userPasswordUpdateSchema, + type UserPasswordUpdateData, +} from "@/entities/user"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/shared/ui/form"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; + +interface PasswordResetDialogProps { + userUnqId: string; + children: React.ReactNode; +} + +export function PasswordResetDialog({ + userUnqId, + children, +}: PasswordResetDialogProps) { + const alert = useAlertStore((state) => state.setMessage); + + const [open, setOpen] = useState(false); + + const form = useForm({ + resolver: zodResolver(userPasswordUpdateSchema), + defaultValues: { + newPassword: "", + newPasswordConfirm: "", + provider: "password", + }, + }); + + const { mutate: onPasswordReset } = useMutationWrapper({ + mutationFn: (data: UserPasswordUpdateData) => + UserApi.initializePassword(userUnqId, data), + onSuccess: () => { + alert("비밀번호가 초기화되었습니다.", () => { + setOpen(false); + form.reset(); + }); + }, + }); + + return ( + + {children} + + + 비밀번호 초기화 + +
+ onPasswordReset(data))} + className="space-y-6" + > + ( + + 새 비밀번호 + + + + + + )} + /> + ( + + 새 비밀번호 확인 + + + + + + )} + /> + + + + + + + + +
+
+ ); +} diff --git a/apps/cm/src/pages/auth/login/ui/SignupModal.tsx b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx new file mode 100644 index 000000000..d61949ccc --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx @@ -0,0 +1,701 @@ +"use client"; + +import { useState, useEffect } from "react"; +import type { UserSaveData } from "@/entities/user"; +import { ENDPOINT, BASE_URL } from "@/shared/config"; +import { SignupClient } from "@/shared/api/signup-client"; +import "./signup-modal.css"; + +interface SignupModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onClose: () => void; + onSuccess: () => void; +} + +function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { + const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement'); + const [agreed, setAgreed] = useState(false); + const [formData, setFormData] = useState({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + expryYmd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + + const [errors, setErrors] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const [isCheckingId, setIsCheckingId] = useState(false); + const [isCheckingEmail, setIsCheckingEmail] = useState(false); + const [idAvailable, setIdAvailable] = useState(null); + const [emailAvailable, setEmailAvailable] = useState(null); + + // 모달이 닫힐 때 초기화 + useEffect(() => { + if (!open) { + setCurrentStep('agreement'); + setAgreed(false); + setFormData({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + expryYmd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + setErrors({}); + setIdAvailable(null); + setEmailAvailable(null); + } + }, [open]); + + const handleAgreementChange = (e: React.ChangeEvent) => { + setAgreed(e.target.checked); + }; + + const handleNextStep = () => { + if (agreed) { + setCurrentStep('signup'); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + + // 에러 초기화 + if (errors[name]) { + setErrors(prev => ({ ...prev, [name]: "" })); + } + }; + + const validateForm = () => { + const newErrors: Record = {}; + + // 필수 필드 검증 + if (!formData.userId) newErrors.userId = "아이디를 입력해주세요."; + if (!formData.userName) newErrors.userName = "이름을 입력해주세요."; + if (!formData.password) newErrors.password = "비밀번호를 입력해주세요."; + if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; + if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요."; + if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요."; + if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요."; + + // 비밀번호 확인 + if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "비밀번호가 일치하지 않습니다."; + } + + // 비밀번호 정규식 검증 + const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; + if (formData.password && !passwordRegex.test(formData.password)) { + newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; + } + + // 이메일 형식 검증 + if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "올바른 이메일 형식을 입력해주세요."; + } + + // 중복 체크 검증 + if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다."; + if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다."; + + setErrors(newErrors); + const isValid = Object.keys(newErrors).length === 0; + console.log("폼 검증 결과:", { isValid, errors: newErrors, formData }); + return isValid; + }; + + const checkUserIdDuplicate = async () => { + if (!formData.userId) { + setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." })); + return; + } + + setIsCheckingId(true); + try { + // SignupClient를 통해 중복 체크 API 호출 + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL, + }); + const response = await signupClient.post( + `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, + { userId: formData.userId } + ); + + const exists = response.data; + setIdAvailable(!exists); + if (exists) { + setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." })); + } else { + setErrors(prev => ({ ...prev, userId: "" })); + } + } catch (error) { + console.error("아이디 중복 체크 오류:", error); + setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." })); + } finally { + setIsCheckingId(false); + } + }; + + const checkEmailDuplicate = async () => { + if (!formData.email) { + setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." })); + return; + } + + setIsCheckingEmail(true); + try { + // SignupClient를 통해 중복 체크 API 호출 + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL, + }); + const response = await signupClient.post( + `${ENDPOINT.USER_SERVICE.USERS}/exists`, + { email: formData.email } + ); + + const exists = response.data; + setEmailAvailable(!exists); + if (exists) { + setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." })); + } else { + setErrors(prev => ({ ...prev, email: "" })); + } + } catch (error) { + console.error("이메일 중복 체크 오류:", error); + setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." })); + } finally { + setIsCheckingEmail(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + console.log("회원가입 버튼 클릭됨"); + + if (!validateForm()) { + console.log("폼 검증 실패"); + return; + } + + setIsLoading(true); + try { + const signupData: UserSaveData = { + userId: formData.userId, + userName: formData.userName, + email: formData.email || undefined, + password: formData.password, + pswdHnt: formData.pswdHnt, + pswdAns: formData.pswdAns || undefined, + mphn: formData.mphn || undefined, + telno: formData.telno || undefined, + zip: formData.zip || undefined, + addr: formData.addr || undefined, + daddr: formData.daddr || undefined, + laeSeCd: formData.laeSeCd, + ogdpSeCd: formData.ogdpSeCd, + expryYmd: "", + instSeCd: formData.instSeCd || undefined, + dtlInstCd: formData.dtlInstCd || undefined, + enoCd: formData.enoCd || undefined, + }; + + // SignupClient를 통해 익명사용자 회원가입 API 호출 + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL + }); + const response = await signupClient.post( + `${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`, + signupData + ); + + console.log("회원가입 성공:", response.data); + + // 성공 시 모달 닫기 및 콜백 호출 + onSuccess(); + onClose(); + + // 폼 초기화 + setFormData({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + expryYmd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + setErrors({}); + setIdAvailable(null); + setEmailAvailable(null); + + } catch (error: unknown) { + console.error("회원가입 오류:", error); + if (error instanceof Error) { + setErrors(prev => ({ ...prev, submit: error.message })); + } else { + setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." })); + } + } finally { + setIsLoading(false); + } + }; + + if (!open) return null; + + return ( +
+
e.stopPropagation()}> +
+

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

+ +
+ + {currentStep === 'agreement' ? ( +
+
+

개인정보 수집 및 이용 동의

+

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

+ +

1. 수집하는 개인정보 항목

+
    +
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • +
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
+ +

2. 개인정보의 수집 및 이용목적

+
    +
  • 회원가입 및 관리
  • +
  • 서비스 제공 및 운영
  • +
  • 고객상담 및 문의응답
  • +
+ +

3. 개인정보의 보유 및 이용기간

+

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

+ +

4. 동의 거부권 및 거부 시 불이익

+

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+
+ +
+ +
+ +
+ + +
+
+ ) : ( +
+
+

기본 정보

+ +
+
+ +
+ + +
+ {idAvailable === true && 사용 가능한 아이디입니다.} + {errors.userId && {errors.userId}} +
+
+ +
+
+ + + {errors.userName && {errors.userName}} +
+
+ +
+
+ +
+ + +
+ {emailAvailable === true && 사용 가능한 이메일입니다.} + {errors.email && {errors.email}} +
+
+ +
+
+ + + {errors.password && {errors.password}} +
+
+ +
+
+ + + {errors.confirmPassword && {errors.confirmPassword}} +
+
+ +
+
+ + + {errors.pswdHnt && {errors.pswdHnt}} +
+
+ +
+
+ + + {errors.pswdAns && {errors.pswdAns}} +
+
+
+ +
+

연락처 정보

+ +
+
+ + + {errors.mphn && {errors.mphn}} +
+
+ +
+
+ + + {errors.telno && {errors.telno}} +
+
+
+ +
+

주소 정보

+ +
+
+ + + {errors.zip && {errors.zip}} +
+
+ +
+
+ + + {errors.addr && {errors.addr}} +
+
+ +
+
+ + + {errors.daddr && {errors.daddr}} +
+
+
+ +
+

기관 정보

+ +
+
+ + + {errors.laeSeCd && {errors.laeSeCd}} +
+
+ +
+
+ + + {errors.ogdpSeCd && {errors.ogdpSeCd}} +
+
+ +
+
+ + + {errors.instSeCd && {errors.instSeCd}} +
+
+ +
+
+ + + {errors.dtlInstCd && {errors.dtlInstCd}} +
+
+
+ + {errors.submit && ( +
+
{errors.submit}
+
+ )} + +
+ + + +
+
+ )} +
+
+ ); +} + +export default SignupModal; diff --git a/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx b/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx new file mode 100644 index 000000000..06193951e --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx @@ -0,0 +1,776 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { SignupClient } from "@/shared/api/signup-client"; +import { BASE_URL, ENDPOINT } from "@/shared/config"; +import type { UserSaveData } from "@/entities/user"; +import "./user-signup-modal.css"; + +interface UserSignupModalProps { + open: boolean; + onClose: () => void; + onSuccess: () => void; +} + +interface UserSignupData { + userId: string; + userName: string; + email?: string; + password: string; + pswdHnt: string; + pswdAns?: string; + mphn?: string; + telno?: string; + zip?: string; + addr?: string; + daddr?: string; + laeSeCd: string; + ogdpSeCd: string; + instSeCd?: string; + dtlInstCd?: string; + enoCd?: string; +} + + + +// 익명 사용자용 CodeSelect 컴포넌트 +interface AnonymousCodeSelectProps { + upCd: string; + value: string; + onValueChange: (value: string) => void; + disabled?: boolean; + placeholder?: string; +} + +function AnonymousCodeSelect({ upCd, value, onValueChange, disabled, placeholder = "선택" }: AnonymousCodeSelectProps) { + const [codes, setCodes] = useState>([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchCodes = async () => { + if (!upCd) return; + + setIsLoading(true); + try { + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL + }); + + const response = await signupClient.get( + `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}` + ); + + if (response.data && Array.isArray(response.data)) { + setCodes(response.data); + } else { + setCodes([]); + } + } catch (error) { + console.error(`공통 코드 로드 실패 (${upCd}):`, error); + setCodes([]); + } finally { + setIsLoading(false); + } + }; + + fetchCodes(); + }, [upCd]); + + return ( + + ); +} + +function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { + const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement'); + const [agreed, setAgreed] = useState(false); + const [formData, setFormData] = useState({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + + const [errors, setErrors] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const [isCheckingId, setIsCheckingId] = useState(false); + const [isCheckingEmail, setIsCheckingEmail] = useState(false); + const [idAvailable, setIdAvailable] = useState(null); + const [emailAvailable, setEmailAvailable] = useState(null); + + // 모달이 닫힐 때 초기화 + useEffect(() => { + if (!open) { + setCurrentStep('agreement'); + setAgreed(false); + setFormData({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + setErrors({}); + setIdAvailable(null); + setEmailAvailable(null); + } + }, [open]); + + const handleAgreementChange = (e: React.ChangeEvent) => { + setAgreed(e.target.checked); + }; + + const handleNextStep = () => { + if (agreed) { + setCurrentStep('signup'); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + + // 에러 초기화 + if (errors[name]) { + setErrors(prev => ({ ...prev, [name]: "" })); + } + }; + + const validateForm = () => { + const newErrors: Record = {}; + + console.log("폼 검증 시작:", formData); + + // 필수 필드 검증 + if (!formData.userId) newErrors.userId = "아이디를 입력해주세요."; + if (!formData.userName) newErrors.userName = "이름을 입력해주세요."; + if (!formData.password) newErrors.password = "비밀번호를 입력해주세요."; + if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; + if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 선택해주세요."; + if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요."; + if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요."; + + // 비밀번호 확인 + if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "비밀번호가 일치하지 않습니다."; + } + + // 비밀번호 정규식 검증 + const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; + if (formData.password && !passwordRegex.test(formData.password)) { + newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; + } + + // 이메일 형식 검증 + if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "올바른 이메일 형식을 입력해주세요."; + } + + // 중복 체크 검증 + if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다."; + if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다."; + + setErrors(newErrors); + const isValid = Object.keys(newErrors).length === 0; + console.log("폼 검증 결과:", { isValid, errors: newErrors, formData }); + return isValid; + }; + + const checkUserIdDuplicate = async () => { + if (!formData.userId) { + setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." })); + return; + } + + setIsCheckingId(true); + try { + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL, + }); + const response = await signupClient.post( + `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, + { userId: formData.userId } + ); + + const exists = response.data; + setIdAvailable(!exists); + if (exists) { + setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." })); + } else { + setErrors(prev => ({ ...prev, userId: "" })); + } + } catch (error) { + console.error("아이디 중복 체크 오류:", error); + setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." })); + } finally { + setIsCheckingId(false); + } + }; + + const checkEmailDuplicate = async () => { + if (!formData.email) { + setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." })); + return; + } + + setIsCheckingEmail(true); + try { + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL, + }); + const response = await signupClient.post( + `${ENDPOINT.USER_SERVICE.USERS}/exists`, + { email: formData.email } + ); + + const exists = response.data; + setEmailAvailable(!exists); + if (exists) { + setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." })); + } else { + setErrors(prev => ({ ...prev, email: "" })); + } + } catch (error) { + console.error("이메일 중복 체크 오류:", error); + setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." })); + } finally { + setIsCheckingEmail(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + console.log("회원가입 버튼 클릭됨 - handleSubmit"); + console.log("현재 폼 데이터:", formData); + + if (!validateForm()) { + console.log("폼 검증 실패"); + return; + } + + console.log("폼 검증 성공, 회원가입 API 호출 시작"); + setIsLoading(true); + try { + const signupData: UserSignupData = { + userId: formData.userId, + userName: formData.userName, + email: formData.email || undefined, + password: formData.password, + pswdHnt: formData.pswdHnt, + pswdAns: formData.pswdAns || undefined, + mphn: formData.mphn || undefined, + telno: formData.telno || undefined, + zip: formData.zip || undefined, + addr: formData.addr || undefined, + daddr: formData.daddr || undefined, + laeSeCd: formData.laeSeCd, + ogdpSeCd: formData.ogdpSeCd, + instSeCd: formData.instSeCd || undefined, + dtlInstCd: formData.dtlInstCd || undefined, + enoCd: formData.enoCd || undefined, + }; + + // SignupClient를 통해 익명사용자 회원가입 API 호출 + console.log("SignupClient 생성 및 API 호출:", { + baseURL: BASE_URL, + endpoint: ENDPOINT.USER_SERVICE.USER_JOIN, + data: signupData + }); + + const signupClient = SignupClient.getInstance({ + baseURL: BASE_URL + }); + const response = await signupClient.post( + ENDPOINT.USER_SERVICE.USER_JOIN, + signupData + ); + + console.log("회원가입 성공:", response.data); + + // 성공 시 모달 닫기 및 콜백 호출 + onSuccess(); + onClose(); + + // 폼 초기화 + setFormData({ + userId: "", + userName: "", + email: "", + password: "", + confirmPassword: "", + pswdHnt: "", + pswdAns: "", + mphn: "", + telno: "", + zip: "", + addr: "", + daddr: "", + laeSeCd: "", + ogdpSeCd: "", + instSeCd: "", + dtlInstCd: "", + enoCd: "", + }); + setErrors({}); + setIdAvailable(null); + setEmailAvailable(null); + + } catch (error: unknown) { + console.error("회원가입 오류:", error); + if (error instanceof Error) { + setErrors(prev => ({ ...prev, submit: error.message })); + } else { + setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." })); + } + } finally { + setIsLoading(false); + } + }; + + if (!open) return null; + + return ( +
+
+
+

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

+ +
+ + {currentStep === 'agreement' ? ( +
+
+

개인정보 수집 및 이용 동의

+

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

+ +

1. 수집하는 개인정보 항목

+
    +
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • +
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
+ +

2. 개인정보의 수집 및 이용목적

+
    +
  • 회원가입 및 관리
  • +
  • 서비스 제공 및 운영
  • +
  • 고객상담 및 문의응답
  • +
+ +

3. 개인정보의 보유 및 이용기간

+

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

+ +

4. 동의 거부권 및 거부 시 불이익

+

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+
+ +
+ +
+ +
+ + +
+
+ ) : ( +
+
+

기본 정보

+ +
+
+ +
+ + +
+ {idAvailable === true && 사용 가능한 아이디입니다.} + {errors.userId && {errors.userId}} +
+
+ +
+
+ + + {errors.userName && {errors.userName}} +
+
+ +
+
+ +
+ + +
+ {emailAvailable === true && 사용 가능한 이메일입니다.} + {errors.email && {errors.email}} +
+
+ +
+
+ + + {errors.password && {errors.password}} +
+
+ +
+
+ + + {errors.confirmPassword && {errors.confirmPassword}} +
+
+ +
+
+ + setFormData(prev => ({ ...prev, pswdHnt: value }))} + disabled={isLoading} + placeholder="비밀번호 힌트를 선택하세요" + /> + {errors.pswdHnt && {errors.pswdHnt}} +
+
+ +
+
+ + + {errors.pswdAns && {errors.pswdAns}} +
+
+
+ +
+

연락처 정보

+ +
+
+ + + {errors.mphn && {errors.mphn}} +
+
+ +
+
+ + + {errors.telno && {errors.telno}} +
+
+
+ +
+

주소 정보

+ +
+
+ + + {errors.zip && {errors.zip}} +
+
+ +
+
+ + + {errors.addr && {errors.addr}} +
+
+ +
+
+ + + {errors.daddr && {errors.daddr}} +
+
+
+ +
+

기관 정보

+ +
+
+ + setFormData(prev => ({ ...prev, laeSeCd: value }))} + disabled={isLoading} + placeholder="법인구분을 선택하세요" + /> + {errors.laeSeCd && {errors.laeSeCd}} +
+
+ +
+
+ + setFormData(prev => ({ ...prev, ogdpSeCd: value }))} + disabled={isLoading} + placeholder="기관구분을 선택하세요" + /> + {errors.ogdpSeCd && {errors.ogdpSeCd}} +
+
+ +
+
+ + + {errors.instSeCd && {errors.instSeCd}} +
+
+ +
+
+ + + {errors.dtlInstCd && {errors.dtlInstCd}} +
+
+
+ + {errors.submit && ( +
+
{errors.submit}
+
+ )} + +
+ + + +
+
+ )} +
+
+ ); +} + +export default UserSignupModal; diff --git a/apps/cm/src/pages/auth/login/ui/login-page.tsx b/apps/cm/src/pages/auth/login/ui/login-page.tsx index 251cd2819..fed7be4ad 100644 --- a/apps/cm/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cm/src/pages/auth/login/ui/login-page.tsx @@ -7,6 +7,7 @@ import { LoginApi } from "@/entities/auth"; import { ENDPOINT } from "@/shared/config"; import OtpModal from "./OtpModal"; import TwoFAModal from "./TwoFAModal"; +import UserSignupModal from "./UserSignupModal"; import "./login.css"; import { useRouter } from "next/navigation"; @@ -14,8 +15,10 @@ function LoginPage() { const [isOtpEnabled, setIsOtpEnabled] = useState(false); const [showOtpModal, setShowOtpModal] = useState(false); const [showTwoFAModal, setShowTwoFAModal] = useState(false); + const [showSignupModal, setShowSignupModal] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); const router = useRouter(); // 모달 상태 디버깅 @@ -27,6 +30,7 @@ function LoginPage() { e.preventDefault(); setIsLoading(true); setError(""); + setSuccessMessage(""); try { const formData = new FormData(e.target as HTMLFormElement); @@ -123,6 +127,18 @@ function LoginPage() { localStorage.removeItem("tempToken"); }; + const handleSignupSuccess = () => { + console.log("회원가입 성공"); + setError(""); // 에러 메시지 초기화 + setSuccessMessage("회원가입이 완료되었습니다. 로그인해주세요."); + }; + + const handleSignupClose = () => { + console.log("회원가입 모달 닫기"); + setShowSignupModal(false); + setSuccessMessage(""); // 성공 메시지 초기화 + }; + return ( <>
@@ -214,6 +230,28 @@ function LoginPage() { {error}
)} + {successMessage && ( +
+ {successMessage} +
+ )} +
+ 계정이 없으신가요? + +

copyright © 2023 KECO. All rights reserved.

@@ -269,6 +307,13 @@ function LoginPage() { open={showTwoFAModal} onOpenChange={setShowTwoFAModal} /> + + {/* 회원가입 모달 */} + ); } diff --git a/apps/cm/src/pages/auth/login/ui/signup-modal.css b/apps/cm/src/pages/auth/login/ui/signup-modal.css new file mode 100644 index 000000000..0fb1585c9 --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/signup-modal.css @@ -0,0 +1,277 @@ +/* 모달 오버레이 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + padding: 20px; +} + +/* 모달 컨텐츠 */ +.modal-content { + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; +} + +/* 모달 헤더 */ +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e5e7eb; + background-color: #f9fafb; + border-radius: 8px 8px 0 0; +} + +.modal-header h2 { + margin: 0; + font-size: 1.8rem; + font-weight: 600; + color: #1f2937; +} + +.close-button { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #6b7280; + padding: 4px; + border-radius: 4px; + transition: color 0.2s; +} + +.close-button:hover { + color: #374151; +} + +/* 폼 스타일 */ +.signup-form { + padding: 24px; +} + +.form-section { + margin-bottom: 32px; +} + +.form-section h3 { + margin: 0 0 16px 0; + font-size: 1.4rem; + font-weight: 600; + color: #374151; + padding-bottom: 8px; + border-bottom: 2px solid #e5e7eb; +} + +.form-row { + margin-bottom: 16px; +} + +.form-group { + display: flex; + flex-direction: column; +} + +.form-group label { + margin-bottom: 6px; + font-weight: 500; + color: #374151; + font-size: 1.1rem; +} + +.form-group input, +.form-group select { + padding: 12px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1.1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: white; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-group input:disabled, +.form-group select:disabled { + background-color: #f3f4f6; + color: #6b7280; + cursor: not-allowed; +} + +/* 입력 필드와 버튼이 함께 있는 경우 */ +.input-with-button { + display: flex; + gap: 8px; + align-items: flex-end; +} + +.input-with-button input { + flex: 1; +} + +.check-button { + padding: 10px 16px; + background-color: #6b7280; + color: white; + border: none; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; + min-width: 80px; +} + +.check-button:hover:not(:disabled) { + background-color: #4b5563; +} + +.check-button:disabled { + background-color: #d1d5db; + cursor: not-allowed; +} + +/* 메시지 스타일 */ +.error-message { + color: #dc2626; + font-size: 0.8rem; + margin-top: 4px; + display: block; +} + +.success-message { + color: #059669; + font-size: 0.8rem; + margin-top: 4px; + display: block; +} + +.submit-error { + background-color: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 12px; + border-radius: 6px; + margin-bottom: 16px; + text-align: center; +} + +/* 폼 액션 버튼 */ +.form-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 20px; + border-top: 1px solid #e5e7eb; + margin-top: 24px; +} + +.cancel-button, +.submit-button { + padding: 12px 24px; + border-radius: 6px; + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + border: none; + min-width: 100px; +} + +.cancel-button { + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.cancel-button:hover:not(:disabled) { + background-color: #e5e7eb; +} + +.submit-button { + background-color: #3b82f6; + color: white; +} + +.submit-button:hover:not(:disabled) { + background-color: #2563eb; +} + +.cancel-button:disabled, +.submit-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .modal-content { + margin: 10px; + max-height: 95vh; + } + + .modal-header { + padding: 16px 20px; + } + + .signup-form { + padding: 20px; + } + + .form-actions { + flex-direction: column; + } + + .cancel-button, + .submit-button { + width: 100%; + } + + .input-with-button { + flex-direction: column; + align-items: stretch; + } + + .check-button { + width: 100%; + margin-top: 8px; + } +} + +/* 스크롤바 스타일 */ +.modal-content::-webkit-scrollbar { + width: 6px; +} + +.modal-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.modal-content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.modal-content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} diff --git a/apps/cm/src/pages/auth/login/ui/user-signup-modal.css b/apps/cm/src/pages/auth/login/ui/user-signup-modal.css new file mode 100644 index 000000000..241929a2a --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/user-signup-modal.css @@ -0,0 +1,436 @@ +/* 신규 사용자 가입 모달 오버레이 */ +.user-signup-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + padding: 20px; +} + +/* 신규 사용자 가입 모달 컨텐츠 */ +.user-signup-modal-content { + background: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; +} + +/* 신규 사용자 가입 모달 헤더 */ +.user-signup-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e5e7eb; + background-color: #f9fafb; + border-radius: 8px 8px 0 0; +} + +.user-signup-modal-header h2 { + margin: 0; + font-size: 1.8rem; + font-weight: 600; + color: #1f2937; +} + +.user-signup-close-button { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #6b7280; + padding: 4px; + border-radius: 4px; + transition: color 0.2s; +} + +.user-signup-close-button:hover { + color: #374151; +} + +/* 신규 사용자 가입 폼 스타일 */ +.user-signup-form { + padding: 24px; +} + +.user-signup-form-section { + margin-bottom: 32px; +} + +.user-signup-form-section h3 { + margin: 0 0 16px 0; + font-size: 1.4rem; + font-weight: 600; + color: #374151; + padding-bottom: 8px; + border-bottom: 2px solid #e5e7eb; +} + +.user-signup-form-row { + margin-bottom: 16px; +} + +.user-signup-form-group { + display: flex; + flex-direction: column; +} + +.user-signup-form-group label { + margin-bottom: 6px; + font-weight: 500; + color: #374151; + font-size: 1.1rem; +} + +.user-signup-form-group input, +.user-signup-form-group select { + padding: 12px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1.1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: white; +} + +/* CodeSelect 컴포넌트 스타일링 */ +.user-signup-form-group .CodeSelect { + width: 100%; +} + +.user-signup-form-group .CodeSelect select { + width: 100%; + padding: 12px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1.1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: white; +} + +.user-signup-form-group .CodeSelect select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.user-signup-form-group .CodeSelect select:disabled { + background-color: #f3f4f6; + color: #6b7280; + cursor: not-allowed; +} + +/* Select 요소 스타일링 */ +.user-signup-select { + width: 100%; + padding: 12px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1.1rem; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: white; + cursor: pointer; +} + +.user-signup-select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.user-signup-select:disabled { + background-color: #f3f4f6; + color: #6b7280; + cursor: not-allowed; +} + +.user-signup-select option { + padding: 8px; + font-size: 1rem; +} + +.user-signup-form-group input:focus, +.user-signup-form-group select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.user-signup-form-group input:disabled, +.user-signup-form-group select:disabled { + background-color: #f3f4f6; + color: #6b7280; + cursor: not-allowed; +} + +/* 입력 필드와 버튼이 함께 있는 경우 */ +.user-signup-input-with-button { + display: flex; + gap: 8px; + align-items: flex-end; +} + +.user-signup-input-with-button input { + flex: 1; +} + +.user-signup-check-button { + padding: 10px 16px; + background-color: #6b7280; + color: white; + border: none; + border-radius: 6px; + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; + min-width: 80px; +} + +.user-signup-check-button:hover:not(:disabled) { + background-color: #4b5563; +} + +.user-signup-check-button:disabled { + background-color: #d1d5db; + cursor: not-allowed; +} + +/* 메시지 스타일 */ +.user-signup-error-message { + color: #dc2626; + font-size: 0.8rem; + margin-top: 4px; + display: block; +} + +.user-signup-success-message { + color: #059669; + font-size: 0.8rem; + margin-top: 4px; + display: block; +} + +.user-signup-submit-error { + background-color: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 12px; + border-radius: 6px; + margin-bottom: 16px; + text-align: center; +} + +/* 폼 액션 버튼 */ +.user-signup-form-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 20px; + border-top: 1px solid #e5e7eb; + margin-top: 24px; +} + +.user-signup-cancel-button, +.user-signup-submit-button, +.user-signup-back-button { + padding: 12px 24px; + border-radius: 6px; + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + border: none; + min-width: 100px; +} + +.user-signup-cancel-button { + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.user-signup-cancel-button:hover:not(:disabled) { + background-color: #e5e7eb; +} + +.user-signup-submit-button { + background-color: #3b82f6; + color: white; +} + +.user-signup-submit-button:hover:not(:disabled) { + background-color: #2563eb; +} + +.user-signup-back-button { + background-color: #6b7280; + color: white; +} + +.user-signup-back-button:hover:not(:disabled) { + background-color: #4b5563; +} + +.user-signup-cancel-button:disabled, +.user-signup-submit-button:disabled, +.user-signup-back-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* 개인정보제공동의 스타일 */ +.user-signup-agreement-content { + padding: 24px; +} + +.user-signup-agreement-text { + margin-bottom: 24px; +} + +.user-signup-agreement-text h3 { + margin: 0 0 16px 0; + font-size: 1.4rem; + font-weight: 600; + color: #374151; +} + +.user-signup-agreement-text h4 { + margin: 20px 0 8px 0; + font-size: 1.1rem; + font-weight: 600; + color: #374151; +} + +.user-signup-agreement-text p { + margin: 8px 0; + color: #6b7280; + line-height: 1.5; +} + +.user-signup-agreement-text ul { + margin: 8px 0; + padding-left: 20px; + color: #6b7280; +} + +.user-signup-agreement-text li { + margin: 4px 0; + line-height: 1.5; +} + +.user-signup-agreement-checkbox { + margin-bottom: 24px; + padding: 16px; + background-color: #f9fafb; + border-radius: 6px; + border: 1px solid #e5e7eb; +} + +.user-signup-agreement-checkbox label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-weight: 500; + color: #374151; +} + +.user-signup-agreement-checkbox input[type="checkbox"] { + width: 18px; + height: 18px; + margin: 0; +} + +.user-signup-agreement-actions { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.user-signup-next-button { + padding: 12px 24px; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 6px; + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s; + min-width: 100px; +} + +.user-signup-next-button:hover:not(:disabled) { + background-color: #2563eb; +} + +.user-signup-next-button:disabled { + background-color: #d1d5db; + cursor: not-allowed; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .user-signup-modal-content { + margin: 10px; + max-height: 95vh; + } + + .user-signup-modal-header { + padding: 16px 20px; + } + + .user-signup-form { + padding: 20px; + } + + .user-signup-form-actions { + flex-direction: column; + } + + .user-signup-cancel-button, + .user-signup-submit-button, + .user-signup-back-button { + width: 100%; + } + + .user-signup-input-with-button { + flex-direction: column; + align-items: stretch; + } + + .user-signup-check-button { + width: 100%; + margin-top: 8px; + } +} + +/* 스크롤바 스타일 */ +.user-signup-modal-content::-webkit-scrollbar { + width: 6px; +} + +.user-signup-modal-content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +.user-signup-modal-content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.user-signup-modal-content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 3b59d4a23..c4901a00e 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -29,7 +29,7 @@ function DashboardPage() { if (!accessToken || !userId) { console.log('No tokens found, redirecting to login'); setError('로그인이 필요합니다.'); - router.push('/login'); + router.push('/auth/login'); return; } @@ -49,7 +49,7 @@ function DashboardPage() { if (error instanceof Error && error.message.includes('401')) { setError('인증이 만료되었습니다. 다시 로그인해주세요.'); tokenManager.clearAuthToken(); - router.push('/login'); + router.push('/auth/login'); } else { setError('네트워크 오류가 발생했습니다.'); } @@ -65,7 +65,7 @@ function DashboardPage() { localStorage.removeItem('tempToken'); // tokenManager를 통해 쿠키 정리 tokenManager.clearAuthToken(); - router.push('/login'); + router.push('/auth/logout'); }; const handleChangePassword = () => { diff --git a/apps/cm/src/pages/users/api/zip-service.ts b/apps/cm/src/pages/users/api/zip-service.ts new file mode 100644 index 000000000..211638344 --- /dev/null +++ b/apps/cm/src/pages/users/api/zip-service.ts @@ -0,0 +1,41 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface ZipSearchParams extends PageableParams { + zoneNo?: string; // 구역번호 + ctpvNm?: string; // 시도명 + sggNm?: string; // 시군구명 + emdNm?: string; // 읍면동명 + roadNm?: string; // 도로명 + stdgNm?: string; // 법정동명 + dongNm?: string; // 행정동명 + lotnoMno?: number | null; // 지번본번 + lotnoSno?: number | null; // 지번부번 +} + +export interface Zip { + zipId: number; + zoneNo: string; + ctpvNm: string; + sggNm: string; + emdNm: string; + roadNm: string; + lotnoMno: number; + lotnoSno: number; + lotno: string; + zipAddr: string; +} + +type ZipList = PageableList; + +export class ZipApiService { + public async searchZip(params: ZipSearchParams): Promise { + const response = await client.get(`${ENDPOINT.CMS_SERVICE.ZIPS}`, { + params, + }); + return response.data; + } +} + +export const ZipApi = new ZipApiService(); diff --git a/apps/cm/src/pages/users/index.ts b/apps/cm/src/pages/users/index.ts new file mode 100644 index 000000000..096f72195 --- /dev/null +++ b/apps/cm/src/pages/users/index.ts @@ -0,0 +1 @@ +export { CreatePage } from "./ui/create-page"; diff --git a/apps/cm/src/pages/users/model/table-columns.tsx b/apps/cm/src/pages/users/model/table-columns.tsx new file mode 100644 index 000000000..1aad6de27 --- /dev/null +++ b/apps/cm/src/pages/users/model/table-columns.tsx @@ -0,0 +1,88 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { + USER_STATE_CODE, + type UserListResponse, + type UserStateCode, +} from "@/entities/user"; +import { Badge } from "@common/components"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "No", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "userId", + header: "아이디", + }, + { + accessorKey: "userName", + header: "이름", + }, + { + accessorKey: "telno", + header: "전화번호", + }, + { + accessorKey: "email", + header: "이메일", + }, + { + accessorKey: "laeSeNm", + header: "구분", + }, + { + accessorKey: "ogdpSeNm", + header: "소속", + }, + { + accessorKey: "instSeNm", + header: "기관/부서", + }, + { + accessorKey: "userStateCode", + header: "회원상태", + cell: ({ row }) => { + return getUserStateBadge(row.original.userStateCode); + }, + }, + { + accessorKey: "aprvYmd", + header: "승인일자", + }, + { + accessorKey: "loginFailCount", + header: "로그인실패횟수", + }, + { + accessorKey: "lastLoginDate", + header: "최종접속일", + cell: ({ row }) => { + return row.original.lastLoginDate + ? dayjs(row.original.lastLoginDate).format("YYYY-MM-DD") + : ""; + }, + }, +]; + +const getUserStateBadge = (userStateCode: UserStateCode) => { + switch (userStateCode) { + case USER_STATE_CODE.대기: + return 대기; + case USER_STATE_CODE.정상: + return 정상; + case USER_STATE_CODE.정지: + return 정지; + case USER_STATE_CODE.탈퇴: + return 탈퇴; + case USER_STATE_CODE.삭제: + return 삭제; + } +}; diff --git a/apps/cm/src/pages/users/ui/agency-search-dialog.tsx b/apps/cm/src/pages/users/ui/agency-search-dialog.tsx new file mode 100644 index 000000000..9759d42be --- /dev/null +++ b/apps/cm/src/pages/users/ui/agency-search-dialog.tsx @@ -0,0 +1,179 @@ +import { useState } from "react"; +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + getCoreRowModel, + getPaginationRowModel, + type PaginationState, +} from "@tanstack/react-table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogTrigger, + DialogClose, + Button, + Input, + Label, +} from "@common/components"; +import { + LwpoCodeApi, + type LwpoCode, + type LwpoCodeParams, +} from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; + +interface AgencySearchDialogProps { + onSelectAgency: (data: { lwpoCd: string; lwpoCdNm: string }) => void; +} + +function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [selectedAgency, setSelectedAgency] = useState(null); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [searchQuery, setSearchQuery] = useState({ + lwpoCdNm: "", + }); + + const query = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, pagination], + queryFn: () => + LwpoCodeApi.getLwpoCodeList({ + ...searchQuery, + upCd: "AGENCYCD", + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const handleRowClick = (row: LwpoCode) => { + table.setRowSelection({ + [row.lwpoCd.toString()]: true, + }); + setSelectedAgency(row); + }; + + const { tableComponent, tableInstance: table } = useDataTable({ + query, + tableOptions: { + columns: [ + { + accessorKey: "lwpoCd", + header: "기관코드", + meta: { + width: 200, + }, + }, + { + accessorKey: "lwpoCdNm", + header: "기관명", + }, + ], + state: { + pagination, + }, + manualPagination: true, + enableMultiRowSelection: false, + getRowId: (row) => row.lwpoCd.toString(), + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + variant: "secondary", + }, + }); + + const handleSearch = async () => { + await query.refetch(); + queryClient.removeQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE], + }); + setPagination({ + pageIndex: 0, + pageSize: 10, + }); + table.setRowSelection({}); + setSelectedAgency(null); + }; + + const handleSelectAgency = () => { + if (!selectedAgency) return; + onSelectAgency({ + lwpoCd: selectedAgency.lwpoCd, + lwpoCdNm: selectedAgency.lwpoCdNm, + }); + setOpen(false); + }; + + return ( + + + + + + + 기관 검색 + +
{ + e.preventDefault(); + e.stopPropagation(); + handleSearch(); + }} + > +
+
+ + + setSearchQuery({ + ...searchQuery, + lwpoCdNm: e.target.value, + }) + } + placeholder="기관명을 입력하세요" + className="!text-[1.3rem]" + /> + +
+
+
+ {tableComponent} + + + + + + +
+
+ ); +} + +export { AgencySearchDialog }; diff --git a/apps/cm/src/pages/users/ui/create-page.tsx b/apps/cm/src/pages/users/ui/create-page.tsx new file mode 100644 index 000000000..2d55a1cd2 --- /dev/null +++ b/apps/cm/src/pages/users/ui/create-page.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutationWrapper, useAlertStore } from "@/shared/lib"; +import { UserApi, userSaveSchema, type UserSaveData } from "@/entities/user"; +import { UsersForm } from "./users-form"; + +function CreatePage() { + const router = useRouter(); + const alert = useAlertStore((state) => state.setMessage); + + const form = useForm({ + resolver: zodResolver(userSaveSchema), + defaultValues: { + userId: "", + userName: "", + password: "", + pswdHnt: "", + laeSeCd: "", + ogdpSeCd: "", + expryYmd: "", + }, + }); + + // 사용자 등록 mutation + const { mutateAsync: saveUser } = useMutationWrapper({ + mutationFn: (data: UserSaveData) => UserApi.saveUser(data), + onSuccess: () => { + alert("회원이 등록되었습니다.", router.back); + }, + }); + + return ( +
+
+

회원 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cm/src/pages/users/ui/modify-page.tsx b/apps/cm/src/pages/users/ui/modify-page.tsx new file mode 100644 index 000000000..fc93e4b05 --- /dev/null +++ b/apps/cm/src/pages/users/ui/modify-page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { + UserApi, + userUpdateSchema, + type UserUpdateData, +} from "@/entities/user"; +import { ENDPOINT } from "@/shared/config"; +import { useMutationWrapper, useAlertStore } from "@/shared/lib"; +import { UsersForm } from "./users-form"; + +interface ModifyPageProps { + userUnqId: string; +} + +export function ModifyPage({ userUnqId }: ModifyPageProps) { + const router = useRouter(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: userData } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId], + queryFn: () => UserApi.findByUserUnqId(userUnqId), + enabled: !!userUnqId, + }); + + const form = useForm({ + resolver: zodResolver(userUpdateSchema), + defaultValues: { + userId: userData?.userId ?? "", + userName: userData?.userName ?? "", + email: userData?.email ?? "", + userStateCode: userData?.userStateCode ?? "", + pswdHnt: userData?.pswdHnt ?? "", + pswdAns: userData?.pswdAns ?? "", + mphn: userData?.mphn ?? "", + telno: userData?.telno ?? "", + zip: userData?.zip ?? "", + addr: userData?.addr ?? "", + daddr: userData?.daddr ?? "", + laeSeCd: userData?.laeSeCd ?? "", + ogdpSeCd: userData?.ogdpSeCd ?? "", + aprvYn: userData?.aprvYn ?? "", + expryYmd: userData?.expryYmd ?? "", + instSeCd: userData?.instSeCd ?? "", + dtlInstCd: userData?.dtlInstCd ?? "", + aprvYmd: userData?.aprvYmd ?? "", + enoCd: userData?.enoCd ?? "", + }, + }); + + const { mutateAsync: updateUser } = useMutationWrapper({ + mutationFn: (data: UserUpdateData) => UserApi.updateUser(userUnqId, data), + onSuccess: () => { + alert("회원 정보가 수정되었습니다.", router.back); + }, + }); + + return ( +
+
+

회원 정보 수정

+
+ +
+ ); +} diff --git a/apps/cm/src/pages/users/ui/user-detail.tsx b/apps/cm/src/pages/users/ui/user-detail.tsx new file mode 100644 index 000000000..fbfa016c2 --- /dev/null +++ b/apps/cm/src/pages/users/ui/user-detail.tsx @@ -0,0 +1,187 @@ +"use client"; + +import dayjs from "dayjs"; +import { usePathname, useRouter } from "next/navigation"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { USER_STATE_CODE, UserApi, PasswordResetDialog } from "@/entities/user"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; + +function UserDetail({ userUnqId }: { userUnqId: string }) { + const router = useRouter(); + const pathname = usePathname(); + const { confirm } = useConfirm(); + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const { data, refetch } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId], + queryFn: () => UserApi.findByUserUnqId(userUnqId), + enabled: !!userUnqId, + }); + + const { mutate: onLockUser } = useMutationWrapper({ + mutationFn: () => UserApi.lockUser(userUnqId), + onSuccess: () => { + alert("계정 잠금이 활성화 되었습니다."); + refetch(); + }, + }); + + const { mutate: onUnlockUser } = useMutationWrapper({ + mutationFn: () => UserApi.unlockUser(userUnqId), + onSuccess: () => { + alert("계정 잠금이 해제되었습니다."); + refetch(); + }, + }); + + const { mutate: onApplyUser } = useMutationWrapper({ + mutationFn: () => UserApi.applyUser(userUnqId), + onSuccess: () => { + alert("사용자 승인이 완료되었습니다."); + refetch(); + }, + }); + + const { mutateAsync: deleteUser } = useMutationWrapper({ + mutationFn: () => UserApi.deleteUser(userUnqId), + onSuccess: () => { + alert("회원이 삭제되었습니다.", router.back); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + }); + }, + }); + + const handleDelete = async () => { + if (await confirm("삭제하시겠습니까?")) { + await deleteUser(); + } + }; + + return ( +
+
+

회원 정보

+
+
+
+ {data?.userStateCode === USER_STATE_CODE.정지 ? ( + + ) : ( + + )} + + + + {data?.userStateCode === USER_STATE_CODE.대기 && ( + + )} +
+ + + + + + + + 아이디 + {data?.userId} + + + 이름 + {data?.userName} + + + 연락처 + 휴대폰: {data?.mphn} + + + 전화번호: {data?.telno} + + + 이메일 + {data?.email} + + + 주소 + + {data?.zip && `(${data?.zip})`} {data?.addr} + + + + {data?.daddr} + + + 기관구분 + {data?.ogdpSeNm} + + + 세부기관 + {data?.dtlInstNm} + + + 소속 + {data?.laeSeNm} + + + 환경청 + {data?.ecoNm} + + + 가입일 + + {data?.regDt ? dayjs(data?.regDt).format("YYYY-MM-DD") : ""} + + + + 최근로그인 + + {data?.lastLoginDate + ? dayjs(data?.lastLoginDate).format("YYYY-MM-DD HH:mm:ss") + : ""} + + + + 계정상태 + {data?.userStateNm} + + +
+
+
+ + + +
+
+ ); +} + +export { UserDetail }; diff --git a/apps/cm/src/pages/users/ui/users-form.tsx b/apps/cm/src/pages/users/ui/users-form.tsx new file mode 100644 index 000000000..7920ce758 --- /dev/null +++ b/apps/cm/src/pages/users/ui/users-form.tsx @@ -0,0 +1,669 @@ +import { useState, useMemo, useCallback } from "react"; +import { + useWatch, + type Path, + type PathValue, + type UseFormReturn, +} from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; +import { + Button, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeApi, CodeSelect } from "@/entities/code"; +import { + UserApi, + type UserSaveData, + type UserUpdateData, +} from "@/entities/user"; +import { + FormField, + FormItem, + FormControl, + FormMessage, +} from "@/shared/ui/form"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { ZipSearchDialog } from "./zip-search-dialog"; +import { AgencySearchDialog } from "./agency-search-dialog"; + +type UsersFormProps = { + userId?: string; + form: UseFormReturn; + handleSave: (data: T) => Promise; + handleDelete?: () => Promise; +}; + +function UsersForm({ + userId, + form, + ...props +}: UsersFormProps) { + const isModifyMode = !!userId; + + const [watchOgdpSeCd, watchDtlInstCd] = useWatch({ + control: form.control, + name: ["ogdpSeCd" as Path, "dtlInstCd" as Path], + }); + const alert = useAlertStore((state) => state.setMessage); + + const [isValidUserId, setIsValidUserId] = useState(false); + const [isValidEmail, setIsValidEmail] = useState(false); + const [passwordMatch, setPasswordMatch] = useState(true); + const [instSeCdNm, setInstSeCdNm] = useState(""); + + const { data: authSeCd } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "AUTHSECD"], + queryFn: () => CodeApi.getCode("AUTHSECD"), + }); + + const { data: laeSeCd } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], + queryFn: () => CodeApi.getCode("LAESECD"), + }); + + const { mutateAsync: checkDuplicateUserId } = useMutationWrapper({ + mutationFn: (userId: string) => UserApi.existsUserId({ userId }), + onSuccess: (duplicate) => { + if (!duplicate) { + alert("사용가능한 아이디입니다."); + setIsValidUserId(true); + return; + } + alert("중복된 아이디입니다."); + }, + }); + + const { mutateAsync: checkDuplicateEmail } = useMutationWrapper({ + mutationFn: (email: string) => UserApi.existsEmail({ email }), + onSuccess: (duplicate) => { + if (!duplicate) { + alert("사용가능한 이메일입니다."); + setIsValidEmail(true); + return; + } + alert("중복된 이메일입니다."); + }, + }); + + const authSeCdSelectData = useMemo(() => { + if (watchOgdpSeCd === "1") + return ( + authSeCd?.filter( + (code) => + code.lwpoCd === "ROLE_USER1" || + code.lwpoCd === "ROLE_USER2" || + code.lwpoCd === "ROLE_USER3", + ) ?? [] + ); + if (watchOgdpSeCd === "3" || watchOgdpSeCd === "4") + return ( + authSeCd?.filter( + (code) => + code.lwpoCd === "ROLE_USER4" || code.lwpoCd === "ROLE_USER5", + ) ?? [] + ); + return []; + }, [authSeCd, watchOgdpSeCd]); + + const laeSeCdSelectData = useMemo(() => { + if (watchDtlInstCd === "ROLE_USER1") + return laeSeCd?.filter((code) => code.lwpoCd === "G102") ?? []; + if (watchDtlInstCd === "ROLE_USER2") + return laeSeCd?.filter((code) => code.lwpoCd === "G101") ?? []; + if (watchDtlInstCd === "ROLE_USER3") + return laeSeCd?.filter((code) => code.lwpoCd.startsWith("G2")) ?? []; + if (watchDtlInstCd === "ROLE_USER4") + return ( + laeSeCd?.filter( + (code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4, + ) ?? [] + ); + if (watchDtlInstCd === "ROLE_USER5") + return ( + laeSeCd?.filter( + (code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 7, + ) ?? [] + ); + return []; + }, [laeSeCd, watchDtlInstCd]); + + const enoCdSelectData = useMemo( + () => laeSeCd?.filter((code) => code.lwpoCd?.startsWith("G2")) ?? [], + [laeSeCd], + ); + + const handlePasswordConfirmChange = useCallback((value: string) => { + const password = form.getValues("password" as Path); + setPasswordMatch(!password || value === password); + }, []); + + const handleAddressSelect = useCallback( + (data: { zip: string; addr: string }) => { + form.setValue("zip" as Path, data.zip as PathValue>); + form.setValue("addr" as Path, data.addr as PathValue>); + }, + [form], + ); + + const handleAgencySelect = useCallback( + (data: { lwpoCd: string; lwpoCdNm: string }) => { + form.setValue( + "instSeCd" as Path, + data.lwpoCd as PathValue>, + ); + setInstSeCdNm(data.lwpoCdNm); + }, + [form], + ); + + return ( + +
+ + + + + + + {/* 아이디 */} + + 아이디 + + } + render={({ field }) => ( +
+ + + { + field.onChange(e); + setIsValidUserId(false); + }} + className="w-[24rem] !text-[1.3rem]" + placeholder="아이디를 입력하세요" + disabled={isModifyMode} + /> + + + {!isModifyMode && ( + + )} +
+ )} + /> +
+
+ + {/* 비밀번호 */} + + 비밀번호 + + } + render={({ field }) => ( + + + + + + + )} + /> + + + + {/* 비밀번호 확인 */} + + 비밀번호 확인 + +
+ + handlePasswordConfirmChange(e.target.value) + } + className="w-[24rem] !text-[1.3rem]" + placeholder="비밀번호를 다시 입력하세요" + /> + {!passwordMatch && ( +

+ 비밀번호가 일치하지 않습니다. +

+ )} +
+
+
+ + {/* 비밀번호 힌트 */} + + 비밀번호 힌트 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 비밀번호 정답 */} + + 비밀번호 정답 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 이름 */} + + 이름 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 연락처 */} + + 연락처 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 이메일 */} + + 이메일 + + } + render={({ field }) => ( +
+ + + { + field.onChange(e); + setIsValidEmail(false); + }} + className="w-[24rem] !text-[1.3rem]" + placeholder="이메일을 입력하세요" + /> + + + +
+ )} + /> +
+
+ + {/* 주소 */} + + 주소 + +
+
+ } + render={({ field }) => ( + + + + + + )} + /> + +
+ } + render={({ field }) => ( + + + + + + )} + /> +
+
+
+ + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 기관구분 */} + + 기관구분 + + } + render={({ field }) => ( + + + { + field.onChange(value); + form.setValue( + "dtlInstCd" as Path, + "" as PathValue>, + ); + form.setValue( + "laeSeCd" as Path, + "" as PathValue>, + ); + form.setValue( + "enoCd" as Path, + "" as PathValue>, + ); + }} + className="w-[16rem]" + placeholder="기관 선택" + /> + + + + )} + /> + + + + {/* 세부기관 */} + + 세부기관 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 소속 */} + + 소속 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 환경청 */} + + 환경청 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + {/* 환경청 기관/업체명 */} + + 기관/업체명 + +
+ + +
+
+
+
+
+
+
+ ); +} + +export { UsersForm }; diff --git a/apps/cm/src/pages/users/ui/users-page.tsx b/apps/cm/src/pages/users/ui/users-page.tsx new file mode 100644 index 000000000..5362b8b42 --- /dev/null +++ b/apps/cm/src/pages/users/ui/users-page.tsx @@ -0,0 +1,396 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; +import { + Button, + Checkbox, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { CodeApi } from "@/entities/code"; +import { + UserApi, + type UserListResponse, + type UserSearchParams, +} from "@/entities/user"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; +import { columns } from "../model/table-columns"; + +type DateRange = { + from?: Date | undefined; + to?: Date | undefined; +}; + +function UsersPage() { + const router = useRouter(); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [presetDateRange, setPresetDateRange] = useState(""); + const [dateRange, setDateRange] = useState({ + from: undefined, + to: undefined, + }); + const [keywordType, setKeywordType] = useState("all"); + const [keyword, setKeyword] = useState(""); + const [userStateCode, setUserStateCode] = useState(""); + const [aprvYn, setAprvYn] = useState(""); + const [grpId, setGrpId] = useState(""); + + const [searchParams, setSearchParams] = useState({ + keywordType: "all", + keyword: "", + userStateCode: "", + aprvYn: "", + grpId: "", + frDateYmd: "", + toDateYmd: "", + sort: "regDt,desc", + }); + + const { data: userSttsStats } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"], + queryFn: () => UserApi.selectUserSttsStats(), + }); + + const query = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination], + queryFn: () => + UserApi.findPageUser({ + ...searchParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + const { data: userStateCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE], + queryFn: () => CodeApi.getCode("USERSTTS"), + }); + + const handleRowClick = (row: UserListResponse) => { + router.push(`/users/${row.userUnqId}`); + }; + + const { tableComponent, tableInstance: table } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setPagination({ + ...pagination, + pageIndex: 0, + }); + setSearchParams({ + ...searchParams, + keywordType, + keyword, + userStateCode, + aprvYn, + grpId, + frDateYmd: dateRange?.from + ? dayjs(dateRange.from).format("YYYYMMDD") + : "", + toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "", + }); + }; + + const handleReset = () => { + setKeywordType("all"); + setKeyword(""); + setUserStateCode(""); + setAprvYn(""); + setGrpId(""); + setDateRange({ + from: undefined, + to: undefined, + }); + setSearchParams({ + ...searchParams, + keywordType: "all", + keyword: "", + userStateCode: "", + aprvYn: "", + grpId: "", + frDateYmd: "", + toDateYmd: "", + }); + }; + + const handlePresetDateRange = (value: string) => { + let presetDateRange: DateRange; + switch (value) { + case "1M": + presetDateRange = { + from: dayjs().subtract(1, "month").toDate(), + to: dayjs().toDate(), + }; + break; + case "3M": + presetDateRange = { + from: dayjs().subtract(3, "month").toDate(), + to: dayjs().toDate(), + }; + break; + case "6M": + presetDateRange = { + from: dayjs().subtract(6, "month").toDate(), + to: dayjs().toDate(), + }; + break; + default: + presetDateRange = { + from: undefined, + to: undefined, + }; + } + setPresetDateRange(value); + setDateRange(presetDateRange); + }; + + const handleDateRangeChange = (dateRange: DateRange) => { + setDateRange(dateRange); + setPresetDateRange(""); + }; + + const getUserSttsStatsCnt = (code: string): string => { + if (!Array.isArray(userSttsStats)) return "0"; + const count = userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0; + return count.toLocaleString(); + }; + + const handleUserStateCodeChange = (code: string, checked: boolean) => { + let newUserStateCode = userStateCode; + + if (checked) { + newUserStateCode = userStateCode ? `${userStateCode},${code}` : code; + } else { + newUserStateCode = userStateCode + .split(",") + .filter((c) => c !== code) + .join(","); + } + + setUserStateCode(newUserStateCode); + }; + + return ( +
+
+
+
+ +
+ + 승인대기{" "} + + {getUserSttsStatsCnt("00")} + + 명 + +
+
+
+ +
+ + 전체{" "} + + {getUserSttsStatsCnt("TOTAL")} + + 명 + +
+
+
+ +
+ + 잠금{" "} + + {getUserSttsStatsCnt("07")} + + 명 + +
+
+
+ +
+ + 탈퇴{" "} + + {getUserSttsStatsCnt("08")} + + 명 + +
+
+
+
+
+
+ + + setKeyword(e.target.value)} + className="!text-[1.3rem]" + placeholder="검색어를 입력하세요" + /> +
+
+ + {/* Select */} +
+
+ + + +
+
+
+ +
+ {userStateCodes?.map((item) => ( +
+ { + handleUserStateCodeChange(item.lwpoCd, !!checked); + }} + /> + +
+ ))} +
+
+
+
+ + +
+ +
+
+
+
+

회원 목록

+
+ + + +
+
+
+
{tableComponent}
+
+
+ ); +} + +export { UsersPage }; diff --git a/apps/cm/src/pages/users/ui/zip-search-dialog.tsx b/apps/cm/src/pages/users/ui/zip-search-dialog.tsx new file mode 100644 index 000000000..baab27619 --- /dev/null +++ b/apps/cm/src/pages/users/ui/zip-search-dialog.tsx @@ -0,0 +1,224 @@ +import { useState, useRef, useEffect } from "react"; +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + getCoreRowModel, + getPaginationRowModel, + type PaginationState, +} from "@tanstack/react-table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogTrigger, + DialogClose, + Button, + Input, + Label, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { ZipApi, type Zip, type ZipSearchParams } from "../api/zip-service"; + +interface ZipSearchDialogProps { + onSelectAddress: (data: { zip: string; addr: string }) => void; +} + +function ZipSearchDialog({ onSelectAddress }: ZipSearchDialogProps) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [selectedAddress, setSelectedAddress] = useState(null); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [searchQuery, setSearchQuery] = useState({ + roadNm: "", + lotnoMno: null, + lotnoSno: null, + }); + const hasFetchedOnce = useRef(false); + + const query = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.ZIPS, pagination], + queryFn: () => + ZipApi.searchZip({ + ...searchQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + enabled: false, + }); + + const handleRowClick = (row: Zip) => { + table.setRowSelection({ + [row.zipId.toString()]: true, + }); + setSelectedAddress(row); + }; + + const { tableComponent, tableInstance: table } = useDataTable({ + query, + tableOptions: { + columns: [ + { + accessorKey: "zoneNo", + header: "우편번호", + }, + { + accessorKey: "zipAddr", + header: "주소", + }, + ], + state: { + pagination, + }, + manualPagination: true, + enableMultiRowSelection: false, + getRowId: (row) => row.zipId.toString(), + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + variant: "secondary", + }, + }); + + const handleSearch = async () => { + await query.refetch(); + queryClient.removeQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ZIPS], + }); + setPagination({ + pageIndex: 0, + pageSize: 10, + }); + table.setRowSelection({}); + setSelectedAddress(null); + hasFetchedOnce.current = true; + }; + + const handleSelectAddress = () => { + if (!selectedAddress) return; + onSelectAddress({ + zip: selectedAddress.zoneNo, + addr: selectedAddress.zipAddr, + }); + setOpen(false); + }; + + useEffect(() => { + if (hasFetchedOnce.current) query.refetch(); + }, [pagination]); + + return ( + + + + + + + 주소 검색 + + +
{ + e.preventDefault(); + e.stopPropagation(); + handleSearch(); + }} + className="mb-4 space-y-2" + > +
+
+
+ + + setSearchQuery({ + ...searchQuery, + roadNm: e.target.value, + }) + } + placeholder="예: 테헤란로" + className="!text-[1.3rem]" + /> +
+ +
+ + { + const value = e.target.value + ? parseInt(e.target.value) + : null; + setSearchQuery({ + ...searchQuery, + lotnoMno: value, + }); + }} + placeholder="예: 123" + className="!text-[1.3rem]" + /> +
+ +
+ + { + const value = e.target.value + ? parseInt(e.target.value) + : null; + setSearchQuery({ + ...searchQuery, + lotnoSno: value, + }); + }} + placeholder="예: 45" + className="!text-[1.3rem]" + /> +
+
+ +
+ +
+
+
+ + {query.data?.content.length && tableComponent} + + + + + + + +
+
+ ); +} + +export { ZipSearchDialog }; diff --git a/apps/cm/src/shared/api/signup-client.ts b/apps/cm/src/shared/api/signup-client.ts new file mode 100644 index 000000000..2cd459cd7 --- /dev/null +++ b/apps/cm/src/shared/api/signup-client.ts @@ -0,0 +1,71 @@ +import axios from "axios"; +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; + +interface SignupClientConfig { + baseURL: string; +} + +class SignupClient { + private static instance: SignupClient; + private axiosInstance: AxiosInstance; + + private constructor(config: SignupClientConfig) { + this.axiosInstance = axios.create({ + baseURL: config.baseURL, + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + + // 인증 토큰 없이도 요청할 수 있도록 인터셉터 설정 + this.axiosInstance.interceptors.request.use((config) => { + // 인증 토큰을 자동으로 추가하지 않음 + return config; + }); + } + + public static getInstance(config: SignupClientConfig): SignupClient { + if (!SignupClient.instance) { + SignupClient.instance = new SignupClient(config); + } + return SignupClient.instance; + } + + public async get>( + url: string, + config?: AxiosRequestConfig + ): Promise { + const response = await this.axiosInstance.get(url, config); + return response; + } + + public async post, D = unknown>( + url: string, + data?: D, + config?: AxiosRequestConfig + ): Promise { + const response = await this.axiosInstance.post(url, data, config); + return response; + } + + public async put, D = unknown>( + url: string, + data?: D, + config?: AxiosRequestConfig + ): Promise { + const response = await this.axiosInstance.put(url, data, config); + return response; + } + + public async delete>( + url: string, + config?: AxiosRequestConfig + ): Promise { + const response = await this.axiosInstance.delete(url, config); + return response; + } +} + +export type { SignupClientConfig }; +export { SignupClient }; From c93571781d4328fd832b387df3beafb84ba03074 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 15:21:47 +0900 Subject: [PATCH 22/42] =?UTF-8?q?=EC=9A=B4=EC=98=81=EC=8B=A4=ED=83=9C=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 2 +- packages/common/business_components/src/ui/map/Gis.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 31d70b93c..176bc8dd8 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -142,7 +142,7 @@ const Map = () => { isSearchModalOpen={isSearchModalOpen} onCheckedLayersChange={undefined} imgVisble={false} - searchBar={() => } + searchBar={} waterDepth={undefined} />
diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 2ca7ed051..a24157590 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -379,7 +379,7 @@ function Gis({ )} {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} - {isOpen && !isSearchModalOpen && ( + {isOpen && !isSearchModalOpen && DetailView && ( ({}) }} seletectedLayerNm={layerNm} From 940b3208f136240e690dd509b00c71b7267f7d41 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 15:52:56 +0900 Subject: [PATCH 23/42] =?UTF-8?q?=EB=A7=B5=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/layout.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/fo/app/pages/(map-layout)/layout.tsx b/apps/fo/app/pages/(map-layout)/layout.tsx index a9a7a8e93..69a60b7a2 100644 --- a/apps/fo/app/pages/(map-layout)/layout.tsx +++ b/apps/fo/app/pages/(map-layout)/layout.tsx @@ -1,12 +1,24 @@ 'use client' import { NoFooterLayout } from "@common/components"; +import type { MenuItem } from "../../store/common"; +import { useFoStore } from "../../store"; +import { usePathname, useRouter } from "next/navigation"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + + const { getMenuAuth } = useFoStore((state) => state); + const pathname = usePathname(); + const router = useRouter(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; return ( - {children} + {children} ); } From a3d9c320242776a8388dc6aa95881c5d31794e47 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 18:24:57 +0900 Subject: [PATCH 24/42] =?UTF-8?q?=EC=9A=B4=EC=98=81=EC=8B=A4=ED=83=9C=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 231 +++++++++++++++--- apps/fo/package.json | 5 +- .../business_components/src/ui/map/Gis.js | 12 - .../src/ui/map/GisSelect.js | 1 - .../business_components/src/ui/map/GisTree.js | 2 +- .../src/ui/map/searchBar/procSearchBar.tsx | 6 +- 6 files changed, 199 insertions(+), 58 deletions(-) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 176bc8dd8..77ac1b85c 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -3,19 +3,35 @@ 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 { SearchBar } from "../../map/searchBar/searchBar"; +import { ProcSearchBar } from "@common/business_components/ui/map/searchBar"; + +// OpenLayers overlay/extent 유틸 +import Overlay from "ol/Overlay"; +import { getCenter as getExtentCenter } from "ol/extent"; +import type { Coordinate } from "ol/coordinate"; interface FeatureData { fid?: string; ftrIdn?: string; id?: string; - getGeometry?: () => { getExtent: () => number[] }; + getId?: () => string | number | undefined; // 추가 + // OpenLayers Feature 비슷한 최소 인터페이스 + get?: (key: string) => any; + getGeometry?: () => { + getExtent: () => number[]; + getType?: () => string; + getCoordinates?: () => Coordinate; + }; setStyle?: () => void; } interface MapInstance { + addOverlay?: (ov: Overlay) => void; + removeOverlay?: (ov: Overlay) => void; + getOverlays?: () => { getArray: () => Overlay[] }; getLayers: () => { getArray: () => Array<{ + get: (key: string) => any; getSource?: () => { getFeatureById?: (id: string) => FeatureData | null }; }>; }; @@ -23,11 +39,19 @@ interface MapInstance { } const Map = () => { - const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [treeData, setTreeData] = useState([]); const gisMaps = useRef(null); - const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 - const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [isOpen, setIsOpen] = useState(false); + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); + const [featureData, setFeatureData] = useState(null); + + // === Overlay 관리용 Ref === + const overlayRef = useRef(null); + const overlayContainerRef = useRef(null); + + const facilityTypes = [ + { text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }, + ]; const fetchData = async () => { try { @@ -35,28 +59,117 @@ const Map = () => { const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`; const response = await fetch(reqUrl, { method: "GET", - headers: { - accept: "application/json", - }, + headers: { accept: "application/json" }, }); - - if (!response.ok) { - throw new Error(`HTTP 오류! 상태: ${response.status}`); - } - - const data = await response.json(); // 여기서 json 파싱 - setTreeData(data); // 데이터 설정 + if (!response.ok) throw new Error(`HTTP 오류! 상태: ${response.status}`); + const data = await response.json(); + setTreeData(data); } catch (error) { console.error("데이터 불러오기 실패:", error); } }; - // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); - // SearchModal, GisContent 등에서 호출 + // ===== 오버레이 콘텐츠 빌더 ===== + const buildOverlayContent = (layerType: string, feature: FeatureData) => { + const props: Record = {}; + // 안전하게 몇 개 필드 추출(없으면 건너뜀) + const mayGet = (k: string) => (feature.get ? feature.get(k) : undefined); + const safe = (v: any) => (v === undefined || v === null ? "-" : String(v)); + + console.log(feature); + + // 레이어 유형별 대표 필드 추출(없으면 "-") + if (layerType === "tb_il_swage_prcs_fclt") { + // 하수처리시설 + props["하수처리시설명"] = mayGet("swtrmt_fclt_nm"); + props["시설용량(m³/d)"] = mayGet("fclt_cpct"); + props["주소"] = mayGet("rdnm_addr"); + props["가동개시일"] = mayGet("oprtng_strt_ymd"); + props["공법"] = mayGet("rprs_msrc_cn"); + } else if (layerType === "tb_ms_swage_prcs_zone") { + // 처리구역(존) + props["하수처리구역명"] = mayGet("swtrmt_zone_nm"); + props["하수처리시설명"] = mayGet("swtrmt_fclt_nm"); + } else if (layerType === "om_wqlt_station") { + // 수질관측소 + props["관측소명"] = mayGet("stn_ko") ?? mayGet("name"); + props["주소"] = mayGet("addr") ?? mayGet("indicator"); + } + + const titleMap: Record = { + tb_il_swage_prcs_fclt: "하수처리시설", + tb_ms_swage_prcs_zone: "하수처리구역", + om_wqlt_station: "관측소", + }; + const title = titleMap[layerType] || "정보"; + + // 컨테이너 재사용(없으면 생성) + if (!overlayContainerRef.current) { + overlayContainerRef.current = document.createElement("div"); + } + const el = overlayContainerRef.current; + el.className = + "ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] "; + el.innerHTML = ` +
${title}
+ + ${Object.entries(props) + .map( + ([k, v]) => ` + + + + ` + ) + .join("")} +
${k}${safe(v)}
+ `; + return el; + }; + + // ===== 오버레이 생성/표시 ===== + const showOverlay = ( + map: MapInstance, + feature: FeatureData, + layerType: string + ) => { + // 이전 오버레이 제거 + if (overlayRef.current) { + map.removeOverlay?.(overlayRef.current); + overlayRef.current = null; + } + + // 콘텐츠 만들기 + const element = buildOverlayContent(layerType, feature); + + // 앵커 좌표 계산(포인트면 좌표, 아니면 extent 중심) + let position: Coordinate | undefined; + const geom = feature.getGeometry?.(); + if (geom?.getType && geom.getType() === "Point" && geom.getCoordinates) { + position = geom.getCoordinates(); + } else if (geom?.getExtent) { + position = getExtentCenter(geom.getExtent()); + } + + if (!position) return; + + // 오버레이 생성 & 지도에 추가 + const overlay = new Overlay({ + element, + positioning: "bottom-center", + stopEvent: false, + offset: [0, -12], + }); + overlay.setPosition(position); + map.addOverlay?.(overlay); + overlayRef.current = overlay; + }; + + // ===== 피처 선택 시 호출 ===== const onFeatureSelect = (data: FeatureData) => { const map = gisMaps.current; if (!map) { @@ -64,48 +177,58 @@ const Map = () => { return; } - // 항상 같은 select 객체 사용 - const select = GisApp.Module.select as unknown as - | { - resetSelectedFeature: () => void; - selectFeature: (feature: FeatureData, selected: boolean) => void; - } - | undefined; + type SelectModule = { + resetSelectedFeature: () => void; + selectFeature: (feature: FeatureData, selected: boolean) => void; + }; + + const maybeSelect = GisApp?.Module?.select; // 타입 그대로 둠 (any | unknown | null 가능) + + if (!maybeSelect) { + // null/undefined면 일찍 반환 + return; + } + + const select = maybeSelect as SelectModule; // 여기선 null 아님이 보장됨 if (!select) return; - // 1. 기존 선택 초기화 + // 1) 기존 선택 초기화 select.resetSelectedFeature(); - // 2. data가 OpenLayers Feature 인스턴스인지 확인 + // 2) 피처 탐색 + 레이어 유형 파악 + let featureId = String(data.getId?.() ?? ""); + let foundLayerType = featureId.split(".")[0]; let foundFeature: FeatureData | null = null; - if ( + const isOlFeatureInstance = data && typeof data.getGeometry === "function" && - typeof data.setStyle === "function" - ) { - // 이미 Feature 인스턴스라면 그대로 사용 + typeof data.setStyle === "function"; + + if (isOlFeatureInstance) { foundFeature = data; + // 어떤 레이어에서 온 건지 모르는 경우가 많아 기본값으로 시도 } else { - // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - const featureId = data.fid || data.ftrIdn || data.id; map .getLayers() .getArray() .forEach((layer) => { + if (foundFeature) return; // 이미 찾았으면 skip const source = layer.getSource?.(); - if (source?.getFeatureById) { - const feature = source.getFeatureById(String(featureId).trim()); - if (feature) foundFeature = feature; + const feature = source?.getFeatureById?.(featureId); + if (feature) { + foundFeature = feature; + // 레이어 이름(유형) 우선순위: name -> lyrTblNm -> id + foundLayerType = + layer.get("name") || layer.get("lyrTblNm") || layer.get("id"); } }); } - // 3. 피처 선택 + // 3) 피처 선택/줌 + 오버레이 if (foundFeature && foundFeature.getGeometry) { const geometry = foundFeature.getGeometry(); if (geometry) { const extent = geometry.getExtent(); - map.getView().fit(extent, { duration: 300, padding: [50, 50, 50, 50], @@ -115,15 +238,41 @@ const Map = () => { }); } select.selectFeature(foundFeature, true); + + // 표출 대상 레이어만 오버레이 표시 + const normalized = (foundLayerType || "").toString().trim(); + const TARGETS = [ + "tb_il_swage_prcs_fclt", + "tb_ms_swage_prcs_zone", + "om_wqlt_station", + ]; + + if (TARGETS.includes(normalized)) { + showOverlay(map, foundFeature, normalized); + } else { + // 대상 외 레이어 클릭 시 기존 오버레이 제거 + if (overlayRef.current) { + map.removeOverlay?.(overlayRef.current); + overlayRef.current = null; + } + } } else { console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + // 실패 시 기존 오버레이 제거 + if (overlayRef.current) { + map.removeOverlay?.(overlayRef.current); + overlayRef.current = null; + } } + // 우측 패널/검색 모달 상태 setFeatureData(data); - setIsOpen(true); + setIsOpen(false); setIsSearchModalOpen(false); }; + // (선택) 다른 위치 클릭 시 오버레이 닫고 싶다면 Gis 쪽에서 지도 클릭 이벤트 받아 여기서 제거하는 훅을 연결하면 됨. + return (
{ isSearchModalOpen={isSearchModalOpen} onCheckedLayersChange={undefined} imgVisble={false} - searchBar={} + searchBar={ + + } waterDepth={undefined} />
diff --git a/apps/fo/package.json b/apps/fo/package.json index aedf37c33..8697a116a 100644 --- a/apps/fo/package.json +++ b/apps/fo/package.json @@ -17,7 +17,10 @@ "next": "15.1.7", "select": "^1.1.2", "webpack": "^5.98.0", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "ol": "^7.3.0", + "ol-ext": "^4.0.11", + "proj4": "^2.9.0" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index b9736fb37..1f6bc148c 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -159,7 +159,6 @@ function Gis({ // 피처 클릭시 const handleFeatureSelect = useCallback( (data) => { - console.log("handleFeatureSelect"); onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef], @@ -169,8 +168,6 @@ function Gis({ const handleFeatureFocus = useCallback( (layerName, data) => { console.log("handleFeatureFocus"); - console.log(layerName); - console.log(data); // onFeatureFocus?.(data); }, [mapInstanceRef], @@ -194,7 +191,6 @@ function Gis({ // OpenLayers 지도 객체 생성 if (!map) { - console.log("지도 객체 생성"); map = new OlMap({ layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], renderer: "webgl", @@ -222,14 +218,12 @@ function Gis({ } else { // 📌 재마운트 시 target 없으면 다시 지정 if (!map.getTarget()) { - console.log("재마운트"); map.setTarget(mapRef.current); } } const select = GisApp.Module.select; if (select) { - console.log("select 바인딩"); if (select.selectInteraction) { map.removeInteraction(select.selectInteraction); } @@ -264,7 +258,6 @@ function Gis({ // 📌 언마운트 시 완전 제거 return () => { - console.log("지도 제거"); map.setTarget(null); // DOM 연결 해제 mapInstanceRef.current = null; // 참조 초기화 }; @@ -361,12 +354,7 @@ function Gis({ layerNmRef.current = layerNm; setLayerNm(layerNm); setIsOpen(true); - const dummyFeature = { - getProperties: () => ({}), // 최소 구조 - }; - console.log(layerNm) onOpenPopup(layerNm) - // onFeatureSelect(dummyFeature); }} imageVisible={imgVisble} isPlaying={isPlaying} diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index d7d5b5b72..38729b2b2 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -16,7 +16,6 @@ export default React.memo(function GisSelect({ onFeatureSelect }) { // 피처 선택 시 콜백 등록 select.getSelectedInfo(function (features) { if (features?.[0]) { - console.log("GisSelect features", features); onFeatureSelect(features[0]); } }); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 7285caa07..53a26a3fa 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -402,7 +402,7 @@ import { activeTab === "layers" ? "block" : "hidden" }`} > -

레이어

+

시설물


{ text: f.properties.mctpv_korn_nm, val: f.properties.mctpv_cd, })); - setSidoData([{ text: "선택", val: "0" }, ...list]); + setSidoData([{ text: "시도", val: "0" }, ...list]); }; const fetchSigungu = async () => { @@ -260,7 +260,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => {
-
+
{ /> Date: Sun, 10 Aug 2025 19:13:03 +0900 Subject: [PATCH 25/42] =?UTF-8?q?fo=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 77ac1b85c..7c1d78164 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -196,8 +196,8 @@ const Map = () => { select.resetSelectedFeature(); // 2) 피처 탐색 + 레이어 유형 파악 - let featureId = String(data.getId?.() ?? ""); - let foundLayerType = featureId.split(".")[0]; + const featureId = String(data.getId?.() ?? ""); + const foundLayerType = featureId.split(".")[0]; let foundFeature: FeatureData | null = null; const isOlFeatureInstance = data && @@ -217,9 +217,6 @@ const Map = () => { const feature = source?.getFeatureById?.(featureId); if (feature) { foundFeature = feature; - // 레이어 이름(유형) 우선순위: name -> lyrTblNm -> id - foundLayerType = - layer.get("name") || layer.get("lyrTblNm") || layer.get("id"); } }); } From 96deebdaad6d322717d9e7ddcf91c5e95c06a714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Sun, 10 Aug 2025 19:18:17 +0900 Subject: [PATCH 26/42] =?UTF-8?q?Elint=20=EC=98=A4=EB=A5=98=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 --- .../pages/auth/login/ui/UserSignupModal.tsx | 299 +- pnpm-lock.yaml | 11353 ++++++++++++++++ 2 files changed, 11565 insertions(+), 87 deletions(-) create mode 100644 pnpm-lock.yaml diff --git a/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx b/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx index 06193951e..4fb5fdf5d 100644 --- a/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx +++ b/apps/cm/src/pages/auth/login/ui/UserSignupModal.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect } from "react"; import { SignupClient } from "@/shared/api/signup-client"; import { BASE_URL, ENDPOINT } from "@/shared/config"; -import type { UserSaveData } from "@/entities/user"; import "./user-signup-modal.css"; interface UserSignupModalProps { @@ -31,8 +30,6 @@ interface UserSignupData { enoCd?: string; } - - // 익명 사용자용 CodeSelect 컴포넌트 interface AnonymousCodeSelectProps { upCd: string; @@ -42,24 +39,32 @@ interface AnonymousCodeSelectProps { placeholder?: string; } -function AnonymousCodeSelect({ upCd, value, onValueChange, disabled, placeholder = "선택" }: AnonymousCodeSelectProps) { - const [codes, setCodes] = useState>([]); +function AnonymousCodeSelect({ + upCd, + value, + onValueChange, + disabled, + placeholder = "선택", +}: AnonymousCodeSelectProps) { + const [codes, setCodes] = useState< + Array<{ lwpoCd: string; lwpoCdNm: string }> + >([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchCodes = async () => { if (!upCd) return; - + setIsLoading(true); try { const signupClient = SignupClient.getInstance({ - baseURL: BASE_URL + baseURL: BASE_URL, }); - + const response = await signupClient.get( - `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}` + `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`, ); - + if (response.data && Array.isArray(response.data)) { setCodes(response.data); } else { @@ -94,7 +99,9 @@ function AnonymousCodeSelect({ upCd, value, onValueChange, disabled, placeholder } function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { - const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement'); + const [currentStep, setCurrentStep] = useState<"agreement" | "signup">( + "agreement", + ); const [agreed, setAgreed] = useState(false); const [formData, setFormData] = useState({ userId: "", @@ -126,7 +133,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { // 모달이 닫힐 때 초기화 useEffect(() => { if (!open) { - setCurrentStep('agreement'); + setCurrentStep("agreement"); setAgreed(false); setFormData({ userId: "", @@ -159,17 +166,19 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { const handleNextStep = () => { if (agreed) { - setCurrentStep('signup'); + setCurrentStep("signup"); } }; - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = ( + e: React.ChangeEvent, + ) => { const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - + setFormData((prev) => ({ ...prev, [name]: value })); + // 에러 초기화 if (errors[name]) { - setErrors(prev => ({ ...prev, [name]: "" })); + setErrors((prev) => ({ ...prev, [name]: "" })); } }; @@ -182,20 +191,27 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { if (!formData.userId) newErrors.userId = "아이디를 입력해주세요."; if (!formData.userName) newErrors.userName = "이름을 입력해주세요."; if (!formData.password) newErrors.password = "비밀번호를 입력해주세요."; - if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; + if (!formData.confirmPassword) + newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 선택해주세요."; if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요."; if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요."; // 비밀번호 확인 - if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) { + if ( + formData.password && + formData.confirmPassword && + formData.password !== formData.confirmPassword + ) { newErrors.confirmPassword = "비밀번호가 일치하지 않습니다."; } // 비밀번호 정규식 검증 - const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; + const passwordRegex = + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; if (formData.password && !passwordRegex.test(formData.password)) { - newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; + newErrors.password = + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; } // 이메일 형식 검증 @@ -204,8 +220,10 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { } // 중복 체크 검증 - if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다."; - if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다."; + if (idAvailable === false) + newErrors.userId = "이미 사용 중인 아이디입니다."; + if (emailAvailable === false) + newErrors.email = "이미 사용 중인 이메일입니다."; setErrors(newErrors); const isValid = Object.keys(newErrors).length === 0; @@ -215,7 +233,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { const checkUserIdDuplicate = async () => { if (!formData.userId) { - setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." })); + setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." })); return; } @@ -226,19 +244,25 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, - { userId: formData.userId } + { userId: formData.userId }, ); const exists = response.data; setIdAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." })); + setErrors((prev) => ({ + ...prev, + userId: "이미 사용 중인 아이디입니다.", + })); } else { - setErrors(prev => ({ ...prev, userId: "" })); + setErrors((prev) => ({ ...prev, userId: "" })); } } catch (error) { console.error("아이디 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + userId: "아이디 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingId(false); } @@ -246,7 +270,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { const checkEmailDuplicate = async () => { if (!formData.email) { - setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." })); + setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." })); return; } @@ -257,19 +281,25 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/exists`, - { email: formData.email } + { email: formData.email }, ); const exists = response.data; setEmailAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." })); + setErrors((prev) => ({ + ...prev, + email: "이미 사용 중인 이메일입니다.", + })); } else { - setErrors(prev => ({ ...prev, email: "" })); + setErrors((prev) => ({ ...prev, email: "" })); } } catch (error) { console.error("이메일 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + email: "이메일 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingEmail(false); } @@ -279,7 +309,7 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { e.preventDefault(); console.log("회원가입 버튼 클릭됨 - handleSubmit"); console.log("현재 폼 데이터:", formData); - + if (!validateForm()) { console.log("폼 검증 실패"); return; @@ -311,23 +341,23 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { console.log("SignupClient 생성 및 API 호출:", { baseURL: BASE_URL, endpoint: ENDPOINT.USER_SERVICE.USER_JOIN, - data: signupData + data: signupData, }); - + const signupClient = SignupClient.getInstance({ - baseURL: BASE_URL + baseURL: BASE_URL, }); const response = await signupClient.post( ENDPOINT.USER_SERVICE.USER_JOIN, - signupData + signupData, ); console.log("회원가입 성공:", response.data); - + // 성공 시 모달 닫기 및 콜백 호출 onSuccess(); onClose(); - + // 폼 초기화 setFormData({ userId: "", @@ -351,13 +381,12 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { setErrors({}); setIdAvailable(null); setEmailAvailable(null); - } catch (error: unknown) { console.error("회원가입 오류:", error); if (error instanceof Error) { - setErrors(prev => ({ ...prev, submit: error.message })); + setErrors((prev) => ({ ...prev, submit: error.message })); } else { - setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." })); + setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." })); } } finally { setIsLoading(false); @@ -366,40 +395,56 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { if (!open) return null; - return ( -
-
+ return ( +
+
-

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

- +

+ {currentStep === "agreement" ? "개인정보제공동의" : "회원가입"} +

+
- - {currentStep === 'agreement' ? ( + + {currentStep === "agreement" ? (

개인정보 수집 및 이용 동의

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

- +

1. 수집하는 개인정보 항목

    -
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • -
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
  • + 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, + 기관구분 +
  • +
  • + 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, + 상세기관코드, 사업자등록번호 +
- +

2. 개인정보의 수집 및 이용목적

  • 회원가입 및 관리
  • 서비스 제공 및 운영
  • 고객상담 및 문의응답
- +

3. 개인정보의 보유 및 이용기간

-

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

- +

+ 회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 + 기간까지) +

+

4. 동의 거부권 및 거부 시 불이익

-

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+

+ 개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 + 시 회원가입이 제한됩니다. +

- +
- +
- {idAvailable === true && 사용 가능한 아이디입니다.} - {errors.userId && {errors.userId}} + {idAvailable === true && ( + + 사용 가능한 아이디입니다. + + )} + {errors.userId && ( + + {errors.userId} + + )}
@@ -474,7 +527,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { placeholder="이름을 입력하세요" disabled={isLoading} /> - {errors.userName && {errors.userName}} + {errors.userName && ( + + {errors.userName} + + )}
@@ -500,8 +557,16 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { {isCheckingEmail ? "확인중..." : "중복확인"}
- {emailAvailable === true && 사용 가능한 이메일입니다.} - {errors.email && {errors.email}} + {emailAvailable === true && ( + + 사용 가능한 이메일입니다. + + )} + {errors.email && ( + + {errors.email} + + )}
@@ -518,7 +583,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.password && {errors.password}} + {errors.password && ( + + {errors.password} + + )}
@@ -535,7 +604,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.confirmPassword && {errors.confirmPassword}} + {errors.confirmPassword && ( + + {errors.confirmPassword} + + )}
@@ -545,11 +618,17 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { setFormData(prev => ({ ...prev, pswdHnt: value }))} + onValueChange={(value: string) => + setFormData((prev) => ({ ...prev, pswdHnt: value })) + } disabled={isLoading} placeholder="비밀번호 힌트를 선택하세요" /> - {errors.pswdHnt && {errors.pswdHnt}} + {errors.pswdHnt && ( + + {errors.pswdHnt} + + )}
@@ -566,14 +645,18 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={100} disabled={isLoading} /> - {errors.pswdAns && {errors.pswdAns}} + {errors.pswdAns && ( + + {errors.pswdAns} + + )}

연락처 정보

- +
@@ -587,7 +670,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.mphn && {errors.mphn}} + {errors.mphn && ( + + {errors.mphn} + + )}
@@ -604,14 +691,18 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.telno && {errors.telno}} + {errors.telno && ( + + {errors.telno} + + )}

주소 정보

- +
@@ -625,7 +716,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={5} disabled={isLoading} /> - {errors.zip && {errors.zip}} + {errors.zip && ( + + {errors.zip} + + )}
@@ -642,7 +737,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.addr && {errors.addr}} + {errors.addr && ( + + {errors.addr} + + )}
@@ -659,25 +758,35 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.daddr && {errors.daddr}} + {errors.daddr && ( + + {errors.daddr} + + )}

기관 정보

- +
setFormData(prev => ({ ...prev, laeSeCd: value }))} + onValueChange={(value: string) => + setFormData((prev) => ({ ...prev, laeSeCd: value })) + } disabled={isLoading} placeholder="법인구분을 선택하세요" /> - {errors.laeSeCd && {errors.laeSeCd}} + {errors.laeSeCd && ( + + {errors.laeSeCd} + + )}
@@ -687,11 +796,17 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { setFormData(prev => ({ ...prev, ogdpSeCd: value }))} + onValueChange={(value: string) => + setFormData((prev) => ({ ...prev, ogdpSeCd: value })) + } disabled={isLoading} placeholder="기관구분을 선택하세요" /> - {errors.ogdpSeCd && {errors.ogdpSeCd}} + {errors.ogdpSeCd && ( + + {errors.ogdpSeCd} + + )}
@@ -708,7 +823,11 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.instSeCd && {errors.instSeCd}} + {errors.instSeCd && ( + + {errors.instSeCd} + + )} @@ -725,21 +844,27 @@ function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.dtlInstCd && {errors.dtlInstCd}} + {errors.dtlInstCd && ( + + {errors.dtlInstCd} + + )} {errors.submit && (
-
{errors.submit}
+
+ {errors.submit} +
)}
- -
- - )); - } else { - coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data); + if(pk !==null){ + + if (checkFinish) { + toast((t) => ( + + 검토완료 변경시 공사비는 변경할수 없습니다. +
+ 진행하시겠습니까? +
+ + +
+
+ )); + } else { + coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data); + } + }else{ + toast.error('후보지를 먼저 선택해주세요') } } } else if (type === "hist") { From db441ab3533beac9a07b1144520108aeae888952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Sun, 10 Aug 2025 21:08:22 +0900 Subject: [PATCH 28/42] "yargs": "^17.7.2", --- apps/common/features/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/common/features/package.json b/apps/common/features/package.json index 8b62e9ffa..bfa43377e 100644 --- a/apps/common/features/package.json +++ b/apps/common/features/package.json @@ -39,6 +39,7 @@ "sonner": "^2.0.1", "tailwindcss-animate": "^1.0.7", "tus-js-client": "^4.3.1", + "yargs": "^18.0.0", "zod": "^3.24.2", "zustand": "^5.0.3" }, From be43fd38a7c91dcafdf2a04a8525ffa8f9f987af Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 21:32:21 +0900 Subject: [PATCH 29/42] =?UTF-8?q?=EB=AC=BC=EC=9E=AC=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=B3=B4=EA=B8=B0=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-layout)/map/mainMap/page.tsx | 9 +- apps/wr/app/(map-layout)/map/popup/page.tsx | 153 -------- apps/wr/app/layout.tsx | 16 +- apps/wr/app/map/searchBar/SearchInputText.tsx | 122 ------ apps/wr/app/map/searchBar/comboSearchBar.tsx | 298 --------------- apps/wr/app/map/searchBar/procSearchBar.tsx | 298 --------------- apps/wr/app/map/searchBar/searchBar.tsx | 351 ------------------ apps/wr/app/store/store.tsx | 2 +- apps/wr/middleware.ts | 13 +- .../src/ui/map/searchBar/searchBar.tsx | 34 +- 10 files changed, 41 insertions(+), 1255 deletions(-) delete mode 100644 apps/wr/app/(map-layout)/map/popup/page.tsx delete mode 100644 apps/wr/app/map/searchBar/SearchInputText.tsx delete mode 100644 apps/wr/app/map/searchBar/comboSearchBar.tsx delete mode 100644 apps/wr/app/map/searchBar/procSearchBar.tsx delete mode 100644 apps/wr/app/map/searchBar/searchBar.tsx diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 7451ae4b5..4130fff63 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -56,13 +56,11 @@ const Map = () => { const messageHandler = (event: MessageEvent) => { if (event.data.type === "POPUP_CLOSED") { if (event.data.target === "detail") { - console.log("디테일 팝업 닫힘 감지됨"); setFeatureData(null); setIsOpen(false); setIsSearchModalOpen(true); popupDetailRef.current = null; } else { - console.log("기타 팝업 닫힘 감지됨"); popupRef.current = null; } } @@ -74,10 +72,8 @@ const Map = () => { }, []); useEffect(() => { - console.log("isFeatured ", isFeatured); if (isFeatured) { - console.log("isFeatured ", isFeatured); onFeatureSelect(isFeatured); } }, [isFeatured]); @@ -93,7 +89,7 @@ const Map = () => { // SearchModal, GisContent 등에서 호출 const onFeatureFocus = (data: any) => { - console.log(data); + console.log("onFeatureFocus"); }; const onFeatureSelect = (data: any) => { @@ -114,7 +110,6 @@ const Map = () => { let foundFeature: any = null; let featureId = data.fid; const layerName = data.layerNm; - console.log("선택된 레이어 이름:", data, layerName); if (layerName === "하수재이용시설") { // 재이용시설 featureId = `${layerName}.${data.searchPk}`; @@ -134,7 +129,6 @@ const Map = () => { //수요처 featureId = `${layerName}.${data.dmdInfoId.replaceAll("-", "")}`; } else { - console.warn("해당 레이어에 대한 featureId 매핑 없음", layerName); featureId = `${layerName}.${data.fid || data.id || data.ftrIdn}`; } map @@ -214,7 +208,6 @@ const Map = () => { const isFeature = props?.featureData; - console.log('isFeature 1', isFeature) if( typeof isFeature.getProperties === "function" ){ if ( diff --git a/apps/wr/app/(map-layout)/map/popup/page.tsx b/apps/wr/app/(map-layout)/map/popup/page.tsx deleted file mode 100644 index ca7a07b43..000000000 --- a/apps/wr/app/(map-layout)/map/popup/page.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// "use client"; - -// import Gis from "@common/business_components/ui/map/Gis"; -// import { useEffect, useRef, useState, useCallback } from "react"; -// import { mapService } from "../service"; -// import NewWindowPopup from "../components/newWindowPopup"; -// import GisApp from "@common/business_components/ui/map/lib/gis.core"; - -// const Map = () => { -// const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 -// const gisMaps = useRef(null); -// const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 -// const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 -// const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 -// const [checkedLayers, setCheckedLayers] = useState([]); -// const [checkedLayersData, setCheckedLayersData] = useState([]); - -// const fetchData = async () => { -// try { -// const response = await mapService.getLyrStngs(); -// setTreeData(response); // API 응답 저장 -// } catch (error) { -// console.error("데이터 불러오기 실패:", error); -// } -// }; - -// // 팝업 닫기 핸들러를 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) => { -// const 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: any = 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) { -// const geometry = foundFeature.getGeometry(); -// if (geometry) { -// const 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); -// }; - -// // MapSearchBar 래퍼 컴포넌트 -// // const CustomMapSearchBar = (props: any) => { -// // return ( -// // -// // ); -// // }; - -// return ( -//
-// -// // -// { - -// console.log('p' ,props) -// } -// } -// onFeatureSelect={onFeatureSelect} -// setIsOpen={setIsOpen} -// setFeatureData={setFeatureData} -// featureData={featureData} -// setIsSearchModalOpen={setIsSearchModalOpen} -// isOpen={isOpen} -// isSearchModalOpen={isSearchModalOpen} -// onCheckedLayersChange={handleCheckedLayersChange} -// imgVisble={true} -// searchBar={undefined} -// /> -//
-// ); -// }; - -// export default Map; diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index fe4c3aff0..953c1d7ab 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -4,6 +4,8 @@ import i18next from "i18next"; import { I18nextProvider } from "react-i18next"; import koBase from "../public/locales/ko/message.json"; import { StoreProvider } from "./store"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect, useMemo } from "react"; export default function RootLayout({ children, @@ -20,13 +22,21 @@ export default function RootLayout({ }, }); + useEffect(() => { + // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 + void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) + .catch((e) => { + console.error("Auto login failed:", e); + }); + }, []); + return ( - - {children} - + + {children} + diff --git a/apps/wr/app/map/searchBar/SearchInputText.tsx b/apps/wr/app/map/searchBar/SearchInputText.tsx deleted file mode 100644 index 97ba51fb7..000000000 --- a/apps/wr/app/map/searchBar/SearchInputText.tsx +++ /dev/null @@ -1,122 +0,0 @@ -// SearchInputText.tsx -"use client"; -import { Input } from "@common/components/ui"; -import { useEffect, useRef, useState } from "react"; - -interface SearchInputTextProps { - initValue?: string; - setValue: (val: string) => void; - setRtnData: (val: { fnm: string; fid: string }) => void; - searchList: { fnm: string; fid: string }[]; - onEnter?: () => void; - className?: string; -} - -export default function SearchInputText({ - initValue, - setValue, - setRtnData, - searchList, - onEnter, - className, -}: SearchInputTextProps) { - const [showResults, setShowResults] = useState(false); - const [text, setText] = useState(initValue ?? ""); - const [highlightIndex, setHighlightIndex] = useState(-1); - const wrapperRef = useRef(null); - - useEffect(() => { - setText(initValue ?? ""); - }, [initValue]); - - useEffect(() => { - if (searchList.length > 0) { - setShowResults(true); - } else { - setShowResults(false); - } - setHighlightIndex(-1); - }, [searchList]); - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - wrapperRef.current && - !(wrapperRef.current as any).contains(event.target) - ) { - setShowResults(false); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [wrapperRef]); - - const handleChange = (e: React.ChangeEvent) => { - setText(e.target.value); - setValue(e.target.value); - setHighlightIndex(-1); - }; - - const handleSelect = (item: { fnm: string; fid: string }) => { - setText(item.fnm); - setValue(item.fnm); - setRtnData(item); - setShowResults(false); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - if (highlightIndex >= 0 && highlightIndex < searchList.length) { - const selected = searchList[highlightIndex]; - if (selected) handleSelect(selected); - } else if (onEnter) { - onEnter(); - } - } else if (e.key === "ArrowDown") { - e.preventDefault(); - setHighlightIndex((prev) => - prev < searchList.length - 1 ? prev + 1 : prev - ); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setHighlightIndex((prev) => (prev > 0 ? prev - 1 : -1)); - } - }; - - return ( -
- - {showResults && ( -
- {searchList.length > 0 ? ( - searchList.map((item, index) => ( -
handleSelect(item)} - > - {item.fnm} -
- )) - ) : ( -
- 검색 결과가 없습니다. -
- )} -
- )} -
- ); -} diff --git a/apps/wr/app/map/searchBar/comboSearchBar.tsx b/apps/wr/app/map/searchBar/comboSearchBar.tsx deleted file mode 100644 index 15736d710..000000000 --- a/apps/wr/app/map/searchBar/comboSearchBar.tsx +++ /dev/null @@ -1,298 +0,0 @@ -"use client"; -import { SelectBox, type SelectDataType } from "@common/business_components/ui"; -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useState } from "react"; - -const geoserverUrl = - "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json"; - -const FACILITY_TYPE_URL_MAP: Record = { - tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설 - tb_wr_bdst_ruse_fclt: - "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post - tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get - tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get - tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get - tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get - tb_wr_tmpr_dmd_co: - "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get -}; - -interface SearchBarProps { - onFeatureSelect: (feature: any) => void; - gisMaps: React.RefObject; - facilityTypes: { text: string; val: string }[]; -} - -export function SearchBar({ - onFeatureSelect, - gisMaps, - facilityTypes, -}: SearchBarProps) { - const [sidoValue, setSidoValue] = useState("0"); - const [sigunValue, setSigunValue] = useState("0"); - const [sidoData, setSidoData] = useState([ - { text: "선택", val: "0" }, - ]); - const [sigunData, setSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - const [filteredSigunData, setFilteredSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - - const [facilityData, setFacilityData] = useState([ - { text: "선택", val: "0" }, - ]); - const [facilityValue, setFacilityValue] = useState("0"); - - const [facilityType, setFacilityType] = useState( - facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" - ); - - //시도, 시군구 조회 - const fetchSido = async () => { - const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`; - const res = await fetch(url); - const json = await res.json(); - const sidoList = json - .filter( - (item: any) => - typeof item.lwpoCd === "string" && item.lwpoCd.length === 2 - ) - .map((feature: any) => ({ - val: feature.lwpoCd, - text: feature.lwpoCdNm, - })); - setSidoData([{ text: "선택", val: "0" }, ...sidoList]); - - const sigunList = json - .filter( - (item: any) => - typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2 - ) - .map((feature: any) => ({ - val: feature.lwpoCd, - text: feature.lwpoCdNm, - })); - setSigunData(sigunList); - }; - - //시설명 콤보박스 조회 - const fetchFacilitiesBySggNm = async ( - sggCd: string, - sggNm: string, - sidoNm: string, - currentFacilityType?: string - ) => { - const type = currentFacilityType ?? facilityType; - if (!type || type === "0") return; - - const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; - if (!baseUrlTemplate) return; - - const year = new Date().getFullYear().toString(); - let url = baseUrlTemplate - .replace("{year}", year) - .replace("{sggCd}", sggCd) - .replace("{sidoNm}", encodeURIComponent(sidoNm)) - .replace("{sggNm}", encodeURIComponent(sggNm)); - - let list: SelectDataType[] = []; - - if ( - type === "tb_il_swage_prcs_fclt" //하수처리시설 - ) { - const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`; - url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`; - const res = await fetch(url); - const json = await res.json(); - list = json.features.map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", - val: f.id, - })); - } else if ( - type === "tb_wr_bdst_ruse_fclt" // 재이용시설 - ) { - //재용에서 참조하는 하수처리시설 목록 - const paramsRes = await fetch("/reuse-service/reuseFc/pubFc"); - const paramsJson = await paramsRes.json(); - const params = paramsJson.data; - - const res = await fetch(url, { - method: "POST", - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", - val: item.pbnstRuseFcltId, - })); - } else if ( - type === "tb_wr_ruse_fclt_dmd_co" //수요처 - ) { - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.dmdInfoId, - })); - } else if ( - type === "tb_wr_tmpr_dmd_co" //잠재수요처 - ) { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.fctryMngSn, - })); - } else { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", - val: item.bdstInfoSn, - })); - } - - setFacilityData([{ text: "선택", val: "0" }, ...list]); - setFacilityValue("0"); - }; - - //시도, 시군구 초기 데이터 조회 - useEffect(() => { - fetchSido(); - }, []); - - //시군구 데이터 초기화 - const initializeSigunData = useCallback( - (sidoCd: string) => { - if (sidoCd === "0") { - setFilteredSigunData([{ text: "선택", val: "0" }]); - setSigunValue("0"); - return; - } - const prefix = sidoCd.substring(0, 2); - const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]); - setSigunValue("0"); - }, - [sigunData] - ); - - //콤보박스 선택시 연관 콤보박스 처리 - const onSelectValue = useCallback( - (val: string, type: string) => { - if (type === "ctpvCd") { - setSidoValue(val); - initializeSigunData(val); - setFacilityData([{ text: "선택", val: "0" }]); - setFacilityValue("0"); - } else if (type === "sggCd") { - setSigunValue(val); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = sigunData.find((item) => item.val === val)?.text || ""; - const sggCd = sigunData.find((item) => item.val === val)?.val || ""; - if (sggNm && sidoNm) - fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType); - } else if (type === "facilityType") { - setFacilityType(val); - setFacilityValue("0"); - setFacilityData([{ text: "선택", val: "0" }]); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = - sigunData.find((item) => item.val === sigunValue)?.text || ""; - const sggCd = - sigunData.find((item) => item.val === sigunValue)?.val || ""; - if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val); - } else if (type === "fcltCd") { - setFacilityValue(val); - } - }, - [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue] - ); - - //조회 버튼 클릭 - const handleSearch = () => { - if (!gisMaps.current || facilityValue === "0") return; - const layers = gisMaps.current.getLayers().getArray(); - for (const layer of layers) { - const source = layer.getSource?.(); - const feature = source?.getFeatureById?.(facilityValue); - if (feature) { - onFeatureSelect(feature); - return; - } - } - alert("해당 처리시설을 지도에서 찾을 수 없습니다."); - }; - - return ( -
-
-
-
- {facilityTypes.length > 1 && ( - - onSelectValue(val.toString(), "facilityType") - } - value={facilityType} - /> - )} - onSelectValue(val.toString(), "ctpvCd")} - value={sidoValue} - /> - onSelectValue(val.toString(), "sggCd")} - value={sigunValue} - /> - onSelectValue(val.toString(), "fcltCd")} - value={facilityValue} - /> -
-
- -
-
-
-
- ); -} diff --git a/apps/wr/app/map/searchBar/procSearchBar.tsx b/apps/wr/app/map/searchBar/procSearchBar.tsx deleted file mode 100644 index 15736d710..000000000 --- a/apps/wr/app/map/searchBar/procSearchBar.tsx +++ /dev/null @@ -1,298 +0,0 @@ -"use client"; -import { SelectBox, type SelectDataType } from "@common/business_components/ui"; -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useState } from "react"; - -const geoserverUrl = - "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json"; - -const FACILITY_TYPE_URL_MAP: Record = { - tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설 - tb_wr_bdst_ruse_fclt: - "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post - tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get - tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get - tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get - tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get - tb_wr_tmpr_dmd_co: - "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get -}; - -interface SearchBarProps { - onFeatureSelect: (feature: any) => void; - gisMaps: React.RefObject; - facilityTypes: { text: string; val: string }[]; -} - -export function SearchBar({ - onFeatureSelect, - gisMaps, - facilityTypes, -}: SearchBarProps) { - const [sidoValue, setSidoValue] = useState("0"); - const [sigunValue, setSigunValue] = useState("0"); - const [sidoData, setSidoData] = useState([ - { text: "선택", val: "0" }, - ]); - const [sigunData, setSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - const [filteredSigunData, setFilteredSigunData] = useState([ - { text: "선택", val: "0" }, - ]); - - const [facilityData, setFacilityData] = useState([ - { text: "선택", val: "0" }, - ]); - const [facilityValue, setFacilityValue] = useState("0"); - - const [facilityType, setFacilityType] = useState( - facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" - ); - - //시도, 시군구 조회 - const fetchSido = async () => { - const url = `/cms-service/api/v1/code/cache/ADSTDSTRCD/Y`; - const res = await fetch(url); - const json = await res.json(); - const sidoList = json - .filter( - (item: any) => - typeof item.lwpoCd === "string" && item.lwpoCd.length === 2 - ) - .map((feature: any) => ({ - val: feature.lwpoCd, - text: feature.lwpoCdNm, - })); - setSidoData([{ text: "선택", val: "0" }, ...sidoList]); - - const sigunList = json - .filter( - (item: any) => - typeof item.lwpoCd === "string" && item.lwpoCd.length !== 2 - ) - .map((feature: any) => ({ - val: feature.lwpoCd, - text: feature.lwpoCdNm, - })); - setSigunData(sigunList); - }; - - //시설명 콤보박스 조회 - const fetchFacilitiesBySggNm = async ( - sggCd: string, - sggNm: string, - sidoNm: string, - currentFacilityType?: string - ) => { - const type = currentFacilityType ?? facilityType; - if (!type || type === "0") return; - - const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; - if (!baseUrlTemplate) return; - - const year = new Date().getFullYear().toString(); - let url = baseUrlTemplate - .replace("{year}", year) - .replace("{sggCd}", sggCd) - .replace("{sidoNm}", encodeURIComponent(sidoNm)) - .replace("{sggNm}", encodeURIComponent(sggNm)); - - let list: SelectDataType[] = []; - - if ( - type === "tb_il_swage_prcs_fclt" //하수처리시설 - ) { - const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`; - url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`; - const res = await fetch(url); - const json = await res.json(); - list = json.features.map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", - val: f.id, - })); - } else if ( - type === "tb_wr_bdst_ruse_fclt" // 재이용시설 - ) { - //재용에서 참조하는 하수처리시설 목록 - const paramsRes = await fetch("/reuse-service/reuseFc/pubFc"); - const paramsJson = await paramsRes.json(); - const params = paramsJson.data; - - const res = await fetch(url, { - method: "POST", - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", - val: item.pbnstRuseFcltId, - })); - } else if ( - type === "tb_wr_ruse_fclt_dmd_co" //수요처 - ) { - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.dmdInfoId, - })); - } else if ( - type === "tb_wr_tmpr_dmd_co" //잠재수요처 - ) { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.fctryMngSn, - })); - } else { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", - val: item.bdstInfoSn, - })); - } - - setFacilityData([{ text: "선택", val: "0" }, ...list]); - setFacilityValue("0"); - }; - - //시도, 시군구 초기 데이터 조회 - useEffect(() => { - fetchSido(); - }, []); - - //시군구 데이터 초기화 - const initializeSigunData = useCallback( - (sidoCd: string) => { - if (sidoCd === "0") { - setFilteredSigunData([{ text: "선택", val: "0" }]); - setSigunValue("0"); - return; - } - const prefix = sidoCd.substring(0, 2); - const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]); - setSigunValue("0"); - }, - [sigunData] - ); - - //콤보박스 선택시 연관 콤보박스 처리 - const onSelectValue = useCallback( - (val: string, type: string) => { - if (type === "ctpvCd") { - setSidoValue(val); - initializeSigunData(val); - setFacilityData([{ text: "선택", val: "0" }]); - setFacilityValue("0"); - } else if (type === "sggCd") { - setSigunValue(val); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = sigunData.find((item) => item.val === val)?.text || ""; - const sggCd = sigunData.find((item) => item.val === val)?.val || ""; - if (sggNm && sidoNm) - fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType); - } else if (type === "facilityType") { - setFacilityType(val); - setFacilityValue("0"); - setFacilityData([{ text: "선택", val: "0" }]); - const sidoNm = - sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = - sigunData.find((item) => item.val === sigunValue)?.text || ""; - const sggCd = - sigunData.find((item) => item.val === sigunValue)?.val || ""; - if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val); - } else if (type === "fcltCd") { - setFacilityValue(val); - } - }, - [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue] - ); - - //조회 버튼 클릭 - const handleSearch = () => { - if (!gisMaps.current || facilityValue === "0") return; - const layers = gisMaps.current.getLayers().getArray(); - for (const layer of layers) { - const source = layer.getSource?.(); - const feature = source?.getFeatureById?.(facilityValue); - if (feature) { - onFeatureSelect(feature); - return; - } - } - alert("해당 처리시설을 지도에서 찾을 수 없습니다."); - }; - - return ( -
-
-
-
- {facilityTypes.length > 1 && ( - - onSelectValue(val.toString(), "facilityType") - } - value={facilityType} - /> - )} - onSelectValue(val.toString(), "ctpvCd")} - value={sidoValue} - /> - onSelectValue(val.toString(), "sggCd")} - value={sigunValue} - /> - onSelectValue(val.toString(), "fcltCd")} - value={facilityValue} - /> -
-
- -
-
-
-
- ); -} diff --git a/apps/wr/app/map/searchBar/searchBar.tsx b/apps/wr/app/map/searchBar/searchBar.tsx deleted file mode 100644 index ab3dacb0e..000000000 --- a/apps/wr/app/map/searchBar/searchBar.tsx +++ /dev/null @@ -1,351 +0,0 @@ -"use client"; -import { SelectBox, type SelectDataType } from "@common/business_components/ui"; -import { Button } from "@common/components/ui"; -import { useCallback, useEffect, useState } from "react"; -import SearchInputText from "./SearchInputText"; - -const FACILITY_TYPE_URL_MAP: Record = { - tb_il_swage_prcs_fclt: - "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&propertyName=swtrmt_fclt_cd,swtrmt_fclt_nm&cql_filter=", - tb_wr_bdst_ruse_fclt: - "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000/", - tb_wr_rnwtr_utztn_fclt: - "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}/{fcltyNm}", - tb_wr_rswtr_fclt: - "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}/{fcltyNm}", - tb_wr_htdfl_fclt: - "/reuse-service/drain/facilityList/1/1000/{sggCd}/{fcltyNm}", - tb_wr_ruse_fclt_dmd_co: - "/reuse-service/reuseFc/request/1/1000/{sggCd}/{fcltyNm}", - tb_wr_tmpr_dmd_co: - "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}/{fcltyNm}", -}; - -interface SearchBarProps { - gisMaps: React.RefObject; - facilityTypes: { text: string; val: string }[]; - onFeatureFocus: any; -} - -export function SearchBar({ - gisMaps, - facilityTypes, - onFeatureFocus, -}: SearchBarProps) { - console.log('gisMaps ', gisMaps) - console.log('onFeatureFocus ', onFeatureFocus) - - const [sidoValue, setSidoValue] = useState("0"); - const [sigunValue, setSigunValue] = useState("0"); - const [facilityType, setFacilityType] = useState( - facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" - ); - const [fcltyNm, setFcltyNm] = useState(""); - const [searchList, setSearchList] = useState<{ fnm: string; fid: string }[]>( - [] - ); - - const [sidoData, setSidoData] = useState([ - { text: "시도", val: "0" }, - ]); - const [sigunData, setSigunData] = useState([ - { text: "시군구", val: "0" }, - ]); - const [filteredSigunData, setFilteredSigunData] = useState([ - { text: "시군구", val: "0" }, - ]); - - const fetchSido = async () => { - const res = await fetch("/cms-service/api/v1/code/cache/ADSTDSTRCD/Y"); - const json = await res.json(); - const sidoList = json - .filter((item: any) => item.lwpoCd?.length === 2) - .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); - const sigunList = json - .filter((item: any) => item.lwpoCd?.length > 2) - .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); - setSidoData([{ text: "시도", val: "0" }, ...sidoList]); - setSigunData(sigunList); - }; - - const initializeSigunData = useCallback( - (sidoCd: string) => { - if (sidoCd === "0") { - setFilteredSigunData([{ text: "시도", val: "0" }]); - setSigunValue("0"); - return; - } - const prefix = sidoCd.substring(0, 2); - const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); - setSigunValue("0"); - }, - [sigunData] - ); - - useEffect(() => { - fetchSido(); - }, []); - - const onSelectValue = useCallback( - (val: string, type: string) => { - if (type === "ctpvCd") { - setSidoValue(val); - initializeSigunData(val); - setSearchList([]); - setFcltyNm(""); - } else if (type === "sggCd") { - setSigunValue(val); - setSearchList([]); - setFcltyNm(""); - } else if (type === "facilityType") { - setFacilityType(val); - setSearchList([]); - setFcltyNm(""); - } - }, - [initializeSigunData] - ); - - const handleSearch = async () => { - if (!fcltyNm) return alert("시설명을 입력하세요."); - - const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || ""; - const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || ""; - - const type = facilityType; - const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; - if (!baseUrlTemplate) return; - - let url = baseUrlTemplate; - const year = new Date().getFullYear().toString(); - url = url - .replace("{year}", year) - .replace("{sggCd}", sigunValue) - .replace("{sidoNm}", encodeURIComponent(sidoNm)) - .replace("{sggNm}", encodeURIComponent(sggNm)) - .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); - - let list: SelectDataType[] = []; - - if (type === "tb_il_swage_prcs_fclt") { - const cqlFilter = `ctpv_nm='${sidoNm}' and sgg_nm='${sggNm}' and swtrmt_fclt_nm like '%${fcltyNm}%'`; - url += encodeURIComponent(cqlFilter); - const res = await fetch(url); - const json = await res.json(); - list = json.features.map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", - val: f.id, - })); - } else if ( - type === "tb_wr_bdst_ruse_fclt" // 재이용시설 - ) { - //재이용에서 참조하는 하수처리시설 목록 - const procRes = await fetch("/reuse-service/reuseFc/pubFc"); - const procJson = await procRes.json(); - const procData = procJson.data; - const procResult = procData - .filter((code: string) => code.slice(0, 5) === sigunValue) - .map((code: string) => { - return code; - }); - - //처리시설명으로 처리시설코드 조회 - let procUrl2 = - "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}"; - procUrl2 = procUrl2 - .replace("{sggCd}", sigunValue) - .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); - const procRes2 = await fetch(procUrl2, { - method: "POST", - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(procResult), - }); - const procJson2 = await procRes2.json(); - const procResult2 = (procJson2.data || []).map((item: any) => item.fid); - - //최종 처리시설코드로 재이용시설조회 - const res = await fetch(url, { - method: "POST", - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(procResult2), - }); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", - val: item.pbnstRuseFcltId, - })); - } else if ( - type === "tb_wr_ruse_fclt_dmd_co" //수요처 - ) { - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.dmdInfoId, - })); - } else if ( - type === "tb_wr_tmpr_dmd_co" //잠재수요처 - ) { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", - val: item.fctryMngSn, - })); - } else { - // GET 방식 - const res = await fetch(url); - const json = await res.json(); - list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", - val: item.bdstInfoSn, - })); - } - - if (list.length === 0) { - alert("검색 결과가 없습니다."); - setSearchList([]); - setFcltyNm(""); - return; - } - - const newList = list.map((item: any) => { - const code = item.val; - const name = item.text; - return { fnm: name, fid: code }; - }); - setSearchList(newList); - }; - - const moveFocus = (feature: any) => { - console.log('feature ' ,feature) - - // console.log('searchbar:moveFocus') - // const 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: any = null; - // let featureId = data.fid; - // const layerName = data.layerNm; - // 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) { - // const geometry = foundFeature.getGeometry(); - // if (geometry) { - // const 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); - // } - }; - - return ( -
-
-
-
- {facilityTypes.length > 1 && ( - - onSelectValue(val.toString(), "facilityType") - } - value={facilityType} - /> - )} - onSelectValue(val.toString(), "ctpvCd")} - value={sidoValue} - /> - onSelectValue(val.toString(), "sggCd")} - value={sigunValue} - /> - { - setFcltyNm(val); - }} - setRtnData={(selected: { fnm: string; fid: string }) => { - // 선택된 항목이 있을 경우 onFeatureSelect 실행 - const type = facilityType; - moveFocus({ - layerNm: type, - ...selected, - }); - }} - searchList={searchList} - onEnter={handleSearch} - className="mr-5 w-[150px]" - /> -
-
- -
-
-
-
- ); -} diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index c7dcf1ca8..bbff85bcb 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -342,7 +342,7 @@ export const StoreZus = ( getMiddleRegionCd(set); getUnitWaterShedCd(set); getEnvPartCd(set); - LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); + // LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); return { ...initState, ...comFcltSearchAction(set), diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index c5c7f8c01..e47a2a8f5 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -16,15 +16,6 @@ export function middleware(req: NextRequest) { console.log(`[Middleware] Request URL: ${req.url}`); const searchParams = req.nextUrl.searchParams; - // if (pathname.startsWith("/server")) { - // const queryParams = searchParams.toString(); - // const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ - // queryParams ? `?${queryParams}` : "" - // }`; - // console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); - // return NextResponse.rewrite(new URL(newUrl)); - // } - if (pathname.startsWith("/server")) { const decodedParams = Array.from(searchParams.entries()) .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) @@ -46,7 +37,6 @@ export function middleware(req: NextRequest) { const Authorization = req.cookies.get(CLAIM_NAME)?.value; const newUrl = `${serverApiUrl}${pathname}`; - console.log("Authorization cms-service ", Authorization); if (Authorization) requestHeaders.set(CLAIM_NAME, Authorization); //if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -90,7 +80,6 @@ export function middleware(req: NextRequest) { if (pathname.startsWith("/geoserver")) { let queryParams = searchParams.toString(); const typename = searchParams.get("TYPENAME"); - console.log("typename ", typename); const startYmd = searchParams.get("START_YMD") ?? null; const endYmd = searchParams.get("END_YMD") ?? null; @@ -129,7 +118,7 @@ export function middleware(req: NextRequest) { newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; } - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + //console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index e750ab1a7..3f5fc1820 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -69,7 +69,7 @@ export const SearchBar = ({ const initializeSigunData = useCallback( (sidoCd: string) => { if (sidoCd === "0") { - setFilteredSigunData([{ text: "시도", val: "0" }]); + setFilteredSigunData([{ text: "시군구", val: "0" }]); setSigunValue("0"); return; } @@ -110,7 +110,6 @@ export const SearchBar = ({ const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || ""; const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || ""; - const type = facilityType; const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; if (!baseUrlTemplate) return; @@ -149,8 +148,7 @@ export const SearchBar = ({ }); //처리시설명으로 처리시설코드 조회 - let procUrl2 = - "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}"; + let procUrl2 = "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}"; procUrl2 = procUrl2 .replace("{sggCd}", sigunValue) .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); @@ -163,7 +161,7 @@ export const SearchBar = ({ body: JSON.stringify(procResult), }); const procJson2 = await procRes2.json(); - const procResult2 = (procJson2.data || []).map((item: any) => item.fid); + const procResult2 = (procJson2.data || []).map((item: any) => item.bscFctCd); //최종 처리시설코드로 재이용시설조회 const res = await fetch(url, { @@ -179,6 +177,7 @@ export const SearchBar = ({ text: item.ruseUtztnFcltNm ?? "(이름없음)", val: item.pbnstRuseFcltId, })); + } else if ( type === "tb_wr_ruse_fclt_dmd_co" //수요처 ) { @@ -225,6 +224,23 @@ export const SearchBar = ({ //해당 시설물로 이동 const handleFeatureFocus = (data: any) => { + + let featureId = data.fid; + const layerNm = data.layerNm; + + if (!isNaN(Number(featureId))) { + // 숫자면 문자열로 변환 + featureId = String(featureId); + } + + if (layerNm === "tb_wr_bdst_ruse_fclt") { + featureId = featureId.slice(0, 11); + } + + if (!featureId.includes(".")) { + featureId = layerNm + "." + featureId; + } + const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); @@ -237,7 +253,7 @@ export const SearchBar = ({ //select.resetSelectedFeature(); // 2. data가 OpenLayers Feature 인스턴스인지 확인 let foundFeature: any = null; - let featureId = data.fid; + map .getLayers() .getArray() @@ -285,7 +301,7 @@ export const SearchBar = ({ { text: "시설 구분", val: "0" }, ...facilityTypes, ]} - onSelectValue={(val:any) => + onSelectValue={(val: any) => onSelectValue(val.toString(), "facilityType") } value={facilityType} @@ -297,7 +313,7 @@ export const SearchBar = ({ selectClass="rounded-lg bg-white" label="" selectArray={sidoData} - onSelectValue={(val:any) => onSelectValue(val.toString(), "ctpvCd")} + onSelectValue={(val: any) => onSelectValue(val.toString(), "ctpvCd")} value={sidoValue} /> onSelectValue(val.toString(), "sggCd")} + onSelectValue={(val: any) => onSelectValue(val.toString(), "sggCd")} value={sigunValue} /> Date: Sun, 10 Aug 2025 21:45:24 +0900 Subject: [PATCH 30/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 4130fff63..ef337e39c 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -42,16 +42,6 @@ const Map = () => { fetchData(); }, []); - useEffect(() => { - const bc = new BroadcastChannel("mapDetailPopUp"); - bc.onmessage = (event) => { - if (event.data.type === "SELECT_FEATURE") { - onFeatureSelect(event.data.feature); - closePopup(); - } - }; - }, []); - useEffect(() => { const messageHandler = (event: MessageEvent) => { if (event.data.type === "POPUP_CLOSED") { @@ -71,13 +61,6 @@ const Map = () => { }; }, []); - useEffect(() => { - - if (isFeatured) { - onFeatureSelect(isFeatured); - } - }, [isFeatured]); - // 체크된 레이어 정보를 받는 핸들러 const handleCheckedLayersChange = ( checkedIds: string[], @@ -88,7 +71,7 @@ const Map = () => { }; // SearchModal, GisContent 등에서 호출 - const onFeatureFocus = (data: any) => { + const onFeatureFocus = (_data: any) => { console.log("onFeatureFocus"); }; @@ -168,6 +151,23 @@ const Map = () => { setIsSearchModalOpen(false); }; + useEffect(() => { + + if (isFeatured) { + onFeatureSelect(isFeatured); + } + }, [isFeatured, onFeatureSelect]); + + useEffect(() => { + const bc = new BroadcastChannel("mapDetailPopUp"); + bc.onmessage = (event) => { + if (event.data.type === "SELECT_FEATURE") { + onFeatureSelect(event.data.feature); + closePopup(); + } + }; + }, [onFeatureSelect]); + const getSelectedLayers = (isFeature: any) => { const map = gisMaps.current; if (!map) { From 94306f052e50b22d382be6b202e32219637c44a3 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 21:55:26 +0900 Subject: [PATCH 31/42] =?UTF-8?q?=EB=A7=9D=ED=95=A0=20=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/layout.tsx | 2 +- apps/wr/app/store/store.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index 953c1d7ab..17945be51 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -5,7 +5,7 @@ import { I18nextProvider } from "react-i18next"; import koBase from "../public/locales/ko/message.json"; import { StoreProvider } from "./store"; import { LoginApi } from "@/src/entities/auth"; -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; export default function RootLayout({ children, diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index bbff85bcb..e19ec2216 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -161,7 +161,6 @@ import { type rswtStatState, type rswtStatType, } from "./stats/rswtStat"; -import { LoginApi } from "@/src/entities/auth"; import { authActionsExport, authStateExport, @@ -342,7 +341,6 @@ export const StoreZus = ( getMiddleRegionCd(set); getUnitWaterShedCd(set); getEnvPartCd(set); - // LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); return { ...initState, ...comFcltSearchAction(set), From d85e8c057248e4250145e938eb1d489dc4426353 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 21:57:09 +0900 Subject: [PATCH 32/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index ef337e39c..85eb17dba 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -70,10 +70,6 @@ const Map = () => { setCheckedLayersData(checkedNodesData); }; - // SearchModal, GisContent 등에서 호출 - const onFeatureFocus = (_data: any) => { - console.log("onFeatureFocus"); - }; const onFeatureSelect = (data: any) => { const map = gisMaps.current; From 1a4fa66519d3bc1ebbf64f9cd1ee518d029865e0 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 21:57:58 +0900 Subject: [PATCH 33/42] =?UTF-8?q?=EB=B9=8C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 85eb17dba..1e20f4888 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -70,6 +70,11 @@ const Map = () => { setCheckedLayersData(checkedNodesData); }; + // SearchModal, GisContent 등에서 호출 + const onFeatureFocus = (data: any) => { + console.log("onFeatureFocus"); + console.log(data); + }; const onFeatureSelect = (data: any) => { const map = gisMaps.current; From 6d9327bf00e9b36bdd506b6c2ed665855daeabe9 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 22:20:22 +0900 Subject: [PATCH 34/42] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 1e20f4888..1922a8a3f 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -71,12 +71,12 @@ const Map = () => { }; // SearchModal, GisContent 등에서 호출 - const onFeatureFocus = (data: any) => { + const onFeatureFocus = () => { console.log("onFeatureFocus"); - console.log(data); }; const onFeatureSelect = (data: any) => { + const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); @@ -142,7 +142,7 @@ const Map = () => { } select.selectFeature(foundFeature, true); } else { - console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + console.warn("OpenLayers Feature를 찾을 수 없습니다:"); } const currentId = data?.fid || data?.id || data?.ftrIdn; const lastId = featureData?.fid || featureData?.id || featureData?.ftrIdn; @@ -157,7 +157,7 @@ const Map = () => { if (isFeatured) { onFeatureSelect(isFeatured); } - }, [isFeatured, onFeatureSelect]); + }, [isFeatured]); useEffect(() => { const bc = new BroadcastChannel("mapDetailPopUp"); @@ -167,7 +167,7 @@ const Map = () => { closePopup(); } }; - }, [onFeatureSelect]); + }, []); const getSelectedLayers = (isFeature: any) => { const map = gisMaps.current; From 21af492743e9091da8ecebd2ff5a5fe57a75579d Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 22:32:10 +0900 Subject: [PATCH 35/42] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20layout=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/layout.tsx | 12 +++++++++++- apps/fo/app/store/store.tsx | 2 -- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/fo/app/layout.tsx b/apps/fo/app/layout.tsx index af0725a7b..842ec5ed1 100644 --- a/apps/fo/app/layout.tsx +++ b/apps/fo/app/layout.tsx @@ -1,17 +1,27 @@ "use client" import "./globals.css"; import { StoreProvider } from "./store"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect } from "react"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + + useEffect(() => { + // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 + void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) + .catch((e) => { + console.error("Auto login failed:", e); + }); + }, []); return ( - {children} + {children} diff --git a/apps/fo/app/store/store.tsx b/apps/fo/app/store/store.tsx index 589bc4d78..f7f2222c6 100644 --- a/apps/fo/app/store/store.tsx +++ b/apps/fo/app/store/store.tsx @@ -1,4 +1,3 @@ -import { LoginApi } from '@/src/entities/auth/api/login-service'; import { createStore } from 'zustand/vanilla'; import { CommonReducer, commonState, type CommonStore, type CommonType } from './common'; import { dashBoardInitState, dashBoardReducer, type DashBoardStore, type DashBoardType } from './dashboard'; @@ -59,7 +58,6 @@ export const StoreZus = } ) => { return createStore()((set) => { - LoginApi.login({userId: "CLEANWATER", password: "gktneh12!@"}) return { ...initState, ...CommonReducer(set), From d86d0b0fcad7b9d0371fc30ee706e5d0ee66ad5b Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 09:21:40 +0900 Subject: [PATCH 36/42] =?UTF-8?q?=ED=94=BC=EC=B2=98=20=EC=A2=8C=ED=91=9C?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=97=86=EC=9D=84=EB=95=8C=20=EC=95=8C?= =?UTF-8?q?=EB=9E=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 2 -- .../business_components/src/ui/map/searchBar/procSearchBar.tsx | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 7c1d78164..d27dbe834 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -80,8 +80,6 @@ const Map = () => { const mayGet = (k: string) => (feature.get ? feature.get(k) : undefined); const safe = (v: any) => (v === undefined || v === null ? "-" : String(v)); - console.log(feature); - // 레이어 유형별 대표 필드 추출(없으면 "-") if (layerType === "tb_il_swage_prcs_fclt") { // 하수처리시설 diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx index 33388ce62..21239bd7b 100644 --- a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx @@ -249,6 +249,7 @@ export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { nearest: true, }); } else { + alert("위치정보를 찾을 수 없습니다.") console.warn("geometry를 찾을 수 없습니다:", data); } } else { From bde50388d1fa90939167543fb5beae71cfa4438b Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 09:34:05 +0900 Subject: [PATCH 37/42] =?UTF-8?q?=EC=8B=9C=EC=84=A4=EC=9A=A9=EB=9F=89=20?= =?UTF-8?q?=EC=B2=9C=EB=8B=A8=EC=9C=84=20=EC=BD=A4=EB=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index d27dbe834..f41439b92 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -73,6 +73,17 @@ const Map = () => { fetchData(); }, []); + function formatWithCommas(value: any) { + if (value === null || value === undefined) return value; + const num = Number(value); + return isNaN(num) ? value : num.toLocaleString(); + } + + function formatDateString(dateStr: string) { + if (typeof dateStr !== "string" || dateStr.length !== 8) return dateStr; + return `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6)}`; + } + // ===== 오버레이 콘텐츠 빌더 ===== const buildOverlayContent = (layerType: string, feature: FeatureData) => { const props: Record = {}; @@ -84,9 +95,9 @@ const Map = () => { if (layerType === "tb_il_swage_prcs_fclt") { // 하수처리시설 props["하수처리시설명"] = mayGet("swtrmt_fclt_nm"); - props["시설용량(m³/d)"] = mayGet("fclt_cpct"); + props["시설용량(m³/d)"] = formatWithCommas(mayGet("fclt_cpct")); props["주소"] = mayGet("rdnm_addr"); - props["가동개시일"] = mayGet("oprtng_strt_ymd"); + props["가동개시일"] = formatDateString(mayGet("oprtng_strt_ymd")); props["공법"] = mayGet("rprs_msrc_cn"); } else if (layerType === "tb_ms_swage_prcs_zone") { // 처리구역(존) @@ -116,14 +127,14 @@ const Map = () => {
${title}
${Object.entries(props) - .map( - ([k, v]) => ` + .map( + ([k, v]) => ` ` - ) - .join("")} + ) + .join("")}
${k} ${safe(v)}
`; return el; From ce9c536858ceb6cabab5f425e0d5039e4de4cbfc Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 09:45:58 +0900 Subject: [PATCH 38/42] =?UTF-8?q?=EA=B0=80=EB=8F=99=EA=B0=9C=EC=8B=9C?= =?UTF-8?q?=EC=9D=BC=20=EA=B3=B5=EB=B0=B1=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/pages/(map-layout)/map/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index f41439b92..ad3f70295 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -80,6 +80,7 @@ const Map = () => { } function formatDateString(dateStr: string) { + dateStr = dateStr.trim(); if (typeof dateStr !== "string" || dateStr.length !== 8) return dateStr; return `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6)}`; } From 3d480a40a7070dc948f92dec87d01d4331b5196a Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 09:47:20 +0900 Subject: [PATCH 39/42] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/store/rdi/index.tsx | 1 - apps/fo/src/entities/auth/api/login-service.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/apps/fo/app/store/rdi/index.tsx b/apps/fo/app/store/rdi/index.tsx index b1eccc2b7..fd9ca52b6 100644 --- a/apps/fo/app/store/rdi/index.tsx +++ b/apps/fo/app/store/rdi/index.tsx @@ -219,7 +219,6 @@ type SetStateFunction = (state: RdiType | ((state: RdiType) => RdiType)) => void export type RdiStore = RdiType & RdiActions; export const rdiReducer:(set:SetStateFunction)=>RdiActions=(set: SetStateFunction) => { - console.log("tableActionsExport"); return { searchRdiList: async (pageNo: number, pageSize: number, searchYear: string, partCd?: string, nameCd?: string, fcltCpct?: string, operationValue?: string, fcltyNm?: string) => { set((state: RdiType) => ({ diff --git a/apps/fo/src/entities/auth/api/login-service.ts b/apps/fo/src/entities/auth/api/login-service.ts index 3f2d958c6..760858b99 100644 --- a/apps/fo/src/entities/auth/api/login-service.ts +++ b/apps/fo/src/entities/auth/api/login-service.ts @@ -28,9 +28,6 @@ export class LoginApiService implements LoginService { const accessToken = response.headers.get(ACCESS_TOKEN); const tokenId = response.headers.get(AUTH_USER_ID); - console.log('accessToken ', accessToken) - console.log('tokenId ', tokenId) - if (!accessToken || !tokenId) { throw new Error("Missing required authentication tokens"); } From 1e0ef0c71325fa2d3fa94a967ccf00bc9f2e934c Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 10:23:47 +0900 Subject: [PATCH 40/42] =?UTF-8?q?=EC=98=A8=EB=B0=B0=EC=88=98=20=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=A7=80=EB=8F=84=20=ED=91=9C=EC=B6=9C=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 --- .../(popUp-layout)/mapFeaturePopup/page.tsx | 93 +------------------ apps/wr/middleware.ts | 1 - 2 files changed, 1 insertion(+), 93 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx index 94c1411f3..debea36c9 100644 --- a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx +++ b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx @@ -7,95 +7,6 @@ import { Title, UiTable } from "@common/business_components/ui"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; - -export const htdflData ={ - "bdstInfoSn": 54, - "ctpvCd": 11, - "sggCd": 11110, - "daddr": "청라동 100-80", - "bdstNm": "한국중부발전(주)", - "frstArchPrmsnDt": "2013-01-01", - "archArea": "-1.000000000", - "ctpvNm": null, - "sggNm": null, - "lat": 37.57096, - "lot": 126.98143, - "regDt": "2025-04-16", - "rgtr": null, - "mdfcnDt": "2025-04-16", - "mdfr": null, - "reuseBuilding": [ - { - "bdstRuseFcltSn": 4881, - "bdstInfoSn": 54, - "instlCmptnDt": "2025-06-20", - "instlCst": 300, - "operMngCd": null, - "anlOperCst": 400, - "instlCstSprtYn": null, - "instlCstSprtAmt": 0, - "grntchBdstCertYn": null, - "grntchBdstCertYr": null, - "grntchBdstCertNo": "", - "grntchBdstCertGrd": "", - "crgRdctYn": null, - "crgRdctBss": null, - "crgRdctAmt": 0, - "ruseFcltFlg": "HTB", - "bdstUsgCd": 2, - "lawTrgtYn": "N", - "regDt": "2025-04-16", - "rgtr": null, - "mdfcnDt": "2025-04-16", - "mdfr": null, - "htdflObj": { - "bdstRuseFcltSn": 4881, - "etgtType": "", - "fclCpct": 400, - "plwtYn": "Y", - "orgwtWtritSpt": "공장내", - "orgwtOcrnProcs": "1", - "wtritEntrcTp": 3, - "amtgnr": 1, - "prcsProcsYn": "Y", - "prcsFcltCpct": 14, - "regDt": "2025-04-16", - "rgtr": null, - "mdfcnDt": "2025-04-16", - "mdfr": null, - "fmYn": "Y", - "utztnCpct": 1, - "utztnUsg": "1", - "grssClfg": 0, - "combResdch": 0, - "trbt": 4, - "bod": 4, - "smll": "불쾌하지않을것", - "crmty": 4, - "tn": 10, - "tph": 10, - "ph": '1~3', - "clIonQnt": 1, - "cdtnv": 0, - "emsnExitTp": 5 - } - } - ], - "chgBuilding": [ - { - "bdstInfoSn": 54, - "archChgPrmsnDt": null, - "archChgArea": null, - "bdstChgNm": null, - "regDt": "2025-04-16", - "rgtr": null, - "mdfcnDt": "2025-04-16", - "mdfr": null - } - ] -} - - export default function MapFeaturePopup() { const [isFeatureCd, setIsFeatureCd] = useState(null); const { @@ -150,9 +61,7 @@ export default function MapFeaturePopup() { }); } else if (layerNm === "온배수시설") { htdfMapDetile(isFeatureCd).then((res) => { -//가짜데이터로 하였음!!!!!!!!! 기능 수정시 res를 넣어야함 - console.log(res) - setIsDetail(htdflData); + setIsDetail(res); }); } else if (layerNm === "잠재수요처") { ldrmntMapDetile(isFeatureCd).then((res) => { diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index e47a2a8f5..7758ec5ec 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -28,7 +28,6 @@ export function middleware(req: NextRequest) { } if (pathname === "/") { - console.log(" maim 화면이동 "); return NextResponse.redirect(new URL("/map/mainMap", req.url)); } From b5f0ee5cdcb9954774cb6e609b9bbb48d0c2b972 Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 11 Aug 2025 10:32:10 +0900 Subject: [PATCH 41/42] =?UTF-8?q?geometry=20=EC=A0=95=EB=B3=B4=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=EB=95=8C=20=EC=95=8C=EB=9E=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/searchBar/searchBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index 3f5fc1820..eee60ca41 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -277,6 +277,7 @@ export const SearchBar = ({ nearest: true, }); } else { + alert("위치정보를 찾을 수 없습니다."); console.warn("geometry를 찾을 수 없습니다:", data); } } else { From d32e5ea5927a13f70a31af4db45376c16e5c44bd Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 11 Aug 2025 13:19:04 +0900 Subject: [PATCH 42/42] =?UTF-8?q?fix:=20GEOSERVER=5FURL=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(=EC=B9=A8=EC=88=98=EC=9C=84=ED=97=98=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=91=9C=EC=B6=9C=20=EC=97=90=EB=9F=AC=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/.env.development | 3 ++- apps/sn/.env.local | 3 ++- apps/sn/.env.production | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sn/.env.development b/apps/sn/.env.development index f08d2b517..0fffc4f89 100644 --- a/apps/sn/.env.development +++ b/apps/sn/.env.development @@ -3,7 +3,8 @@ NEXT_PUBLIC_PORT = 11302 NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com NEXT_PUBLIC_CLAIM_NAME = Authorization diff --git a/apps/sn/.env.local b/apps/sn/.env.local index 232412579..35701bd81 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -3,7 +3,8 @@ NEXT_PUBLIC_PORT = 3002 NEXT_PUBLIC_PROXY_HOST = http://localhost:3002 NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com NEXT_PUBLIC_CLAIM_NAME = Authorization diff --git a/apps/sn/.env.production b/apps/sn/.env.production index 946499394..3fb05262e 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -7,7 +7,8 @@ NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082 -NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +# NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC