From cb52e2ec9e724bbb8a26ddff15b670c09e1fc34b Mon Sep 17 00:00:00 2001 From: koseonje Date: Sun, 9 Feb 2025 21:56:27 +0900 Subject: [PATCH] =?UTF-8?q?[DDING-97]=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=ED=8F=BC=EC=A7=80=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/utils/CalculationUtils.java | 20 ++ .../ddingdongBE/common/utils/TimeUtils.java | 41 +-- .../domain/form/api/CentralFormApi.java | 104 ++++--- .../controller/CentralFormController.java | 92 +++--- .../dto/response/FormStatisticsResponse.java | 124 ++++++++ .../form/repository/FormFieldRepository.java | 19 +- .../form/repository/dto/FieldListInfo.java | 16 + .../service/FacadeCentralFormService.java | 13 +- .../service/FacadeCentralFormServiceImpl.java | 172 ++++++----- .../form/service/FormStatisticService.java | 20 ++ .../service/FormStatisticServiceImpl.java | 105 +++++++ .../dto/query/FormStatisticsQuery.java | 55 ++++ .../repository/FormAnswerRepository.java | 4 +- .../repository/FormApplicationRepository.java | 41 +++ .../repository/dto/DepartmentInfo.java | 8 + .../repository/dto/RecentFormInfo.java | 9 + .../common/support/FixtureMonkeyFactory.java | 23 +- .../common/utils/CalculationUtilsTest.java | 26 ++ .../FormApplicationRepositoryTest.java | 160 +++++++++- .../repository/FormFieldRepositoryTest.java | 81 +++++ .../service/FormStatisticServiceImplTest.java | 284 ++++++++++++++++++ 21 files changed, 1214 insertions(+), 203 deletions(-) create mode 100644 src/main/java/ddingdong/ddingdongBE/common/utils/CalculationUtils.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/form/controller/dto/response/FormStatisticsResponse.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/form/repository/dto/FieldListInfo.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticService.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImpl.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/form/service/dto/query/FormStatisticsQuery.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/DepartmentInfo.java create mode 100644 src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/RecentFormInfo.java create mode 100644 src/test/java/ddingdong/ddingdongBE/common/utils/CalculationUtilsTest.java create mode 100644 src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepositoryTest.java create mode 100644 src/test/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImplTest.java diff --git a/src/main/java/ddingdong/ddingdongBE/common/utils/CalculationUtils.java b/src/main/java/ddingdong/ddingdongBE/common/utils/CalculationUtils.java new file mode 100644 index 00000000..1fa48348 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/common/utils/CalculationUtils.java @@ -0,0 +1,20 @@ +package ddingdong.ddingdongBE.common.utils; + +public class CalculationUtils { + + public static int calculateRatio(int numerator, int denominator) { + if (denominator == 0) { + return 0; + } + return (int) ((double) numerator / denominator * 100); + } + + public static int calculateDifference(int beforeCount, int count) { + return count - beforeCount; + } + + public static int calculateDifferenceRatio(int beforeCount, int count) { + int difference = calculateDifference(beforeCount, count); + return calculateRatio(difference, beforeCount); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/common/utils/TimeUtils.java b/src/main/java/ddingdong/ddingdongBE/common/utils/TimeUtils.java index b5e3b57e..87750247 100644 --- a/src/main/java/ddingdong/ddingdongBE/common/utils/TimeUtils.java +++ b/src/main/java/ddingdong/ddingdongBE/common/utils/TimeUtils.java @@ -6,31 +6,36 @@ public class TimeUtils { - private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm"; + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm"; - public static LocalDateTime parseToLocalDateTime(String dateString) { - if (dateString == null || dateString.isBlank()) { - return null; + public static LocalDateTime parseToLocalDateTime(String dateString) { + if (dateString == null || dateString.isBlank()) { + return null; + } + + return LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(DATE_FORMAT)); } - return LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(DATE_FORMAT)); - } + public static LocalDateTime processDate(String dateString, LocalDateTime currentDate) { + if (dateString == null) { + return null; + } - public static LocalDateTime processDate(String dateString, LocalDateTime currentDate) { - if (dateString == null) { - return null; - } + if (dateString.isBlank()) { + return null; + } - if (dateString.isBlank()) { - return null; + return parseToLocalDateTime(dateString); } - return parseToLocalDateTime(dateString); - } + public static boolean isDateInRange(LocalDate nowDate, LocalDate startDate, LocalDate endDate) { + if (nowDate == null || startDate == null || endDate == null) { + return false; + } + return !nowDate.isBefore(startDate) && !nowDate.isAfter(endDate); + } - public static boolean isDateInRange(LocalDate nowDate, LocalDate startDate, LocalDate endDate) { - if (nowDate == null || startDate == null || endDate == null) { - return false; + public static String getYearAndMonth(LocalDate date) { + return date.getYear() + "-" + date.getMonthValue(); } - return !nowDate.isBefore(startDate) && !nowDate.isAfter(endDate); } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/api/CentralFormApi.java b/src/main/java/ddingdong/ddingdongBE/domain/form/api/CentralFormApi.java index e43750c4..113b2723 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/form/api/CentralFormApi.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/api/CentralFormApi.java @@ -5,6 +5,7 @@ import ddingdong.ddingdongBE.domain.form.controller.dto.request.UpdateFormRequest; import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormListResponse; import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormResponse; +import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormStatisticsResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -29,56 +30,63 @@ @RequestMapping("/server/central") public interface CentralFormApi { - @Operation(summary = "동아리 지원 폼지 생성 API") - @ApiResponse(responseCode = "201", description = "동아리 지원 폼지 생성 성공") - @ResponseStatus(HttpStatus.CREATED) - @SecurityRequirement(name = "AccessToken") - @PostMapping("/my/forms") - void createForm( - @Valid @RequestBody CreateFormRequest createFormRequest, - @AuthenticationPrincipal PrincipalDetails principalDetails - ); + @Operation(summary = "동아리 지원 폼지 생성 API") + @ApiResponse(responseCode = "201", description = "동아리 지원 폼지 생성 성공") + @ResponseStatus(HttpStatus.CREATED) + @SecurityRequirement(name = "AccessToken") + @PostMapping("/my/forms") + void createForm( + @Valid @RequestBody CreateFormRequest createFormRequest, + @AuthenticationPrincipal PrincipalDetails principalDetails + ); - @Operation(summary = "동아리 지원 폼지 수정 API") - @ApiResponse(responseCode = "204", description = "동아리 지원 폼지 수정 성공") - @ResponseStatus(HttpStatus.NO_CONTENT) - @SecurityRequirement(name = "AccessToken") - @PutMapping("/my/forms/{formId}") - void updateForm( - @Valid @RequestBody UpdateFormRequest updateFormRequest, - @PathVariable("formId") Long formId, - @AuthenticationPrincipal PrincipalDetails principalDetails - ); + @Operation(summary = "동아리 지원 폼지 수정 API") + @ApiResponse(responseCode = "204", description = "동아리 지원 폼지 수정 성공") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + @PutMapping("/my/forms/{formId}") + void updateForm( + @Valid @RequestBody UpdateFormRequest updateFormRequest, + @PathVariable("formId") Long formId, + @AuthenticationPrincipal PrincipalDetails principalDetails + ); - @Operation(summary = "동아리 지원 폼지 삭제 API") - @ApiResponse(responseCode = "204", description = "동아리 지원 폼지 삭제 성공") - @ResponseStatus(HttpStatus.NO_CONTENT) - @SecurityRequirement(name = "AccessToken") - @DeleteMapping("/my/forms/{formId}") - void deleteForm( - @PathVariable("formId") Long formId, - @AuthenticationPrincipal PrincipalDetails principalDetails - ); + @Operation(summary = "동아리 지원 폼지 삭제 API") + @ApiResponse(responseCode = "204", description = "동아리 지원 폼지 삭제 성공") + @ResponseStatus(HttpStatus.NO_CONTENT) + @SecurityRequirement(name = "AccessToken") + @DeleteMapping("/my/forms/{formId}") + void deleteForm( + @PathVariable("formId") Long formId, + @AuthenticationPrincipal PrincipalDetails principalDetails + ); - @Operation(summary = "동아리 지원 폼지 전체조회 API") - @ApiResponse(responseCode = "200", description = "동아리 지원 폼지 전체조회 성공", - content = @Content( - array = @ArraySchema(schema = @Schema(implementation = FormListResponse.class)) - )) - @ResponseStatus(HttpStatus.OK) - @SecurityRequirement(name = "AccessToken") - @GetMapping("/my/forms") - List getAllMyForm( - @AuthenticationPrincipal PrincipalDetails principalDetails - ); + @Operation(summary = "동아리 지원 폼지 전체조회 API") + @ApiResponse(responseCode = "200", description = "동아리 지원 폼지 전체조회 성공", + content = @Content( + array = @ArraySchema(schema = @Schema(implementation = FormListResponse.class)) + )) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping("/my/forms") + List getAllMyForm(@AuthenticationPrincipal PrincipalDetails principalDetails); - @Operation(summary = "동아리 지원 폼지 상세조회 API") - @ApiResponse(responseCode = "200", description = "동아리 지원 폼지 상세조회 성공", - content = @Content(schema = @Schema(implementation = FormResponse.class))) - @ResponseStatus(HttpStatus.OK) - @SecurityRequirement(name = "AccessToken") - @GetMapping("/my/forms/{formId}") - FormResponse getForm( - @PathVariable("formId") Long formId - ); + @Operation(summary = "동아리 지원 폼지 상세조회 API") + @ApiResponse(responseCode = "200", description = "동아리 지원 폼지 상세조회 성공", + content = @Content(schema = @Schema(implementation = FormResponse.class))) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping("/my/forms/{formId}") + FormResponse getForm(@PathVariable("formId") Long formId); + + @Operation(summary = "동아리 지원 폼지 통계 전체조회 API") + @ApiResponse(responseCode = "200", description = "동아리 지원 폼지 통계 전체조회 성공", + content = @Content(schema = @Schema(implementation = FormStatisticsResponse.class))) + @ResponseStatus(HttpStatus.OK) + @SecurityRequirement(name = "AccessToken") + @GetMapping("/my/forms/{formId}/statistics") + FormStatisticsResponse getFormStatistics( + @PathVariable("formId") Long formId, + @AuthenticationPrincipal PrincipalDetails principalDetails + ); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/controller/CentralFormController.java b/src/main/java/ddingdong/ddingdongBE/domain/form/controller/CentralFormController.java index 3ffe33be..1915c473 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/form/controller/CentralFormController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/controller/CentralFormController.java @@ -6,9 +6,11 @@ import ddingdong.ddingdongBE.domain.form.controller.dto.request.UpdateFormRequest; import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormListResponse; import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormResponse; +import ddingdong.ddingdongBE.domain.form.controller.dto.response.FormStatisticsResponse; import ddingdong.ddingdongBE.domain.form.service.FacadeCentralFormService; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery; import ddingdong.ddingdongBE.domain.user.entity.User; import java.util.List; import lombok.RequiredArgsConstructor; @@ -18,44 +20,54 @@ @RequiredArgsConstructor public class CentralFormController implements CentralFormApi { - private final FacadeCentralFormService facadeCentralFormService; - - @Override - public void createForm( - CreateFormRequest createFormRequest, - PrincipalDetails principalDetails - ) { - User user = principalDetails.getUser(); - facadeCentralFormService.createForm(createFormRequest.toCommand(user)); - } - - @Override - public void updateForm( - UpdateFormRequest updateFormRequest, - Long formId, - PrincipalDetails principalDetails - ) { - facadeCentralFormService.updateForm(updateFormRequest.toCommand(formId)); - } - - @Override - public void deleteForm(Long formId, PrincipalDetails principalDetails) { - User user = principalDetails.getUser(); - facadeCentralFormService.deleteForm(formId, user); - } - - @Override - public List getAllMyForm(PrincipalDetails principalDetails) { - User user = principalDetails.getUser(); - List queries = facadeCentralFormService.getAllMyForm(user); - return queries.stream() - .map(FormListResponse::from) - .toList(); - } - - @Override - public FormResponse getForm(Long formId) { - FormQuery query = facadeCentralFormService.getForm(formId); - return FormResponse.from(query); - } + private final FacadeCentralFormService facadeCentralFormService; + + @Override + public void createForm( + CreateFormRequest createFormRequest, + PrincipalDetails principalDetails + ) { + User user = principalDetails.getUser(); + facadeCentralFormService.createForm(createFormRequest.toCommand(user)); + } + + @Override + public void updateForm( + UpdateFormRequest updateFormRequest, + Long formId, + PrincipalDetails principalDetails + ) { + facadeCentralFormService.updateForm(updateFormRequest.toCommand(formId)); + } + + @Override + public void deleteForm(Long formId, PrincipalDetails principalDetails) { + User user = principalDetails.getUser(); + facadeCentralFormService.deleteForm(formId, user); + } + + @Override + public List getAllMyForm(PrincipalDetails principalDetails) { + User user = principalDetails.getUser(); + List queries = facadeCentralFormService.getAllMyForm(user); + return queries.stream() + .map(FormListResponse::from) + .toList(); + } + + @Override + public FormResponse getForm(Long formId) { + FormQuery query = facadeCentralFormService.getForm(formId); + return FormResponse.from(query); + } + + @Override + public FormStatisticsResponse getFormStatistics( + Long formId, + PrincipalDetails principalDetails + ) { + User user = principalDetails.getUser(); + FormStatisticsQuery query = facadeCentralFormService.getStatisticsByForm(user, formId); + return FormStatisticsResponse.from(query); + } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/controller/dto/response/FormStatisticsResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/form/controller/dto/response/FormStatisticsResponse.java new file mode 100644 index 00000000..b164231d --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/controller/dto/response/FormStatisticsResponse.java @@ -0,0 +1,124 @@ +package ddingdong.ddingdongBE.domain.form.controller.dto.response; + +import ddingdong.ddingdongBE.domain.form.entity.FieldType; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.ApplicantStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.DepartmentStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery.FieldStatisticsListQuery; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; + +@Builder +public record FormStatisticsResponse( + @Schema(description = "총 지원자 수", example = "50") + int totalCount, + @ArraySchema(schema = @Schema(implementation = DepartmentStatisticResponse.class)) + List departmentStatistics, + @ArraySchema(schema = @Schema(implementation = ApplicantStatisticResponse.class)) + List applicantStatistics, + @Schema(description = "필드 통계 전체조회", implementation = FieldStatisticsResponse.class) + FieldStatisticsResponse fieldStatistics +) { + + @Builder + record DepartmentStatisticResponse( + @Schema(description = "학과 내 경쟁 순위", example = "1") + int rank, + @Schema(description = "학과명", example = "융합소프트웨어학부") + String label, + @Schema(description = "해당 학과의 지원자 수", example = "50") + int count, + @Schema(description = "전체 지원자 수 대비 비율", example = "30") + int ratio + ) { + + public static DepartmentStatisticResponse from(DepartmentStatisticQuery query) { + return DepartmentStatisticResponse.builder() + .rank(query.rank()) + .label(query.label()) + .count(query.count()) + .ratio(query.ratio()) + .build(); + } + } + + @Builder + record ApplicantStatisticResponse( + @Schema(description = "비교 년도 및 월", example = "2025-1") + String label, + @Schema(description = "해당 년도 및 학기 총 지원자수", example = "40") + int count, + @Schema(description = "전 폼지 대비 증감 값", example = "150") + CompareToBefore comparedToBefore + ) { + record CompareToBefore( + @Schema(description = "증감율 %", example = "50") + int ratio, + @Schema(description = "증가수치 및 감소수치", example = "15") + int value + ) { + + } + public static ApplicantStatisticResponse from(ApplicantStatisticQuery query) { + return ApplicantStatisticResponse.builder() + .label(query.label()) + .count(query.count()) + .comparedToBefore(new CompareToBefore(query.compareRatio(), query.compareValue())) + .build(); + } + } + + @Builder + record FieldStatisticsResponse( + @Schema(description = "섹션종류", example = "[\"공통\"]") + List sections, + @ArraySchema(schema = @Schema(implementation = FieldStatisticsListResponse.class)) + List fields + ) { + record FieldStatisticsListResponse( + @Schema(description = "폼지 질문 id", example = "1") + Long id, + @Schema(description = "폼지 질문", example = "당신 이름은 무엇인가요?") + String question, + @Schema(description = "폼지 질문에 대해 총 작성 개수", example = "20") + int count, + @Schema(description = "폼지 질문 유형", example = "CHECK_BOX") + FieldType type, + @Schema(description = "섹션", example = "공통") + String section + ) { + public static FieldStatisticsListResponse from(FieldStatisticsListQuery query) { + return new FieldStatisticsListResponse(query.id(), query.question(), query.count(), query.fieldType(), + query.section()); + } + } + + public static FieldStatisticsResponse from(FieldStatisticsQuery query) { + List fieldStatisticsListResponses = query.fieldStatisticsListQueries().stream() + .map(FieldStatisticsListResponse::from) + .toList(); + return FieldStatisticsResponse.builder() + .sections(query.sections()) + .fields(fieldStatisticsListResponses) + .build(); + } + } + + public static FormStatisticsResponse from(FormStatisticsQuery query) { + List departmentStatisticResponse = query.departmentStatisticQueries().stream() + .map(DepartmentStatisticResponse::from) + .toList(); + List applicantStatisticResponse = query.applicantStatisticQueries().stream() + .map(ApplicantStatisticResponse::from) + .toList(); + return FormStatisticsResponse.builder() + .totalCount(query.totalCount()) + .departmentStatistics(departmentStatisticResponse) + .applicantStatistics(applicantStatisticResponse) + .fieldStatistics(FieldStatisticsResponse.from(query.fieldStatisticsQuery())) + .build(); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepository.java index c5c02429..dcce8133 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepository.java @@ -2,12 +2,27 @@ import ddingdong.ddingdongBE.domain.form.entity.Form; import ddingdong.ddingdongBE.domain.form.entity.FormField; +import ddingdong.ddingdongBE.domain.form.repository.dto.FieldListInfo; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface FormFieldRepository extends JpaRepository { - List findAllByForm(Form form); + List findAllByForm(Form form); - List findAllByFormAndSection(Form form, String section); + @Query(value = """ + SELECT f.id AS id, f.question AS question, f.field_type AS type, f.section AS section, COUNT(fa.id) AS count + FROM ( + SELECT * + FROM form_field field + WHERE field.form_id = :formId + ) AS f + LEFT JOIN form_answer AS fa + ON fa.field_id = f.id + GROUP BY f.id + ORDER BY f.id + """, nativeQuery = true) + List findFieldWithAnswerCountByFormId(@Param("formId") Long formId); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/repository/dto/FieldListInfo.java b/src/main/java/ddingdong/ddingdongBE/domain/form/repository/dto/FieldListInfo.java new file mode 100644 index 00000000..8d07d082 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/repository/dto/FieldListInfo.java @@ -0,0 +1,16 @@ +package ddingdong.ddingdongBE.domain.form.repository.dto; + +import ddingdong.ddingdongBE.domain.form.entity.FieldType; + +public interface FieldListInfo { + + Long getId(); + + String getQuestion(); + + Integer getCount(); + + FieldType getType(); + + String getSection(); +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormService.java b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormService.java index 8f156f3a..f95020d5 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormService.java @@ -4,18 +4,21 @@ import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery; import ddingdong.ddingdongBE.domain.user.entity.User; import java.util.List; public interface FacadeCentralFormService { - void createForm(CreateFormCommand command); + void createForm(CreateFormCommand command); - void updateForm(UpdateFormCommand command); + void updateForm(UpdateFormCommand command); - void deleteForm(Long formId, User user); + void deleteForm(Long formId, User user); - List getAllMyForm(User user); + List getAllMyForm(User user); - FormQuery getForm(Long formId); + FormQuery getForm(Long formId); + + FormStatisticsQuery getStatisticsByForm(User user, Long formId); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImpl.java b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImpl.java index 862d1d52..3167ed74 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImpl.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FacadeCentralFormServiceImpl.java @@ -12,6 +12,10 @@ import ddingdong.ddingdongBE.domain.form.service.dto.command.UpdateFormCommand.UpdateFormFieldCommand; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormListQuery; import ddingdong.ddingdongBE.domain.form.service.dto.query.FormQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.ApplicantStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.DepartmentStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery; import ddingdong.ddingdongBE.domain.user.entity.User; import java.time.LocalDate; import java.util.List; @@ -25,85 +29,93 @@ @Transactional(readOnly = true) public class FacadeCentralFormServiceImpl implements FacadeCentralFormService { - private final FormService formService; - private final FormFieldService formFieldService; - private final ClubService clubService; - - @Transactional - @Override - public void createForm(CreateFormCommand createFormCommand) { - Club club = clubService.getByUserId(createFormCommand.user().getId()); - Form form = createFormCommand.toEntity(club); - Form savedForm = formService.create(form); - - List formFields = toCreateFormFields(savedForm, - createFormCommand.formFieldCommands()); - formFieldService.createAll(formFields); - } - - @Transactional - @Override - public void updateForm(UpdateFormCommand updateFormCommand) { - Form originform = formService.getById(updateFormCommand.formId()); - Form updateForm = updateFormCommand.toEntity(); - originform.update(updateForm); - - List originFormFields = formFieldService.findAllByForm(originform); - formFieldService.deleteAll(originFormFields); - - List updateFormFields = toUpdateFormFields(originform, - updateFormCommand.formFieldCommands()); - formFieldService.createAll(updateFormFields); - } - - @Transactional - @Override - public void deleteForm(Long formId, User user) { - Club club = clubService.getByUserId(user.getId()); - Form form = formService.getById(formId); - validateEqualsClub(club, form); - formService.delete(form); //테이블 생성 시 외래 키에 cascade 설정하여 formField 삭제도 자동으로 됨. - } - - @Override - public List getAllMyForm(User user) { - Club club = clubService.getByUserId(user.getId()); - List
forms = formService.getAllByClub(club); - return forms.stream() - .map(this::buildFormListQuery) - .toList(); - } - - @Override - public FormQuery getForm(Long formId) { - Form form = formService.getById(formId); - List formFields = formFieldService.findAllByForm(form); - return FormQuery.of(form, formFields); - } - - private FormListQuery buildFormListQuery(Form form) { - boolean isActive = TimeUtils.isDateInRange(LocalDate.now(), form.getStartDate(), - form.getEndDate()); - return FormListQuery.from(form, isActive); - } - - private void validateEqualsClub(Club club, Form form) { - if (!Objects.equals(club.getId(), form.getClub().getId())) { - throw new NonHaveAuthority(); + private final FormService formService; + private final FormFieldService formFieldService; + private final ClubService clubService; + private final FormStatisticService formStatisticService; + + @Transactional + @Override + public void createForm(CreateFormCommand createFormCommand) { + Club club = clubService.getByUserId(createFormCommand.user().getId()); + Form form = createFormCommand.toEntity(club); + Form savedForm = formService.create(form); + + List formFields = toCreateFormFields(savedForm, createFormCommand.formFieldCommands()); + formFieldService.createAll(formFields); + } + + @Transactional + @Override + public void updateForm(UpdateFormCommand updateFormCommand) { + Form originform = formService.getById(updateFormCommand.formId()); + Form updateForm = updateFormCommand.toEntity(); + originform.update(updateForm); + + List originFormFields = formFieldService.findAllByForm(originform); + formFieldService.deleteAll(originFormFields); + + List updateFormFields = toUpdateFormFields(originform, updateFormCommand.formFieldCommands()); + formFieldService.createAll(updateFormFields); + } + + @Transactional + @Override + public void deleteForm(Long formId, User user) { + Club club = clubService.getByUserId(user.getId()); + Form form = formService.getById(formId); + validateEqualsClub(club, form); + formService.delete(form); //테이블 생성 시 외래 키에 cascade 설정하여 formField 삭제도 자동으로 됨. + } + + @Override + public List getAllMyForm(User user) { + Club club = clubService.getByUserId(user.getId()); + List forms = formService.getAllByClub(club); + return forms.stream() + .map(this::buildFormListQuery) + .toList(); + } + + @Override + public FormQuery getForm(Long formId) { + Form form = formService.getById(formId); + List formFields = formFieldService.findAllByForm(form); + return FormQuery.of(form, formFields); + } + + @Override + public FormStatisticsQuery getStatisticsByForm(User user, Long formId) { + Club club = clubService.getByUserId(user.getId()); + Form form = formService.getById(formId); + int totalCount = formStatisticService.getTotalApplicationCountByForm(form); + List departmentStatisticQueries = formStatisticService.createDepartmentStatistics(totalCount, form); + List applicantStatisticQueries = formStatisticService.createApplicationStatistics(club, form); + FieldStatisticsQuery fieldStatisticsQuery = formStatisticService.createFieldStatisticsByForm(form); + + return new FormStatisticsQuery(totalCount, departmentStatisticQueries, applicantStatisticQueries, fieldStatisticsQuery); + } + + private FormListQuery buildFormListQuery(Form form) { + boolean isActive = TimeUtils.isDateInRange(LocalDate.now(), form.getStartDate(), form.getEndDate()); + return FormListQuery.from(form, isActive); + } + + private void validateEqualsClub(Club club, Form form) { + if (!Objects.equals(club.getId(), form.getClub().getId())) { + throw new NonHaveAuthority(); + } + } + + private List toUpdateFormFields(Form originform, List updateFormFieldCommands) { + return updateFormFieldCommands.stream() + .map(formFieldCommand -> formFieldCommand.toEntity(originform)) + .toList(); + } + + private List toCreateFormFields(Form savedForm, List createFormFieldCommands) { + return createFormFieldCommands.stream() + .map(formFieldCommand -> formFieldCommand.toEntity(savedForm)) + .toList(); } - } - - private List toUpdateFormFields(Form originform, - List updateFormFieldCommands) { - return updateFormFieldCommands.stream() - .map(formFieldCommand -> formFieldCommand.toEntity(originform)) - .toList(); - } - - private List toCreateFormFields(Form savedForm, - List createFormFieldCommands) { - return createFormFieldCommands.stream() - .map(formFieldCommand -> formFieldCommand.toEntity(savedForm)) - .toList(); - } } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticService.java b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticService.java new file mode 100644 index 00000000..21e7ad0c --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticService.java @@ -0,0 +1,20 @@ +package ddingdong.ddingdongBE.domain.form.service; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.form.entity.Form; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.ApplicantStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.DepartmentStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery; +import java.util.List; + +public interface FormStatisticService { + + int getTotalApplicationCountByForm(Form form); + + List createDepartmentStatistics(int totalCount, Form form); + + List createApplicationStatistics(Club club, Form form); + + FieldStatisticsQuery createFieldStatisticsByForm(Form form); + +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImpl.java b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImpl.java new file mode 100644 index 00000000..3da6e28a --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImpl.java @@ -0,0 +1,105 @@ +package ddingdong.ddingdongBE.domain.form.service; + +import ddingdong.ddingdongBE.common.utils.CalculationUtils; +import ddingdong.ddingdongBE.common.utils.TimeUtils; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.form.entity.Form; +import ddingdong.ddingdongBE.domain.form.repository.FormFieldRepository; +import ddingdong.ddingdongBE.domain.form.repository.dto.FieldListInfo; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.ApplicantStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.DepartmentStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery.FieldStatisticsListQuery; +import ddingdong.ddingdongBE.domain.formapplication.repository.FormAnswerRepository; +import ddingdong.ddingdongBE.domain.formapplication.repository.FormApplicationRepository; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.DepartmentInfo; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.RecentFormInfo; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FormStatisticServiceImpl implements FormStatisticService { + + private static final int DEPARTMENT_INFORMATION_SIZE = 5; + private static final int APPLICANT_RATIO_INFORMATION_SIZE = 3; + private static final int DEFAULT_APPLICATION_RATE = 100; + + private final FormApplicationRepository formApplicationRepository; + private final FormFieldRepository formFieldRepository; + private final FormAnswerRepository formAnswerRepository; + + @Override + public int getTotalApplicationCountByForm(Form form) { + return formApplicationRepository.countByForm(form).intValue(); + } + + @Override + public List createDepartmentStatistics(int totalCount, Form form) { + List departmentInfos = formApplicationRepository.findTopDepartmentsByFormId( + form.getId(), + DEPARTMENT_INFORMATION_SIZE + ); + + return IntStream.range(0, departmentInfos.size()) + .mapToObj(index -> { + DepartmentInfo departmentInfo = departmentInfos.get(index); + int rank = index + 1; + String department = departmentInfo.getDepartment(); + int count = parseToInt(departmentInfo.getCount()); + int ratio = CalculationUtils.calculateRatio(count, totalCount); + return new DepartmentStatisticQuery(rank, department, count, ratio); + }) + .toList(); + } + + @Override + public List createApplicationStatistics(Club club, Form form) { + LocalDate endDate = form.getEndDate(); + List recentForms = formApplicationRepository.findRecentFormByDateWithApplicationCount( + club.getId(), + endDate, + APPLICANT_RATIO_INFORMATION_SIZE + ); + + return IntStream.range(0, recentForms.size()) + .mapToObj(index -> { + RecentFormInfo recentFormInfo = recentForms.get(index); + + String label = TimeUtils.getYearAndMonth(recentFormInfo.getDate()); + int count = parseToInt(recentFormInfo.getCount()); + if (index == 0) { + return new ApplicantStatisticQuery(label, count, 0, 0); + } + int beforeCount = parseToInt(recentForms.get(index - 1).getCount()); + int compareRatio = CalculationUtils.calculateDifferenceRatio(beforeCount, count); + int compareValue = CalculationUtils.calculateDifference(beforeCount, count); + + return new ApplicantStatisticQuery(label, count, compareRatio, compareValue); + }) + .toList(); + } + + @Override + public FieldStatisticsQuery createFieldStatisticsByForm(Form form) { + List sections = form.getSections(); + List fieldListInfos = formFieldRepository.findFieldWithAnswerCountByFormId(form.getId()); + List fieldStatisticsListQueries = toFieldListQueries(fieldListInfos); + return new FieldStatisticsQuery(sections, fieldStatisticsListQueries); + } + + private List toFieldListQueries(List fieldListInfos) { + return fieldListInfos.stream() + .map(FieldStatisticsListQuery::from) + .toList(); + } + + private int parseToInt(Integer count) { + return count == null ? 0 : count; + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/form/service/dto/query/FormStatisticsQuery.java b/src/main/java/ddingdong/ddingdongBE/domain/form/service/dto/query/FormStatisticsQuery.java new file mode 100644 index 00000000..353e0021 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/form/service/dto/query/FormStatisticsQuery.java @@ -0,0 +1,55 @@ +package ddingdong.ddingdongBE.domain.form.service.dto.query; + +import ddingdong.ddingdongBE.domain.form.entity.FieldType; +import ddingdong.ddingdongBE.domain.form.repository.dto.FieldListInfo; +import java.util.List; +import lombok.Builder; + +@Builder +public record FormStatisticsQuery( + int totalCount, + List departmentStatisticQueries, + List applicantStatisticQueries, + FieldStatisticsQuery fieldStatisticsQuery +) { + + public record DepartmentStatisticQuery( + int rank, + String label, + int count, + int ratio + ) { + } + + public record ApplicantStatisticQuery( + String label, + int count, + int compareRatio, + int compareValue + ) { + } + + public record FieldStatisticsQuery( + List sections, + List fieldStatisticsListQueries + ) { + + public record FieldStatisticsListQuery( + Long id, + String question, + int count, + FieldType fieldType, + String section + ) { + public static FieldStatisticsListQuery from(FieldListInfo fieldListInfo) { + return new FieldStatisticsListQuery( + fieldListInfo.getId(), + fieldListInfo.getQuestion(), + fieldListInfo.getCount(), + fieldListInfo.getType(), + fieldListInfo.getSection() + ); + } + } + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormAnswerRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormAnswerRepository.java index 30150b5f..d82287cc 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormAnswerRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormAnswerRepository.java @@ -1,5 +1,6 @@ package ddingdong.ddingdongBE.domain.formapplication.repository; +import ddingdong.ddingdongBE.domain.form.entity.FormField; import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer; import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,5 +9,6 @@ public interface FormAnswerRepository extends JpaRepository { - List findAllByFormApplication(FormApplication formApplication); + int countByFormField(FormField formField); + List findAllByFormApplication(FormApplication formApplication); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormApplicationRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormApplicationRepository.java index 072a5975..9475367b 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormApplicationRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/FormApplicationRepository.java @@ -1,11 +1,18 @@ package ddingdong.ddingdongBE.domain.formapplication.repository; +import ddingdong.ddingdongBE.domain.form.entity.Form; import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.DepartmentInfo; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.RecentFormInfo; +import java.time.LocalDate; +import java.util.List; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +@Repository public interface FormApplicationRepository extends JpaRepository { @Query(value = """ @@ -22,4 +29,38 @@ Slice findPageByFormIdOrderById( @Param("currentCursorId") Long currentCursorId ); + Long countByForm(Form form); + + @Query(value = """ + SELECT f.department, COUNT(f.id) AS count + FROM form_application f + WHERE f.form_id = :formId + GROUP BY f.department + ORDER BY count DESC + LIMIT :size + """, nativeQuery = true) + List findTopDepartmentsByFormId( + @Param("formId") Long formId, + @Param("size") int size + ); + + @Query(value = """ + SELECT recent_forms.start_date AS date, COUNT(fa.id) AS count + FROM ( + SELECT * + FROM form + WHERE club_id = :clubId + AND start_date <= :date + ORDER BY start_date + LIMIT :size + ) AS recent_forms + LEFT JOIN form_application fa + ON recent_forms.id = fa.form_id + GROUP BY recent_forms.id + """, nativeQuery = true) + List findRecentFormByDateWithApplicationCount( + @Param("clubId") Long clubId, + @Param("date") LocalDate date, + @Param("size") int size + ); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/DepartmentInfo.java b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/DepartmentInfo.java new file mode 100644 index 00000000..1c29fae4 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/DepartmentInfo.java @@ -0,0 +1,8 @@ +package ddingdong.ddingdongBE.domain.formapplication.repository.dto; + +public interface DepartmentInfo { + + String getDepartment(); + + Integer getCount(); +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/RecentFormInfo.java b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/RecentFormInfo.java new file mode 100644 index 00000000..fc024e81 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/formapplication/repository/dto/RecentFormInfo.java @@ -0,0 +1,9 @@ +package ddingdong.ddingdongBE.domain.formapplication.repository.dto; + +import java.time.LocalDate; + +public interface RecentFormInfo { + + LocalDate getDate(); + Integer getCount(); +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java b/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java index 69d96838..8b7c1008 100644 --- a/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java +++ b/src/test/java/ddingdong/ddingdongBE/common/support/FixtureMonkeyFactory.java @@ -1,7 +1,12 @@ package ddingdong.ddingdongBE.common.support; import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.generator.ArbitraryGeneratorContext; import com.navercorp.fixturemonkey.api.introspector.BuilderArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.jqwik.JavaArbitraryResolver; +import com.navercorp.fixturemonkey.api.jqwik.JqwikPlugin; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.arbitraries.StringArbitrary; public class FixtureMonkeyFactory { @@ -13,9 +18,21 @@ public static FixtureMonkey getBuilderIntrospectorMonkey() { public static FixtureMonkey getNotNullBuilderIntrospectorMonkey() { return FixtureMonkey.builder() - .objectIntrospector(BuilderArbitraryIntrospector.INSTANCE) - .defaultNotNull(true) - .build(); + .objectIntrospector(BuilderArbitraryIntrospector.INSTANCE) + .defaultNotNull(true) + .plugin( + new JqwikPlugin() + .javaArbitraryResolver(new JavaArbitraryResolver() { + @Override + public Arbitrary strings(StringArbitrary stringArbitrary, ArbitraryGeneratorContext context) { +// if (context.findAnnotation(MaxOfLength.class).isPresent()) { +// return stringArbitrary.ofMaxLength(10); +// } + return stringArbitrary; + } + }) + ) + .build(); } } diff --git a/src/test/java/ddingdong/ddingdongBE/common/utils/CalculationUtilsTest.java b/src/test/java/ddingdong/ddingdongBE/common/utils/CalculationUtilsTest.java new file mode 100644 index 00000000..2183ea7e --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/utils/CalculationUtilsTest.java @@ -0,0 +1,26 @@ +package ddingdong.ddingdongBE.common.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CalculationUtilsTest { + + @DisplayName("beforeCount기준으로 beforeCount와 count의 증감율을 구한다.") + @Test + void calculateDifferenceRatio() { + // given + int beforeCount = 30; + int count = 45; + + int beforeCount2 = 45; + int count2 = 30; + // when + int result = CalculationUtils.calculateDifferenceRatio(beforeCount, count); + int result2 = CalculationUtils.calculateDifferenceRatio(beforeCount2, count2); + // then + assertThat(result).isEqualTo(50); + assertThat(result2).isEqualTo(-33); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormApplicationRepositoryTest.java b/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormApplicationRepositoryTest.java index bee1c0b0..7f8e5d33 100644 --- a/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormApplicationRepositoryTest.java +++ b/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormApplicationRepositoryTest.java @@ -1,30 +1,31 @@ package ddingdong.ddingdongBE.domain.form.repository; +import static org.assertj.core.api.Assertions.assertThat; + import com.navercorp.fixturemonkey.FixtureMonkey; import ddingdong.ddingdongBE.common.support.DataJpaTestSupport; import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; import ddingdong.ddingdongBE.domain.form.entity.Form; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.DepartmentInfo; import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication; import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus; import ddingdong.ddingdongBE.domain.formapplication.repository.FormApplicationRepository; +import ddingdong.ddingdongBE.domain.formapplication.repository.dto.RecentFormInfo; import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; import ddingdong.ddingdongBE.domain.user.entity.Role; import ddingdong.ddingdongBE.domain.user.entity.User; import ddingdong.ddingdongBE.domain.user.repository.UserRepository; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Slice; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - class FormApplicationRepositoryTest extends DataJpaTestSupport { @Autowired @@ -149,4 +150,151 @@ void findPageByFormIdOrderById() { assertThat(retrievedApplications.size()).isEqualTo(2); assertThat(retrievedApplications.get(0).getId()).isGreaterThan(retrievedApplications.get(1).getId()); } + + + @DisplayName("지원자 수 상위 5개 학과와 그 개수를 반환한다.") + @Test + void findTopFiveDepartmentsByForm_ShouldReturnTopFiveDepartments() throws InterruptedException { + // given + Form form = fixture.giveMeBuilder(Form.class) + .set("club", null) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(form); + FormApplication formApplication = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication2 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication3 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication4 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication5 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication6 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + + formApplicationRepository.saveAll( + List.of(formApplication, formApplication2, formApplication3, formApplication4, formApplication5, + formApplication6) + ); + // when + List topFive = formApplicationRepository.findTopDepartmentsByFormId(savedForm.getId(),5); + // then + + assertThat(topFive.size()).isEqualTo(3); + assertThat(topFive.get(0).getCount()).isEqualTo(3); + assertThat(topFive.get(0).getDepartment()).isEqualTo("학과3"); + assertThat(topFive.get(1).getCount()).isEqualTo(2); + assertThat(topFive.get(1).getDepartment()).isEqualTo("학과2"); + assertThat(topFive.get(2).getCount()).isEqualTo(1); + assertThat(topFive.get(2).getDepartment()).isEqualTo("학과1"); + } + + @DisplayName("주어진 날짜를 기준으로 주어진 개수만큼 최신 폼지의 시작일과 지원 수를 반환한다.") + @Test + void findRecentFormByDateWithApplicationCount() { + // given + Club club1 = fixture.giveMeBuilder(Club.class) + .set("id", 1L) + .set("user", null) + .set("score", Score.from(BigDecimal.ZERO)) + .set("clubMembers", null) + .sample(); + Club savedClub = clubRepository.save(club1); + Form form = fixture.giveMeBuilder(Form.class) + .set("id", 1L) + .set("club", savedClub) + .set("startDate", LocalDate.of(2020, 3, 1)) + .set("endDate", LocalDate.of(2020, 4, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(form); + FormApplication formApplication = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication2 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + + formApplicationRepository.saveAll(List.of(formApplication, formApplication2)); + + Form form2 = fixture.giveMeBuilder(Form.class) + .set("id", 2L) + .set("club", savedClub) + .set("startDate", LocalDate.of(2020, 1, 1)) + .set("endDate", LocalDate.of(2020, 2, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm2 = formRepository.save(form2); + FormApplication formApplication3 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm2) + .build(); + formApplicationRepository.save(formApplication3); + + Form form3 = fixture.giveMeBuilder(Form.class) + .set("id", 1L) + .set("club", savedClub) + .set("startDate", LocalDate.of(2020, 5, 1)) + .set("endDate", LocalDate.of(2020, 6, 1)) + .set("sections", List.of("공통")) + .sample(); + formRepository.save(form3); + // when + List recentFormInfos = formApplicationRepository.findRecentFormByDateWithApplicationCount( + savedClub.getId(), + savedForm.getEndDate(), + 3 + ); + // then + assertThat(recentFormInfos.size()).isEqualTo(2); + assertThat(recentFormInfos.get(0).getCount()).isEqualTo(1); + assertThat(recentFormInfos.get(0).getDate()).isEqualTo(LocalDate.of(2020, 1, 1)); + assertThat(recentFormInfos.get(1).getCount()).isEqualTo(2); + assertThat(recentFormInfos.get(1).getDate()).isEqualTo(LocalDate.of(2020, 3, 1)); + + + } } diff --git a/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepositoryTest.java b/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepositoryTest.java new file mode 100644 index 00000000..e9646e48 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/form/repository/FormFieldRepositoryTest.java @@ -0,0 +1,81 @@ +package ddingdong.ddingdongBE.domain.form.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import ddingdong.ddingdongBE.common.support.DataJpaTestSupport; +import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; +import ddingdong.ddingdongBE.domain.form.entity.FieldType; +import ddingdong.ddingdongBE.domain.form.entity.Form; +import ddingdong.ddingdongBE.domain.form.entity.FormField; +import ddingdong.ddingdongBE.domain.form.repository.dto.FieldListInfo; +import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer; +import ddingdong.ddingdongBE.domain.formapplication.repository.FormAnswerRepository; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class FormFieldRepositoryTest extends DataJpaTestSupport { + + @Autowired + private FormRepository formRepository; + + @Autowired + private FormFieldRepository formFieldRepository; + + @Autowired + private FormAnswerRepository formAnswerRepository; + + private static final FixtureMonkey fixture = FixtureMonkeyFactory.getNotNullBuilderIntrospectorMonkey(); + + @DisplayName("원하는 필드 정보와 해당 필드의 답변 개수를 반환한다.") + @Test + void findFieldWithAnswerCountByFormId() { + // given + Form formFirst = fixture.giveMeBuilder(Form.class) + .set("id", 1L) + .set("club", null) + .set("startDate", LocalDate.of(2020, 2, 1)) + .set("endDate", LocalDate.of(2020, 3, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(formFirst); + FormField formField = FormField.builder() + .question("설문 질문") + .required(true) + .fieldOrder(1) + .section("기본 정보") + .options(List.of("옵션1", "옵션2", "옵션3")) + .fieldType(FieldType.TEXT) + .form(savedForm) + .build(); + FormField savedField = formFieldRepository.save(formField); + + FormAnswer answer = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + FormAnswer answer2 = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + FormAnswer answer3 = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + formAnswerRepository.saveAll(List.of(answer, answer2, answer3)); + + // when + List fieldListInfos = formFieldRepository.findFieldWithAnswerCountByFormId( + savedForm.getId()); + // then + assertThat(fieldListInfos.size()).isEqualTo(1); + assertThat(fieldListInfos.get(0).getQuestion()).isEqualTo(formField.getQuestion()); + assertThat(fieldListInfos.get(0).getCount()).isEqualTo(3); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImplTest.java b/src/test/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImplTest.java new file mode 100644 index 00000000..2dff65e1 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/domain/form/service/FormStatisticServiceImplTest.java @@ -0,0 +1,284 @@ +package ddingdong.ddingdongBE.domain.form.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; +import ddingdong.ddingdongBE.common.support.TestContainerSupport; +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; +import ddingdong.ddingdongBE.domain.form.entity.FieldType; +import ddingdong.ddingdongBE.domain.form.entity.Form; +import ddingdong.ddingdongBE.domain.form.entity.FormField; +import ddingdong.ddingdongBE.domain.form.repository.FormFieldRepository; +import ddingdong.ddingdongBE.domain.form.repository.FormRepository; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.ApplicantStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.DepartmentStatisticQuery; +import ddingdong.ddingdongBE.domain.form.service.dto.query.FormStatisticsQuery.FieldStatisticsQuery; +import ddingdong.ddingdongBE.domain.formapplication.entity.FormAnswer; +import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplication; +import ddingdong.ddingdongBE.domain.formapplication.entity.FormApplicationStatus; +import ddingdong.ddingdongBE.domain.formapplication.repository.FormAnswerRepository; +import ddingdong.ddingdongBE.domain.formapplication.repository.FormApplicationRepository; +import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class FormStatisticServiceImplTest extends TestContainerSupport { + + @Autowired + private FormApplicationRepository formApplicationRepository; + + @Autowired + private FormStatisticService formStatisticService; + + @Autowired + private FormRepository formRepository; + + @Autowired + private FormFieldRepository formFieldRepository; + + @Autowired + private FormAnswerRepository formAnswerRepository; + + @Autowired + private ClubRepository clubRepository; + + private final FixtureMonkey fixture = FixtureMonkeyFactory.getNotNullBuilderIntrospectorMonkey(); + + @DisplayName("폼지에 지원한 총 지원자 수를 반환한다.") + @Test + void getTotalApplicationCountByForm() { + // given + Form form = fixture.giveMeBuilder(Form.class) + .set("club", null) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(form); + FormApplication formApplication = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication2 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication3 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + formApplicationRepository.saveAll(List.of(formApplication, formApplication2, formApplication3)); + // when + int count = formStatisticService.getTotalApplicationCountByForm(savedForm); + // then + assertThat(count).isEqualTo(3); + } + + @DisplayName("폼지 내 지원자 수가 높은 학과 순으로 관련 정보를 정해진 수만큼 반환한다.") + @Test + void createDepartmentRankByForm() { + // given + Form form = fixture.giveMeBuilder(Form.class) + .set("club", null) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(form); + FormApplication formApplication = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication2 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication3 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과2") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication4 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication5 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication6 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과3") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + + formApplicationRepository.saveAll( + List.of(formApplication, formApplication2, formApplication3, formApplication4, formApplication5, + formApplication6) + ); + int totalCount = formStatisticService.getTotalApplicationCountByForm(savedForm); + // when + List departmentRanks = formStatisticService.createDepartmentStatistics(totalCount, + savedForm); + // then + assertThat(departmentRanks.size()).isEqualTo(3); + assertThat(departmentRanks.get(0).rank()).isEqualTo(1); + assertThat(departmentRanks.get(0).label()).isEqualTo("학과3"); + assertThat(departmentRanks.get(0).count()).isEqualTo(3); + assertThat(departmentRanks.get(1).rank()).isEqualTo(2); + assertThat(departmentRanks.get(1).label()).isEqualTo("학과2"); + assertThat(departmentRanks.get(1).count()).isEqualTo(2); + assertThat(departmentRanks.get(2).rank()).isEqualTo(3); + assertThat(departmentRanks.get(2).label()).isEqualTo("학과1"); + assertThat(departmentRanks.get(2).count()).isEqualTo(1); + + } + + @DisplayName("주어진 폼지와 이전 폼지의 비교 증감 정보를 정해진 개수만큼 반환한다") + @Test + void createApplicationRateByForm() { + // given + Club club1 = fixture.giveMeBuilder(Club.class) + .set("id", 1L) + .set("user", null) + .set("score", Score.from(BigDecimal.ZERO)) + .set("clubMembers", null) + .sample(); + Club savedClub = clubRepository.save(club1); + Form formFirst = fixture.giveMeBuilder(Form.class) + .set("id", 1L) + .set("club", savedClub) + .set("startDate", LocalDate.of(2020, 2, 1)) + .set("endDate", LocalDate.of(2020, 3, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(formFirst); + FormApplication formApplication = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + FormApplication formApplication2 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm) + .build(); + + formApplicationRepository.saveAll(List.of(formApplication, formApplication2)); + + Form form2 = fixture.giveMeBuilder(Form.class) + .set("id", 2L) + .set("club", savedClub) + .set("startDate", LocalDate.of(2020, 7, 3)) + .set("endDate", LocalDate.of(2020, 8, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm2 = formRepository.save(form2); + FormApplication formApplication3 = FormApplication.builder() + .name("이름1") + .studentNumber("학번1") + .department("학과1") + .status(FormApplicationStatus.SUBMITTED) + .form(savedForm2) + .build(); + formApplicationRepository.save(formApplication3); + // when + List applicationRateQueries = formStatisticService.createApplicationStatistics(savedClub, + savedForm2); + // then + + assertThat(applicationRateQueries.size()).isEqualTo(2); + assertThat(applicationRateQueries.get(0).label()).isEqualTo("2020-2"); + assertThat(applicationRateQueries.get(0).count()).isEqualTo(2); + assertThat(applicationRateQueries.get(0).compareRatio()).isEqualTo(0); + assertThat(applicationRateQueries.get(0).compareValue()).isEqualTo(0); + + assertThat(applicationRateQueries.get(1).label()).isEqualTo("2020-7"); + assertThat(applicationRateQueries.get(1).count()).isEqualTo(1); + assertThat(applicationRateQueries.get(1).compareRatio()).isEqualTo(-50); + assertThat(applicationRateQueries.get(1).compareValue()).isEqualTo(-1); + + + } + + @DisplayName("해당 폼지의 섹션 종류와 질문 정보 및 질문 답변 개수를 반환한다.") + @Test + void createFieldStatisticsByForm() { + // given + Form formFirst = fixture.giveMeBuilder(Form.class) + .set("id", 1L) + .set("club", null) + .set("startDate", LocalDate.of(2020, 2, 1)) + .set("endDate", LocalDate.of(2020, 3, 1)) + .set("sections", List.of("공통")) + .sample(); + Form savedForm = formRepository.save(formFirst); + FormField formField = FormField.builder() + .question("설문 질문") + .required(true) + .fieldOrder(1) + .section("기본 정보") + .options(List.of("옵션1", "옵션2", "옵션3")) + .fieldType(FieldType.TEXT) + .form(savedForm) + .build(); + FormField savedField = formFieldRepository.save(formField); + + FormAnswer answer = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + FormAnswer answer2 = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + FormAnswer answer3 = FormAnswer.builder() + .formApplication(null) + .value(null) + .formField(savedField) + .build(); + formAnswerRepository.saveAll(List.of(answer, answer2, answer3)); + // when + FieldStatisticsQuery fieldStatistics = formStatisticService.createFieldStatisticsByForm( + savedForm); + // then + assertThat(fieldStatistics.fieldStatisticsListQueries().size()).isEqualTo(1); + assertThat(fieldStatistics.fieldStatisticsListQueries().get(0).count()).isEqualTo(3); + assertThat(fieldStatistics.fieldStatisticsListQueries().get(0).question()).isEqualTo("설문 질문"); + } +}