From 5ad96f381228627d568166d2ff044745b2835142 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Sun, 18 Jan 2026 19:31:31 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=EC=83=88=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=20=EC=BB=A4=EB=B0=8B=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gg/agit/konect/domain/club/service/ClubService.java | 1 + 1 file changed, 1 insertion(+) 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 69e2c167..474f3e45 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 @@ -84,6 +84,7 @@ public ClubsResponse getClubs(ClubCondition condition, Integer userId) { Page clubSummaryInfoPage = clubQueryRepository.findAllByFilter( pageable, condition.query(), condition.isRecruiting(), user.getUniversity().getId() ); + return ClubsResponse.of(clubSummaryInfoPage); } From 1e95ca40f1db47f0e22959bef868d50e9e667014 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 17:07:29 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/controller/ClubApi.java | 164 +++++++++++++- .../club/controller/ClubController.java | 97 ++++++++- .../domain/club/dto/ClubMemberCondition.java | 13 ++ .../club/dto/ClubPositionCreateRequest.java | 22 ++ .../club/dto/ClubPositionUpdateRequest.java | 16 ++ .../club/dto/ClubPositionsResponse.java | 56 +++++ .../club/dto/MemberPositionChangeRequest.java | 14 ++ .../club/dto/PresidentTransferRequest.java | 14 ++ .../club/dto/VicePresidentChangeRequest.java | 12 ++ .../domain/club/enums/ClubPositionGroup.java | 39 +++- .../konect/domain/club/model/ClubMember.java | 25 +++ .../domain/club/model/ClubPosition.java | 30 +++ .../club/repository/ClubMemberRepository.java | 36 ++++ .../repository/ClubPositionRepository.java | 110 ++++++++++ .../service/ClubMemberManagementService.java | 201 ++++++++++++++++++ .../club/service/ClubPositionService.java | 152 +++++++++++++ .../domain/club/service/ClubService.java | 14 +- .../konect/global/code/ApiResponseCode.java | 16 +- 18 files changed, 1021 insertions(+), 10 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubMemberCondition.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubPositionCreateRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubPositionUpdateRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/MemberPositionChangeRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/PresidentTransferRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/VicePresidentChangeRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java create mode 100644 src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java 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 7094f1cb..5bc66c5a 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 @@ -2,8 +2,10 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -19,12 +21,19 @@ import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; +import gg.agit.konect.domain.club.dto.ClubMemberCondition; import gg.agit.konect.domain.club.dto.ClubMembersResponse; import gg.agit.konect.domain.club.dto.ClubMembershipsResponse; +import gg.agit.konect.domain.club.dto.ClubPositionCreateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionUpdateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionsResponse; 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.ClubsResponse; +import gg.agit.konect.domain.club.dto.MemberPositionChangeRequest; +import gg.agit.konect.domain.club.dto.PresidentTransferRequest; +import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -98,10 +107,17 @@ ResponseEntity getClubApplicationAnswers( @UserId Integer userId ); - @Operation(summary = "동아리 멤버 리스트를 조회한다.") + @Operation(summary = "동아리 멤버 리스트를 조회한다.", description = """ + 동아리 회원만 멤버 리스트를 조회할 수 있습니다. + positionGroup 파라미터로 특정 직책 그룹의 회원만 필터링할 수 있습니다. + + ## 에러 + - FORBIDDEN_CLUB_MEMBER_ACCESS (403): 동아리 멤버 조회 권한이 없습니다. + """) @GetMapping("/{clubId}/members") ResponseEntity getClubMembers( @PathVariable(name = "clubId") Integer clubId, + @Valid @ParameterObject @ModelAttribute ClubMemberCondition condition, @UserId Integer userId ); @@ -231,4 +247,150 @@ ResponseEntity updateRecruitment( @PathVariable(name = "clubId") Integer clubId, @UserId Integer userId ); + + @Operation(summary = "동아리 직책 목록을 조회한다.", description = """ + 동아리의 모든 직책을 우선순위 순으로 조회합니다. + 각 직책의 회원 수, 수정/삭제 가능 여부도 함께 반환됩니다. + + ## 에러 + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + """) + @GetMapping("/{clubId}/positions") + ResponseEntity getClubPositions( + @PathVariable(name = "clubId") Integer clubId, + @UserId Integer userId + ); + + @Operation(summary = "동아리 직책을 생성한다.", description = """ + 동아리 회장 또는 매니저만 직책을 생성할 수 있습니다. + PRESIDENT와 VICE_PRESIDENT 직책은 생성할 수 없으며, MANAGER 또는 MEMBER 그룹의 직책만 생성 가능합니다. + + ## 에러 + - POSITION_NAME_DUPLICATED (400): 동일한 직책 이름이 이미 존재합니다. + - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + """) + @PostMapping("/{clubId}/positions") + ResponseEntity createClubPosition( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody ClubPositionCreateRequest request, + @UserId Integer userId + ); + + @Operation(summary = "동아리 직책의 이름을 수정한다.", description = """ + 동아리 회장 또는 매니저만 직책 이름을 수정할 수 있습니다. + PRESIDENT와 VICE_PRESIDENT 직책의 이름은 변경할 수 없습니다. + + ## 에러 + - POSITION_NAME_DUPLICATED (400): 동일한 직책 이름이 이미 존재합니다. + - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. + - FORBIDDEN_POSITION_NAME_CHANGE (403): 해당 직책의 이름은 변경할 수 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @PatchMapping("/{clubId}/positions/{positionId}") + ResponseEntity updateClubPositionName( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "positionId") Integer positionId, + @Valid @RequestBody ClubPositionUpdateRequest request, + @UserId Integer userId + ); + + @Operation(summary = "동아리 직책을 삭제한다.", description = """ + 동아리 회장 또는 매니저만 직책을 삭제할 수 있습니다. + PRESIDENT와 VICE_PRESIDENT 직책은 삭제할 수 없습니다. + 해당 직책을 사용 중인 회원이 없어야 하며, 해당 그룹에 최소 2개의 직책이 있어야 삭제 가능합니다. + + ## 에러 + - CANNOT_DELETE_ESSENTIAL_POSITION (400): 필수 직책은 삭제할 수 없습니다. + - POSITION_IN_USE (400): 해당 직책을 사용 중인 회원이 있어 삭제할 수 없습니다. + - INSUFFICIENT_POSITION_COUNT (400): 해당 그룹에 최소 2개의 직책이 있어야 삭제 가능합니다. + - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @DeleteMapping("/{clubId}/positions/{positionId}") + ResponseEntity deleteClubPosition( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "positionId") Integer positionId, + @UserId Integer userId + ); + + @Operation(summary = "동아리 회원의 직책을 변경한다.", description = """ + 동아리 회장 또는 매니저만 회원의 직책을 변경할 수 있습니다. + 자기 자신의 직책은 변경할 수 없으며, 상위 직급만 하위 직급의 회원을 관리할 수 있습니다. + + ## 에러 + - CANNOT_CHANGE_OWN_POSITION (400): 자기 자신의 직책은 변경할 수 없습니다. + - CANNOT_MANAGE_HIGHER_POSITION (400): 자신보다 높은 직급의 회원은 관리할 수 없습니다. + - VICE_PRESIDENT_ALREADY_EXISTS (409): 부회장은 이미 존재합니다. + - MANAGER_LIMIT_EXCEEDED (400): 운영진은 최대 20명까지 임명 가능합니다. + - FORBIDDEN_MEMBER_POSITION_CHANGE (403): 회원 직책 변경 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_MEMBER (404): 동아리 회원을 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @PatchMapping("/{clubId}/members/{memberId}/position") + ResponseEntity changeMemberPosition( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "memberId") Integer memberId, + @Valid @RequestBody MemberPositionChangeRequest request, + @UserId Integer userId + ); + + @Operation(summary = "동아리 회장 권한을 위임한다.", description = """ + 현재 회장만 회장 권한을 다른 회원에게 위임할 수 있습니다. + 회장 위임 시 현재 회장은 일반회원으로 강등됩니다. + + ## 에러 + - ILLEGAL_ARGUMENT (400): 자기 자신에게는 위임할 수 없습니다. + - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 회장 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_MEMBER (404): 동아리 회원을 찾을 수 없습니다. + - NOT_FOUND_CLUB_PRESIDENT (404): 동아리 회장을 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @PostMapping("/{clubId}/president/transfer") + ResponseEntity transferPresident( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody PresidentTransferRequest request, + @UserId Integer userId + ); + + @Operation(summary = "동아리 부회장을 변경한다.", description = """ + 동아리 회장만 부회장을 임명하거나 해제할 수 있습니다. + vicePresidentUserId가 null이면 부회장을 해제하고, 값이 있으면 해당 회원을 부회장으로 임명합니다. + + ## 에러 + - CANNOT_CHANGE_OWN_POSITION (400): 자기 자신을 부회장으로 임명할 수 없습니다. + - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 회장 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_MEMBER (404): 동아리 회원을 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @PatchMapping("/{clubId}/vice-president") + ResponseEntity changeVicePresident( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody VicePresidentChangeRequest request, + @UserId Integer userId + ); + + @Operation(summary = "동아리 회원을 강제 탈퇴시킨다.", description = """ + 동아리 회장 또는 매니저만 회원을 강제 탈퇴시킬 수 있습니다. + 자기 자신은 강제 탈퇴시킬 수 없으며, 회장은 강제 탈퇴시킬 수 없습니다. + 상위 직급만 하위 직급의 회원을 강제 탈퇴시킬 수 있습니다. + + ## 에러 + - CANNOT_REMOVE_SELF (400): 자기 자신을 강제 탈퇴시킬 수 없습니다. + - CANNOT_DELETE_CLUB_PRESIDENT (400): 회장은 강제 탈퇴시킬 수 없습니다. + - CANNOT_MANAGE_HIGHER_POSITION (400): 자신보다 높은 직급의 회원은 관리할 수 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_CLUB_MEMBER (404): 동아리 회원을 찾을 수 없습니다. + """) + @DeleteMapping("/{clubId}/members/{memberId}") + ResponseEntity removeMember( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "memberId") Integer memberId, + @UserId Integer userId + ); } diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java index 4c22b0de..62fc2001 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java @@ -17,12 +17,21 @@ import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; +import gg.agit.konect.domain.club.dto.ClubMemberCondition; import gg.agit.konect.domain.club.dto.ClubMembersResponse; import gg.agit.konect.domain.club.dto.ClubMembershipsResponse; +import gg.agit.konect.domain.club.dto.ClubPositionCreateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionUpdateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionsResponse; 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.ClubsResponse; +import gg.agit.konect.domain.club.dto.MemberPositionChangeRequest; +import gg.agit.konect.domain.club.dto.PresidentTransferRequest; +import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; +import gg.agit.konect.domain.club.service.ClubMemberManagementService; +import gg.agit.konect.domain.club.service.ClubPositionService; import gg.agit.konect.domain.club.service.ClubService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; @@ -34,6 +43,8 @@ public class ClubController implements ClubApi { private final ClubService clubService; + private final ClubPositionService clubPositionService; + private final ClubMemberManagementService clubMemberManagementService; @Override public ResponseEntity getClubs( @@ -94,9 +105,10 @@ public ResponseEntity getClubApplicationAnswers( @Override public ResponseEntity getClubMembers( @PathVariable(name = "clubId") Integer clubId, + @Valid @ParameterObject @ModelAttribute ClubMemberCondition condition, @UserId Integer userId ) { - ClubMembersResponse response = clubService.getClubMembers(clubId, userId); + ClubMembersResponse response = clubService.getClubMembers(clubId, userId, condition); return ResponseEntity.ok(response); } @@ -176,4 +188,87 @@ public ResponseEntity updateRecruitment( clubService.updateRecruitment(clubId, userId, request); return ResponseEntity.noContent().build(); } + + @Override + public ResponseEntity getClubPositions( + @PathVariable(name = "clubId") Integer clubId, + @UserId Integer userId + ) { + ClubPositionsResponse response = clubPositionService.getClubPositions(clubId, userId); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity createClubPosition( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody ClubPositionCreateRequest request, + @UserId Integer userId + ) { + ClubPositionsResponse response = clubPositionService.createClubPosition(clubId, userId, request); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity updateClubPositionName( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "positionId") Integer positionId, + @Valid @RequestBody ClubPositionUpdateRequest request, + @UserId Integer userId + ) { + ClubPositionsResponse response = clubPositionService.updateClubPositionName( + clubId, positionId, userId, request + ); + return ResponseEntity.ok(response); + } + + @Override + public ResponseEntity deleteClubPosition( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "positionId") Integer positionId, + @UserId Integer userId + ) { + clubPositionService.deleteClubPosition(clubId, positionId, userId); + return ResponseEntity.noContent().build(); + } + + @Override + public ResponseEntity changeMemberPosition( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "memberId") Integer memberId, + @Valid @RequestBody MemberPositionChangeRequest request, + @UserId Integer userId + ) { + clubMemberManagementService.changeMemberPosition(clubId, memberId, userId, request); + return ResponseEntity.noContent().build(); + } + + @Override + public ResponseEntity transferPresident( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody PresidentTransferRequest request, + @UserId Integer userId + ) { + clubMemberManagementService.transferPresident(clubId, userId, request); + return ResponseEntity.noContent().build(); + } + + @Override + public ResponseEntity changeVicePresident( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody VicePresidentChangeRequest request, + @UserId Integer userId + ) { + clubMemberManagementService.changeVicePresident(clubId, userId, request); + return ResponseEntity.noContent().build(); + } + + @Override + public ResponseEntity removeMember( + @PathVariable(name = "clubId") Integer clubId, + @PathVariable(name = "memberId") Integer memberId, + @UserId Integer userId + ) { + clubMemberManagementService.removeMember(clubId, memberId, userId); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubMemberCondition.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubMemberCondition.java new file mode 100644 index 00000000..53c16cd4 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubMemberCondition.java @@ -0,0 +1,13 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; + +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import io.swagger.v3.oas.annotations.media.Schema; + +public record ClubMemberCondition( + @Schema(description = "직책 그룹으로 필터링 (null이면 전체 조회)", example = "PRESIDENT", requiredMode = NOT_REQUIRED) + ClubPositionGroup positionGroup +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionCreateRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionCreateRequest.java new file mode 100644 index 00000000..2c837d68 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionCreateRequest.java @@ -0,0 +1,22 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record ClubPositionCreateRequest( + @NotBlank(message = "직책 이름은 필수 입력입니다.") + @Size(max = 50, message = "직책 이름은 최대 50자까지 입력 가능합니다.") + @Schema(description = "직책 이름", example = "부운영진", requiredMode = REQUIRED) + String name, + + @NotNull(message = "직책 그룹은 필수 입력입니다.") + @Schema(description = "직책 그룹 (MANAGER 또는 MEMBER만 가능)", example = "MANAGER", requiredMode = REQUIRED) + ClubPositionGroup positionGroup +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionUpdateRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionUpdateRequest.java new file mode 100644 index 00000000..759f20cc --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionUpdateRequest.java @@ -0,0 +1,16 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record ClubPositionUpdateRequest( + @NotBlank(message = "직책 이름은 필수 입력입니다.") + @Size(max = 50, message = "직책 이름은 최대 50자까지 입력 가능합니다.") + @Schema(description = "직책 이름", example = "총무", requiredMode = REQUIRED) + String name +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java new file mode 100644 index 00000000..59498c9e --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java @@ -0,0 +1,56 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import gg.agit.konect.domain.club.model.ClubPosition; +import io.swagger.v3.oas.annotations.media.Schema; + +public record ClubPositionsResponse( + @Schema(description = "직책 리스트", requiredMode = REQUIRED) + List positions +) { + public record InnerClubPosition( + @Schema(description = "직책 ID", example = "1", requiredMode = REQUIRED) + Integer positionId, + + @Schema(description = "직책 이름", example = "회장", requiredMode = REQUIRED) + String name, + + @Schema(description = "직책 그룹", example = "PRESIDENT", requiredMode = REQUIRED) + ClubPositionGroup positionGroup, + + @Schema(description = "우선순위 (낮을수록 높은 직급)", example = "0", requiredMode = REQUIRED) + Integer priority, + + @Schema(description = "해당 직책의 회원 수", example = "1", requiredMode = REQUIRED) + Long memberCount, + + @Schema(description = "이름 변경 가능 여부", example = "false", requiredMode = REQUIRED) + Boolean canRename, + + @Schema(description = "삭제 가능 여부", example = "false", requiredMode = REQUIRED) + Boolean canDelete + ) { + public static InnerClubPosition of( + ClubPosition position, + Long memberCount + ) { + return new InnerClubPosition( + position.getId(), + position.getName(), + position.getClubPositionGroup(), + position.getPriority(), + memberCount, + position.canRename(), + position.canDelete() + ); + } + } + + public static ClubPositionsResponse of(List positions) { + return new ClubPositionsResponse(positions); + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/MemberPositionChangeRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/MemberPositionChangeRequest.java new file mode 100644 index 00000000..7d0c71a6 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/MemberPositionChangeRequest.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record MemberPositionChangeRequest( + @NotNull(message = "직책 ID는 필수 입력입니다.") + @Schema(description = "변경할 직책 ID", example = "3", requiredMode = REQUIRED) + Integer positionId +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/PresidentTransferRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/PresidentTransferRequest.java new file mode 100644 index 00000000..2e4cb344 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/PresidentTransferRequest.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record PresidentTransferRequest( + @NotNull(message = "새 회장의 사용자 ID는 필수 입력입니다.") + @Schema(description = "새 회장으로 임명할 사용자 ID", example = "2", requiredMode = REQUIRED) + Integer newPresidentUserId +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/VicePresidentChangeRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/VicePresidentChangeRequest.java new file mode 100644 index 00000000..a65ec1fd --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/VicePresidentChangeRequest.java @@ -0,0 +1,12 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record VicePresidentChangeRequest( + @Schema(description = "부회장으로 임명할 사용자 ID (null이면 부회장 해제)", example = "3", requiredMode = NOT_REQUIRED) + Integer vicePresidentUserId +) { + +} diff --git a/src/main/java/gg/agit/konect/domain/club/enums/ClubPositionGroup.java b/src/main/java/gg/agit/konect/domain/club/enums/ClubPositionGroup.java index 7892a954..376c13bb 100644 --- a/src/main/java/gg/agit/konect/domain/club/enums/ClubPositionGroup.java +++ b/src/main/java/gg/agit/konect/domain/club/enums/ClubPositionGroup.java @@ -4,14 +4,45 @@ @Getter public enum ClubPositionGroup { - PRESIDENT("회장"), - MANAGER("운영진"), - MEMBER("일반회원"), + PRESIDENT("회장", 0, 1, 1), + VICE_PRESIDENT("부회장", 1, 0, 1), + MANAGER("운영진", 2, 0, 20), + MEMBER("일반회원", 3, 0, Integer.MAX_VALUE), ; private final String description; + private final int priority; + private final int minCount; + private final int maxCount; - ClubPositionGroup(String description) { + ClubPositionGroup(String description, int priority, int minCount, int maxCount) { this.description = description; + this.priority = priority; + this.minCount = minCount; + this.maxCount = maxCount; + } + + public boolean canManage(ClubPositionGroup target) { + return this.priority < target.priority; + } + + public boolean isHigherThan(ClubPositionGroup target) { + return this.priority < target.priority; + } + + public boolean isPresident() { + return this == PRESIDENT; + } + + public boolean isVicePresident() { + return this == VICE_PRESIDENT; + } + + public boolean isManager() { + return this == MANAGER; + } + + public boolean isMember() { + return this == MEMBER; } } diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java b/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java index 22e1321d..d3825eb6 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java @@ -3,6 +3,7 @@ import static jakarta.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; +import gg.agit.konect.domain.club.enums.ClubPositionGroup; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.global.model.BaseEntity; import jakarta.persistence.Column; @@ -58,4 +59,28 @@ public boolean isPresident() { public boolean isSameUser(Integer userId) { return this.user.getId().equals(userId); } + + public ClubPositionGroup getPositionGroup() { + return clubPosition.getClubPositionGroup(); + } + + public void changePosition(ClubPosition newPosition) { + this.clubPosition = newPosition; + } + + public boolean canManage(ClubMember target) { + return this.getPositionGroup().canManage(target.getPositionGroup()); + } + + public boolean isVicePresident() { + return clubPosition.isVicePresident(); + } + + public boolean isManager() { + return clubPosition.isManager(); + } + + public boolean isMember() { + return clubPosition.isMember(); + } } diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubPosition.java b/src/main/java/gg/agit/konect/domain/club/model/ClubPosition.java index 8e167db0..2a3928ce 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubPosition.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubPosition.java @@ -64,4 +64,34 @@ private ClubPosition(Integer id, String name, ClubPositionGroup clubPositionGrou public boolean isPresident() { return clubPositionGroup == ClubPositionGroup.PRESIDENT; } + + public boolean isVicePresident() { + return clubPositionGroup == ClubPositionGroup.VICE_PRESIDENT; + } + + public boolean isManager() { + return clubPositionGroup == ClubPositionGroup.MANAGER; + } + + public boolean isMember() { + return clubPositionGroup == ClubPositionGroup.MEMBER; + } + + public boolean canRename() { + return clubPositionGroup == ClubPositionGroup.MANAGER + || clubPositionGroup == ClubPositionGroup.MEMBER; + } + + public boolean canDelete() { + return clubPositionGroup == ClubPositionGroup.MANAGER + || clubPositionGroup == ClubPositionGroup.MEMBER; + } + + public void updateName(String newName) { + this.name = newName; + } + + public int getPriority() { + return clubPositionGroup.getPriority(); + } } 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 46eff01b..47c3d3d9 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 @@ -23,6 +23,19 @@ public interface ClubMemberRepository extends Repository findAllByClubId(@Param("clubId") Integer clubId); + @Query(""" + SELECT cm + FROM ClubMember cm + JOIN FETCH cm.user + JOIN FETCH cm.clubPosition cp + WHERE cm.club.id = :clubId + AND cp.clubPositionGroup = :positionGroup + """) + List findAllByClubIdAndPositionGroup( + @Param("clubId") Integer clubId, + @Param("positionGroup") ClubPositionGroup positionGroup + ); + @Query(""" SELECT cm FROM ClubMember cm @@ -99,5 +112,28 @@ boolean existsByClubIdAndUserIdAndPositionGroupIn( """) List findByUserIdIn(@Param("userIds") List userIds); + @Query(""" + SELECT COUNT(cm) + FROM ClubMember cm + WHERE cm.clubPosition.id = :positionId + """) + long countByPositionId(@Param("positionId") Integer positionId); + + @Query(""" + SELECT COUNT(cm) + FROM ClubMember cm + JOIN cm.clubPosition cp + WHERE cm.club.id = :clubId + AND cp.clubPositionGroup = :positionGroup + """) + long countByClubIdAndPositionGroup( + @Param("clubId") Integer clubId, + @Param("positionGroup") ClubPositionGroup positionGroup + ); + + void delete(ClubMember clubMember); + void deleteByUserId(Integer userId); + + ClubMember save(ClubMember clubMember); } diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java new file mode 100644 index 00000000..00b45bbc --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java @@ -0,0 +1,110 @@ +package gg.agit.konect.domain.club.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import gg.agit.konect.domain.club.model.ClubPosition; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; + +public interface ClubPositionRepository extends Repository { + + @Query(value = """ + SELECT cp + FROM ClubPosition cp + WHERE cp.id = :id + """) + Optional findById(@Param(value = "id") Integer id); + + default ClubPosition getById(Integer id) { + return findById(id).orElseThrow(() -> + CustomException.of(ApiResponseCode.NOT_FOUND_CLUB_POSITION)); + } + + @Query(value = """ + SELECT cp + FROM ClubPosition cp + WHERE cp.club.id = :clubId + ORDER BY cp.clubPositionGroup + """) + List findAllByClubId(@Param(value = "clubId") Integer clubId); + + @Query(value = """ + SELECT cp + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.clubPositionGroup = :positionGroup + """) + List findAllByClubIdAndPositionGroup( + @Param(value = "clubId") Integer clubId, + @Param(value = "positionGroup") ClubPositionGroup positionGroup + ); + + @Query(value = """ + SELECT cp + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.name = :name + """) + Optional findByClubIdAndName( + @Param(value = "clubId") Integer clubId, + @Param(value = "name") String name + ); + + @Query(value = """ + SELECT cp + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.clubPositionGroup = :positionGroup + ORDER BY cp.id + LIMIT 1 + """) + Optional findFirstByClubIdAndPositionGroup( + @Param(value = "clubId") Integer clubId, + @Param(value = "positionGroup") ClubPositionGroup positionGroup + ); + + @Query(value = """ + SELECT COUNT(cp) + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.clubPositionGroup = :positionGroup + """) + long countByClubIdAndPositionGroup( + @Param(value = "clubId") Integer clubId, + @Param(value = "positionGroup") ClubPositionGroup positionGroup + ); + + @Query(value = """ + SELECT CASE WHEN COUNT(cp) > 0 THEN true ELSE false END + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.name = :name + """) + boolean existsByClubIdAndName( + @Param(value = "clubId") Integer clubId, + @Param(value = "name") String name + ); + + @Query(value = """ + SELECT CASE WHEN COUNT(cp) > 0 THEN true ELSE false END + FROM ClubPosition cp + WHERE cp.club.id = :clubId + AND cp.name = :name + AND cp.id <> :id + """) + boolean existsByClubIdAndNameAndIdNot( + @Param(value = "clubId") Integer clubId, + @Param(value = "name") String name, + @Param(value = "id") Integer id + ); + + ClubPosition save(ClubPosition clubPosition); + + void delete(ClubPosition clubPosition); +} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java new file mode 100644 index 00000000..579ccc97 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -0,0 +1,201 @@ +package gg.agit.konect.domain.club.service; + +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT; +import static gg.agit.konect.global.code.ApiResponseCode.*; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.club.dto.MemberPositionChangeRequest; +import gg.agit.konect.domain.club.dto.PresidentTransferRequest; +import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.club.model.ClubPosition; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.club.repository.ClubPositionRepository; +import gg.agit.konect.domain.club.repository.ClubRepository; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ClubMemberManagementService { + + private final ClubRepository clubRepository; + private final ClubMemberRepository clubMemberRepository; + private final ClubPositionRepository clubPositionRepository; + private final UserRepository userRepository; + + @Transactional + public void changeMemberPosition( + Integer clubId, + Integer targetUserId, + Integer requesterId, + MemberPositionChangeRequest request + ) { + Club club = clubRepository.getById(clubId); + + if (targetUserId.equals(requesterId)) { + throw CustomException.of(CANNOT_CHANGE_OWN_POSITION); + } + + ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + ClubMember target = clubMemberRepository.findByClubIdAndUserId(clubId, targetUserId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + if (!requester.canManage(target)) { + throw CustomException.of(CANNOT_MANAGE_HIGHER_POSITION); + } + + ClubPosition newPosition = clubPositionRepository.getById(request.positionId()); + + if (!newPosition.getClub().getId().equals(clubId)) { + throw CustomException.of(NOT_FOUND_CLUB_POSITION); + } + + ClubPositionGroup newPositionGroup = newPosition.getClubPositionGroup(); + + if (!requester.getPositionGroup().canManage(newPositionGroup)) { + throw CustomException.of(FORBIDDEN_MEMBER_POSITION_CHANGE); + } + + if (newPositionGroup == VICE_PRESIDENT) { + long vicePresidentCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, VICE_PRESIDENT); + if (vicePresidentCount >= 1) { + throw CustomException.of(VICE_PRESIDENT_ALREADY_EXISTS); + } + } + + if (newPositionGroup == MANAGER) { + long managerCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, MANAGER); + if (managerCount >= 20) { + throw CustomException.of(MANAGER_LIMIT_EXCEEDED); + } + } + + target.changePosition(newPosition); + } + + @Transactional + public void transferPresident( + Integer clubId, + Integer currentPresidentId, + PresidentTransferRequest request + ) { + Club club = clubRepository.getById(clubId); + + ClubMember currentPresident = clubMemberRepository.findByClubIdAndUserId(clubId, currentPresidentId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_PRESIDENT)); + + if (!currentPresident.isPresident()) { + throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); + } + + Integer newPresidentUserId = request.newPresidentUserId(); + + if (currentPresidentId.equals(newPresidentUserId)) { + throw CustomException.of(ILLEGAL_ARGUMENT); + } + + ClubMember newPresident = clubMemberRepository.findByClubIdAndUserId(clubId, newPresidentUserId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + ClubPosition presidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, PRESIDENT) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + + currentPresident.changePosition(memberPosition); + newPresident.changePosition(presidentPosition); + } + + @Transactional + public void changeVicePresident( + Integer clubId, + Integer requesterId, + VicePresidentChangeRequest request + ) { + Club club = clubRepository.getById(clubId); + + ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + if (!requester.isPresident()) { + throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); + } + + ClubPosition vicePresidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, VICE_PRESIDENT) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + + Optional currentVicePresidentOpt = clubMemberRepository.findAllByClubIdAndPositionGroup(clubId, VICE_PRESIDENT) + .stream() + .findFirst(); + + Integer newVicePresidentUserId = request.vicePresidentUserId(); + + if (newVicePresidentUserId == null) { + if (currentVicePresidentOpt.isPresent()) { + ClubMember currentVicePresident = currentVicePresidentOpt.get(); + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + currentVicePresident.changePosition(memberPosition); + } + return; + } + + if (requesterId.equals(newVicePresidentUserId)) { + throw CustomException.of(CANNOT_CHANGE_OWN_POSITION); + } + + ClubMember newVicePresident = clubMemberRepository.findByClubIdAndUserId(clubId, newVicePresidentUserId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + if (currentVicePresidentOpt.isPresent()) { + ClubMember currentVicePresident = currentVicePresidentOpt.get(); + if (!currentVicePresident.getId().getUserId().equals(newVicePresidentUserId)) { + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + currentVicePresident.changePosition(memberPosition); + } + } + + newVicePresident.changePosition(vicePresidentPosition); + } + + @Transactional + public void removeMember(Integer clubId, Integer targetUserId, Integer requesterId) { + Club club = clubRepository.getById(clubId); + + if (targetUserId.equals(requesterId)) { + throw CustomException.of(CANNOT_REMOVE_SELF); + } + + ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + ClubMember target = clubMemberRepository.findByClubIdAndUserId(clubId, targetUserId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + + if (target.isPresident()) { + throw CustomException.of(CANNOT_DELETE_CLUB_PRESIDENT); + } + + if (!requester.canManage(target)) { + throw CustomException.of(CANNOT_MANAGE_HIGHER_POSITION); + } + + clubMemberRepository.delete(target); + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java new file mode 100644 index 00000000..3299e629 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java @@ -0,0 +1,152 @@ +package gg.agit.konect.domain.club.service; + +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; +import static gg.agit.konect.global.code.ApiResponseCode.*; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.club.dto.ClubPositionCreateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionUpdateRequest; +import gg.agit.konect.domain.club.dto.ClubPositionsResponse; +import gg.agit.konect.domain.club.enums.ClubPositionGroup; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.club.model.ClubPosition; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.club.repository.ClubPositionRepository; +import gg.agit.konect.domain.club.repository.ClubRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ClubPositionService { + + private static final Set MANAGER_ALLOWED_GROUPS = + EnumSet.of(PRESIDENT, MANAGER); + + private final ClubRepository clubRepository; + private final ClubPositionRepository clubPositionRepository; + private final ClubMemberRepository clubMemberRepository; + + public ClubPositionsResponse getClubPositions(Integer clubId, Integer userId) { + Club club = clubRepository.getById(clubId); + + List positions = clubPositionRepository.findAllByClubId(clubId); + + List positionResponses = positions.stream() + .map(position -> { + long memberCount = clubMemberRepository.countByPositionId(position.getId()); + return ClubPositionsResponse.InnerClubPosition.of(position, memberCount); + }) + .toList(); + + return ClubPositionsResponse.of(positionResponses); + } + + @Transactional + public ClubPositionsResponse createClubPosition( + Integer clubId, + Integer userId, + ClubPositionCreateRequest request + ) { + Club club = clubRepository.getById(clubId); + + validateManagerPermission(clubId, userId); + + ClubPositionGroup positionGroup = request.positionGroup(); + if (positionGroup == ClubPositionGroup.PRESIDENT || positionGroup == ClubPositionGroup.VICE_PRESIDENT) { + throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); + } + + if (clubPositionRepository.existsByClubIdAndName(clubId, request.name())) { + throw CustomException.of(POSITION_NAME_DUPLICATED); + } + + ClubPosition newPosition = ClubPosition.builder() + .name(request.name()) + .clubPositionGroup(positionGroup) + .club(club) + .build(); + + clubPositionRepository.save(newPosition); + + return getClubPositions(clubId, userId); + } + + @Transactional + public ClubPositionsResponse updateClubPositionName( + Integer clubId, + Integer positionId, + Integer userId, + ClubPositionUpdateRequest request + ) { + Club club = clubRepository.getById(clubId); + + validateManagerPermission(clubId, userId); + + ClubPosition position = clubPositionRepository.getById(positionId); + + if (!position.getClub().getId().equals(clubId)) { + throw CustomException.of(NOT_FOUND_CLUB_POSITION); + } + + if (!position.canRename()) { + throw CustomException.of(FORBIDDEN_POSITION_NAME_CHANGE); + } + + if (clubPositionRepository.existsByClubIdAndNameAndIdNot(clubId, request.name(), positionId)) { + throw CustomException.of(POSITION_NAME_DUPLICATED); + } + + position.updateName(request.name()); + + return getClubPositions(clubId, userId); + } + + @Transactional + public void deleteClubPosition(Integer clubId, Integer positionId, Integer userId) { + Club club = clubRepository.getById(clubId); + + validateManagerPermission(clubId, userId); + + ClubPosition position = clubPositionRepository.getById(positionId); + + if (!position.getClub().getId().equals(clubId)) { + throw CustomException.of(NOT_FOUND_CLUB_POSITION); + } + + if (!position.canDelete()) { + throw CustomException.of(CANNOT_DELETE_ESSENTIAL_POSITION); + } + + long memberCount = clubMemberRepository.countByPositionId(positionId); + if (memberCount > 0) { + throw CustomException.of(POSITION_IN_USE); + } + + ClubPositionGroup positionGroup = position.getClubPositionGroup(); + long sameGroupCount = clubPositionRepository.countByClubIdAndPositionGroup(clubId, positionGroup); + if (sameGroupCount < 2) { + throw CustomException.of(INSUFFICIENT_POSITION_COUNT); + } + + clubPositionRepository.delete(position); + } + + private void validateManagerPermission(Integer clubId, Integer userId) { + boolean hasPermission = clubMemberRepository.existsByClubIdAndUserIdAndPositionGroupIn( + clubId, userId, MANAGER_ALLOWED_GROUPS + ); + + if (!hasPermission) { + throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); + } + } +} 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 474f3e45..8b916959 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 @@ -29,6 +29,7 @@ import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; +import gg.agit.konect.domain.club.dto.ClubMemberCondition; import gg.agit.konect.domain.club.dto.ClubMembersResponse; import gg.agit.konect.domain.club.dto.ClubMembershipsResponse; import gg.agit.konect.domain.club.dto.ClubRecruitmentCreateRequest; @@ -43,6 +44,7 @@ import gg.agit.konect.domain.club.model.ClubApplyQuestionAnswers; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubMembers; +import gg.agit.konect.domain.club.model.ClubPosition; import gg.agit.konect.domain.club.model.ClubRecruitment; import gg.agit.konect.domain.club.model.ClubRecruitmentImage; import gg.agit.konect.domain.club.model.ClubSummaryInfo; @@ -50,6 +52,7 @@ import gg.agit.konect.domain.club.repository.ClubApplyQuestionRepository; import gg.agit.konect.domain.club.repository.ClubApplyRepository; import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.club.repository.ClubPositionRepository; import gg.agit.konect.domain.club.repository.ClubQueryRepository; import gg.agit.konect.domain.club.repository.ClubRecruitmentRepository; import gg.agit.konect.domain.club.repository.ClubRepository; @@ -71,6 +74,7 @@ public class ClubService { private final ClubQueryRepository clubQueryRepository; private final ClubRepository clubRepository; private final ClubMemberRepository clubMemberRepository; + private final ClubPositionRepository clubPositionRepository; private final ClubRecruitmentRepository clubRecruitmentRepository; private final ClubApplyRepository clubApplyRepository; private final ClubApplyQuestionRepository clubApplyQuestionRepository; @@ -162,13 +166,19 @@ private List findApplicationsByRecruitmentPeriod( ); } - public ClubMembersResponse getClubMembers(Integer clubId, Integer userId) { + public ClubMembersResponse getClubMembers(Integer clubId, Integer userId, ClubMemberCondition condition) { boolean isMember = clubMemberRepository.existsByClubIdAndUserId(clubId, userId); if (!isMember) { throw CustomException.of(FORBIDDEN_CLUB_MEMBER_ACCESS); } - List clubMembers = clubMemberRepository.findAllByClubId(clubId); + List clubMembers; + if (condition != null && condition.positionGroup() != null) { + clubMembers = clubMemberRepository.findAllByClubIdAndPositionGroup(clubId, condition.positionGroup()); + } else { + clubMembers = clubMemberRepository.findAllByClubId(clubId); + } + return ClubMembersResponse.from(clubMembers); } diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 2419c7d2..7ec4b6ab 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -21,8 +21,16 @@ public enum ApiResponseCode { FAILED_EXTRACT_EMAIL(HttpStatus.BAD_REQUEST, "OAuth 로그인 과정에서 이메일 정보를 가져올 수 없습니다."), FAILED_EXTRACT_PROVIDER_ID(HttpStatus.BAD_REQUEST, "OAuth 로그인 과정에서 제공자 식별자를 가져올 수 없습니다."), CANNOT_CREATE_CHAT_ROOM_WITH_SELF(HttpStatus.BAD_REQUEST, "자기 자신과는 채팅방을 만들 수 없습니다."), - REQUIRED_CLUB_APPLY_ANSWER_MISSING(HttpStatus.BAD_REQUEST, "필수 가입 답변이 누락되었습니다."), + CANNOT_CHANGE_OWN_POSITION(HttpStatus.BAD_REQUEST, "자기 자신의 직책은 변경할 수 없습니다."), CANNOT_DELETE_CLUB_PRESIDENT(HttpStatus.BAD_REQUEST, "동아리 회장인 경우 회장을 양도하고 탈퇴해야 합니다."), + CANNOT_DELETE_ESSENTIAL_POSITION(HttpStatus.BAD_REQUEST, "필수 직책은 삭제할 수 없습니다."), + CANNOT_MANAGE_HIGHER_POSITION(HttpStatus.BAD_REQUEST, "자신보다 높은 직급의 회원은 관리할 수 없습니다."), + CANNOT_REMOVE_SELF(HttpStatus.BAD_REQUEST, "자기 자신을 강제 탈퇴시킬 수 없습니다."), + INSUFFICIENT_POSITION_COUNT(HttpStatus.BAD_REQUEST, "해당 그룹에 최소 2개의 직책이 있어야 삭제 가능합니다."), + MANAGER_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "운영진은 최대 20명까지 임명 가능합니다."), + POSITION_IN_USE(HttpStatus.BAD_REQUEST, "해당 직책을 사용 중인 회원이 있어 삭제할 수 없습니다."), + POSITION_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "동일한 직책 이름이 이미 존재합니다."), + REQUIRED_CLUB_APPLY_ANSWER_MISSING(HttpStatus.BAD_REQUEST, "필수 가입 답변이 누락되었습니다."), STUDY_TIMER_NOT_RUNNING(HttpStatus.BAD_REQUEST, "실행 중인 스터디 타이머가 없습니다."), STUDY_TIMER_TIME_MISMATCH(HttpStatus.BAD_REQUEST, "스터디 타이머 시간이 유효하지 않습니다."), INVALID_RECRUITMENT_DATE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "상시 모집인 경우 모집 시작일과 마감일을 지정할 수 없습니다."), @@ -37,8 +45,10 @@ public enum ApiResponseCode { FORBIDDEN_CLUB_FEE_INFO(HttpStatus.FORBIDDEN, "회비 정보 조회 권한이 없습니다."), FORBIDDEN_CLUB_MANAGER_ACCESS(HttpStatus.FORBIDDEN, "동아리 매니저 권한이 없습니다."), FORBIDDEN_CLUB_MEMBER_ACCESS(HttpStatus.FORBIDDEN, "동아리 멤버 조회 권한이 없습니다."), - FORBIDDEN_COUNCIL_NOTICE_ACCESS(HttpStatus.FORBIDDEN, "총동아리연합회 공지사항 조회 권한이 없습니다."), FORBIDDEN_CLUB_RECRUITMENT_CREATE(HttpStatus.FORBIDDEN, "동아리 모집 공고를 생성할 권한이 없습니다."), + FORBIDDEN_COUNCIL_NOTICE_ACCESS(HttpStatus.FORBIDDEN, "총동아리연합회 공지사항 조회 권한이 없습니다."), + FORBIDDEN_MEMBER_POSITION_CHANGE(HttpStatus.FORBIDDEN, "회원 직책 변경 권한이 없습니다."), + FORBIDDEN_POSITION_NAME_CHANGE(HttpStatus.FORBIDDEN, "해당 직책의 이름은 변경할 수 없습니다."), // 404 Not Found (리소스를 찾을 수 없음) NO_HANDLER_FOUND(HttpStatus.NOT_FOUND, "유효하지 않은 API 경로입니다."), @@ -53,6 +63,7 @@ public enum ApiResponseCode { NOT_FOUND_UNREGISTERED_USER(HttpStatus.NOT_FOUND, "임시 유저를 찾을 수 없습니다."), UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다."), NOT_FOUND_CLUB_APPLY_QUESTION(HttpStatus.NOT_FOUND, "동아리 가입 문항을 찾을 수 없습니다."), + NOT_FOUND_CLUB_POSITION(HttpStatus.NOT_FOUND, "동아리 직책을 찾을 수 없습니다."), NOT_FOUND_RANKING_TYPE(HttpStatus.NOT_FOUND, "순위 타입을 찾을 수 없습니다."), NOT_FOUND_BANK(HttpStatus.NOT_FOUND, "해당하는 은행을 찾을 수 없습니다."), @@ -67,6 +78,7 @@ public enum ApiResponseCode { DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."), ALREADY_APPLIED_CLUB(HttpStatus.CONFLICT, "이미 동아리에 가입 신청을 완료했습니다."), DUPLICATE_CLUB_APPLY_QUESTION(HttpStatus.CONFLICT, "중복된 가입 문항이 포함되어 있습니다."), + VICE_PRESIDENT_ALREADY_EXISTS(HttpStatus.CONFLICT, "부회장은 이미 존재합니다."), ALREADY_RUNNING_STUDY_TIMER(HttpStatus.CONFLICT, "이미 실행 중인 스터디 타이머가 있습니다."), ALREADY_EXIST_CLUB_RECRUITMENT(HttpStatus.CONFLICT, "이미 동아리 모집 공고가 존재합니다."), From cd44c7f70be06372ca744d2e19bbb7a124347e8f Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 17:23:48 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84(?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B6=8C=ED=95=9C=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/controller/ClubApi.java | 16 ++++++ .../club/controller/ClubController.java | 10 ++++ .../domain/club/dto/ClubCreateRequest.java | 53 +++++++++++++++++++ .../club/repository/ClubRepository.java | 2 + .../domain/club/service/ClubService.java | 48 +++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubCreateRequest.java 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 5bc66c5a..284c4c1e 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 @@ -18,6 +18,7 @@ import gg.agit.konect.domain.club.dto.ClubApplyQuestionsResponse; import gg.agit.konect.domain.club.dto.ClubApplyRequest; import gg.agit.konect.domain.club.dto.ClubCondition; +import gg.agit.konect.domain.club.dto.ClubCreateRequest; import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; @@ -64,6 +65,21 @@ ResponseEntity getClubDetail( @UserId Integer userId ); + @Operation(summary = "동아리를 생성한다.", description = """ + 새로운 동아리를 생성합니다. + 동아리 생성 시 기본 직책 4개(회장, 부회장, 운영진, 일반회원)가 자동으로 생성되며, + 생성자는 자동으로 회장으로 등록됩니다. + + ## 에러 + - INVALID_REQUEST_BODY (400): 요청 본문의 형식이 올바르지 않거나 필수 값이 누락된 경우 + - NOT_FOUND_USER (404): 유저를 찾을 수 없습니다. + """) + @PostMapping + ResponseEntity createClub( + @Valid @RequestBody ClubCreateRequest request, + @UserId Integer userId + ); + @Operation(summary = "가입한 동아리 리스트를 조회한다.") @GetMapping("/joined") ResponseEntity getJoinedClubs( diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java index 62fc2001..b0d75ae4 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java @@ -14,6 +14,7 @@ import gg.agit.konect.domain.club.dto.ClubApplyQuestionsResponse; import gg.agit.konect.domain.club.dto.ClubApplyRequest; import gg.agit.konect.domain.club.dto.ClubCondition; +import gg.agit.konect.domain.club.dto.ClubCreateRequest; import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; @@ -64,6 +65,15 @@ public ResponseEntity getClubDetail( return ResponseEntity.ok(response); } + @Override + public ResponseEntity createClub( + @Valid @RequestBody ClubCreateRequest request, + @UserId Integer userId + ) { + ClubDetailResponse response = clubService.createClub(userId, request); + return ResponseEntity.ok(response); + } + @Override public ResponseEntity getJoinedClubs(@UserId Integer userId) { ClubMembershipsResponse response = clubService.getJoinedClubs(userId); diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubCreateRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubCreateRequest.java new file mode 100644 index 00000000..fd12fdb0 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubCreateRequest.java @@ -0,0 +1,53 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import gg.agit.konect.domain.club.enums.ClubCategory; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.university.model.University; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record ClubCreateRequest( + @NotBlank(message = "동아리 이름은 필수 입력입니다.") + @Size(max = 50, message = "동아리 이름은 최대 50자까지 입력 가능합니다.") + @Schema(description = "동아리 이름", example = "BCSD Lab", requiredMode = REQUIRED) + String name, + + @NotBlank(message = "동아리 설명은 필수 입력입니다.") + @Size(max = 100, message = "동아리 설명은 최대 100자까지 입력 가능합니다.") + @Schema(description = "동아리 한 줄 소개", example = "코리아텍 중앙 SW 개발 동아리", requiredMode = REQUIRED) + String description, + + @NotBlank(message = "동아리 소개는 필수 입력입니다.") + @Schema(description = "동아리 상세 소개", example = "BCSD Lab은...", requiredMode = REQUIRED) + String introduce, + + @NotBlank(message = "동아리 이미지 URL은 필수 입력입니다.") + @Size(max = 255, message = "이미지 URL은 최대 255자까지 입력 가능합니다.") + @Schema(description = "동아리 대표 이미지 URL", example = "https://example.com/image.png", requiredMode = REQUIRED) + String imageUrl, + + @NotBlank(message = "동아리 위치는 필수 입력입니다.") + @Size(max = 255, message = "위치는 최대 255자까지 입력 가능합니다.") + @Schema(description = "동아리 위치", example = "다솔관 123호", requiredMode = REQUIRED) + String location, + + @NotNull(message = "동아리 카테고리는 필수 입력입니다.") + @Schema(description = "동아리 카테고리", example = "IT", requiredMode = REQUIRED) + ClubCategory clubCategory +) { + public Club toEntity(University university) { + return Club.builder() + .name(name) + .description(description) + .introduce(introduce) + .imageUrl(imageUrl) + .location(location) + .clubCategory(clubCategory) + .university(university) + .build(); + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubRepository.java index 2d563f7e..6fa8e58d 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubRepository.java @@ -24,4 +24,6 @@ default Club getById(Integer id) { return findById(id).orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CLUB)); } + + Club save(Club club); } 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 8b916959..f1a1631d 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 @@ -1,7 +1,9 @@ package gg.agit.konect.domain.club.service; import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MEMBER; import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT; import static gg.agit.konect.global.code.ApiResponseCode.*; import java.time.LocalDateTime; @@ -26,6 +28,7 @@ import gg.agit.konect.domain.club.dto.ClubApplyQuestionsResponse; import gg.agit.konect.domain.club.dto.ClubApplyRequest; import gg.agit.konect.domain.club.dto.ClubCondition; +import gg.agit.konect.domain.club.dto.ClubCreateRequest; import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; @@ -424,6 +427,51 @@ public void updateRecruitment(Integer clubId, Integer userId, ClubRecruitmentUpd } } + @Transactional + public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) { + User user = userRepository.getById(userId); + Club club = request.toEntity(user.getUniversity()); + + Club savedClub = clubRepository.save(club); + + List defaultPositions = List.of( + ClubPosition.builder() + .name("회장") + .clubPositionGroup(PRESIDENT) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("부회장") + .clubPositionGroup(VICE_PRESIDENT) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("운영진") + .clubPositionGroup(MANAGER) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("일반회원") + .clubPositionGroup(MEMBER) + .club(savedClub) + .build() + ); + + defaultPositions.forEach(clubPositionRepository::save); + + ClubPosition presidentPosition = defaultPositions.get(0); + ClubMember president = ClubMember.builder() + .club(savedClub) + .user(user) + .clubPosition(presidentPosition) + .isFeePaid(false) + .build(); + + clubMemberRepository.save(president); + + return getClubDetail(savedClub.getId(), userId); + } + private boolean hasClubManageAccess( Integer clubId, Integer userId, From 40961309bb51f2ab128e39be0f28dca83d260108 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 19:25:45 +0900 Subject: [PATCH 04/13] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ClubMemberManagementService.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index 579ccc97..469b3820 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -1,8 +1,6 @@ package gg.agit.konect.domain.club.service; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.*; import static gg.agit.konect.global.code.ApiResponseCode.*; import java.util.Optional; @@ -20,7 +18,6 @@ import gg.agit.konect.domain.club.repository.ClubMemberRepository; import gg.agit.konect.domain.club.repository.ClubPositionRepository; import gg.agit.konect.domain.club.repository.ClubRepository; -import gg.agit.konect.domain.user.model.User; import gg.agit.konect.domain.user.repository.UserRepository; import gg.agit.konect.global.exception.CustomException; import lombok.RequiredArgsConstructor; @@ -30,6 +27,7 @@ @Transactional(readOnly = true) public class ClubMemberManagementService { + public static final int MAX_MANAGER_COUNT = 20; private final ClubRepository clubRepository; private final ClubMemberRepository clubMemberRepository; private final ClubPositionRepository clubPositionRepository; @@ -79,7 +77,7 @@ public void changeMemberPosition( if (newPositionGroup == MANAGER) { long managerCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, MANAGER); - if (managerCount >= 20) { + if (managerCount >= MAX_MANAGER_COUNT) { throw CustomException.of(MANAGER_LIMIT_EXCEEDED); } } @@ -114,7 +112,8 @@ public void transferPresident( ClubPosition presidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, PRESIDENT) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, + ClubPositionGroup.MEMBER) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); currentPresident.changePosition(memberPosition); @@ -136,10 +135,12 @@ public void changeVicePresident( throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); } - ClubPosition vicePresidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, VICE_PRESIDENT) + ClubPosition vicePresidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, + VICE_PRESIDENT) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); - Optional currentVicePresidentOpt = clubMemberRepository.findAllByClubIdAndPositionGroup(clubId, VICE_PRESIDENT) + Optional currentVicePresidentOpt = clubMemberRepository.findAllByClubIdAndPositionGroup(clubId, + VICE_PRESIDENT) .stream() .findFirst(); @@ -148,7 +149,8 @@ public void changeVicePresident( if (newVicePresidentUserId == null) { if (currentVicePresidentOpt.isPresent()) { ClubMember currentVicePresident = currentVicePresidentOpt.get(); - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, + ClubPositionGroup.MEMBER) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); currentVicePresident.changePosition(memberPosition); } @@ -165,7 +167,8 @@ public void changeVicePresident( if (currentVicePresidentOpt.isPresent()) { ClubMember currentVicePresident = currentVicePresidentOpt.get(); if (!currentVicePresident.getId().getUserId().equals(newVicePresidentUserId)) { - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, ClubPositionGroup.MEMBER) + ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, + ClubPositionGroup.MEMBER) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); currentVicePresident.changePosition(memberPosition); } From da78a290021d5df894b388968e139d65fb532f8b Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 20:25:08 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EC=A7=81=EA=B8=89=20=EB=82=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B0=80=EB=82=98=EB=8B=A4=20=EC=88=9C?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/club/repository/ClubMemberRepository.java | 4 +++- .../konect/domain/club/repository/ClubPositionRepository.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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 47c3d3d9..f8a300bb 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 @@ -18,8 +18,9 @@ public interface ClubMemberRepository extends Repository findAllByClubId(@Param("clubId") Integer clubId); @@ -30,6 +31,7 @@ public interface ClubMemberRepository extends Repository findAllByClubIdAndPositionGroup( @Param("clubId") Integer clubId, diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java index 00b45bbc..474d906d 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java @@ -30,7 +30,7 @@ default ClubPosition getById(Integer id) { SELECT cp FROM ClubPosition cp WHERE cp.club.id = :clubId - ORDER BY cp.clubPositionGroup + ORDER BY cp.clubPositionGroup ASC, cp.name ASC """) List findAllByClubId(@Param(value = "clubId") Integer clubId); From a01dc72d3e219dbb42de6fb55eb5b92ee9ceec07 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 20:28:49 +0900 Subject: [PATCH 06/13] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A4=91=EB=B3=B5=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/club/repository/ClubMemberRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3108d89e..aa1b515f 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 @@ -134,9 +134,9 @@ long countByClubIdAndPositionGroup( ); void delete(ClubMember clubMember); + ClubMember save(ClubMember clubMember); void deleteByUserId(Integer userId); - ClubMember save(ClubMember clubMember); } From 641ee0b2ceb39fc1649ac972a2a72b990121a3ea Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Mon, 19 Jan 2026 21:55:21 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0,=20ClubPositionReposito?= =?UTF-8?q?ry=EC=9D=98=20=EC=A0=95=EB=A0=AC=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/club/model/ClubMember.java | 12 +++ .../club/repository/ClubMemberRepository.java | 9 ++- .../repository/ClubPositionRepository.java | 41 +++++++++- .../domain/club/service/ClubService.java | 80 ++++++------------- .../konect/global/code/ApiResponseCode.java | 4 +- 5 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java b/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java index 1ade6999..8b300616 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubMember.java @@ -63,4 +63,16 @@ public boolean isSameUser(Integer userId) { public void updatePosition(ClubPosition clubPosition) { this.clubPosition = clubPosition; } + + public void changePosition(ClubPosition clubPosition) { + this.clubPosition = clubPosition; + } + + public ClubPositionGroup getPositionGroup() { + return this.clubPosition.getClubPositionGroup(); + } + + public boolean canManage(ClubMember target) { + return this.getPositionGroup().canManage(target.getPositionGroup()); + } } 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 aa1b515f..234fed03 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 @@ -20,7 +20,14 @@ public interface ClubMemberRepository extends Repository findAllByClubId(@Param("clubId") Integer clubId); diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java index 85ee707d..5aa5e01b 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java @@ -1,12 +1,51 @@ package gg.agit.konect.domain.club.repository; +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CLUB_POSITION; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import gg.agit.konect.domain.club.enums.ClubPositionGroup; import gg.agit.konect.domain.club.model.ClubPosition; +import gg.agit.konect.global.exception.CustomException; public interface ClubPositionRepository extends Repository { ClubPosition save(ClubPosition clubPosition); -} + void delete(ClubPosition clubPosition); + + Optional findById(Integer id); + + default ClubPosition getById(Integer id) { + return findById(id) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + } + @Query(""" + SELECT cp + FROM ClubPosition cp + WHERE cp.club.id = :clubId + ORDER BY + CASE cp.clubPositionGroup + WHEN gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT THEN 0 + WHEN gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT THEN 1 + WHEN gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER THEN 2 + WHEN gg.agit.konect.domain.club.enums.ClubPositionGroup.MEMBER THEN 3 + END ASC, + cp.name ASC + """) + List findAllByClubId(@Param("clubId") Integer clubId); + + boolean existsByClubIdAndName(Integer clubId, String name); + + boolean existsByClubIdAndNameAndIdNot(Integer clubId, String name, Integer id); + + long countByClubIdAndPositionGroup(Integer clubId, ClubPositionGroup positionGroup); + + Optional findFirstByClubIdAndPositionGroup(Integer clubId, ClubPositionGroup positionGroup); +} 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 1f1e8b65..3a6e0f66 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 @@ -1,9 +1,6 @@ package gg.agit.konect.domain.club.service; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MEMBER; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.*; import static gg.agit.konect.global.code.ApiResponseCode.*; import java.time.LocalDateTime; @@ -117,14 +114,32 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) Club savedClub = clubRepository.save(club); - ClubPosition presidentPosition = ClubPosition.builder() - .name("회장") - .clubPositionGroup(PRESIDENT) - .club(savedClub) - .build(); + List defaultPositions = List.of( + ClubPosition.builder() + .name("회장") + .clubPositionGroup(PRESIDENT) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("부회장") + .clubPositionGroup(VICE_PRESIDENT) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("운영진") + .clubPositionGroup(MANAGER) + .club(savedClub) + .build(), + ClubPosition.builder() + .name("일반회원") + .clubPositionGroup(MEMBER) + .club(savedClub) + .build() + ); - clubPositionRepository.save(presidentPosition); + defaultPositions.forEach(clubPositionRepository::save); + ClubPosition presidentPosition = defaultPositions.get(0); ClubMember president = ClubMember.builder() .club(savedClub) .user(user) @@ -476,51 +491,6 @@ public void updateRecruitment(Integer clubId, Integer userId, ClubRecruitmentUpd } } - @Transactional - public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) { - User user = userRepository.getById(userId); - Club club = request.toEntity(user.getUniversity()); - - Club savedClub = clubRepository.save(club); - - List defaultPositions = List.of( - ClubPosition.builder() - .name("회장") - .clubPositionGroup(PRESIDENT) - .club(savedClub) - .build(), - ClubPosition.builder() - .name("부회장") - .clubPositionGroup(VICE_PRESIDENT) - .club(savedClub) - .build(), - ClubPosition.builder() - .name("운영진") - .clubPositionGroup(MANAGER) - .club(savedClub) - .build(), - ClubPosition.builder() - .name("일반회원") - .clubPositionGroup(MEMBER) - .club(savedClub) - .build() - ); - - defaultPositions.forEach(clubPositionRepository::save); - - ClubPosition presidentPosition = defaultPositions.get(0); - ClubMember president = ClubMember.builder() - .club(savedClub) - .user(user) - .clubPosition(presidentPosition) - .isFeePaid(false) - .build(); - - clubMemberRepository.save(president); - - return getClubDetail(savedClub.getId(), userId); - } - private boolean hasClubManageAccess( Integer clubId, Integer userId, diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 948649d8..652f398c 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -64,7 +64,6 @@ public enum ApiResponseCode { NOT_FOUND_UNREGISTERED_USER(HttpStatus.NOT_FOUND, "임시 유저를 찾을 수 없습니다."), UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다."), NOT_FOUND_CLUB_APPLY_QUESTION(HttpStatus.NOT_FOUND, "동아리 가입 문항을 찾을 수 없습니다."), - NOT_FOUND_CLUB_POSITION(HttpStatus.NOT_FOUND, "동아리 직책을 찾을 수 없습니다."), NOT_FOUND_RANKING_TYPE(HttpStatus.NOT_FOUND, "순위 타입을 찾을 수 없습니다."), NOT_FOUND_BANK(HttpStatus.NOT_FOUND, "해당하는 은행을 찾을 수 없습니다."), @@ -85,8 +84,7 @@ public enum ApiResponseCode { // 500 Internal Server Error (서버 오류) CLIENT_ABORTED(HttpStatus.INTERNAL_SERVER_ERROR, "클라이언트에 의해 연결이 중단되었습니다."), - UNEXPECTED_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 예기치 못한 에러가 발생했습니다.") - ; + UNEXPECTED_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 예기치 못한 에러가 발생했습니다."); private final HttpStatus httpStatus; private final String message; From 1cc9a42577f5d19b11c342a8fa83eeaabe66ebac Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Tue, 20 Jan 2026 17:18:50 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20=EB=8F=99=EC=95=84=EB=A6=AC?= =?UTF-8?q?=20=EB=82=B4=20=EC=A7=81=EC=B1=85&=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/service/ClubMemberManagementService.java | 9 +++++++++ .../konect/domain/club/service/ClubPositionService.java | 4 ++-- .../java/gg/agit/konect/global/code/ApiResponseCode.java | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index 469b3820..d9efb721 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -49,6 +49,11 @@ public void changeMemberPosition( ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubPositionGroup requesterGroup = requester.getPositionGroup(); + if (requesterGroup != PRESIDENT && requesterGroup != VICE_PRESIDENT) { + throw CustomException.of(FORBIDDEN_MEMBER_POSITION_CHANGE); + } + ClubMember target = clubMemberRepository.findByClubIdAndUserId(clubId, targetUserId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); @@ -199,6 +204,10 @@ public void removeMember(Integer clubId, Integer targetUserId, Integer requester throw CustomException.of(CANNOT_MANAGE_HIGHER_POSITION); } + if (target.getPositionGroup() != MEMBER) { + throw CustomException.of(CANNOT_REMOVE_NON_MEMBER); + } + clubMemberRepository.delete(target); } } diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java index 3299e629..b827a21b 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java @@ -1,7 +1,7 @@ package gg.agit.konect.domain.club.service; -import static gg.agit.konect.domain.club.enums.ClubPositionGroup.MANAGER; import static gg.agit.konect.domain.club.enums.ClubPositionGroup.PRESIDENT; +import static gg.agit.konect.domain.club.enums.ClubPositionGroup.VICE_PRESIDENT; import static gg.agit.konect.global.code.ApiResponseCode.*; import java.util.EnumSet; @@ -29,7 +29,7 @@ public class ClubPositionService { private static final Set MANAGER_ALLOWED_GROUPS = - EnumSet.of(PRESIDENT, MANAGER); + EnumSet.of(PRESIDENT, VICE_PRESIDENT); private final ClubRepository clubRepository; private final ClubPositionRepository clubPositionRepository; diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 652f398c..209ffb86 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -26,6 +26,7 @@ public enum ApiResponseCode { CANNOT_DELETE_ESSENTIAL_POSITION(HttpStatus.BAD_REQUEST, "필수 직책은 삭제할 수 없습니다."), CANNOT_MANAGE_HIGHER_POSITION(HttpStatus.BAD_REQUEST, "자신보다 높은 직급의 회원은 관리할 수 없습니다."), CANNOT_REMOVE_SELF(HttpStatus.BAD_REQUEST, "자기 자신을 강제 탈퇴시킬 수 없습니다."), + CANNOT_REMOVE_NON_MEMBER(HttpStatus.BAD_REQUEST, "일반회원만 강제 탈퇴할 수 있습니다. 먼저 직책을 변경한 후 탈퇴시켜주세요."), INSUFFICIENT_POSITION_COUNT(HttpStatus.BAD_REQUEST, "해당 그룹에 최소 2개의 직책이 있어야 삭제 가능합니다."), MANAGER_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "운영진은 최대 20명까지 임명 가능합니다."), POSITION_IN_USE(HttpStatus.BAD_REQUEST, "해당 직책을 사용 중인 회원이 있어 삭제할 수 없습니다."), From 6729d4771dbf77468c335eaad75ede94e29b1602 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Tue, 20 Jan 2026 22:05:16 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B2=80=ED=86=A0=20=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/repository/ClubMemberRepository.java | 7 ++ .../repository/ClubPositionRepository.java | 9 +- .../service/ClubMemberManagementService.java | 116 ++++++++---------- .../club/service/ClubPositionService.java | 2 +- 4 files changed, 67 insertions(+), 67 deletions(-) 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 234fed03..b37b0927 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 @@ -11,6 +11,8 @@ import gg.agit.konect.domain.club.enums.ClubPositionGroup; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubMemberId; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; public interface ClubMemberRepository extends Repository { @@ -100,6 +102,11 @@ boolean existsByClubIdAndUserIdAndPositionGroupIn( """) Optional findByClubIdAndUserId(@Param("clubId") Integer clubId, @Param("userId") Integer userId); + default ClubMember getByClubIdAndUserId(Integer clubId, Integer userId) { + return findByClubIdAndUserId(clubId, userId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CLUB_MEMBER)); + } + boolean existsByClubIdAndUserId(Integer clubId, Integer userId); List findByUserId(Integer userId); diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java index 5aa5e01b..0fc55f63 100644 --- a/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubPositionRepository.java @@ -45,7 +45,12 @@ default ClubPosition getById(Integer id) { boolean existsByClubIdAndNameAndIdNot(Integer clubId, String name, Integer id); - long countByClubIdAndPositionGroup(Integer clubId, ClubPositionGroup positionGroup); + long countByClubIdAndClubPositionGroup(Integer clubId, ClubPositionGroup positionGroup); - Optional findFirstByClubIdAndPositionGroup(Integer clubId, ClubPositionGroup positionGroup); + Optional findFirstByClubIdAndClubPositionGroup(Integer clubId, ClubPositionGroup positionGroup); + + default ClubPosition getFirstByClubIdAndClubPositionGroup(Integer clubId, ClubPositionGroup positionGroup) { + return findFirstByClubIdAndClubPositionGroup(clubId, positionGroup) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + } } diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index d9efb721..7b682c2f 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -12,13 +12,13 @@ import gg.agit.konect.domain.club.dto.PresidentTransferRequest; import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; import gg.agit.konect.domain.club.enums.ClubPositionGroup; -import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubPosition; import gg.agit.konect.domain.club.repository.ClubMemberRepository; import gg.agit.konect.domain.club.repository.ClubPositionRepository; import gg.agit.konect.domain.club.repository.ClubRepository; import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.code.ApiResponseCode; import gg.agit.konect.global.exception.CustomException; import lombok.RequiredArgsConstructor; @@ -40,22 +40,14 @@ public void changeMemberPosition( Integer requesterId, MemberPositionChangeRequest request ) { - Club club = clubRepository.getById(clubId); + clubRepository.getById(clubId); - if (targetUserId.equals(requesterId)) { - throw CustomException.of(CANNOT_CHANGE_OWN_POSITION); - } - - ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + validateNotSelf(requesterId, targetUserId, CANNOT_CHANGE_OWN_POSITION); - ClubPositionGroup requesterGroup = requester.getPositionGroup(); - if (requesterGroup != PRESIDENT && requesterGroup != VICE_PRESIDENT) { - throw CustomException.of(FORBIDDEN_MEMBER_POSITION_CHANGE); - } + ClubMember requester = clubMemberRepository.getByClubIdAndUserId(clubId, requesterId); + validateManagerPermission(requester); - ClubMember target = clubMemberRepository.findByClubIdAndUserId(clubId, targetUserId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember target = clubMemberRepository.getByClubIdAndUserId(clubId, targetUserId); if (!requester.canManage(target)) { throw CustomException.of(CANNOT_MANAGE_HIGHER_POSITION); @@ -75,14 +67,14 @@ public void changeMemberPosition( if (newPositionGroup == VICE_PRESIDENT) { long vicePresidentCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, VICE_PRESIDENT); - if (vicePresidentCount >= 1) { + if (target.getPositionGroup() != VICE_PRESIDENT && vicePresidentCount >= 1) { throw CustomException.of(VICE_PRESIDENT_ALREADY_EXISTS); } } if (newPositionGroup == MANAGER) { long managerCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, MANAGER); - if (managerCount >= MAX_MANAGER_COUNT) { + if (target.getPositionGroup() != MANAGER && managerCount >= MAX_MANAGER_COUNT) { throw CustomException.of(MANAGER_LIMIT_EXCEEDED); } } @@ -96,30 +88,18 @@ public void transferPresident( Integer currentPresidentId, PresidentTransferRequest request ) { - Club club = clubRepository.getById(clubId); + clubRepository.getById(clubId); - ClubMember currentPresident = clubMemberRepository.findByClubIdAndUserId(clubId, currentPresidentId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_PRESIDENT)); - - if (!currentPresident.isPresident()) { - throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); - } + ClubMember currentPresident = clubMemberRepository.getByClubIdAndUserId(clubId, currentPresidentId); + validatePresidentPermission(currentPresident); Integer newPresidentUserId = request.newPresidentUserId(); + validateNotSelf(currentPresidentId, newPresidentUserId, ILLEGAL_ARGUMENT); - if (currentPresidentId.equals(newPresidentUserId)) { - throw CustomException.of(ILLEGAL_ARGUMENT); - } - - ClubMember newPresident = clubMemberRepository.findByClubIdAndUserId(clubId, newPresidentUserId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember newPresident = clubMemberRepository.getByClubIdAndUserId(clubId, newPresidentUserId); - ClubPosition presidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, PRESIDENT) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); - - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, - ClubPositionGroup.MEMBER) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + ClubPosition presidentPosition = clubPositionRepository.getFirstByClubIdAndClubPositionGroup(clubId, PRESIDENT); + ClubPosition memberPosition = clubPositionRepository.getFirstByClubIdAndClubPositionGroup(clubId, MEMBER); currentPresident.changePosition(memberPosition); newPresident.changePosition(presidentPosition); @@ -131,18 +111,15 @@ public void changeVicePresident( Integer requesterId, VicePresidentChangeRequest request ) { - Club club = clubRepository.getById(clubId); + clubRepository.getById(clubId); - ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember requester = clubMemberRepository.getByClubIdAndUserId(clubId, requesterId); + validatePresidentPermission(requester); - if (!requester.isPresident()) { - throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); - } - - ClubPosition vicePresidentPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, - VICE_PRESIDENT) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + ClubPosition vicePresidentPosition = clubPositionRepository.getFirstByClubIdAndClubPositionGroup( + clubId, + VICE_PRESIDENT + ); Optional currentVicePresidentOpt = clubMemberRepository.findAllByClubIdAndPositionGroup(clubId, VICE_PRESIDENT) @@ -154,27 +131,22 @@ public void changeVicePresident( if (newVicePresidentUserId == null) { if (currentVicePresidentOpt.isPresent()) { ClubMember currentVicePresident = currentVicePresidentOpt.get(); - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, - ClubPositionGroup.MEMBER) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + ClubPosition memberPosition = clubPositionRepository.getFirstByClubIdAndClubPositionGroup(clubId, + MEMBER); currentVicePresident.changePosition(memberPosition); } return; } - if (requesterId.equals(newVicePresidentUserId)) { - throw CustomException.of(CANNOT_CHANGE_OWN_POSITION); - } + validateNotSelf(requesterId, newVicePresidentUserId, CANNOT_CHANGE_OWN_POSITION); - ClubMember newVicePresident = clubMemberRepository.findByClubIdAndUserId(clubId, newVicePresidentUserId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember newVicePresident = clubMemberRepository.getByClubIdAndUserId(clubId, newVicePresidentUserId); if (currentVicePresidentOpt.isPresent()) { ClubMember currentVicePresident = currentVicePresidentOpt.get(); if (!currentVicePresident.getId().getUserId().equals(newVicePresidentUserId)) { - ClubPosition memberPosition = clubPositionRepository.findFirstByClubIdAndPositionGroup(clubId, - ClubPositionGroup.MEMBER) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_POSITION)); + ClubPosition memberPosition = clubPositionRepository.getFirstByClubIdAndClubPositionGroup(clubId, + MEMBER); currentVicePresident.changePosition(memberPosition); } } @@ -184,17 +156,14 @@ public void changeVicePresident( @Transactional public void removeMember(Integer clubId, Integer targetUserId, Integer requesterId) { - Club club = clubRepository.getById(clubId); + clubRepository.getById(clubId); - if (targetUserId.equals(requesterId)) { - throw CustomException.of(CANNOT_REMOVE_SELF); - } + validateNotSelf(requesterId, targetUserId, CANNOT_REMOVE_SELF); - ClubMember requester = clubMemberRepository.findByClubIdAndUserId(clubId, requesterId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember requester = clubMemberRepository.getByClubIdAndUserId(clubId, requesterId); + validateManagerPermission(requester); - ClubMember target = clubMemberRepository.findByClubIdAndUserId(clubId, targetUserId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CLUB_MEMBER)); + ClubMember target = clubMemberRepository.getByClubIdAndUserId(clubId, targetUserId); if (target.isPresident()) { throw CustomException.of(CANNOT_DELETE_CLUB_PRESIDENT); @@ -210,4 +179,23 @@ public void removeMember(Integer clubId, Integer targetUserId, Integer requester clubMemberRepository.delete(target); } + + private void validateNotSelf(Integer userId1, Integer userId2, ApiResponseCode errorCode) { + if (userId1.equals(userId2)) { + throw CustomException.of(errorCode); + } + } + + private void validatePresidentPermission(ClubMember member) { + if (!member.isPresident()) { + throw CustomException.of(FORBIDDEN_CLUB_MANAGER_ACCESS); + } + } + + private void validateManagerPermission(ClubMember member) { + ClubPositionGroup positionGroup = member.getPositionGroup(); + if (positionGroup != PRESIDENT && positionGroup != VICE_PRESIDENT) { + throw CustomException.of(FORBIDDEN_MEMBER_POSITION_CHANGE); + } + } } diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java index b827a21b..d75fda7e 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java @@ -132,7 +132,7 @@ public void deleteClubPosition(Integer clubId, Integer positionId, Integer userI } ClubPositionGroup positionGroup = position.getClubPositionGroup(); - long sameGroupCount = clubPositionRepository.countByClubIdAndPositionGroup(clubId, positionGroup); + long sameGroupCount = clubPositionRepository.countByClubIdAndClubPositionGroup(clubId, positionGroup); if (sameGroupCount < 2) { throw CustomException.of(INSUFFICIENT_POSITION_COUNT); } From 73800744076fc50be2c74a5d697420f8b77da4be Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Wed, 21 Jan 2026 21:19:30 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/controller/ClubApi.java | 28 +++++++++- .../club/controller/ClubController.java | 11 ++++ .../domain/club/dto/AddMemberRequest.java | 17 ++++++ .../club/dto/ClubPositionsResponse.java | 12 +--- .../service/ClubMemberManagementService.java | 56 +++++++++++++++++++ .../club/service/ClubPositionService.java | 2 +- .../domain/club/service/ClubService.java | 28 +++------- .../council/dto/CouncilCreateRequest.java | 4 +- .../domain/council/dto/CouncilResponse.java | 4 +- .../council/dto/CouncilUpdateRequest.java | 2 +- .../konect/domain/council/model/Council.java | 14 ++--- .../council/service/CouncilService.java | 2 +- .../konect/global/code/ApiResponseCode.java | 1 + 13 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java 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 7a54de88..55a8f5e3 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 @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import gg.agit.konect.domain.club.dto.AddMemberRequest; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; @@ -405,15 +406,36 @@ ResponseEntity changeVicePresident( @UserId Integer userId ); + @Operation(summary = "동아리에 회원을 직접 추가한다.", description = """ + 동아리 회장 또는 부회장만 회원을 직접 추가할 수 있습니다. + 회장 직책으로는 추가할 수 없으며, 부회장과 운영진은 인원 제한이 있습니다. + + ## 에러 + - ALREADY_CLUB_MEMBER (409): 이미 동아리 회원입니다. + - VICE_PRESIDENT_ALREADY_EXISTS (409): 부회장은 이미 존재합니다. + - MANAGER_LIMIT_EXCEEDED (400): 운영진은 최대 20명까지 임명 가능합니다. + - FORBIDDEN_MEMBER_POSITION_CHANGE (403): 회원 추가 권한이 없습니다. + - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. + - NOT_FOUND_USER (404): 유저를 찾을 수 없습니다. + - NOT_FOUND_CLUB_POSITION (404): 동아리 직책을 찾을 수 없습니다. + """) + @PostMapping("/{clubId}/members") + ResponseEntity addMember( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody AddMemberRequest request, + @UserId Integer userId + ); + @Operation(summary = "동아리 회원을 강제 탈퇴시킨다.", description = """ - 동아리 회장 또는 매니저만 회원을 강제 탈퇴시킬 수 있습니다. - 자기 자신은 강제 탈퇴시킬 수 없으며, 회장은 강제 탈퇴시킬 수 없습니다. - 상위 직급만 하위 직급의 회원을 강제 탈퇴시킬 수 있습니다. + 동아리 회장 또는 부회장만 회원을 강제 탈퇴시킬 수 있습니다. + 일반회원만 강제 탈퇴 가능하며, 부회장이나 운영진은 먼저 직책을 변경한 후 탈퇴시켜야 합니다. ## 에러 - CANNOT_REMOVE_SELF (400): 자기 자신을 강제 탈퇴시킬 수 없습니다. + - CANNOT_REMOVE_NON_MEMBER (400): 일반회원만 강제 탈퇴할 수 있습니다. - CANNOT_DELETE_CLUB_PRESIDENT (400): 회장은 강제 탈퇴시킬 수 없습니다. - CANNOT_MANAGE_HIGHER_POSITION (400): 자신보다 높은 직급의 회원은 관리할 수 없습니다. + - FORBIDDEN_MEMBER_POSITION_CHANGE (403): 회원 관리 권한이 없습니다. - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. - NOT_FOUND_CLUB_MEMBER (404): 동아리 회원을 찾을 수 없습니다. """) diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java index afd4b95c..88a51697 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import gg.agit.konect.domain.club.dto.AddMemberRequest; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; @@ -283,6 +284,16 @@ public ResponseEntity changeVicePresident( return ResponseEntity.noContent().build(); } + @Override + public ResponseEntity addMember( + @PathVariable(name = "clubId") Integer clubId, + @Valid @RequestBody AddMemberRequest request, + @UserId Integer userId + ) { + clubMemberManagementService.addMember(clubId, userId, request); + return ResponseEntity.ok().build(); + } + @Override public ResponseEntity removeMember( @PathVariable(name = "clubId") Integer clubId, diff --git a/src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java new file mode 100644 index 00000000..ef9a4e53 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java @@ -0,0 +1,17 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record AddMemberRequest( + @NotNull(message = "사용자 ID는 필수 입력입니다.") + @Schema(description = "추가할 사용자 ID", example = "123", requiredMode = REQUIRED) + Integer userId, + + @NotNull(message = "직책 ID는 필수 입력입니다.") + @Schema(description = "부여할 직책 ID", example = "4", requiredMode = REQUIRED) + Integer positionId +) { +} diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java index 59498c9e..0dc2b207 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubPositionsResponse.java @@ -26,13 +26,7 @@ public record InnerClubPosition( Integer priority, @Schema(description = "해당 직책의 회원 수", example = "1", requiredMode = REQUIRED) - Long memberCount, - - @Schema(description = "이름 변경 가능 여부", example = "false", requiredMode = REQUIRED) - Boolean canRename, - - @Schema(description = "삭제 가능 여부", example = "false", requiredMode = REQUIRED) - Boolean canDelete + Long memberCount ) { public static InnerClubPosition of( ClubPosition position, @@ -43,9 +37,7 @@ public static InnerClubPosition of( position.getName(), position.getClubPositionGroup(), position.getPriority(), - memberCount, - position.canRename(), - position.canDelete() + memberCount ); } } diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index 7b682c2f..d57b4058 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.club.dto.AddMemberRequest; import gg.agit.konect.domain.club.dto.MemberPositionChangeRequest; import gg.agit.konect.domain.club.dto.PresidentTransferRequest; import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; @@ -82,6 +83,61 @@ public void changeMemberPosition( target.changePosition(newPosition); } + @Transactional + public void addMember( + Integer clubId, + Integer requesterId, + AddMemberRequest request + ) { + clubRepository.getById(clubId); + + ClubMember requester = clubMemberRepository.getByClubIdAndUserId(clubId, requesterId); + validateManagerPermission(requester); + + Integer targetUserId = request.userId(); + + if (clubMemberRepository.existsByClubIdAndUserId(clubId, targetUserId)) { + throw CustomException.of(ALREADY_CLUB_MEMBER); + } + + userRepository.getById(targetUserId); + + ClubPosition position = clubPositionRepository.getById(request.positionId()); + + if (!position.getClub().getId().equals(clubId)) { + throw CustomException.of(NOT_FOUND_CLUB_POSITION); + } + + ClubPositionGroup positionGroup = position.getClubPositionGroup(); + + if (positionGroup == PRESIDENT) { + throw CustomException.of(FORBIDDEN_MEMBER_POSITION_CHANGE); + } + + if (positionGroup == VICE_PRESIDENT) { + long vicePresidentCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, VICE_PRESIDENT); + if (vicePresidentCount >= 1) { + throw CustomException.of(VICE_PRESIDENT_ALREADY_EXISTS); + } + } + + if (positionGroup == MANAGER) { + long managerCount = clubMemberRepository.countByClubIdAndPositionGroup(clubId, MANAGER); + if (managerCount >= MAX_MANAGER_COUNT) { + throw CustomException.of(MANAGER_LIMIT_EXCEEDED); + } + } + + ClubMember newMember = ClubMember.builder() + .club(clubRepository.getById(clubId)) + .user(userRepository.getById(targetUserId)) + .clubPosition(position) + .isFeePaid(false) + .build(); + + clubMemberRepository.save(newMember); + } + @Transactional public void transferPresident( Integer clubId, diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java index d75fda7e..a9427445 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubPositionService.java @@ -87,7 +87,7 @@ public ClubPositionsResponse updateClubPositionName( Integer userId, ClubPositionUpdateRequest request ) { - Club club = clubRepository.getById(clubId); + clubRepository.getById(clubId); validateManagerPermission(clubId, userId); 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 3a6e0f66..e006191e 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 @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.List; @@ -114,28 +115,13 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) Club savedClub = clubRepository.save(club); - List defaultPositions = List.of( - ClubPosition.builder() - .name("회장") - .clubPositionGroup(PRESIDENT) + List defaultPositions = Arrays.stream(ClubPositionGroup.values()) + .map(group -> ClubPosition.builder() + .name(group.getDescription()) + .clubPositionGroup(group) .club(savedClub) - .build(), - ClubPosition.builder() - .name("부회장") - .clubPositionGroup(VICE_PRESIDENT) - .club(savedClub) - .build(), - ClubPosition.builder() - .name("운영진") - .clubPositionGroup(MANAGER) - .club(savedClub) - .build(), - ClubPosition.builder() - .name("일반회원") - .clubPositionGroup(MEMBER) - .club(savedClub) - .build() - ); + .build()) + .toList(); defaultPositions.forEach(clubPositionRepository::save); diff --git a/src/main/java/gg/agit/konect/domain/council/dto/CouncilCreateRequest.java b/src/main/java/gg/agit/konect/domain/council/dto/CouncilCreateRequest.java index b93f0776..448ef3e9 100644 --- a/src/main/java/gg/agit/konect/domain/council/dto/CouncilCreateRequest.java +++ b/src/main/java/gg/agit/konect/domain/council/dto/CouncilCreateRequest.java @@ -57,7 +57,7 @@ public record CouncilCreateRequest( @Size(max = 255, message = "총동아리연합회 인스타 주소는 최대 255자 입니다.") @Pattern(regexp = "^https?://(www\\.)?instagram\\.com/[a-zA-Z0-9._]+/?$", message = "올바른 인스타그램 URL 형식이 아닙니다.") @Schema(description = "총동아리연합회 인스타 주소", example = "https://www.instagram.com/koreatech_council", requiredMode = REQUIRED) - String instagramUrl + String instagramUserName ) { public Council toEntity(University university) { return Council.builder() @@ -68,7 +68,7 @@ public Council toEntity(University university) { .personalColor(personalColor) .phoneNumber(phoneNumber) .email(email) - .instagramUrl(instagramUrl) + .instagramUserName(instagramUserName) .operatingHour(operatingHour) .university(university) .build(); diff --git a/src/main/java/gg/agit/konect/domain/council/dto/CouncilResponse.java b/src/main/java/gg/agit/konect/domain/council/dto/CouncilResponse.java index d8b11e20..79236c71 100644 --- a/src/main/java/gg/agit/konect/domain/council/dto/CouncilResponse.java +++ b/src/main/java/gg/agit/konect/domain/council/dto/CouncilResponse.java @@ -28,7 +28,7 @@ public record CouncilResponse( String operatingHour, @Schema(description = "총동아리연합회 인스타 주소", example = "https://www.instagram.com/koreatech_council", requiredMode = REQUIRED) - String instagramUrl + String instagramUserName ) { public static CouncilResponse from(Council council) { return new CouncilResponse( @@ -39,7 +39,7 @@ public static CouncilResponse from(Council council) { council.getLocation(), council.getPersonalColor(), council.getOperatingHour(), - council.getInstagramUrl() + council.getInstagramUserName() ); } } diff --git a/src/main/java/gg/agit/konect/domain/council/dto/CouncilUpdateRequest.java b/src/main/java/gg/agit/konect/domain/council/dto/CouncilUpdateRequest.java index 02f86e5e..778e48ea 100644 --- a/src/main/java/gg/agit/konect/domain/council/dto/CouncilUpdateRequest.java +++ b/src/main/java/gg/agit/konect/domain/council/dto/CouncilUpdateRequest.java @@ -55,7 +55,7 @@ public record CouncilUpdateRequest( @Size(max = 255, message = "총동아리연합회 인스타 주소는 최대 255자 입니다.") @Pattern(regexp = "^https?://(www\\.)?instagram\\.com/[a-zA-Z0-9._]+/?$", message = "올바른 인스타그램 URL 형식이 아닙니다.") @Schema(description = "총동아리연합회 인스타 주소", example = "https://www.instagram.com/koreatech_council", requiredMode = REQUIRED) - String instagramUrl + String instagramUserName ) { } diff --git a/src/main/java/gg/agit/konect/domain/council/model/Council.java b/src/main/java/gg/agit/konect/domain/council/model/Council.java index a13241ac..8083f4dd 100644 --- a/src/main/java/gg/agit/konect/domain/council/model/Council.java +++ b/src/main/java/gg/agit/konect/domain/council/model/Council.java @@ -4,8 +4,8 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; -import gg.agit.konect.global.model.BaseEntity; import gg.agit.konect.domain.university.model.University; +import gg.agit.konect.global.model.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -58,8 +58,8 @@ public class Council extends BaseEntity { private String email; @NotNull - @Column(name = "instagram_url", nullable = false) - private String instagramUrl; + @Column(name = "instagram_user_name", nullable = false) + private String instagramUserName; @NotNull @Column(name = "operating_hour", nullable = false) @@ -79,7 +79,7 @@ private Council( String personalColor, String phoneNumber, String email, - String instagramUrl, + String instagramUserName, String operatingHour, University university ) { @@ -91,7 +91,7 @@ private Council( this.personalColor = personalColor; this.phoneNumber = phoneNumber; this.email = email; - this.instagramUrl = instagramUrl; + this.instagramUserName = instagramUserName; this.operatingHour = operatingHour; this.university = university; } @@ -102,7 +102,7 @@ public void update( String introduce, String location, String personalColor, - String instagramUrl, + String instagramUserName, String operatingHour ) { this.name = name; @@ -110,7 +110,7 @@ public void update( this.introduce = introduce; this.location = location; this.personalColor = personalColor; - this.instagramUrl = instagramUrl; + this.instagramUserName = instagramUserName; this.operatingHour = operatingHour; } } diff --git a/src/main/java/gg/agit/konect/domain/council/service/CouncilService.java b/src/main/java/gg/agit/konect/domain/council/service/CouncilService.java index 17b73fd0..f00a6124 100644 --- a/src/main/java/gg/agit/konect/domain/council/service/CouncilService.java +++ b/src/main/java/gg/agit/konect/domain/council/service/CouncilService.java @@ -57,7 +57,7 @@ public void updateCouncil(Integer userId, CouncilUpdateRequest request) { request.introduce(), request.location(), request.personalColor(), - request.instagramUrl(), + request.instagramUserName(), request.operatingHour() ); } diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 209ffb86..5338995d 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -78,6 +78,7 @@ public enum ApiResponseCode { DUPLICATE_STUDENT_NUMBER(HttpStatus.CONFLICT, "이미 사용 중인 학번입니다."), DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."), ALREADY_APPLIED_CLUB(HttpStatus.CONFLICT, "이미 동아리에 가입 신청을 완료했습니다."), + ALREADY_CLUB_MEMBER(HttpStatus.CONFLICT, "이미 동아리 회원입니다."), DUPLICATE_CLUB_APPLY_QUESTION(HttpStatus.CONFLICT, "중복된 가입 문항이 포함되어 있습니다."), VICE_PRESIDENT_ALREADY_EXISTS(HttpStatus.CONFLICT, "부회장은 이미 존재합니다."), ALREADY_RUNNING_STUDY_TIMER(HttpStatus.CONFLICT, "이미 실행 중인 스터디 타이머가 있습니다."), From 2592eaae0621941e1ef07a292aa9917302b903da Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Wed, 21 Jan 2026 21:42:29 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20ClubApi=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=EA=B8=B0=EC=9E=AC=EB=90=9C=20description?= =?UTF-8?q?=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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 55a8f5e3..22eff1ee 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 @@ -80,7 +80,7 @@ ResponseEntity createClub( ); @Operation(summary = "동아리 정보를 수정한다.", description = """ - 동아리 회장 또는 매니저만 동아리 정보를 수정할 수 있습니다. + 동아리 회장 또는 부회장만 동아리 정보를 수정할 수 있습니다. 수정 가능 항목: 동아리명, 한 줄 소개, 로고 이미지, 위치, 분과, 상세 소개 ## 에러 @@ -261,7 +261,7 @@ ResponseEntity createRecruitment( ); @Operation(summary = "동아리 모집 정보를 수정한다.", description = """ - 동아리 회장 또는 매니저만 모집 공고를 수정할 수 있습니다. + 동아리 회장 또는 부회장만 모집 공고를 수정할 수 있습니다. ## 에러 - INVALID_RECRUITMENT_DATE_NOT_ALLOWED (400): 상시 모집인 경우 모집 시작일과 마감일을 지정할 수 없습니다. @@ -293,7 +293,7 @@ ResponseEntity getClubPositions( ); @Operation(summary = "동아리 직책을 생성한다.", description = """ - 동아리 회장 또는 매니저만 직책을 생성할 수 있습니다. + 동아리 회장 또는 부회장만 직책을 생성할 수 있습니다. PRESIDENT와 VICE_PRESIDENT 직책은 생성할 수 없으며, MANAGER 또는 MEMBER 그룹의 직책만 생성 가능합니다. ## 에러 @@ -309,7 +309,7 @@ ResponseEntity createClubPosition( ); @Operation(summary = "동아리 직책의 이름을 수정한다.", description = """ - 동아리 회장 또는 매니저만 직책 이름을 수정할 수 있습니다. + 동아리 회장 또는 부회장만 직책 이름을 수정할 수 있습니다. PRESIDENT와 VICE_PRESIDENT 직책의 이름은 변경할 수 없습니다. ## 에러 @@ -328,7 +328,7 @@ ResponseEntity updateClubPositionName( ); @Operation(summary = "동아리 직책을 삭제한다.", description = """ - 동아리 회장 또는 매니저만 직책을 삭제할 수 있습니다. + 동아리 회장 또는 부회장만 직책을 삭제할 수 있습니다. PRESIDENT와 VICE_PRESIDENT 직책은 삭제할 수 없습니다. 해당 직책을 사용 중인 회원이 없어야 하며, 해당 그룹에 최소 2개의 직책이 있어야 삭제 가능합니다. @@ -348,7 +348,7 @@ ResponseEntity deleteClubPosition( ); @Operation(summary = "동아리 회원의 직책을 변경한다.", description = """ - 동아리 회장 또는 매니저만 회원의 직책을 변경할 수 있습니다. + 동아리 회장 또는 부회장만 회원의 직책을 변경할 수 있습니다. 자기 자신의 직책은 변경할 수 없으며, 상위 직급만 하위 직급의 회원을 관리할 수 있습니다. ## 에러 From f158aa2a0f5ed1000f1408d0ff9d33584fcc3813 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Wed, 21 Jan 2026 22:14:52 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20=EB=8F=99=EC=95=84=EB=A6=AC?= =?UTF-8?q?=20=EB=B6=80=EC=9B=90=20=EC=B6=94=EA=B0=80=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20DTO=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/club/controller/ClubApi.java | 14 +++++++------- .../domain/club/controller/ClubController.java | 4 ++-- ...emberRequest.java => ClubMemberAddRequest.java} | 2 +- .../club/service/ClubMemberManagementService.java | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/main/java/gg/agit/konect/domain/club/dto/{AddMemberRequest.java => ClubMemberAddRequest.java} (94%) 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 22eff1ee..d437d094 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 @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import gg.agit.konect.domain.club.dto.AddMemberRequest; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; @@ -23,6 +22,7 @@ import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; +import gg.agit.konect.domain.club.dto.ClubMemberAddRequest; import gg.agit.konect.domain.club.dto.ClubMemberCondition; import gg.agit.konect.domain.club.dto.ClubMembersResponse; import gg.agit.konect.domain.club.dto.ClubMembershipsResponse; @@ -111,7 +111,7 @@ ResponseEntity getManagedClubs( - 동아리 관리자만 해당 동아리의 지원 내역을 조회할 수 있습니다. - 현재 지정된 모집 일정 범위에 지원한 내역만 볼 수 있습니다. - 상시 모집의 경우 모든 내역을 봅니다. - + ## 에러 - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. @@ -125,7 +125,7 @@ ResponseEntity getClubApplications( @Operation(summary = "동아리 지원 답변을 조회한다.", description = """ - 동아리 관리자만 해당 동아리의 지원 답변을 조회할 수 있습니다. - + ## 에러 - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. - NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다. @@ -170,7 +170,7 @@ ResponseEntity applyClub( @Operation(summary = "동아리 회비 정보를 조회한다.", description = """ 동아리 가입 신청을 완료했거나 동아리 관리자 권한이 있는 사용자만 회비 계좌 정보를 조회할 수 있습니다. - + ## 에러 - FORBIDDEN_CLUB_FEE_INFO (403): 회비 정보 조회 권한이 없습니다. """) @@ -185,7 +185,7 @@ ResponseEntity getFeeInfo( - 모든 필드를 전달하면 생성/수정합니다. - 모든 필드가 null이면 회비 정보를 삭제합니다. - 일부 필드가 누락된 경우 에러가 발생합니다. - + ## 에러 - FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다. - INVALID_REQUEST_BODY (400): 요청 본문의 형식이 올바르지 않거나 필수 값이 누락된 경우 @@ -262,7 +262,7 @@ ResponseEntity createRecruitment( @Operation(summary = "동아리 모집 정보를 수정한다.", description = """ 동아리 회장 또는 부회장만 모집 공고를 수정할 수 있습니다. - + ## 에러 - INVALID_RECRUITMENT_DATE_NOT_ALLOWED (400): 상시 모집인 경우 모집 시작일과 마감일을 지정할 수 없습니다. - INVALID_RECRUITMENT_DATE_REQUIRED (400): 상시 모집이 아닐 경우 모집 시작일과 마감일이 필수입니다. @@ -422,7 +422,7 @@ ResponseEntity changeVicePresident( @PostMapping("/{clubId}/members") ResponseEntity addMember( @PathVariable(name = "clubId") Integer clubId, - @Valid @RequestBody AddMemberRequest request, + @Valid @RequestBody ClubMemberAddRequest request, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java index 88a51697..dacf88ae 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubController.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import gg.agit.konect.domain.club.dto.AddMemberRequest; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; import gg.agit.konect.domain.club.dto.ClubApplyQuestionsReplaceRequest; @@ -19,6 +18,7 @@ import gg.agit.konect.domain.club.dto.ClubDetailResponse; import gg.agit.konect.domain.club.dto.ClubFeeInfoReplaceRequest; import gg.agit.konect.domain.club.dto.ClubFeeInfoResponse; +import gg.agit.konect.domain.club.dto.ClubMemberAddRequest; import gg.agit.konect.domain.club.dto.ClubMemberCondition; import gg.agit.konect.domain.club.dto.ClubMembersResponse; import gg.agit.konect.domain.club.dto.ClubMembershipsResponse; @@ -287,7 +287,7 @@ public ResponseEntity changeVicePresident( @Override public ResponseEntity addMember( @PathVariable(name = "clubId") Integer clubId, - @Valid @RequestBody AddMemberRequest request, + @Valid @RequestBody ClubMemberAddRequest request, @UserId Integer userId ) { clubMemberManagementService.addMember(clubId, userId, request); diff --git a/src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubMemberAddRequest.java similarity index 94% rename from src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java rename to src/main/java/gg/agit/konect/domain/club/dto/ClubMemberAddRequest.java index ef9a4e53..0b51d477 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/AddMemberRequest.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubMemberAddRequest.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -public record AddMemberRequest( +public record ClubMemberAddRequest( @NotNull(message = "사용자 ID는 필수 입력입니다.") @Schema(description = "추가할 사용자 ID", example = "123", requiredMode = REQUIRED) Integer userId, diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index d57b4058..f9fdacdc 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.club.dto.AddMemberRequest; +import gg.agit.konect.domain.club.dto.ClubMemberAddRequest; import gg.agit.konect.domain.club.dto.MemberPositionChangeRequest; import gg.agit.konect.domain.club.dto.PresidentTransferRequest; import gg.agit.konect.domain.club.dto.VicePresidentChangeRequest; @@ -87,7 +87,7 @@ public void changeMemberPosition( public void addMember( Integer clubId, Integer requesterId, - AddMemberRequest request + ClubMemberAddRequest request ) { clubRepository.getById(clubId); From 8090b432c110eda10905e7f47d3451894f4df649 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Wed, 21 Jan 2026 22:25:36 +0900 Subject: [PATCH 13/13] =?UTF-8?q?ci:=20checkstyle=EC=9D=98=20permission=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/checkstyle.yml | 156 ++++++++++++++++--------------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/.github/workflows/checkstyle.yml b/.github/workflows/checkstyle.yml index 5c26a408..0260a845 100644 --- a/.github/workflows/checkstyle.yml +++ b/.github/workflows/checkstyle.yml @@ -1,76 +1,80 @@ -name: Checkstyle - -on: - pull_request: - branches: - - main - - develop - push: - branches: - - main - - develop - -jobs: - checkstyle: - name: Code Style Check - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - submodules: recursive - token: ${{ secrets.SUBMODULE_TOKEN }} - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Cache Gradle packages - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Run Checkstyle - run: ./gradlew checkstyleMain --no-daemon - - - name: Upload Checkstyle Report - if: failure() - uses: actions/upload-artifact@v4 - with: - name: checkstyle-report - path: | - build/reports/checkstyle/main.html - build/reports/checkstyle/main.xml - retention-days: 7 - - - name: Comment PR with Checkstyle Results - if: github.event_name == 'pull_request' && failure() - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const path = require('path'); - - let comment = '## ⚠️ Checkstyle 위반 사항 발견\n\n'; - comment += 'Checkstyle 검사에서 코딩 컨벤션 위반이 발견되었습니다.\n\n'; - comment += '### 📋 상세 리포트\n'; - comment += '- [Main 소스 리포트 다운로드](../actions/runs/${{ github.run_id }})\n'; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }); +name: Checkstyle + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - main + - develop + +permissions: + contents: read + pull-requests: write + +jobs: + checkstyle: + name: Code Style Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.SUBMODULE_TOKEN }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Checkstyle + run: ./gradlew checkstyleMain --no-daemon + + - name: Upload Checkstyle Report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: checkstyle-report + path: | + build/reports/checkstyle/main.html + build/reports/checkstyle/main.xml + retention-days: 7 + + - name: Comment PR with Checkstyle Results + if: github.event_name == 'pull_request' && failure() + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const path = require('path'); + + let comment = '## ⚠️ Checkstyle 위반 사항 발견\n\n'; + comment += 'Checkstyle 검사에서 코딩 컨벤션 위반이 발견되었습니다.\n\n'; + comment += '### 📋 상세 리포트\n'; + comment += '- [Main 소스 리포트 다운로드](../actions/runs/${{ github.run_id }})\n'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + });