diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 68cbeb04..ed8f90d6 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -609,3 +609,28 @@ include::{snippets}/get-activity-participants/fail/not-found-study/response-fiel .존재하지 않는 활동을 전달한 경우 include::{snippets}/get-activity-participants/fail/not-found-activity/response-body.adoc[] include::{snippets}/get-activity-participants/fail/not-found-activity/response-fields.adoc[] + + +=== 스터디 활동 삭제 + +스터디 활동을 삭제하는 API입니다. + +==== POST /api/v1/studies/{studyId}/activities/{activityId} + +===== 요청 +include::{snippets}/delete-activity/success/http-request.adoc[] +include::{snippets}/delete-activity/success/path-parameters.adoc[] +include::{snippets}/delete-activity/success/request-headers.adoc[] + +===== 응답 성공 (200) +include::{snippets}/delete-activity/success/response-body.adoc[] +include::{snippets}/delete-activity/success/response-fields.adoc[] + +===== 응답 실패 (403) +.삭제를 시도하는 멤버가 일반 멤버인 경우 +include::{snippets}/delete-activity/fail/forbidden/response-body.adoc[] +include::{snippets}/delete-activity/fail/forbidden/response-fields.adoc[] + +.스터디의 멤버가 아닌 경우 +include::{snippets}/delete-activity/fail/not-joined-member/response-body.adoc[] +include::{snippets}/delete-activity/fail/not-joined-member/response-fields.adoc[] \ No newline at end of file diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApi.java b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApi.java new file mode 100644 index 00000000..6e353be0 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApi.java @@ -0,0 +1,42 @@ +package com.stumeet.server.activity.adapter.in; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.stumeet.server.activity.application.port.in.ActivityDeleteUseCase; +import com.stumeet.server.activity.application.port.in.command.ActivityDeleteCommand; +import com.stumeet.server.common.annotation.WebAdapter; +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.model.ApiResponse; +import com.stumeet.server.common.response.SuccessCode; + +import lombok.RequiredArgsConstructor; + +@WebAdapter +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class ActivityDeleteApi { + + private final ActivityDeleteUseCase activityDeleteUseCase; + + @DeleteMapping("/studies/{studyId}/activities/{activityId}") + public ResponseEntity> delete( + @AuthenticationPrincipal LoginMember member, + @PathVariable Long studyId, + @PathVariable Long activityId + ) { + ActivityDeleteCommand command = ActivityDeleteCommand.builder() + .memberId(member.getId()) + .studyId(studyId) + .activityId(activityId) + .build(); + activityDeleteUseCase.delete(command); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.DELETE_SUCCESS)); + } +} diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java index 604cd7f3..24e27498 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java @@ -2,6 +2,7 @@ import com.stumeet.server.activity.adapter.out.mapper.ActivityImagePersistenceMapper; import com.stumeet.server.activity.adapter.out.model.ActivityImageJpaEntity; +import com.stumeet.server.activity.application.port.out.ActivityImageCommandPort; import com.stumeet.server.activity.application.port.out.ActivityImageCreatePort; import com.stumeet.server.activity.application.port.out.ActivityImageQueryPort; import com.stumeet.server.activity.domain.model.ActivityImage; @@ -12,7 +13,7 @@ @PersistenceAdapter @RequiredArgsConstructor -public class ActivityImagePersistenceAdapter implements ActivityImageCreatePort, ActivityImageQueryPort { +public class ActivityImagePersistenceAdapter implements ActivityImageCreatePort, ActivityImageQueryPort, ActivityImageCommandPort { private final JpaActivityImageRepository jpaActivityImageRepository; private final ActivityImagePersistenceMapper activityImagePersistenceMapper; @@ -30,4 +31,9 @@ public List findAllByActivityId(Long activityId) { .map(activityImagePersistenceMapper::toDomain) .toList(); } + + @Override + public void deleteAllByActivityId(Long activityId) { + jpaActivityImageRepository.deleteAllByActivityId(activityId); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java index 1acdc8b5..007324ba 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java @@ -2,6 +2,7 @@ import com.stumeet.server.activity.adapter.out.mapper.ActivityParticipantPersistenceMapper; import com.stumeet.server.activity.adapter.out.model.ActivityParticipantJpaEntity; +import com.stumeet.server.activity.application.port.out.ActivityParticipantCommandPort; import com.stumeet.server.activity.application.port.out.ActivityParticipantCreatePort; import com.stumeet.server.activity.application.port.out.ActivityParticipantQueryPort; import com.stumeet.server.activity.domain.model.ActivityParticipant; @@ -12,7 +13,7 @@ @PersistenceAdapter @RequiredArgsConstructor -public class ActivityParticipantPersistenceAdapter implements ActivityParticipantCreatePort, ActivityParticipantQueryPort { +public class ActivityParticipantPersistenceAdapter implements ActivityParticipantCreatePort, ActivityParticipantQueryPort, ActivityParticipantCommandPort { private final JpaActivityParticipantRepository jpaActivityParticipantRepository; private final ActivityParticipantPersistenceMapper activityParticipantPersistenceMapper; @@ -32,4 +33,8 @@ public List findAllByActivityId(Long activityId) { .toList(); } + @Override + public void deleteByActivityId(Long activityId) { + jpaActivityParticipantRepository.deleteAllByActivityId(activityId); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java index e595edaf..decc28f8 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java @@ -9,7 +9,9 @@ import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.adapter.out.mapper.ActivityPersistenceMapper; import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; +import com.stumeet.server.activity.application.port.out.ActivityAuthorValidationPort; import com.stumeet.server.activity.application.port.out.ActivityCreatePort; +import com.stumeet.server.activity.application.port.out.ActivityDeletePort; import com.stumeet.server.activity.application.port.out.ActivityQueryPort; import com.stumeet.server.activity.domain.exception.NotExistsActivityException; import com.stumeet.server.activity.domain.model.Activity; @@ -20,7 +22,7 @@ @PersistenceAdapter @RequiredArgsConstructor -public class ActivityPersistenceAdapter implements ActivityCreatePort, ActivityQueryPort { +public class ActivityPersistenceAdapter implements ActivityCreatePort, ActivityQueryPort, ActivityAuthorValidationPort, ActivityDeletePort { private final JpaActivityRepository jpaActivityRepository; private final ActivityPersistenceMapper activityPersistenceMapper; @@ -59,4 +61,15 @@ public List getBriefsByCondition(Boolean isNotice, Lo ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate) { return jpaActivityRepository.findBriefsByCondition(isNotice, memberId, studyId, category, startDate, endDate); } + + @Override + public boolean isNotActivityAuthor(Long memberId, Long activityId) { + return jpaActivityRepository.findByIdAndAuthorId(activityId, memberId) + .isEmpty(); + } + + @Override + public void deleteById(Long activityId) { + jpaActivityRepository.deleteById(activityId); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java index e9525b3f..90ff15c2 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java @@ -7,4 +7,6 @@ public interface JpaActivityImageRepository extends JpaRepository { List findAllByActivityId(Long activityId); + + void deleteAllByActivityId(Long activityId); } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java index df652e6e..0c7d4fc2 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java @@ -7,4 +7,5 @@ public interface JpaActivityParticipantRepository extends JpaRepository { List findAllByActivityId(Long activityId); + void deleteAllByActivityId(Long activityId); } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java index 859b7dbd..109f44c7 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java @@ -1,7 +1,12 @@ package com.stumeet.server.activity.adapter.out.persistence; +import java.util.Optional; + import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; public interface JpaActivityRepository extends JpaRepository, JpaActivityRepositoryCustom { + + Optional findByIdAndAuthorId(@Param("id") Long activityId, @Param("authorId") Long memberId); } diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityAuthorityValidationUseCase.java b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityAuthorityValidationUseCase.java new file mode 100644 index 00000000..31dac5d9 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityAuthorityValidationUseCase.java @@ -0,0 +1,6 @@ +package com.stumeet.server.activity.application.port.in; + +public interface ActivityAuthorityValidationUseCase { + + void checkDeleteAuthority(Long studyId, Long memberId, Long activityId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityDeleteUseCase.java b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityDeleteUseCase.java new file mode 100644 index 00000000..515b00c1 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityDeleteUseCase.java @@ -0,0 +1,8 @@ +package com.stumeet.server.activity.application.port.in; + +import com.stumeet.server.activity.application.port.in.command.ActivityDeleteCommand; + +public interface ActivityDeleteUseCase { + + void delete(ActivityDeleteCommand command); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityDeleteCommand.java b/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityDeleteCommand.java new file mode 100644 index 00000000..8432f09e --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityDeleteCommand.java @@ -0,0 +1,11 @@ +package com.stumeet.server.activity.application.port.in.command; + +import lombok.Builder; + +@Builder +public record ActivityDeleteCommand( + Long memberId, + Long studyId, + Long activityId +) { +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityAuthorValidationPort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityAuthorValidationPort.java new file mode 100644 index 00000000..f7ea9504 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityAuthorValidationPort.java @@ -0,0 +1,6 @@ +package com.stumeet.server.activity.application.port.out; + +public interface ActivityAuthorValidationPort { + + boolean isNotActivityAuthor(Long memberId, Long activityId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityDeletePort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityDeletePort.java new file mode 100644 index 00000000..2b6fed87 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityDeletePort.java @@ -0,0 +1,6 @@ +package com.stumeet.server.activity.application.port.out; + +public interface ActivityDeletePort { + + void deleteById(Long activityId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCommandPort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCommandPort.java new file mode 100644 index 00000000..508a38b6 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCommandPort.java @@ -0,0 +1,6 @@ +package com.stumeet.server.activity.application.port.out; + +public interface ActivityImageCommandPort { + + void deleteAllByActivityId(Long activityId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCommandPort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCommandPort.java new file mode 100644 index 00000000..1aa3d03f --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCommandPort.java @@ -0,0 +1,6 @@ +package com.stumeet.server.activity.application.port.out; + +public interface ActivityParticipantCommandPort { + + void deleteByActivityId(Long activityId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/service/ActivityAuthorityValidationService.java b/src/main/java/com/stumeet/server/activity/application/service/ActivityAuthorityValidationService.java new file mode 100644 index 00000000..55a35745 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityAuthorityValidationService.java @@ -0,0 +1,24 @@ +package com.stumeet.server.activity.application.service; + +import com.stumeet.server.activity.application.port.in.ActivityAuthorityValidationUseCase; +import com.stumeet.server.activity.application.port.out.ActivityAuthorValidationPort; +import com.stumeet.server.activity.domain.exception.ActivityManagementAccessDeniedException; +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.studymember.application.port.out.StudyMemberValidationPort; + +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +public class ActivityAuthorityValidationService implements ActivityAuthorityValidationUseCase { + + private final StudyMemberValidationPort studyMemberValidationPort; + private final ActivityAuthorValidationPort activityAuthorValidationPort; + + @Override + public void checkDeleteAuthority(Long studyId, Long memberId, Long activityId) { + if (studyMemberValidationPort.isNotAdmin(studyId, memberId) && activityAuthorValidationPort.isNotActivityAuthor(memberId, activityId)) { + throw new ActivityManagementAccessDeniedException(); + } + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/service/ActivityDeleteService.java b/src/main/java/com/stumeet/server/activity/application/service/ActivityDeleteService.java new file mode 100644 index 00000000..0e6109fc --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityDeleteService.java @@ -0,0 +1,42 @@ +package com.stumeet.server.activity.application.service; + +import org.springframework.transaction.annotation.Transactional; + +import com.stumeet.server.activity.application.port.in.ActivityAuthorityValidationUseCase; +import com.stumeet.server.activity.application.port.in.ActivityDeleteUseCase; +import com.stumeet.server.activity.application.port.in.command.ActivityDeleteCommand; +import com.stumeet.server.activity.application.port.out.ActivityDeletePort; +import com.stumeet.server.activity.application.port.out.ActivityParticipantCommandPort; +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.study.application.port.in.ActivityImageDeleteUseCase; +import com.stumeet.server.study.application.port.in.ActivityParticipantDeleteUseCase; +import com.stumeet.server.study.application.port.in.StudyValidationUseCase; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; + +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +@Transactional +public class ActivityDeleteService implements ActivityDeleteUseCase { + + private final StudyMemberValidationUseCase studyMemberValidationUseCase; + private final StudyValidationUseCase studyValidationUseCase; + private final ActivityAuthorityValidationUseCase activityAuthorityValidationUseCase; + + private final ActivityImageDeleteUseCase activityImageDeleteUseCase; + private final ActivityParticipantDeleteUseCase activityParticipantDeleteUseCase; + + private final ActivityDeletePort activityDeletePort; + + @Override + public void delete(ActivityDeleteCommand command) { + studyValidationUseCase.checkById(command.studyId()); + studyMemberValidationUseCase.checkStudyJoinMember(command.studyId(), command.memberId()); + activityAuthorityValidationUseCase.checkDeleteAuthority(command.studyId(), command.memberId(), command.activityId()); + + activityImageDeleteUseCase.deleteByActivityId(command.studyId(), command.activityId()); + activityParticipantDeleteUseCase.deleteByActivityId(command.activityId()); + activityDeletePort.deleteById(command.activityId()); + } +} diff --git a/src/main/java/com/stumeet/server/activity/domain/exception/ActivityManagementAccessDeniedException.java b/src/main/java/com/stumeet/server/activity/domain/exception/ActivityManagementAccessDeniedException.java new file mode 100644 index 00000000..383096d0 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/domain/exception/ActivityManagementAccessDeniedException.java @@ -0,0 +1,10 @@ +package com.stumeet.server.activity.domain.exception; + +import com.stumeet.server.common.exception.model.InvalidStateException; +import com.stumeet.server.common.response.ErrorCode; + +public class ActivityManagementAccessDeniedException extends InvalidStateException { + public ActivityManagementAccessDeniedException() { + super(ErrorCode.ACTIVITY_MANAGEMENT_ACCESS_DENIED_EXCEPTION); + } +} diff --git a/src/main/java/com/stumeet/server/common/response/ErrorCode.java b/src/main/java/com/stumeet/server/common/response/ErrorCode.java index 0862f201..15cd8be8 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -34,6 +34,7 @@ public enum ErrorCode { INVALID_ACTIVITY_CATEGORY_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 활동 카테고리입니다."), START_DATE_NOT_YET_EXCEPTION(HttpStatus.BAD_REQUEST, "시작일 전에 스터디를 완료할 수 없습니다."), + /* 401 - UNAUTHORIZED */ @@ -42,6 +43,7 @@ public enum ErrorCode { JWT_TOKEN_PARSING_EXCEPTION(HttpStatus.UNAUTHORIZED, "JWT 토큰 파싱에 실패했습니다."), NOT_EXIST_OAUTH_PROVIDER(HttpStatus.UNAUTHORIZED, "존재하지 않는 OAuth 제공자입니다."), + /* 403 - FORBIDDEN */ @@ -50,6 +52,7 @@ public enum ErrorCode { NOT_STUDY_ADMIN_EXCEPTION(HttpStatus.FORBIDDEN, "스터디 관리자가 아닙니다."), ALREADY_STUDY_JOIN_MEMBER_EXCEPTION(HttpStatus.FORBIDDEN, "스터디에 이미 가입한 사용자입니다."), NOT_EXIST_ACTIVITY_STATUS_EXCEPTION(HttpStatus.FORBIDDEN, "존재하지 않는 활동 상태입니다."), + ACTIVITY_MANAGEMENT_ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "활동 관리 권한이 없습니다."), /* 404 - NOT FOUND diff --git a/src/main/java/com/stumeet/server/file/application/port/in/FileDeleteUseCase.java b/src/main/java/com/stumeet/server/file/application/port/in/FileDeleteUseCase.java index 126f6253..e871c5de 100644 --- a/src/main/java/com/stumeet/server/file/application/port/in/FileDeleteUseCase.java +++ b/src/main/java/com/stumeet/server/file/application/port/in/FileDeleteUseCase.java @@ -5,4 +5,6 @@ public interface FileDeleteUseCase { void deleteStudyRelatedImage(Long studyId); void deleteUserRelatedImage(Long userId); + + void deleteActivityRelatedImage(Long studyId, Long activityId); } diff --git a/src/main/java/com/stumeet/server/file/application/service/FileDeleteService.java b/src/main/java/com/stumeet/server/file/application/service/FileDeleteService.java index 183544aa..d3e45374 100644 --- a/src/main/java/com/stumeet/server/file/application/service/FileDeleteService.java +++ b/src/main/java/com/stumeet/server/file/application/service/FileDeleteService.java @@ -10,8 +10,9 @@ @RequiredArgsConstructor public class FileDeleteService implements FileDeleteUseCase { - private final String STUDY_PREFIX = "study/%d/"; private final String USER_PREFIX = "user/%d/"; + private final String STUDY_PREFIX = "study/%d/"; + private final String ACTIVITY_PREFIX = "study/%d/activity/%d"; private final FileCommandPort fileCommandPort; @@ -25,4 +26,9 @@ public void deleteStudyRelatedImage(Long studyId) { public void deleteUserRelatedImage(Long userId) { fileCommandPort.deleteFolder(String.format(USER_PREFIX, userId)); } + + @Override + public void deleteActivityRelatedImage(Long studyId, Long activityId) { + fileCommandPort.deleteFolder(String.format(ACTIVITY_PREFIX, studyId, activityId)); + } } diff --git a/src/main/java/com/stumeet/server/study/application/port/in/ActivityImageDeleteUseCase.java b/src/main/java/com/stumeet/server/study/application/port/in/ActivityImageDeleteUseCase.java new file mode 100644 index 00000000..83813416 --- /dev/null +++ b/src/main/java/com/stumeet/server/study/application/port/in/ActivityImageDeleteUseCase.java @@ -0,0 +1,6 @@ +package com.stumeet.server.study.application.port.in; + +public interface ActivityImageDeleteUseCase { + + void deleteByActivityId(Long studyId, Long activityId); +} diff --git a/src/main/java/com/stumeet/server/study/application/port/in/ActivityParticipantDeleteUseCase.java b/src/main/java/com/stumeet/server/study/application/port/in/ActivityParticipantDeleteUseCase.java new file mode 100644 index 00000000..20d43cfd --- /dev/null +++ b/src/main/java/com/stumeet/server/study/application/port/in/ActivityParticipantDeleteUseCase.java @@ -0,0 +1,6 @@ +package com.stumeet.server.study.application.port.in; + +public interface ActivityParticipantDeleteUseCase { + + void deleteByActivityId(Long activityId); +} diff --git a/src/main/java/com/stumeet/server/study/application/service/ActivityImageDeleteService.java b/src/main/java/com/stumeet/server/study/application/service/ActivityImageDeleteService.java new file mode 100644 index 00000000..17f6e410 --- /dev/null +++ b/src/main/java/com/stumeet/server/study/application/service/ActivityImageDeleteService.java @@ -0,0 +1,25 @@ +package com.stumeet.server.study.application.service; + +import org.springframework.transaction.annotation.Transactional; + +import com.stumeet.server.activity.application.port.out.ActivityImageCommandPort; +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.file.application.port.in.FileDeleteUseCase; +import com.stumeet.server.study.application.port.in.ActivityImageDeleteUseCase; + +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +@Transactional +public class ActivityImageDeleteService implements ActivityImageDeleteUseCase { + + private final FileDeleteUseCase fileDeleteUseCase; + private final ActivityImageCommandPort activityImageCommandPort; + + @Override + public void deleteByActivityId(Long studyId, Long activityId) { + activityImageCommandPort.deleteAllByActivityId(activityId); + fileDeleteUseCase.deleteActivityRelatedImage(studyId, activityId); + } +} diff --git a/src/main/java/com/stumeet/server/study/application/service/ActivityParticipantDeleteService.java b/src/main/java/com/stumeet/server/study/application/service/ActivityParticipantDeleteService.java new file mode 100644 index 00000000..a54f4271 --- /dev/null +++ b/src/main/java/com/stumeet/server/study/application/service/ActivityParticipantDeleteService.java @@ -0,0 +1,22 @@ +package com.stumeet.server.study.application.service; + +import org.springframework.transaction.annotation.Transactional; + +import com.stumeet.server.activity.application.port.out.ActivityParticipantCommandPort; +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.study.application.port.in.ActivityParticipantDeleteUseCase; + +import lombok.RequiredArgsConstructor; + +@UseCase +@RequiredArgsConstructor +@Transactional +public class ActivityParticipantDeleteService implements ActivityParticipantDeleteUseCase { + + private final ActivityParticipantCommandPort activityParticipantCommandPort; + + @Override + public void deleteByActivityId(Long activityId) { + activityParticipantCommandPort.deleteByActivityId(activityId); + } +} diff --git a/src/test/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApiTest.java b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApiTest.java new file mode 100644 index 00000000..2dc7c8d1 --- /dev/null +++ b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityDeleteApiTest.java @@ -0,0 +1,105 @@ +package com.stumeet.server.activity.adapter.in; + +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.helper.WithMockMember; +import com.stumeet.server.stub.ActivityStub; +import com.stumeet.server.stub.StudyStub; +import com.stumeet.server.stub.TokenStub; +import com.stumeet.server.template.ApiTest; + +class ActivityDeleteApiTest extends ApiTest { + + @Nested + @DisplayName("스터디 활동 삭제 API") + class DeleteStudy { + + private final String PATH = "/api/v1/studies/{studyId}/activities/{activityId}"; + + @Test + @WithMockMember + @DisplayName("[성공] 관리자가 스터디 활동 삭제를 성공한다.") + void success_admin_delete_activity() throws Exception { + mockMvc.perform(delete(PATH, StudyStub.getStudyId(), ActivityStub.getActivityId()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andDo(document("delete-activity/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 상태"), + fieldWithPath("message").description("응답 메시지") + ))); + } + + @Test + @WithMockMember(id = 4L) + @DisplayName("[성공] 작성자가 스터디 활동 삭제를 성공한다.") + void success_author_delete_activity() throws Exception { + mockMvc.perform(delete(PATH, StudyStub.getStudyId(), ActivityStub.getActivityId()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()); + } + + @Test + @WithMockMember(id = 2L) + @DisplayName("[실패] 삭제를 시도하는 멤버가 일반 멤버인 경우 활동 삭제에 실패한다.") + void fail_to_delete_when_member_not_has_authority() throws Exception { + mockMvc.perform(delete(PATH, StudyStub.getStudyId(), ActivityStub.getActivityId()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isForbidden()) + .andDo(document("delete-activity/fail/forbidden", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 상태"), + fieldWithPath("message").description("응답 메시지") + ))); + } + + @Test + @WithMockMember(id = 3L) + @DisplayName("[실패] 스터디의 멤버가 아닌 경우 활동 삭제에 실패한다.") + void fail_to_delete_when_member_not_joined_study() throws Exception { + mockMvc.perform(delete(PATH, StudyStub.getStudyId(), ActivityStub.getActivityId()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isForbidden()) + .andDo(document("delete-activity/fail/not-joined-member", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰")), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 상태"), + fieldWithPath("message").description("응답 메시지") + ))); + } + } +} \ No newline at end of file diff --git a/src/test/resources/db/setup.sql b/src/test/resources/db/setup.sql index 540ac472..256cec16 100644 --- a/src/test/resources/db/setup.sql +++ b/src/test/resources/db/setup.sql @@ -45,67 +45,71 @@ INSERT INTO study_member (member_id, study_id, is_admin, is_sent_grape) VALUES ( -- [TABLE: activity] -- 스터디 1 (공지) 기본 활동 INSERT INTO activity(id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) -VALUES (1, 1, 1, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); +VALUES (1, 1, 4, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); -INSERT INTO activity_image (id, activity_id, image) VALUES (1, 1, 'https://example.com/images/image1.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (2, 1, 'https://example.com/images/image2.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (3, 1, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (1, 1, 1, 'NONE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (2, 1, 2, 'NONE'); +INSERT INTO activity_image (activity_id, image) VALUES (1, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (activity_id, image) VALUES (1, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (activity_id, image) VALUES (1, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (1, 1, 'NONE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (1, 2, 'NONE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (1, 4, 'NONE'); -- 스터디 1 기본 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (2, 1, 1, 'DEFAULT', 'title', 'content', false, '2024-04-08T00:00:00', '2050-05-01T00:00:00', null); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (3, 2, 1, 'NONE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (4, 2, 2, 'NONE'); +INSERT INTO activity_image (activity_id, image) VALUES (2, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (activity_id, image) VALUES (2, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (activity_id, image) VALUES (2, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (2, 1, 'NONE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (2, 2, 'NONE'); -- 스터디 1 모임 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (3, 1, 1, 'MEET', 'title', 'content', false, '2024-04-15T00:00:00', '2024-05-01T00:00:00', '성신여대 카페구월'); -INSERT INTO activity_image (id, activity_id, image) VALUES (7, 3, 'https://example.com/images/image1.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (8, 3, 'https://example.com/images/image2.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (9, 3, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (5, 3, 1, 'ATTENDANCE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (6, 3, 2, 'ACKNOWLEDGED_ABSENCE'); +INSERT INTO activity_image (activity_id, image) VALUES (3, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (activity_id, image) VALUES (3, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (activity_id, image) VALUES (3, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (3, 1, 'ATTENDANCE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (3, 2, 'ACKNOWLEDGED_ABSENCE'); -- 스터디 1 (공지) 모임 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (4, 1, 1, 'MEET', 'title', 'content', true, '2024-04-22T00:00:00', '2024-04-23T00:00:00', '성신여대 카페구월'); -INSERT INTO activity_image (id, activity_id, image) VALUES (10, 4, 'https://example.com/images/image1.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (11, 4, 'https://example.com/images/image2.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (12, 4, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (7, 4, 1, 'ATTENDANCE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (8, 4, 2, 'ACKNOWLEDGED_ABSENCE'); +INSERT INTO activity_image (activity_id, image) VALUES (4, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (activity_id, image) VALUES (4, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (activity_id, image) VALUES (4, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (4, 1, 'ATTENDANCE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (4, 2, 'ACKNOWLEDGED_ABSENCE'); -- 스터디 1 과제 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (5, 1, 1, 'ASSIGNMENT', 'title', 'content', false, '2024-04-01T00:00:00', '2024-04-08T00:00:00', null); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (9, 5, 1, 'UNSUBMITTED'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (10, 5, 2, 'PERFORMED'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (5, 1, 'UNSUBMITTED'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (5, 2, 'PERFORMED'); -- 스터디 1 (공지) 과제 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (6, 1, 1, 'ASSIGNMENT', 'title', 'content', true, '2024-04-08T00:00:00', '2024-04-15T00:00:00', null); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (11, 6, 1, 'UNSUBMITTED'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (12, 6, 2, 'PERFORMED'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (6, 1, 'UNSUBMITTED'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (6, 2, 'PERFORMED'); -- 스터디 1 미참여 과제 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (7, 1, 1, 'ASSIGNMENT', 'title', 'content', false, '2024-04-08T00:00:00', '2024-04-15T00:00:00', null); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (13, 6, 2, 'PERFORMED'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (6, 2, 'PERFORMED'); -- 스터디 2 (공지) 기본 활동 INSERT INTO activity(id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (8, 2, 1, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2024-04-08T00:00:00', '성신여대 카페구월'); -INSERT INTO activity_image (id, activity_id, image) VALUES (13, 8, 'https://example.com/images/image1.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (14, 8, 'https://example.com/images/image2.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (15, 8, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (14, 8, 1, 'NONE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (15, 8, 2, 'NONE'); \ No newline at end of file +INSERT INTO activity_image (activity_id, image) VALUES (8, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (activity_id, image) VALUES (8, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (activity_id, image) VALUES (8, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (8, 1, 'NONE'); +INSERT INTO activity_participant (activity_id, member_id, status) VALUES (8, 2, 'NONE'); \ No newline at end of file