From 402d36ca33a6cf3382137b41f7af50e411b38815 Mon Sep 17 00:00:00 2001 From: zxcv9203 <41960243+zxcv9203@users.noreply.github.com> Date: Wed, 15 May 2024 20:46:37 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[STMT-179]=20=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=EB=94=94=20=ED=99=9C=EB=8F=99=20=EC=83=9D=EC=84=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: [STMT-179] 스터디 활동 기능 구현 * :truck: [STMT-179] 잘못된 멤버 ID 스텁 객체 위치 수정 * :recycle: [STMT-179] 위치 정보는 모임에서만 받으므로 NULLABLE하게 변경 * :white_check_mark: [STMT-179] 스터디 활동 생성 API 테스트 코드 작성 및 문서화 진행 * :recycle: [STMT-179] 활동 생성시 응답으로 공통 응답 객체 추가 * :recycle: [STMT-179] 활동 생성 멤버 ID에 대한 변수명 수정 * :recycle: [STMT-179] 활동 도메인을 생성하기 위한 객체의 이름을 ~Source로 변경 및 패키지 위치 변경 * :recycle: [STMT-179] 활동 생성시 스터디 생성에 대한 검증 추가 * :recycle: [STMT-179] 활동 생성시 Enum이 아닌 String으로 반환하도록 변경 --- src/docs/asciidoc/index.adoc | 38 ++- .../adapter/in/ActivityCreateApi.java | 37 +++ .../ActivityImagePersistenceMapper.java | 12 +- .../ActivityParticipantPersistenceMapper.java | 8 + .../out/mapper/ActivityPersistenceMapper.java | 7 +- .../out/model/ActivityImageJpaEntity.java | 4 +- .../adapter/out/model/ActivityJpaEntity.java | 2 +- .../ActivityImagePersistenceAdapter.java | 25 ++ ...ActivityParticipantPersistenceAdapter.java | 26 ++ .../ActivityPersistenceAdapter.java | 23 ++ .../JpaActivityImageRepository.java | 7 + .../JpaActivityParticipantRepository.java | 7 + .../persistence/JpaActivityRepository.java | 7 + .../port/in/ActivityCreateUseCase.java | 7 + .../in/command/ActivityCreateCommand.java | 44 ++++ .../in/mapper/ActivityImageUseCaseMapper.java | 24 ++ .../ActivityParticipantUseCaseMapper.java | 29 +++ .../port/in/mapper/ActivityUseCaseMapper.java | 31 +++ .../port/out/ActivityCreatePort.java | 8 + .../port/out/ActivityImageCreatePort.java | 9 + .../out/ActivityParticipantCreatePort.java | 9 + .../service/ActivityCreateService.java | 54 ++++ .../model/ActivityCreateSource.java} | 8 +- .../NotExistsActivityCategoryException.java | 14 ++ .../domain/model/ActivityCategory.java | 25 +- .../server/common/response/ErrorCode.java | 1 + .../server/common/response/SuccessCode.java | 3 +- .../exception/StudyNotExistsException.java | 5 +- .../exception/NotStudyAdminException.java | 2 +- .../migration/V1.5__create_activity_table.sql | 2 +- .../adapter/in/ActivityCreateApiTest.java | 235 ++++++++++++++++++ .../service/ActivityCreateServiceTest.java | 121 +++++++++ .../com/stumeet/server/stub/ActivityStub.java | 96 +++++++ .../com/stumeet/server/stub/MemberStub.java | 2 +- .../in/web/StudyMemberLeaveApiTest.java | 2 +- ...paStudyMemberRepositoryCustomImplTest.java | 2 +- 36 files changed, 906 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/stumeet/server/activity/adapter/in/ActivityCreateApi.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java create mode 100644 src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/in/ActivityCreateUseCase.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityCreateCommand.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityImageUseCaseMapper.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityParticipantUseCaseMapper.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityUseCaseMapper.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/out/ActivityCreatePort.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCreatePort.java create mode 100644 src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCreatePort.java create mode 100644 src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java rename src/main/java/com/stumeet/server/activity/application/{port/in/command/ActivityConstructCommand.java => service/model/ActivityCreateSource.java} (70%) create mode 100644 src/main/java/com/stumeet/server/activity/domain/exception/NotExistsActivityCategoryException.java create mode 100644 src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java create mode 100644 src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java create mode 100644 src/test/java/com/stumeet/server/stub/ActivityStub.java diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index a4d64896..1707d0e3 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -421,4 +421,40 @@ include::{snippets}/presigned-url-generate/fail/invalid-file-name/response-field .파일의 확장자가 유효하지 않은 경우 include::{snippets}/presigned-url-generate/fail/invalid-file-extension/response-body.adoc[] -include::{snippets}/presigned-url-generate/fail/invalid-file-extension/response-fields.adoc[] \ No newline at end of file +include::{snippets}/presigned-url-generate/fail/invalid-file-extension/response-fields.adoc[] + +== 스터디 활동 관리 + +=== 스터디 활동 생성 + +스터디 활동을 생성하는 API입니다. + +==== POST /api/v1/studies/{studyId}/activities + +===== 요청 +include::{snippets}/create-activity/success/http-request.adoc[] +include::{snippets}/create-activity/success/path-parameters.adoc[] +include::{snippets}/create-activity/success/request-headers.adoc[] +include::{snippets}/create-activity/success/request-fields.adoc[] + +===== 응답 성공 (201) +.응답 없음 +include::{snippets}/create-activity/success/response-body.adoc[] + +===== 응답 실패 (400) +.활동 생성 요청 값이 유효하지 않은 경우 +include::{snippets}/create-activity/fail/invalid-request/response-body.adoc[] +include::{snippets}/create-activity/fail/invalid-request/response-fields.adoc[] + +.존재하지 않는 활동 카테고리로 요청한 경우 +include::{snippets}/create-activity/fail/not-exists-category/response-body.adoc[] +include::{snippets}/create-activity/fail/not-exists-category/response-fields.adoc[] +===== 응답 실패 (403) +.스터디의 관리자가 아닌 경우 +include::{snippets}/create-activity/fail/not-admin/response-body.adoc[] +include::{snippets}/create-activity/fail/not-admin/response-fields.adoc[] + +====== 응답 실패 (404) +.존재하지 않는 스터디 ID를 요청한 경우 +include::{snippets}/create-activity/fail/not-exists-study/response-body.adoc[] +include::{snippets}/create-activity/fail/not-exists-study/response-fields.adoc[] \ No newline at end of file diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/ActivityCreateApi.java b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityCreateApi.java new file mode 100644 index 00000000..8a679e27 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityCreateApi.java @@ -0,0 +1,37 @@ +package com.stumeet.server.activity.adapter.in; + +import com.stumeet.server.activity.application.port.in.ActivityCreateUseCase; +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +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 jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@WebAdapter +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class ActivityCreateApi { + + private final ActivityCreateUseCase activityCreateUseCase; + + @PostMapping("/studies/{studyId}/activities") + public ResponseEntity> create( + @PathVariable Long studyId, + @AuthenticationPrincipal LoginMember loginMember, + @RequestBody @Valid ActivityCreateCommand command + ) { + activityCreateUseCase.create(studyId, command, loginMember.getMember().getId()); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.ACTIVITY_CREATE_SUCCESS)); + } +} diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityImagePersistenceMapper.java b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityImagePersistenceMapper.java index 3923b57f..146bc8c6 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityImagePersistenceMapper.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityImagePersistenceMapper.java @@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; + @Component @RequiredArgsConstructor public class ActivityImagePersistenceMapper { @@ -15,7 +17,7 @@ public ActivityImage toDomain(ActivityImageJpaEntity entity) { return ActivityImage.builder() .id(entity.getId()) .activity(activityPersistenceMapper.toDomain(entity.getActivity())) - .url(entity.getImage()) + .url(entity.getUrl()) .build(); } @@ -23,7 +25,13 @@ public ActivityImageJpaEntity toEntity(ActivityImage domain) { return ActivityImageJpaEntity.builder() .id(domain.getId()) .activity(activityPersistenceMapper.toEntity(domain.getActivity())) - .image(domain.getUrl()) + .url(domain.getUrl()) .build(); } + + public List toEntities(List images) { + return images.stream() + .map(this::toEntity) + .toList(); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityParticipantPersistenceMapper.java b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityParticipantPersistenceMapper.java index c5abb8e7..f5afb891 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityParticipantPersistenceMapper.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityParticipantPersistenceMapper.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; + @Component @RequiredArgsConstructor public class ActivityParticipantPersistenceMapper { @@ -37,4 +39,10 @@ public ActivityParticipantJpaEntity toEntity(ActivityParticipant domain) { .status(domain.getStatus()) .build(); } + + public List toEntities(List participants) { + return participants.stream() + .map(this::toEntity) + .toList(); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityPersistenceMapper.java b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityPersistenceMapper.java index 6a8de12f..314c1ce4 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityPersistenceMapper.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityPersistenceMapper.java @@ -3,9 +3,8 @@ import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; import com.stumeet.server.activity.adapter.out.model.ActivityLinkedStudyJpaEntity; import com.stumeet.server.activity.adapter.out.model.ActivityMemberJpaEntity; -import com.stumeet.server.activity.application.port.in.command.ActivityConstructCommand; +import com.stumeet.server.activity.application.service.model.ActivityCreateSource; import com.stumeet.server.activity.domain.model.Activity; -import com.stumeet.server.activity.domain.model.ActivityCategory; import com.stumeet.server.activity.domain.model.Meet; import org.springframework.stereotype.Component; @@ -13,9 +12,9 @@ public class ActivityPersistenceMapper { public Activity toDomain(ActivityJpaEntity entity) { - ActivityConstructCommand request = ActivityConstructCommand.builder() + ActivityCreateSource request = ActivityCreateSource.builder() .id(entity.getId()) - .author(ActivityConstructCommand.ActivityMemberConstructCommand.builder() + .author(ActivityCreateSource.ActivityMemberCreateSource.builder() .id(entity.getAuthor().getId()) .name(entity.getAuthor().getName()) .image(entity.getAuthor().getImage()) diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityImageJpaEntity.java b/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityImageJpaEntity.java index 6975e1f8..5a79cbdb 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityImageJpaEntity.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityImageJpaEntity.java @@ -23,7 +23,7 @@ public class ActivityImageJpaEntity extends BaseTimeEntity { @Comment("활동") private ActivityJpaEntity activity; - @Column(name = "url", nullable = false, length = 500) + @Column(name = "image", nullable = false, length = 500) @Comment("이미지 url") - private String image; + private String url; } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityJpaEntity.java b/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityJpaEntity.java index 80fd6bb6..87929887 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityJpaEntity.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/model/ActivityJpaEntity.java @@ -26,7 +26,7 @@ public class ActivityJpaEntity extends BaseTimeEntity { private ActivityLinkedStudyJpaEntity study; @OneToOne - @JoinColumn(name = "member_id") + @JoinColumn(name = "author_id") @Comment("활동을 생성한 멤버") private ActivityMemberJpaEntity author; 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 new file mode 100644 index 00000000..4d48244f --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityImagePersistenceAdapter.java @@ -0,0 +1,25 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +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.ActivityImageCreatePort; +import com.stumeet.server.activity.domain.model.ActivityImage; +import com.stumeet.server.common.annotation.PersistenceAdapter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@PersistenceAdapter +@RequiredArgsConstructor +public class ActivityImagePersistenceAdapter implements ActivityImageCreatePort { + + private final JpaActivityImageRepository jpaActivityImageRepository; + private final ActivityImagePersistenceMapper activityImagePersistenceMapper; + + @Override + public void create(List images) { + List entities = activityImagePersistenceMapper.toEntities(images); + + jpaActivityImageRepository.saveAll(entities); + } +} 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 new file mode 100644 index 00000000..1ee07919 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityParticipantPersistenceAdapter.java @@ -0,0 +1,26 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +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.ActivityParticipantCreatePort; +import com.stumeet.server.activity.domain.model.ActivityParticipant; +import com.stumeet.server.common.annotation.PersistenceAdapter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@PersistenceAdapter +@RequiredArgsConstructor +public class ActivityParticipantPersistenceAdapter implements ActivityParticipantCreatePort { + + private final JpaActivityParticipantRepository jpaActivityParticipantRepository; + private final ActivityParticipantPersistenceMapper activityParticipantPersistenceMapper; + + + @Override + public void create(List participants) { + List entities = activityParticipantPersistenceMapper.toEntities(participants); + + jpaActivityParticipantRepository.saveAll(entities); + } +} 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 new file mode 100644 index 00000000..11d898b7 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java @@ -0,0 +1,23 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +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.ActivityCreatePort; +import com.stumeet.server.activity.domain.model.Activity; +import com.stumeet.server.common.annotation.PersistenceAdapter; +import lombok.RequiredArgsConstructor; + +@PersistenceAdapter +@RequiredArgsConstructor +public class ActivityPersistenceAdapter implements ActivityCreatePort { + + private final JpaActivityRepository jpaActivityRepository; + private final ActivityPersistenceMapper activityPersistenceMapper; + + @Override + public Activity create(Activity activity) { + ActivityJpaEntity entity = activityPersistenceMapper.toEntity(activity); + + return activityPersistenceMapper.toDomain(jpaActivityRepository.save(entity)); + } +} 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 new file mode 100644 index 00000000..3de95133 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityImageRepository.java @@ -0,0 +1,7 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +import com.stumeet.server.activity.adapter.out.model.ActivityImageJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaActivityImageRepository extends JpaRepository { +} 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 new file mode 100644 index 00000000..43944b5c --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityParticipantRepository.java @@ -0,0 +1,7 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +import com.stumeet.server.activity.adapter.out.model.ActivityParticipantJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaActivityParticipantRepository extends JpaRepository { +} 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 new file mode 100644 index 00000000..cd87983d --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepository.java @@ -0,0 +1,7 @@ +package com.stumeet.server.activity.adapter.out.persistence; + +import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaActivityRepository extends JpaRepository { +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityCreateUseCase.java b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityCreateUseCase.java new file mode 100644 index 00000000..ea1a8835 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityCreateUseCase.java @@ -0,0 +1,7 @@ +package com.stumeet.server.activity.application.port.in; + +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; + +public interface ActivityCreateUseCase { + void create(Long studyId, ActivityCreateCommand command, Long memberId); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityCreateCommand.java b/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityCreateCommand.java new file mode 100644 index 00000000..9564083f --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityCreateCommand.java @@ -0,0 +1,44 @@ +package com.stumeet.server.activity.application.port.in.command; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record ActivityCreateCommand( + @NotBlank(message = "활동 카테고리를 입력해주세요") + String category, + + @NotBlank(message = "활동 제목을 입력해주세요") + @Size(max = 100, message = "활동 제목은 100자 이하여야 합니다") + String title, + + @NotBlank(message = "활동 내용을 입력해주세요") + @Size(max = 500, message = "활동 내용은 500자 이하여야 합니다") + String content, + + @NotNull(message = "이미지 리스트를 전달해주세요") + @Size(max = 5, message = "이미지는 5개 이하로 등록할 수 있습니다") + List images, + + boolean isNotice, + + @DateTimeFormat(pattern = "yyyy-MM-dd''HH:mm:ss") + LocalDateTime startDate, + + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime endDate, + + String location, + + @NotNull(message = "참여 멤버 리스트를 전달해주세요") + @Size(min = 1, message = "참여 멤버는 1명 이상이어야 합니다") + List participants + +) { +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityImageUseCaseMapper.java b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityImageUseCaseMapper.java new file mode 100644 index 00000000..882eee67 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityImageUseCaseMapper.java @@ -0,0 +1,24 @@ +package com.stumeet.server.activity.application.port.in.mapper; + +import com.stumeet.server.activity.domain.model.Activity; +import com.stumeet.server.activity.domain.model.ActivityImage; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ActivityImageUseCaseMapper { + + public ActivityImage toDomain(String image, Activity activity) { + return ActivityImage.builder() + .activity(activity) + .url(image) + .build(); + + } + public List toDomains(List images, Activity activity) { + return images.stream() + .map(image -> toDomain(image, activity)) + .toList(); + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityParticipantUseCaseMapper.java b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityParticipantUseCaseMapper.java new file mode 100644 index 00000000..ec59e8f3 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityParticipantUseCaseMapper.java @@ -0,0 +1,29 @@ +package com.stumeet.server.activity.application.port.in.mapper; + +import com.stumeet.server.activity.domain.model.Activity; +import com.stumeet.server.activity.domain.model.ActivityMember; +import com.stumeet.server.activity.domain.model.ActivityParticipant; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ActivityParticipantUseCaseMapper { + + public ActivityParticipant toDomain(Long participant, Activity activity) { + return ActivityParticipant.builder() + .activity(activity) + .member(ActivityMember.builder() + .id(participant) + .build()) + .activity(activity) + .status(activity.getCategory().getDefaultStatus()) + .build(); + } + + public List toDomains(List participants, Activity activity) { + return participants.stream() + .map(participant -> toDomain(participant, activity)) + .toList(); + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityUseCaseMapper.java b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityUseCaseMapper.java new file mode 100644 index 00000000..97ab8add --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/mapper/ActivityUseCaseMapper.java @@ -0,0 +1,31 @@ +package com.stumeet.server.activity.application.port.in.mapper; + + +import com.stumeet.server.activity.application.service.model.ActivityCreateSource; +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.domain.model.ActivityCategory; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class ActivityUseCaseMapper { + + public ActivityCreateSource toSource(Long studyId, ActivityCreateCommand command, Long id) { + return ActivityCreateSource.builder() + .id(null) + .studyId(studyId) + .author(ActivityCreateSource.ActivityMemberCreateSource.builder() + .id(id) + .build()) + .category(ActivityCategory.getByName(command.category())) + .title(command.title()) + .content(command.content()) + .isNotice(command.isNotice()) + .startDate(command.startDate()) + .endDate(command.endDate()) + .createdAt(LocalDateTime.now()) + .location(command.location()) + .build(); + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityCreatePort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityCreatePort.java new file mode 100644 index 00000000..95768621 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityCreatePort.java @@ -0,0 +1,8 @@ +package com.stumeet.server.activity.application.port.out; + +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.domain.model.Activity; + +public interface ActivityCreatePort { + Activity create(Activity activity); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCreatePort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCreatePort.java new file mode 100644 index 00000000..3e3208d0 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityImageCreatePort.java @@ -0,0 +1,9 @@ +package com.stumeet.server.activity.application.port.out; + +import com.stumeet.server.activity.domain.model.ActivityImage; + +import java.util.List; + +public interface ActivityImageCreatePort { + void create(List images); +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCreatePort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCreatePort.java new file mode 100644 index 00000000..65bb90bf --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityParticipantCreatePort.java @@ -0,0 +1,9 @@ +package com.stumeet.server.activity.application.port.out; + +import com.stumeet.server.activity.domain.model.ActivityParticipant; + +import java.util.List; + +public interface ActivityParticipantCreatePort { + void create(List participants); +} diff --git a/src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java b/src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java new file mode 100644 index 00000000..d0eb903e --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java @@ -0,0 +1,54 @@ +package com.stumeet.server.activity.application.service; + +import com.stumeet.server.activity.application.port.in.ActivityCreateUseCase; +import com.stumeet.server.activity.application.service.model.ActivityCreateSource; +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.application.port.in.mapper.ActivityImageUseCaseMapper; +import com.stumeet.server.activity.application.port.in.mapper.ActivityParticipantUseCaseMapper; +import com.stumeet.server.activity.application.port.in.mapper.ActivityUseCaseMapper; +import com.stumeet.server.activity.application.port.out.ActivityCreatePort; +import com.stumeet.server.activity.application.port.out.ActivityImageCreatePort; +import com.stumeet.server.activity.application.port.out.ActivityParticipantCreatePort; +import com.stumeet.server.activity.domain.model.Activity; +import com.stumeet.server.activity.domain.model.ActivityImage; +import com.stumeet.server.activity.domain.model.ActivityParticipant; +import com.stumeet.server.common.annotation.UseCase; +import com.stumeet.server.study.application.port.in.StudyValidationUseCase; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@UseCase +@Transactional +@RequiredArgsConstructor +public class ActivityCreateService implements ActivityCreateUseCase { + + private final ActivityCreatePort activityCreatePort; + private final ActivityImageCreatePort activityImageCreatePort; + private final ActivityParticipantCreatePort activityParticipantPort; + + private final StudyMemberValidationUseCase studyMemberValidationUseCase; + private final StudyValidationUseCase studyValidationUseCase; + + private final ActivityUseCaseMapper activityUseCaseMapper; + private final ActivityImageUseCaseMapper activityImageUseCaseMapper; + private final ActivityParticipantUseCaseMapper activityMemberUseCaseMapper; + + @Override + public void create(Long studyId, ActivityCreateCommand command, Long memberId) { + studyValidationUseCase.checkById(studyId); + studyMemberValidationUseCase.checkAdmin(studyId, memberId); + + ActivityCreateSource activitySource = activityUseCaseMapper.toSource(studyId, command, memberId); + Activity activity = activitySource.category().create(activitySource); + Activity createdActivity = activityCreatePort.create(activity); + + List images = activityImageUseCaseMapper.toDomains(command.images(), createdActivity); + activityImageCreatePort.create(images); + + List participants = activityMemberUseCaseMapper.toDomains(command.participants(), createdActivity); + activityParticipantPort.create(participants); + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityConstructCommand.java b/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java similarity index 70% rename from src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityConstructCommand.java rename to src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java index ead0f171..15ae2a35 100644 --- a/src/main/java/com/stumeet/server/activity/application/port/in/command/ActivityConstructCommand.java +++ b/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java @@ -1,4 +1,4 @@ -package com.stumeet.server.activity.application.port.in.command; +package com.stumeet.server.activity.application.service.model; import com.stumeet.server.activity.domain.model.ActivityCategory; import lombok.Builder; @@ -6,10 +6,10 @@ import java.time.LocalDateTime; @Builder -public record ActivityConstructCommand( +public record ActivityCreateSource( Long id, Long studyId, - ActivityMemberConstructCommand author, + ActivityMemberCreateSource author, ActivityCategory category, String title, String content, @@ -20,7 +20,7 @@ public record ActivityConstructCommand( String location ) { @Builder - public record ActivityMemberConstructCommand( + public record ActivityMemberCreateSource( Long id, String name, String image diff --git a/src/main/java/com/stumeet/server/activity/domain/exception/NotExistsActivityCategoryException.java b/src/main/java/com/stumeet/server/activity/domain/exception/NotExistsActivityCategoryException.java new file mode 100644 index 00000000..01cee48b --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/domain/exception/NotExistsActivityCategoryException.java @@ -0,0 +1,14 @@ +package com.stumeet.server.activity.domain.exception; + +import com.stumeet.server.common.exception.model.InvalidStateException; +import com.stumeet.server.common.response.ErrorCode; + +import java.text.MessageFormat; + +public class NotExistsActivityCategoryException extends InvalidStateException { + private static final String MESSAGE = "존재하지 않는 활동 카테고리입니다. 입력받은 카테고리 : {0}"; + + public NotExistsActivityCategoryException(String category) { + super(MessageFormat.format(MESSAGE, category), ErrorCode.INVALID_ACTIVITY_CATEGORY_EXCEPTION); + } +} diff --git a/src/main/java/com/stumeet/server/activity/domain/model/ActivityCategory.java b/src/main/java/com/stumeet/server/activity/domain/model/ActivityCategory.java index b0069aee..d820bfb6 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/ActivityCategory.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/ActivityCategory.java @@ -1,7 +1,7 @@ package com.stumeet.server.activity.domain.model; -import com.stumeet.server.activity.application.port.in.command.ActivityConstructCommand; -import com.stumeet.server.activity.domain.exception.NotExistsActivityStatusException; +import com.stumeet.server.activity.application.service.model.ActivityCreateSource; +import com.stumeet.server.activity.domain.exception.NotExistsActivityCategoryException; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -10,9 +10,9 @@ @RequiredArgsConstructor @Getter public enum ActivityCategory { - DEFAULT { + DEFAULT(DefaultStatus.NONE) { @Override - public Activity create(ActivityConstructCommand command) { + public Activity create(ActivityCreateSource command) { return Default.builder() .id(command.id()) .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) @@ -33,9 +33,9 @@ public Activity create(ActivityConstructCommand command) { } }, - MEET { + MEET(MeetStatus.MEET_NOT_STARTED) { @Override - public Activity create(ActivityConstructCommand command) { + public Activity create(ActivityCreateSource command) { return Meet.builder() .id(command.id()) .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) @@ -56,9 +56,9 @@ public Activity create(ActivityConstructCommand command) { .build(); } }, - ASSIGNMENT { + ASSIGNMENT(AssignmentStatus.ASSIGNMENT_NOT_STARTED) { @Override - public Activity create(ActivityConstructCommand command) { + public Activity create(ActivityCreateSource command) { return Assignment.builder() .id(command.id()) .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) @@ -79,6 +79,13 @@ public Activity create(ActivityConstructCommand command) { } }; - public abstract Activity create(ActivityConstructCommand command); + private final ActivityStatus defaultStatus; + public static ActivityCategory getByName(String category) { + return Arrays.stream(ActivityCategory.values()) + .filter(c -> c.name().equalsIgnoreCase(category)) + .findAny() + .orElseThrow(() -> new NotExistsActivityCategoryException(category)); + } + public abstract Activity create(ActivityCreateSource command); } 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 7abef8d2..71a43009 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -28,6 +28,7 @@ public enum ErrorCode { INVALID_FILE_NAME_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 이름입니다."), INVALID_FILE_CONTENT_TYPE_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 컨텐트 타입 입니다."), INVALID_FILE_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 확장자입니다."), + INVALID_ACTIVITY_CATEGORY_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 활동 카테고리입니다."), /* 401 - UNAUTHORIZED */ diff --git a/src/main/java/com/stumeet/server/common/response/SuccessCode.java b/src/main/java/com/stumeet/server/common/response/SuccessCode.java index a09268e6..767dbd5d 100644 --- a/src/main/java/com/stumeet/server/common/response/SuccessCode.java +++ b/src/main/java/com/stumeet/server/common/response/SuccessCode.java @@ -26,7 +26,8 @@ public enum SuccessCode { SIGN_UP_SUCCESS(HttpStatus.CREATED, "회원가입에 성공했습니다."), FILE_UPLOAD_SUCCESS(HttpStatus.CREATED, "파일 업로드에 성공했습니다."), STUDY_CREATE_SUCCESS(HttpStatus.CREATED, "스터디 그룹 생성에 성공했습니다."), - STUDY_JOIN_SUCCESS(HttpStatus.CREATED, "스터디 가입에 성공했습니다."); + STUDY_JOIN_SUCCESS(HttpStatus.CREATED, "스터디 가입에 성공했습니다."), + ACTIVITY_CREATE_SUCCESS(HttpStatus.CREATED, "활동 생성에 성공했습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/stumeet/server/study/domain/exception/StudyNotExistsException.java b/src/main/java/com/stumeet/server/study/domain/exception/StudyNotExistsException.java index f9b122e1..fa362d48 100644 --- a/src/main/java/com/stumeet/server/study/domain/exception/StudyNotExistsException.java +++ b/src/main/java/com/stumeet/server/study/domain/exception/StudyNotExistsException.java @@ -3,8 +3,11 @@ import com.stumeet.server.common.exception.model.NotExistsException; import com.stumeet.server.common.response.ErrorCode; +import java.text.MessageFormat; + public class StudyNotExistsException extends NotExistsException { + public static final String MESSAGE = "존재하지 않는 스터디입니다. 전달받은 id : {0}"; public StudyNotExistsException(Long id) { - super("존재하지 않는 스터디입니다. 전달받은 id : " + id, ErrorCode.STUDY_NOT_FOUND); + super(MessageFormat.format(MESSAGE, id), ErrorCode.STUDY_NOT_FOUND); } } diff --git a/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java b/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java index 7955cda7..b08e14aa 100644 --- a/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java +++ b/src/main/java/com/stumeet/server/studymember/domain/exception/NotStudyAdminException.java @@ -7,7 +7,7 @@ public class NotStudyAdminException extends InvalidStateException { - private static final String MESSAGE = "스터디 관리자가 아닙니다. 전달받은 studyId={0}, memberId={1}"; + public static final String MESSAGE = "스터디 관리자가 아닙니다. 전달받은 studyId={0}, memberId={1}"; public NotStudyAdminException(Long studyId, Long memberId) { super(MessageFormat.format(MESSAGE, studyId, memberId), ErrorCode.NOT_STUDY_ADMIN_EXCEPTION); } diff --git a/src/main/resources/db/migration/V1.5__create_activity_table.sql b/src/main/resources/db/migration/V1.5__create_activity_table.sql index 74ba6b72..5c9f0e27 100644 --- a/src/main/resources/db/migration/V1.5__create_activity_table.sql +++ b/src/main/resources/db/migration/V1.5__create_activity_table.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS `activity` `start_date` DATETIME NOT NULL COMMENT '활동 시작일', `end_date` DATETIME NOT NULL COMMENT '활동 종료일', - `location` VARCHAR(255) NOT NULL COMMENT '활동 장소', + `location` VARCHAR(255) COMMENT '활동 장소', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시간', diff --git a/src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java new file mode 100644 index 00000000..696fc976 --- /dev/null +++ b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java @@ -0,0 +1,235 @@ +package com.stumeet.server.activity.adapter.in; + +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.domain.exception.NotExistsActivityCategoryException; +import com.stumeet.server.common.auth.model.AuthenticationHeader; +import com.stumeet.server.common.response.ErrorCode; +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; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.contract.spec.internal.HttpStatus; +import org.springframework.http.MediaType; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ActivityCreateApiTest extends ApiTest { + + @Nested + @DisplayName("활동 생성") + class Create { + + private static final String PATH = "/api/v1/studies/{studyId}/activities"; + + @Test + @WithMockMember + @DisplayName("[성공] 활동 생성에 성공한다.") + void successTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isCreated()) + .andDo(document("create-activity/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + requestFields( + fieldWithPath("category").description("활동 카테고리"), + fieldWithPath("title").description("활동 제목"), + fieldWithPath("content").description("활동 내용"), + fieldWithPath("images[]").description("활동 이미지 URL 리스트"), + fieldWithPath("isNotice").description("공지 여부"), + fieldWithPath("startDate").description("활동 시작 일시"), + fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 활동 생성 요청 시 필수값이 누락된 경우 예외가 발생한다.") + void invalidRequestTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getInvalidCreateActivity(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isBadRequest()) + .andDo(document("create-activity/fail/invalid-request", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + requestFields( + fieldWithPath("category").description("활동 카테고리"), + fieldWithPath("title").description("활동 제목"), + fieldWithPath("content").description("활동 내용"), + fieldWithPath("images[]").description("활동 이미지 URL 리스트"), + fieldWithPath("isNotice").description("공지 여부"), + fieldWithPath("startDate").description("활동 시작 일시"), + fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data[].message").description("유효성 검사 실패 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 존재하지 않는 스터디로 활동 생성 요청 시 예외가 발생한다.") + void notExistsStudyTest() throws Exception { + Long studyId = StudyStub.getInvalidStudyId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isNotFound()) + .andDo(document("create-activity/fail/not-exists-study", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + requestFields( + fieldWithPath("category").description("활동 카테고리"), + fieldWithPath("title").description("활동 제목"), + fieldWithPath("content").description("활동 내용"), + fieldWithPath("images[]").description("활동 이미지 URL 리스트"), + fieldWithPath("isNotice").description("공지 여부"), + fieldWithPath("startDate").description("활동 시작 일시"), + fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 존재하지 않는 활동 카테고리로 생성 요청을 하는 경우 예외가 발생한다.") + void notExistsActivityCategoryTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getInvalidCategoryCreateActivity(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(HttpStatus.BAD_REQUEST)) + .andExpect(jsonPath("$.message").value(ErrorCode.INVALID_ACTIVITY_CATEGORY_EXCEPTION.getMessage())) + .andDo(document("create-activity/fail/not-exists-category", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + requestFields( + fieldWithPath("category").description("활동 카테고리"), + fieldWithPath("title").description("활동 제목"), + fieldWithPath("content").description("활동 내용"), + fieldWithPath("images[]").description("활동 이미지 URL 리스트"), + fieldWithPath("isNotice").description("공지 여부"), + fieldWithPath("startDate").description("활동 시작 일시"), + fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember(id = 2L) + @DisplayName("[실패] 생성 요청을 한 사용자가 스터디의 관리자가 아닌 경우 예외가 발생한다.") + void notAdminTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + + mockMvc.perform(post(PATH, studyId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .content(toJson(request))) + .andExpect(status().isForbidden()) + .andDo(document("create-activity/fail/not-admin", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("studyId").description("스터디 ID") + ), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") + ), + requestFields( + fieldWithPath("category").description("활동 카테고리"), + fieldWithPath("title").description("활동 제목"), + fieldWithPath("content").description("활동 내용"), + fieldWithPath("images[]").description("활동 이미지 URL 리스트"), + fieldWithPath("isNotice").description("공지 여부"), + fieldWithPath("startDate").description("활동 시작 일시"), + fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java b/src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java new file mode 100644 index 00000000..ec547bb8 --- /dev/null +++ b/src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java @@ -0,0 +1,121 @@ +package com.stumeet.server.activity.application.service; + +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.application.port.in.mapper.ActivityImageUseCaseMapper; +import com.stumeet.server.activity.application.port.in.mapper.ActivityParticipantUseCaseMapper; +import com.stumeet.server.activity.application.port.in.mapper.ActivityUseCaseMapper; +import com.stumeet.server.activity.application.port.out.ActivityCreatePort; +import com.stumeet.server.activity.application.port.out.ActivityImageCreatePort; +import com.stumeet.server.activity.application.port.out.ActivityParticipantCreatePort; +import com.stumeet.server.activity.domain.exception.NotExistsActivityCategoryException; +import com.stumeet.server.activity.domain.model.Activity; +import com.stumeet.server.stub.ActivityStub; +import com.stumeet.server.stub.MemberStub; +import com.stumeet.server.stub.StudyStub; +import com.stumeet.server.study.application.port.in.StudyValidationUseCase; +import com.stumeet.server.study.domain.exception.StudyNotExistsException; +import com.stumeet.server.studymember.application.port.in.StudyMemberValidationUseCase; +import com.stumeet.server.studymember.domain.exception.NotStudyAdminException; +import com.stumeet.server.template.UnitTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.text.MessageFormat; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.*; + +class ActivityCreateServiceTest extends UnitTest { + + @InjectMocks + private ActivityCreateService activityCreateService; + + @Mock + private ActivityCreatePort activityCreatePort; + + @Mock + private ActivityImageCreatePort activityImageCreatePort; + + @Mock + private ActivityParticipantCreatePort activityParticipantPort; + + @Mock + private StudyMemberValidationUseCase studyMemberValidationUseCase; + + @Mock + private ActivityUseCaseMapper activityUseCaseMapper; + + + @Mock + private ActivityImageUseCaseMapper activityImageUseCaseMapper; + + @Mock + private ActivityParticipantUseCaseMapper activityParticipantUseCaseMapper; + + @Mock + private StudyValidationUseCase studyValidationUseCase; + + @Nested + @DisplayName("활동 생성") + class Create { + + @Test + @DisplayName("[성공] 활동 생성에 성공한다.") + void successTest() { + Long studyId = StudyStub.getStudyId(); + Long memberId = MemberStub.getMemberId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + Activity activity = ActivityStub.getDefaultActivity(); + + given(activityUseCaseMapper.toSource(any(), any(), any())) + .willReturn(ActivityStub.getDefaultConstructCommand()); + given(activityCreatePort.create(any())) + .willReturn(ActivityStub.getDefaultActivity()); + given(activityImageUseCaseMapper.toDomains(any(), any())) + .willReturn(ActivityStub.getActivityImages(activity)); + given(activityParticipantUseCaseMapper.toDomains(any(), any())) + .willReturn(ActivityStub.getActivityParticipants(activity)); + + activityCreateService.create(studyId, request, memberId); + + then(activityCreatePort).should().create(any()); + then(activityImageCreatePort).should().create(any()); + then(activityParticipantPort).should().create(any()); + } + + @Test + @DisplayName("[실패] 존재하지 않는 스터디로 활동 생성 요청 시 예외가 발생한다.") + void notExistsStudyTest() { + Long studyId = StudyStub.getStudyId(); + Long memberId = MemberStub.getInvalidMemberId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + + willThrow(new StudyNotExistsException(studyId)) + .given(studyValidationUseCase).checkById(studyId); + + assertThatCode(() -> activityCreateService.create(studyId, request, memberId)) + .isInstanceOf(StudyNotExistsException.class) + .hasMessage(MessageFormat.format(StudyNotExistsException.MESSAGE, studyId)); + } + @Test + @DisplayName("[실패] 생성 요청을 한 사용자가 스터디의 관리자가 아닌 경우 예외가 발생한다.") + void notAdminTest() { + Long studyId = StudyStub.getStudyId(); + Long memberId = MemberStub.getInvalidMemberId(); + ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); + + willThrow(new NotStudyAdminException(studyId, memberId)) + .given(studyMemberValidationUseCase).checkAdmin(studyId, memberId); + + assertThatCode(() -> activityCreateService.create(studyId, request, memberId)) + .isInstanceOf(NotStudyAdminException.class) + .hasMessage(MessageFormat.format(NotStudyAdminException.MESSAGE, studyId, memberId)); + + } + + } +} \ No newline at end of file diff --git a/src/test/java/com/stumeet/server/stub/ActivityStub.java b/src/test/java/com/stumeet/server/stub/ActivityStub.java new file mode 100644 index 00000000..44434544 --- /dev/null +++ b/src/test/java/com/stumeet/server/stub/ActivityStub.java @@ -0,0 +1,96 @@ +package com.stumeet.server.stub; + +import com.stumeet.server.activity.application.service.model.ActivityCreateSource; +import com.stumeet.server.activity.application.port.in.command.ActivityCreateCommand; +import com.stumeet.server.activity.domain.model.*; + +import java.time.LocalDateTime; +import java.util.List; + +public class ActivityStub { + private ActivityStub() {} + + public static ActivityCreateCommand getInvalidCreateActivity() { + return ActivityCreateCommand.builder() + .category("DEFAULT") + .title("") + .content("") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .isNotice(false) + .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) + .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) + .participants(List.of()) + .build(); + } + + public static ActivityCreateCommand getInvalidCategoryCreateActivity() { + return ActivityCreateCommand.builder() + .category("INVALID") + .title("title") + .content("content") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .isNotice(false) + .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) + .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) + .participants(List.of(MemberStub.getMemberId())) + .build(); + } + + public static ActivityCreateCommand getDefaultActivityCreateCommand() { + return ActivityCreateCommand.builder() + .category("DEFAULT") + .title("title") + .content("content") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .isNotice(false) + .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) + .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) + .participants(List.of(MemberStub.getMemberId())) + .build(); + } + + public static ActivityCreateSource getDefaultConstructCommand() { + return ActivityCreateSource.builder() + .id(null) + .studyId(StudyStub.getStudyId()) + .author(ActivityCreateSource.ActivityMemberCreateSource.builder() + .id(MemberStub.getMemberId()) + .build()) + .category(ActivityCategory.DEFAULT) + .title("title") + .content("content") + .isNotice(false) + .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) + .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static Activity getDefaultActivity() { + return ActivityCategory.DEFAULT.create(getDefaultConstructCommand()); + } + + public static List getActivityImages(Activity activity) { + return List.of( + ActivityImage.builder() + .activity(activity) + .url("https://example.com/image1.png") + .build(), + ActivityImage.builder() + .activity(activity) + .url("https://example.com/image2.png") + .build() + ); + } + + public static List getActivityParticipants(Activity activity) { + return List.of( + ActivityParticipant.builder() + .activity(activity) + .member(ActivityMember.builder().id(MemberStub.getMemberId()).build()) + .status(activity.getCategory().getDefaultStatus()) + .build() + ); + } + +} diff --git a/src/test/java/com/stumeet/server/stub/MemberStub.java b/src/test/java/com/stumeet/server/stub/MemberStub.java index e9e1c151..c5e8f636 100644 --- a/src/test/java/com/stumeet/server/stub/MemberStub.java +++ b/src/test/java/com/stumeet/server/stub/MemberStub.java @@ -104,7 +104,7 @@ public static Long getMemberId() { return 1L; } - public static Long getInvalidStudyId() { + public static Long getInvalidMemberId() { return 0L; } } diff --git a/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java index 54239dff..2d6d2cb2 100644 --- a/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java +++ b/src/test/java/com/stumeet/server/studymember/adapter/in/web/StudyMemberLeaveApiTest.java @@ -155,7 +155,7 @@ void notExistStudyTest() throws Exception { @DisplayName("[실패] 스터디에 가입된 멤버가 아닌 경우 스터디 멤버 강퇴에 실패한다.") void notJoinedStudyMemberTest() throws Exception { Long studyId = StudyStub.getStudyId(); - Long invalidMemberId = MemberStub.getInvalidStudyId(); + Long invalidMemberId = MemberStub.getInvalidMemberId(); mockMvc.perform(delete(path, studyId, invalidMemberId) .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) diff --git a/src/test/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepositoryCustomImplTest.java b/src/test/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepositoryCustomImplTest.java index e7557b0f..97965817 100644 --- a/src/test/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepositoryCustomImplTest.java +++ b/src/test/java/com/stumeet/server/studymember/adapter/out/persistence/JpaStudyMemberRepositoryCustomImplTest.java @@ -56,7 +56,7 @@ void studyJoinMemberSuccessTest() { void notStudyJoinMemberTest() { boolean want = false; Long studyId = StudyStub.getStudyId(); - Long memberId = MemberStub.getInvalidStudyId(); + Long memberId = MemberStub.getInvalidMemberId(); boolean got = studyMemberRepositoryCustom.isStudyJoinMember(studyId, memberId);