diff --git a/src/main/java/in/koreatech/koin/_common/event/ClubCreateEvent.java b/src/main/java/in/koreatech/koin/_common/event/ClubCreateEvent.java new file mode 100644 index 0000000000..cffdc43bbc --- /dev/null +++ b/src/main/java/in/koreatech/koin/_common/event/ClubCreateEvent.java @@ -0,0 +1,6 @@ +package in.koreatech.koin._common.event; + +public record ClubCreateEvent( + String clubName +) { +} diff --git a/src/main/java/in/koreatech/koin/admin/club/service/AdminClubService.java b/src/main/java/in/koreatech/koin/admin/club/service/AdminClubService.java index 4977b10486..9045fce739 100644 --- a/src/main/java/in/koreatech/koin/admin/club/service/AdminClubService.java +++ b/src/main/java/in/koreatech/koin/admin/club/service/AdminClubService.java @@ -1,16 +1,15 @@ package in.koreatech.koin.admin.club.service; +import static in.koreatech.koin.domain.club.enums.SNSType.*; + +import java.util.AbstractMap; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.AbstractMap; -import java.util.List; import java.util.stream.Stream; -import static in.koreatech.koin.domain.club.enums.SNSType.*; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -36,7 +35,7 @@ import in.koreatech.koin.domain.club.model.Club; import in.koreatech.koin.domain.club.model.ClubAdmin; import in.koreatech.koin.domain.club.model.ClubCategory; - +import in.koreatech.koin.domain.club.model.ClubSNS; import in.koreatech.koin.domain.club.model.redis.ClubCreateRedis; import in.koreatech.koin.domain.club.repository.ClubAdminRepository; import in.koreatech.koin.domain.club.repository.ClubCategoryRepository; @@ -44,8 +43,6 @@ import in.koreatech.koin.domain.club.repository.redis.ClubCreateRedisRepository; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; -import in.koreatech.koin.domain.club.model.ClubSNS; - import lombok.RequiredArgsConstructor; @Service diff --git a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java index f687e7f65e..96f188db86 100644 --- a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java +++ b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java @@ -14,8 +14,10 @@ import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin._common.auth.Auth; +import in.koreatech.koin._common.auth.UserId; import in.koreatech.koin.domain.club.dto.request.CreateClubRequest; import in.koreatech.koin.domain.club.dto.request.CreateQnaRequest; +import in.koreatech.koin.domain.club.dto.request.EmpowermentClubManagerRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubIntroductionRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubRequest; import in.koreatech.koin.domain.club.dto.response.ClubHotResponse; @@ -94,7 +96,8 @@ ResponseEntity updateClubIntroduction( @Operation(summary = "동아리를 상세조회한다") @PostMapping("/{clubId}") ResponseEntity getClub( - @Parameter(in = PATH) @PathVariable Integer clubId + @Parameter(in = PATH) @PathVariable Integer clubId, + @UserId Integer userId ); @ApiResponses( @@ -221,4 +224,20 @@ ResponseEntity deleteQna( @Parameter(in = PATH) @PathVariable Integer qnaId, @Auth(permit = {STUDENT}) Integer studentId ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "동아리 관리자 권한을 위임한다") + @PutMapping("/empowerment") + ResponseEntity empowermentClubManager( + @RequestBody @Valid EmpowermentClubManagerRequest request, + @Auth(permit = {STUDENT}) Integer studentId + ); } diff --git a/src/main/java/in/koreatech/koin/domain/club/controller/ClubController.java b/src/main/java/in/koreatech/koin/domain/club/controller/ClubController.java index b0e1c9d4d2..2ba291dd60 100644 --- a/src/main/java/in/koreatech/koin/domain/club/controller/ClubController.java +++ b/src/main/java/in/koreatech/koin/domain/club/controller/ClubController.java @@ -16,8 +16,10 @@ import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin._common.auth.Auth; +import in.koreatech.koin._common.auth.UserId; import in.koreatech.koin.domain.club.dto.request.CreateClubRequest; import in.koreatech.koin.domain.club.dto.request.CreateQnaRequest; +import in.koreatech.koin.domain.club.dto.request.EmpowermentClubManagerRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubIntroductionRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubRequest; import in.koreatech.koin.domain.club.dto.response.ClubHotResponse; @@ -76,9 +78,10 @@ public ResponseEntity getClubByCategory( @GetMapping("/{clubId}") public ResponseEntity getClub( - @Parameter(in = PATH) @PathVariable Integer clubId + @Parameter(in = PATH) @PathVariable Integer clubId, + @UserId Integer userId ) { - ClubResponse response = clubService.getClub(clubId); + ClubResponse response = clubService.getClub(clubId, userId); return ResponseEntity.ok(response); } @@ -133,4 +136,13 @@ public ResponseEntity deleteQna( clubService.deleteQna(clubId, qnaId, studentId); return ResponseEntity.noContent().build(); } + + @PutMapping("/empowerment") + public ResponseEntity empowermentClubManager( + @RequestBody @Valid EmpowermentClubManagerRequest request, + @Auth(permit = {STUDENT}) Integer studentId + ) { + clubService.empowermentClubManager(request, studentId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/in/koreatech/koin/domain/club/dto/request/CreateClubRequest.java b/src/main/java/in/koreatech/koin/domain/club/dto/request/CreateClubRequest.java index 2279d1c4c0..27a01c438f 100644 --- a/src/main/java/in/koreatech/koin/domain/club/dto/request/CreateClubRequest.java +++ b/src/main/java/in/koreatech/koin/domain/club/dto/request/CreateClubRequest.java @@ -27,7 +27,7 @@ public record CreateClubRequest( @Schema(description = "동아리 관리자 ID 리스트", requiredMode = REQUIRED) @NotEmpty(message = "동아리 관리자는 필수 입력 사항입니다.") - List clubAdmins, + List clubManagers, @Schema(description = "동아리 분과 카테고리 ID", example = "1", requiredMode = REQUIRED) @NotNull(message = "동아리 분과 카테고리 ID는 필수 입력 사항입니다.") @@ -54,7 +54,7 @@ public record CreateClubRequest( String phoneNumber ) { @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) - public record InnerClubAdminRequest( + public record InnerClubManagerRequest( @Schema(description = "동아리 관리자 id", example = "bcsdlab", requiredMode = REQUIRED) String userid ) { diff --git a/src/main/java/in/koreatech/koin/domain/club/dto/request/EmpowermentClubManagerRequest.java b/src/main/java/in/koreatech/koin/domain/club/dto/request/EmpowermentClubManagerRequest.java new file mode 100644 index 0000000000..57ac295147 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/club/dto/request/EmpowermentClubManagerRequest.java @@ -0,0 +1,16 @@ +package in.koreatech.koin.domain.club.dto.request; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; + +public record EmpowermentClubManagerRequest( + @Schema(description = "동아리 아이디", example = "1", requiredMode = REQUIRED) + Integer clubId, + + @Schema(description = "위임받는 사용자의 아이디", example = "example", requiredMode = REQUIRED) + @NotEmpty(message = "위임받는 사용자의 아이디를 입력해주세요.") + String changedManagerId +) { +} diff --git a/src/main/java/in/koreatech/koin/domain/club/dto/response/ClubResponse.java b/src/main/java/in/koreatech/koin/domain/club/dto/response/ClubResponse.java index 2649664075..d557c5509e 100644 --- a/src/main/java/in/koreatech/koin/domain/club/dto/response/ClubResponse.java +++ b/src/main/java/in/koreatech/koin/domain/club/dto/response/ClubResponse.java @@ -44,9 +44,12 @@ public record ClubResponse( Optional openChat, @Schema(description = "전화번호", example = "010-1234-5678") - Optional phoneNumber + Optional phoneNumber, + + @Schema(description = "동아리 관리자 여부", example = "true") + Boolean manager ) { - public static ClubResponse from(Club club, List clubSNSs) { + public static ClubResponse from(Club club, List clubSNSs, Boolean manager) { Optional instagram = Optional.empty(); Optional googleForm = Optional.empty(); Optional openChat = Optional.empty(); @@ -73,7 +76,8 @@ public static ClubResponse from(Club club, List clubSNSs) { instagram, googleForm, openChat, - phoneNumber + phoneNumber, + manager ); } } diff --git a/src/main/java/in/koreatech/koin/domain/club/exception/AlreadyManagerException.java b/src/main/java/in/koreatech/koin/domain/club/exception/AlreadyManagerException.java new file mode 100644 index 0000000000..f2ea5d8249 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/club/exception/AlreadyManagerException.java @@ -0,0 +1,19 @@ +package in.koreatech.koin.domain.club.exception; + +import in.koreatech.koin._common.exception.custom.KoinException; + +public class AlreadyManagerException extends KoinException { + private static final String DEFAULT_MESSAGE = "이미 동아리의 관리자입니다."; + + public AlreadyManagerException(String message) { + super(message); + } + + public AlreadyManagerException(String message, String detail) { + super(message, detail); + } + + public static AlreadyManagerException withDetail(String detail) { + return new AlreadyManagerException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/club/model/redis/ClubCreateRedis.java b/src/main/java/in/koreatech/koin/domain/club/model/redis/ClubCreateRedis.java index 61241fde59..e493702d49 100644 --- a/src/main/java/in/koreatech/koin/domain/club/model/redis/ClubCreateRedis.java +++ b/src/main/java/in/koreatech/koin/domain/club/model/redis/ClubCreateRedis.java @@ -1,5 +1,7 @@ package in.koreatech.koin.domain.club.model.redis; +import static in.koreatech.koin.domain.club.dto.request.CreateClubRequest.InnerClubManagerRequest; + import java.time.LocalDateTime; import java.util.List; @@ -25,7 +27,7 @@ public class ClubCreateRedis { private String imageUrl; - private List clubAdmins; + private List clubAdmins; private Integer clubCategoryId; @@ -50,7 +52,7 @@ private ClubCreateRedis( String id, String name, String imageUrl, - List clubAdmins, + List clubAdmins, Integer clubCategoryId, String location, String description, @@ -81,7 +83,7 @@ public static ClubCreateRedis of(CreateClubRequest request, Integer requesterId) .id(request.name()) .name(request.name()) .imageUrl(request.imageUrl()) - .clubAdmins(request.clubAdmins()) + .clubAdmins(request.clubManagers()) .clubCategoryId(request.clubCategoryId()) .location(request.location()) .description(request.description()) diff --git a/src/main/java/in/koreatech/koin/domain/club/repository/ClubAdminRepository.java b/src/main/java/in/koreatech/koin/domain/club/repository/ClubAdminRepository.java index e908767c89..b28382be76 100644 --- a/src/main/java/in/koreatech/koin/domain/club/repository/ClubAdminRepository.java +++ b/src/main/java/in/koreatech/koin/domain/club/repository/ClubAdminRepository.java @@ -5,7 +5,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import in.koreatech.koin.domain.club.model.Club; import in.koreatech.koin.domain.club.model.ClubAdmin; +import in.koreatech.koin.domain.user.model.User; public interface ClubAdminRepository extends Repository { @@ -26,4 +28,8 @@ SELECT COUNT(ca) int countAll(); boolean existsByClubIdAndUserId(Integer clubId, Integer studentId); + + boolean existsByClubAndUser(Club club, User user); + + void deleteByClubAndUser(Club club, User user); } diff --git a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java index 165270a4d6..aa4f6026c6 100644 --- a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java +++ b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java @@ -3,12 +3,15 @@ import java.util.List; import java.util.Objects; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin._common.auth.exception.AuthorizationException; +import in.koreatech.koin._common.event.ClubCreateEvent; import in.koreatech.koin.domain.club.dto.request.CreateClubRequest; import in.koreatech.koin.domain.club.dto.request.CreateQnaRequest; +import in.koreatech.koin.domain.club.dto.request.EmpowermentClubManagerRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubIntroductionRequest; import in.koreatech.koin.domain.club.dto.request.UpdateClubRequest; import in.koreatech.koin.domain.club.dto.response.ClubHotResponse; @@ -16,10 +19,12 @@ import in.koreatech.koin.domain.club.dto.response.ClubsByCategoryResponse; import in.koreatech.koin.domain.club.dto.response.QnasResponse; import in.koreatech.koin.domain.club.enums.SNSType; +import in.koreatech.koin.domain.club.exception.AlreadyManagerException; import in.koreatech.koin.domain.club.exception.ClubHotNotFoundException; import in.koreatech.koin.domain.club.exception.ClubLikeNotFoundException; import in.koreatech.koin.domain.club.exception.DuplicateClubLikiException; import in.koreatech.koin.domain.club.model.Club; +import in.koreatech.koin.domain.club.model.ClubAdmin; import in.koreatech.koin.domain.club.model.ClubCategory; import in.koreatech.koin.domain.club.model.ClubLike; import in.koreatech.koin.domain.club.model.ClubQna; @@ -57,11 +62,14 @@ public class ClubService { private final ClubLikeRepository clubLikeRepository; private final UserRepository userRepository; private final ClubCreateRedisRepository clubCreateRedisRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public void createClubRequest(CreateClubRequest request, Integer studentId) { ClubCreateRedis createRedis = ClubCreateRedis.of(request, studentId); clubCreateRedisRepository.save(createRedis); + + eventPublisher.publishEvent(new ClubCreateEvent(request.name())); } @Transactional @@ -73,8 +81,9 @@ public ClubResponse updateClub(Integer clubId, UpdateClubRequest request, Intege club.update(request.name(), request.imageUrl(), clubCategory, request.location(), request.description()); List newSNS = updateClubSNS(request, club); + Boolean manager = clubAdminRepository.existsByClubIdAndUserId(clubId, studentId); - return ClubResponse.from(club, newSNS); + return ClubResponse.from(club, newSNS, manager); } private List updateClubSNS(UpdateClubRequest request, Club club) { @@ -103,8 +112,9 @@ public ClubResponse updateClubIntroduction( club.updateIntroduction(request.introduction()); List clubSNSs = club.getClubSNSs(); + Boolean manager = clubAdminRepository.existsByClubIdAndUserId(clubId, studentId); - return ClubResponse.from(club, clubSNSs); + return ClubResponse.from(club, clubSNSs, manager); } private void isClubAdmin(Integer clubId, Integer studentId) { @@ -114,12 +124,13 @@ private void isClubAdmin(Integer clubId, Integer studentId) { } @Transactional - public ClubResponse getClub(Integer clubId) { + public ClubResponse getClub(Integer clubId, Integer userId) { Club club = clubRepository.getByIdWithPessimisticLock(clubId); club.increaseHits(); List clubSNSs = clubSNSRepository.findAllByClub(club); + Boolean manager = clubAdminRepository.existsByClubIdAndUserId(clubId, userId); - return ClubResponse.from(club, clubSNSs); + return ClubResponse.from(club, clubSNSs, manager); } public ClubsByCategoryResponse getClubByCategory(Integer categoryId, Boolean hitSort) { @@ -217,4 +228,24 @@ private void validateQnaDeleteAuthorization(Integer clubId, ClubQna qna, Integer return; throw AuthorizationException.withDetail("studentId: " + studentId); } + + @Transactional + public void empowermentClubManager(EmpowermentClubManagerRequest request, Integer studentId) { + Club club = clubRepository.getById(request.clubId()); + User currentManager = userRepository.getById(studentId); + User changedManager = userRepository.getByUserId(request.changedManagerId()); + + isClubAdmin(request.clubId(), studentId); + if (clubAdminRepository.existsByClubAndUser(club, changedManager)) { + throw AlreadyManagerException.withDetail(""); + } + clubAdminRepository.deleteByClubAndUser(club, currentManager); + + ClubAdmin newClubManager = ClubAdmin.builder() + .club(club) + .user(changedManager) + .build(); + + clubAdminRepository.save(newClubManager); + } } diff --git a/src/main/java/in/koreatech/koin/infrastructure/slack/eventlistener/ClubEventListener.java b/src/main/java/in/koreatech/koin/infrastructure/slack/eventlistener/ClubEventListener.java new file mode 100644 index 0000000000..7285e0639a --- /dev/null +++ b/src/main/java/in/koreatech/koin/infrastructure/slack/eventlistener/ClubEventListener.java @@ -0,0 +1,30 @@ +package in.koreatech.koin.infrastructure.slack.eventlistener; + +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import in.koreatech.koin._common.event.ClubCreateEvent; +import in.koreatech.koin.infrastructure.slack.client.SlackClient; +import in.koreatech.koin.infrastructure.slack.model.SlackNotificationFactory; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +@Transactional(propagation = Propagation.REQUIRES_NEW) +public class ClubEventListener { + + private final SlackClient slackClient; + private final SlackNotificationFactory slackNotificationFactory; + + @Async + @TransactionalEventListener(phase = AFTER_COMMIT) + public void onClubCreateEvent(ClubCreateEvent event){ + var notification = slackNotificationFactory.generateClubCreateSendNotification(event.clubName()); + slackClient.sendMessage(notification); + } +} diff --git a/src/main/java/in/koreatech/koin/infrastructure/slack/model/SlackNotificationFactory.java b/src/main/java/in/koreatech/koin/infrastructure/slack/model/SlackNotificationFactory.java index 22caf7ba16..2e3b4ccbaa 100644 --- a/src/main/java/in/koreatech/koin/infrastructure/slack/model/SlackNotificationFactory.java +++ b/src/main/java/in/koreatech/koin/infrastructure/slack/model/SlackNotificationFactory.java @@ -219,4 +219,19 @@ public SlackNotification generateUserEmailVerificationSendNotification( ) .build(); } + + /** + * 동아리 생성 요청 알림 + */ + public SlackNotification generateClubCreateSendNotification( + String clubName + ) { + return SlackNotification.builder() + .slackUrl(eventNotificationUrl) + .text(String.format(""" + `%s` 동아리 생성 요청이 들어왔습니다. + """, clubName) + ) + .build(); + } }