diff --git a/src/main/java/com/join/core/block/controller/BlockController.java b/src/main/java/com/join/core/block/controller/BlockController.java index 4e4d3c8..0736885 100644 --- a/src/main/java/com/join/core/block/controller/BlockController.java +++ b/src/main/java/com/join/core/block/controller/BlockController.java @@ -3,9 +3,11 @@ import com.join.core.auth.domain.UserPrincipal; import com.join.core.block.controller.specification.BlockControllerSpecification; import com.join.core.block.dto.request.CreateBlockRequest; +import com.join.core.block.dto.request.CreateOngoingStudyBlockRequest; import com.join.core.block.dto.response.CreateBlockResponse; import com.join.core.block.service.BlockService; import com.join.core.block.service.dto.CreateBlockParams; +import com.join.core.block.service.dto.CreateOngoingStudyBlockParams; import com.join.core.common.response.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -29,7 +31,7 @@ public ApiResponse create( @AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody @Valid CreateBlockRequest createBlockRequest ) { - return ApiResponse.created(blockService.createBlock( + return ApiResponse.created(blockService.block( new CreateBlockParams( userPrincipal.getAvatarToken(), createBlockRequest.targetAvatarToken(), @@ -37,4 +39,20 @@ public ApiResponse create( ) )); } + + @PreAuthorize("isAuthenticated()") + @PostMapping("/block/study-member") + public ApiResponse createBlockStudyEnrollment( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestBody @Valid CreateOngoingStudyBlockRequest createOngoingStudyBlockRequest + ) { + return ApiResponse.created(blockService.blockStudyEnrollment( + new CreateOngoingStudyBlockParams( + userPrincipal.getAvatarToken(), + createOngoingStudyBlockRequest.targetAvatarToken(), + createOngoingStudyBlockRequest.studyToken(), + createOngoingStudyBlockRequest.blockDate() + ) + )); + } } diff --git a/src/main/java/com/join/core/block/controller/specification/BlockControllerSpecification.java b/src/main/java/com/join/core/block/controller/specification/BlockControllerSpecification.java index e0460d1..71b9638 100644 --- a/src/main/java/com/join/core/block/controller/specification/BlockControllerSpecification.java +++ b/src/main/java/com/join/core/block/controller/specification/BlockControllerSpecification.java @@ -2,11 +2,13 @@ import com.join.core.auth.domain.UserPrincipal; import com.join.core.block.dto.request.CreateBlockRequest; +import com.join.core.block.dto.request.CreateOngoingStudyBlockRequest; import com.join.core.block.dto.response.CreateBlockResponse; import com.join.core.common.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.RequestBody; @@ -20,4 +22,13 @@ ApiResponse create( @AuthenticationPrincipal UserPrincipal userPrincipal, @RequestBody CreateBlockRequest createBlockRequest ); + + @Tag(name = "${swagger.tag.block}") + @Operation(summary = "진행 중인 스터디 멤버 차단 - 인증 필수", + description = "마감된 스터디의 팀원을 차단할 경우 예외 발생 - 일반 사용자 차단 API로 요청", + security = {@SecurityRequirement(name = "session-token")}) + ApiResponse createBlockStudyEnrollment( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestBody @Valid CreateOngoingStudyBlockRequest createOngoingStudyBlockRequest + ); } diff --git a/src/main/java/com/join/core/block/dto/request/CreateOngoingStudyBlockRequest.java b/src/main/java/com/join/core/block/dto/request/CreateOngoingStudyBlockRequest.java new file mode 100644 index 0000000..91b1614 --- /dev/null +++ b/src/main/java/com/join/core/block/dto/request/CreateOngoingStudyBlockRequest.java @@ -0,0 +1,14 @@ +package com.join.core.block.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +@Schema +public record CreateOngoingStudyBlockRequest( + @NotNull @Schema(example = "avt_token") String targetAvatarToken, + @NotNull @Schema(example = "st_token") String studyToken, + @NotNull @Schema(example = "2025-01-20") LocalDate blockDate +) { +} diff --git a/src/main/java/com/join/core/block/service/BlockService.java b/src/main/java/com/join/core/block/service/BlockService.java index 2ddb6e3..de186d2 100644 --- a/src/main/java/com/join/core/block/service/BlockService.java +++ b/src/main/java/com/join/core/block/service/BlockService.java @@ -6,12 +6,18 @@ import com.join.core.block.dto.response.CreateBlockResponse; import com.join.core.block.mapper.BlockMapper; import com.join.core.block.service.dto.CreateBlockParams; +import com.join.core.block.service.dto.CreateOngoingStudyBlockParams; import com.join.core.common.exception.ErrorCode; import com.join.core.common.exception.impl.BadRequestException; import com.join.core.common.exception.impl.EntityAlreadyExistsException; +import com.join.core.common.exception.impl.NoPermissionException; +import com.join.core.enrollment.domain.Enrollment; +import com.join.core.enrollment.service.EnrollmentReader; +import com.join.core.study.domain.Study; import com.join.core.study.service.StudyReader; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -21,14 +27,16 @@ public class BlockService { private final BlockReader blockReader; private final AvatarReader avatarReader; private final StudyReader studyReader; + private final EnrollmentReader enrollmentReader; private final BlockMapper blockMapper; - public CreateBlockResponse createBlock(CreateBlockParams params) { + @Transactional + public CreateBlockResponse block(CreateBlockParams params) { Avatar avatar = avatarReader.getAvatarByAvatarToken(params.subjectAvatarToken()); Avatar target = avatarReader.getAvatarByAvatarToken(params.targetAvatarToken()); checkDuplicated(avatar.getAvatarToken(), target.getAvatarToken()); checkTarget(avatar.getAvatarToken(), target.getAvatarToken()); - checkOngoingStudy(avatar.getAvatarToken(), target.getAvatarToken()); + checkMemberOfNonOngoingStudy(avatar.getAvatarToken(), target.getAvatarToken()); Block block = blockStore.save(blockMapper.toEntity(avatar, target, params.blockDate())); return blockMapper.toCreateBlockResponse(block); } @@ -45,9 +53,40 @@ private void checkTarget(String subjectAvatarToken, String targetAvatarToken) { } } - private void checkOngoingStudy(String subjectAvatarToken, String targetAvatarToken) { + private void checkMemberOfNonOngoingStudy(String subjectAvatarToken, String targetAvatarToken) { if (studyReader.existsByEnrollmentsAvatarToken(subjectAvatarToken, targetAvatarToken)) { throw new EntityAlreadyExistsException(ErrorCode.ACTIVE_STUDY_EXISTS); } } + + @Transactional + public CreateBlockResponse blockStudyEnrollment(CreateOngoingStudyBlockParams params) { + Avatar avatar = avatarReader.getAvatarByAvatarToken(params.subjectAvatarToken()); + Avatar target = avatarReader.getAvatarByAvatarToken(params.targetAvatarToken()); + Study study = studyReader.getStudyByToken(params.studyToken()); + + checkDuplicated(avatar.getAvatarToken(), target.getAvatarToken()); + checkTarget(avatar.getAvatarToken(), target.getAvatarToken()); + study.checkActiveStatus(); + checkEnrollment(avatar.getId(), target.getId(), study.getId()); + + withdrawFromStudy(avatar.getId(), study.getId()); + Block block = blockMapper.toEntity(avatar, target, params.blockDate()); + + return blockMapper.toCreateBlockResponse(blockStore.save(block)); + } + + private void checkEnrollment(Long subjectId, Long targetId, Long studyId) { + if (!enrollmentReader.existEnrollmentByAvatarIdAndStudyId(subjectId, studyId)) { + throw new NoPermissionException(ErrorCode.NOT_MEMBER_OF_STUDY); + } + if (!enrollmentReader.existEnrollmentByAvatarIdAndStudyId(targetId, studyId)) { + throw new BadRequestException(ErrorCode.TARGET_IS_NOT_MEMBER_OF_STUDY); + } + } + + private void withdrawFromStudy(Long avatarId, Long studyId) { + Enrollment enrollment = enrollmentReader.getEnrollmentByAvatarIdAndStudyId(avatarId, studyId); + enrollment.withdraw(); + } } diff --git a/src/main/java/com/join/core/block/service/dto/CreateOngoingStudyBlockParams.java b/src/main/java/com/join/core/block/service/dto/CreateOngoingStudyBlockParams.java new file mode 100644 index 0000000..49ce4f4 --- /dev/null +++ b/src/main/java/com/join/core/block/service/dto/CreateOngoingStudyBlockParams.java @@ -0,0 +1,11 @@ +package com.join.core.block.service.dto; + +import java.time.LocalDate; + +public record CreateOngoingStudyBlockParams( + String subjectAvatarToken, + String targetAvatarToken, + String studyToken, + LocalDate blockDate +) { +} diff --git a/src/main/java/com/join/core/common/exception/ErrorCode.java b/src/main/java/com/join/core/common/exception/ErrorCode.java index 0dba324..9fd37b5 100644 --- a/src/main/java/com/join/core/common/exception/ErrorCode.java +++ b/src/main/java/com/join/core/common/exception/ErrorCode.java @@ -73,6 +73,7 @@ public enum ErrorCode { STUDY_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "주어진 식별자로 스터디를 찾을 수 없습니다."), DUPLICATE_APPLICATION(HttpStatus.BAD_REQUEST, "S-002", "이미 지원한 스터디입니다."), APPLICATION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "S-003", "주어진 식별자로 지원 정보를 찾을 수 없습니다."), + NOT_ACTIVE_STUDY(HttpStatus.BAD_REQUEST, "S-004", "진행 중인 스터디가 아닙니다."), /** * 회차 관련 오류 @@ -134,7 +135,9 @@ public enum ErrorCode { */ BLOCK_ALREADY_EXISTS(HttpStatus.CONFLICT, "BL-001", "이미 존재하는 차단 내역입니다."), SELF_BLOCK_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "BL-002", "자기 자신을 차단할 수 없습니다."), - ACTIVE_STUDY_EXISTS(HttpStatus.BAD_REQUEST, "BL-003", "함께 진행 중인 스터디가 존재합니다."); + ACTIVE_STUDY_EXISTS(HttpStatus.BAD_REQUEST, "BL-003", "함께 진행 중인 스터디가 존재합니다."), + ONGOING_STUDY_MEMBER_ONLY(HttpStatus.BAD_REQUEST, "BL-004", "해당 기능은 진행 중인 스터디의 팀원만 차단할 수 있습니다."), + TARGET_IS_NOT_MEMBER_OF_STUDY(HttpStatus.BAD_REQUEST, "BL-005", "차단하려는 상대가 스터디의 팀원이 아닙니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/join/core/enrollment/domain/Enrollment.java b/src/main/java/com/join/core/enrollment/domain/Enrollment.java index 98815cf..459d11a 100644 --- a/src/main/java/com/join/core/enrollment/domain/Enrollment.java +++ b/src/main/java/com/join/core/enrollment/domain/Enrollment.java @@ -60,4 +60,7 @@ public Enrollment(Study study, Avatar avatar, EnrollmentStatus status, LocalDate this.role = role; } + public void withdraw() { + this.status = EnrollmentStatus.LEFT; + } } diff --git a/src/main/java/com/join/core/enrollment/repository/EnrollmentReaderImpl.java b/src/main/java/com/join/core/enrollment/repository/EnrollmentReaderImpl.java index 23beebe..001680f 100644 --- a/src/main/java/com/join/core/enrollment/repository/EnrollmentReaderImpl.java +++ b/src/main/java/com/join/core/enrollment/repository/EnrollmentReaderImpl.java @@ -2,8 +2,10 @@ import com.join.core.avatar.domain.Avatar; import com.join.core.common.exception.ErrorCode; +import com.join.core.common.exception.impl.EntityNotFoundException; import com.join.core.common.exception.impl.InvalidParamException; import com.join.core.enrollment.constant.EnrollmentStatus; +import com.join.core.enrollment.domain.Enrollment; import com.join.core.enrollment.service.EnrollmentReader; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -30,6 +32,12 @@ public boolean existEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId) return enrollmentRepository.existsByAvatarIdAndStudyIdAndStatus(avatarId, studyId, EnrollmentStatus.JOINED); } + @Override + public Enrollment getEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId) { + return enrollmentRepository.findByAvatarIdAndStudyId(avatarId, studyId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_MEMBER_OF_STUDY)); + } + @Override public void validateEnrollment(Long avatarId, Long studyId) { boolean exists = enrollmentRepository.existsByAvatarIdAndStudyIdAndStatusNot(avatarId, studyId, EnrollmentStatus.PENDING); @@ -37,5 +45,4 @@ public void validateEnrollment(Long avatarId, Long studyId) { throw new InvalidParamException(ErrorCode.INVALID_PARAMETER, "스터디 참여자가 아닙니다."); } } - -} \ No newline at end of file +} diff --git a/src/main/java/com/join/core/enrollment/repository/EnrollmentRepository.java b/src/main/java/com/join/core/enrollment/repository/EnrollmentRepository.java index 95f4299..c3d09ec 100644 --- a/src/main/java/com/join/core/enrollment/repository/EnrollmentRepository.java +++ b/src/main/java/com/join/core/enrollment/repository/EnrollmentRepository.java @@ -4,10 +4,12 @@ import com.join.core.enrollment.domain.Enrollment; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface EnrollmentRepository extends JpaRepository { boolean existsByAvatarIdAndStudyIdAndStatus(Long avatarId, Long studyId, EnrollmentStatus status); - + Optional findByAvatarIdAndStudyId(Long avatarId, Long studyId); boolean existsByAvatarIdAndStudyIdAndStatusNot(Long avatarId, Long studyId, EnrollmentStatus enrollmentStatus); } diff --git a/src/main/java/com/join/core/enrollment/service/EnrollmentReader.java b/src/main/java/com/join/core/enrollment/service/EnrollmentReader.java index 4ec93b7..d8f4101 100644 --- a/src/main/java/com/join/core/enrollment/service/EnrollmentReader.java +++ b/src/main/java/com/join/core/enrollment/service/EnrollmentReader.java @@ -1,6 +1,7 @@ package com.join.core.enrollment.service; import com.join.core.avatar.domain.Avatar; +import com.join.core.enrollment.domain.Enrollment; public interface EnrollmentReader { @@ -8,5 +9,5 @@ public interface EnrollmentReader { Avatar getLeaderByStudyId(Long studyId); boolean existEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId); void validateEnrollment(Long avatarId, Long studyId); - + Enrollment getEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId); } diff --git a/src/main/java/com/join/core/study/domain/Study.java b/src/main/java/com/join/core/study/domain/Study.java index f9c691c..4522460 100644 --- a/src/main/java/com/join/core/study/domain/Study.java +++ b/src/main/java/com/join/core/study/domain/Study.java @@ -3,6 +3,8 @@ import com.join.core.address.domain.Address; import com.join.core.avatar.domain.Avatar; import com.join.core.category.domain.Category; +import com.join.core.common.exception.ErrorCode; +import com.join.core.common.exception.impl.BadRequestException; import com.join.core.common.exception.impl.InvalidParamException; import com.join.core.common.util.TokenGenerator; import com.join.core.schedule.domain.StudySchedule; @@ -181,11 +183,16 @@ public boolean isWriter(Long avatarId) { return getWriter().getId().equals(avatarId); } + public void checkActiveStatus() { + if (status != StudyStatus.ACTIVE) { + throw new BadRequestException(ErrorCode.NOT_ACTIVE_STUDY); + } + } + public void deleteBookmarkCount() { if (this.bookmarkCnt <= 0) { throw new IllegalStateException("북마크 수는 음수가 될 수 없습니다."); } this.bookmarkCnt--; } - }