diff --git a/build.gradle b/build.gradle index 36f5fe0..fc42bfd 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ subprojects { // postgreSQL runtimeOnly 'org.postgresql:postgresql' + // 쿼리 인자값 확인 + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1' + // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/pothole-core/src/main/java/pothole_solution/core/global/config/AppConfig.java b/pothole-core/src/main/java/pothole_solution/core/global/config/AppConfig.java new file mode 100644 index 0000000..743a320 --- /dev/null +++ b/pothole-core/src/main/java/pothole_solution/core/global/config/AppConfig.java @@ -0,0 +1,14 @@ +package pothole_solution.core.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import pothole_solution.core.global.util.formatter.LocalDateFormatter; + +@Configuration +public class AppConfig { + @Bean + public LocalDateFormatter localDateFormatter() { + return new LocalDateFormatter(); + } + +} diff --git a/pothole-core/src/main/java/pothole_solution/core/global/exception/CustomException.java b/pothole-core/src/main/java/pothole_solution/core/global/exception/CustomException.java index 3e70aa8..be12a42 100644 --- a/pothole-core/src/main/java/pothole_solution/core/global/exception/CustomException.java +++ b/pothole-core/src/main/java/pothole_solution/core/global/exception/CustomException.java @@ -14,6 +14,7 @@ public class CustomException extends RuntimeException { public static final CustomException INVALID_URL = new CustomException(ExceptionStatus.INVALID_URL); public static final CustomException INTERNAL_SERVER_ERROR = new CustomException(ExceptionStatus.INTERNAL_SERVER_ERROR); public static final CustomException NOT_EXISTED_FILE = new CustomException(ExceptionStatus.NOT_EXISTED_FILE); + public static final CustomException MISSING_PARAMETER = new CustomException(ExceptionStatus.MISSING_PARAMETER); // session exception public static final CustomException UNAUTHORIZED_SESSION = new CustomException(ExceptionStatus.UNAUTHORIZED_SESSION); @@ -38,4 +39,7 @@ public class CustomException extends RuntimeException { public static final CustomException INVALID_POTHOLE_IMG_URL = new CustomException(ExceptionStatus.INVALID_POTHOLE_IMG_URL); public static final CustomException INVALID_POTHOLE_IMG_NAME = new CustomException(ExceptionStatus.INVALID_POTHOLE_IMG_NAME); public static final CustomException INVALID_POTHOLE_IMG = new CustomException(ExceptionStatus.INVALID_POTHOLE_IMG); + + // Report Exception + public static final CustomException MISMATCH_PERIOD = new CustomException(ExceptionStatus.MISMATCH_PERIOD); } diff --git a/pothole-core/src/main/java/pothole_solution/core/global/exception/ExceptionStatus.java b/pothole-core/src/main/java/pothole_solution/core/global/exception/ExceptionStatus.java index d150b62..013fc87 100644 --- a/pothole-core/src/main/java/pothole_solution/core/global/exception/ExceptionStatus.java +++ b/pothole-core/src/main/java/pothole_solution/core/global/exception/ExceptionStatus.java @@ -14,6 +14,7 @@ public enum ExceptionStatus { INVALID_URL(BAD_REQUEST, 2001, "잘못된 URL 요청입니다."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 2002,"서버 내부 오류입니다."), NOT_EXISTED_FILE(NOT_EXTENDED, 2003,"존재하지 않는 파일입니다."), + MISSING_PARAMETER(BAD_REQUEST, 2004,"놓친 파라미터가 존재합니다."), // session exception UNAUTHORIZED_SESSION(UNAUTHORIZED, 2100, "인증되지 않은 세션입니다. 로그인을 해주세요."), @@ -37,7 +38,10 @@ public enum ExceptionStatus { FAILED_UPLOAD(HttpStatus.INTERNAL_SERVER_ERROR, 5000, "포트홀 이미지 업로드에 실패했습니다."), INVALID_POTHOLE_IMG_URL(BAD_REQUEST, 5001, "잘못된 포트홀 이미지 URL 요청입니다."), INVALID_POTHOLE_IMG_NAME(BAD_REQUEST, 5002, "포트홀 이미지의 이름이 존재하지 않거나 잘못되었습니다."), - INVALID_POTHOLE_IMG(BAD_REQUEST, 5003, "포트홀 이미지가 존재하지 않거나 잘못되었습니다."); + INVALID_POTHOLE_IMG(BAD_REQUEST, 5003, "포트홀 이미지가 존재하지 않거나 잘못되었습니다."), + + // Report exception + MISMATCH_PERIOD(BAD_REQUEST, 6000, "존재하지 않는 기간입니다."); private final HttpStatus httpStatus; private final int code; diff --git a/pothole-core/src/main/java/pothole_solution/core/global/exception/GlobalExceptionHandler.java b/pothole-core/src/main/java/pothole_solution/core/global/exception/GlobalExceptionHandler.java index 7cdbf4b..645cdfb 100644 --- a/pothole-core/src/main/java/pothole_solution/core/global/exception/GlobalExceptionHandler.java +++ b/pothole-core/src/main/java/pothole_solution/core/global/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -30,11 +31,22 @@ public ResponseEntity handleValidException() { } /** - * Progress Converter Exception Handler + * Invalid Parameter Exception Handler */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity handleProgressConverterValidException() { - CustomException exception = CustomException.NONE_PROGRESS_STATUS; + CustomException exception = CustomException.INVALID_PARAMETER; + return ResponseEntity + .status(exception.getStatus().getHttpStatus()) + .body(new BaseResponse<>(exception.getStatus())); + } + + /** + * Missing Request Parameter Exception Handler + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingServletRequestParameterException() { + CustomException exception = CustomException.MISSING_PARAMETER; return ResponseEntity .status(exception.getStatus().getHttpStatus()) .body(new BaseResponse<>(exception.getStatus())); diff --git a/pothole-core/src/main/java/pothole_solution/core/global/util/formatter/LocalDateFormatter.java b/pothole-core/src/main/java/pothole_solution/core/global/util/formatter/LocalDateFormatter.java new file mode 100644 index 0000000..ffaf915 --- /dev/null +++ b/pothole-core/src/main/java/pothole_solution/core/global/util/formatter/LocalDateFormatter.java @@ -0,0 +1,22 @@ +package pothole_solution.core.global.util.formatter; + +import org.jetbrains.annotations.NotNull; +import org.springframework.format.Formatter; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class LocalDateFormatter implements Formatter { + @NotNull + @Override + public LocalDate parse(@NotNull String text, @NotNull Locale locale) { + return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + + @NotNull + @Override + public String print(@NotNull LocalDate object, @NotNull Locale locale) { + return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object); + } +} diff --git a/pothole-core/src/main/java/pothole_solution/core/infra/p6spy/P6SpyFormatter.java b/pothole-core/src/main/java/pothole_solution/core/infra/p6spy/P6SpyFormatter.java new file mode 100644 index 0000000..c880f57 --- /dev/null +++ b/pothole-core/src/main/java/pothole_solution/core/infra/p6spy/P6SpyFormatter.java @@ -0,0 +1,53 @@ +package pothole_solution.core.infra.p6spy; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.P6SpyOptions; +import com.p6spy.engine.spy.appender.MessageFormattingStrategy; +import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.sql.SQLException; + +@Component +public class P6SpyFormatter extends JdbcEventListener implements MessageFormattingStrategy { + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + P6SpyOptions.getActiveInstance().setLogMessageFormat(getClass().getName()); + } + + @Override + public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) { + StringBuilder sb = new StringBuilder(); + sb.append(category).append(" ").append(elapsed).append("ms"); + if (StringUtils.hasText(sql)) { +// sb.append(highlight(format(sql))); + sb.append(format(sql)); + } + return sb.toString(); + } + + private String format(String sql) { + if (isDDL(sql)) { + return FormatStyle.DDL.getFormatter().format(sql); + } else if (isBasic(sql)) { + return FormatStyle.BASIC.getFormatter().format(sql); + } + return sql; + } + + private String highlight(String sql) { + return FormatStyle.HIGHLIGHT.getFormatter().format(sql); + } + + private boolean isDDL(String sql) { + return sql.startsWith("create") || sql.startsWith("alter") || sql.startsWith("comment"); + } + + private boolean isBasic(String sql) { + return sql.startsWith("select") || sql.startsWith("insert") || sql.startsWith("update") || sql.startsWith("delete"); + } + +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/global/converter/ReportPeriodEnumConverter.java b/pothole-manager-api/src/main/java/pothole_solution/manager/global/converter/ReportPeriodEnumConverter.java new file mode 100644 index 0000000..e690696 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/global/converter/ReportPeriodEnumConverter.java @@ -0,0 +1,12 @@ +package pothole_solution.manager.global.converter; + +import org.jetbrains.annotations.NotNull; +import org.springframework.core.convert.converter.Converter; +import pothole_solution.manager.report.entity.ReportPeriod; + +public class ReportPeriodEnumConverter implements Converter { + @Override + public ReportPeriod convert(@NotNull String period) { + return ReportPeriod.enumOf(period); + } +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/global/formatter/EnumFormatter.java b/pothole-manager-api/src/main/java/pothole_solution/manager/global/formatter/EnumFormatter.java new file mode 100644 index 0000000..e768642 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/global/formatter/EnumFormatter.java @@ -0,0 +1,15 @@ +package pothole_solution.manager.global.formatter; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import pothole_solution.manager.global.converter.ReportPeriodEnumConverter; + +@Configuration +public class EnumFormatter implements WebMvcConfigurer { + @Override + public void addFormatters(@NotNull FormatterRegistry registry) { + registry.addConverter(new ReportPeriodEnumConverter()); + } +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/controller/ReportController.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/controller/ReportController.java new file mode 100644 index 0000000..27d201d --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/controller/ReportController.java @@ -0,0 +1,34 @@ +package pothole_solution.manager.report.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import pothole_solution.core.global.util.response.BaseResponse; +import pothole_solution.manager.report.dto.RespPotDngrCntByPeriodDto; +import pothole_solution.manager.report.entity.ReportPeriod; +import pothole_solution.manager.report.service.ReportService; + +import java.time.LocalDate; +import java.util.List; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/pothole/v1/manager") +public class ReportController { + private final ReportService reportService; + + @GetMapping("/pothole-report") + public BaseResponse> getPotDngrCntByPeriod(@RequestParam(value = "startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam(value = "endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate, + @RequestParam(value = "reportPeriod") ReportPeriod reportPeriod) { + + List periodPotholeCounts = reportService.getPeriodPotholeDangerousCount(startDate, endDate, reportPeriod); + + return new BaseResponse<>(periodPotholeCounts); + } +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/dto/RespPotDngrCntByPeriodDto.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/dto/RespPotDngrCntByPeriodDto.java new file mode 100644 index 0000000..1745190 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/dto/RespPotDngrCntByPeriodDto.java @@ -0,0 +1,15 @@ +package pothole_solution.manager.report.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RespPotDngrCntByPeriodDto { + String period; + Long dangerous0to20; + Long dangerous20to40; + Long dangerous40to60; + Long dangerous60to80; + Long dangerous80to100; +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/entity/ReportPeriod.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/entity/ReportPeriod.java new file mode 100644 index 0000000..25db607 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/entity/ReportPeriod.java @@ -0,0 +1,24 @@ +package pothole_solution.manager.report.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import pothole_solution.core.global.exception.CustomException; + +@Getter +@AllArgsConstructor +public enum ReportPeriod { + MONTHLY("YYYY-MM"), + WEEKLY("YYYY-MM-W"), + DAILY("YYYY-MM-DD"), + AUTO("auto"); + + private final String queryOfPeriod; + + public static ReportPeriod enumOf(String period) { + try { + return ReportPeriod.valueOf(period.toUpperCase()); + } catch (RuntimeException e) { + throw CustomException.MISMATCH_PERIOD; + } + } +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/repository/ReportQueryDslRepository.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/repository/ReportQueryDslRepository.java new file mode 100644 index 0000000..51ed524 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/repository/ReportQueryDslRepository.java @@ -0,0 +1,57 @@ +package pothole_solution.manager.report.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.core.types.dsl.StringTemplate; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import pothole_solution.manager.report.dto.RespPotDngrCntByPeriodDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static pothole_solution.core.domain.pothole.entity.QPothole.pothole; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class ReportQueryDslRepository { + private final JPAQueryFactory jpaQueryFactory; + + public List getPotDngrCntByPeriod(LocalDateTime startDate, LocalDateTime endDate, String queryOfPeriod) { + return jpaQueryFactory + .select( + Projections.constructor(RespPotDngrCntByPeriodDto.class, + convertDateFormat(queryOfPeriod), + countDangerousBetween(1, 20), + countDangerousBetween(21, 40), + countDangerousBetween(41, 60), + countDangerousBetween(61, 80), + countDangerousBetween(81, 100) + ) + ) + .from(pothole) + .where(pothole.createdAt.between(startDate, endDate)) + .groupBy(convertDateFormat(queryOfPeriod)) + .orderBy(convertDateFormat(queryOfPeriod).asc()) + .fetch(); + } + + private static StringTemplate convertDateFormat(String queryOfPeriod) { + return Expressions.stringTemplate( + "to_char({0},'" + queryOfPeriod + "')" + , pothole.createdAt); + } + + private static NumberExpression countDangerousBetween(int from, int to) { + return Expressions.cases() + .when(pothole.dangerous.between(from, to)) + .then(1L) + .otherwise(0L) + .sum(); + } + +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/AutoPeriodCalculator.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/AutoPeriodCalculator.java new file mode 100644 index 0000000..03319ca --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/AutoPeriodCalculator.java @@ -0,0 +1,18 @@ +package pothole_solution.manager.report.service; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; + +public class AutoPeriodCalculator { + private static final int CRITERIA_OF_MONTHLY = 3; + private static final int CRITERIA_OF_WEEKLY = 3; + + protected static boolean isMonthly(LocalDate startDate, LocalDate endDate) { + return ChronoUnit.MONTHS.between(startDate, endDate) >= CRITERIA_OF_MONTHLY; + } + + protected static boolean isWeekly(LocalDate startDate, LocalDate endDate) { + return ChronoUnit.MONTHS.between(startDate, endDate) < CRITERIA_OF_MONTHLY + && ChronoUnit.WEEKS.between(startDate, endDate) >= CRITERIA_OF_WEEKLY; + } +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportService.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportService.java new file mode 100644 index 0000000..a23e896 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportService.java @@ -0,0 +1,11 @@ +package pothole_solution.manager.report.service; + +import pothole_solution.manager.report.entity.ReportPeriod; +import pothole_solution.manager.report.dto.RespPotDngrCntByPeriodDto; + +import java.time.LocalDate; +import java.util.List; + +public interface ReportService { + List getPeriodPotholeDangerousCount(LocalDate startDate, LocalDate endDate, ReportPeriod period); +} diff --git a/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportServiceImpl.java b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportServiceImpl.java new file mode 100644 index 0000000..bd4fe96 --- /dev/null +++ b/pothole-manager-api/src/main/java/pothole_solution/manager/report/service/ReportServiceImpl.java @@ -0,0 +1,47 @@ +package pothole_solution.manager.report.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pothole_solution.manager.report.dto.RespPotDngrCntByPeriodDto; +import pothole_solution.manager.report.entity.ReportPeriod; +import pothole_solution.manager.report.repository.ReportQueryDslRepository; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static pothole_solution.manager.report.service.AutoPeriodCalculator.*; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class ReportServiceImpl implements ReportService { + private final ReportQueryDslRepository reportRepository; + + @Override + @Transactional(readOnly = true) + public List getPeriodPotholeDangerousCount(LocalDate startDate, LocalDate endDate, ReportPeriod reportPeriod) { + + String queryOfPeriod = getQueryOfPeriod(startDate, endDate, reportPeriod); + + return reportRepository.getPotDngrCntByPeriod(startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX), queryOfPeriod); + + } + + private String getQueryOfPeriod(LocalDate startDate, LocalDate endDate, ReportPeriod reportPeriod) { + if (!reportPeriod.equals(ReportPeriod.AUTO)) { + return reportPeriod.getQueryOfPeriod(); + } + if (isMonthly(startDate, endDate)) { + return ReportPeriod.MONTHLY.getQueryOfPeriod(); + } + if (isWeekly(startDate, endDate)) { + return ReportPeriod.WEEKLY.getQueryOfPeriod(); + } + return ReportPeriod.DAILY.getQueryOfPeriod(); + } + +}