diff --git a/package-lock.json b/package-lock.json index 5edeedbb..156df7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10572,9 +10572,9 @@ "integrity": "sha512-007VucCkqNOMMb9ggRLNuJowwaJcyOh4sKAFcdGfahfGc7JQbf94zSzjdBq/wVyHWUEs5o3+idhFZ0wbZMRmVQ==" }, "flatted": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.0.tgz", - "integrity": "sha512-XprP7lDrVT+kE2c2YlfiV+IfS9zxukiIOvNamPNsImNhXadSsQEbosItdL9bUQlCZXR13SvPk20BjWSWLA7m4A==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "flatten": { "version": "1.0.3", @@ -23486,6 +23486,12 @@ "resolved": "https://registry.npmjs.org/redux-debounced/-/redux-debounced-0.5.0.tgz", "integrity": "sha512-O2anhB0A6yQZH19uLETFtajcUQLcyiJcgC0hHSoFr5T3hWGtt0C5s6KNnb2RX51MwCh5VCl9ehZTv91F/rsZww==" }, + "redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "dev": true + }, "redux-saga": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", diff --git a/package.json b/package.json index f6a709f1..0597e581 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "react-app-rewire-sass-rule": "^2.1.1", "react-app-rewired": "^2.1.6", "react-snap": "1.23.0", + "redux-persist": "^6.0.0", "sass-loader": "^8.0.2", "typescript": "4.3.5" }, diff --git a/src/components/flight/FlightApprovalsReport.js b/src/components/flight/FlightApprovalsReport.js index fc9f32b0..f35166fd 100644 --- a/src/components/flight/FlightApprovalsReport.js +++ b/src/components/flight/FlightApprovalsReport.js @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Flatpickr from 'react-flatpickr'; import { Button, Input, CustomInput, Col, Row } from '@component/ui'; import { Search, Calendar } from 'react-feather'; @@ -16,6 +16,19 @@ export default function FlightApprovalsReport(props) { endDate: dayjs().format('YYYY-MM-DD') }); + useEffect(() => { + const popupSyncSearchData = JSON.parse(localStorage.getItem('popupState')); + if (popupSyncSearchData) { + setSearchDate({ + startDate: popupSyncSearchData.startDate, + endDate: popupSyncSearchData.endDate + }); + setFilterArea(popupSyncSearchData.selected); + setFilterId(popupSyncSearchData.filter); + localStorage.removeItem('popupState'); + } + }, []); + const handleKeyDown = e => { if (e.key === 'Enter') { props.handlerSearch(filterId, searchDate, filterArea); @@ -23,7 +36,7 @@ export default function FlightApprovalsReport(props) { }; return ( -
+

비행승인 신청 검토결과 현황

diff --git a/src/components/flight/FlightApprovalsTable.js b/src/components/flight/FlightApprovalsTable.js index f7679f95..002f738f 100644 --- a/src/components/flight/FlightApprovalsTable.js +++ b/src/components/flight/FlightApprovalsTable.js @@ -23,7 +23,6 @@ export default function FlightApprovalsTable(props) { // 행 토글 const [expandedRows, setExpandedRows] = useState({}); - console.log('>>', expandedRows); // 승인, 미승인, 비대상 건수 계산 useEffect(() => { resApprovalCd(); @@ -364,16 +363,12 @@ export default function FlightApprovalsTable(props) { // 테이블 내부 행 클릭 이벤트 const handleInRowClick = row => { - console.log('>>', row); - handlerOpenModal(row.approvalCd, row.fltElev, row.fltElevMax); props.handlerDetail(row); }; // 테이블 행 클릭 이벤트 const handleRowClick = row => { - console.log('>>', row); - handlerOpenModal( row.areaList[0].approvalCd, row.areaList[0].fltElev, diff --git a/src/containers/flight/flightApprovalsContainer.js b/src/containers/flight/flightApprovalsContainer.js index f618c5b8..1d285189 100644 --- a/src/containers/flight/flightApprovalsContainer.js +++ b/src/containers/flight/flightApprovalsContainer.js @@ -25,7 +25,7 @@ import { setLogout } from '@src/redux/features/account/auth/authThunk'; import WebsocketClient from '../../components/websocket/WebsocketClient'; import { clientDispatchTopMenu } from '@src/redux/features/layout/layoutSlice'; -export default function FlightApprovalsContainer() { +export default function FlightApprovalsContainer({ mode }) { const dispatch = useDispatch(); const history = useHistory(); @@ -48,6 +48,10 @@ export default function FlightApprovalsContainer() { const { laancAprvList } = useSelector(state => state.laancState); const map = useSelector(state => state.mapState.map); + // popup + const [isPopup, setIsPopup] = useState(false); + const [popup, setPopup] = useState(null); + const popupRef = useRef(null); const previewGeo2 = { type: 'FeatureCollection', @@ -66,20 +70,113 @@ export default function FlightApprovalsContainer() { }, []); useEffect(() => { - if (areaCoordList.length != 0) { + if (areaCoordList.length !== 0) { handlerPreviewDraw(); } }, [areaCoordList]); useEffect(() => { if (map) { - setMapObject(map); + window._mapbox = map; + let mapInstance = mode === 'container' ? map : window.opener._mapbox; + setMapObject(mapInstance); } }, [map]); - useEffect(async () => { - if (areaCoordList.length === 0) return; - }, [areaCoordList]); + useEffect(() => { + const childMessage = e => { + if (e.data.type) { + const { type } = e.data; + const { payload } = e.data; + console.log(payload); + switch (type) { + case 'initalState': + popupRef.current.postMessage({ + type: 'initalState', + payload: { + filter, + selected, + startDate, + endDate + } + }); + + return; + case 'search': + const { search, searchDate, filterArea } = payload; + + handlerSearch(search, searchDate, filterArea); + return; + case 'detail': + const { area } = payload; + handlerDetail(area); + return; + case 'closedSync': + popupRef.current.close(); + // localStorage.removeItem('popupState'); + return; + default: + break; + } + } + }; + let timer; + if (popup) { + timer = setInterval(() => { + if (popup.closed) { + setIsPopup(false); + clearInterval(timer); + } + }, 1000); // 1초마다 체크 + } + window.addEventListener('message', childMessage); + + return () => { + clearInterval(timer); + window.removeEventListener('message', childMessage); + }; + }, [popup]); + + useEffect(() => { + const handleBeforeUnload = e => { + localStorage.removeItem('persist:root'); + + if (popupRef.current) { + popupRef.current.close(); + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); + + const handleDragEnd = e => { + setIsPopup(true); + const el = document.querySelector('.flight-approval-layer'); + + const popupWidth = el.offsetWidth; // 팝업의 너비 + const popupHeight = el.offsetHeight; // 팝업의 너비 + // const popupX = window.screenX + window.outerWidth - e.clientX; // 오른쪽 끝에 띄우기 + // const popupY = Math.round( + // window.screenY + window.outerHeight / 2 - popupHeight / 2 + // ); + + const popupX = + window.screenX + + (window.outerWidth - popupWidth) / 2 + + e.clientX - + window.innerWidth / 2; // 드래그 끝나는 지점 + const popupY = Math.round( + window.screenY + window.outerHeight / 2 - popupHeight / 2 + ); + const option = `width=${popupWidth},height=${popupHeight},left=${popupX},top=${popupY}`; + popupRef.current = window.open('/rightMenu', 'NewWindow', option); + // setPopupOption(option); + setPopup(popupRef.current); + }; const handlerSearch = (search, searchDate, filterArea) => { setStartDate(searchDate.startDate); @@ -116,7 +213,14 @@ export default function FlightApprovalsContainer() { ); } // ); + setFilter(search); + if (popup) { + popupRef.current.postMessage({ + type: 'handlerSearchRs', + payload: { search } + }); + } }; const handlerDetail = area => { @@ -128,27 +232,30 @@ export default function FlightApprovalsContainer() { }; const handlerMapInit = () => { - if (map.getSource('preview')) { + let mapInstance = mode === 'container' ? map : window.opener._mapbox; + + if (mapInstance.getSource('preview')) { } else { - map.addSource('preview', { + mapInstance.addSource('preview', { type: 'geojson', data: previewGeo2 }); - map.addLayer(flightlayerWayPoint('preview')); - map.addLayer(flightlayerBuffer('preview')); - map.addLayer(flightlayerPolygon('preview')); - map.addLayer(flightlayerPolyline('preview')); + mapInstance.addLayer(flightlayerWayPoint('preview')); + mapInstance.addLayer(flightlayerBuffer('preview')); + mapInstance.addLayer(flightlayerPolygon('preview')); + mapInstance.addLayer(flightlayerPolyline('preview')); } dispatch(clientSetIsMapLoading(true)); - const preview = map.getSource('preview'); + const preview = mapInstance.getSource('preview'); if (preview) setPreviewLayer(preview); setIsMapLoading(true); - setMapObject(map); - dispatch(clientMapInit(map)); + setMapObject(mapInstance); + + dispatch(clientMapInit(mapInstance)); }; const handlerPreviewDraw = () => { if (areaCoordList.length > 0) { @@ -190,53 +297,61 @@ export default function FlightApprovalsContainer() { return ( <> -
-

- - UTM -

-
    -
  • - +
  • +
+
    +
  • + +
  • +
  • + +
  • +
+
+
+ +
+ + + {!isPopup && ( +
+
+
+ + - - - -
    -
  • - -
  • -
  • - -
  • -
-
-
- -
-
-
-
- - +
-
+ )} ); } diff --git a/src/containers/rightMenuContainer.js b/src/containers/rightMenuContainer.js new file mode 100644 index 00000000..da3b5a61 --- /dev/null +++ b/src/containers/rightMenuContainer.js @@ -0,0 +1,137 @@ +import { useEffect, useState } from 'react'; +import dayjs from 'dayjs'; +import { useDispatch } from '@src/redux/store'; +import { getLaancAprvList } from '@src/redux/features/laanc/laancThunk'; +import FlightApprovalsTable from '@src/components/flight/FlightApprovalsTable'; +import FlightApprovalsReport from '@src/components/flight/FlightApprovalsReport'; + +function RightMenuContainer() { + const [filter, setFilter] = useState(''); + const [startDate, setStartDate] = useState(dayjs().format('YYYY-MM-DD')); + const [endDate, setEndDate] = useState(); + const [selected, setSelected] = useState(null); + + const dispatch = useDispatch(); + + useEffect(() => { + handlerOpnerPostMessage('initalState', null); + window.addEventListener('message', opnerMessage); + + return () => { + window.removeEventListener('message', opnerMessage); + }; + }, []); + + const opnerMessage = e => { + const { type } = e.data; + const { payload } = e.data; + + switch (type) { + case 'initalState': + setFilter(payload.filter); + setSelected(payload.selected); + setStartDate(payload.startDate); + setEndDate(payload.endDate); + + return; + case 'handlerSearchRs': + console.log(payload.filter); + setFilter(payload.filter); + + return; + default: + break; + } + }; + + const handlerOpnerPostMessage = (type, payload) => { + switch (type) { + case 'initalState': + window.opener.postMessage({ type, payload }); + return; + case 'search': + window.opener.postMessage({ type, payload }); + return; + case 'detail': + window.opener.postMessage({ type, payload }); + return; + case 'closedSync': + window.opener.postMessage({ type, payload }); + default: + break; + } + }; + + const handlerSearch = (search, searchDate, filterArea) => { + if ( + search != '' && + (search === '승인' || search === '미승인' || search === '비대상') + ) { + dispatch( + getLaancAprvList({ + searchStDt: searchDate.startDate, + searchEndDt: searchDate.endDate, + selectZone: filterArea, + approvalCd: search === '승인' ? 'S' : search === '미승인' ? 'F' : 'U' + }) + ); + } else if (search != '') { + dispatch( + getLaancAprvList({ + searchStDt: searchDate.startDate, + searchEndDt: searchDate.endDate, + selectZone: filterArea, + applyNo: search + }) + ); + } else { + dispatch( + getLaancAprvList({ + searchStDt: searchDate.startDate, + searchEndDt: searchDate.endDate, + selectZone: filterArea + }) + ); + } + localStorage.setItem( + 'popupState', + JSON.stringify({ + filter: search, + selected: filterArea, + startDate: searchDate.startDate, + endDate: searchDate.endDate + }) + ); + handlerOpnerPostMessage('search', { search, searchDate, filterArea }); + }; + + const handlerDetail = area => { + handlerOpnerPostMessage('detail', { area }); + }; + + const handleBeforeUnload = () => { + handlerOpnerPostMessage('closedSync', ''); + }; + + return ( +
+
+
+ + +
+
+
+ ); +} + +export default RightMenuContainer; diff --git a/src/index.js b/src/index.js index b82688a7..bd7ccd81 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,8 @@ import { HelmetProvider } from 'react-helmet-async'; // ** Redux Imports import { Provider } from 'react-redux'; import { store } from '@src/redux/store'; +import { persistStore } from 'redux-persist'; +import { PersistGate } from 'redux-persist/integration/react'; // ** Toast & ThemeColors Context import { ToastContainer } from 'react-toastify'; @@ -43,16 +45,20 @@ import * as serviceWorker from './serviceWorker'; // ** Lazy load app const LazyApp = lazy(() => import('./App')); const rootElement = document.getElementById('root'); +const persistor = persistStore(store); + const element = ( - }> - - - - - - - + + }> + + + + + + + + ); diff --git a/src/redux/rootReducer.ts b/src/redux/rootReducer.ts index bb217d0a..d2200170 100644 --- a/src/redux/rootReducer.ts +++ b/src/redux/rootReducer.ts @@ -1,4 +1,6 @@ import { combineReducers } from '@reduxjs/toolkit'; +import { persistReducer, createTransform } from 'redux-persist'; +import storage from 'redux-persist/lib/storage'; import { layoutReducer } from './features/layout/layoutSlice'; import { dashboardReducer } from './features/dashboard/dashboardSlice'; import { droneReducer } from './features/basis/drone/droneSlice'; @@ -21,6 +23,28 @@ import { controlGpHisReducer } from './features/control/gp/gpSlice'; import { controlGpDtlReducer } from './features/control/gp/gpSlice'; import { controlGpCountReducer } from './features/control/gp/gpSlice'; import { crtfyhpReducer } from './features/comn/crtfyhp/crtfyhpSlice'; +import { laancState } from './features/laanc/laancState'; + +// Transform 정의 - laancReducers 리듀서에서 laancAprvList 상태만 저장 +const saveSubsetFilter = createTransform>( + inboundState => { + return { + laancAprvList: inboundState.laancAprvList, + areaCoordList: inboundState.areaCoordList + }; + }, + outboundState => { + return outboundState as laancState; + }, + { whitelist: ['laancState'] } +); + +const rootPersistConfig = { + key: 'root', // localStorage key + storage, // localStorage + whitelist: ['laancState'], // target (reducer name) + transforms: [saveSubsetFilter] // 특정 상태만 저장 +}; const rootReducer = (state: any, action: any) => { const combineReducer = combineReducers({ @@ -65,4 +89,4 @@ const rootReducer = (state: any, action: any) => { return combineReducer(state, action); }; -export default rootReducer; +export default persistReducer(rootPersistConfig, rootReducer); diff --git a/src/router/routes/index.js b/src/router/routes/index.js index 7ebcb4f1..df7579ed 100644 --- a/src/router/routes/index.js +++ b/src/router/routes/index.js @@ -97,6 +97,14 @@ const Routes = [ authRoute: true } }, + { + path: '/rightMenu', + component: lazy(() => import('../../views/rightMenuView')), + layout: 'BlankLayout', + meta: { + authRoute: true + } + }, { path: '/history/record/sample1', component: lazy(() => diff --git a/src/views/flight/FlightView.js b/src/views/flight/FlightView.js index c739a567..e771029d 100644 --- a/src/views/flight/FlightView.js +++ b/src/views/flight/FlightView.js @@ -1,3 +1,4 @@ +import { useEffect, useRef } from 'react'; import '@styles/react/libs/flatpickr/flatpickr.scss'; import '@styles/react/libs/tables/react-dataTable-component.scss'; import '../../assets/css/custom.css'; @@ -10,7 +11,7 @@ export default function FlightView() { {/* 관제시스템 */} - ; + ;
); } diff --git a/src/views/rightMenuView.js b/src/views/rightMenuView.js new file mode 100644 index 00000000..e5f99c03 --- /dev/null +++ b/src/views/rightMenuView.js @@ -0,0 +1,13 @@ +import '@styles/react/libs/flatpickr/flatpickr.scss'; +import '@styles/react/libs/tables/react-dataTable-component.scss'; +import '../assets/css/custom.css'; +import RightMenuContainer from '../containers/rightMenuContainer'; +import FlightApprovalsContainer from '../containers/flight/flightApprovalsContainer'; + +export default function rightMenuView() { + return ( +
+ +
+ ); +}