diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index e2f5cbf95..0045047e1 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -9,8 +9,13 @@ export const postData = async (url: string, data: object): Promise => { body: JSON.stringify(data), }; const res = await fetch(`${BASE_URL}/${url}`, options); - const resData = await res.json(); - return resData; + if (res.ok) { + const text = await res.text(); + if (text) { + return JSON.parse(text); // JSON 데이터가 있으면 파싱하여 반환 + } + return true; // 응답 본문이 비어 있으면 true 반환 + } }; export const postMutiPart = async (url: string, data: FormData): Promise => { diff --git a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx index 0b72cc768..5b5e18143 100644 --- a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx +++ b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx @@ -43,6 +43,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const accessToken = tokenManager.getToken(CLAIM_NAME); const tokenId = tokenManager.getToken(AUTH_USER_ID); + //TODO 삭제 + console.log(accessToken) + const response = await axios.get( `${ENDPOINT.USER_SERVICE.USERS}/info/me`, { diff --git a/apps/sn/app/components/lib/common/apiHelpers.ts b/apps/sn/app/components/lib/common/apiHelpers.ts index 508556571..6f205c875 100644 --- a/apps/sn/app/components/lib/common/apiHelpers.ts +++ b/apps/sn/app/components/lib/common/apiHelpers.ts @@ -10,5 +10,5 @@ export const searchData = async>(url: string, para }, {} as Record) ); const data = await getData(`${url}?${params.toString()}`); - return data && data.content + return data && {data : data.content, total : data.totalElements} } diff --git a/apps/sn/app/components/ui/LoadingBar/index.tsx b/apps/sn/app/components/ui/LoadingBar/index.tsx new file mode 100644 index 000000000..37991d1d2 --- /dev/null +++ b/apps/sn/app/components/ui/LoadingBar/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { LoadingWithAnimation } from '@common/components/ui/loading'; + +interface LoadingOverlayProps { + isLoading: boolean; +} + +const LoadingOverlay: React.FC = ({ isLoading }) => { + if (!isLoading) return null; + + return ( +
+
+ +
+
+ ); +}; + +export default LoadingOverlay; \ No newline at end of file diff --git a/apps/sn/app/components/ui/contentLayout.tsx b/apps/sn/app/components/ui/contentLayout.tsx index 738e26b16..75d61235f 100644 --- a/apps/sn/app/components/ui/contentLayout.tsx +++ b/apps/sn/app/components/ui/contentLayout.tsx @@ -1,14 +1,17 @@ import { ContentsDiv, MainDiv, Title } from '@common/business_components/ui' import React, { type ReactNode } from 'react' +import LoadingOverlay from './LoadingBar' type LayoutNode = { children : ReactNode title : string + isLoading?: boolean } -const ContentLayout = ({children, title} : LayoutNode) => { +const ContentLayout = ({children, title, isLoading} : LayoutNode) => { return ( <ContentsDiv> + <LoadingOverlay isLoading={isLoading ?? false}/> {children} </ContentsDiv> </MainDiv> diff --git a/apps/sn/app/oper/floodPre/page.tsx b/apps/sn/app/oper/floodPre/page.tsx index ec0608fd7..e8aacad26 100644 --- a/apps/sn/app/oper/floodPre/page.tsx +++ b/apps/sn/app/oper/floodPre/page.tsx @@ -1,6 +1,6 @@ 'use client' import { SearchDiv, SearchFormLeft, SearchFormRight, UiTable } from '@common/business_components/ui' -import { Button, DatePicker, Input, Label } from '@common/components/ui' +import { Button, DatePicker, Input, Label, LoadingWithAnimation } from '@common/components/ui' import React, { useEffect, useState } from 'react' import OperFileUploadForm from '../../components/ui/operFileUploadForm' import ListDiv from '@common/business_components/ui/list_div' @@ -9,11 +9,12 @@ import { searchData } from '@/components/lib/common/apiHelpers' import { tableHeadlist } from '..' import { handleDownload } from '@/components/lib/utils/fileDownload' import { formatDate } from '@/components/lib/utils/date' -import { deleteData, getData, postData } from '@/components/lib/api' +import { deleteData, postData } from '@/components/lib/api' +import LoadingOverlay from '@/components/ui/LoadingBar' const Page = () => { // 조회조건 - const [fromDate, setFromDate] = useState(new Date()) + const [fromDate, setFromDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date }) const [toDate, setToDate] = useState(new Date()) const [manageYear, setManageYear] = useState('') const [mngQu, setMngQu] = useState('') @@ -25,18 +26,26 @@ const Page = () => { const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('') //파일 일련번호 const [tableData, setTableData] = useState<any>([{}]) //테이블 데이터 const [curPage, setCurPage] = useState(1) //현재 페이지 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [isLoading, setIsLoading] = useState(true) //로딩 상태 const fetchData = async () => { - const data = await searchData('v1/sbmrgPrevnt', { - fromRegDt: formatDate(fromDate), - fromMngYr: manageYear, - toMngYr: mngQu, - toRegDt: formatDate(toDate), - atflNm: pjNm, - rgtrNm: usr, - page: curPage - 1, - size: '10' - }) - setTableData(data) + setIsLoading(true) + try { + const { data, total } = await searchData('v1/sbmrgPrevnt', { + fromRegDt: formatDate(fromDate), + fromMngYr: manageYear, + toMngYr: mngQu, + toRegDt: formatDate(toDate), + atflNm: pjNm, + rgtrNm: usr, + page: curPage - 1, + size: '10' + }) + setTableData(data) + setTotalData(total) + } + catch (error) { console.error('Error fetching data:', error) } + finally {setIsLoading(false); } } useEffect(() => { setCurPage(1) @@ -70,12 +79,12 @@ const Page = () => { mngQu: quater, atflCd: cd, atflId: id, - sbmrgPrevntBizNm:fileName + sbmrgPrevntBizNm: fileName }) - if (data) {alert('파일이 등록되었습니다.'); fetchData() } + if (data) { alert('파일이 등록되었습니다.'); fetchData() } } return ( - <ContentLayout title='도시침수 예방사업'> + <ContentLayout title='도시침수 예방사업' isLoading={isLoading}> <SearchDiv> <SearchFormLeft> <div className='flex items-center gap-4'> @@ -122,7 +131,7 @@ const Page = () => { </div> </SearchFormLeft> <SearchFormRight> - <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={async () => await fetchData() } >조회</Button> + <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={async () => await fetchData()} >조회</Button> </SearchFormRight> </SearchDiv> <ListDiv className="mt-4"> @@ -133,7 +142,7 @@ const Page = () => { <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile} >삭제</Button> </div> </div> - <UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={10} cellClick={(index) => { if (index != null) {const fileName = tableData[index]?.sbmrgPrevntBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable> + (<UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={totalData} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.sbmrgPrevntBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn); } }} onPageChange={async (page: number) => { setCurPage(page); await fetchData() }}></UiTable>) </ListDiv> <ListDiv className="mt-4"> <OperFileUploadForm title='도시침수 예방사업 사업추진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} /> diff --git a/apps/sn/app/oper/oldPp/page.tsx b/apps/sn/app/oper/oldPp/page.tsx index f7ca87ca5..cf49a1f18 100644 --- a/apps/sn/app/oper/oldPp/page.tsx +++ b/apps/sn/app/oper/oldPp/page.tsx @@ -5,13 +5,13 @@ import React, { useEffect, useState } from 'react' import OperFileUploadForm from '../../components/ui/operFileUploadForm' import ListDiv from '@common/business_components/ui/list_div' import ContentLayout from '@/components/ui/contentLayout' -import { tableHeadlist } from '..' +import { tableHeadlistOldPp } from '..' import { handleDownload } from '@/components/lib/utils/fileDownload' import { searchData } from '@/components/lib/common/apiHelpers' import { formatDate } from '@/components/lib/utils/date' import { deleteData, getData, postData } from '@/components/lib/api' const Page = () => { - const [fromDate, setFromDate] = useState<Date>(new Date()); + const [fromDate, setFromDate] = useState<Date>(()=>{const date = new Date(); date.setDate(date.getDate()-7); return date}); const [toDate, setToDate] = useState<Date>(new Date()); const [manageYear, setManageYear] = useState('') const [mngQu, setMngQu] = useState('') @@ -19,23 +19,25 @@ const Page = () => { const [pjNm, setpjNm] = useState('') const [fileNm, setFileNm] = useState('') const [atflId, setAtflId] = useState('') - const [atflCd, setAtflCd] = useState('') - const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState('') + const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') const [tableData, setTableData] = useState<any>([{}]) const [curPage, setCurPage] = useState(1) + const [totalData, setTotalData] = useState(0) //총 데이터 수 // 데이터 조회 const fetchData = async () => { - const data = await searchData('v1/dcrpPpl', { + const {data, total} = await searchData('v1/dcrpPpl', { fromRegDt: formatDate(fromDate), fromMngYr: manageYear, toMngYr: mngQu, toRegDt: formatDate(toDate), - atflNm: pjNm, + dcrpPplMtncBizNm: pjNm, rgtrNm: usr, page: curPage - 1, size: '10' }) setTableData(data) + setTotalData(total) + console.log(data, total) } useEffect(() => { setCurPage(1) @@ -47,19 +49,19 @@ const Page = () => { alert('파일을 선택해주세요') return false } - handleDownload(`/attach-service/api/v1/download/${atflId}}`, fileNm) + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) return true } // 파일 삭제 const isDeleteFile = async () => { - if (fileNm === '' || fileNm === undefined) { + if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { alert('삭제할 파일을 선택해주세요') return false } - const data = await getData(`/attach-service/api/v1/atfl/attachments/${atflId}`) + const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) if (data) { - const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`) - if (delData) { + const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`) + if (delData.status == 200) { alert('삭제되었습니다.') fetchData() } else { @@ -74,9 +76,10 @@ const Page = () => { mngYr: year, mngQu: quater, atflCd: cd, - atflId: id + atflId: id, + dcrpPplMtncBizNm: fileNm }) - if (data.ok) { setAtflId(data.atflId); setAtflCd(data.atflCd) } + if (data) {alert('파일이 등록되었습니다.');fetchData() } } return ( <ContentLayout title='노후 하수관로 정비사업'> @@ -119,7 +122,7 @@ const Page = () => { <Input className='w-[100px]' value={pjNm} - onChange={(e) => setpjNm(e.target.value)} + onChange={(e) => {setpjNm(e.target.value)}} /> </div> </div> @@ -137,7 +140,7 @@ const Page = () => { <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button> </div> </div> - <UiTable headName='' tableData={tableData} headlist={tableHeadlist} pageSize={10} total={20} children={undefined} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.fileNm ?? ""; setFileNm(fileName); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable> + <UiTable headName='' tableData={tableData} headlist={tableHeadlistOldPp} pageSize={10} total={totalData} children={undefined} cellClick={(index) => { if (index != null) { const fileName = tableData[index]?.dcrpPplMtncBizNm ?? ""; setFileNm(fileName); const id = tableData[index]?.atflId ?? ''; setAtflId(id); setDcrpPplMtncBizSn(tableData[index]?.dcrpPplMtncBizSn) } }} onPageChange={async (page:number)=>{setCurPage(page); await fetchData()}}></UiTable> </ListDiv> <ListDiv className="mt-4"> <OperFileUploadForm title='노후 하수관로 정비사업 주진현황 등록' onSubmit={async (file, year, quater, cd, id) => { await registFile(file, year, quater, cd, id) }} /> diff --git a/apps/sn/public/favicon.ico b/apps/sn/public/favicon.ico new file mode 100644 index 000000000..d304b6981 Binary files /dev/null and b/apps/sn/public/favicon.ico differ 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 4085e2907..1869b766a 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 @@ -14,6 +14,9 @@ import CircleStyle from 'ol/style/Circle'; import VectorLayer from 'ol/layer/Vector'; import { Point } from 'ol/geom'; import { boundingExtent } from 'ol/extent'; +import TileWMTS from 'ol/source/WMTS'; +import WMTS from 'ol/tilegrid/WMTS'; +import { get as getProjection } from 'ol/proj'; import olSourceImageWMS from 'ol/source/ImageWMS'; import { GeoJSON } from 'ol/format'; import GisOlEditLayer from './gis.edit'; @@ -32,20 +35,26 @@ GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; //맵 정보 -GisApp.EMAP_URL = '/emap/'; -GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; -GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; -GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg'; GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M'; GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; 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 = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키 +GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`; + +GisApp.EMAP_URL = '/emap/'; +GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; +GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; +GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg'; GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png'; GisApp.BASE_MAP_TYPES = []; GisApp.FROM_PROJ = 'EPSG:4326'; -GisApp.TO_PROJ = 'EPSG:3857'; +// GisApp.TO_PROJ = 'EPSG:3857'; +GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계 GisApp.TO_DATA_PROJ = 'EPSG:4326'; GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/'; GisApp.layerCode = {}; @@ -86,6 +95,7 @@ GisApp.core.prototype = { minZoom: 5, maxZoom: 22, }, + options: { /*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' }, 4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' }, @@ -109,11 +119,15 @@ GisApp.core.prototype = { GisApp.Module.core = this; GisApp.Module.layer = new GisOlLayer(this.defaults); GisApp.Module.edit = new GisOlEditLayer(this.defaults); - + // TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨 this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base'); this.baseLayers['base'] = this.getBaseMap('base'); this.baseLayers['satellite'] = this.getBaseMap('satellite'); this.baseLayers['3d'] = this.getBaseMap('3d'); + // this.baseLayers['3d'] = this.getNGIIBaseMap('3d'); + // this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base'); + // this.baseLayers['base'] = this.getNGIIBaseMap('base'); + // this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite'); }, layerType: { point: 'POINT', @@ -126,6 +140,46 @@ GisApp.core.prototype = { this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY)); this.setZoom(15); }, + // NGII Base Map + getNGIIBaseMap: function (_id) { + const layers = { + base: 'korean_map', // 일반지도 + satellite: 'satellite_map', // 위성지도 + night: 'night_map', // 야간지도 + }; + // 선택된 레이어 + const layerName = layers[_id] || layers.base; + // EPSG:5179 좌표계 설정 + const projection = getProjection('EPSG:5179'); + const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위 + const resolutions = [ + 2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255, + ]; + const tileGrid = new WMTS({ + extent: extent, + resolutions: resolutions, + matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs + }); + + // WMTS 소스 생성 + const wmtsSource = new TileWMTS({ + url: GisApp.NGII_WMTS_URL, + layer: layerName, + matrixSet: 'EPSG:5179', + format: 'image/png', + projection: projection, + tileGrid: tileGrid, + style: 'default', + crossOrigin: 'anonymous', + }); + + // TileLayer 생성 + return new TileLayer({ + source: wmtsSource, + visible: true, + zIndex: 0, + }); + }, getBaseMap: function (_id) { let url = GisApp.VWORLD_URL; if (_id === 'satellite') { @@ -182,7 +236,7 @@ GisApp.core.prototype = { setCenter: function (x, y, transYn) { if (!x || !y) return; let view = this.map.getView(); - + if (!transYn) { // 값이 없으면 let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ); @@ -553,7 +607,7 @@ GisApp.core.prototype = { */ convertLayerInfosToLayerCode: function (array, key, parentName) { let initialValue = {}; - + for (let i = 0; i < array.length; i++) { let item = {}; Object.assign(item, array[i]);