diff --git a/pav-server/src/main/java/com/palnet/biz/api/bas/dos/controller/StatisticsDosController.java b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/controller/StatisticsDosController.java new file mode 100644 index 00000000..b364824b --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/controller/StatisticsDosController.java @@ -0,0 +1,103 @@ +package com.palnet.biz.api.bas.dos.controller; + +import com.palnet.biz.api.bas.dos.model.AllStatDataRS; +import com.palnet.biz.api.bas.dos.model.CptStatRQ; +import com.palnet.biz.api.bas.dos.model.CptStatRS; +import com.palnet.biz.api.bas.dos.service.StatisticsDosService; +import com.palnet.biz.api.comn.response.ErrorResponse; +import com.palnet.biz.api.comn.response.SuccessResponse; +import com.palnet.comn.code.ErrorCode; +import com.palnet.comn.exception.CustomException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@Slf4j +@RequestMapping("/api/statistics/dos") +@RequiredArgsConstructor +@Tag(name = "드론 원스톱 통계 컨트롤러", description = "드론원스톱 통계 관련 API") +public class StatisticsDosController { + + private final StatisticsDosService statisticsDosService; + + @GetMapping("/top-data") + @Operation(summary = "통계 페이지 상단 데이터 조회", description = "가장 많은 비행승인 데이터가 들어온 관할기관 데이터를 조회합니다.") + public ResponseEntity allData(){ + + AllStatDataRS result = null; + + try { + result = statisticsDosService.allData(); + } catch(CustomException e){ + ErrorCode errorCode = ErrorCode.fromCode(e.getSourceErrorCode()); + String paramMessage = (String) e.getParamArray()[0]; + + Map resultMap = new HashMap<>(); + log.error("IGNORE : ", e); + resultMap.put("result", false); + resultMap.put("errorCode", errorCode.code()); + resultMap.put("errorMessage", errorCode.message()); + resultMap.put("errorDesc", paramMessage); + return ResponseEntity.ok().body(new SuccessResponse<>(resultMap)); + } catch (Exception e) { + /** + * try{ + ... + } + * try 영역 안 코드들중 문제가 생기면 오는 곳. + * log.error 로그로 원인 파악과 함께 API를 호출한 곳에 서버에러 내려줌 + */ + log.error("IGONE : {}", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse("Server Error", "-1")); + } + + + return ResponseEntity.ok().body(result); + } + + @GetMapping("/table-data") + @Operation(summary = "통계 페이지 테이블 데이터 조회", description = "관할 기관 별 날짜별로 데이터 건수를 조회합니다.") + public ResponseEntity tableData(CptStatRQ rq){ + + CptStatRS result = null; + + try { + result = statisticsDosService.cptStatData(rq); + } catch(CustomException e){ + ErrorCode errorCode = ErrorCode.fromCode(e.getSourceErrorCode()); + String paramMessage = (String) e.getParamArray()[0]; + + Map resultMap = new HashMap<>(); + log.error("IGNORE : ", e); + resultMap.put("result", false); + resultMap.put("errorCode", errorCode.code()); + resultMap.put("errorMessage", errorCode.message()); + resultMap.put("errorDesc", paramMessage); + return ResponseEntity.ok().body(new SuccessResponse<>(resultMap)); + } catch (Exception e) { + /** + * try{ + ... + } + * try 영역 안 코드들중 문제가 생기면 오는 곳. + * log.error 로그로 원인 파악과 함께 API를 호출한 곳에 서버에러 내려줌 + */ + log.error("IGONE : {}", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse("Server Error", "-1")); + } + + return ResponseEntity.ok().body(result); + } + + + + +} diff --git a/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/AllStatDataRS.java b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/AllStatDataRS.java new file mode 100644 index 00000000..f72a346b --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/AllStatDataRS.java @@ -0,0 +1,37 @@ +package com.palnet.biz.api.bas.dos.model; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class AllStatDataRS { + + private List fullApproval; + + private List controlApproval; + + private List nonControlApproval; + + @Data + public static class GroupModel{ + + private List groupName; + + private Long all; + + private Long year; + + private Long month; + + private Long day; + + public GroupModel(){ + this.groupName = new ArrayList<>(); + } + + + } + +} diff --git a/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRQ.java b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRQ.java new file mode 100644 index 00000000..1e9dcd83 --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRQ.java @@ -0,0 +1,19 @@ +package com.palnet.biz.api.bas.dos.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDate; + +@Data +public class CptStatRQ { + + @Schema(description = "[ year: 연도별, month: 월별, day: 일일 ] 카테고리 선택 컬럼" , example = "month") + private String category; + + @Schema(description = "검색 시작일자" , example = "2024-03-01") + private LocalDate startDt; + + @Schema(description = "검색 종료일자" , example = "2024-12-31") + private LocalDate endDt; +} diff --git a/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRS.java b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRS.java new file mode 100644 index 00000000..97807758 --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/model/CptStatRS.java @@ -0,0 +1,44 @@ +package com.palnet.biz.api.bas.dos.model; + +import lombok.Data; + +import java.util.List; + +@Data +public class CptStatRS { + + private List cptList; + + + + @Data + public static class CptStat{ + + private String cptName; // 관할청 이름 + + private String cptCd; + + private List countModel; // 카운트 + + private List coordinateModels; // 중심좌표 + + } + + @Data + public static class DateCountModel{ + private String date; + + private Long count; + } + + @Data + public static class CoordinateModel{ + + private Double lat; // 위도 36.. ~~ + + private Double lon; // 경도 126.. ~~ + + } + + +} diff --git a/pav-server/src/main/java/com/palnet/biz/api/bas/dos/service/StatisticsDosService.java b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/service/StatisticsDosService.java new file mode 100644 index 00000000..10cce123 --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/api/bas/dos/service/StatisticsDosService.java @@ -0,0 +1,69 @@ +package com.palnet.biz.api.bas.dos.service; + +import com.palnet.biz.api.bas.dos.model.AllStatDataRS; +import com.palnet.biz.api.bas.dos.model.CptStatRQ; +import com.palnet.biz.api.bas.dos.model.CptStatRS; +import com.palnet.biz.jpa.repository.dos.DosFltPlanAreaQueryRepository; +import com.palnet.biz.jpa.repository.dos.DosFltPlanAreaRepository; +import com.palnet.biz.jpa.repository.dos.DosFltPlanBasRepository; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class StatisticsDosService { + + private final DosFltPlanAreaQueryRepository dosFltPlanAreaQueryRepository; + + public AllStatDataRS allData() { + + List fullApproval = dosFltPlanAreaQueryRepository.allApplyTopData(); + List controlApproval = dosFltPlanAreaQueryRepository.controlApplyTopData(true); + List nonControlApproval = dosFltPlanAreaQueryRepository.controlApplyTopData(false); + + AllStatDataRS result = new AllStatDataRS(); + result.setFullApproval(fullApproval); + result.setControlApproval(controlApproval); + result.setNonControlApproval(nonControlApproval); + + return result; + } + + + public CptStatRS cptStatData(CptStatRQ rq) { + + + List cptList = dosFltPlanAreaQueryRepository.cptStatData(rq); + + CptStatRS result = new CptStatRS(); + result.setCptList(cptList); + + return result; + } + + @Getter + @RequiredArgsConstructor + public enum CompetentAgency{ + + F0002("김포항공관리사무소"), + F0001("서울지방항공청"), + F0003("양양공항출장소"), + F0004("원주공항출장소"), + F0006("군산공항출장소"), + F0007("부산지방항공청"), + F0010("울진공항출장소"), + C0001("울산공항출장소"), + C0002("여수공항출장소"), + C0003("무안공항출장소"), + ; + + private final String desc; + + } + +} diff --git a/pav-server/src/main/java/com/palnet/biz/jpa/repository/ctr/CtrCntrlQueryRepository.java b/pav-server/src/main/java/com/palnet/biz/jpa/repository/ctr/CtrCntrlQueryRepository.java index 1a9fa43b..056cd475 100644 --- a/pav-server/src/main/java/com/palnet/biz/jpa/repository/ctr/CtrCntrlQueryRepository.java +++ b/pav-server/src/main/java/com/palnet/biz/jpa/repository/ctr/CtrCntrlQueryRepository.java @@ -1361,7 +1361,8 @@ public class CtrCntrlQueryRepository{ .from(qCntrlBasEntity) .leftJoin(qCtrCntrHstry) .on(qCntrlBasEntity.cntrlId.eq(qCtrCntrHstry.cntrlId)) - .where(builder) .orderBy(qCtrCntrHstry.hstrySno.asc()) + .where(builder) + .orderBy(qCtrCntrHstry.hstrySno.asc()) .fetch(); return result; diff --git a/pav-server/src/main/java/com/palnet/biz/jpa/repository/dos/DosFltPlanAreaQueryRepository.java b/pav-server/src/main/java/com/palnet/biz/jpa/repository/dos/DosFltPlanAreaQueryRepository.java new file mode 100644 index 00000000..c3df98a8 --- /dev/null +++ b/pav-server/src/main/java/com/palnet/biz/jpa/repository/dos/DosFltPlanAreaQueryRepository.java @@ -0,0 +1,317 @@ +package com.palnet.biz.jpa.repository.dos; + +import com.palnet.biz.api.bas.dos.model.AllStatDataRS; +import com.palnet.biz.api.bas.dos.model.CptStatRQ; +import com.palnet.biz.api.bas.dos.model.CptStatRS; +import com.palnet.biz.api.bas.dos.service.StatisticsDosService; +import com.palnet.biz.jpa.entity.QDosFltPlanArea; +import com.palnet.biz.jpa.entity.QDosFltPlanBas; +import com.palnet.comn.code.ErrorCode; +import com.palnet.comn.exception.CustomException; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class DosFltPlanAreaQueryRepository { + + private final JPAQueryFactory query; + + + /** + * 총 데이터 조회 + * @return + */ + public List allApplyTopData() { + + QDosFltPlanArea qDosFltPlanArea = QDosFltPlanArea.dosFltPlanArea; + QDosFltPlanBas qDosFltPlanBas = QDosFltPlanBas.dosFltPlanBas; + + Integer year = LocalDate.now().getYear(); + Integer month = LocalDate.now().getMonth().getValue(); + + NumberTemplate yearTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN YEAR({0}) = {1} THEN 1 END)", qDosFltPlanBas.applyDt, year); + NumberTemplate monthTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN YEAR({0}) = {1} AND MONTH({2}) = {3} THEN 1 END)", qDosFltPlanBas.applyDt, year, qDosFltPlanBas.applyDt, month); + NumberTemplate todayTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN DATE({0}) = CURDATE() THEN 1 END)", qDosFltPlanBas.applyDt); + + BooleanBuilder builder = new BooleanBuilder(); + builder.and(Expressions.booleanTemplate("{0} IS NOT NULL", qDosFltPlanArea.cptCd)); + + Map groupModel = query + .select( + Projections.bean( + AllStatDataRS.GroupModel.class, + qDosFltPlanArea.cptCd.as("groupName"), + qDosFltPlanArea.count().as("all"), + yearTemplate.as("year"), + monthTemplate.as("month"), + todayTemplate.as("day") + ) + ) + .from(qDosFltPlanArea) + .leftJoin(qDosFltPlanBas) + .on(qDosFltPlanArea.planSno.eq(qDosFltPlanBas.planSno)) + .where(builder) + .groupBy(qDosFltPlanArea.cptCd) + .fetch() + .stream() + .collect(Collectors.toMap( + key -> { + + if(key.getGroupName() == null) return ""; + + StringBuilder result = new StringBuilder(); + + key.getGroupName().forEach(node -> { + result.append(node); + result.append(","); + }); + + result.deleteCharAt(result.length() - 1); + + return result.toString(); + }, + value -> value + )); + + return this.topDataParsing(groupModel); + } + + /** + * 관제, 비관제 데이터 조회 + * @param controlFlag TRUE : 관제권 조회, FALSE : 비 관제권 조회 + * @return + */ + public List controlApplyTopData(Boolean controlFlag) { + QDosFltPlanArea qDosFltPlanArea = QDosFltPlanArea.dosFltPlanArea; + QDosFltPlanBas qDosFltPlanBas = QDosFltPlanBas.dosFltPlanBas; + + Integer year = LocalDate.now().getYear(); + Integer month = LocalDate.now().getMonth().getValue(); + + NumberTemplate yearTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN YEAR({0}) = {1} THEN 1 END)", qDosFltPlanBas.applyDt, year); + NumberTemplate monthTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN YEAR({0}) = {1} AND MONTH({2}) = {3} THEN 1 END)", qDosFltPlanBas.applyDt, year, qDosFltPlanBas.applyDt, month); + NumberTemplate todayTemplate = Expressions.numberTemplate(Long.class, "COUNT(CASE WHEN DATE({0}) = CURDATE() THEN 1 END)", qDosFltPlanBas.applyDt); + + ListPath groupingColumn = null; + BooleanBuilder builder = new BooleanBuilder(); + builder.and(Expressions.booleanTemplate("{0} IS NOT NULL", qDosFltPlanArea.cptCd)); + + if(controlFlag){ + builder.and(Expressions.booleanTemplate("{0} IS NOT NULL", qDosFltPlanArea.innerCptCd)); + groupingColumn = qDosFltPlanArea.innerCptCd; + } else { + builder.and(Expressions.booleanTemplate("{0} IS NULL", qDosFltPlanArea.innerCptCd)); + groupingColumn = qDosFltPlanArea.cptCd; + } + + Map groupModel = query + .select( + Projections.bean( + AllStatDataRS.GroupModel.class, + groupingColumn.as("groupName"), + qDosFltPlanArea.count().as("all"), + yearTemplate.as("year"), + monthTemplate.as("month"), + todayTemplate.as("day") + ) + ) + .from(qDosFltPlanArea) + .leftJoin(qDosFltPlanBas) + .on(qDosFltPlanArea.planSno.eq(qDosFltPlanBas.planSno)) + .where(builder) + .groupBy(groupingColumn) + .fetch() + .stream() + .collect(Collectors.toMap( + key -> { + if(key.getGroupName() == null) return ""; + + StringBuilder result = new StringBuilder(); + + key.getGroupName().forEach(node -> { + result.append(node); + result.append(","); + }); + + result.deleteCharAt(result.length() - 1); + + return result.toString(); + }, + value -> value + )); + + return this.topDataParsing(groupModel); + } + + + /** + * 공항별 데이터 통계 + * @param rq + * @return + */ + public List cptStatData(CptStatRQ rq) { + + QDosFltPlanArea qDosFltPlanArea = QDosFltPlanArea.dosFltPlanArea; + QDosFltPlanBas qDosFltPlanBas = QDosFltPlanBas.dosFltPlanBas; + + StatisticsDosService.CompetentAgency[] constants = StatisticsDosService.CompetentAgency.values(); + + List cptStatList = new ArrayList<>(); + + for(StatisticsDosService.CompetentAgency competnetAgency : constants){ + + String format = this.getFormat(rq.getCategory()); + StringTemplate formattedDate = Expressions.stringTemplate("DATE_FORMAT({0},{1})", qDosFltPlanBas.applyDt , format); + + String cptCd = competnetAgency.name(); + + BooleanBuilder builder = new BooleanBuilder(); + builder.and(Expressions.booleanTemplate("{0} LIKE CONCAT('%', {1}, '%')", qDosFltPlanArea.cptCd, cptCd)); + + if(!rq.getCategory().equals("year")){ + builder.and(qDosFltPlanBas.applyDt.goe(rq.getStartDt())); + builder.and(qDosFltPlanBas.applyDt.loe(rq.getEndDt())); + } + + List countModel = query + .select( + Projections.bean( + CptStatRS.DateCountModel.class, + formattedDate.as("date"), + qDosFltPlanArea.count().as("count") + ) + ) + .from(qDosFltPlanArea) + .leftJoin(qDosFltPlanBas) + .on(qDosFltPlanArea.planSno.eq(qDosFltPlanBas.planSno)) + .where(builder) + .groupBy(formattedDate) + .fetch(); + + List coordinateModels = query + .select( + Projections.bean( + CptStatRS.CoordinateModel.class, + qDosFltPlanArea.lat.as("lat"), + qDosFltPlanArea.lon.as("lon") + ) + ) + .from(qDosFltPlanArea) + .leftJoin(qDosFltPlanBas) + .on(qDosFltPlanArea.planSno.eq(qDosFltPlanBas.planSno)) + .where(builder) + .fetch(); + + + CptStatRS.CptStat cptStatModel = new CptStatRS.CptStat(); + cptStatModel.setCptName(competnetAgency.getDesc()); + cptStatModel.setCptCd(competnetAgency.name()); + cptStatModel.setCountModel(countModel); + cptStatModel.setCoordinateModels(coordinateModels); + + cptStatList.add(cptStatModel); + } + + return cptStatList; + } + + + private List topDataParsing(Map groupModel){ + Map currentMap = new ConcurrentHashMap<>(groupModel); + + // CptCd가 한 개가 아닌 값들에 대한 로직 + for(Map.Entry entry : currentMap.entrySet()){ + + String[] cptCdArray = entry.getKey().split(","); + + // CptCd가 1개일 경우 continue + if(cptCdArray.length <= 1) continue; + + for(String cptCd : cptCdArray){ + + // 기존 Map에 없는 값일 경우 CptCd를 Key로 새로 만들어 put + if(currentMap.get(cptCd) == null){ + AllStatDataRS.GroupModel node = new AllStatDataRS.GroupModel(); + node.setGroupName(Collections.singletonList(cptCd)); + node.setAll(entry.getValue().getAll()); + node.setYear(entry.getValue().getYear()); + node.setMonth(entry.getValue().getMonth()); + node.setDay(entry.getValue().getDay()); + + currentMap.put(cptCd, node); + continue; + } + + // 기존 값이 있을 경우 객체를 새로운 메모리에 할당하여 put + AllStatDataRS.GroupModel node = new AllStatDataRS.GroupModel(); + node.setGroupName(Collections.singletonList(cptCd)); + node.setAll(currentMap.get(cptCd).getAll() + entry.getValue().getAll()); + node.setYear(currentMap.get(cptCd).getYear() + entry.getValue().getYear()); + node.setMonth(currentMap.get(cptCd).getMonth() + entry.getValue().getMonth()); + node.setDay(currentMap.get(cptCd).getDay() + entry.getValue().getDay()); + + currentMap.put(cptCd, node); + } + + currentMap.remove(entry.getKey()); + } + + // Key의 맞는 GroupName Set + currentMap.forEach((key, value) -> { + value.setGroupName(Collections.singletonList(key)); + }); + + // 총 카운트가 가장많은 값 추출 + Long max = currentMap.values().stream() + .mapToLong(AllStatDataRS.GroupModel::getAll) + .max() + .orElse(0); + + List result = new ArrayList<>(); + + // 가장 많은 값만 반환 리스트에 ADD + for(Map.Entry entry : currentMap.entrySet()){ + if(entry.getValue().getAll().equals(max)){ + result.add(entry.getValue()); + } + } + + if(result.isEmpty()) result.add(new AllStatDataRS.GroupModel()); + + return result; + } + + private String getFormat(String category){ + String format = null; + + switch (category){ + case "year": + format = "%Y"; + break; + case "month": + format = "%Y-%m"; + break; + case "day": + format = "%Y-%m-%d"; + break; + default: + throw new CustomException(ErrorCode.NON_VALID_PARAMETER); + } + + return format; + } + + +}