diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationApi.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationApi.java index 44674295..4b0ae96f 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationApi.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationApi.java @@ -1,7 +1,9 @@ package gg.agit.konect.domain.club.controller; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -9,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; +import gg.agit.konect.domain.club.dto.ClubApplicationCondition; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsResponse; @@ -44,6 +47,9 @@ ResponseEntity applyClub( - 동아리 관리자만 해당 동아리의 지원 내역을 조회할 수 있습니다. - 현재 지정된 모집 일정 범위에 지원한 내역만 볼 수 있습니다. - 상시 모집의 경우 모든 내역을 봅니다. + - 정렬 기준: APPLIED_AT(신청 일시), STUDENT_NUMBER(학번), NAME(이름) + - 정렬 방향: ASC(오름차순), DESC(내림차순) + - 기본 정렬: 신청 일시 최신순 (APPLIED_AT DESC) ## 에러 - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. @@ -53,6 +59,7 @@ ResponseEntity applyClub( @GetMapping("/{clubId}/applications") ResponseEntity getClubApplications( @PathVariable(name = "clubId") Integer clubId, + @Valid @ParameterObject @ModelAttribute ClubApplicationCondition condition, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationController.java index 0a4fedd7..55bdbf7b 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubApplicationController.java @@ -1,12 +1,15 @@ package gg.agit.konect.domain.club.controller; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; +import gg.agit.konect.domain.club.dto.ClubApplicationCondition; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsResponse; @@ -38,9 +41,10 @@ public ResponseEntity applyClub( @Override public ResponseEntity getClubApplications( @PathVariable(name = "clubId") Integer clubId, + @Valid @ParameterObject @ModelAttribute ClubApplicationCondition condition, @UserId Integer userId ) { - ClubApplicationsResponse response = clubService.getClubApplications(clubId, userId); + ClubApplicationsResponse response = clubService.getClubApplications(clubId, userId, condition); return ResponseEntity.ok(response); } diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationCondition.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationCondition.java new file mode 100644 index 00000000..847aad60 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationCondition.java @@ -0,0 +1,37 @@ +package gg.agit.konect.domain.club.dto; + +import org.springframework.data.domain.Sort; + +import gg.agit.konect.domain.club.enums.ClubApplicationSortBy; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +public record ClubApplicationCondition( + @Schema(description = "페이지 번호", example = "1", defaultValue = "1") + @Min(value = 1, message = "페이지 번호는 1 이상이어야 합니다") + Integer page, + + @Schema(description = "페이지 당 항목 수", example = "10", defaultValue = "10") + @Min(value = 1, message = "페이지 당 항목 수는 1 이상이어야 합니다") + @Max(value = 100, message = "페이지 당 항목 수는 100 이하여야 합니다") + Integer limit, + + @Schema(description = "정렬 기준", example = "APPLIED_AT", defaultValue = "APPLIED_AT") + ClubApplicationSortBy sortBy, + + @Schema(description = "정렬 방향", example = "DESC", defaultValue = "DESC") + Sort.Direction sortDirection +) { + private static final int DEFAULT_PAGE = 1; + private static final int DEFAULT_LIMIT = 10; + private static final ClubApplicationSortBy DEFAULT_SORT_BY = ClubApplicationSortBy.APPLIED_AT; + private static final Sort.Direction DEFAULT_SORT_DIRECTION = Sort.Direction.DESC; + + public ClubApplicationCondition { + page = page != null ? page : DEFAULT_PAGE; + limit = limit != null ? limit : DEFAULT_LIMIT; + sortBy = sortBy != null ? sortBy : DEFAULT_SORT_BY; + sortDirection = sortDirection != null ? sortDirection : DEFAULT_SORT_DIRECTION; + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationsResponse.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationsResponse.java index 82255be2..4bf09a62 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationsResponse.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationsResponse.java @@ -5,10 +5,24 @@ import java.time.LocalDateTime; import java.util.List; +import org.springframework.data.domain.Page; + import gg.agit.konect.domain.club.model.ClubApply; import io.swagger.v3.oas.annotations.media.Schema; public record ClubApplicationsResponse( + @Schema(description = "조건에 해당하는 지원 내역 총 개수", example = "10", requiredMode = REQUIRED) + Long totalCount, + + @Schema(description = "현재 페이지에서 조회된 지원 내역 개수", example = "5", requiredMode = REQUIRED) + Integer currentCount, + + @Schema(description = "최대 페이지", example = "2", requiredMode = REQUIRED) + Integer totalPage, + + @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) + Integer currentPage, + @Schema(description = "동아리 지원 내역 리스트", requiredMode = REQUIRED) List applications ) { @@ -41,9 +55,13 @@ public static ClubApplicationResponse from(ClubApply clubApply) { } } - public static ClubApplicationsResponse from(List clubApplies) { + public static ClubApplicationsResponse from(Page page) { return new ClubApplicationsResponse( - clubApplies.stream() + page.getTotalElements(), + page.getNumberOfElements(), + page.getTotalPages(), + page.getNumber() + 1, + page.getContent().stream() .map(ClubApplicationResponse::from) .toList() ); diff --git a/src/main/java/gg/agit/konect/domain/club/enums/ClubApplicationSortBy.java b/src/main/java/gg/agit/konect/domain/club/enums/ClubApplicationSortBy.java new file mode 100644 index 00000000..669caaab --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/enums/ClubApplicationSortBy.java @@ -0,0 +1,7 @@ +package gg.agit.konect.domain.club.enums; + +public enum ClubApplicationSortBy { + APPLIED_AT, + STUDENT_NUMBER, + NAME +} diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java new file mode 100644 index 00000000..6f003305 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java @@ -0,0 +1,110 @@ +package gg.agit.konect.domain.club.repository; + +import static gg.agit.konect.domain.club.model.QClubApply.clubApply; +import static gg.agit.konect.domain.user.model.QUser.user; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import gg.agit.konect.domain.club.dto.ClubApplicationCondition; +import gg.agit.konect.domain.club.enums.ClubApplicationSortBy; +import gg.agit.konect.domain.club.model.ClubApply; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ClubApplyQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Page findAllByClubId( + Integer clubId, + ClubApplicationCondition condition + ) { + PageRequest pageable = PageRequest.of(condition.page() - 1, condition.limit()); + OrderSpecifier orderSpecifier = createOrderSpecifier( + condition.sortBy(), + condition.sortDirection() + ); + + List content = jpaQueryFactory + .selectFrom(clubApply) + .join(clubApply.user, user).fetchJoin() + .where(clubApply.club.id.eq(clubId)) + .orderBy(orderSpecifier) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = jpaQueryFactory + .select(clubApply.count()) + .from(clubApply) + .where(clubApply.club.id.eq(clubId)) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + + public Page findAllByClubIdAndCreatedAtBetween( + Integer clubId, + LocalDateTime startDateTime, + LocalDateTime endDateTime, + ClubApplicationCondition condition + ) { + PageRequest pageable = PageRequest.of(condition.page() - 1, condition.limit()); + OrderSpecifier orderSpecifier = createOrderSpecifier( + condition.sortBy(), + condition.sortDirection() + ); + + List content = jpaQueryFactory + .selectFrom(clubApply) + .join(clubApply.user, user).fetchJoin() + .where( + clubApply.club.id.eq(clubId), + clubApply.createdAt.between(startDateTime, endDateTime) + ) + .orderBy(orderSpecifier) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = jpaQueryFactory + .select(clubApply.count()) + .from(clubApply) + .where( + clubApply.club.id.eq(clubId), + clubApply.createdAt.between(startDateTime, endDateTime) + ) + .fetchOne(); + + return new PageImpl<>(content, pageable, total != null ? total : 0L); + } + + private OrderSpecifier createOrderSpecifier( + ClubApplicationSortBy sortBy, + Sort.Direction sortDirection + ) { + boolean isAsc = sortDirection.isAscending(); + return switch (sortBy) { + case APPLIED_AT -> isAsc + ? clubApply.createdAt.asc() + : clubApply.createdAt.desc(); + case STUDENT_NUMBER -> isAsc + ? user.studentNumber.asc() + : user.studentNumber.desc(); + case NAME -> isAsc + ? user.name.asc() + : user.name.desc(); + }; + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index 0a6b1668..bd19c1de 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -22,6 +22,7 @@ import gg.agit.konect.domain.bank.repository.BankRepository; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; +import gg.agit.konect.domain.club.dto.ClubApplicationCondition; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubAppliedClubsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; @@ -55,6 +56,7 @@ import gg.agit.konect.domain.club.model.ClubRecruitmentImage; import gg.agit.konect.domain.club.model.ClubSummaryInfo; import gg.agit.konect.domain.club.repository.ClubApplyAnswerRepository; +import gg.agit.konect.domain.club.repository.ClubApplyQueryRepository; import gg.agit.konect.domain.club.repository.ClubApplyQuestionRepository; import gg.agit.konect.domain.club.repository.ClubApplyRepository; import gg.agit.konect.domain.club.repository.ClubMemberRepository; @@ -82,6 +84,7 @@ public class ClubService { EnumSet.of(PRESIDENT, VICE_PRESIDENT); private final ClubQueryRepository clubQueryRepository; + private final ClubApplyQueryRepository clubApplyQueryRepository; private final ClubRepository clubRepository; private final ClubMemberRepository clubMemberRepository; private final ClubPositionRepository clubPositionRepository; @@ -235,7 +238,11 @@ public ClubAppliedClubsResponse getAppliedClubs(Integer userId) { return ClubAppliedClubsResponse.from(clubApplies); } - public ClubApplicationsResponse getClubApplications(Integer clubId, Integer userId) { + public ClubApplicationsResponse getClubApplications( + Integer clubId, + Integer userId, + ClubApplicationCondition condition + ) { clubRepository.getById(clubId); if (!hasClubManageAccess(clubId, userId, MANAGER_ALLOWED_GROUPS)) { @@ -243,9 +250,9 @@ public ClubApplicationsResponse getClubApplications(Integer clubId, Integer user } ClubRecruitment recruitment = clubRecruitmentRepository.getByClubId(clubId); - List clubApplies = findApplicationsByRecruitmentPeriod(clubId, recruitment); + Page clubAppliesPage = findApplicationsByRecruitmentPeriod(clubId, recruitment, condition); - return ClubApplicationsResponse.from(clubApplies); + return ClubApplicationsResponse.from(clubAppliesPage); } public ClubApplicationAnswersResponse getClubApplicationAnswers( @@ -307,21 +314,23 @@ public void rejectClubApplication(Integer clubId, Integer applicationId, Integer clubApplyRepository.delete(clubApply); } - private List findApplicationsByRecruitmentPeriod( + private Page findApplicationsByRecruitmentPeriod( Integer clubId, - ClubRecruitment recruitment + ClubRecruitment recruitment, + ClubApplicationCondition condition ) { if (recruitment.getIsAlwaysRecruiting()) { - return clubApplyRepository.findAllByClubIdWithUser(clubId); + return clubApplyQueryRepository.findAllByClubId(clubId, condition); } LocalDateTime startDateTime = recruitment.getStartDate().atStartOfDay(); LocalDateTime endDateTime = recruitment.getEndDate().atTime(LocalTime.MAX); - return clubApplyRepository.findAllByClubIdAndCreatedAtBetweenWithUser( + return clubApplyQueryRepository.findAllByClubIdAndCreatedAtBetween( clubId, startDateTime, - endDateTime + endDateTime, + condition ); }