diff --git a/public/css/common.css b/public/css/common.css index 6f7b459c..2c5e9079 100644 --- a/public/css/common.css +++ b/public/css/common.css @@ -71,6 +71,8 @@ .schInput{height: 3.5rem; font-family: 'NanumSquare', sans-serif;font-size: var(--fs-m); color: var(--white);background-color: var(--tertiary1);padding: 0 1.2rem; border: 0;} .schInput::placeholder { color:rgba(var(--white-rgb),.3); } .dateInput {background:url(../images/ico_input_cal.svg) no-repeat center right .5rem/2.4rem; padding-right: 3rem; cursor: pointer;} +.dateInput { position: relative; } +.dateInput::-webkit-calendar-picker-indicator { opacity: 0; position: absolute; right: 0; width: 3rem; height: 100%;cursor: pointer; } .dateInput::placeholder { color:var(--white); } /* ========================= @@ -107,6 +109,15 @@ .colList.lineSB li a .title {font-size: var(--fs-m); font-weight: var(--fw-bold);} .colList.lineSB li a .meta {font-size: var(--fs-s); font-weight: var(--fw-regular); color:rgba(var(--white-rgb),.5);} +/* 페이지네이션 */ +.pagination {display: flex; align-items: center; justify-content: center; gap: .5rem; padding: 1.4rem 0;} +.pagination button {min-width: 2.8rem; height: 2.8rem; padding: 0 .6rem; border-radius: .4rem; background-color: var(--secondary1); color: var(--white); font-size: var(--fs-m); font-weight: var(--fw-bold); border: 1px solid var(--secondary3); cursor: pointer; transition: background-color .15s ease, border-color .15s ease;} +.pagination button:hover {background-color: var(--secondary3); border-color: var(--secondary4);} +.pagination button.on {background-color: var(--primary1); border-color: var(--primary1);} +.pagination button.on:hover {background-color: var(--primary2); border-color: var(--primary2);} +.pagination button.disabled {opacity: 0.4; cursor: default; pointer-events: none;} +.pagination .ellipsis {color: rgba(var(--white-rgb), .4); font-size: var(--fs-m); padding: 0 .2rem; user-select: none;} + /* 아코디언리스트 */ .accordionWrap {display: flex;flex-direction: column;transition: max-height 0.3s ease;} .accordionWrap .acdHeader {display: flex; justify-content: space-between; align-items: center; height: 4rem; background-color: var(--secondary1); padding: 1rem; border-bottom: .1rem solid var(--secondary3);} @@ -292,7 +303,7 @@ align-items: center; z-index: 999; } -.popupUtillWrap { position: absolute;top: 50%; left: 50%;transform: translate(-50%, -50%);z-index :85;} +.popupUtillWrap { position: fixed;top: 50%; left: 50%;transform: translate(-50%, -50%);z-index :100;} .popupUtill {display: flex; flex-direction: column; width: 52.5rem; height:auto;max-height: 80vh;overflow: hidden; background-color: var(--secondary2); border: .1rem solid var(--secondary3); padding:2.5rem 3rem;} .popupUtill > .puHeader {display: flex; justify-content: space-between; align-items: center; padding-bottom: 2rem;} .popupUtill > .puHeader > .title {font-weight: var(--fw-bold); font-size: var(--fs-xl);} diff --git a/src/App.jsx b/src/App.jsx index eb48247b..4cbb8b79 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -35,16 +35,16 @@ export default function App() { /publish/* 로 접근하여 퍼블리시 결과물 미리보기 프로덕션 빌드 시 이 라우트와 관련 모듈이 제외됨 ===================== */} - {import.meta.env.DEV && PublishRouter && ( - Loading publish...}> - - - } - /> - )} + {/*{import.meta.env.DEV && PublishRouter && (*/} + {/* Loading publish...}>*/} + {/* */} + {/* */} + {/* }*/} + {/* />*/} + {/*)}*/} ); 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/api/weatherApi.js b/src/api/weatherApi.js new file mode 100644 index 00000000..66f2b84f --- /dev/null +++ b/src/api/weatherApi.js @@ -0,0 +1,309 @@ +/** + * 기상해양 API + */ + +const SPECIAL_NEWS_ENDPOINT = '/api/gis/weather/special-news/search'; + +/** + * 기상특보 목록 조회 + * + * @param {Object} params + * @param {string} params.startPresentationDate - 조회 시작일 (e.g. '2026-01-01') + * @param {string} params.endPresentationDate - 조회 종료일 (e.g. '2026-01-31') + * @param {number} params.page - 페이지 번호 + * @param {number} params.limit - 페이지당 항목 수 + * @returns {Promise<{ list: Array, totalPage: number }>} + */ +export async function fetchWeatherAlerts({ startPresentationDate, endPresentationDate, page, limit }) { + try { + const response = await fetch(SPECIAL_NEWS_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + startPresentationDate, + endPresentationDate, + page, + limit, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + return { + list: result?.specialNewsDetailList || [], + totalPage: result?.totalPage || 0, + }; + } catch (error) { + console.error('[fetchWeatherAlerts] Error:', error); + throw error; + } +} + +const TYPHOON_LIST_ENDPOINT = '/api/gis/weather/typhoon/list/search'; +const TYPHOON_DETAIL_ENDPOINT = '/api/gis/weather/typhoon/search'; + +/** + * 태풍 목록 조회 + * + * @param {Object} params + * @param {string} params.typhoonBeginningYear - 조회 연도 + * @param {string} params.typhoonBeginningMonth - 조회 월 (빈 문자열이면 전체) + * @param {number} params.page - 페이지 번호 + * @param {number} params.limit - 페이지당 항목 수 + * @returns {Promise<{ list: Array, totalPage: number }>} + */ +export async function fetchTyphoonList({ typhoonBeginningYear, typhoonBeginningMonth, page, limit }) { + try { + const response = await fetch(TYPHOON_LIST_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + typhoonBeginningYear, + typhoonBeginningMonth, + page, + limit, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + const grouped = result?.typhoonList || []; + const list = grouped.flatMap((group) => group.typhoonList || []); + + return { + list, + totalPage: result?.totalPage || 0, + }; + } catch (error) { + console.error('[fetchTyphoonList] Error:', error); + throw error; + } +} + +/** + * 태풍 상세(진행정보) 조회 + * + * @param {Object} params + * @param {string} params.typhoonSequence - 태풍 순번 + * @param {string} params.year - 연도 + * @returns {Promise} + */ +export async function fetchTyphoonDetail({ typhoonSequence, year }) { + try { + const response = await fetch(TYPHOON_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + typhoonSequence, + year, + page: 1, + limit: 10000, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + return result?.typhoonSelectDto || []; + } catch (error) { + console.error('[fetchTyphoonDetail] Error:', error); + throw error; + } +} + +const TIDE_INFORMATION_ENDPOINT = '/api/gis/weather/tide-information/search'; +const SUNRISE_SUNSET_DETAIL_ENDPOINT = '/api/gis/weather/tide-information/observatory/detail/search'; + +/** + * 조석정보 통합 조회 (조위관측소 + 일출몰관측지역) + * + * @returns {Promise<{ observatories: Array, sunriseSunsets: Array }>} + */ +export async function fetchTideInformation() { + try { + const response = await fetch(TIDE_INFORMATION_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 { + observatories: result?.observatorySearchDto || [], + sunriseSunsets: result?.sunriseSunsetSearchDto || [], + }; + } catch (error) { + console.error('[fetchTideInformation] Error:', error); + throw error; + } +} + +/** + * 일출일몰 상세 조회 + * + * @param {Object} params - SunriseSunsetSearchDto + * @param {string} params.locationName - 지역명 + * @param {string} params.locationType - 지역 유형 + * @param {Object} params.coordinate - 좌표 + * @param {boolean} params.isChecked - 체크 여부 + * @param {Array} params.locationCoordinates - 좌표 배열 + * @returns {Promise} SunriseSunsetSelectDetailDto 또는 null + */ +export async function fetchSunriseSunsetDetail(params) { + try { + const response = await fetch(SUNRISE_SUNSET_DETAIL_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}`); + } + + const result = await response.json(); + + return result?.sunriseSunsetSelectDetailDto?.[0] || null; + } catch (error) { + console.error('[fetchSunriseSunsetDetail] Error:', error); + throw error; + } +} + +const OBSERVATORY_ENDPOINT = '/api/gis/weather/observatory/search'; +const OBSERVATORY_DETAIL_ENDPOINT = '/api/gis/weather/observatory/select/detail/search'; + +/** + * 관측소 목록 조회 + * + * @returns {Promise} ObservatorySearchDto 배열 + */ +export async function fetchObservatoryList() { + try { + const response = await fetch(OBSERVATORY_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?.dtoList || []; + } catch (error) { + console.error('[fetchObservatoryList] Error:', error); + throw error; + } +} + +/** + * 관측소 상세정보 조회 + * + * @param {Object} params + * @param {string} params.observatoryId - 관측소 ID + * @param {string} params.toDate - 조회 기준일 (e.g. '2026-02-10') + * @returns {Promise} ObservatorySelectDetailDto 또는 null + */ +const AIRPORT_ENDPOINT = '/api/gis/weather/airport/search'; +const AIRPORT_DETAIL_ENDPOINT = '/api/gis/weather/airport/select'; + +/** + * 공항 목록 조회 + * + * @returns {Promise} AirportSearchDto 배열 + */ +export async function fetchAirportList() { + try { + const response = await fetch(AIRPORT_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?.airportSearchDto || []; + } catch (error) { + console.error('[fetchAirportList] Error:', error); + throw error; + } +} + +/** + * 공항 상세정보 조회 + * + * @param {Object} params + * @param {string} params.airportId - 공항 ID + * @returns {Promise} AirportSelectDto 또는 null + */ +export async function fetchAirportDetail({ airportId }) { + try { + const response = await fetch(AIRPORT_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ airportId }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result?.airportSelectDto || null; + } catch (error) { + console.error('[fetchAirportDetail] Error:', error); + throw error; + } +} + +export async function fetchObservatoryDetail({ observatoryId, toDate }) { + try { + const response = await fetch(OBSERVATORY_DETAIL_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ observatoryId, toDate }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + return result?.observatorySelectDetail?.[0] || null; + } catch (error) { + console.error('[fetchObservatoryDetail] Error:', error); + throw error; + } +} diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index c20389d5..d4b44506 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -19,6 +19,8 @@ import DisplayComponent from '../../component/wrap/side/DisplayComponent'; // 구현된 페이지 import ReplayPage from '../../pages/ReplayPage'; import AreaSearchPage from '../../areaSearch/components/AreaSearchPage'; +import WeatherPage from '../../pages/WeatherPage'; +import SatellitePage from '../../pages/SatellitePage'; /** * 사이드바 컴포넌트 @@ -64,8 +66,8 @@ export default function Sidebar() { const renderPanel = () => { const panelMap = { gnb1: DisplayComponent ? : null, - gnb2: Panel2Component ? : null, - gnb3: Panel3Component ? : null, + gnb2: , + gnb3: , gnb4: Panel4Component ? : null, gnb5: Panel5Component ? : null, gnb6: Panel6Component ? : null, @@ -78,11 +80,11 @@ export default function Sidebar() { }; return ( -
- -
- {renderPanel()} -
-
+
+ +
+ {renderPanel()} +
+
); } diff --git a/src/map/MapContainer.jsx b/src/map/MapContainer.jsx index 36da42d5..20994cb9 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'; @@ -400,6 +401,8 @@ export default function MapContainer() { worldMap, encMap, darkMap, + satelliteLayer, + csvDeckLayer, eastAsiaMap, korMap, ], @@ -429,7 +432,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) => { 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/pages/WeatherPage.jsx b/src/pages/WeatherPage.jsx new file mode 100644 index 00000000..0e98bc16 --- /dev/null +++ b/src/pages/WeatherPage.jsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import WeatherAlert from '@/weather/components/WeatherAlert'; +import TyphoonInfo from '@/weather/components/TyphoonInfo'; +import TidalObservation from '@/weather/components/TidalObservation'; +import TidalInfo from '@/weather/components/TidalInfo'; +import AviationWeather from '@/weather/components/AviationWeather'; + +const tabs = [ + { id: 'weather01', label: '기상특보' }, + { id: 'weather02', label: '태풍정보' }, + { id: 'weather03', label: '조위관측' }, + { id: 'weather04', label: '조석정보' }, + { id: 'weather05', label: '항공기상' }, +]; + +const tabComponents = { + weather01: WeatherAlert, + weather02: TyphoonInfo, + weather03: TidalObservation, + weather04: TidalInfo, + weather05: AviationWeather, +}; + +export default function WeatherPage({ isOpen, onToggle }) { + const [activeTab, setActiveTab] = useState('weather01'); + + const ActiveComponent = tabComponents[activeTab]; + + return ( + + ); +} diff --git a/src/publish/pages/Panel3Component.jsx b/src/publish/pages/Panel3Component.jsx index 55780771..359d8240 100644 --- a/src/publish/pages/Panel3Component.jsx +++ b/src/publish/pages/Panel3Component.jsx @@ -41,9 +41,10 @@ export default function Panel3Component({ isOpen, onToggle }) {
  • diff --git a/src/satellite/components/SatelliteImageManage.jsx b/src/satellite/components/SatelliteImageManage.jsx new file mode 100644 index 00000000..4dc146db --- /dev/null +++ b/src/satellite/components/SatelliteImageManage.jsx @@ -0,0 +1,387 @@ +import { useState, useEffect, useCallback } from 'react'; +import { fetchCommonCodeList } from '@/api/commonApi'; +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); + + // 공통코드 옵션 + const [videoKindOptions, setVideoKindOptions] = useState([]); + const [videoOriginOptions, setVideoOriginOptions] = useState([]); + const [videoOrbitOptions, setVideoOrbitOptions] = useState([]); + const [videoCycleOptions, setVideoCycleOptions] = useState([]); + + // 폼 필터 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); + + // 마운트 시 공통코드 로드 + useEffect(() => { + fetchCommonCodeList('000109').then(setVideoKindOptions).catch(() => setVideoKindOptions([])); + fetchCommonCodeList('000111').then(setVideoOriginOptions).catch(() => setVideoOriginOptions([])); + fetchCommonCodeList('000110').then(setVideoOrbitOptions).catch(() => setVideoOrbitOptions([])); + fetchCommonCodeList('000108').then(setVideoCycleOptions).catch(() => setVideoCycleOptions([])); + }, []); + + 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..8f787f1c --- /dev/null +++ b/src/satellite/components/SatelliteManageRegisterPopup.jsx @@ -0,0 +1,254 @@ +import { useState, useEffect } from 'react'; +import {createPortal} from "react-dom"; +import { showToast } from '@/components/common/Toast'; +import { fetchCommonCodeList } from '@/api/commonApi'; +import { + fetchSatelliteCompanyList, + 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 [companyOptions, setCompanyOptions] = 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, companyList] = await Promise.all([ + fetchCommonCodeList('000092'), + fetchSatelliteCompanyList(), + ]); + if (cancelled) return; + setSensorTypeOptions(codeList); + setCompanyOptions(companyList); + + 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 () => { + if (!companyNo) { + showToast('사업자명을 선택해주세요.'); + return; + } + if (!satelliteName.trim()) { + showToast('위성명을 입력해주세요.'); + return; + } + + 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 createPortal( +
    +
    +
    + + {isEditMode ? '위성 관리 상세' : '위성 관리 등록'} + +
    + +
    + {isLoading &&
    조회 중...
    } + {error &&
    {error}
    } + + {!isLoading && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역에 대한 내용을 등록하는 표입니다.
    사업자명 * + +
    위성명 * + setSatelliteName(e.target.value)} + /> +
    센서 타입 + +
    촬영 해상도 + setPhotoResolution(e.target.value)} + /> +
    주파수 + setFrequency(e.target.value)} + /> +
    상세내역 +