Browse Source

비행승인 새페이지 작업중

master
sanguu516 3 months ago
parent
commit
345516f9ec
  1. 2
      .nvmrc
  2. 1127
      package-lock.json
  3. 3
      package.json
  4. 105
      src/components/flight/NewFlightApprovalsReport.js
  5. 444
      src/components/flight/NewFlightApprovalsTable.js
  6. 207
      src/containers/flight/NewFlightApprovalsContainer.js
  7. 28
      src/redux/features/laanc/laancState.ts
  8. 11
      src/router/routes/RouteFlight.js
  9. 15
      src/views/flight/NewFlightView.js

2
.nvmrc

@ -1 +1 @@
14.16.0
v14.16.0

1127
package-lock.json generated

File diff suppressed because it is too large Load Diff

3
package.json

@ -29,6 +29,8 @@
"@types/stompjs": "^2.3.4",
"@uppy/react": "1.10.8",
"animate.css": "4.1.1",
"ant-design": "^1.0.0",
"antd": "^5.18.1",
"apexcharts": "3.23.0",
"availity-reactstrap-validation-safe": "2.6.1",
"axios": "1.6.7",
@ -112,6 +114,7 @@
"styled-components": "5.1.1",
"sweetalert2": "10.14.0",
"sweetalert2-react-content": "3.0.1",
"table": "^6.8.2",
"three": "0.124.0",
"threebox-plugin": "2.2.7",
"typesafe-actions": "^5.1.0",

105
src/components/flight/NewFlightApprovalsReport.js

@ -0,0 +1,105 @@
import { useState } from 'react';
import Flatpickr from 'react-flatpickr';
import { Button, Input, CustomInput, Col, Row } from '@component/ui';
import { Search, Calendar } from 'react-feather';
import dayjs from 'dayjs';
export default function NewFlightApprovalsReport(props) {
// 식별번호
const [filterId, setFilterId] = useState('');
// 지역
const [filterArea, setFilterArea] = useState('');
// 달력
const [searchDate, setSearchDate] = useState({
startDate: dayjs().format('YYYY-MM-DD'),
endDate: dayjs().format('YYYY-MM-DD')
});
const handleKeyDown = e => {
if (e.key === 'Enter') {
props.handlerSearch(filterId, searchDate, filterArea);
}
};
return (
<div className='layer-content'>
<div className='layer-ti'>
<h4>비행승인 신청 검토결과 현황</h4>
</div>
<div className='layer-ti-sub'>
검색일자 또는 신청번호/검토결과를 입력해주세요.
</div>
<div className='layer-search layer-search-form'>
<div className='calendar-flat'>
<Flatpickr
placeholder='날짜를 선택해주세요'
id='searchDate'
options={{
mode: 'range',
defaultDate: [searchDate.startDate, searchDate.endDate]
}}
onChange={date => {
setSearchDate({
startDate: dayjs(date[0]).format('YYYY-MM-DD'),
endDate: dayjs(date[1]).format('YYYY-MM-DD')
});
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/>
<Calendar size={14} />
</div>
<div className='list-input'>
<Input
type='text'
bsSize='sm'
placeholder='신청번호 또는 검토결과를 입력해주세요.'
value={filterId}
onChange={e => setFilterId(`${e.target.value}`)}
onKeyPress={handleKeyDown}
/>
</div>
<div className='search-box'>
<div
className='search-list-ti'
style={{
color: '#555',
border: '1px solid #ddd',
minWidth: '50px',
width: '60px'
}}
>
지역
</div>
<div className='search-list'>
<div className='search-list-cont'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={filterArea}
onChange={e => setFilterArea(e.target.value)}
>
<option value='' selected>
전체
</option>
<option value='gimpo'>김포공항 관제권</option>
</CustomInput>
</div>
</div>
<Button
color='primary'
onClick={() =>
props.handlerSearch(filterId, searchDate, filterArea)
}
size='sm'
>
검색
</Button>
</div>
</div>
</div>
);
}

444
src/components/flight/NewFlightApprovalsTable.js

@ -0,0 +1,444 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from '@src/redux/store';
import { Button, Card } from '@component/ui';
import dayjs from 'dayjs';
import { openModal } from '@src/redux/features/comn/message/messageSlice';
import { Table } from 'antd';
import { FaAngleDown, FaAngleUp } from 'react-icons/fa';
export default function NewFlightApprovalsTable(props) {
const dispatch = useDispatch();
// 비행승인 목록
const { laancAprvList, laancElev } = useSelector(state => state.laancState);
// 승인, 미승인, 비대상 건수
const [approvalCdValue, setApprovalCdValue] = useState({
S: 0,
F: 0,
U: 0
});
// 확장된 행 키
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
// 승인, 미승인, 비대상 건수 계산
useEffect(() => {
resApprovalCd();
}, [laancAprvList]);
const columns = [
{
title: (
<>
신청
<br />
번호
</>
),
dataIndex: 'applyNo',
align: 'center',
width: '40px',
key: 'applyNo',
editable: true
},
{
title: (
<>
신청 <br />
일자
</>
),
dataIndex: 'applyDt',
width: '60px',
align: 'center',
key: 'applyDt',
editable: true,
render: text => dayjs(text).format('YYYY-MM-DD')
},
{
title: (
<>
신청 <br />
구역
</>
),
dataIndex: 'areaList',
align: 'center',
width: '85px',
key: 'areaList',
editable: true,
render: areaList => <>{areaList.length}</>
},
{
title: <>신청자</>,
dataIndex: 'applyDt',
width: '78px',
align: 'center',
key: 'applyDt',
editable: true,
render: text => '홍*동'
},
{
title: (
<>
비행 <br />
구역
</>
),
dataIndex: 'applyDt',
width: '75px',
align: 'center',
key: 'applyDt',
editable: true,
render: text => '서울시 마포구상암동 1674 (원추)'
},
{
title: (
<>
증심좌표 <br />
(위도/경도)
</>
),
dataIndex: 'areaList',
align: 'center',
width: '85px',
key: 'latLon',
editable: true,
render: areaList => (
<>
{areaList[0].lat.toFixed(5)},
<br />
{areaList[0].lon.toFixed(5)}
</>
)
},
{
title: (
<>
반경 <br />
(M)
</>
),
dataIndex: 'areaList',
align: 'center',
width: '70px',
key: 'bufferZone',
editable: true,
render: areaList => <>{areaList[0].bufferZone}</>
},
{
title: (
<>
고도 <br />
(M)
</>
),
dataIndex: 'areaList',
key: 'fltElev',
align: 'center',
width: '70px',
editable: true,
render: areaList => <>{areaList[0].fltElev}</>
},
{
title: (
<>
검토 <br />
결과
</>
),
dataIndex: 'areaList',
align: 'center',
width: '85px',
key: 'approvalCd',
editable: true,
render: areaList => (
<>
{areaList[0].approvalCd === 'U'
? '비대상'
: areaList[0].approvalCd === 'S'
? '승인'
: '미승인'}
</>
)
},
{
title: <>더보기</>,
dataIndex: 'areaList',
align: 'center',
width: '80px',
key: 'more',
editable: true,
render: (areaList, record) =>
areaList.length > 2 ? (
<Button color='flat-dark' onClick={() => handleExpand(record.key)}>
{expandedRowKeys.includes(record.key) ? (
<>
더보기
<FaAngleUp />
</>
) : (
<>
더보기
<FaAngleDown />
</>
)}
</Button>
) : (
<>-</>
)
}
];
const expandedRowRender = record => {
const childColumns = [
{
dataIndex: 'applyNo',
width: '30px',
align: 'center',
key: 'applyNo'
},
{
dataIndex: 'applyDt',
width: '85px',
align: 'center',
key: 'applyDt'
},
{
dataIndex: 'zoneNo',
align: 'center',
width: '85px',
key: 'zoneNo',
editable: true
},
{
align: 'center',
width: '85px',
key: 'latLon',
dataIndex: ['lat', 'lon'],
render: (text, record) => {
const lat = record.lat;
const lon = record.lon;
return (
<>
{lat.toFixed(5)} /<br />
{lon.toFixed(5)}
</>
);
}
},
{
dataIndex: 'bufferZone',
align: 'center',
width: '85px',
key: 'bufferZone'
},
{
dataIndex: 'fltElev',
align: 'center',
width: '85px',
key: 'fltElev'
},
{
dataIndex: 'approvalCd',
align: 'center',
key: 'approvalCd',
render: text => (
<>{text === 'U' ? '비대상' : text === 'S' ? '승인' : '미승인'}</>
)
}
];
const data = [];
record.areaList.map((item, index) => {
if (index < 1) return;
data.push({
key: `${record.applyNo}-${index}`,
applyNo: item.applyNo,
applyDt: item.applyDt,
zoneNo: item.zoneNo,
lat: item.lat,
lon: item.lon,
bufferZone: item.bufferZone,
fltElev: item.fltElev,
approvalCd: item.approvalCd
});
});
return (
<Table
rowClassName={record => {
let className = '';
if (record.approvalCd === 'S') {
className += 'flight-approval-row';
} else if (record.approvalCd === 'F') {
className += 'flight-not-approval-row';
} else className;
return className;
}}
size='small'
bordered
columns={childColumns}
dataSource={data}
pagination={false}
showHeader={false}
/>
);
};
// 모달 오픈 핸들러
const handlerOpenModal = (approval, fltElev, fltElevMax) => {
if (approval === 'F') {
dispatch(
openModal({
header: '미승인 사유',
body: `관제권 내 제한고도(신청고도${fltElev}m/허용고도${fltElevMax}m) 입니다.`,
type: 'error'
})
);
} else if (approval === 'S') {
dispatch(
openModal({
header: '승인 사유',
body: `관제권 내 허용고도(신청고도${fltElev}m/허용고도${fltElevMax}m) 입니다.`,
type: 'success'
})
);
} else {
dispatch(
openModal({
header: '비대상',
body: `해당 구역은 비 대상(신청고도${fltElev}m/허용고도${
fltElevMax === undefined ? 150 : null
}m) 지역 입니다.`,
type: 'error'
})
);
}
};
// 테이블 내부 행 클릭 이벤트
const handleInRowClick = row => {
console.log('>>', row);
handlerOpenModal(row.approvalCd, row.fltElev, row.fltElevMax);
props.handlerDetail(row);
};
// 날짜 포맷 변경
const formatDate = dateString => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
};
// 승인, 미승인, 비대상 건수 계산
const resApprovalCd = () => {
let approvalCdValue = { S: 0, F: 0, U: 0 };
laancAprvList?.map(item => {
item.areaList.map(area => {
if (area.approvalCd === 'S') {
approvalCdValue.S += 1;
} else if (area.approvalCd === 'F') {
approvalCdValue.F += 1;
} else {
approvalCdValue.U += 1;
}
});
});
setApprovalCdValue({
F: approvalCdValue.F,
S: approvalCdValue.S,
U: approvalCdValue.U
});
};
const handleExpand = key => {
const expanded = expandedRowKeys.includes(key);
const keys = expanded
? expandedRowKeys.filter(k => k !== key)
: [...expandedRowKeys, key];
setExpandedRowKeys(keys);
};
return (
<div className='layer-content'>
<div className='layer-ti d-flex justify-content-between align-items-center'>
<h4>비행승인 신청 검토결과 목록</h4>
<span className='search-case'>
{formatDate(props.startDate)}
{props.endDate && props.startDate != props.endDate
? '~' + formatDate(props.endDate) + ' '
: null}
{approvalCdValue.S + approvalCdValue.F + approvalCdValue.U}
</span>
</div>
<div className='search-case-list'>
<div>
<ul>
<li className='approval' style={{ cursor: 'pointer' }}>
승인 {approvalCdValue.S}
</li>
<li className='not-approved' style={{ cursor: 'pointer' }}>
미승인 {approvalCdValue.F}
</li>
<li className='non-target' style={{ cursor: 'pointer' }}>
비대상 {approvalCdValue.U}
</li>
</ul>
</div>
</div>
<div className='invoice-list-wrapper'>
<Card>
<div
className='invoice-list-dataTable flight-approval'
style={{ width: '100%' }}
>
{laancAprvList?.length > 0 ? (
<Table
dataSource={laancAprvList.map((item, index) => ({
...item,
key: index
}))}
columns={columns}
size='small'
rowClassName={record => {
let className = '';
if (record.areaList[0].approvalCd === 'S') {
className += 'flight-approval-row editable-row';
} else if (record.areaList[0].approvalCd === 'F') {
className += 'flight-not-approval-row editable-row';
} else className += 'editable-row';
return className;
}}
expandable={{
expandedRowRender,
expandedRowKeys: expandedRowKeys,
onExpand: handleExpand,
rowExpandable: record => record.areaList.length > 1 // areaList가 1개 이상인 경우에만 확장 가능
}}
tableLayout='auto'
rowHoverable={false}
expandIconColumnIndex={-1} // 기본 확장 아이콘을 숨김
/>
) : (
<div
className='d-flex justify-content-center align-items-center '
style={{ height: '100px', color: '#000' }}
>
<p>비행승인 신청 건수가 없습니다.</p>
</div>
)}
</div>
</Card>
</div>
</div>
);
}

207
src/containers/flight/NewFlightApprovalsContainer.js

@ -0,0 +1,207 @@
import { useEffect, useRef, useState, lazy, Suspense } from 'react';
import { useDispatch, useSelector } from '@src/redux/store';
import NewFlightApprovalsReport from '../../components/flight/NewFlightApprovalsReport';
import {
InitFeature,
handlerFitBounds,
handlerGetCircleCoord,
flightlayerWayPoint,
flightlayerPolyline,
flightlayerPolygon,
flightlayerBuffer
} from '../../utility/MapUtils';
import { useHistory } from 'react-router-dom';
import { clientSaveAreaCoordinateList } from '@src/redux/features/laanc/laancSlice';
import { MapControl } from '../../components/map/MapControl';
import { clientSetIsMapLoading } from '@src/redux/features/laanc/laancSlice';
import { clientMapInit } from '@src/redux/features/control/map/mapSlice';
import NewFlightApprovalsTable from '@src/components/flight/NewFlightApprovalsTable';
import { getLaancAprvList } from '@src/redux/features/laanc/laancThunk';
import dayjs from 'dayjs';
import { setLogout } from '@src/redux/features/account/auth/authThunk';
export default function NewFlightApprovalsContainer() {
const dispatch = useDispatch();
const history = useHistory();
const [selected, setSelected] = useState(null);
const [isMapLoading, setIsMapLoading] = useState(false);
// 비행구역 그리기
const [filter, setFilter] = useState('');
// 지도
const [mapObject, setMapObject] = useState();
const { areaCoordList, isOpenModal } = useSelector(state => state.laancState);
//
const [startDate, setStartDate] = useState(dayjs().format('YYYY-MM-DD'));
const [endDate, setEndDate] = useState();
// 미니맵 레이어
const [previewLayer, setPreviewLayer] = useState();
const { laancAprvList } = useSelector(state => state.laancState);
const map = useSelector(state => state.mapState.map);
const previewGeo2 = {
type: 'FeatureCollection',
features: []
};
useEffect(() => {
const searchStDt = dayjs().format('YYYY-MM-DD');
const searchEndDt = dayjs().format('YYYY-MM-DD');
dispatch(
getLaancAprvList({
searchStDt,
searchEndDt
})
);
}, []);
useEffect(() => {
if (areaCoordList.length != 0) {
handlerPreviewDraw();
}
}, [areaCoordList]);
useEffect(() => {
if (map) {
setMapObject(map);
}
}, [map]);
useEffect(async () => {
if (areaCoordList.length === 0) return;
}, [areaCoordList]);
const handlerSearch = (search, searchDate, filterArea) => {
setStartDate(searchDate.startDate);
setEndDate(searchDate.endDate);
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
})
);
}
// );
setFilter(search);
};
const handlerDetail = area => {
setSelected(area.planAreaSno);
dispatch(clientSaveAreaCoordinateList([area]));
handlerMapInit();
};
const handlerMapInit = () => {
if (map.getSource('preview')) {
} else {
map.addSource('preview', {
type: 'geojson',
data: previewGeo2
});
map.addLayer(flightlayerWayPoint('preview'));
map.addLayer(flightlayerBuffer('preview'));
map.addLayer(flightlayerPolygon('preview'));
map.addLayer(flightlayerPolyline('preview'));
}
dispatch(clientSetIsMapLoading(true));
const preview = map.getSource('preview');
if (preview) setPreviewLayer(preview);
setIsMapLoading(true);
setMapObject(map);
dispatch(clientMapInit(map));
};
const handlerPreviewDraw = () => {
if (areaCoordList.length > 0) {
const areas = areaCoordList[0];
previewGeo2.features = [];
let fitZoomPaths = [];
const radius = areas.bufferZone;
const circleCoords = handlerGetCircleCoord(
[areas.lon, areas.lat],
radius
);
const circle = InitFeature('Polygon', 'CIRCLE');
circle.properties.center = [areas.lon, areas.lat];
circle.geometry.coordinates = circleCoords;
previewGeo2.features.push(circle);
mapObject.setCenter(circle.properties.center);
fitZoomPaths = circleCoords[0];
handlerFitBounds(mapObject, fitZoomPaths, 400, 'CIRCLE', 'flight');
// mapObject.setPaintProperty('waypoint', 'circle-radius', 10);
mapObject.getSource('preview').setData(previewGeo2);
}
};
const handlerLogout = async () => {
const { payload } = await dispatch(setLogout());
if (payload === 'SUCCESS') {
history.replace('/account/login');
}
};
return (
<>
<div className='map' style={{ width: '100%' }}>
<MapControl />
</div>
<div className='right-menu active'>
<div className='right-layer active flight-approval-layer'>
<div className='layer-content'>
<NewFlightApprovalsReport handlerSearch={handlerSearch} />
<NewFlightApprovalsTable
filter={filter}
startDate={startDate}
endDate={endDate}
selected={selected}
handlerDetail={handlerDetail}
/>
</div>
</div>
</div>
</>
);
}

28
src/redux/features/laanc/laancState.ts

@ -641,22 +641,18 @@ export interface ILaancAprvListRs {
schFltEndDt: string;
updateDt: string;
createDt: string;
areaList: [
{
planAreaSno: number;
planSno: number;
zoneNo: string;
bufferZone: number;
fltElev: number;
areaList: {
planAreaSno: number;
planSno: number;
zoneNo: string;
bufferZone: number;
fltElev: number;
lat: number;
lon: number;
approvalCd: string;
bufferCoordList: {
lat: number;
lon: number;
approvalCd: string;
bufferCoordList: [
{
lat: number;
lon: number;
}
];
}
];
}[];
}[];
}

11
src/router/routes/RouteFlight.js

@ -2,8 +2,17 @@ import { lazy } from 'react';
const RouteFlight = [
{
path: '/analysis/history/list',
path: '/flight/approvals',
component: lazy(() => import('../../views/flight/FlightView')),
meta: {
authRoute: true
}
},
{
path: '/flight/approvals/new',
component: lazy(() => import('../../views/flight/NewFlightView')),
layout: 'BlankLayout',
meta: {
authRoute: true
}

15
src/views/flight/NewFlightView.js

@ -0,0 +1,15 @@
import '@styles/react/libs/flatpickr/flatpickr.scss';
import '@styles/react/libs/tables/react-dataTable-component.scss';
import '../../assets/css/custom.css';
import NewFlightApprovalsContainer from '../../containers/flight/NewFlightApprovalsContainer';
export default function NewFlightView() {
return (
<div className='pal-container'>
{/* <Helmet>
<title>관제시스템</title>
</Helmet> */}
<NewFlightApprovalsContainer />;
</div>
);
}
Loading…
Cancel
Save