Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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;
import org.springframework.web.bind.annotation.RequestBody;
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;
Expand Down Expand Up @@ -44,6 +47,9 @@ ResponseEntity<ClubFeeInfoResponse> applyClub(
- 동아리 관리자만 해당 동아리의 지원 내역을 조회할 수 있습니다.
- 현재 지정된 모집 일정 범위에 지원한 내역만 볼 수 있습니다.
- 상시 모집의 경우 모든 내역을 봅니다.
- 정렬 기준: APPLIED_AT(신청 일시), STUDENT_NUMBER(학번), NAME(이름)
- 정렬 방향: ASC(오름차순), DESC(내림차순)
- 기본 정렬: 신청 일시 최신순 (APPLIED_AT DESC)

## 에러
- FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다.
Expand All @@ -53,6 +59,7 @@ ResponseEntity<ClubFeeInfoResponse> applyClub(
@GetMapping("/{clubId}/applications")
ResponseEntity<ClubApplicationsResponse> getClubApplications(
@PathVariable(name = "clubId") Integer clubId,
@Valid @ParameterObject @ModelAttribute ClubApplicationCondition condition,
@UserId Integer userId
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,9 +41,10 @@ public ResponseEntity<ClubFeeInfoResponse> applyClub(
@Override
public ResponseEntity<ClubApplicationsResponse> 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClubApplicationResponse> applications
) {
Expand Down Expand Up @@ -41,9 +55,13 @@ public static ClubApplicationResponse from(ClubApply clubApply) {
}
}

public static ClubApplicationsResponse from(List<ClubApply> clubApplies) {
public static ClubApplicationsResponse from(Page<ClubApply> page) {
return new ClubApplicationsResponse(
clubApplies.stream()
page.getTotalElements(),
page.getNumberOfElements(),
page.getTotalPages(),
page.getNumber() + 1,
page.getContent().stream()
.map(ClubApplicationResponse::from)
.toList()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gg.agit.konect.domain.club.enums;

public enum ClubApplicationSortBy {
APPLIED_AT,
STUDENT_NUMBER,
NAME
}
Original file line number Diff line number Diff line change
@@ -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<ClubApply> findAllByClubId(
Integer clubId,
ClubApplicationCondition condition
) {
PageRequest pageable = PageRequest.of(condition.page() - 1, condition.limit());
OrderSpecifier<?> orderSpecifier = createOrderSpecifier(
condition.sortBy(),
condition.sortDirection()
);

List<ClubApply> 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<ClubApply> 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<ClubApply> 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();
};
}
}
25 changes: 17 additions & 8 deletions src/main/java/gg/agit/konect/domain/club/service/ClubService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -235,17 +238,21 @@ 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)) {
throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS);
}

ClubRecruitment recruitment = clubRecruitmentRepository.getByClubId(clubId);
List<ClubApply> clubApplies = findApplicationsByRecruitmentPeriod(clubId, recruitment);
Page<ClubApply> clubAppliesPage = findApplicationsByRecruitmentPeriod(clubId, recruitment, condition);

return ClubApplicationsResponse.from(clubApplies);
return ClubApplicationsResponse.from(clubAppliesPage);
}

public ClubApplicationAnswersResponse getClubApplicationAnswers(
Expand Down Expand Up @@ -307,21 +314,23 @@ public void rejectClubApplication(Integer clubId, Integer applicationId, Integer
clubApplyRepository.delete(clubApply);
}

private List<ClubApply> findApplicationsByRecruitmentPeriod(
private Page<ClubApply> 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
);
}

Expand Down
Loading