김지은 10 months ago
parent
commit
a6e2b2498a
  1. 2
      src/components/laanc/step/LaancStep2.js
  2. 529
      src/containers/statistics/FlightResultContainer.js
  3. 36
      src/modules/statistics/actions/index.ts
  4. 30
      src/modules/statistics/apis/index.ts
  5. 39
      src/modules/statistics/models/index.ts
  6. 25
      src/modules/statistics/reducers/index.ts
  7. 60
      src/modules/statistics/sagas/index.ts
  8. 6
      src/navigation/statistics/index.js
  9. 4
      src/router/routes/RouteStatistics.js
  10. 8
      src/views/statistics/FlightResultView.js

2
src/components/laanc/step/LaancStep2.js

@ -358,7 +358,7 @@ export default function LaancStep2({
조종자 준수사항
</ModalHeader>
<ModalBody className='notam-info'>
<div style={{ textAlign: 'center' }}>
<div>
초경량비행장치 조종자는 제129조제1항에 따라 다음 호의 어느
하나에 해당하는 행위를 해서는 안된다.
<br /> 다만, 무인비행장치의 조종자에 대해서는 제4호 제5호를

529
src/containers/statistics/FlightResultContainer.js

@ -0,0 +1,529 @@
import { CustomMainLayout } from '../../components/layout/CustomMainLayout';
import {
Button,
Col,
Row,
Card,
CardHeader,
CardTitle,
CardBody,
CustomInput
} from 'reactstrap';
import { Search } from 'react-feather';
import { FcAlarmClock, FcWorkflow, FcBarChart } from 'react-icons/fc';
import { Bar, Doughnut } from 'react-chartjs-2';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as StcsActions from '../../modules/statistics/actions';
export default function ResultContainer({
tooltipShadow,
gridLineColor,
labelColor
}) {
const dispatch = useDispatch();
const { result, resultSearch } = useSelector(state => state.statisticsState);
const [searchType, setSearchType] = useState({
category: 'FLT_RESULT',
dateType: 'year',
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
day: new Date().getDate()
});
const [dateLists, setDateLists] = useState({
month: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
day: []
});
const titleName = '비행 실적 통계';
useEffect(() => {
dispatch(StcsActions.RESULT_STCS.request());
}, []);
// 해당 월에 맞는 요일 표출
useEffect(() => {
const { year, month } = searchType;
const lastDay = new Date(year, Number(month), 0).getDate();
const dayList = Array.from({ length: lastDay }, (_, index) => index + 1);
setDateLists({ ...dateLists, day: dayList });
}, [searchType.month]);
useEffect(() => {
const { category, dateType, year, month, day } = searchType;
const dateMapping = {
month: year,
day: `${year}-${month}`,
'one-day': `${year}-${month}-${day}`
};
const date = dateMapping[dateType] || '';
dispatch(
StcsActions.RESULT_STCS_SEARCH.request({
cate: category,
date,
type: dateType
})
);
}, [searchType]);
// 검색조건 handler
const handleChangeSearchType = useCallback(
(type, val) => {
setSearchType({
...searchType,
[type]: val
});
},
[searchType]
);
// 그래프 타이틀 handler
const handlerTitleName = category => {
const categoryMappings = {
TIME: '비행 실적',
DISTANCE: '비행 계획',
FLT_COUNT: '비행 승인'
};
return categoryMappings[category];
};
const handlerBarTicks = () => {
const data = resultSearch.topData.map(i => i.value);
const max = Math.max(...data);
const min = Math.min(...data);
const stepSize = handlerStepSize(max);
return { max, min, stepSize };
};
const handlerStepSize = max => {
if (max <= 1000) {
return 100;
} else if (max <= 5000) {
return 500;
} else {
return 1000;
}
};
const options = {
elements: {
rectangle: {
borderWidth: 2,
borderSkipped: 'bottom'
}
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 500,
legend: {
display: false
},
tooltips: {
// Updated default tooltip UI
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
},
scales: {
xAxes: [
{
display: true,
gridLines: {
display: true,
color: gridLineColor,
zeroLineColor: gridLineColor
},
scaleLabel: {
display: false
},
ticks: {
fontColor: labelColor
}
}
],
yAxes: [
{
display: true,
gridLines: {
color: gridLineColor,
zeroLineColor: gridLineColor
},
ticks: {
...handlerBarTicks(),
fontColor: labelColor
}
}
]
}
},
data = {
labels: resultSearch?.topData.map(i => i.name),
datasets: [
{
data: resultSearch?.topData.map(i => i.value),
backgroundColor: '#00bcd4',
borderColor: '#00bcd4',
barThickness: 15
}
]
};
const options2 = {
responsive: true,
maintainAspectRatio: true,
responsiveAnimationDuration: 500,
cutoutPercentage: 60,
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 18,
boxWidth: 8,
fontColor: labelColor,
fontSize: 14,
fontWeight: 500,
fontFamily: ['Rubik', 'Montserrat', 'NotoSansKR']
}
},
tooltips: {
callbacks: {
label(tooltipItem, data) {
const label = data.datasets[0].labels[tooltipItem.index] || '',
value = data.datasets[0].data[tooltipItem.index];
const output = ` ${label} : ${value} %`;
return output;
}
},
shadowOffsetX: 1,
shadowOffsetY: 1,
shadowBlur: 8,
shadowColor: tooltipShadow,
backgroundColor: '#fff',
titleFontColor: '#000',
bodyFontColor: '#000'
}
},
data2 = {
labels: resultSearch?.graphData.map(i => i.name),
datasets: [
{
labels: resultSearch?.graphData.map(i => i.name),
data: resultSearch?.graphData.map(i => i.value),
// 레드버전
// backgroundColor: [
// '#ffe8d1',
// '#ffb59e',
// '#f0826b',
// '#Bd4f38',
// '#8a1c05'
// ],
backgroundColor: [
'#ccffff',
'#99ffff',
'#66ffff',
'#33efff',
'#00bcd4'
],
borderWidth: 0,
pointStyle: 'rectRounded'
}
]
};
return (
<CustomMainLayout title={titleName}>
<div className='pal-card-box statistics'>
<div>
<Row>
<Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcAlarmClock />
</span>
<span>비행 실적</span>
<span>PA0001</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>8 10시간 35 12</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>1 35 12</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>35 12</span>
</td>
</tr>
</table>
</div>
</Col>
<Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcWorkflow />
</span>
<span>비행 계획</span>
<span>PA0002</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>10,845m</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>1,201m</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>53m</span>
</td>
</tr>
</table>
</div>
</Col>
<Col md='4'>
<div>
<table className='statistics-table'>
<tr>
<th rowSpan='2'>
<span>
<FcBarChart />
</span>
<span>비행 승인</span>
<span>PA0002</span>
</th>
<td colSpan='3'>
<span className='date'></span>
<span className='date-data'>1,024,845</span>
</td>
</tr>
<tr>
<td>
<span className='date'></span>
<span className='date-data'>111,201</span>
</td>
<td>
<span className='date'></span>
<span className='date-data'>153</span>
</td>
</tr>
</table>
</div>
</Col>
</Row>
</div>
<div>
<Row>
<Col>
<div className='mt-2 cont-ti d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row'>
<div>
<h4>검색조건</h4>
</div>
{/* <div className='d-flex align-items-center'>
<Button.Ripple color='primary' size='sm'>
<Search size={16} />
검색
</Button.Ripple>
</div> */}
</div>
<Card>
<CardBody className='pal-card-body'>
<div className='search-cont'>
<dl>
<dt>
<div className='search-box'>
<div className='search-list-ti'>검색조건</div>
<div className='search-list'>
<div className='search-list-cont'>
<Row>
<Col className='list-input' md='4' sm='12'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
onChange={e =>
handleChangeSearchType(
'category',
e.target.value
)
}
>
<option value={'FLT_RESULT'}>
비행 실적
</option>
<option value={'FLT_PLAN'}>
비행 계획
</option>
<option value={'FLT_PLAN_APRVN'}>
비행 승인
</option>
</CustomInput>
</Col>
</Row>
</div>
</div>
</div>
</dt>
</dl>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
<div>
<Row className='mt-2'>
<Col md='8' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} 통계
</CardTitle>
<div className='select-date-wrap'>
<Row>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.type}
onChange={e =>
handleChangeSearchType('dateType', e.target.value)
}
>
<option value='year'></option>
<option value='month'></option>
<option value='day'></option>
<option value='one-day'>시간</option>
</CustomInput>
</div>
{searchType.dateType === 'month' ||
searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<>
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.year}
onChange={e =>
handleChangeSearchType('year', e.target.value)
}
>
<option>2023</option>
</CustomInput>
</div>
{searchType.dateType === 'day' ||
searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.month}
onChange={e =>
handleChangeSearchType(
'month',
e.target.value
)
}
>
{dateLists.month.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
{searchType.dateType === 'one-day' ? (
<div className='select-date'>
<CustomInput
inline
type='select'
id=''
bsSize='sm'
value={searchType.day}
onChange={e =>
handleChangeSearchType('day', e.target.value)
}
>
{dateLists.day.map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</CustomInput>
</div>
) : null}
</>
) : null}
</Row>
{/* <Calendar size={14} />
<Flatpickr
options={{
mode: 'range',
defaultDate: ['2019-05-01', '2019-05-10']
}}
className='form-control flat-picker bg-transparent border-0 shadow-none'
/> */}
</div>
</CardHeader>
<CardBody>
<div style={{ height: '400px' }}>
<Bar data={data} options={options} height={400} />
</div>
</CardBody>
</Card>
</Col>
<Col md='4' className='chart-wrap'>
<Card>
<CardHeader className='d-flex justify-content-between align-items-sm-center align-items-start flex-sm-row flex-column'>
<CardTitle tag='h4'>
{handlerTitleName(searchType.category)} TOP5
</CardTitle>
</CardHeader>
<CardBody>
<div style={{ height: '275px' }}>
<Doughnut data={data2} options={options2} height={275} />
</div>
{/* <div className='d-flex justify-content-between mt-3 mb-1'>
<div className='d-flex align-items-center'></div>
</div> */}
</CardBody>
</Card>
</Col>
</Row>
</div>
</div>
</CustomMainLayout>
);
}

36
src/modules/statistics/actions/index.ts

@ -1,6 +1,6 @@
import { AxiosError } from 'axios';
import { createAsyncAction, ActionType } from 'typesafe-actions';
import { Flight, FlightSearch, FlightSearchRq } from '../models';
import { IStcsRs, IStcsSearchRq, IStcsSearchRs } from '../models';
// 비행 통계 (비행시간, 비행거리, 비행횟수)
const FLIGHT_STCS_REQUEST = 'statistics/flight/FLIGHT_STCS_REQUEST';
@ -15,21 +15,47 @@ const FLIGHT_STCS_SEARCH_SUCCESS =
const FLIGHT_STCS_SEARCH_FAILURE =
'statistics/flight/FLIGHT_STCS_SEARCH_FAILURE';
// 비행 실적 통계 (비행실적, 비행계획, 비행승인)
const RESULT_STCS_REQUEST = 'statistics/flight/RESULT_STCS_REQUEST';
const RESULT_STCS_SUCCESS = 'statistics/flight/RESULT_STCS_SUCCESS';
const RESULT_STCS_FAILURE = 'statistics/flight/RESULT_STCS_FAILURE';
// 비행 실적 통계 카테고리별 검색
const RESULT_STCS_SEARCH_REQUEST = 'statistics/RESULT_STCS_SEARCH_REQUEST';
const RESULT_STCS_SEARCH_SUCCESS =
'statistics/flight/RESULT_STCS_SEARCH_SUCCESS';
const RESULT_STCS_SEARCH_FAILURE =
'statistics/flight/RESULT_STCS_SEARCH_FAILURE';
export const FLIGHT_STCS = createAsyncAction(
FLIGHT_STCS_REQUEST,
FLIGHT_STCS_SUCCESS,
FLIGHT_STCS_FAILURE
)<null, Flight, AxiosError>();
)<null, IStcsRs[], AxiosError>();
export const FLIGHT_STCS_SEARCH = createAsyncAction(
FLIGHT_STCS_SEARCH_REQUEST,
FLIGHT_STCS_SEARCH_SUCCESS,
FLIGHT_STCS_SEARCH_FAILURE
)<FlightSearchRq, FlightSearch, AxiosError>();
)<IStcsSearchRq, IStcsSearchRs, AxiosError>();
export const RESULT_STCS = createAsyncAction(
RESULT_STCS_REQUEST,
RESULT_STCS_SUCCESS,
RESULT_STCS_FAILURE
)<null, IStcsRs[], AxiosError>();
export const RESULT_STCS_SEARCH = createAsyncAction(
RESULT_STCS_SEARCH_REQUEST,
RESULT_STCS_SEARCH_SUCCESS,
RESULT_STCS_SEARCH_FAILURE
)<IStcsSearchRq, IStcsSearchRs, AxiosError>();
const actions = {
FLIGHT_STCS,
FLIGHT_STCS_SEARCH
FLIGHT_STCS_SEARCH,
RESULT_STCS,
RESULT_STCS_SEARCH
};
export type StcsAction = ActionType<typeof actions>;
export type StatisticsAction = ActionType<typeof actions>;

30
src/modules/statistics/apis/index.ts

@ -1,12 +1,12 @@
import axios from '../../utils/customAxiosUtil';
import qs from 'qs';
import { FlightSearchRq } from '../models';
import { IStcsSearchRq } from '../models';
export const stcsAPI = {
export const statisticsAPI = {
flight: async () => {
return await axios.get('/api/main/dash/stcs/flight-static');
return await axios.get('/api/main/statistics/flight-static');
},
flightSearch: async (data: FlightSearchRq) => {
flightSearch: async (data: IStcsSearchRq) => {
const { type } = data;
const params = {};
Object.keys(data).forEach(i => {
@ -19,6 +19,26 @@ export const stcsAPI = {
arrayFormat: 'repeat'
});
return await axios.get(`api/main/dash/stcs/flight/${type}${queryString}`);
return await axios.get(`api/main/statistics/flight/${type}${queryString}`);
},
result: async () => {
return await axios.get('/api/main/statistics/flight/result-static');
},
resultSearch: async (data: IStcsSearchRq) => {
const { type } = data;
const params = {};
Object.keys(data).forEach(i => {
if (data[i] && i !== 'type') {
params[`${i}`] = data[i];
}
});
const queryString = qs.stringify(params, {
addQueryPrefix: true,
arrayFormat: 'repeat'
});
return await axios.get(
`/api/main/statistics/flight/result/${type}${queryString}`
);
}
};

39
src/modules/statistics/models/index.ts

@ -1,46 +1,41 @@
export interface IStcsState {
flight: Flight;
flightSearch: FlightSearch;
export interface IStatisticsState {
flight: IStcsRs[];
flightSearch: IStcsSearchRs;
result: IStcsRs[];
resultSearch: IStcsSearchRs;
}
export interface Flight {
count: number;
data: {
export interface IStcsRs {
name: string;
year: string;
month: string;
day: string;
}[];
}
export interface FlightSearch {
count: number;
data: {
graphData: SearchData[];
topData: SearchData[];
};
export interface IStcsSearchRs {
graphData: IStcsSearchData[];
topData: IStcsSearchData[];
}
export interface SearchData {
export interface IStcsSearchData {
name: string;
value: number;
}
export interface FlightSearchRq {
export interface IStcsSearchRq {
cate: string;
date: string;
type: string;
}
export const initialState = {
flight: {
count: 0,
data: []
},
flight: [],
flightSearch: {
count: 0,
data: {
graphData: [],
topData: []
}
},
result: [],
resultSearch: {
graphData: [],
topData: []
}
};

25
src/modules/statistics/reducers/index.ts

@ -1,11 +1,12 @@
import { createReducer } from 'typesafe-actions';
import produce from 'immer';
import * as Actions from '../actions';
import { IStcsState, initialState } from '../models';
import { IStatisticsState, initialState } from '../models';
export const statisticsReducer = createReducer<IStcsState, Actions.StcsAction>(
initialState
)
export const statisticsReducer = createReducer<
IStatisticsState,
Actions.StatisticsAction
>(initialState)
// 비행 통계 (비행시간, 비행거리, 비행횟수)
.handleAction(Actions.FLIGHT_STCS.success, (state, action) =>
produce(state, draft => {
@ -20,4 +21,20 @@ export const statisticsReducer = createReducer<IStcsState, Actions.StcsAction>(
const data = action.payload;
draft.flightSearch = data || state.flightSearch;
})
)
// 비행 실적 통계 (비행실적, 비행계획, 비행승인)
.handleAction(Actions.RESULT_STCS.success, (state, action) =>
produce(state, draft => {
const data = action.payload;
draft.result = data || state.result;
})
)
// 비행 실적 통계 카테고리별 검색
.handleAction(Actions.RESULT_STCS_SEARCH.success, (state, action) =>
produce(state, draft => {
const data = action.payload;
draft.resultSearch = data || state.resultSearch;
})
);

60
src/modules/statistics/sagas/index.ts

@ -9,7 +9,7 @@ function* flightStcsSaga(
) {
try {
const payload = action.payload;
const res = yield call(Apis.stcsAPI.flight);
const res = yield call(Apis.statisticsAPI.flight);
const { data, errorCode } = res;
if (errorCode) {
@ -36,7 +36,7 @@ function* flightStcsSearchSaga(
) {
try {
const payload = action.payload;
const res = yield call(Apis.stcsAPI.flightSearch, payload);
const res = yield call(Apis.statisticsAPI.flightSearch, payload);
const { data, errorCode } = res;
if (errorCode) {
@ -58,7 +58,63 @@ function* flightStcsSearchSaga(
}
}
function* resultStcsSaga(
action: ActionType<typeof Actions.RESULT_STCS.request>
) {
try {
const payload = action.payload;
const res = yield call(Apis.statisticsAPI.result);
const { data, errorCode } = res;
if (errorCode) {
// 오류메시지 호출
yield put(
MessageActions.IS_ERROR({
errorCode: errorCode,
errorMessage: '처리중 오류가 발생하였습니다',
isHistoryBack: false,
isRefresh: false
})
);
return;
}
yield put(Actions.RESULT_STCS.success(data));
} catch (error) {
yield put(Actions.RESULT_STCS.failure(error));
}
}
function* resultStcsSearchSaga(
action: ActionType<typeof Actions.RESULT_STCS_SEARCH.request>
) {
try {
const payload = action.payload;
const res = yield call(Apis.statisticsAPI.resultSearch, payload);
const { data, errorCode } = res;
if (errorCode) {
// 오류메시지 호출
yield put(
MessageActions.IS_ERROR({
errorCode: errorCode,
errorMessage: '처리중 오류가 발생하였습니다',
isHistoryBack: false,
isRefresh: false
})
);
return;
}
yield put(Actions.RESULT_STCS_SEARCH.success(data));
} catch (error) {
yield put(Actions.RESULT_STCS_SEARCH.failure(error));
}
}
export function* statisticsSaga() {
yield takeEvery(Actions.FLIGHT_STCS.request, flightStcsSaga);
yield takeEvery(Actions.FLIGHT_STCS_SEARCH.request, flightStcsSearchSaga);
yield takeEvery(Actions.RESULT_STCS.request, resultStcsSaga);
yield takeEvery(Actions.RESULT_STCS_SEARCH.request, resultStcsSearchSaga);
}

6
src/navigation/statistics/index.js

@ -15,5 +15,11 @@ export default [
type: 'item',
title: '비정상상황 통계',
navLink: '/statistics/abnormal'
},
{
id: 'statistics_001_03',
type: 'item',
title: '비행 실적',
navLink: '/statistics/result'
}
];

4
src/router/routes/RouteStatistics.js

@ -10,6 +10,10 @@ const RouteStatistics = [
component: lazy(() =>
import('../../views/statistics/AbnormalSituationView')
)
},
{
path: '/statistics/result',
component: lazy(() => import('../../views/statistics/FlightResultView'))
}
];

8
src/views/statistics/FlightResultView.js

@ -0,0 +1,8 @@
import '../../assets/css/custom.css';
import '@styles/react/libs/flatpickr/flatpickr.scss';
import '@styles/react/libs/tables/react-dataTable-component.scss';
import FlightResultContainer from '../../containers/statistics/FlightResultContainer';
export default function FlightResultView() {
return <FlightResultContainer />;
}
Loading…
Cancel
Save