From 66465926a26db0a83e11e757667da25390666d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 27 Jan 2026 10:00:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=97=AC=EB=B6=80,=20=EC=83=81=EC=8B=9C?= =?UTF-8?q?=20=EB=AA=A8=EC=A7=91=20=EC=97=AC=EB=B6=80,=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EB=A7=88=EA=B0=90=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 --- .../domain/club/controller/ClubApi.java | 3 +++ .../konect/domain/club/dto/ClubsResponse.java | 25 ++++++++++++++++-- .../domain/club/model/ClubSummaryInfo.java | 3 +++ .../club/repository/ClubApplyRepository.java | 11 ++++++++ .../club/repository/ClubMemberRepository.java | 11 ++++++++ .../club/repository/ClubQueryRepository.java | 5 ++++ .../domain/club/service/ClubService.java | 26 ++++++++++++++++++- 7 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java index cd1e21db..3c3db2b7 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java @@ -51,6 +51,9 @@ public interface ClubApi { - isRecruiting가 true일 경우, 모집 중인 동아리만 조회하며 모집일(마감일)이 빠른 순으로 정렬됩니다. - isRecruiting가 false일 경우, 전체 동아리를 조회하되 모집 중인 동아리를 먼저 보여줍니다. - status은 BEFORE(모집 전), ONGOING(모집 중), CLOSED(모집 마감)으로 반환됩니다. + - isAlwaysRecruiting는 상시 모집 여부입니다. + - applicationDeadline는 모집 마감일(endDate)이며, 모집 공고가 없거나 상시 모집이면 null로 반환됩니다. + - isApplied는 가입 승인 대기 중 여부이며, 지원 내역이 존재하지만 아직 멤버가 아닌 경우 true로 반환됩니다. """) @GetMapping ResponseEntity getClubs( diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java index 42ffd2ee..8cd1b11f 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java @@ -2,10 +2,14 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import java.time.LocalDate; import java.util.List; +import java.util.Set; import org.springframework.data.domain.Page; +import com.fasterxml.jackson.annotation.JsonFormat; + import gg.agit.konect.domain.club.enums.RecruitmentStatus; import gg.agit.konect.domain.club.model.ClubSummaryInfo; import io.swagger.v3.oas.annotations.media.Schema; @@ -45,10 +49,20 @@ public record InnerClubResponse( @Schema(description = "동아리 모집 상태", example = "ONGOING", requiredMode = REQUIRED) RecruitmentStatus status, + @Schema(description = "가입 승인 대기 중 여부", example = "false", requiredMode = REQUIRED) + Boolean isApplied, + + @Schema(description = "상시 모집 여부", example = "false", requiredMode = REQUIRED) + Boolean isAlwaysRecruiting, + + @Schema(description = "지원 마감일", example = "2025.12.31", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy.MM.dd") + LocalDate applicationDeadline, + @Schema(description = "동아리 태그 리스트", example = "[\"IT\", \"프로그래밍\"]", requiredMode = REQUIRED) List tags ) { - public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo) { + public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo, boolean isApplied) { return new InnerClubResponse( clubSummaryInfo.id(), clubSummaryInfo.name(), @@ -56,19 +70,26 @@ public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo) { clubSummaryInfo.categoryName(), clubSummaryInfo.description(), clubSummaryInfo.status(), + isApplied, + clubSummaryInfo.isAlwaysRecruiting(), + clubSummaryInfo.applicationDeadline(), clubSummaryInfo.tags() ); } } public static ClubsResponse of(Page page) { + return of(page, Set.of()); + } + + public static ClubsResponse of(Page page, Set pendingAppliedClubIds) { return new ClubsResponse( page.getTotalElements(), page.getNumberOfElements(), page.getTotalPages(), page.getNumber() + 1, page.stream() - .map(InnerClubResponse::from) + .map(club -> InnerClubResponse.from(club, pendingAppliedClubIds.contains(club.id()))) .toList() ); } diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubSummaryInfo.java b/src/main/java/gg/agit/konect/domain/club/model/ClubSummaryInfo.java index 099c3c4f..9cc4b8ca 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubSummaryInfo.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubSummaryInfo.java @@ -1,5 +1,6 @@ package gg.agit.konect.domain.club.model; +import java.time.LocalDate; import java.util.List; import gg.agit.konect.domain.club.enums.RecruitmentStatus; @@ -11,6 +12,8 @@ public record ClubSummaryInfo( String categoryName, String description, RecruitmentStatus status, + Boolean isAlwaysRecruiting, + LocalDate applicationDeadline, List tags ) { diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java index fc71a0c4..7324ea4b 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubApplyRepository.java @@ -16,6 +16,17 @@ public interface ClubApplyRepository extends Repository { boolean existsByClubIdAndUserId(Integer clubId, Integer userId); + @Query(""" + SELECT ca.club.id + FROM ClubApply ca + WHERE ca.user.id = :userId + AND ca.club.id IN :clubIds + """) + List findClubIdsByUserIdAndClubIdIn( + @Param("userId") Integer userId, + @Param("clubIds") List clubIds + ); + ClubApply save(ClubApply clubApply); void deleteByUserId(Integer userId); diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java index b37b0927..16f375f6 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubMemberRepository.java @@ -109,6 +109,17 @@ default ClubMember getByClubIdAndUserId(Integer clubId, Integer userId) { boolean existsByClubIdAndUserId(Integer clubId, Integer userId); + @Query(""" + SELECT cm.id.clubId + FROM ClubMember cm + WHERE cm.id.userId = :userId + AND cm.id.clubId IN :clubIds + """) + List findClubIdsByUserIdAndClubIdIn( + @Param("userId") Integer userId, + @Param("clubIds") List clubIds + ); + List findByUserId(Integer userId); @Query(""" diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java index fc8a7f2f..4aaba0a2 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java @@ -203,6 +203,9 @@ private List convertToSummaryInfo(List clubs, Map convertToSummaryInfo(List clubs, Map pendingAppliedClubIds = findPendingAppliedClubIds(clubSummaryInfoPage, userId); + return ClubsResponse.of(clubSummaryInfoPage, pendingAppliedClubIds); + } + + private Set findPendingAppliedClubIds(Page clubSummaryInfoPage, Integer userId) { + List clubIds = clubSummaryInfoPage.getContent().stream() + .map(ClubSummaryInfo::id) + .filter(Objects::nonNull) + .toList(); + + if (clubIds.isEmpty()) { + return Set.of(); + } + + List appliedClubIds = clubApplyRepository.findClubIdsByUserIdAndClubIdIn(userId, clubIds); + if (appliedClubIds.isEmpty()) { + return Set.of(); + } + + Set pendingClubIds = new HashSet<>(appliedClubIds); + List memberClubIds = clubMemberRepository.findClubIdsByUserIdAndClubIdIn(userId, clubIds); + pendingClubIds.removeAll(memberClubIds); + + return pendingClubIds; } public ClubDetailResponse getClubDetail(Integer clubId, Integer userId) { From 25f35ead6130168f20d5714ac92096a5a3a6ed63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 27 Jan 2026 10:39:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=EC=97=AC=EB=B6=80=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/domain/club/controller/ClubApi.java | 2 +- .../agit/konect/domain/club/dto/ClubsResponse.java | 13 +++++++------ .../domain/club/repository/ClubQueryRepository.java | 5 ++++- .../konect/domain/club/service/ClubService.java | 6 +++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java index 3c3db2b7..742a7c3f 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubApi.java @@ -53,7 +53,7 @@ public interface ClubApi { - status은 BEFORE(모집 전), ONGOING(모집 중), CLOSED(모집 마감)으로 반환됩니다. - isAlwaysRecruiting는 상시 모집 여부입니다. - applicationDeadline는 모집 마감일(endDate)이며, 모집 공고가 없거나 상시 모집이면 null로 반환됩니다. - - isApplied는 가입 승인 대기 중 여부이며, 지원 내역이 존재하지만 아직 멤버가 아닌 경우 true로 반환됩니다. + - isPendingApproval은 가입 승인 대기 중 여부이며, 지원 내역이 존재하지만 아직 멤버가 아닌 경우 true로 반환됩니다. """) @GetMapping ResponseEntity getClubs( diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java index 8cd1b11f..ebeec3e0 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubsResponse.java @@ -1,6 +1,7 @@ package gg.agit.konect.domain.club.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import java.time.LocalDate; import java.util.List; @@ -50,19 +51,19 @@ public record InnerClubResponse( RecruitmentStatus status, @Schema(description = "가입 승인 대기 중 여부", example = "false", requiredMode = REQUIRED) - Boolean isApplied, + Boolean isPendingApproval, @Schema(description = "상시 모집 여부", example = "false", requiredMode = REQUIRED) Boolean isAlwaysRecruiting, - @Schema(description = "지원 마감일", example = "2025.12.31", requiredMode = REQUIRED) + @Schema(description = "지원 마감일(상시 모집이거나 모집 공고가 없으면 null)", example = "2025.12.31", requiredMode = NOT_REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd") LocalDate applicationDeadline, @Schema(description = "동아리 태그 리스트", example = "[\"IT\", \"프로그래밍\"]", requiredMode = REQUIRED) List tags ) { - public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo, boolean isApplied) { + public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo, boolean isPendingApproval) { return new InnerClubResponse( clubSummaryInfo.id(), clubSummaryInfo.name(), @@ -70,7 +71,7 @@ public static InnerClubResponse from(ClubSummaryInfo clubSummaryInfo, boolean is clubSummaryInfo.categoryName(), clubSummaryInfo.description(), clubSummaryInfo.status(), - isApplied, + isPendingApproval, clubSummaryInfo.isAlwaysRecruiting(), clubSummaryInfo.applicationDeadline(), clubSummaryInfo.tags() @@ -82,14 +83,14 @@ public static ClubsResponse of(Page page) { return of(page, Set.of()); } - public static ClubsResponse of(Page page, Set pendingAppliedClubIds) { + public static ClubsResponse of(Page page, Set pendingApprovalClubIds) { return new ClubsResponse( page.getTotalElements(), page.getNumberOfElements(), page.getTotalPages(), page.getNumber() + 1, page.stream() - .map(club -> InnerClubResponse.from(club, pendingAppliedClubIds.contains(club.id()))) + .map(club -> InnerClubResponse.from(club, pendingApprovalClubIds.contains(club.id()))) .toList() ); } diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java index 4aaba0a2..6f6f6484 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubQueryRepository.java @@ -205,6 +205,9 @@ private List convertToSummaryInfo(List clubs, Map convertToSummaryInfo(List clubs, Map pendingAppliedClubIds = findPendingAppliedClubIds(clubSummaryInfoPage, userId); - return ClubsResponse.of(clubSummaryInfoPage, pendingAppliedClubIds); + Set pendingApprovalClubIds = findPendingApprovalClubIds(clubSummaryInfoPage, userId); + return ClubsResponse.of(clubSummaryInfoPage, pendingApprovalClubIds); } - private Set findPendingAppliedClubIds(Page clubSummaryInfoPage, Integer userId) { + private Set findPendingApprovalClubIds(Page clubSummaryInfoPage, Integer userId) { List clubIds = clubSummaryInfoPage.getContent().stream() .map(ClubSummaryInfo::id) .filter(Objects::nonNull)