diff --git a/src/components/laanc/LaancQr.js b/src/components/laanc/LaancQr.js new file mode 100644 index 0000000..74af976 --- /dev/null +++ b/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 ( + <> + { + setIsPopUp(!isPopUp); + }} + > + QR인증 + + + QR Code + + + + + + + ); +} diff --git a/src/components/laanc/list/LaancGrid.js b/src/components/laanc/list/LaancGrid.js index 4a305bf..ca3b796 100644 --- a/src/components/laanc/list/LaancGrid.js +++ b/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, diff --git a/src/components/laanc/list/LaancSearch.js b/src/components/laanc/list/LaancSearch.js index 7a4edea..2e93a14 100644 --- a/src/components/laanc/list/LaancSearch.js +++ b/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'); diff --git a/src/components/laanc/step/LaancStep1.js b/src/components/laanc/step/LaancStep1.js index fb1e45d..a600128 100644 --- a/src/components/laanc/step/LaancStep1.js +++ b/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: ( + <> + 유효성 검사에 실패하여 미 승인 대상입니다. +
+ 제출하신 비행계획서의 고도는 {laancElev[0]}m이하에서만 비행이 + 가능합니다. +
+ 고도 설정을 다시 확인해주시기 바랍니다. + + ) + }); + 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: ( + <> + 야간 비행은 특별 비행에 해당됩니다. +
+ 특별 비행의 경우 드론원스톱을 통해서 신청해주시기 바랍니다. + + ) + }); + return false; + } else if (schFltStDt.format('A h:mm') === 'PM 5:00') { + setIsErrorModal({ + isOpen: true, + title: '비행구역 및 비행일자 중복', + desc: ( + <> + 설정하신 비행구역 및 비행시간에 이미 승인완료된 신청건이 있습니다. +
다시 설정 부탁드립니다. + + ) + }); + 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인 구역이 있습니다. +
+ 버퍼존을 다시 확인해주시기 바랍니다. + + ), + 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: ( + <> + 검토 결과 미 승인 대상입니다. +

+ 제줄하신 비행계획서는 별도의 승인이 필요없습니다. +
+ 조종자 준수사항에 유의하여 비행하시기 바랍니다. +

+ + ) + }); + return; + } else if (detailData.areaList[0].fltMethod === '군집비행') { + handleChange({ + type: 'area', + name: 'fltMethod', + value: '' + }); + setIsLaancModal({ + isOpen: true, + title: '군집 비행 목적', + desc: ( + <> + 군집 비행의 경우 담당자와 협의가 필요합니다.
+ 아래 링크를 통해 담당자와 협의 부탁드립니다. + + ), + 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({ - +
비행 유형
+
비행 계획 정보
@@ -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 } /> @@ -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({ - {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' ? ( <> @@ -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} /> @@ -920,6 +1335,19 @@ export default function LaancStep1({ 다음 + setIsPopUp(!isPopUp)} + className='modal-dialog-centered modal-lg notam-modal' + style={{ height: '25vh', width: '25vw' }} + > + + diff --git a/src/containers/analysis/simulator/AnalysisSimulationContainer.js b/src/containers/analysis/simulator/AnalysisSimulationContainer.js index ae42011..8befd73 100644 --- a/src/containers/analysis/simulator/AnalysisSimulationContainer.js +++ b/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'), diff --git a/src/containers/laanc/LaancContainer.js b/src/containers/laanc/LaancContainer.js index 202c605..1acb2bd 100644 --- a/src/containers/laanc/LaancContainer.js +++ b/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} - + ); diff --git a/src/containers/laanc/LaancPlanContainer.js b/src/containers/laanc/LaancPlanContainer.js index 2e77166..766ce36 100644 --- a/src/containers/laanc/LaancPlanContainer.js +++ b/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: ( - <> - 유효성 검사에 실패하여 미 승인 대상입니다. -
- 제출하신 비행계획서의 고도는 {laancElev[0]}m이하에서만 비행이 - 가능합니다. -
- 고도 설정을 다시 확인해주시기 바랍니다. - - ) - }); - 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: ( - <> - 검토 결과 미 승인 대상입니다. -

- 제줄하신 비행계획서는 별도의 승인이 필요없습니다. -
- 조종자 준수사항에 유의하여 비행하시기 바랍니다. -

- - ) - }); - return; - } else if (detailData.areaList[0].fltMethod === '군집비행') { - handleChange({ - type: 'area', - name: 'fltMethod', - value: '' - }); - setIsLaancModal({ - isOpen: true, - title: '군집 비행 목적', - desc: ( - <> - 군집 비행의 경우 담당자와 협의가 필요합니다.
- 아래 링크를 통해 담당자와 협의 부탁드립니다. - - ), - 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: ( - <> - 유효성 검사에 실패하여 미 승인 대상입니다. -

- 기체가 보험에 가입되어 있지 않거나 유효기간이 - 만료되었습니다. -
- 기체 번호를 다시 확인해주시기 바랍니다. -

- - ) - }); - 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: ( - <> - 야간 비행은 특별 비행에 해당됩니다. -
- 특별 비행의 경우 드론원스톱을 통해서 신청해주시기 바랍니다. - - ) - }); - return false; - } else if (schFltStDt.format('A h:mm') === 'PM 5:00') { - setIsErrorModal({ - isOpen: true, - title: '비행구역 및 비행일자 중복', - desc: ( - <> - 설정하신 비행구역 및 비행시간에 이미 승인완료된 신청건이 있습니다. -
다시 설정 부탁드립니다. - - ) - }); - 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인 구역이 있습니다. -
- 버퍼존을 다시 확인해주시기 바랍니다. - - ), - 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 && ( <> )} @@ -550,6 +112,7 @@ export default function LaancPlanContainer({ /> )} + diff --git a/src/modules/laanc/actions/laancActions.ts b/src/modules/laanc/actions/laancActions.ts index d5680b9..6ced9c7 100644 --- a/src/modules/laanc/actions/laancActions.ts +++ b/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 )(); +// laanc ts qr +export const LAANC_TS_QR = createAsyncAction( + LAANC_TS_QR_REQUEST, + LAANC_TS_QR_SUCCESS, + LAANC_TS_QR_FAILURE +)(); + 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; diff --git a/src/modules/laanc/apis/laancApi.ts b/src/modules/laanc/apis/laancApi.ts index 0e0e028..2e1b502 100644 --- a/src/modules/laanc/apis/laancApi.ts +++ b/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; } }; diff --git a/src/modules/laanc/models/laancModels.ts b/src/modules/laanc/models/laancModels.ts index 98d6a89..693ef28 100644 --- a/src/modules/laanc/models/laancModels.ts +++ b/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; +} diff --git a/src/modules/laanc/reducers/laancReducers.ts b/src/modules/laanc/reducers/laancReducers.ts index 8a7e536..cba4abd 100644 --- a/src/modules/laanc/reducers/laancReducers.ts +++ b/src/modules/laanc/reducers/laancReducers.ts @@ -83,6 +83,12 @@ export const laancReducer = createReducer( 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 => { diff --git a/src/modules/laanc/sagas/laancSagas.ts b/src/modules/laanc/sagas/laancSagas.ts index e9dc4b3..f5d516e 100644 --- a/src/modules/laanc/sagas/laancSagas.ts +++ b/src/modules/laanc/sagas/laancSagas.ts @@ -171,6 +171,23 @@ function* postValidTsSaga( ); } } +// laanc ts qr +function* getTsQrSaga(action: ActionType) { + 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);