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
Expand Up @@ -11,6 +11,7 @@
import org.springframework.web.bind.annotation.RequestParam;

import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse;
import gg.agit.konect.domain.club.dto.ClubMemberApplicationAnswersResponse;
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 @@ -115,6 +116,29 @@ ResponseEntity<ClubApplicationAnswersResponse> getApprovedMemberApplicationAnswe
@UserId Integer requesterId
);

@Operation(summary = "승인된 회원들의 지원서를 리스트로 조회한다.", description = """
- 동아리 관리자만 해당 동아리의 승인된 회원 지원서를 리스트로 조회할 수 있습니다.
- 승인된 회원별 최신 지원서 답변을 리스트로 반환합니다.
- 정렬 기준: APPLIED_AT(신청 일시), STUDENT_NUMBER(학번), NAME(이름)
- 정렬 방향: ASC(오름차순), DESC(내림차순)
- 기본 정렬: 신청 일시 오래된 순 (APPLIED_AT ASC)

## 에러
- FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다.
- NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다.
""")
@GetMapping("/{clubId}/member-applications/answers")
ResponseEntity<ClubMemberApplicationAnswersResponse> getApprovedMemberApplicationAnswersList(
@PathVariable(name = "clubId") Integer clubId,
@Min(value = 1, message = "페이지 번호는 1 이상이어야 합니다.")
@RequestParam(defaultValue = "1") Integer page,
@Min(value = 1, message = "페이지 당 항목 수는 1 이상이어야 합니다.")
@RequestParam(defaultValue = "10") Integer limit,
@RequestParam(defaultValue = "APPLIED_AT") ClubApplicationSortBy sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction sortDirection,
@UserId Integer requesterId
);

@Operation(summary = "동아리 지원 답변을 조회한다.", description = """
- 동아리 관리자만 해당 동아리의 지원 답변을 조회할 수 있습니다.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse;
import gg.agit.konect.domain.club.dto.ClubApplicationCondition;
import gg.agit.konect.domain.club.dto.ClubMemberApplicationAnswersResponse;
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 @@ -87,6 +88,21 @@ public ResponseEntity<ClubApplicationAnswersResponse> getApprovedMemberApplicati
return ResponseEntity.ok(response);
}

@Override
public ResponseEntity<ClubMemberApplicationAnswersResponse> getApprovedMemberApplicationAnswersList(
@PathVariable(name = "clubId") Integer clubId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer limit,
@RequestParam(defaultValue = "APPLIED_AT") ClubApplicationSortBy sortBy,
@RequestParam(defaultValue = "ASC") Sort.Direction sortDirection,
@UserId Integer requesterId
) {
ClubApplicationCondition condition = new ClubApplicationCondition(page, limit, sortBy, sortDirection);
ClubMemberApplicationAnswersResponse response =
clubApplicationService.getApprovedMemberApplicationAnswersList(clubId, requesterId, condition);
return ResponseEntity.ok(response);
}

@Override
public ResponseEntity<ClubApplicationAnswersResponse> getClubApplicationAnswers(
@PathVariable(name = "clubId") Integer clubId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package gg.agit.konect.domain.club.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

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 ClubMemberApplicationAnswersResponse(
@Schema(description = "지원서 총 개수", example = "3", requiredMode = REQUIRED)
Long totalCount,

@Schema(description = "현재 페이지에서 조회된 지원서 개수", example = "3", 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<ClubApplicationAnswersResponse> applications
) {
public static ClubMemberApplicationAnswersResponse from(
Page<ClubApply> page,
List<ClubApplicationAnswersResponse> applications
) {
return new ClubMemberApplicationAnswersResponse(
page.getTotalElements(),
page.getNumberOfElements(),
page.getTotalPages(),
page.getNumber() + 1,
applications
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ public interface ClubApplyAnswerRepository extends Repository<ClubApplyAnswer, I
ORDER BY question.displayOrder ASC, question.id ASC
""")
List<ClubApplyAnswer> findAllByApplyIdWithQuestion(@Param("applyId") Integer applyId);

@Query("""
SELECT answer
FROM ClubApplyAnswer answer
JOIN FETCH answer.question question
WHERE answer.apply.id IN :applyIds
ORDER BY answer.apply.id ASC, question.displayOrder ASC, question.id ASC
""")
List<ClubApplyAnswer> findAllByApplyIdsWithQuestion(@Param("applyIds") List<Integer> applyIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import gg.agit.konect.domain.club.dto.ClubApplicationCondition;
Expand Down Expand Up @@ -158,21 +159,7 @@ public Page<ClubApply> findApprovedMemberApplicationsByClubId(
condition.sortDirection()
);

BooleanExpression isClubMember = isAlreadyClubMember(clubId);
BooleanExpression activeUserOnly = user.deletedAt.isNull();
BooleanExpression approvedOnly = clubApply.status.eq(ClubApplyStatus.APPROVED);
BooleanExpression latestApprovedApplicationOnly = isLatestApprovedApplicationByUser(clubId);

List<ClubApply> content = jpaQueryFactory
.selectFrom(clubApply)
.join(clubApply.user, user).fetchJoin()
.where(
clubApply.club.id.eq(clubId),
activeUserOnly,
isClubMember,
approvedOnly,
latestApprovedApplicationOnly
)
List<ClubApply> content = approvedMemberApplicationBaseQuery(clubId)
.orderBy(orderSpecifier)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
Expand All @@ -181,18 +168,30 @@ public Page<ClubApply> findApprovedMemberApplicationsByClubId(
Long total = jpaQueryFactory
.select(clubApply.count())
.from(clubApply)
.where(
clubApply.club.id.eq(clubId),
activeUserOnly,
isClubMember,
approvedOnly,
latestApprovedApplicationOnly
)
.join(clubApply.user, user)
.where(approvedMemberApplicationPredicates(clubId))
.fetchOne();

return new PageImpl<>(content, pageable, total != null ? total : 0L);
}

private JPAQuery<ClubApply> approvedMemberApplicationBaseQuery(Integer clubId) {
return jpaQueryFactory
.selectFrom(clubApply)
.join(clubApply.user, user).fetchJoin()
.where(approvedMemberApplicationPredicates(clubId));
}

private BooleanExpression[] approvedMemberApplicationPredicates(Integer clubId) {
return new BooleanExpression[] {
clubApply.club.id.eq(clubId),
user.deletedAt.isNull(),
isAlreadyClubMember(clubId),
clubApply.status.eq(ClubApplyStatus.APPROVED),
isLatestApprovedApplicationByUser(clubId)
};
}

private BooleanExpression isAlreadyClubMember(Integer clubId) {
return JPAExpressions
.selectOne()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ List<ClubApplyQuestion> findAllVisibleAtApplyTime(
@Param("appliedAt") LocalDateTime appliedAt
);

@Query("""
SELECT question
FROM ClubApplyQuestion question
WHERE question.club.id = :clubId
AND question.createdAt <= :maxAppliedAt
AND (question.deletedAt IS NULL OR question.deletedAt > :minAppliedAt)
ORDER BY question.displayOrder ASC, question.id ASC
""")
List<ClubApplyQuestion> findAllCandidatesVisibleBetweenApplyTimes(
@Param("clubId") Integer clubId,
@Param("minAppliedAt") LocalDateTime minAppliedAt,
@Param("maxAppliedAt") LocalDateTime maxAppliedAt
);

ClubApplyQuestion save(ClubApplyQuestion question);

List<ClubApplyQuestion> saveAll(Iterable<ClubApplyQuestion> questions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import gg.agit.konect.domain.chat.service.ChatRoomMembershipService;
import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse;
import gg.agit.konect.domain.club.dto.ClubApplicationCondition;
import gg.agit.konect.domain.club.dto.ClubMemberApplicationAnswersResponse;
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 @@ -118,11 +119,64 @@ public ClubApplicationAnswersResponse getApprovedMemberApplicationAnswers(
clubMemberRepository.getByClubIdAndUserId(clubId, targetUserId);

ClubApply clubApply = clubApplyRepository.getLatestApprovedByClubIdAndUserId(clubId, targetUserId);
List<ClubApplyQuestion> questions =
clubApplyQuestionRepository.findAllVisibleAtApplyTime(clubId, clubApply.getCreatedAt());
List<ClubApplyAnswer> answers = clubApplyAnswerRepository.findAllByApplyIdWithQuestion(clubApply.getId());
return toClubApplicationAnswersResponse(clubId, clubApply);
}

return ClubApplicationAnswersResponse.of(clubApply, questions, answers);
public ClubMemberApplicationAnswersResponse getApprovedMemberApplicationAnswersList(
Integer clubId,
Integer requesterId,
ClubApplicationCondition condition
) {
clubRepository.getById(clubId);

clubPermissionValidator.validateManagerAccess(clubId, requesterId);

Page<ClubApply> approvedApplicationsPage =
clubApplyQueryRepository.findApprovedMemberApplicationsByClubId(clubId, condition);
List<ClubApply> approvedApplications = approvedApplicationsPage.getContent();

if (approvedApplications.isEmpty()) {
return ClubMemberApplicationAnswersResponse.from(approvedApplicationsPage, List.of());
}

List<Integer> applyIds = approvedApplications.stream()
.map(ClubApply::getId)
.toList();
List<LocalDateTime> appliedAts = approvedApplications.stream()
.map(ClubApply::getCreatedAt)
.distinct()
.toList();

LocalDateTime minAppliedAt = appliedAts.stream()
.min(LocalDateTime::compareTo)
.orElseThrow(() -> new IllegalStateException("지원서 신청 시점이 비어 있습니다."));
LocalDateTime maxAppliedAt = appliedAts.stream()
.max(LocalDateTime::compareTo)
.orElseThrow(() -> new IllegalStateException("지원서 신청 시점이 비어 있습니다."));

Map<Integer, List<ClubApplyAnswer>> answersByApplyId = clubApplyAnswerRepository
.findAllByApplyIdsWithQuestion(applyIds)
.stream()
.collect(Collectors.groupingBy(answer -> answer.getApply().getId()));
List<ClubApplyQuestion> questionCandidates = clubApplyQuestionRepository
.findAllCandidatesVisibleBetweenApplyTimes(clubId, minAppliedAt, maxAppliedAt);
Map<LocalDateTime, List<ClubApplyQuestion>> questionsByAppliedAt = appliedAts.stream()
.collect(Collectors.toMap(
appliedAt -> appliedAt,
appliedAt -> questionCandidates.stream()
.filter(question -> isVisibleAtApplyTime(question, appliedAt))
.toList()
));

List<ClubApplicationAnswersResponse> responses = approvedApplications.stream()
.map(application -> toClubApplicationAnswersResponse(
application,
questionsByAppliedAt.getOrDefault(application.getCreatedAt(), List.of()),
answersByApplyId.getOrDefault(application.getId(), List.of())
))
.toList();

return ClubMemberApplicationAnswersResponse.from(approvedApplicationsPage, responses);
}

public ClubApplicationAnswersResponse getClubApplicationAnswers(
Expand All @@ -135,13 +189,40 @@ public ClubApplicationAnswersResponse getClubApplicationAnswers(
clubPermissionValidator.validateManagerAccess(clubId, userId);

ClubApply clubApply = clubApplyRepository.getByIdAndClubId(applicationId, clubId);
return toClubApplicationAnswersResponse(clubId, clubApply);
}

private ClubApplicationAnswersResponse toClubApplicationAnswersResponse(Integer clubId, ClubApply clubApply) {
List<ClubApplyAnswer> answers = clubApplyAnswerRepository.findAllByApplyIdWithQuestion(clubApply.getId());
return toClubApplicationAnswersResponse(clubId, clubApply, answers);
}

private ClubApplicationAnswersResponse toClubApplicationAnswersResponse(
Integer clubId,
ClubApply clubApply,
List<ClubApplyAnswer> answers
) {
List<ClubApplyQuestion> questions =
clubApplyQuestionRepository.findAllVisibleAtApplyTime(clubId, clubApply.getCreatedAt());
List<ClubApplyAnswer> answers = clubApplyAnswerRepository.findAllByApplyIdWithQuestion(applicationId);

return toClubApplicationAnswersResponse(clubApply, questions, answers);
}

private ClubApplicationAnswersResponse toClubApplicationAnswersResponse(
ClubApply clubApply,
List<ClubApplyQuestion> questions,
List<ClubApplyAnswer> answers
) {
return ClubApplicationAnswersResponse.of(clubApply, questions, answers);
}

private boolean isVisibleAtApplyTime(ClubApplyQuestion question, LocalDateTime appliedAt) {
LocalDateTime createdAt = question.getCreatedAt();
LocalDateTime deletedAt = question.getDeletedAt();
return (createdAt.isBefore(appliedAt) || createdAt.isEqual(appliedAt))
&& (deletedAt == null || deletedAt.isAfter(appliedAt));
}

@Transactional
public void approveClubApplication(Integer clubId, Integer applicationId, Integer userId) {
Club club = clubRepository.getById(clubId);
Expand Down