Browse Source

Merge pull request 'LAANC-QR-CODE' (#2) from LAANC-QR-CODE into master

Reviewed-on: pav/pav-fe-kac#2
pull/1/head
박상현 9 months ago
parent
commit
e53daafd31
  1. 127
      src/components/laanc/LaancQr.js
  2. 3
      src/components/laanc/list/LaancGrid.js
  3. 11
      src/components/laanc/list/LaancSearch.js
  4. 572
      src/components/laanc/step/LaancStep1.js
  5. 3
      src/containers/analysis/simulator/AnalysisSimulationContainer.js
  6. 11
      src/containers/laanc/LaancContainer.js
  7. 449
      src/containers/laanc/LaancPlanContainer.js
  8. 17
      src/modules/laanc/actions/laancActions.ts
  9. 5
      src/modules/laanc/apis/laancApi.ts
  10. 11
      src/modules/laanc/models/laancModels.ts
  11. 6
      src/modules/laanc/reducers/laancReducers.ts
  12. 17
      src/modules/laanc/sagas/laancSagas.ts

127
src/components/laanc/LaancQr.js

@ -0,0 +1,127 @@
import { useEffect, useState, useRef } from 'react';
import { ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { ErrorModal } from '../../components/modal/ErrorModal';
import axios from '../../modules/utils/customAxiosUtil';
import { debounce } from 'lodash';
// 이제 handleUserEvent는 300ms 동안 추가 호출이 없을 때만 실행됩니다.
export default function LaancQr({ isPopUp, setIsPopUp, data, handlerStep }) {
const [isPolling, setIsPolling] = useState(true);
const [isErrorModal, setIsErrorModal] = useState({
isOpen: false,
title: '',
desc: ''
});
const pollingIntervalRef = useRef(null);
// 언마운트시 폴링 중지
useEffect(() => {
return () => {
stopPolling();
};
}, []);
// 폴링 로직
useEffect(() => {
if (isPolling) {
startPolling();
} else {
stopPolling();
}
}, [isPolling]);
// 폴링 시작
const startPolling = () => {
pollingIntervalRef.current = setInterval(axiosData, 3000);
};
// 폴링 중지
const stopPolling = () => {
clearInterval(pollingIntervalRef.current);
};
// QR 인증 폴링
const axiosData = async () => {
try {
const res = await axios.get(`api/bas/laanc/ts/qr/${data.confirmKey}`);
handleResponse(res, 'polling');
} catch (error) {
handleError(error);
}
};
// axios 호출 처리 로직
const handleResponse = (res, type) => {
if (res.data.result === true) {
// dispatch(LaancAction.LAANC_TS_QR.success(res.data));
setIsPolling(false);
setIsPopUp(false);
handlerStep(2);
} else if (res.data.result === '시간 만료') {
setIsErrorModal({
isOpen: true,
title: '인증 만료',
desc: <>인증 시간이 만료되었습니다.</>
});
setIsPopUp(false);
} else if (type === 'user') {
// setIsPolling(true);
startPolling();
}
};
// axios 호출 에러 처리 로직
const handleError = error => {
console.log('>>', error);
setIsErrorModal({
isOpen: true,
title: '오류',
desc: <>처리중 오류가 발생하였습니다</>
});
};
// 사용자 확인 버튼 헨들러
const handleUserEvent = debounce(async event => {
stopPolling();
try {
const res = await axios.get(`api/bas/laanc/ts/qr/${data.confirmKey}`);
handleResponse(res, 'user');
} catch (error) {
handleError(error);
}
}, 3000);
return (
<>
<ModalHeader
toggle={() => {
setIsPopUp(!isPopUp);
}}
>
QR인증
</ModalHeader>
<ModalBody
className='notam-info'
style={{
display: 'flex',
justifyContent: 'center',
height: '18vw'
}}
>
<img src={data?.qrcode} alt='QR Code' />
</ModalBody>
<ModalFooter>
<Button
color='primary'
// onClick={() => {
// setIsPopUp(!isPopUp);
// }}
onClick={handleUserEvent}
>
확인
</Button>
</ModalFooter>
<ErrorModal modal={isErrorModal} setModal={setIsErrorModal} />
</>
);
}

3
src/components/laanc/list/LaancGrid.js

@ -1,10 +1,9 @@
import { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Document, Page, pdfjs } from 'react-pdf';
import { pdfjs } from 'react-pdf';
import { GridDatabase } from '@src/components/crud/grid/GridDatatable';
import { Row, Col, Card, Button, Spinner, Modal } from 'reactstrap';
import * as LaancAction from '../../../modules/laanc/actions/laancActions';
import LaancStep2 from '../step/LaancStep2';
import moment from 'moment';
import {
AREA_COORDINATE_LIST_SAVE,

11
src/components/laanc/list/LaancSearch.js

@ -6,7 +6,7 @@ import Flatpickr from 'react-flatpickr';
import moment from 'moment';
import * as LaancAction from '../../../modules/laanc/actions/laancActions';
function LaancSearch() {
function LaancSearch({ isSearch }) {
const dispatch = useDispatch();
// 날짜 데이터
const [date, setDate] = useState({
@ -19,7 +19,14 @@ function LaancSearch() {
dispatch(LaancAction.LAANC_APRV_LIST.request({ ...date }));
}, []);
// 날짜 변경 헨들러
// Laanc 승인 신청시 검색
useEffect(() => {
if (isSearch) {
dispatch(LaancAction.LAANC_APRV_LIST.request({ ...date }));
}
}, [isSearch]);
// 날짜 선택 헨들러
const handlerChangeDate = selectedDates => {
if (selectedDates.length === 2) {
const createStDate = moment(selectedDates[0]).format('YYYY-MM-DD');

572
src/components/laanc/step/LaancStep1.js

@ -9,6 +9,9 @@ import FlightArea from '../map/FlightArea';
import { ErrorModal } from '../../modal/ErrorModal';
import { InfoModal } from '../../modal/InfoModal';
import { LaancModal } from '../LaancModal';
import { FLIGHT_PLAN_AREA_BUFFER_LIST } from '../../../modules/basis/flight/actions/basisFlightAction';
import LaancQr from '../../../components/laanc/LaancQr';
import axios from '../../../modules/utils/customAxiosUtil';
import moment from 'moment';
import {
Row,
@ -21,18 +24,18 @@ import {
UncontrolledPopover,
PopoverBody,
Label,
Input
Input,
Modal
} from 'reactstrap';
export default function LaancStep1({
handleChange,
handlerNext,
data,
detailData,
setDetailData,
centeredModal,
setCenteredModal,
handlerStep,
currentParm,
handlerLaancClose,
handlerBufferApply
handlerLaancClose
}) {
const dispatch = useDispatch();
@ -56,6 +59,10 @@ export default function LaancStep1({
const queryParams = new URLSearchParams(location.search);
const mapParam = queryParams.get('map');
// qr 인증 데이터
const [qrData, setQrData] = useState();
// qr 팝업
const [isPopUp, setIsPopUp] = useState(false);
// 아이콘 팝오버
const [popoverCommercial, setPopoverCommercial] = useState(false);
const [popoverSchFltStDt, setPopoverSchFltStDt] = useState(false);
@ -80,8 +87,8 @@ export default function LaancStep1({
url: ''
});
// URL 쿼리 파라미터 중 'map' 값을 가져옵니다.
useEffect(() => {
// URL 쿼리 파라미터 중 'map' 값을 가져옵니다.
if (!currentParm) setCenteredModal(mapParam != 'true' ? false : true);
}, [location]);
@ -99,6 +106,330 @@ export default function LaancStep1({
}
}, [areaCoordList]);
// 적용 버튼 Reducer 업데이트 될때마다 검사 로직
useEffect(() => {
if (detailData.areaList[0].fltElev != 0) {
const maxElev = 150;
const controlledAltitudeExceededWarning =
laancArea?.duplicated &&
parseInt(
detailData.areaList[0].fltElev.replace('/^0+/', 'm', ''),
10
) >= laancElev[0] &&
parseInt(detailData.areaList[0].fltElev.replace('/^0+/', 'm', ''), 10) <
maxElev;
if (controlledAltitudeExceededWarning) {
setIsErrorModal({
isOpen: true,
title: '검토 결과 사전안내',
desc: (
<>
유효성 검사에 실패하여 승인 대상입니다.
<br />
제출하신 비행계획서의 고도는 {laancElev[0]}m이하에서만 비행이
가능합니다.
<br />
고도 설정을 다시 확인해주시기 바랍니다.
</>
)
});
handleChange({
type: 'area',
name: 'fltElev',
value: 0
});
}
}
}, [[laancElev]]);
// 비행계획서 작성 핸들러
const handleChange = ({ name, value, type, index, pIndex }) => {
const arrName = `${type}List`;
switch (type) {
case 'coord':
setDetailData(prevState => {
return {
...prevState,
areaList: [
{
...prevState.areaList[0],
coordList: value
}
]
};
});
break;
case 'area':
if (name === 'fltMethod' && value != '직접입력') {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updatedetailData = {
...prevState[arrName][0],
[name]: value,
fltMothoeRm: ''
};
arr[0] = updatedetailData;
return {
...prevState,
[arrName]: arr
};
});
} else if (
detailData.areaList[0].areaType === 'LINE' ||
name === 'bufferZone'
) {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const prevBufferZone = prevState[arrName][0].bufferZone;
const updatedetailData = {
...prevState[arrName][0],
[name]: value,
concatBufferZone: prevBufferZone
};
arr[0] = updatedetailData;
return {
...prevState,
[arrName]: arr
};
});
} else {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updatedetailData = {
...prevState[arrName][0],
[name]: value
};
arr[0] = updatedetailData;
return {
...prevState,
[arrName]: arr
};
});
}
break;
case 'pilot':
case 'arcrft':
{
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updatedetailData = {
...prevState[arrName][0],
[name]: value
};
arr[0] = updatedetailData;
return {
...prevState,
[arrName]: arr
};
});
}
break;
case 'plan':
default:
setDetailData(prevState => ({
...prevState,
[name]: value
}));
break;
}
};
// 스텝 1 다음 버튼 이벤트
const handlerNext = () => {
// 시작일자
const schFltStDt = moment(detailData.schFltStDt, 'YYYY-MM-DD HH:mm:ss');
// 종료일자
const schFltEndDt = moment(detailData.schFltEndDt, 'YYYY-MM-DD HH:mm:ss');
const currentDate = moment(); // 현재 날짜와 시간을 가져옵니다.
const validateAircraftWeightCode =
!detailData.arcrftList[0].arcrftTypeCd &&
(detailData.commercial === 'COMMERCIAL' ||
detailData.arcrftList[0].arcrftWghtCd == '9' ||
detailData.arcrftList[0].arcrftWghtCd == '10' ||
detailData.arcrftList[0].arcrftWghtCd == '11');
const validateidntfNumCode =
!detailData.arcrftList[0].idntfNum &&
(detailData.commercial === 'COMMERCIAL' ||
detailData.arcrftList[0].arcrftWghtCd == '9' ||
detailData.arcrftList[0].arcrftWghtCd == '10' ||
detailData.arcrftList[0].arcrftWghtCd == '11');
if (!detailData.fltType) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행 종류(상업/비상업)를 선택해주세요.'
});
return false;
} else if (
!schFltStDt.isAfter(currentDate) ||
!schFltEndDt.isAfter(currentDate)
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행 일자가 이미 지난 일자입니다.'
});
return false;
} else if (schFltStDt.isAfter(schFltEndDt)) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행일자를 확인해주세요.'
});
return false;
} else if (schFltStDt.format('A h:mm') === 'PM 11:00') {
setIsErrorModal({
isOpen: true,
title: '특별 비행',
desc: (
<>
야간 비행은 특별 비행에 해당됩니다.
<br />
특별 비행의 경우 드론원스톱을 통해서 신청해주시기 바랍니다.
</>
)
});
return false;
} else if (schFltStDt.format('A h:mm') === 'PM 5:00') {
setIsErrorModal({
isOpen: true,
title: '비행구역 및 비행일자 중복',
desc: (
<>
설정하신 비행구역 비행시간에 이미 승인완료된 신청건이 있습니다.
<br /> 다시 설정 부탁드립니다.
</>
)
});
return false;
} else if (!detailData.fltPurpose) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행목적을 선택해 주세요.'
});
return false;
} else if (
!detailData.areaList[0].fltElev ||
detailData.areaList[0].fltElev === 0
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '고도를 입력해 주세요.'
});
return false;
} else if (!detailData.areaList[0].bufferZone) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '반경을 입력해 주세요.'
});
return false;
} else if (
detailData.areaList[0].concatBufferZone !=
detailData.areaList[0].bufferZone &&
detailData.areaList[0].areaType === 'LINE'
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: <>적용 버튼을 누르지 않고 값을 변경 없습니다.</>
});
} else if (!detailData.areaList[0].fltMethod) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행방식를 입력해 주세요.'
});
return false;
} else if (
detailData.areaList[0].fltMethod === '00' &&
!detailData.areaList[0].fltMothoeRm
) {
// 비행 방식 직접 입력칸 활성화 후 작성 시 조건문
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행방식을 입력해 주세요.'
});
return false;
} else if (validateAircraftWeightCode) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '기체 종류를 입력해 주세요.'
});
return false;
} else if (validateidntfNumCode) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '기체 신고 번호를 입력해 주세요.'
});
return false;
} else {
handlerLaanc();
}
};
// 비행 구역 적용 버튼 핸들러
const handlerBufferApply = async () => {
if (areaCoordList) {
if (areaCoordList[0].coordList.length > 0) {
dispatch(LaancAction.LAANC_VALID_AREA.request(detailData.areaList));
const array = [];
const copy = { ...areaCoordList[0] };
copy.bufferZone = detailData.areaList[0].bufferZone;
array.push(copy);
dispatch(FLIGHT_PLAN_AREA_BUFFER_LIST.request(array));
try {
const elev = await axios.post(
`api/bas/laanc/valid/elev`,
detailData.areaList
);
if (elev.data[0] === 0) {
setIsErrorModal({
title: '비행 불가 지역',
desc: (
<>
설정하신 비행구역 허용고도가 0m인 구역이 있습니다.
<br />
버퍼존을 다시 확인해주시기 바랍니다.
</>
),
isOpen: true
});
}
dispatch(LaancAction.LAANC_ALTITUDE.success(elev.data));
} catch (error) {
{
setIsErrorModal({
isOpen: true,
title: '오류',
desc: '처리중 오류가 발생하였습니다'
});
}
}
}
}
};
// Input 요소가 포커스될 때 커서를 맨 뒤로 이동
const handleInputClick = type => {
switch (type) {
@ -106,7 +437,7 @@ export default function LaancStep1({
const drawFlightZone =
fltElevRef.current &&
type === 'fltElev' &&
data.areaList[0].coordList[0].lat != 0;
detailData.areaList[0].coordList[0].lat != 0;
if (drawFlightZone) {
const input = fltElevRef.current;
@ -114,7 +445,7 @@ export default function LaancStep1({
input.setSelectionRange(inputValue.length - 1, inputValue.length - 1);
input.focus();
} else if (data.areaList[0].coordList[0].lat === 0) {
} else if (detailData.areaList[0].coordList[0].lat === 0) {
fltElevRef.current.blur();
setIsErrorModal({
isOpen: true,
@ -138,25 +469,26 @@ export default function LaancStep1({
// 비사업 클릭시 기존 값 초기화 작업
const initialValue = () => {
if (data.arcrftList[0].idntfNum) {
if (detailData.arcrftList[0].idntfNum) {
handleChange({
type: 'arcrft',
name: 'idntfNum',
value: ''
});
}
if (data.arcrftList[0].arcrftTypeCd) {
if (detailData.arcrftList[0].arcrftTypeCd) {
handleChange({
type: 'arcrft',
name: 'arcrftTypeCd',
value: ''
});
}
return;
};
// 날짜 선택 핸들러
const handleOpenFlatpickr = () => {
if (data.areaList[0].coordList[0].lat === 0) {
if (detailData.areaList[0].coordList[0].lat === 0) {
setIsErrorModal({
isOpen: true,
title: '비행 구역 설정',
@ -167,7 +499,7 @@ export default function LaancStep1({
}
};
// 고도 150 미만 핸들러
// 고도, 비행방식 핸들러
const handleBlur = (value, type) => {
const maxElev = 150;
@ -181,7 +513,7 @@ export default function LaancStep1({
case 'fltElev':
if (
parseInt(value.replace('/^0+/', 'm', ''), 10) > maxElev &&
data.areaList[0].coordList[0].lat != 0
detailData.areaList[0].coordList[0].lat != 0
) {
handleChange({
type: 'area',
@ -259,12 +591,87 @@ export default function LaancStep1({
url: 'https://drone.onestop.go.kr/introduce/systemintro3 '
});
}
}
};
// laanc 승인 api 200 시 step 이동
const handlerLaanc = async () => {
if (laancArea && laancElev[0]) {
// laanc 필요 없이 날 수 있음
const laancNotRequired =
!laancArea.duplicated &&
detailData.fltType != 'COMMERCIAL' &&
detailData.arcrftList[0].arcrftWghtCd != '11';
const maxElev = 150;
if (laancNotRequired) {
setIsErrorModal({
isOpen: true,
title: '검토 결과 사전안내',
desc: (
<>
검토 결과 승인 대상입니다.
<p>
제줄하신 비행계획서는 별도의 승인이 필요없습니다.
<br />
조종자 준수사항에 유의하여 비행하시기 바랍니다.
</p>
</>
)
});
return;
} else if (detailData.areaList[0].fltMethod === '군집비행') {
handleChange({
type: 'area',
name: 'fltMethod',
value: ''
});
setIsLaancModal({
isOpen: true,
title: '군집 비행 목적',
desc: (
<>
군집 비행의 경우 담당자와 협의가 필요합니다. <br />
아래 링크를 통해 담당자와 협의 부탁드립니다.
</>
),
type: '처리부서안내 바로가기',
url: 'https://drone.onestop.go.kr/introduce/systemintro3 '
});
} else if (
parseInt(detailData.areaList[0].fltElev) <= laancElev[0] &&
parseInt(detailData.areaList[0].fltElev) < maxElev
) {
try {
// 성공적으로 응답 받았을 때 처리할 내용 추가
const tsData = await axios.get(
detailData.arcrftList[0].idntfNum
? `api/bas/laanc/ts/qr?idntfNum=${detailData.arcrftList[0].idntfNum}`
: `api/bas/laanc/ts/qr`
);
// dispatch(
// LaancAction.LAANC_TS_QR.success({
// confirmKey: tsData.confirmKey,
// qrcode: `data:image/png;base64,${tsData.qrcode}`
// })
// );
setQrData({
confirmKey: tsData.data.confirmKey,
qrcode: `data:image/png;base64,${tsData.data.qrcode}`
});
// handleChange({
// type: 'area',
// name: 'bufferZone',
// value: value
// });
setIsPopUp(true);
return;
} catch (error) {
console.log('>>', error);
setIsErrorModal({
isOpen: true,
title: '오류',
desc: <>처리중 오류가 발생하였습니다</>
});
}
}
}
};
@ -318,7 +725,7 @@ export default function LaancStep1({
</FormGroup>
</Col>
<Col className='list-input' md='6'>
<FormGroup>
<a>
<div className='ti'>비행 유형</div>
<Label for='test' className='pal-popover'>
<span className='necessary'>*</span>
@ -349,7 +756,7 @@ export default function LaancStep1({
bsSize='sm'
name='fltType'
id='fltType'
value={data.fltType}
value={detailData.fltType}
onChange={e => {
const { name, value } = e.target;
handleChange({
@ -363,7 +770,7 @@ export default function LaancStep1({
<option value='COMMERCIAL'>사업</option>
<option value='NON_COMMERCIAL'>비사업</option>
</Input>
</FormGroup>
</a>
</Col>
<Col className='list-input' md='12'>
<div className='ti'>비행 계획 정보</div>
@ -399,11 +806,13 @@ export default function LaancStep1({
id='schFltStDt'
name='schFltStDt'
data-enable-time
defaultValue={data.schFltStDt}
value={data.schFltStDt}
defaultValue={detailData.schFltStDt}
value={detailData.schFltStDt}
ref={schFltStDtRef}
onFocus={() => handleOpenFlatpickr()}
options={{
enableTime: true,
time_24hr: true,
minDate: moment().format('YYYY-MM-DD'),
maxDate: moment().add(90, 'day').format('YYYY-MM-DD')
}}
@ -415,21 +824,23 @@ export default function LaancStep1({
value
});
if (laancSun.length > 0) {
const filteredData = laancSun.filter(data => {
const dataDateTime = moment(
data.locDate,
'YYYYMMDD'
);
return dataDateTime.isSame(
moment(value, 'YYYYMMDD')
);
});
const filtereddetailData = laancSun.filter(
detailData => {
const detailDataDateTime = moment(
detailData.locDate,
'YYYYMMDD'
);
return detailDataDateTime.isSame(
moment(value, 'YYYYMMDD')
);
}
);
const schFltStDt = moment(value).format('HHmmss');
filteredData.forEach(data => {
filtereddetailData.forEach(detailData => {
if (
schFltStDt <= data.civilm ||
schFltStDt >= data.civile
schFltStDt <= detailData.civilm ||
schFltStDt >= detailData.civile
) {
setIsLaancModal({
isOpen: true,
@ -448,9 +859,9 @@ export default function LaancStep1({
handleChange({
name: 'schFltStDt',
value:
schFltStDt <= data.civilm ||
schFltStDt >= data.civile
? moment(data.civilm, 'HHmmss')
schFltStDt <= detailData.civilm ||
schFltStDt >= detailData.civile
? moment(detailData.civilm, 'HHmmss')
.add(5, 'minute')
.format('YYYY-MM-DD HH:mm:ss')
: moment()
@ -497,11 +908,13 @@ export default function LaancStep1({
id='schFltEndDt'
name='schFltEndDt'
data-enable-time
defaultValue={data.schFltEndDt}
defaultValue={detailData.schFltEndDt}
ref={schFltEndDtRef}
value={data.schFltEndDt}
value={detailData.schFltEndDt}
onFocus={handleOpenFlatpickr}
options={{
enableTime: true,
time_24hr: true,
minDate: moment().format('YYYY-MM-DD'),
maxDate: moment().add(6, 'month').format('YYYY-MM-DD')
}}
@ -513,21 +926,23 @@ export default function LaancStep1({
value
});
if (laancSun.length > 0) {
const filteredData = laancSun.filter(data => {
const dataDateTime = moment(
data.locDate,
'YYYYMMDD'
);
return dataDateTime.isSame(
moment(value, 'YYYYMMDD')
);
});
const filtereddetailData = laancSun.filter(
detailData => {
const detailDataDateTime = moment(
detailData.locDate,
'YYYYMMDD'
);
return detailDataDateTime.isSame(
moment(value, 'YYYYMMDD')
);
}
);
const schFltEndDt = moment(value).format('HHmmss');
filteredData.forEach(data => {
filtereddetailData.forEach(detailData => {
if (
schFltEndDt <= data.civilm ||
schFltEndDt >= data.civile
schFltEndDt <= detailData.civilm ||
schFltEndDt >= detailData.civile
) {
setIsLaancModal({
isOpen: true,
@ -546,9 +961,9 @@ export default function LaancStep1({
handleChange({
name: 'schFltEndDt',
value:
schFltEndDt <= data.civilm ||
schFltEndDt >= data.civile
? moment(data.civile, 'HHmmss')
schFltEndDt <= detailData.civilm ||
schFltEndDt >= detailData.civile
? moment(detailData.civile, 'HHmmss')
.add(-5, 'minute')
.format('YYYY-MM-DD HH:mm:ss')
: moment()
@ -573,7 +988,7 @@ export default function LaancStep1({
type='select'
id='fltPurpose'
name='fltPurpose'
value={data.fltPurpose}
value={detailData.fltPurpose}
bsSize='sm'
onChange={e => {
const { name, value } = e.target;
@ -634,8 +1049,8 @@ export default function LaancStep1({
type='text'
id='fltElev'
name='fltElev'
// defaultValue={data.email || ''}
value={data.areaList[0].fltElev + 'm'}
// defaultValue={detailData.email || ''}
value={detailData.areaList[0].fltElev + 'm'}
bsSize='sm'
onBlur={e => handleBlur(e.target.value, 'fltElev')}
onChange={e => {
@ -665,8 +1080,8 @@ export default function LaancStep1({
type='text'
id='bufferZone'
name='bufferZone'
// defaultValue={data.email || ''}
value={data.areaList[0].bufferZone + 'm'}
// defaultValue={detailData.email || ''}
value={detailData.areaList[0].bufferZone + 'm'}
bsSize='sm'
onChange={e => {
const { name, value } = e.target;
@ -705,7 +1120,7 @@ export default function LaancStep1({
id='fltMethod'
name='fltMethod'
onBlur={e => handleBlur(e.target.value, 'fltMethod')}
value={data.areaList[0].fltMethod}
value={detailData.areaList[0].fltMethod}
bsSize='sm'
onChange={e => {
const { name, value } = e.target;
@ -751,10 +1166,10 @@ export default function LaancStep1({
value
});
}}
value={data.areaList[0].fltMothoeRm}
value={detailData.areaList[0].fltMothoeRm}
placeholder='직접입력 선택 후 활성화'
disabled={
data.areaList[0].fltMethod === '00' ? false : true
detailData.areaList[0].fltMethod === '00' ? false : true
}
/>
</FormGroup>
@ -775,7 +1190,7 @@ export default function LaancStep1({
name='arcrftWghtCd'
bsSize='sm'
placeholder=''
value={data.arcrftList[0].arcrftWghtCd}
value={detailData.arcrftList[0].arcrftWghtCd}
onChange={e => {
const { name, value } = e.target;
handleChange({
@ -794,10 +1209,10 @@ export default function LaancStep1({
</Input>
</FormGroup>
</Col>
{data.fltType === 'COMMERCIAL' ||
data.arcrftList[0].arcrftWghtCd == '11' ||
data.arcrftList[0].arcrftWghtCd == '10' ||
data.arcrftList[0].arcrftWghtCd == '9' ? (
{detailData.fltType === 'COMMERCIAL' ||
detailData.arcrftList[0].arcrftWghtCd == '11' ||
detailData.arcrftList[0].arcrftWghtCd == '10' ||
detailData.arcrftList[0].arcrftWghtCd == '9' ? (
<>
<Col className='list-input' md='4'>
<FormGroup>
@ -808,7 +1223,7 @@ export default function LaancStep1({
type='select'
id='arcrftTypeCd'
name='arcrftTypeCd'
value={data.arcrftList[0].arcrftTypeCd}
value={detailData.arcrftList[0].arcrftTypeCd}
bsSize='sm'
onChange={e => {
const { name, value } = e.target;
@ -836,7 +1251,7 @@ export default function LaancStep1({
type='text'
id='idntfNum'
name='idntfNum'
value={data.arcrftList[0].idntfNum}
value={detailData.arcrftList[0].idntfNum}
bsSize='sm'
onChange={e => {
const { name, value } = e.target;
@ -870,7 +1285,7 @@ export default function LaancStep1({
centeredModal={centeredModal}
setCenteredModal={setCenteredModal}
handleChange={handleChange}
data={data}
detailData={detailData}
page={1}
/>
</div>
@ -920,6 +1335,19 @@ export default function LaancStep1({
다음
</Button>
</ModalFooter>
<Modal
isOpen={isPopUp}
toggle={() => setIsPopUp(!isPopUp)}
className='modal-dialog-centered modal-lg notam-modal'
style={{ height: '25vh', width: '25vw' }}
>
<LaancQr
isPopUp={isPopUp}
setIsPopUp={setIsPopUp}
data={qrData}
handlerStep={handlerStep}
/>
</Modal>
<ErrorModal modal={isErrorModal} setModal={setIsErrorModal} />
<InfoModal modal={isInfoModal} setModal={setIsInfoModal} />
<LaancModal modal={isLaancModal} setModal={setIsLaancModal} />

3
src/containers/analysis/simulator/AnalysisSimulationContainer.js

@ -65,8 +65,9 @@ export const AnalysisSimulationContainer = props => {
// 드론 갯수
const [dronLength, setDronLength] = useState(0);
// 비행 시간 카운터
const [countArray, setCountArray] = useState([]);
// 검색 데이터
const [params, setParams] = useState({
stDate: moment().subtract(1, 'day').format('YYYY-MM-DD'),
endDate: moment().subtract(0, 'day').format('YYYY-MM-DD'),

11
src/containers/laanc/LaancContainer.js

@ -17,6 +17,8 @@ export default function LaancContainer() {
const [currentParm, setCurrentParm] = useState(false);
//LAANC 신청하기 모달
const [disabledAnimation, setDisabledAnimation] = useState(false);
// laanc 신청시 자동 검색
const [isSearch, setIsSearch] = useState(false);
// 마운트 시 지도 표출 여부
const location = useLocation();
@ -32,6 +34,13 @@ export default function LaancContainer() {
setDisabledAnimation(mapParam != 'true' ? false : true);
}, [location]);
// Laanc 신청 이후 자동 검색
useEffect(() => {
if (disabledAnimation) {
setIsSearch(false);
} else setIsSearch(true);
}, [disabledAnimation]);
// LAANC 신청하기 버튼 클릭 헨들러
const handleApply = () => {
dispatch(drawTypeChangeAction(''));
@ -68,7 +77,7 @@ export default function LaancContainer() {
) : null}
</div>
<LaancSearch />
<LaancSearch isSearch={isSearch} />
<LaancGrid />
</CustomMainLayout>
);

449
src/containers/laanc/LaancPlanContainer.js

@ -2,20 +2,13 @@ import { useEffect, useState } from 'react';
import LaancStep1 from '../../components/laanc/step/LaancStep1'; // laanc step 1
import LaancStep2 from '../../components/laanc/step/LaancStep2'; // laanc step 2
import LaancStep3 from '../../components/laanc/step/LaacnStep3'; // laanc step 3
import moment from 'moment';
import { ErrorModal } from '../../components/modal/ErrorModal';
import { LaancModal } from '../../components/laanc/LaancModal';
import { initFlightBas } from '../../modules/laanc/models/laancModels';
import { Modal } from 'reactstrap';
import {
AREA_DETAIL_INIT,
FLIGHT_PLAN_AREA_BUFFER_LIST
} from '../../modules/basis/flight/actions/basisFlightAction';
import { AREA_DETAIL_INIT } from '../../modules/basis/flight/actions/basisFlightAction';
import { useDispatch, useSelector } from 'react-redux';
import { drawTypeChangeAction } from '../../modules/control/map/actions/controlMapActions';
import * as LaancAction from '../../modules/laanc/actions/laancActions';
import * as AreaAction from '../../modules/basis/flight/actions/basisFlightAction';
import axios from '../../modules/utils/customAxiosUtil';
export default function LaancPlanContainer({
currentParm,
@ -23,12 +16,9 @@ export default function LaancPlanContainer({
setDisabledAnimation
}) {
const dispatch = useDispatch();
// 비행 구역 정보
const { areaCoordList } = useSelector(state => state.flightState);
// 로그인 정보
const { user } = useSelector(state => state.authState);
// 관제권안 정보,고도 정보
const { laancArea, laancElev } = useSelector(state => state.laancState);
// laanc step
const [step, setStep] = useState(1);
// laanc 초기값
@ -64,435 +54,11 @@ export default function LaancPlanContainer({
};
}, []);
// 적용 버튼 Reducer 업데이트 될때마다 검사 로직
useEffect(() => {
if (detailData.areaList[0].fltElev != 0) {
const maxElev = 150;
const controlledAltitudeExceededWarning =
laancArea?.duplicated &&
parseInt(
detailData.areaList[0].fltElev.replace('/^0+/', 'm', ''),
10
) >= laancElev[0] &&
parseInt(detailData.areaList[0].fltElev.replace('/^0+/', 'm', ''), 10) <
maxElev;
if (controlledAltitudeExceededWarning) {
setIsErrorModal({
isOpen: true,
title: '검토 결과 사전안내',
desc: (
<>
유효성 검사에 실패하여 승인 대상입니다.
<br />
제출하신 비행계획서의 고도는 {laancElev[0]}m이하에서만 비행이
가능합니다.
<br />
고도 설정을 다시 확인해주시기 바랍니다.
</>
)
});
handleChange({
type: 'area',
name: 'fltElev',
value: 0
});
}
}
}, [[laancElev]]);
// laanc 승인 api 200 시 step 이동
const handlerLaanc = async () => {
if (laancArea && laancElev[0]) {
// laanc 필요 없이 날 수 있음
const laancNotRequired =
!laancArea.duplicated &&
detailData.fltType != 'COMMERCIAL' &&
detailData.arcrftList[0].arcrftWghtCd != '11';
const maxElev = 150;
if (laancNotRequired) {
setIsErrorModal({
isOpen: true,
title: '검토 결과 사전안내',
desc: (
<>
검토 결과 승인 대상입니다.
<p>
제줄하신 비행계획서는 별도의 승인이 필요없습니다.
<br />
조종자 준수사항에 유의하여 비행하시기 바랍니다.
</p>
</>
)
});
return;
} else if (detailData.areaList[0].fltMethod === '군집비행') {
handleChange({
type: 'area',
name: 'fltMethod',
value: ''
});
setIsLaancModal({
isOpen: true,
title: '군집 비행 목적',
desc: (
<>
군집 비행의 경우 담당자와 협의가 필요합니다. <br />
아래 링크를 통해 담당자와 협의 부탁드립니다.
</>
),
type: '처리부서안내 바로가기',
url: 'https://drone.onestop.go.kr/introduce/systemintro3 '
});
} else if (
parseInt(detailData.areaList[0].fltElev) <= laancElev[0] &&
parseInt(detailData.areaList[0].fltElev) < maxElev
) {
try {
// 성공적으로 응답 받았을 때 처리할 내용 추가
const tsData = await axios.post(`api/bas/laanc/valid/ts/pilot`, [
detailData.arcrftList[0].idntfNum
]);
if (!tsData.data.valid) {
setIsErrorModal({
isOpen: true,
title: '검토 결과 사전안내',
desc: (
<>
유효성 검사에 실패하여 승인 대상입니다.
<p>
기체가 보험에 가입되어 있지 않거나 유효기간이
만료되었습니다.
<br />
기체 번호를 다시 확인해주시기 바랍니다.
</p>
</>
)
});
return;
} else {
setStep(2);
}
} catch (error) {
setIsErrorModal({
isOpen: true,
title: '오류',
desc: <>처리중 오류가 발생하였습니다</>
});
}
}
}
};
// step 핸들러
const handlerStep = step => {
setStep(step);
};
// 날씨 핸들러
const handlerWeather = () => {
setFormModal(!formModal);
};
// 비행계획서 작성 핸들러
const handleChange = ({ name, value, type, index, pIndex }) => {
const arrName = `${type}List`;
switch (type) {
case 'coord':
setDetailData(prevState => {
return {
...prevState,
areaList: [
{
...prevState.areaList[0],
coordList: value
}
]
};
});
break;
case 'area':
if (name === 'fltMethod' && value != '직접입력') {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updateData = {
...prevState[arrName][0],
[name]: value,
fltMothoeRm: ''
};
arr[0] = updateData;
return {
...prevState,
[arrName]: arr
};
});
} else if (
detailData.areaList[0].areaType === 'LINE' ||
name === 'bufferZone'
) {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const prevBufferZone = prevState[arrName][0].bufferZone;
const updateData = {
...prevState[arrName][0],
[name]: value,
concatBufferZone: prevBufferZone
};
arr[0] = updateData;
return {
...prevState,
[arrName]: arr
};
});
} else {
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updateData = {
...prevState[arrName][0],
[name]: value
};
arr[0] = updateData;
return {
...prevState,
[arrName]: arr
};
});
}
break;
case 'pilot':
case 'arcrft':
{
setDetailData(prevState => {
const arr = [...prevState[arrName]];
const updateData = {
...prevState[arrName][0],
[name]: value
};
arr[0] = updateData;
return {
...prevState,
[arrName]: arr
};
});
}
break;
case 'plan':
default:
setDetailData(prevState => ({
...prevState,
[name]: value
}));
break;
}
};
// 스텝 1 다음 버튼 이벤트
const handlerNext = () => {
// 시작일자
const schFltStDt = moment(detailData.schFltStDt, 'YYYY-MM-DD HH:mm:ss');
// 종료일자
const schFltEndDt = moment(detailData.schFltEndDt, 'YYYY-MM-DD HH:mm:ss');
const currentDate = moment(); // 현재 날짜와 시간을 가져옵니다.
const validateAircraftWeightCode =
!detailData.arcrftList[0].arcrftTypeCd &&
(detailData.commercial === 'COMMERCIAL' ||
detailData.arcrftList[0].arcrftWghtCd == '9' ||
detailData.arcrftList[0].arcrftWghtCd == '10' ||
detailData.arcrftList[0].arcrftWghtCd == '11');
const validateidntfNumCode =
!detailData.arcrftList[0].idntfNum &&
(detailData.commercial === 'COMMERCIAL' ||
detailData.arcrftList[0].arcrftWghtCd == '9' ||
detailData.arcrftList[0].arcrftWghtCd == '10' ||
detailData.arcrftList[0].arcrftWghtCd == '11');
if (!detailData.fltType) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행 종류(상업/비상업)를 선택해주세요.'
});
return false;
} else if (
!schFltStDt.isAfter(currentDate) ||
!schFltEndDt.isAfter(currentDate)
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행 일자가 이미 지난 일자입니다.'
});
return false;
} else if (schFltStDt.isAfter(schFltEndDt)) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행일자를 확인해주세요.'
});
return false;
} else if (schFltStDt.format('A h:mm') === 'PM 11:00') {
setIsErrorModal({
isOpen: true,
title: '특별 비행',
desc: (
<>
야간 비행은 특별 비행에 해당됩니다.
<br />
특별 비행의 경우 드론원스톱을 통해서 신청해주시기 바랍니다.
</>
)
});
return false;
} else if (schFltStDt.format('A h:mm') === 'PM 5:00') {
setIsErrorModal({
isOpen: true,
title: '비행구역 및 비행일자 중복',
desc: (
<>
설정하신 비행구역 비행시간에 이미 승인완료된 신청건이 있습니다.
<br /> 다시 설정 부탁드립니다.
</>
)
});
return false;
} else if (!detailData.fltPurpose) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행목적을 선택해 주세요.'
});
return false;
} else if (
!detailData.areaList[0].fltElev ||
detailData.areaList[0].fltElev === 0
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '고도를 입력해 주세요.'
});
return false;
} else if (!detailData.areaList[0].bufferZone) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '반경을 입력해 주세요.'
});
return false;
} else if (
detailData.areaList[0].concatBufferZone !=
detailData.areaList[0].bufferZone &&
detailData.areaList[0].areaType === 'LINE'
) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: <>적용 버튼을 누르지 않고 값을 변경 없습니다.</>
});
// handleChange({
// type: 'area',
// name: 'bufferZone',
// value: detailData.areaList[0].concatBufferZone
// });
} else if (!detailData.areaList[0].fltMethod) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행방식를 입력해 주세요.'
});
return false;
} else if (
detailData.areaList[0].fltMethod === '00' &&
!detailData.areaList[0].fltMothoeRm
) {
// 비행 방식 직접 입력칸 활성화 후 작성 시 조건문
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '비행방식을 입력해 주세요.'
});
return false;
} else if (validateAircraftWeightCode) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '기체 종류를 입력해 주세요.'
});
return false;
} else if (validateidntfNumCode) {
setIsErrorModal({
isOpen: true,
title: '필수값 입력 오류',
desc: '기체 신고 번호를 입력해 주세요.'
});
return false;
} else {
handlerLaanc();
}
};
// 비행 구역 적용 버튼 핸들러
const handlerBufferApply = async () => {
if (areaCoordList) {
if (areaCoordList[0].coordList.length > 0) {
// dispatch(LaancAction.LAANC_ALTITUDE.request(detailData.areaList));
dispatch(LaancAction.LAANC_VALID_AREA.request(detailData.areaList));
const array = [];
const copy = { ...areaCoordList[0] };
copy.bufferZone = detailData.areaList[0].bufferZone;
array.push(copy);
dispatch(FLIGHT_PLAN_AREA_BUFFER_LIST.request(array));
try {
const elev = await axios.post(
`api/bas/laanc/valid/elev`,
detailData.areaList
);
if (elev.data[0] === 0) {
// dispatch(AREA_DETAIL_INIT());
// dispatch(AreaAction.AREA_DETAIL_INIT());
// dispatch(drawTypeChangeAction(''));
// dispatch(LaancAction.LAANC_APPROVAL_INIT());
setIsErrorModal({
title: '비행 불가 지역',
desc: (
<>
설정하신 비행구역 허용고도가 0m인 구역이 있습니다.
<br />
버퍼존을 다시 확인해주시기 바랍니다.
</>
),
isOpen: true
});
}
dispatch(LaancAction.LAANC_ALTITUDE.success(elev.data));
} catch (error) {
{
setIsErrorModal({
isOpen: true,
title: '오류',
desc: '처리중 오류가 발생하였습니다'
});
}
}
}
}
};
// Laanc 승인 요청 취소 버튼 헨들러
const handlerLaancClose = () => {
setStep(1);
@ -513,17 +79,13 @@ export default function LaancPlanContainer({
{step === 1 && (
<>
<LaancStep1
data={detailData}
handleChange={handleChange}
handlerNext={handlerNext}
handlerWeather={handlerWeather}
setDisabledAnimation={setDisabledAnimation}
disabledAnimation={disabledAnimation}
detailData={detailData}
setDetailData={setDetailData}
centeredModal={centeredModal}
setCenteredModal={setCenteredModal}
currentParm={currentParm}
handlerStep={handlerStep}
handlerLaancClose={handlerLaancClose}
handlerBufferApply={handlerBufferApply}
/>
</>
)}
@ -550,6 +112,7 @@ export default function LaancPlanContainer({
/>
)}
</Modal>
<ErrorModal modal={isErrorModal} setModal={setIsErrorModal} />
<LaancModal modal={isLaancModal} setModal={setIsLaancModal} />
</div>

17
src/modules/laanc/actions/laancActions.ts

@ -14,7 +14,8 @@ import {
FlightPlanAreaData,
VaildElevData,
VaildAreaData,
LaancTsData
LaancTsData,
LaancTsQrData
} from '../models/laancModels';
// laanc 비행계획서 승인
@ -57,11 +58,17 @@ const LAANC_VALID_TS_REQUEST = 'laanc/valid/ts/REQUEST';
const LAANC_VALID_TS_SUCCESS = 'laanc/valid/ts/SUCCESS';
const LAANC_VALID_TS_FAILURE = 'laanc/valid/ts/FAILURE';
// laanc ts qr
const LAANC_TS_QR_REQUEST = 'laanc/ts/qr/REQUEST';
const LAANC_TS_QR_SUCCESS = 'laanc/ts/qr/SUCCESS';
const LAANC_TS_QR_FAILURE = 'laanc/ts/qr/FAILURE';
// laanc 초기화
const INIT_LAANC = 'laanc/init';
// laanc approval detail 초기화
const INIT_APPROVAL_DETAIL = 'laanc/init/approval/detail';
// 허뎓 고도 초기화
// const INIT_ALTITUDE = 'laanc/init/altitude';
@ -142,6 +149,13 @@ export const LAANC_VALID_TS = createAsyncAction(
LAANC_VALID_TS_FAILURE
)<string, LaancTsData, AxiosError>();
// laanc ts qr
export const LAANC_TS_QR = createAsyncAction(
LAANC_TS_QR_REQUEST,
LAANC_TS_QR_SUCCESS,
LAANC_TS_QR_FAILURE
)<string, LaancTsQrData, AxiosError>();
const actions = {
LAANC_FLIGHT_Approval,
LAANC_FLIGHT_CREATE,
@ -152,6 +166,7 @@ const actions = {
LAANC_ALTITUDE,
LAANC_VALID_AREA,
LAANC_VALID_TS,
LAANC_TS_QR,
LAANC_APPROVAL_DETAIL_INIT
};
export type LaancAction = ActionType<typeof actions>;

5
src/modules/laanc/apis/laancApi.ts

@ -54,5 +54,10 @@ export const laancApi = {
postValidTs: async (data: string) => {
const res = await axios.post(`api/bas/laanc/valid/ts/pilot/${data}`);
return res;
},
// laanc ts qr
getTsQr: async (data: string) => {
const res = await axios.get(`api/bas/laanc/ts/qr/${data}`);
return res.data;
}
};

11
src/modules/laanc/models/laancModels.ts

@ -10,6 +10,7 @@ export interface laancState {
laancElev: number[] | undefined;
laancArea: VaildAreaData | undefined;
laancTs: LaancTsData | undefined;
laancQrData: LaancTsQrData | undefined;
}
// laanc계획서 초기값
@ -377,6 +378,7 @@ export const laancControlData = {
laancElev: undefined,
laancArea: undefined,
laancTs: undefined,
laancQrData: undefined,
detail: {
planSno: 0,
groupId: '',
@ -817,3 +819,12 @@ export interface LaancTsData {
];
valid: boolean;
}
// laanc Ts QR
export interface LaancTsQrData {
rspCode: string;
rspMessage: string;
arcrftinsuranceyn: string;
arcrftdeclaration: string;
corpregyn: string;
}

6
src/modules/laanc/reducers/laancReducers.ts

@ -83,6 +83,12 @@ export const laancReducer = createReducer<laancState, Actions.LaancAction>(
const data = action.payload;
draft.laancTs = data;
})
)
.handleAction(Actions.LAANC_TS_QR.success, (state, action) =>
produce(state, draft => {
const data = action.payload;
draft.laancQrData = data;
})
);
// .handleAction(Actions.LAANC_ALTITUDE_INIT, (state, action) =>
// produce(state, draft => {

17
src/modules/laanc/sagas/laancSagas.ts

@ -171,6 +171,23 @@ function* postValidTsSaga(
);
}
}
// laanc ts qr
function* getTsQrSaga(action: ActionType<typeof Actions.LAANC_TS_QR.request>) {
try {
const detail = action.payload;
const res = yield call(Apis.laancApi.getTsQr, detail);
// yield put(Actions.LAANC_TS_QR.success(res.data));
} catch (error) {
yield put(
MessageActions.IS_ERROR({
errorCode: ERROR_MESSAGE.code,
errorMessage: ERROR_MESSAGE.message,
isHistoryBack: false,
isRefresh: false
})
);
}
}
export function* laancSaga() {
yield takeEvery(Actions.LAANC_FLIGHT_Approval.request, postApprovalSaga);
yield debounce(500, Actions.LAANC_FLIGHT_CREATE.request, postCreateSaga);

Loading…
Cancel
Save