Skip to content

Commit

Permalink
Merge pull request #96 from ecolink-JOIN/feat/#76-block-study-enrollment
Browse files Browse the repository at this point in the history
[Feat] 스터디 멤버 차단
  • Loading branch information
homebdy authored Feb 7, 2025
2 parents caac54b + f42beb7 commit a1788ce
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,12 +31,28 @@ public ApiResponse<CreateBlockResponse> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateBlockRequest createBlockRequest
) {
return ApiResponse.created(blockService.createBlock(
return ApiResponse.created(blockService.block(
new CreateBlockParams(
userPrincipal.getAvatarToken(),
createBlockRequest.targetAvatarToken(),
createBlockRequest.blockDate()
)
));
}

@PreAuthorize("isAuthenticated()")
@PostMapping("/block/study-member")
public ApiResponse<CreateBlockResponse> createBlockStudyEnrollment(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateOngoingStudyBlockRequest createOngoingStudyBlockRequest
) {
return ApiResponse.created(blockService.blockStudyEnrollment(
new CreateOngoingStudyBlockParams(
userPrincipal.getAvatarToken(),
createOngoingStudyBlockRequest.targetAvatarToken(),
createOngoingStudyBlockRequest.studyToken(),
createOngoingStudyBlockRequest.blockDate()
)
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,4 +22,13 @@ ApiResponse<CreateBlockResponse> create(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody CreateBlockRequest createBlockRequest
);

@Tag(name = "${swagger.tag.block}")
@Operation(summary = "진행 중인 스터디 멤버 차단 - 인증 필수",
description = "마감된 스터디의 팀원을 차단할 경우 예외 발생 - 일반 사용자 차단 API로 요청",
security = {@SecurityRequirement(name = "session-token")})
ApiResponse<CreateBlockResponse> createBlockStudyEnrollment(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestBody @Valid CreateOngoingStudyBlockRequest createOngoingStudyBlockRequest
);
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
45 changes: 42 additions & 3 deletions src/main/java/com/join/core/block/service/BlockService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
5 changes: 4 additions & 1 deletion src/main/java/com/join/core/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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", "진행 중인 스터디가 아닙니다."),

/**
* 회차 관련 오류
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/join/core/enrollment/domain/Enrollment.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ public Enrollment(Study study, Avatar avatar, EnrollmentStatus status, LocalDate
this.role = role;
}

public void withdraw() {
this.status = EnrollmentStatus.LEFT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,12 +32,17 @@ 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);
if (!exists) {
throw new InvalidParamException(ErrorCode.INVALID_PARAMETER, "스터디 참여자가 아닙니다.");
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Enrollment, Long> {

boolean existsByAvatarIdAndStudyIdAndStatus(Long avatarId, Long studyId, EnrollmentStatus status);

Optional<Enrollment> findByAvatarIdAndStudyId(Long avatarId, Long studyId);
boolean existsByAvatarIdAndStudyIdAndStatusNot(Long avatarId, Long studyId, EnrollmentStatus enrollmentStatus);

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.join.core.enrollment.service;

import com.join.core.avatar.domain.Avatar;
import com.join.core.enrollment.domain.Enrollment;

public interface EnrollmentReader {

double getAverageByStudyId(Long studyId);
Avatar getLeaderByStudyId(Long studyId);
boolean existEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId);
void validateEnrollment(Long avatarId, Long studyId);

Enrollment getEnrollmentByAvatarIdAndStudyId(Long avatarId, Long studyId);
}
9 changes: 8 additions & 1 deletion src/main/java/com/join/core/study/domain/Study.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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--;
}

}

0 comments on commit a1788ce

Please sign in to comment.