diff --git a/src/docs/asciidoc/analysis.adoc b/src/docs/asciidoc/analysis.adoc new file mode 100644 index 00000000..f591d69f --- /dev/null +++ b/src/docs/asciidoc/analysis.adoc @@ -0,0 +1,92 @@ +== 분석 Analysis + +=== 전형별 지원자 수 조회 +전형별 지원자들의 수를 조회할 수 있습니다. + +=== 요청 형식 + +==== Request Header +include::{snippets}/analysis-controller-test/전형별_지원자수를_조회한다/request-headers.adoc[] + +==== 요청 +include::{snippets}/analysis-controller-test/전형별_지원자수를_조회한다/http-request.adoc[] + +==== 응답 +include::{snippets}/analysis-controller-test/전형별_지원자수를_조회한다/http-response.adoc[] + +=== 전형별 성적 분포 조회 +1차 합격자, 2차 전형자, 최종 합격자들의 전형별 성적 분포를 조회할 수 있습니다. + +=== 요청 형식 + +==== Request Header +include::{snippets}/analysis-controller-test/_1차_합격자들의_성적_분포를_조회한다/request-headers.adoc[] + +==== Query Parameter +include::{snippets}/analysis-controller-test/_1차_합격자들의_성적_분포를_조회한다/query-parameters.adoc[] + +==== 요청 + +===== 1차 합격자 +include::{snippets}/analysis-controller-test/_1차_합격자들의_성적_분포를_조회한다/http-request.adoc[] + +===== 2차 전형자 +include::{snippets}/analysis-controller-test/_2차_전형자들의_성적_분포를_조회한다/http-request.adoc[] + +===== 최종 합격자 +include::{snippets}/analysis-controller-test/최종_합격자들의_성적_분포를_조회한다/http-request.adoc[] + +==== 응답 + +===== 정상 응답 +include::{snippets}/analysis-controller-test/최종_합격자들의_성적_분포를_조회한다/http-response.adoc[] + +=== 전형별 성비 조회 +1차 합격자, 2차 전형자, 최종 합격자들의 전형별, 지역별 성비를 조회할 수 있습니다. + +=== 요청 형식 + +==== Request Header +include::{snippets}/analysis-controller-test/전형별_성비를_조회한다/request-headers.adoc[] + +==== Query Parameter +include::{snippets}/analysis-controller-test/전형별_성비를_조회한다/query-parameters.adoc[] + +==== 요청 +include::{snippets}/analysis-controller-test/전형별_성비를_조회한다/http-request.adoc[] + +==== 응답 + +===== 정상 응답 +include::{snippets}/analysis-controller-test/전형별_성비를_조회한다/http-response.adoc[] + +=== 전형별 출신학교 조회 +1차 합격자, 2차 전형자, 최종 합격자들의 출신학교를 조회할 수 있습니다. + +=== 요청 형식 + +==== Request Header +include::{snippets}/analysis-controller-test/부산_특정구_출신_지원자들의_출신학교_통계를_조회한다/request-headers.adoc[] + +==== Query Parameter +include::{snippets}/analysis-controller-test/부산_특정구_출신_지원자들의_출신학교_통계를_조회한다/query-parameters.adoc[] + +==== 요청 +===== 부산 특정구 출신 +include::{snippets}/analysis-controller-test/부산_특정구_출신_지원자들의_출신학교_통계를_조회한다/http-request.adoc[] + +==== 부산 출신 +include::{snippets}/analysis-controller-test/부산_출신_지원자들의_출신학교_통계를_조회한다/http-request.adoc[] + +==== 타지역 출신 +include::{snippets}/analysis-controller-test/타지역_출신_지원자들의_출신학교_통계를_조회한다/http-request.adoc[] + +==== 응답 +==== 부산 특정구 출신 정상 응답 +include::{snippets}/analysis-controller-test/부산_특정구_출신_지원자들의_출신학교_통계를_조회한다/http-response.adoc[] + +==== 부산 출신 정상 응답 +include::{snippets}/analysis-controller-test/부산_출신_지원자들의_출신학교_통계를_조회한다/http-response.adoc[] + +==== 타지역 출신 정상 응답 +include::{snippets}/analysis-controller-test/타지역_출신_지원자들의_출신학교_통계를_조회한다/http-response.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 4a46528e..d64e5a35 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -27,4 +27,6 @@ include::fair.adoc[] include::message.adoc[] +include::analysis.adoc[] + include::enum.adoc[] diff --git a/src/main/java/com/bamdoliro/maru/application/analysis/QueryGenderRatioUseCase.java b/src/main/java/com/bamdoliro/maru/application/analysis/QueryGenderRatioUseCase.java new file mode 100644 index 00000000..008260e8 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/application/analysis/QueryGenderRatioUseCase.java @@ -0,0 +1,76 @@ +package com.bamdoliro.maru.application.analysis; + +import com.bamdoliro.maru.domain.form.domain.Form; +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.domain.form.domain.type.Gender; +import com.bamdoliro.maru.infrastructure.persistence.form.FormRepository; +import com.bamdoliro.maru.presentation.analysis.dto.request.GenderRatioRequest; +import com.bamdoliro.maru.presentation.analysis.dto.response.GenderRatioResponse; +import com.bamdoliro.maru.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@UseCase +public class QueryGenderRatioUseCase { + private final FormRepository formRepository; + + public List execute(GenderRatioRequest request) { + FormType.Category mainCategory = request.getMainCategory(); + List result = new ArrayList<>(); + List subCategories = new ArrayList<>(); + + if (mainCategory.equals(FormType.Category.REGULAR)) + subCategories.add(FormType.Category.REGULAR); + else if (mainCategory.equals(FormType.Category.SPECIAL)) + subCategories.addAll(List.of( + FormType.Category.MEISTER_TALENT, + FormType.Category.SOCIAL_INTEGRATION)); + else if (mainCategory.equals(FormType.Category.SUPERNUMERARY)) + subCategories.addAll(List.of( + FormType.Category.NATIONAL_VETERANS_EDUCATION, + FormType.Category.SPECIAL_ADMISSION)); + + Map> formLists = subCategories.stream() + .collect(Collectors.toMap( + category -> category, + category -> formRepository.findByCategory(category).stream() + .filter(form -> request.getStatusList().contains(form.getStatus())) + .collect(Collectors.toList()) + )); + + for(Map.Entry> entry : formLists.entrySet()) { + FormType.Category category = entry.getKey(); + List
formList = entry.getValue(); + + long busanMale = formList.stream().filter(this::isBusan).filter(this::isMale).count(); + long busanFemale = formList.stream().filter(this::isBusan).filter(this::isFemale).count(); + long otherLocationMale = formList.stream().filter(this::isNotBusan).filter(this::isMale).count(); + long otherLocationFemale = formList.stream().filter(this::isNotBusan).filter(this::isFemale).count(); + + result.add(new GenderRatioResponse(category, busanMale, busanFemale, otherLocationMale, otherLocationFemale)); + } + + return result; + } + + private boolean isBusan(Form form) { + return form.getEducation().getSchool().isBusan(); + } + + private boolean isNotBusan(Form form) { + return !isBusan(form); + } + + private boolean isMale(Form form) { + return form.getApplicant().getGender().equals(Gender.MALE); + } + + private boolean isFemale(Form form) { + return !isMale(form); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/application/analysis/QueryGradeDistributionUseCase.java b/src/main/java/com/bamdoliro/maru/application/analysis/QueryGradeDistributionUseCase.java new file mode 100644 index 00000000..21d2ecbb --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/application/analysis/QueryGradeDistributionUseCase.java @@ -0,0 +1,37 @@ +package com.bamdoliro.maru.application.analysis; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.FormRepository; +import com.bamdoliro.maru.presentation.analysis.dto.request.GradeDistributionRequest; +import com.bamdoliro.maru.presentation.analysis.dto.response.GradeDistributionResponse; +import com.bamdoliro.maru.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +@UseCase +public class QueryGradeDistributionUseCase { + + private final FormRepository formRepository; + + public List execute(GradeDistributionRequest request) { + List result = new java.util.ArrayList<>(formRepository.findGradeGroupByTypeAndStatus(request.getStatusList()) + .stream() + .map(GradeDistributionResponse::new) + .toList()); + + List existingTypes = result + .stream() + .map(GradeDistributionResponse::getType) + .toList(); + + for (FormType formType : FormType.values()) { + if (!existingTypes.contains(formType)) { + result.add(new GradeDistributionResponse(formType, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCase.java b/src/main/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCase.java new file mode 100644 index 00000000..0b5ff0f8 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCase.java @@ -0,0 +1,37 @@ +package com.bamdoliro.maru.application.analysis; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.FormRepository; +import com.bamdoliro.maru.presentation.analysis.dto.response.NumberOfApplicantsResponse; +import com.bamdoliro.maru.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@UseCase +public class QueryNumberOfApplicantsUseCase { + + private final FormRepository formRepository; + + public List execute() { + List result = formRepository.findTypeAndCountGroupByType() + .stream() + .map(NumberOfApplicantsResponse::new) + .collect(Collectors.toList()); + + List existingTypes = result + .stream() + .map(NumberOfApplicantsResponse::getType) + .toList(); + + for (FormType formType: FormType.values()) { + if (!existingTypes.contains(formType)) { + result.add(new NumberOfApplicantsResponse(formType, 0L)); + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/application/analysis/QuerySchoolStatusUseCase.java b/src/main/java/com/bamdoliro/maru/application/analysis/QuerySchoolStatusUseCase.java new file mode 100644 index 00000000..883b31d9 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/application/analysis/QuerySchoolStatusUseCase.java @@ -0,0 +1,33 @@ +package com.bamdoliro.maru.application.analysis; + +import com.bamdoliro.maru.infrastructure.persistence.form.FormRepository; +import com.bamdoliro.maru.presentation.analysis.dto.request.SchoolStatusRequest; +import com.bamdoliro.maru.presentation.analysis.dto.response.SchoolStatusResponse; +import com.bamdoliro.maru.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +@UseCase +public class QuerySchoolStatusUseCase { + + private final FormRepository formRepository; + + public List execute(SchoolStatusRequest request) { + if (request.getIsBusan()) { + String keyword = "부산광역시"; + keyword += request.getGu() == null ? "" : (" " + request.getGu()); + + return formRepository.findSchoolByAddress(request.getStatusList(), keyword) + .stream() + .map(SchoolStatusResponse::new) + .toList(); + } + + return formRepository.findNotBusanSchool(request.getStatusList()) + .stream() + .map(SchoolStatusResponse::new) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/domain/form/domain/type/FormType.java b/src/main/java/com/bamdoliro/maru/domain/form/domain/type/FormType.java index 18c9fbfa..fa827815 100644 --- a/src/main/java/com/bamdoliro/maru/domain/form/domain/type/FormType.java +++ b/src/main/java/com/bamdoliro/maru/domain/form/domain/type/FormType.java @@ -25,8 +25,8 @@ public enum FormType implements EnumProperty { MULTI_CHILDREN("다자녀가정자녀", Category.SPECIAL, Category.SOCIAL_INTEGRATION, Category.SOCIETY_DIVERSITY), FARMING_AND_FISHING("농어촌지역출신자", Category.SPECIAL, Category.SOCIAL_INTEGRATION, Category.SOCIETY_DIVERSITY), - NATIONAL_VETERANS_EDUCATION("국가보훈대상자 중 교육지원대상자녀", Category.SUPERNUMERARY, null, null), - SPECIAL_ADMISSION("특례입학대상자", Category.SUPERNUMERARY, null, null), + NATIONAL_VETERANS_EDUCATION("국가보훈대상자 중 교육지원대상자녀", Category.SUPERNUMERARY, Category.NATIONAL_VETERANS_EDUCATION, null), + SPECIAL_ADMISSION("특례입학대상자", Category.SUPERNUMERARY, Category.SPECIAL_ADMISSION, null), ; @@ -46,6 +46,8 @@ public enum Category implements EnumProperty { // Middle Category SOCIAL_INTEGRATION("사회통합전형"), MEISTER_TALENT("마이스터인재전형"), + NATIONAL_VETERANS_EDUCATION("국가보훈대상자 중 교육지원대상자녀"), + SPECIAL_ADMISSION("특례입학대상자"), // Sub Category EQUAL_OPPORTUNITY("기회균등전형"), diff --git a/src/main/java/com/bamdoliro/maru/domain/form/domain/value/School.java b/src/main/java/com/bamdoliro/maru/domain/form/domain/value/School.java index 10125bff..84684803 100644 --- a/src/main/java/com/bamdoliro/maru/domain/form/domain/value/School.java +++ b/src/main/java/com/bamdoliro/maru/domain/form/domain/value/School.java @@ -19,10 +19,13 @@ public class School { @Column(name = "school_location", nullable = false, length = 20) private String location; + @Column(name = "school_address", nullable = false, length = 40) + private String address; + @Column(name = "school_code", nullable = false, length = 10) private String code; public boolean isBusan() { return location.equals("부산광역시"); } -} +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/neis/SearchSchoolService.java b/src/main/java/com/bamdoliro/maru/infrastructure/neis/SearchSchoolService.java index 1bf2cf34..19acfd86 100644 --- a/src/main/java/com/bamdoliro/maru/infrastructure/neis/SearchSchoolService.java +++ b/src/main/java/com/bamdoliro/maru/infrastructure/neis/SearchSchoolService.java @@ -27,6 +27,7 @@ public List execute(String q) throws JsonProcessingException { .map(s -> SchoolResponse.builder() .name(s.getSchoolName()) .location(s.getLocation()) + .address((s.getAddress())) .code(s.getStandardSchoolCode()) .build()) .toList(); diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryCustom.java b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryCustom.java index 7c2dd0ab..4f9cbe87 100644 --- a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryCustom.java +++ b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryCustom.java @@ -2,13 +2,19 @@ import com.bamdoliro.maru.domain.form.domain.Form; import com.bamdoliro.maru.domain.form.domain.type.FormStatus; +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.NumberOfApplicantsVo; import com.bamdoliro.maru.infrastructure.persistence.form.vo.FormUrlVo; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.GradeVo; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.SchoolStatusVo; import java.util.List; public interface FormRepositoryCustom { List findByStatus(FormStatus status); + List findByType(FormType type); + List findByCategory(FormType.Category category); List findReceivedSpecialForm(); List findReceivedRegularOrSupernumeraryForm(); List findFirstRoundForm(); @@ -18,4 +24,8 @@ public interface FormRepositoryCustom { List findSecondRoundForm(); List findByFormIdList(List idList); List findFormUrlByFormIdList(List idList); -} + List findTypeAndCountGroupByType(); + List findGradeGroupByTypeAndStatus(List round); + List findSchoolByAddress(List round, String keyword); + List findNotBusanSchool(List round); +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryImpl.java b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryImpl.java index d8cc004e..5a09b03f 100644 --- a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryImpl.java +++ b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/FormRepositoryImpl.java @@ -3,8 +3,7 @@ import com.bamdoliro.maru.domain.form.domain.Form; import com.bamdoliro.maru.domain.form.domain.type.FormStatus; import com.bamdoliro.maru.domain.form.domain.type.FormType; -import com.bamdoliro.maru.infrastructure.persistence.form.vo.FormUrlVo; -import com.bamdoliro.maru.infrastructure.persistence.form.vo.QFormUrlVo; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.*; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -12,6 +11,7 @@ import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import static com.bamdoliro.maru.domain.form.domain.QForm.form; @@ -30,6 +30,26 @@ public List findByStatus(FormStatus status) { .fetch(); } + @Override + public List findByType(FormType type) { + return queryFactory + .selectFrom(form) + .where(eqType(type)) + .orderBy(form.examinationNumber.asc()) + .fetch(); + } + + @Override + public List findByCategory(FormType.Category category) { + List matchingFormTypes = getFormTypesByCategory(category); + + return queryFactory + .selectFrom(form) + .where(form.type.in(matchingFormTypes)) + .orderBy(form.examinationNumber.asc()) + .fetch(); + } + private BooleanExpression eqStatus(FormStatus status) { if (Objects.isNull(status)) { return null; @@ -38,6 +58,20 @@ private BooleanExpression eqStatus(FormStatus status) { return form.status.eq(status); } + private BooleanExpression eqType(FormType type) { + if (Objects.isNull(type)) { + return null; + } + + return form.type.eq(type); + } + + private List getFormTypesByCategory(FormType.Category category) { + return Stream.of(FormType.values()) + .filter(formType -> formType.categoryEquals(category)) + .toList(); + } + @Override public List findReceivedSpecialForm() { return queryFactory @@ -151,4 +185,63 @@ public List findFormUrlByFormIdList(List idList) { .orderBy(form.examinationNumber.asc()) .fetch(); } -} + + @Override + public List findTypeAndCountGroupByType() { + return queryFactory + .select(new QNumberOfApplicantsVo( + form.type, + form.count() + )) + .from(form) + .groupBy(form.type) + .fetch(); + } + + @Override + public List findGradeGroupByTypeAndStatus(List round) { + return queryFactory + .select(new QGradeVo( + form.type, + form.score.firstRoundScore.max(), + form.score.firstRoundScore.min(), + form.score.firstRoundScore.avg(), + form.score.totalScore.max(), + form.score.totalScore.min(), + form.score.totalScore.avg() + )) + .from(form) + .where(form.status.in(round)) + .groupBy(form.type) + .fetch(); + } + + @Override + public List findSchoolByAddress(List round, String keyword) { + return queryFactory + .select(new QSchoolStatusVo( + form.applicant.name, + form.education.school.name, + form.education.school.address + )) + .from(form) + .where(form.education.school.address.contains(keyword) + .or(form.education.school.location.eq(keyword)) + .and(form.status.in(round))) + .fetch(); + } + + @Override + public List findNotBusanSchool(List round) { + return queryFactory + .select(new QSchoolStatusVo( + form.applicant.name, + form.education.school.name, + form.education.school.address + )) + .from(form) + .where(form.education.school.location.eq("부산광역시").not() + .and(form.status.in(round))) + .fetch(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/GradeVo.java b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/GradeVo.java new file mode 100644 index 00000000..41755097 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/GradeVo.java @@ -0,0 +1,27 @@ +package com.bamdoliro.maru.infrastructure.persistence.form.vo; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; + +@Getter +public class GradeVo { + private FormType type; + private Double firstRoundMax; + private Double firstRoundMin; + private Double firstRoundAvg; + private Double totalMax; + private Double totalMin; + private Double totalAvg; + + @QueryProjection + public GradeVo(FormType type, Double firstRoundMax, Double firstRoundMin, Double firstRoundAvg, Double totalMax, Double totalMin, Double totalAvg) { + this.type = type; + this.firstRoundMax = firstRoundMax; + this.firstRoundMin = firstRoundMin; + this.firstRoundAvg = firstRoundAvg; + this.totalMax = totalMax; + this.totalMin = totalMin; + this.totalAvg = totalAvg; + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/NumberOfApplicantsVo.java b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/NumberOfApplicantsVo.java new file mode 100644 index 00000000..8f9c080c --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/NumberOfApplicantsVo.java @@ -0,0 +1,17 @@ +package com.bamdoliro.maru.infrastructure.persistence.form.vo; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; + +@Getter +public class NumberOfApplicantsVo { + private FormType type; + private Long count; + + @QueryProjection + public NumberOfApplicantsVo(FormType type, Long count) { + this.type = type; + this.count = count; + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/SchoolStatusVo.java b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/SchoolStatusVo.java new file mode 100644 index 00000000..4350ba73 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/infrastructure/persistence/form/vo/SchoolStatusVo.java @@ -0,0 +1,18 @@ +package com.bamdoliro.maru.infrastructure.persistence.form.vo; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; + +@Getter +public class SchoolStatusVo { + private String applicantName; + private String schoolName; + private String schoolAddress; + + @QueryProjection + public SchoolStatusVo(String applicantName, String schoolName, String schoolAddress) { + this.applicantName = applicantName; + this.schoolName = schoolName; + this.schoolAddress = schoolAddress; + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/AnalysisController.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/AnalysisController.java new file mode 100644 index 00000000..b5305f49 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/AnalysisController.java @@ -0,0 +1,70 @@ +package com.bamdoliro.maru.presentation.analysis; + +import com.bamdoliro.maru.application.analysis.QueryGenderRatioUseCase; +import com.bamdoliro.maru.application.analysis.QueryNumberOfApplicantsUseCase; +import com.bamdoliro.maru.application.analysis.QueryGradeDistributionUseCase; +import com.bamdoliro.maru.application.analysis.QuerySchoolStatusUseCase; +import com.bamdoliro.maru.domain.user.domain.User; +import com.bamdoliro.maru.presentation.analysis.dto.request.GenderRatioRequest; +import com.bamdoliro.maru.presentation.analysis.dto.request.GradeDistributionRequest; +import com.bamdoliro.maru.presentation.analysis.dto.request.SchoolStatusRequest; +import com.bamdoliro.maru.presentation.analysis.dto.response.GenderRatioResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.NumberOfApplicantsResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.GradeDistributionResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.SchoolStatusResponse; +import com.bamdoliro.maru.shared.auth.AuthenticationPrincipal; +import com.bamdoliro.maru.shared.auth.Authority; +import com.bamdoliro.maru.shared.response.CommonResponse; +import com.bamdoliro.maru.shared.response.ListCommonResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/analysis") +@RestController +public class AnalysisController { + private final QueryNumberOfApplicantsUseCase queryNumberOfApplicantsUseCase; + private final QueryGradeDistributionUseCase queryGradeDistributionUseCase; + private final QueryGenderRatioUseCase queryGenderRatioUseCase; + private final QuerySchoolStatusUseCase querySchoolStatusUseCase; + + @GetMapping("/number-of-applicants") + public ListCommonResponse getNumberOfApplicants( + @AuthenticationPrincipal(authority = Authority.ADMIN)User user + ) { + return CommonResponse.ok( + queryNumberOfApplicantsUseCase.execute() + ); + } + + @GetMapping("/grade-distribution") + public ListCommonResponse getGradeDistribution( + @AuthenticationPrincipal(authority = Authority.ADMIN)User user, + @ModelAttribute @Valid GradeDistributionRequest request + ) { + return CommonResponse.ok( + queryGradeDistributionUseCase.execute(request) + ); + } + + @GetMapping("/gender-ratio") + public ListCommonResponse getGenderRatio( + @AuthenticationPrincipal(authority = Authority.ADMIN)User user, + @ModelAttribute @Valid GenderRatioRequest request + ) { + return CommonResponse.ok( + queryGenderRatioUseCase.execute(request) + ); + } + + @GetMapping("/school-status") + public ListCommonResponse getSchoolStatus( + @AuthenticationPrincipal(authority = Authority.ADMIN)User user, + @ModelAttribute @Valid SchoolStatusRequest request + ) { + return CommonResponse.ok( + querySchoolStatusUseCase.execute(request) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GenderRatioRequest.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GenderRatioRequest.java new file mode 100644 index 00000000..e4af7ffa --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GenderRatioRequest.java @@ -0,0 +1,23 @@ +package com.bamdoliro.maru.presentation.analysis.dto.request; + +import com.bamdoliro.maru.domain.form.domain.type.FormStatus; +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class GenderRatioRequest { + @NotNull(message = "필수값입니다.") + private List statusList; + + @NotNull(message = "필수값입니다.") + private FormType.Category mainCategory; +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GradeDistributionRequest.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GradeDistributionRequest.java new file mode 100644 index 00000000..beb0333e --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/GradeDistributionRequest.java @@ -0,0 +1,20 @@ +package com.bamdoliro.maru.presentation.analysis.dto.request; + +import com.bamdoliro.maru.domain.form.domain.type.FormStatus; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class GradeDistributionRequest { + + @NotNull(message = "필수값입니다.") + List statusList; +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/SchoolStatusRequest.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/SchoolStatusRequest.java new file mode 100644 index 00000000..b1137393 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/request/SchoolStatusRequest.java @@ -0,0 +1,27 @@ +package com.bamdoliro.maru.presentation.analysis.dto.request; + +import com.bamdoliro.maru.domain.form.domain.type.FormStatus; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class SchoolStatusRequest { + + @NotNull(message = "필수값입니다.") + private List statusList; + + @NotNull(message = "필수값입니다.") + private Boolean isBusan; + + @Nullable + private String gu; +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GenderRatioResponse.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GenderRatioResponse.java new file mode 100644 index 00000000..152143c1 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GenderRatioResponse.java @@ -0,0 +1,17 @@ +package com.bamdoliro.maru.presentation.analysis.dto.response; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GenderRatioResponse { + private FormType.Category category; + private long busanMale; + private long busanFemale; + private long otherLocationMale; + private long otherLocationFemale; +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GradeDistributionResponse.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GradeDistributionResponse.java new file mode 100644 index 00000000..7104a85d --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/GradeDistributionResponse.java @@ -0,0 +1,30 @@ +package com.bamdoliro.maru.presentation.analysis.dto.response; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.GradeVo; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GradeDistributionResponse { + private FormType type; + private Double firstRoundMax; + private Double firstRoundMin; + private Double firstRoundAvg; + private Double totalMax; + private Double totalMin; + private Double totalAvg; + + public GradeDistributionResponse(GradeVo vo) { + this.type = vo.getType(); + this.firstRoundMax = vo.getFirstRoundMax(); + this.firstRoundMin = vo.getFirstRoundMin(); + this.firstRoundAvg = vo.getFirstRoundAvg(); + this.totalMax = vo.getTotalMax(); + this.totalMin = vo.getTotalMin(); + this.totalAvg = vo.getTotalAvg(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/NumberOfApplicantsResponse.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/NumberOfApplicantsResponse.java new file mode 100644 index 00000000..c46a0da5 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/NumberOfApplicantsResponse.java @@ -0,0 +1,20 @@ +package com.bamdoliro.maru.presentation.analysis.dto.response; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.NumberOfApplicantsVo; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NumberOfApplicantsResponse { + private FormType type; + private Long count; + + public NumberOfApplicantsResponse(NumberOfApplicantsVo vo){ + this.type = vo.getType(); + this.count = vo.getCount(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/SchoolStatusResponse.java b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/SchoolStatusResponse.java new file mode 100644 index 00000000..947081c7 --- /dev/null +++ b/src/main/java/com/bamdoliro/maru/presentation/analysis/dto/response/SchoolStatusResponse.java @@ -0,0 +1,21 @@ +package com.bamdoliro.maru.presentation.analysis.dto.response; + +import com.bamdoliro.maru.infrastructure.persistence.form.vo.SchoolStatusVo; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SchoolStatusResponse { + private String applicantName; + private String schoolName; + private String schoolAddress; + + public SchoolStatusResponse(SchoolStatusVo schoolStatusVo) { + this.applicantName = schoolStatusVo.getApplicantName(); + this.schoolName = schoolStatusVo.getSchoolName(); + this.schoolAddress = schoolStatusVo.getSchoolAddress(); + } +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/form/dto/request/EducationRequest.java b/src/main/java/com/bamdoliro/maru/presentation/form/dto/request/EducationRequest.java index c47bd342..838b6a7e 100644 --- a/src/main/java/com/bamdoliro/maru/presentation/form/dto/request/EducationRequest.java +++ b/src/main/java/com/bamdoliro/maru/presentation/form/dto/request/EducationRequest.java @@ -25,15 +25,19 @@ public class EducationRequest { @Size(min = 4, max = 4, message = "4자여야 합니다.") private String graduationYear; - @NotBlank + @NotBlank(message = "필수값입니다.") @Size(max = 20, message = "20자 이하여야 합니다.") private String schoolName; - @NotBlank + @NotBlank(message = "필수값입니다.") @Size(max = 20, message = "20자 이하여야 합니다.") private String schoolLocation; - @NotBlank + @NotBlank(message = "필수값입니다.") + @Size(max = 40, message = "40자 이하여야 합니다.") + private String schoolAddress; + + @NotBlank(message = "필수값입니다.") @Size(min = 7, max = 7, message = "7자여야 합니다.") private String schoolCode; @@ -56,6 +60,7 @@ public Education toValue() { new School( schoolName, schoolLocation, + schoolAddress, schoolCode ), new Teacher( @@ -65,4 +70,4 @@ public Education toValue() { ) ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/bamdoliro/maru/presentation/form/dto/response/EducationResponse.java b/src/main/java/com/bamdoliro/maru/presentation/form/dto/response/EducationResponse.java index bb4191da..cbe2e1ca 100644 --- a/src/main/java/com/bamdoliro/maru/presentation/form/dto/response/EducationResponse.java +++ b/src/main/java/com/bamdoliro/maru/presentation/form/dto/response/EducationResponse.java @@ -14,6 +14,7 @@ public class EducationResponse { private String schoolName; private String schoolLocation; private String schoolCode; + private String schoolAddress; private String teacherName; private String teacherPhoneNumber; @@ -22,6 +23,7 @@ public EducationResponse(Education education) { this.graduationYear = education.getGraduationYear(); this.schoolName = education.getSchool().getName(); this.schoolLocation = education.getSchool().getLocation(); + this.schoolAddress = education.getSchool().getAddress(); this.schoolCode = education.getSchool().getCode(); this.teacherName = education.getTeacher().getName(); this.teacherPhoneNumber = education.getTeacher().getPhoneNumber().toString(); diff --git a/src/main/java/com/bamdoliro/maru/presentation/school/dto/response/SchoolResponse.java b/src/main/java/com/bamdoliro/maru/presentation/school/dto/response/SchoolResponse.java index b9846bfc..0bbf9120 100644 --- a/src/main/java/com/bamdoliro/maru/presentation/school/dto/response/SchoolResponse.java +++ b/src/main/java/com/bamdoliro/maru/presentation/school/dto/response/SchoolResponse.java @@ -11,5 +11,6 @@ public class SchoolResponse { private String name; private String location; + private String address; private String code; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1e425339..602c8e54 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -78,7 +78,7 @@ spring: activate: on-profile: dev -debug: true +debug: false --- spring: config: @@ -101,7 +101,7 @@ spring: port: 6379 password: bamdoliro -debug: true +debug: false --- spring: config: @@ -135,4 +135,4 @@ spring: port: 6379 password: bamdoliro -debug: true +debug: false diff --git a/src/test/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCaseTest.java b/src/test/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCaseTest.java new file mode 100644 index 00000000..6da5b580 --- /dev/null +++ b/src/test/java/com/bamdoliro/maru/application/analysis/QueryNumberOfApplicantsUseCaseTest.java @@ -0,0 +1,52 @@ +package com.bamdoliro.maru.application.analysis; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.infrastructure.persistence.form.FormRepository; +import com.bamdoliro.maru.infrastructure.persistence.form.vo.NumberOfApplicantsVo; +import com.bamdoliro.maru.presentation.analysis.dto.response.NumberOfApplicantsResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class QueryNumberOfApplicantsUseCaseTest { + + @InjectMocks + private QueryNumberOfApplicantsUseCase queryNumberOfApplicantsUseCase; + + @Mock + private FormRepository formRepository; + + @Test + void 유저가_전형별_지원자_수를_조회한다() { + // given + List voList = List.of( + new NumberOfApplicantsVo(FormType.REGULAR, 1L), + new NumberOfApplicantsVo(FormType.MEISTER_TALENT, 1L), + new NumberOfApplicantsVo(FormType.NATIONAL_BASIC_LIVING, 1L), + new NumberOfApplicantsVo(FormType.FROM_NORTH_KOREA, 1L), + new NumberOfApplicantsVo(FormType.SPECIAL_ADMISSION, 1L) + ); + given(formRepository.findTypeAndCountGroupByType()).willReturn(voList); + + // when + List responseList = queryNumberOfApplicantsUseCase.execute(); + + // then + assertEquals(responseList.size(), FormType.values().length); + assertEquals(responseList + .stream() + .mapToLong(NumberOfApplicantsResponse::getCount) + .sum(), 5L); + + verify(formRepository, times(1)).findTypeAndCountGroupByType(); + } +} \ No newline at end of file diff --git a/src/test/java/com/bamdoliro/maru/presentation/analysis/AnalysisControllerTest.java b/src/test/java/com/bamdoliro/maru/presentation/analysis/AnalysisControllerTest.java new file mode 100644 index 00000000..36dc3583 --- /dev/null +++ b/src/test/java/com/bamdoliro/maru/presentation/analysis/AnalysisControllerTest.java @@ -0,0 +1,265 @@ +package com.bamdoliro.maru.presentation.analysis; + + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.domain.user.domain.User; +import com.bamdoliro.maru.presentation.analysis.dto.request.GenderRatioRequest; +import com.bamdoliro.maru.presentation.analysis.dto.request.GradeDistributionRequest; +import com.bamdoliro.maru.presentation.analysis.dto.request.SchoolStatusRequest; +import com.bamdoliro.maru.shared.fixture.AnalysisFixture; +import com.bamdoliro.maru.shared.fixture.AuthFixture; +import com.bamdoliro.maru.shared.fixture.UserFixture; +import com.bamdoliro.maru.shared.util.RestDocsTestSupport; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.List; +import java.util.Objects; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AnalysisControllerTest extends RestDocsTestSupport { + + @Test + void 전형별_지원자수를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + given(queryNumberOfApplicantsUseCase.execute()).willReturn(AnalysisFixture.createNumberOfApplicantsResponse()); + + mockMvc.perform(get("/analysis/number-of-applicants") + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ) + )); + } + + @Test + void _1차_합격자들의_성적_분포를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + given(queryGradeDistributionUseCase.execute(any(GradeDistributionRequest.class))).willReturn(AnalysisFixture.createGradeDistributionResponse()); + + mockMvc.perform(get("/analysis/grade-distribution") + .param("statusList", "FIRST_PASSED", "FAILED", "PASSED") + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)") + ) + )); + } + + @Test + void _2차_전형자들의_성적_분포를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + given(queryGradeDistributionUseCase.execute(any(GradeDistributionRequest.class))).willReturn(AnalysisFixture.createGradeDistributionResponse()); + + mockMvc.perform(get("/analysis/grade-distribution") + .param("statusList", "FAILED", "PASSED") + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)") + ) + )); + } + + @Test + void 최종_합격자들의_성적_분포를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + given(queryGradeDistributionUseCase.execute(any(GradeDistributionRequest.class))).willReturn(AnalysisFixture.createGradeDistributionResponse()); + + mockMvc.perform(get("/analysis/grade-distribution") + .param("statusList", "PASSED") + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)") + ) + )); + } + + @Test + void 전형별_성비를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.put("statusList", List.of("FIRST_PASSED", "FAILED", "PASSED")); + multiValueMap.add("mainCategory", "REGULAR"); + given(queryGenderRatioUseCase.execute(any(GenderRatioRequest.class))).willReturn(AnalysisFixture.createGenderRatioResponse( + FormType.Category.valueOf(Objects.requireNonNull(multiValueMap.get("mainCategory")).get(0)) + )); + + mockMvc.perform(get("/analysis/gender-ratio") + .params(multiValueMap) + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)"), + parameterWithName("mainCategory") + .description("메인 카테고리(FormType.Category 참고)") + ) + )); + } + + @Test + void 부산_특정구_출신_지원자들의_출신학교_통계를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.put("statusList", List.of("FIRST_PASSED", "FAILED", "PASSED")); + multiValueMap.add("isBusan", "true"); + multiValueMap.add("gu", "사상구"); + given(querySchoolStatusUseCase.execute(any(SchoolStatusRequest.class))).willReturn(AnalysisFixture.createSchoolStatusResponse(Objects.requireNonNull(multiValueMap.get("isBusan")), multiValueMap.get("gu"))); + + mockMvc.perform(get("/analysis/school-status") + .params(multiValueMap) + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)"), + parameterWithName("isBusan") + .description("부산 지역 학교 검색 여부(true면 부산, false면 부산 외 다른 모든 타지역)"), + parameterWithName("gu") + .description("구(isBusan이 true이고, null인 경우 부산지역 전체 조회)") + ) + )); + } + + @Test + void 부산_출신_지원자들의_출신학교_통계를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.put("statusList", List.of("FIRST_PASSED", "FAILED", "PASSED")); + multiValueMap.add("isBusan", "true"); + multiValueMap.add("gu", null); + given(querySchoolStatusUseCase.execute(any(SchoolStatusRequest.class))).willReturn(AnalysisFixture.createSchoolStatusResponse(Objects.requireNonNull(multiValueMap.get("isBusan")), multiValueMap.get("gu"))); + + mockMvc.perform(get("/analysis/school-status") + .params(multiValueMap) + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)"), + parameterWithName("isBusan") + .description("부산 지역 학교 검색 여부(true면 부산, false면 부산 외 다른 모든 타지역)"), + parameterWithName("gu") + .description("구(isBusan이 true이고, null인 경우 부산지역 전체 조회)") + ) + )); + } + + @Test + void 타지역_출신_지원자들의_출신학교_통계를_조회한다() throws Exception { + User user = UserFixture.createAdminUser(); + given(authenticationArgumentResolver.supportsParameter(any(MethodParameter.class))).willReturn(true); + given(authenticationArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(user); + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.put("statusList", List.of("FIRST_PASSED", "FAILED", "PASSED")); + multiValueMap.add("isBusan", "false"); + multiValueMap.add("gu", null); + given(querySchoolStatusUseCase.execute(any(SchoolStatusRequest.class))).willReturn(AnalysisFixture.createSchoolStatusResponse(Objects.requireNonNull(multiValueMap.get("isBusan")), multiValueMap.get("gu"))); + + mockMvc.perform(get("/analysis/school-status") + .params(multiValueMap) + .header(HttpHeaders.AUTHORIZATION, AuthFixture.createAuthHeader()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + + .andExpect(status().isOk()) + + .andDo(restDocs.document( + requestHeaders( + headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token") + ), + queryParameters( + parameterWithName("statusList") + .description("조회할 원서 상태 목록(1차 합격자면 FIRST_PASSED, FAILED, PASSED, 2차 전형자면 FAILED, PASSED, 최종 합격자면 PASSED)"), + parameterWithName("isBusan") + .description("부산 지역 학교 검색 여부(true면 부산, false면 부산 외 다른 모든 타지역)"), + parameterWithName("gu") + .description("구(isBusan이 true이고, null인 경우 부산지역 전체 조회)") + ) + )); + } +} \ No newline at end of file diff --git a/src/test/java/com/bamdoliro/maru/presentation/form/FormControllerTest.java b/src/test/java/com/bamdoliro/maru/presentation/form/FormControllerTest.java index a01d821e..17720d9a 100644 --- a/src/test/java/com/bamdoliro/maru/presentation/form/FormControllerTest.java +++ b/src/test/java/com/bamdoliro/maru/presentation/form/FormControllerTest.java @@ -141,6 +141,9 @@ class FormControllerTest extends RestDocsTestSupport { fieldWithPath("education.schoolLocation") .type(JsonFieldType.STRING) .description("출신 학교 지역"), + fieldWithPath("education.schoolAddress") + .type(JsonFieldType.STRING) + .description("출신 학교 주소지"), fieldWithPath("education.schoolCode") .type(JsonFieldType.STRING) .description("출신 학교 코드"), @@ -753,6 +756,9 @@ class FormControllerTest extends RestDocsTestSupport { fieldWithPath("education.schoolLocation") .type(JsonFieldType.STRING) .description("출신 학교 지역"), + fieldWithPath("education.schoolAddress") + .type(JsonFieldType.STRING) + .description("출신 학교 주소지"), fieldWithPath("education.schoolCode") .type(JsonFieldType.STRING) .description("출신 학교 코드"), diff --git a/src/test/java/com/bamdoliro/maru/shared/fixture/AnalysisFixture.java b/src/test/java/com/bamdoliro/maru/shared/fixture/AnalysisFixture.java new file mode 100644 index 00000000..66c9748c --- /dev/null +++ b/src/test/java/com/bamdoliro/maru/shared/fixture/AnalysisFixture.java @@ -0,0 +1,82 @@ +package com.bamdoliro.maru.shared.fixture; + +import com.bamdoliro.maru.domain.form.domain.type.FormType; +import com.bamdoliro.maru.presentation.analysis.dto.response.GenderRatioResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.GradeDistributionResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.NumberOfApplicantsResponse; +import com.bamdoliro.maru.presentation.analysis.dto.response.SchoolStatusResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static com.bamdoliro.maru.shared.util.RandomUtil.randomDouble; +import static com.bamdoliro.maru.shared.util.RandomUtil.randomNumber; + +public class AnalysisFixture { + + public static List createNumberOfApplicantsResponse() { + List responseList = new ArrayList<>(); + for (FormType formType : FormType.values()) { + responseList.add(new NumberOfApplicantsResponse(formType, (long)randomNumber(0, 50))); + } + return responseList; + } + + public static List createGradeDistributionResponse() { + List responseList = new ArrayList<>(); + for (FormType formType : FormType.values()) { + responseList.add(new GradeDistributionResponse(formType, randomDouble(0.0, 240.0), randomDouble(0.0, 240.0), randomDouble(0.0, 240.0), randomDouble(0.0, 240.0), randomDouble(0.0, 240.0), randomDouble(0.0, 240.0))); + } + return responseList; + } + + public static List createGenderRatioResponse(FormType.Category mainCategory) { + return List.of( + new GenderRatioResponse( + FormType.Category.REGULAR, + randomNumber(0, 30), + randomNumber(0, 15), + randomNumber(0, 30), + randomNumber(0, 15) + ) + ); + } + + public static List createSchoolStatusResponse(List isBusan, List gu) { + if (Objects.equals(isBusan.get(0), "true")) { + if (gu.get(0) == null) { + return List.of( + new SchoolStatusResponse( + "김밤돌", + "신라중학교", + "부산광역시 사상구 백양대로700번길 35-8" + ), + new SchoolStatusResponse( + "김이로", + "성동중학교", + "부산광역시 남구 남동천로 38" + ), + new SchoolStatusResponse( + "금곰돌", + "가락중학교", + "부산광역시 강서구 가락대로 1405") + ); + } + return List.of( + new SchoolStatusResponse( + "김밤돌", + "신라중학교", + "부산광역시 사상구 백양대로700번길 35-8" + ) + ); + } + return List.of( + new SchoolStatusResponse( + "김밤돌", + "비전중학교", + "경기도 비전시 비전구 비전로 1" + ) + ); + } +} diff --git a/src/test/java/com/bamdoliro/maru/shared/fixture/FormFixture.java b/src/test/java/com/bamdoliro/maru/shared/fixture/FormFixture.java index f2d20375..356af708 100644 --- a/src/test/java/com/bamdoliro/maru/shared/fixture/FormFixture.java +++ b/src/test/java/com/bamdoliro/maru/shared/fixture/FormFixture.java @@ -74,7 +74,7 @@ public static Form createForm(FormType type) { new Education( GraduationType.EXPECTED, "2021", - new School("비전중학교", "경기도", "7631003"), + new School("비전중학교", "경기도", "경기도 비전시 비전구 비전로 1", "7631003"), new Teacher("나교사", new PhoneNumber("0519701234"), new PhoneNumber("01012344321")) ), new Grade( @@ -140,7 +140,7 @@ public static Form createRandomForm(User user) { new Education( GraduationType.EXPECTED, "2021", - new School("비전중학교", "경기도", "7631003"), + new School("비전중학교", "경기도", "경기도 비전시 비전구 비전로 1", "7631003"), new Teacher("나교사", new PhoneNumber("0519701234"), new PhoneNumber("01012344321")) ), new Grade( @@ -218,7 +218,7 @@ public static Form createRandomQualificationExaminationForm(User user) { new Education( GraduationType.QUALIFICATION_EXAMINATION, "2021", - new School("비전중학교", "경기도", "7631003"), + new School("비전중학교", "경기도", "경기도 비전시 비전구 비전로 1", "7631003"), null ), new Grade( @@ -289,6 +289,7 @@ public static Form createQualificationExaminationForm(FormType type) { new School( "비전중학교", "경기도", + "경기도 비전시 비전구 비전로 1", "7631003" ), null @@ -328,6 +329,7 @@ public static SubmitFormRequest createFormRequest(FormType type) { "2021", "비전중학교", "경기도", + "경기도 비전시 비전구 비전로 1", "7631003", "나교사", "0519701234", @@ -369,6 +371,7 @@ public static SubmitFormRequest createQualificationExaminationFormRequest(FormTy "2021", "비전중학교", "경기도", + "경기도 비전시 비전구 비전로 1", "7631003", null, null, @@ -404,6 +407,7 @@ public static UpdateFormRequest createUpdateFormRequest(FormType type) { "2021", "비전중학교", "경기도", + "경기도 비전시 비전구 비전로 1", "7631003", "나교사", "0519701234", @@ -461,6 +465,7 @@ public static FormResponse createFormResponse() { "비전중학교", "경기도", "7631003", + "경기도 비전시 비전구 비전로 1", "나교사", "0519701234" ), @@ -550,4 +555,4 @@ public static FormUrlResponse createFormUrlResponse() { createFormUrlVo() ); } -} +} \ No newline at end of file diff --git a/src/test/java/com/bamdoliro/maru/shared/fixture/SchoolFixture.java b/src/test/java/com/bamdoliro/maru/shared/fixture/SchoolFixture.java index 46d468ea..f329dc87 100644 --- a/src/test/java/com/bamdoliro/maru/shared/fixture/SchoolFixture.java +++ b/src/test/java/com/bamdoliro/maru/shared/fixture/SchoolFixture.java @@ -8,22 +8,22 @@ public class SchoolFixture { public static List createSchoolListResponse() { return List.of( - new SchoolResponse("부산소프트웨어마이스터고등학교", "부산광역시", "7150658") - ); - } + new SchoolResponse("부산소프트웨어마이스터고등학교", "부산광역시", "부산광역시 강서구 가락대로 1393", "7150658") +); +} - public static List createSchoolMaxListResponse() { - return List.of( - new SchoolResponse("비전고등학교", "경기도", "7531109"), - new SchoolResponse("비전중학교", "경기도", "7631003"), - new SchoolResponse("비전초등학교", "경기도", "7631034"), - new SchoolResponse("공동체비전고등학교", "충청남도", "8140222"), - new SchoolResponse("다른비전고등학교", "경상북도", "7531109"), - new SchoolResponse("다른비전중학교", "경기도", "7631003"), - new SchoolResponse("다른비전초등학교", "경상남도", "7631034"), - new SchoolResponse("다른공동체비전고등학교", "충청남도", "8140222"), - new SchoolResponse("또다른비전고등학교", "대구광역시", "7531109"), - new SchoolResponse("또다른비전중학교", "경기도", "7631003") - ); - } +public static List createSchoolMaxListResponse() { +return List.of( +new SchoolResponse("비전고등학교", "경기도", "경기도 비전시 비전구 비전로 2", "7531109"), +new SchoolResponse("비전중학교", "경기도","경기도 비전시 비전구 비전로 1", "7631003"), +new SchoolResponse("비전초등학교", "경기도", "경기도 비전시 비전구 비전로 3", "7631034"), +new SchoolResponse("공동체비전고등학교", "충청남도", "충청남도 비전시 비전구 비전로 1", "8140222"), +new SchoolResponse("다른비전고등학교", "경상북도", "경상북도 비전시 비전구 비전로 1", "7531109"), +new SchoolResponse("다른비전중학교", "경기도", "경기도 비전시 비전구 비전로 4", "7631003"), +new SchoolResponse("다른비전초등학교", "경상남도", "경상남도 비전시 비전구 비전로 1", "7631034"), +new SchoolResponse("다른공동체비전고등학교", "충청남도", "충청남도 비전시 비전구 비전로 2", "8140222"), +new SchoolResponse("또다른비전고등학교", "대구광역시", "대구광역시 비전구 비전로 1", "7531109"), +new SchoolResponse("또다른비전중학교", "경기도", "경기도 비전시 비전구 비전로 5", "7631003") +); +} } diff --git a/src/test/java/com/bamdoliro/maru/shared/response/EnumControllerTest.java b/src/test/java/com/bamdoliro/maru/shared/response/EnumControllerTest.java index 2a383d01..56c80893 100644 --- a/src/test/java/com/bamdoliro/maru/shared/response/EnumControllerTest.java +++ b/src/test/java/com/bamdoliro/maru/shared/response/EnumControllerTest.java @@ -63,7 +63,7 @@ void enums() throws Exception { ), customResponseFields("custom-response", beneathPath("gender").withSubsectionId("gender"), - attributes(key("title").value("GraduationType")), + attributes(key("title").value("Gender")), enumConvertFieldDescriptor((enumDocs.getGender())) ), customResponseFields("custom-response", diff --git a/src/test/java/com/bamdoliro/maru/shared/response/SharedController.java b/src/test/java/com/bamdoliro/maru/shared/response/SharedController.java index b3459002..f2d7d2b0 100644 --- a/src/test/java/com/bamdoliro/maru/shared/response/SharedController.java +++ b/src/test/java/com/bamdoliro/maru/shared/response/SharedController.java @@ -36,7 +36,7 @@ public EnumDocs findEnums() { .certificate(getDocs(Certificate.values())) .formStatus(getDocs(FormStatus.values())) .formType(getDocs(FormType.values())) - .gender(getDocs(GraduationType.values())) + .gender(getDocs(Gender.values())) .graduationType(getDocs(GraduationType.values())) .fairStatus(getDocs(FairStatus.values())) .fairType(getDocs(FairType.values())) diff --git a/src/test/java/com/bamdoliro/maru/shared/util/ControllerTest.java b/src/test/java/com/bamdoliro/maru/shared/util/ControllerTest.java index 96bea802..19f1359c 100644 --- a/src/test/java/com/bamdoliro/maru/shared/util/ControllerTest.java +++ b/src/test/java/com/bamdoliro/maru/shared/util/ControllerTest.java @@ -1,5 +1,9 @@ package com.bamdoliro.maru.shared.util; +import com.bamdoliro.maru.application.analysis.QueryGenderRatioUseCase; +import com.bamdoliro.maru.application.analysis.QueryGradeDistributionUseCase; +import com.bamdoliro.maru.application.analysis.QueryNumberOfApplicantsUseCase; +import com.bamdoliro.maru.application.analysis.QuerySchoolStatusUseCase; import com.bamdoliro.maru.application.auth.LogInUseCase; import com.bamdoliro.maru.application.auth.LogOutUseCase; import com.bamdoliro.maru.application.auth.RefreshTokenUseCase; @@ -53,6 +57,7 @@ import com.bamdoliro.maru.domain.auth.service.TokenService; import com.bamdoliro.maru.infrastructure.message.SendMessageService; import com.bamdoliro.maru.infrastructure.neis.SearchSchoolService; +import com.bamdoliro.maru.presentation.analysis.AnalysisController; import com.bamdoliro.maru.presentation.auth.AuthController; import com.bamdoliro.maru.presentation.fair.FairController; import com.bamdoliro.maru.presentation.form.DraftFormController; @@ -75,7 +80,17 @@ import org.springframework.test.web.servlet.MockMvc; @Disabled -@WebMvcTest({UserController.class, AuthController.class, SharedController.class, SchoolController.class, QuestionController.class, FormController.class, NoticeController.class, DraftFormController.class, FairController.class, MessageController.class}) +@WebMvcTest({UserController.class, + AuthController.class, + SharedController.class, + SchoolController.class, + QuestionController.class, + FormController.class, + NoticeController.class, + DraftFormController.class, + FairController.class, + MessageController.class, + AnalysisController.class}) public abstract class ControllerTest { @Autowired @@ -232,6 +247,22 @@ public abstract class ControllerTest { @MockBean protected QueryFormUrlUseCase queryFormUrlUseCase; + @MockBean + protected SendMessageUseCase sendMessageUseCase; + + @MockBean + protected QueryNumberOfApplicantsUseCase queryNumberOfApplicantsUseCase; + + @MockBean + protected QueryGradeDistributionUseCase queryGradeDistributionUseCase; + + @MockBean + protected QueryGenderRatioUseCase queryGenderRatioUseCase; + + @MockBean + protected QuerySchoolStatusUseCase querySchoolStatusUseCase; + + @MockBean protected TokenService tokenService; @@ -241,8 +272,6 @@ public abstract class ControllerTest { @MockBean protected SendMessageService sendMessageService; - @MockBean - protected SendMessageUseCase sendMessageUseCase; @MockBean protected JwtProperties jwtProperties; diff --git a/src/test/java/com/bamdoliro/maru/shared/util/RandomUtil.java b/src/test/java/com/bamdoliro/maru/shared/util/RandomUtil.java index c1ab939c..76e39edc 100644 --- a/src/test/java/com/bamdoliro/maru/shared/util/RandomUtil.java +++ b/src/test/java/com/bamdoliro/maru/shared/util/RandomUtil.java @@ -9,4 +9,8 @@ public static String randomPhoneNumber() { public static int randomNumber(int min, int max) { return (int) ((Math.random() * (max - min)) + min); } + + public static double randomDouble(double min, double max) { + return Math.round(((Math.random() * (max - min)) + min) * 1000.0) / 1000.0; + } }