From dee9d7f7727dcadcce86d640a49012272b20831c Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Tue, 27 Jan 2026 22:13:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A6=84/=ED=95=99?= =?UTF-8?q?=EB=B2=88/=EC=8B=A0=EC=B2=AD=EC=9D=BC=EC=8B=9C=20=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EC=9B=90=20=EB=82=B4=EC=97=AD=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/controller/ClubApplicationApi.java | 7 ++ .../controller/ClubApplicationController.java | 6 +- .../club/dto/ClubApplicationCondition.java | 37 ++++++ .../club/dto/ClubApplicationsResponse.java | 22 +++- .../club/enums/ClubApplicationSortBy.java | 7 ++ .../repository/ClubApplyQueryRepository.java | 110 ++++++++++++++++++ .../domain/club/service/ClubService.java | 25 ++-- 7 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubApplicationCondition.java create mode 100644 src/main/java/gg/agit/konect/domain/club/enums/ClubApplicationSortBy.java create mode 100644 src/main/java/gg/agit/konect/domain/club/repository/ClubApplyQueryRepository.java 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 ); } From c2837d3245e5da7bd6e574318d632708e4d2ab5e Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Thu, 29 Jan 2026 16:08:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20rebase=20=EC=A4=91=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/controller/ClubRecruitmentApi.java | 26 ++----------------- .../controller/ClubRecruitmentController.java | 17 +++--------- 2 files changed, 5 insertions(+), 38 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentApi.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentApi.java index e6f1725f..50a22930 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentApi.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentApi.java @@ -3,14 +3,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import gg.agit.konect.domain.club.dto.ClubRecruitmentCreateRequest; import gg.agit.konect.domain.club.dto.ClubRecruitmentResponse; -import gg.agit.konect.domain.club.dto.ClubRecruitmentUpdateRequest; +import gg.agit.konect.domain.club.dto.ClubRecruitmentUpsertRequest; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -37,26 +35,6 @@ ResponseEntity getRecruitments( @UserId Integer userId ); - @Operation(summary = "동아리 모집 정보를 생성한다.", description = """ - 동아리 회장만 모집 공고를 생성할 수 있습니다. - 한 동아리당 하나의 모집 공고만 생성 가능합니다. - - ## 에러 - - INVALID_RECRUITMENT_DATE_NOT_ALLOWED (400): 상시 모집인 경우 모집 시작일과 마감일을 지정할 수 없습니다. - - INVALID_RECRUITMENT_DATE_REQUIRED (400): 상시 모집이 아닐 경우 모집 시작일과 마감일이 필수입니다. - - INVALID_RECRUITMENT_PERIOD (400): 모집 시작일은 모집 마감일보다 이전이어야 합니다. - - FORBIDDEN_CLUB_RECRUITMENT_CREATE (403): 동아리 모집 공고를 생성할 권한이 없습니다. - - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. - - NOT_FOUND_USER (404): 유저를 찾을 수 없습니다. - - ALREADY_EXIST_CLUB_RECRUITMENT (409): 이미 동아리 모집 공고가 존재합니다. - """) - @PostMapping("/{clubId}/recruitments") - ResponseEntity createRecruitment( - @RequestBody @Valid ClubRecruitmentCreateRequest request, - @PathVariable(name = "clubId") Integer clubId, - @UserId Integer userId - ); - @Operation(summary = "동아리 모집 정보를 수정한다.", description = """ 동아리 회장 또는 부회장만 모집 공고를 수정할 수 있습니다. @@ -71,7 +49,7 @@ ResponseEntity createRecruitment( """) @PutMapping("/{clubId}/recruitments") ResponseEntity updateRecruitment( - @Valid @RequestBody ClubRecruitmentUpdateRequest request, + @Valid @RequestBody ClubRecruitmentUpsertRequest request, @PathVariable(name = "clubId") Integer clubId, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentController.java index 75563ddf..4d374429 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubRecruitmentController.java @@ -6,9 +6,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import gg.agit.konect.domain.club.dto.ClubRecruitmentCreateRequest; import gg.agit.konect.domain.club.dto.ClubRecruitmentResponse; -import gg.agit.konect.domain.club.dto.ClubRecruitmentUpdateRequest; +import gg.agit.konect.domain.club.dto.ClubRecruitmentUpsertRequest; import gg.agit.konect.domain.club.service.ClubService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; @@ -30,23 +29,13 @@ public ResponseEntity getRecruitments( return ResponseEntity.ok(response); } - @Override - public ResponseEntity createRecruitment( - @RequestBody @Valid ClubRecruitmentCreateRequest request, - @PathVariable(name = "clubId") Integer clubId, - @UserId Integer userId - ) { - clubService.createRecruitment(clubId, userId, request); - return ResponseEntity.ok().build(); - } - @Override public ResponseEntity updateRecruitment( - @Valid @RequestBody ClubRecruitmentUpdateRequest request, + @Valid @RequestBody ClubRecruitmentUpsertRequest request, @PathVariable(name = "clubId") Integer clubId, @UserId Integer userId ) { - clubService.updateRecruitment(clubId, userId, request); + clubService.upsertRecruitment(clubId, userId, request); return ResponseEntity.noContent().build(); } }