From e79c50baead73f9340e9dd8cfa6a49cc641cf2d4 Mon Sep 17 00:00:00 2001 From: "jeonghyo.K" Date: Wed, 11 Feb 2026 13:46:36 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=84=B1=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/commonApi.js | 32 ++ src/api/satelliteApi.js | 481 +++++++++++++++++ src/components/layout/Sidebar.jsx | 15 +- src/map/MapContainer.jsx | 59 ++- src/map/layers/satelliteLayer.js | 131 +++++ src/pages/SatellitePage.jsx | 57 +++ .../components/SatelliteImageManage.jsx | 369 +++++++++++++ src/satellite/components/SatelliteManage.jsx | 204 ++++++++ .../SatelliteManageRegisterPopup.jsx | 231 +++++++++ .../components/SatelliteProviderManage.jsx | 182 +++++++ .../SatelliteProviderRegisterPopup.jsx | 241 +++++++++ .../components/SatelliteRegisterPopup.jsx | 483 ++++++++++++++++++ src/satellite/components/Slider.jsx | 34 ++ src/satellite/hooks/useDraggable.js | 36 ++ src/stores/satelliteStore.js | 13 + src/weather/components/TidalInfo.jsx | 2 +- 16 files changed, 2534 insertions(+), 36 deletions(-) create mode 100644 src/api/commonApi.js create mode 100644 src/api/satelliteApi.js create mode 100644 src/map/layers/satelliteLayer.js create mode 100644 src/pages/SatellitePage.jsx create mode 100644 src/satellite/components/SatelliteImageManage.jsx create mode 100644 src/satellite/components/SatelliteManage.jsx create mode 100644 src/satellite/components/SatelliteManageRegisterPopup.jsx create mode 100644 src/satellite/components/SatelliteProviderManage.jsx create mode 100644 src/satellite/components/SatelliteProviderRegisterPopup.jsx create mode 100644 src/satellite/components/SatelliteRegisterPopup.jsx create mode 100644 src/satellite/components/Slider.jsx create mode 100644 src/satellite/hooks/useDraggable.js create mode 100644 src/stores/satelliteStore.js diff --git a/src/api/commonApi.js b/src/api/commonApi.js new file mode 100644 index 00000000..1bdafaf8 --- /dev/null +++ b/src/api/commonApi.js @@ -0,0 +1,32 @@ +/** + * 공통코드 API + */ + +const COMMON_CODE_LIST_ENDPOINT = '/api/cmn/code/list/search'; + +/** + * 공통코드 목록 조회 + * + * @param {string} commonCodeTypeNumber - 공통코드 유형 번호 + * @returns {Promise>} + */ +export async function fetchCommonCodeList(commonCodeTypeNumber) { + try { + const response = await fetch(COMMON_CODE_LIST_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ commonCodeTypeNumber }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.codeList || []; + } catch (error) { + console.error('[fetchCommonCodeList] Error:', error); + throw error; + } +} diff --git a/src/api/satelliteApi.js b/src/api/satelliteApi.js new file mode 100644 index 00000000..abe82b4d --- /dev/null +++ b/src/api/satelliteApi.js @@ -0,0 +1,481 @@ +/** + * 위성 API + */ + +const SATELLITE_VIDEO_SEARCH_ENDPOINT = '/api/gis/satlit/search'; +const SATELLITE_CSV_ENDPOINT = '/api/gis/satlit/excelToJson'; +const SATELLITE_DETAIL_ENDPOINT = '/api/gis/satlit/id/search'; +const SATELLITE_UPDATE_ENDPOINT = '/api/gis/satlit/update'; +const SATELLITE_COMPANY_LIST_ENDPOINT = '/api/gis/satlit/sat-bz/all/search'; +const SATELLITE_MANAGE_LIST_ENDPOINT = '/api/gis/satlit/sat-mng/bz/search'; +const SATELLITE_SAVE_ENDPOINT = '/api/gis/satlit/save'; +const SATELLITE_COMPANY_SEARCH_ENDPOINT = '/api/gis/satlit/sat-bz/search'; +const SATELLITE_COMPANY_SAVE_ENDPOINT = '/api/gis/satlit/sat-bz/save'; +const SATELLITE_COMPANY_DETAIL_ENDPOINT = '/api/gis/satlit/sat-bz/id/search'; +const SATELLITE_COMPANY_UPDATE_ENDPOINT = '/api/gis/satlit/sat-bz/update'; +const SATELLITE_MANAGE_SEARCH_ENDPOINT = '/api/gis/satlit/sat-mng/search'; +const SATELLITE_MANAGE_SAVE_ENDPOINT = '/api/gis/satlit/sat-mng/save'; +const SATELLITE_MANAGE_DETAIL_ENDPOINT = '/api/gis/satlit/sat-mng/id/search'; +const SATELLITE_MANAGE_UPDATE_ENDPOINT = '/api/gis/satlit/sat-mng/update'; + +/** + * 위성영상 목록 조회 + * + * @param {Object} params + * @param {number} params.page - 페이지 번호 + * @param {string} [params.startDate] - 촬영 시작일 + * @param {string} [params.endDate] - 촬영 종료일 + * @param {string} [params.satelliteVideoName] - 위성영상명 + * @param {string} [params.satelliteVideoTransmissionCycle] - 전송주기 + * @param {string} [params.satelliteVideoKind] - 영상 종류 + * @param {string} [params.satelliteVideoOrbit] - 위성 궤도 + * @param {string} [params.satelliteVideoOrigin] - 영상 출처 + * @returns {Promise<{ list: Array, totalPage: number }>} + */ +export async function fetchSatelliteVideoList({ + page, + startDate, + endDate, + satelliteVideoName, + satelliteVideoTransmissionCycle, + satelliteVideoKind, + satelliteVideoOrbit, + satelliteVideoOrigin, + }) { + try { + const response = await fetch(SATELLITE_VIDEO_SEARCH_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + page, + startDate, + endDate, + satelliteVideoName, + satelliteVideoTransmissionCycle, + satelliteVideoKind, + satelliteVideoOrbit, + satelliteVideoOrigin, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + return { + list: result?.satelliteVideoInfoList || [], + totalPage: result?.totalPage || 0, + }; + } catch (error) { + console.error('[fetchSatelliteVideoList] Error:', error); + throw error; + } +} + +/** + * 위성영상 CSV → JSON 변환 (선박 좌표 추출) + * + * @param {string} csvFileName - CSV 파일명 + * @returns {Promise>} + */ +export async function fetchSatelliteCsvFeatures(csvFileName) { + try { + const response = await fetch(SATELLITE_CSV_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ csvFileName }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + const data = result?.jsonData; + if (!data) return []; + + const parsed = typeof data === 'string' ? JSON.parse(data) : data; + return parsed.map(({ lon, lat }) => ({ + coordinates: [parseFloat(lon), parseFloat(lat)], + })); + } catch (error) { + console.error('[fetchSatelliteCsvFeatures] Error:', error); + throw error; + } +} + +/** + * 위성영상 상세조회 + * + * @param {number} satelliteId - 위성 ID + * @returns {Promise} SatelliteVideoInfoOneDto + */ +export async function fetchSatelliteVideoDetail(satelliteId) { + try { + const response = await fetch(SATELLITE_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ satelliteId }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.satelliteVideoInfoById || null; + } catch (error) { + console.error('[fetchSatelliteVideoDetail] Error:', error); + throw error; + } +} + +/** + * 위성영상 수정 + * + * @param {Object} params + * @param {number} params.satelliteId + * @param {number} params.satelliteManageId + * @param {string} [params.photographDate] + * @param {string} [params.satelliteVideoName] + * @param {string} [params.satelliteVideoTransmissionCycle] + * @param {string} [params.satelliteVideoKind] + * @param {string} [params.satelliteVideoOrbit] + * @param {string} [params.satelliteVideoOrigin] + * @param {string} [params.photographPurpose] + * @param {string} [params.photographMode] + * @param {string} [params.purchaseCode] + * @param {number} [params.purchasePrice] + */ +export async function updateSatelliteVideo(params) { + try { + const response = await fetch(SATELLITE_UPDATE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[updateSatelliteVideo] Error:', error); + throw error; + } +} + +/** + * 사업자 목록 조회 + * + * @returns {Promise>} + */ +export async function fetchSatelliteCompanyList() { + try { + const response = await fetch(SATELLITE_COMPANY_LIST_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({}), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.satelliteCompanyNameList || []; + } catch (error) { + console.error('[fetchSatelliteCompanyList] Error:', error); + throw error; + } +} + +/** + * 사업자별 위성명 목록 조회 + * + * @param {number} companyNo - 사업자 번호 + * @returns {Promise>} + */ +export async function fetchSatelliteManageList(companyNo) { + try { + const response = await fetch(SATELLITE_MANAGE_LIST_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ companyNo }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.satelliteManageInfoList || []; + } catch (error) { + console.error('[fetchSatelliteManageList] Error:', error); + throw error; + } +} + +/** + * 위성영상 등록 (multipart/form-data) + * + * @param {FormData} formData - 파일(tifFile, csvFile, cloudMaskFile) + 폼 필드 + */ +export async function saveSatelliteVideo(formData) { + try { + const response = await fetch(SATELLITE_SAVE_ENDPOINT, { + method: 'POST', + credentials: 'include', + body: formData, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[saveSatelliteVideo] Error:', error); + throw error; + } +} + +/** + * 위성 사업자 목록 검색 + * + * @param {Object} params + * @param {string} [params.companyTypeCode] - 사업자 분류 코드 + * @param {string} [params.companyName] - 사업자명 + * @returns {Promise<{ list: Array, totalPage: number }>} + */ +export async function searchSatelliteCompany({ companyTypeCode, companyName, page, limit }) { + try { + const response = await fetch(SATELLITE_COMPANY_SEARCH_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ companyTypeCode, companyName, page, limit }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return { + list: result?.satelliteCompanySearchList || [], + totalPage: result?.totalPage || 0, + }; + } catch (error) { + console.error('[searchSatelliteCompany] Error:', error); + throw error; + } +} + +/** + * 위성 사업자 등록 + * + * @param {Object} params + * @param {string} params.companyTypeCode - 사업자 분류 코드 + * @param {string} params.companyName - 사업자명 + * @param {string} params.nationalCode - 국가코드 + * @param {string} [params.location] - 소재지 + * @param {string} [params.companyDetail] - 상세내역 + */ +export async function saveSatelliteCompany(params) { + try { + const response = await fetch(SATELLITE_COMPANY_SAVE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[saveSatelliteCompany] Error:', error); + throw error; + } +} + +/** + * 위성 사업자 상세조회 + * + * @param {number} companyNo - 사업자 번호 + * @returns {Promise} SatelliteCompanySearchDto + */ +export async function fetchSatelliteCompanyDetail(companyNo) { + try { + const response = await fetch(SATELLITE_COMPANY_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ companyNo }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.satelliteCompany || null; + } catch (error) { + console.error('[fetchSatelliteCompanyDetail] Error:', error); + throw error; + } +} + +/** + * 위성 사업자 수정 + * + * @param {Object} params + * @param {number} params.companyNo - 사업자 번호 + * @param {string} params.companyTypeCode - 사업자 분류 코드 + * @param {string} params.companyName - 사업자명 + * @param {string} params.nationalCode - 국가코드 + * @param {string} [params.location] - 소재지 + * @param {string} [params.companyDetail] - 상세내역 + */ +export async function updateSatelliteCompany(params) { + try { + const response = await fetch(SATELLITE_COMPANY_UPDATE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[updateSatelliteCompany] Error:', error); + throw error; + } +} + +/** + * 위성관리 목록 검색 + * + * @param {Object} params + * @param {number} [params.companyNo] - 사업자 번호 + * @param {string} [params.satelliteName] - 위성명 + * @param {string} [params.sensorType] - 센서 타입 + * @returns {Promise<{ list: Array, totalPage: number }>} + */ +export async function searchSatelliteManage({ companyNo, satelliteName, sensorType, page, limit }) { + try { + const response = await fetch(SATELLITE_MANAGE_SEARCH_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ companyNo, satelliteName, sensorType, page, limit }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return { + list: result?.satelliteManageInfoSearchList || [], + totalPage: result?.totalPage || 0, + }; + } catch (error) { + console.error('[searchSatelliteManage] Error:', error); + throw error; + } +} + +/** + * 위성 관리 등록 + * + * @param {Object} params + * @param {number} params.companyNo - 사업자 번호 + * @param {string} params.satelliteName - 위성명 + * @param {string} [params.sensorType] - 센서 타입 + * @param {string} [params.photoResolution] - 촬영 해상도 + * @param {string} [params.frequency] - 주파수 + * @param {string} [params.photoDetail] - 상세내역 + */ +export async function saveSatelliteManage(params) { + try { + const response = await fetch(SATELLITE_MANAGE_SAVE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[saveSatelliteManage] Error:', error); + throw error; + } +} + +/** + * 위성 관리 상세조회 + * + * @param {number} satelliteManageId - 위성 관리 ID + * @returns {Promise} SatelliteManageInfoDto + */ +export async function fetchSatelliteManageDetail(satelliteManageId) { + try { + const response = await fetch(SATELLITE_MANAGE_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ satelliteManageId }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.satelliteManageInfo || null; + } catch (error) { + console.error('[fetchSatelliteManageDetail] Error:', error); + throw error; + } +} + +/** + * 위성 관리 수정 + * + * @param {Object} params + * @param {number} params.satelliteManageId - 위성 관리 ID + * @param {number} params.companyNo - 사업자 번호 + * @param {string} params.satelliteName - 위성명 + * @param {string} [params.sensorType] - 센서 타입 + * @param {string} [params.photoResolution] - 촬영 해상도 + * @param {string} [params.frequency] - 주파수 + * @param {string} [params.photoDetail] - 상세내역 + */ +export async function updateSatelliteManage(params) { + try { + const response = await fetch(SATELLITE_MANAGE_UPDATE_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('[updateSatelliteManage] Error:', error); + throw error; + } +} diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index 3b4c4414..97e6b157 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -19,6 +19,7 @@ import DisplayComponent from '../../component/wrap/side/DisplayComponent'; // 구현된 페이지 import ReplayPage from '../../pages/ReplayPage'; import WeatherPage from '../../pages/WeatherPage'; +import SatellitePage from '../../pages/SatellitePage'; /** * 사이드바 컴포넌트 @@ -64,7 +65,7 @@ export default function Sidebar() { const renderPanel = () => { const panelMap = { gnb1: DisplayComponent ? : null, - gnb2: Panel2Component ? : null, + gnb2: , gnb3: , gnb4: Panel4Component ? : null, gnb5: Panel5Component ? : null, @@ -78,11 +79,11 @@ export default function Sidebar() { }; return ( -
- -
- {renderPanel()} -
-
+
+ +
+ {renderPanel()} +
+
); } diff --git a/src/map/MapContainer.jsx b/src/map/MapContainer.jsx index 5aefd676..044abc95 100644 --- a/src/map/MapContainer.jsx +++ b/src/map/MapContainer.jsx @@ -7,6 +7,7 @@ import { defaults as defaultInteractions, DragBox } from 'ol/interaction'; import { platformModifierKeyOnly } from 'ol/events/condition'; import { createBaseLayers } from './layers/baseLayer'; +import { satelliteLayer, csvDeckLayer } from './layers/satelliteLayer'; import { useMapStore, BASE_MAP_TYPES } from '../stores/mapStore'; import useShipStore from '../stores/shipStore'; import useShipData from '../hooks/useShipData'; @@ -347,6 +348,8 @@ export default function MapContainer() { worldMap, encMap, darkMap, + satelliteLayer, + csvDeckLayer, eastAsiaMap, korMap, ], @@ -376,7 +379,7 @@ export default function MapContainer() { const state = useShipStore.getState(); const { features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, - nationalVisibility, darkSignalVisible } = state; + nationalVisibility, darkSignalVisible } = state; // 국적 코드 매핑 (shipStore.js와 동일) const mapNational = (code) => { @@ -439,32 +442,32 @@ export default function MapContainer() { }, []); return ( - <> -
- - {showLegend && (replayCompleted ? : )} - {hoverInfo && ( - - )} - {detailModals.map((modal) => ( - - ))} - - - - {replayCompleted && ( - { - useReplayStore.getState().reset(); - useAnimationStore.getState().reset(); - unregisterReplayLayers(); - showLiveShips(); // 라이브 선박 다시 표시 - shipBatchRenderer.immediateRender(); - }} - /> - )} - + <> +
+ + {showLegend && (replayCompleted ? : )} + {hoverInfo && ( + + )} + {detailModals.map((modal) => ( + + ))} + + + + {replayCompleted && ( + { + useReplayStore.getState().reset(); + useAnimationStore.getState().reset(); + unregisterReplayLayers(); + showLiveShips(); // 라이브 선박 다시 표시 + shipBatchRenderer.immediateRender(); + }} + /> + )} + ); } diff --git a/src/map/layers/satelliteLayer.js b/src/map/layers/satelliteLayer.js new file mode 100644 index 00000000..0b31678f --- /dev/null +++ b/src/map/layers/satelliteLayer.js @@ -0,0 +1,131 @@ +/** + * 위성영상 레이어 + * - TIF 영상: OL TileLayer + GeoServer WMS + * - CSV 선박 점: Deck.gl ScatterplotLayer + 별도 Deck 인스턴스 → OL WebGLTileLayer 래핑 + * + * 참조: mda-react-front/src/common/targetLayer.ts (satelliteLayer, deckSatellite 등) + * 참조: mda-react-front/src/util/satellite.ts (createSatellitePictureLayer, removeSatelliteLayer) + */ +import TileLayer from 'ol/layer/Tile'; +import TileWMS from 'ol/source/TileWMS'; +import WebGLTileLayer from 'ol/layer/WebGLTile'; +import { transformExtent, toLonLat } from 'ol/proj'; +import { Deck } from '@deck.gl/core'; +import { ScatterplotLayer } from '@deck.gl/layers'; + +// ===================== +// TIF 영상 레이어 (GeoServer WMS) +// ===================== +export const satelliteLayer = new TileLayer({ + source: new TileWMS({ + url: '/geo/geoserver/mda/wms', + params: { tiled: true, LAYERS: '' }, + }), + className: 'satellite-map', + zIndex: 10, + visible: false, +}); + +// ===================== +// CSV 선박 점 레이어 (Deck.gl ScatterplotLayer) +// ===================== +export const csvScatterLayer = new ScatterplotLayer({ + id: 'satellite-csv-layer', + data: [], + getPosition: (d) => d.coordinates, + getFillColor: [232, 232, 21], + getRadius: 3, + radiusUnits: 'pixels', + pickable: false, +}); + +export const csvDeck = new Deck({ + initialViewState: { + longitude: 127.1388684, + latitude: 37.4449168, + zoom: 6, + transitionDuration: 0, + }, + controller: false, + layers: [csvScatterLayer], +}); + +export const csvDeckLayer = new WebGLTileLayer({ + source: undefined, + zIndex: 200, + visible: false, + render: (frameState) => { + const { center, zoom } = frameState.viewState; + csvDeck.setProps({ + viewState: { + longitude: toLonLat(center)[0], + latitude: toLonLat(center)[1], + zoom: zoom - 1, + }, + }); + csvDeck.redraw(); + return csvDeck.canvas; + }, +}); + +// ===================== +// 표출/제거 함수 +// ===================== + +/** + * 위성영상 TIF를 지도에 표출 + * @param {import('ol/Map').default} map - OL 맵 인스턴스 + * @param {string} tifGeoName - GeoServer 레이어명 + * @param {[number,number,number,number]} extent - [minX, minY, maxX, maxY] EPSG:4326 + * @param {number} opacity - 0~1 + * @param {number} brightness - 0~200 (%) + */ +export function showSatelliteImage(map, tifGeoName, extent, opacity, brightness) { + const extent3857 = transformExtent(extent, 'EPSG:4326', 'EPSG:3857'); + + const source = new TileWMS({ + url: '/geo/geoserver/mda/wms', + params: { tiled: true, LAYERS: tifGeoName }, + hidpi: false, + transition: 0, + }); + + satelliteLayer.setExtent(extent3857); + satelliteLayer.setSource(source); + satelliteLayer.setOpacity(Number(opacity)); + satelliteLayer.setVisible(true); + + // CSS brightness 적용 + const el = document.querySelector('.satellite-map'); + if (el) { + el.style.filter = `brightness(${brightness}%)`; + } + + // 해당 영상 범위로 지도 이동 + map.getView().fit(extent3857); + + // 타일 로딩 강제 트리거 + source.refresh(); +} + +/** + * CSV 선박 좌표를 Deck.gl ScatterplotLayer로 표시 + * @param {Array<{ coordinates: [number, number] }>} features + */ +export function showCsvFeatures(features) { + const layer = csvScatterLayer.clone({ data: features }); + csvDeck.setProps({ layers: [layer] }); + csvDeckLayer.setVisible(true); +} + +/** + * 위성영상 + CSV 레이어 제거 + */ +export function hideSatelliteImage() { + satelliteLayer.setVisible(false); + satelliteLayer.setSource(null); + + const emptyLayer = csvScatterLayer.clone({ data: [] }); + csvDeck.setProps({ layers: [emptyLayer] }); + csvDeckLayer.setVisible(false); +} diff --git a/src/pages/SatellitePage.jsx b/src/pages/SatellitePage.jsx new file mode 100644 index 00000000..478ae26f --- /dev/null +++ b/src/pages/SatellitePage.jsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import SatelliteImageManage from '@/satellite/components/SatelliteImageManage'; +import SatelliteProviderManage from '@/satellite/components/SatelliteProviderManage'; +import SatelliteManage from '@/satellite/components/SatelliteManage'; + +const tabs = [ + { id: 'satellite01', label: '위성영상 관리' }, + { id: 'satellite02', label: '위성사업자 관리' }, + { id: 'satellite03', label: '위성 관리' }, +]; + +const tabComponents = { + satellite01: SatelliteImageManage, + satellite02: SatelliteProviderManage, + satellite03: SatelliteManage, +}; + +export default function SatellitePage({ isOpen, onToggle }) { + const [activeTab, setActiveTab] = useState('satellite01'); + + const ActiveComponent = tabComponents[activeTab]; + + return ( + + ); +} diff --git a/src/satellite/components/SatelliteImageManage.jsx b/src/satellite/components/SatelliteImageManage.jsx new file mode 100644 index 00000000..d2ce2ffa --- /dev/null +++ b/src/satellite/components/SatelliteImageManage.jsx @@ -0,0 +1,369 @@ +import { useState, useCallback } from 'react'; +import { fetchSatelliteVideoList, fetchSatelliteCsvFeatures } from '@/api/satelliteApi'; +import { useSatelliteStore } from '@/stores/satelliteStore'; +import { useMapStore } from '@/stores/mapStore'; +import { satelliteLayer, showSatelliteImage, showCsvFeatures, hideSatelliteImage } from '@/map/layers/satelliteLayer'; +import Slider from './Slider'; +import SatelliteRegisterPopup from './SatelliteRegisterPopup'; + +const LIMIT = 10; + +export default function SatelliteImageManage() { + const [isAccordionOpen, setIsAccordionOpen] = useState(false); + + // 폼 필터 state + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [satelliteVideoName, setSatelliteVideoName] = useState(''); + const [satelliteVideoKind, setSatelliteVideoKind] = useState(''); + const [satelliteVideoOrigin, setSatelliteVideoOrigin] = useState(''); + const [satelliteVideoOrbit, setSatelliteVideoOrbit] = useState(''); + const [satelliteVideoTransmissionCycle, setSatelliteVideoTransmissionCycle] = useState(''); + + // 결과 state + const [list, setList] = useState([]); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // 지도 표출 state + const [activeImageId, setActiveImageId] = useState(null); + + // 상세 팝업 state + const [detailPopupId, setDetailPopupId] = useState(null); + + // 등록 팝업 state + const [isRegisterOpen, setIsRegisterOpen] = useState(false); + + // 위성 투명도/밝기 스토어 + const opacity = useSatelliteStore((s) => s.opacity); + const brightness = useSatelliteStore((s) => s.brightness); + const setOpacity = useSatelliteStore((s) => s.setOpacity); + const setBrightness = useSatelliteStore((s) => s.setBrightness); + + const toggleAccordion = () => setIsAccordionOpen((prev) => !prev); + + const search = useCallback(async (targetPage) => { + setIsLoading(true); + setError(null); + + try { + const result = await fetchSatelliteVideoList({ + page: targetPage, + startDate, + endDate, + satelliteVideoName, + satelliteVideoTransmissionCycle, + satelliteVideoKind, + satelliteVideoOrbit, + satelliteVideoOrigin, + }); + setList(result.list); + setTotalPage(result.totalPage); + setPage(targetPage); + } catch (err) { + setError('위성영상 조회 중 오류가 발생했습니다.'); + setList([]); + setTotalPage(0); + } finally { + setIsLoading(false); + } + }, [startDate, endDate, satelliteVideoName, satelliteVideoTransmissionCycle, satelliteVideoKind, satelliteVideoOrbit, satelliteVideoOrigin]); + + const handleSearch = () => { + search(1); + }; + + const handlePageChange = (newPage) => { + search(newPage); + }; + + // 투명도 변경 (Slider → 스토어 + 레이어 실시간 반영) + const handleOpacityChange = (v) => { + const val = v / 100; + setOpacity(val); + satelliteLayer.setOpacity(val); + }; + + // 밝기 변경 (Slider → 스토어 + CSS filter 실시간 반영) + const handleBrightnessChange = (v) => { + setBrightness(v); + const el = document.querySelector('.satellite-map'); + if (el) { + el.style.filter = `brightness(${v}%)`; + } + }; + + // btnMap: 위성영상 지도 표출 토글 + const handleShowOnMap = async (item) => { + const map = useMapStore.getState().map; + if (!map) return; + + // 같은 영상이면 제거 (토글) + if (activeImageId === item.satelliteManageId) { + hideSatelliteImage(); + setActiveImageId(null); + return; + } + + const { opacity: curOpacity, brightness: curBrightness } = useSatelliteStore.getState(); + const extent = [item.tifMinX, item.tifMinY, item.tifMaxX, item.tifMaxY]; + showSatelliteImage(map, item.tifGeoName, extent, curOpacity, curBrightness); + + // CSV 선박 점 표시 + if (item.csvFileName) { + try { + const features = await fetchSatelliteCsvFeatures(item.csvFileName); + showCsvFeatures(features); + } catch { + // CSV 없으면 무시 + } + } + + setActiveImageId(item.satelliteManageId); + }; + + return ( +
+
+
위성영상 관리
+ +
+
    +
  • + +
  • + + {/* 아코디언 — 상세검색 */} +
    +
  • + + +
  • +
  • + + +
  • +
    + +
  • + +
  • +
  • + +
  • +
  • + <> +
    + 투명도 +
    + +
    +
    +
    + 밝기 +
    + +
    +
    + + +
  • +
+
+
+ +
+
+ {/* 스크롤영역 */} +
+ {isLoading &&
조회 중...
} + + {error &&
{error}
} + + {!isLoading && !error && list.length === 0 && ( +
검색 결과가 없습니다.
+ )} + + {!isLoading && list.length > 0 && ( +
+ {list.map((item) => ( +
    +
  • +
    + {item.satelliteVideoName} + {item.photographDate} +
    +
  • +
  • +
      +
    • + 위성명 + {item.satelliteName} +
    • +
    • + 위성영상파일 + {item.tifFileName} +
    • +
    • + 영상 종류 + {item.satelliteVideoKind} +
    • +
    • + 영상 출처 + {item.satelliteVideoOrigin} +
    • +
    +
    + + + +
    +
  • +
+ ))} +
+ )} + + {!isLoading && totalPage > 1 && ( +
+ + {page > 3 && } + {page > 4 && ...} + {Array.from({ length: 5 }, (_, i) => page - 2 + i) + .filter((p) => p >= 1 && p <= totalPage) + .map((p) => ( + + ))} + {page < totalPage - 3 && ...} + {page < totalPage - 2 && } + +
+ )} +
+ {/* 하단버튼 영역 */} +
+ {/**/} + +
+
+
+ + {detailPopupId && ( + setDetailPopupId(null)} + onSaved={() => search(page)} + /> + )} + + {isRegisterOpen && ( + setIsRegisterOpen(false)} + onSaved={() => search(page)} + /> + )} +
+ ); +} diff --git a/src/satellite/components/SatelliteManage.jsx b/src/satellite/components/SatelliteManage.jsx new file mode 100644 index 00000000..c86b2989 --- /dev/null +++ b/src/satellite/components/SatelliteManage.jsx @@ -0,0 +1,204 @@ +import { useState, useEffect, useCallback } from 'react'; +import { fetchCommonCodeList } from '@/api/commonApi'; +import { fetchSatelliteCompanyList, searchSatelliteManage } from '@/api/satelliteApi'; +import SatelliteManageRegisterPopup from './SatelliteManageRegisterPopup'; + +export default function SatelliteManage() { + // 드롭다운 옵션 + const [companyOptions, setCompanyOptions] = useState([]); + const [sensorTypeOptions, setSensorTypeOptions] = useState([]); + + // 검색 폼 + const [companyNo, setCompanyNo] = useState(''); + const [satelliteName, setSatelliteName] = useState(''); + const [sensorType, setSensorType] = useState(''); + + // 결과 + const [list, setList] = useState([]); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // 등록 팝업 state + const [isRegisterOpen, setIsRegisterOpen] = useState(false); + + // 상세/수정 팝업 state + const [detailManageId, setDetailManageId] = useState(null); + + // 마운트 시 사업자명 목록 + 센서타입 공통코드 병렬 로드 + useEffect(() => { + fetchSatelliteCompanyList() + .then(setCompanyOptions) + .catch(() => setCompanyOptions([])); + + fetchCommonCodeList('000092') + .then(setSensorTypeOptions) + .catch(() => setSensorTypeOptions([])); + }, []); + + const search = useCallback(async (targetPage) => { + setIsLoading(true); + setError(null); + + try { + const limit = 10; + const page = targetPage != null ? targetPage : 1; + const result = await searchSatelliteManage({ + companyNo: companyNo ? Number(companyNo) : undefined, + satelliteName, + sensorType, + page, + limit, + }); + setList(result.list); + setTotalPage(result.totalPage); + setPage(targetPage); + } catch { + setError('위성 관리 조회 중 오류가 발생했습니다.'); + setList([]); + setTotalPage(0); + } finally { + setIsLoading(false); + } + }, [companyNo, satelliteName, sensorType]); + + const handleSearch = () => { + search(1); + }; + + const handlePageChange = (newPage) => { + search(newPage); + }; + + return ( +
+
+
위성 관리
+
+
    +
  • + + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ +
+
+ {/* 스크롤영역 */} +
+ {isLoading &&
조회 중...
} + + {error &&
{error}
} + + {!isLoading && !error && list.length === 0 && ( +
검색 결과가 없습니다.
+ )} + + {!isLoading && list.length > 0 && ( +
+ {list.map((item) => ( +
    setDetailManageId(item.satelliteManageId)}> +
  • + 사업자명 + {item.companyName} +
  • +
  • + 위성명 + {item.satelliteName} +
  • +
  • + 센서 타입 + {item.sensorType} +
  • +
  • + 촬영 해상도 + {item.photoResolution} +
  • +
+ ))} +
+ )} + + {!isLoading && totalPage > 1 && ( +
+ + {page > 3 && } + {page > 4 && ...} + {Array.from({ length: 5 }, (_, i) => page - 2 + i) + .filter((p) => p >= 1 && p <= totalPage) + .map((p) => ( + + ))} + {page < totalPage - 3 && ...} + {page < totalPage - 2 && } + +
+ )} +
+ {/* 하단버튼 영역 */} +
+ +
+
+
+ + {isRegisterOpen && ( + setIsRegisterOpen(false)} + onSaved={() => search(page)} + /> + )} + + {detailManageId && ( + setDetailManageId(null)} + onSaved={() => search(page)} + /> + )} +
+ ); +} diff --git a/src/satellite/components/SatelliteManageRegisterPopup.jsx b/src/satellite/components/SatelliteManageRegisterPopup.jsx new file mode 100644 index 00000000..23ef156c --- /dev/null +++ b/src/satellite/components/SatelliteManageRegisterPopup.jsx @@ -0,0 +1,231 @@ +import { useState, useEffect } from 'react'; +import { fetchCommonCodeList } from '@/api/commonApi'; +import { + saveSatelliteManage, + fetchSatelliteManageDetail, + updateSatelliteManage, +} from '@/api/satelliteApi'; +import useDraggable from '../hooks/useDraggable'; + +/** + * 위성 관리 등록/수정 팝업 + * @param {{ satelliteManageId?: number, onClose: () => void, onSaved: () => void }} props + * satelliteManageId가 있으면 수정 모드, 없으면 등록 모드 + */ +export default function SatelliteManageRegisterPopup({ satelliteManageId, onClose, onSaved }) { + const isEditMode = !!satelliteManageId; + + const [sensorTypeOptions, setSensorTypeOptions] = useState([]); + + const [companyNo, setCompanyNo] = useState(''); + const [satelliteName, setSatelliteName] = useState(''); + const [sensorType, setSensorType] = useState(''); + const [photoResolution, setPhotoResolution] = useState(''); + const [frequency, setFrequency] = useState(''); + const [photoDetail, setPhotoDetail] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const { position, handleMouseDown } = useDraggable(); + + // 마운트 시 센서타입 공통코드 로드 + 수정 모드면 상세조회 + useEffect(() => { + let cancelled = false; + + async function load() { + setIsLoading(true); + setError(null); + + try { + const codeList = await fetchCommonCodeList('000092'); + if (cancelled) return; + setSensorTypeOptions(codeList); + + if (satelliteManageId) { + const data = await fetchSatelliteManageDetail(satelliteManageId); + if (cancelled || !data) return; + + setCompanyNo(data.companyNo ?? ''); + setSatelliteName(data.satelliteName || ''); + setSensorType(data.sensorType || ''); + setPhotoResolution(data.photoResolution || ''); + setFrequency(data.frequency || ''); + setPhotoDetail(data.photoDetail || ''); + } + } catch { + setError('데이터 조회 중 오류가 발생했습니다.'); + } finally { + if (!cancelled) setIsLoading(false); + } + } + + load(); + return () => { cancelled = true; }; + }, [satelliteManageId]); + + const handleSave = async () => { + setIsSaving(true); + setError(null); + + try { + if (isEditMode) { + await updateSatelliteManage({ + satelliteManageId, + companyNo: Number(companyNo), + satelliteName, + sensorType, + photoResolution, + frequency, + photoDetail, + }); + } else { + await saveSatelliteManage({ + companyNo: Number(companyNo), + satelliteName, + sensorType, + photoResolution, + frequency, + photoDetail, + }); + } + onSaved?.(); + onClose(); + } catch { + setError('저장 중 오류가 발생했습니다.'); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+
+ + {isEditMode ? '위성 관리 상세' : '위성 관리 등록'} + +
+ +
+ {isLoading &&
조회 중...
} + {error &&
{error}
} + + {!isLoading && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역에 대한 내용을 등록하는 표입니다.
사업자명 * + setCompanyNo(e.target.value)} + /> +
위성명 * + setSatelliteName(e.target.value)} + /> +
센서 타입 + +
촬영 해상도 + setPhotoResolution(e.target.value)} + /> +
주파수 + setFrequency(e.target.value)} + /> +
상세내역 +