From 55f3a262f161e8eac22407a89b1e5aa11db7bea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=B0=AC=EB=AF=B8?= Date: Mon, 15 Jul 2024 18:50:46 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8[STMT-179]=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20request=20body=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=EC=9A=94=EC=B2=AD=20=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :card_file_box: [STMT-179] activity 테이블에 활동 기간 nullable, link 컬럼 추가 * :sparkles: [STMT-179] request body의 필드 유효성 검증 방침 변경: null 허용 * :sparkles: [STMT-179] 활동을 상속받은 각 활동 유형마다 필수 입력값 검증 로직 추가 * :sparkles: [STMT-179] 활동 생성시 멤버 관리자 여부 대신 스터디 멤버인지 검증 * :white_check_mark: [STMT-179] 활동 생성 기존 테스트 수정 및 추가 예외 사항 테스트 케이스 작성 * :memo: [STMT-179] 활동 생성 API 명세서 추가 작성 --- src/docs/asciidoc/index.adoc | 16 +- .../adapter/out/model/ActivityJpaEntity.java | 6 +- .../in/command/ActivityCreateCommand.java | 12 +- .../port/in/mapper/ActivityUseCaseMapper.java | 5 +- .../service/ActivityCreateService.java | 2 +- .../service/model/ActivityCreateSource.java | 3 +- .../activity/domain/model/Activity.java | 8 +- .../domain/model/ActivityCategory.java | 86 +++++----- .../activity/domain/model/ActivityPeriod.java | 42 +++++ .../activity/domain/model/Assignment.java | 11 +- .../server/activity/domain/model/Default.java | 4 +- .../server/activity/domain/model/Meet.java | 22 ++- .../server/common/response/ErrorCode.java | 6 +- .../V1.6__modify_activity_dates_nullable.sql | 5 + ...1.7__add_link_column_to_activity_table.sql | 2 + .../adapter/in/ActivityCreateApiTest.java | 152 ++++++++++++++++-- .../service/ActivityCreateServiceTest.java | 13 +- .../com/stumeet/server/stub/ActivityStub.java | 46 +++++- 18 files changed, 351 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/stumeet/server/activity/domain/model/ActivityPeriod.java create mode 100644 src/main/resources/db/migration/V1.6__modify_activity_dates_nullable.sql create mode 100644 src/main/resources/db/migration/V1.7__add_link_column_to_activity_table.sql diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index ed8f90d6..6611a166 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -473,14 +473,26 @@ include::{snippets}/create-activity/fail/not-exists-category/response-fields.ado ===== 응답 실패 (403) .스터디의 관리자가 아닌 경우 -include::{snippets}/create-activity/fail/not-admin/response-body.adoc[] -include::{snippets}/create-activity/fail/not-admin/response-fields.adoc[] +include::{snippets}/create-activity/fail/not-study-member/response-body.adoc[] +include::{snippets}/create-activity/fail/not-study-member/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[] +.모임 활동 생성 시 장소 값이 null인 경우 +include::{snippets}/create-activity/fail/location-null-for-meet/response-body.adoc[] +include::{snippets}/create-activity/fail/location-null-for-meet/response-fields.adoc[] + +.모임, 과제 활동 생성 시 활동 기간이 null인 경우 +include::{snippets}/create-activity/fail/period-null/response-body.adoc[] +include::{snippets}/create-activity/fail/period-null/response-fields.adoc[] + +.모임, 과제 활동 생성 시 활동 기간이 유효하지 않은 경우 +include::{snippets}/create-activity/fail/period-invalid/response-body.adoc[] +include::{snippets}/create-activity/fail/period-invalid/response-fields.adoc[] + === 스터디 활동 상세 목록 조회 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 87929887..ab6beffb 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 @@ -47,15 +47,15 @@ public class ActivityJpaEntity extends BaseTimeEntity { @Comment("공지 여부") private boolean isNotice; - @Column(name = "start_date", nullable = false) + @Column(name = "start_date") @Comment("활동 시작일") private LocalDateTime startDate; - @Column(name = "end_date", nullable = false) + @Column(name = "end_date") @Comment("활동 종료일") private LocalDateTime endDate; - @Column(name = "location", nullable = false) + @Column(name = "location") @Comment("활동 장소") private String location; } 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 index 9564083f..208eaf37 100644 --- 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 @@ -1,5 +1,6 @@ package com.stumeet.server.activity.application.port.in.command; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -9,6 +10,8 @@ import java.time.LocalDateTime; import java.util.List; +import com.stumeet.server.common.annotation.validator.NullOrNotBlank; + @Builder public record ActivityCreateCommand( @NotBlank(message = "활동 카테고리를 입력해주세요") @@ -28,16 +31,21 @@ public record ActivityCreateCommand( boolean isNotice, - @DateTimeFormat(pattern = "yyyy-MM-dd''HH:mm:ss") + @Nullable + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime startDate, + @Nullable @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime endDate, + @NullOrNotBlank String location, + @NullOrNotBlank + String link, + @NotNull(message = "참여 멤버 리스트를 전달해주세요") - @Size(min = 1, message = "참여 멤버는 1명 이상이어야 합니다") List participants ) { 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 index db7c6d23..dc540eea 100644 --- 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 @@ -35,10 +35,11 @@ public ActivityCreateSource toSource(Long studyId, ActivityCreateCommand command .category(ActivityCategory.getByName(command.category())) .title(command.title()) .content(command.content()) - .isNotice(command.isNotice()) + .location(command.location()) + .link(command.link()) .startDate(command.startDate()) .endDate(command.endDate()) - .location(command.location()) + .isNotice(command.isNotice()) .createdAt(LocalDateTime.now()) .build(); } 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 index d0eb903e..504496e1 100644 --- a/src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityCreateService.java @@ -39,7 +39,7 @@ public class ActivityCreateService implements ActivityCreateUseCase { @Override public void create(Long studyId, ActivityCreateCommand command, Long memberId) { studyValidationUseCase.checkById(studyId); - studyMemberValidationUseCase.checkAdmin(studyId, memberId); + studyMemberValidationUseCase.checkStudyJoinMember(studyId, memberId); ActivityCreateSource activitySource = activityUseCaseMapper.toSource(studyId, command, memberId); Activity activity = activitySource.category().create(activitySource); diff --git a/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java b/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java index 528825d5..ce3d23f1 100644 --- a/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java +++ b/src/main/java/com/stumeet/server/activity/application/service/model/ActivityCreateSource.java @@ -13,10 +13,11 @@ public record ActivityCreateSource( ActivityCategory category, String title, String content, - boolean isNotice, LocalDateTime startDate, LocalDateTime endDate, String location, + String link, + boolean isNotice, LocalDateTime createdAt ) { @Builder diff --git a/src/main/java/com/stumeet/server/activity/domain/model/Activity.java b/src/main/java/com/stumeet/server/activity/domain/model/Activity.java index ccb1d103..3b4252db 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/Activity.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/Activity.java @@ -6,6 +6,8 @@ import java.time.LocalDateTime; +import org.springframework.cglib.core.Local; + @AllArgsConstructor(access = AccessLevel.PROTECTED) @Getter public abstract class Activity { @@ -22,13 +24,15 @@ public abstract class Activity { private String content; - private boolean isNotice; + private String link; + + private String location; private LocalDateTime startDate; private LocalDateTime endDate; - private String location; + private boolean isNotice; private LocalDateTime createdAt; 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 15401836..01000095 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 @@ -2,6 +2,7 @@ import com.stumeet.server.activity.application.service.model.ActivityCreateSource; import com.stumeet.server.activity.domain.exception.NotExistsActivityCategoryException; + import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -12,80 +13,83 @@ public enum ActivityCategory { DEFAULT(DefaultStatus.NONE) { @Override - public Activity create(ActivityCreateSource command) { + public Activity create(ActivityCreateSource source) { return Default.builder() - .id(command.id()) - .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) + .id(source.id()) + .study(ActivityLinkedStudy.builder().id(source.studyId()).build()) .author(ActivityMember.builder() - .id(command.author().id()) - .name(command.author().name()) - .image(command.author().image()) + .id(source.author().id()) + .name(source.author().name()) + .image(source.author().image()) .build() ) - .category(command.category()) - .title(command.title()) - .content(command.content()) - .isNotice(command.isNotice()) - .startDate(command.startDate()) - .endDate(command.endDate()) - .createdAt(command.createdAt()) + .category(source.category()) + .title(source.title()) + .content(source.content()) + .link(source.link()) + .isNotice(source.isNotice()) + .createdAt(source.createdAt()) .build(); } }, MEET(MeetStatus.MEET_NOT_STARTED) { @Override - public Activity create(ActivityCreateSource command) { + public Activity create(ActivityCreateSource source) { return Meet.builder() - .id(command.id()) - .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) + .id(source.id()) + .study(ActivityLinkedStudy.builder().id(source.studyId()).build()) .author(ActivityMember.builder() - .id(command.author().id()) - .name(command.author().name()) - .image(command.author().image()) + .id(source.author().id()) + .name(source.author().name()) + .image(source.author().image()) .build() ) - .category(command.category()) - .title(command.title()) - .content(command.content()) - .isNotice(command.isNotice()) - .startDate(command.startDate()) - .endDate(command.endDate()) - .location(command.location()) - .createdAt(command.createdAt()) + .category(source.category()) + .title(source.title()) + .content(source.content()) + .location(source.location()) + .link(source.link()) + .startDate(source.startDate()) + .endDate(source.endDate()) + .isNotice(source.isNotice()) + .createdAt(source.createdAt()) .build(); } }, ASSIGNMENT(AssignmentStatus.ASSIGNMENT_NOT_STARTED) { @Override - public Activity create(ActivityCreateSource command) { + public Activity create(ActivityCreateSource source) { return Assignment.builder() - .id(command.id()) - .study(ActivityLinkedStudy.builder().id(command.studyId()).build()) + .id(source.id()) + .study(ActivityLinkedStudy.builder().id(source.studyId()).build()) .author(ActivityMember.builder() - .id(command.author().id()) - .name(command.author().name()) - .image(command.author().image()) + .id(source.author().id()) + .name(source.author().name()) + .image(source.author().image()) .build() ) - .category(command.category()) - .title(command.title()) - .content(command.content()) - .isNotice(command.isNotice()) - .startDate(command.startDate()) - .endDate(command.endDate()) - .createdAt(command.createdAt()) + .category(source.category()) + .title(source.title()) + .content(source.content()) + .link(source.content()) + .startDate(source.startDate()) + .endDate(source.endDate()) + .isNotice(source.isNotice()) + .createdAt(source.createdAt()) .build(); } }; 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); + + public abstract Activity create(ActivityCreateSource source); } diff --git a/src/main/java/com/stumeet/server/activity/domain/model/ActivityPeriod.java b/src/main/java/com/stumeet/server/activity/domain/model/ActivityPeriod.java new file mode 100644 index 00000000..af5d2d03 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/domain/model/ActivityPeriod.java @@ -0,0 +1,42 @@ +package com.stumeet.server.activity.domain.model; + +import java.time.LocalDateTime; + +import com.stumeet.server.common.exception.model.BadRequestException; +import com.stumeet.server.common.response.ErrorCode; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ActivityPeriod { + + private LocalDateTime startDate; + + private LocalDateTime endDate; + + @Builder + private ActivityPeriod(LocalDateTime startDate, LocalDateTime endDate) { + validate(startDate, endDate); + + this.startDate = startDate; + this.endDate = endDate; + } + + private void validate(LocalDateTime startDate, LocalDateTime endDate) { + validateNonNull(startDate, endDate); + validatePeriod(startDate, endDate); + } + + private void validateNonNull(LocalDateTime startDate, LocalDateTime endDate) { + if (startDate == null || endDate == null) { + throw new BadRequestException(ErrorCode.ACTIVITY_PERIOD_REQUIRED_EXCEPTION); + } + } + + private void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) { + if (startDate.isAfter(endDate)) { + throw new BadRequestException(ErrorCode.INVALID_PERIOD_EXCEPTION); + } + } +} diff --git a/src/main/java/com/stumeet/server/activity/domain/model/Assignment.java b/src/main/java/com/stumeet/server/activity/domain/model/Assignment.java index a84a3035..586d82ee 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/Assignment.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/Assignment.java @@ -6,8 +6,15 @@ public class Assignment extends Activity { + private ActivityPeriod period; + @Builder - protected Assignment(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, boolean isNotice, LocalDateTime startDate, LocalDateTime endDate, LocalDateTime createdAt) { - super(id, study, author, category, title, content, isNotice, startDate, endDate, null, createdAt); + protected Assignment(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, boolean isNotice, LocalDateTime startDate, LocalDateTime endDate, String link, LocalDateTime createdAt) { + super(id, study, author, category, title, content, link, null, startDate, endDate, isNotice, createdAt); + + this.period = ActivityPeriod.builder() + .startDate(startDate) + .endDate(endDate) + .build(); } } diff --git a/src/main/java/com/stumeet/server/activity/domain/model/Default.java b/src/main/java/com/stumeet/server/activity/domain/model/Default.java index e839caa4..ca13ec6d 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/Default.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/Default.java @@ -8,7 +8,7 @@ public class Default extends Activity { @Builder - protected Default(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, boolean isNotice, LocalDateTime startDate, LocalDateTime endDate, String location, LocalDateTime createdAt) { - super(id, study, author, category, title, content, isNotice, startDate, endDate, location, createdAt); + protected Default(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, String link, boolean isNotice, LocalDateTime createdAt) { + super(id, study, author, category, title, content, link, null, null, null, isNotice, createdAt); } } diff --git a/src/main/java/com/stumeet/server/activity/domain/model/Meet.java b/src/main/java/com/stumeet/server/activity/domain/model/Meet.java index 642022f2..e64ec857 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/Meet.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/Meet.java @@ -5,11 +5,29 @@ import java.time.LocalDateTime; +import com.stumeet.server.common.exception.model.BadRequestException; +import com.stumeet.server.common.response.ErrorCode; + @Getter public class Meet extends Activity { + private ActivityPeriod period; + @Builder - protected Meet(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, boolean isNotice, LocalDateTime startDate, LocalDateTime endDate, String location, LocalDateTime createdAt) { - super(id, study, author, category, title, content, isNotice, startDate, endDate, location, createdAt); + protected Meet(Long id, ActivityLinkedStudy study, ActivityMember author, ActivityCategory category, String title, String content, String location, String link, LocalDateTime startDate, LocalDateTime endDate, boolean isNotice, LocalDateTime createdAt) { + super(id, study, author, category, title, content, link, location, startDate, endDate, isNotice, createdAt); + + validateLocationNonNull(location); + + this.period = ActivityPeriod.builder() + .startDate(startDate) + .endDate(endDate) + .build(); + } + + private void validateLocationNonNull(String location) { + if (location == null) { + throw new BadRequestException(ErrorCode.LOCATION_REQUIRED_FOR_MEET_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 220b735c..f75a88d5 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -20,7 +20,6 @@ public enum ErrorCode { NOT_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 값이 존재하지 않습니다."), FILE_SIZE_LIMIT_EXCEEDED_EXCEPTION(HttpStatus.BAD_REQUEST, "첨부파일은 최대 5MB 까지 가능합니다."), INVALID_PAGINATION_PARAMETERS_EXCEPTION(HttpStatus.BAD_REQUEST, "제공된 페이지네이션 매개변수가 유효하지 않습니다. 'page'와 'size' 매개변수를 함께 포함하거나 함께 생략해야 합니다."), - INVALID_PERIOD_EXCEPTION(HttpStatus.BAD_REQUEST, "종료일이 시작일보다 빠릅니다."), DUPLICATE_NICKNAME_EXCEPTION(HttpStatus.BAD_REQUEST, "닉네임이 중복되었습니다."), NOT_MATCHED_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 토큰과 매칭되는 토큰이 없습니다."), @@ -31,9 +30,12 @@ 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_PERIOD_EXCEPTION(HttpStatus.BAD_REQUEST, "종료일이 시작일보다 빠릅니다."), + ACTIVITY_PERIOD_REQUIRED_EXCEPTION(HttpStatus.BAD_REQUEST, "모임, 과제 활동 생성 시 종료일과 시작일 값이 필수입니다."), INVALID_ACTIVITY_CATEGORY_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 활동 카테고리입니다."), START_DATE_NOT_YET_EXCEPTION(HttpStatus.BAD_REQUEST, "시작일 전에 스터디를 완료할 수 없습니다."), + LOCATION_REQUIRED_FOR_MEET_EXCEPTION(HttpStatus.BAD_REQUEST, "모임 활동 생성 시 장소 값이 필수입니다."), /* diff --git a/src/main/resources/db/migration/V1.6__modify_activity_dates_nullable.sql b/src/main/resources/db/migration/V1.6__modify_activity_dates_nullable.sql new file mode 100644 index 00000000..51f34390 --- /dev/null +++ b/src/main/resources/db/migration/V1.6__modify_activity_dates_nullable.sql @@ -0,0 +1,5 @@ +ALTER TABLE activity + MODIFY COLUMN start_date DATETIME NULL; + +ALTER TABLE activity + MODIFY COLUMN end_date DATETIME NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V1.7__add_link_column_to_activity_table.sql b/src/main/resources/db/migration/V1.7__add_link_column_to_activity_table.sql new file mode 100644 index 00000000..186f9ef1 --- /dev/null +++ b/src/main/resources/db/migration/V1.7__add_link_column_to_activity_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE activity + ADD COLUMN link VARCHAR(255) NULL; \ No newline at end of file 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 index 0fc8951b..9a788d45 100644 --- a/src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java +++ b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityCreateApiTest.java @@ -60,9 +60,10 @@ void successTest() throws Exception { fieldWithPath("content").description("활동 내용"), fieldWithPath("images[]").description("활동 이미지 URL 리스트"), fieldWithPath("isNotice").description("공지 여부"), - fieldWithPath("startDate").description("활동 시작 일시"), - fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("startDate").description("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), fieldWithPath("participants").description("참여자 ID 리스트") ), responseFields( @@ -99,9 +100,10 @@ void invalidRequestTest() throws Exception { fieldWithPath("content").description("활동 내용"), fieldWithPath("images[]").description("활동 이미지 URL 리스트"), fieldWithPath("isNotice").description("공지 여부"), - fieldWithPath("startDate").description("활동 시작 일시"), - fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("startDate").description("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), fieldWithPath("participants").description("참여자 ID 리스트") ), responseFields( @@ -139,9 +141,10 @@ void notExistsStudyTest() throws Exception { fieldWithPath("content").description("활동 내용"), fieldWithPath("images[]").description("활동 이미지 URL 리스트"), fieldWithPath("isNotice").description("공지 여부"), - fieldWithPath("startDate").description("활동 시작 일시"), - fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("startDate").description("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), fieldWithPath("participants").description("참여자 ID 리스트") ), responseFields( @@ -180,9 +183,10 @@ void notExistsActivityCategoryTest() throws Exception { fieldWithPath("content").description("활동 내용"), fieldWithPath("images[]").description("활동 이미지 URL 리스트"), fieldWithPath("isNotice").description("공지 여부"), - fieldWithPath("startDate").description("활동 시작 일시"), - fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("startDate").description("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), fieldWithPath("participants").description("참여자 ID 리스트") ), responseFields( @@ -193,9 +197,9 @@ void notExistsActivityCategoryTest() throws Exception { } @Test - @WithMockMember(id = 2L) - @DisplayName("[실패] 생성 요청을 한 사용자가 스터디의 관리자가 아닌 경우 예외가 발생한다.") - void notAdminTest() throws Exception { + @WithMockMember(id = 3L) + @DisplayName("[실패] 생성 요청을 한 사용자가 스터디 멤버가 아닌 경우 예외가 발생한다.") + void notStudyMemberTest() throws Exception { Long studyId = StudyStub.getStudyId(); ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); @@ -204,7 +208,7 @@ void notAdminTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(toJson(request))) .andExpect(status().isForbidden()) - .andDo(document("create-activity/fail/not-admin", + .andDo(document("create-activity/fail/not-study-member", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), pathParameters( @@ -219,9 +223,129 @@ void notAdminTest() throws Exception { fieldWithPath("content").description("활동 내용"), fieldWithPath("images[]").description("활동 이미지 URL 리스트"), fieldWithPath("isNotice").description("공지 여부"), - fieldWithPath("startDate").description("활동 시작 일시"), - fieldWithPath("endDate").description("활동 종료 일시"), + fieldWithPath("startDate").description("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 모임 활동 생성 시 장소 값이 NULL인 경우 예외가 발생한다.") + void locationNullForMeetTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getMeetActivityCreateCommandLocationNull(); + + 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/location-null-for-meet", + 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("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + @Test + @WithMockMember + @DisplayName("[실패] 모임, 과제 활동 생성 시 활동 기간이 NULL인 경우 예외가 발생한다.") + void activityPeriodRequiredTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getMeetActivityCreateCommandPeriodNull(); + + 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/period-null", + 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("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), + fieldWithPath("participants").description("참여자 ID 리스트") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 모임, 과제 활동 생성 시 활동 기간이 유효하지 않은 경우 예외가 발생한다.") + void activityPeriodInvalidTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + ActivityCreateCommand request = ActivityStub.getMeetActivityCreateCommandPeriodInvalid(); + + 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/period-invalid", + 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("활동 시작 일시").optional(), + fieldWithPath("endDate").description("활동 종료 일시").optional(), + fieldWithPath("location").description("활동 장소").optional(), + fieldWithPath("link").description("링크").optional(), fieldWithPath("participants").description("참여자 ID 리스트") ), responseFields( 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 index f4c520db..56ebce83 100644 --- a/src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java +++ b/src/test/java/com/stumeet/server/activity/application/service/ActivityCreateServiceTest.java @@ -15,6 +15,7 @@ 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.studymember.domain.exception.StudyMemberNotJoinedException; import com.stumeet.server.template.UnitTest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -101,18 +102,18 @@ void notExistsStudyTest() { .hasMessage(MessageFormat.format(StudyNotExistsException.MESSAGE, studyId)); } @Test - @DisplayName("[실패] 생성 요청을 한 사용자가 스터디의 관리자가 아닌 경우 예외가 발생한다.") - void notAdminTest() { + @DisplayName("[실패] 생성 요청을 한 사용자가 스터디 멤버가 아닌 경우 예외가 발생한다.") + void notStudyMemberTest() { Long studyId = StudyStub.getStudyId(); Long memberId = MemberStub.getInvalidMemberId(); ActivityCreateCommand request = ActivityStub.getDefaultActivityCreateCommand(); - willThrow(new NotStudyAdminException(studyId, memberId)) - .given(studyMemberValidationUseCase).checkAdmin(studyId, memberId); + willThrow(new StudyMemberNotJoinedException(studyId, memberId)) + .given(studyMemberValidationUseCase).checkStudyJoinMember(studyId, memberId); assertThatCode(() -> activityCreateService.create(studyId, request, memberId)) - .isInstanceOf(NotStudyAdminException.class) - .hasMessage(MessageFormat.format(NotStudyAdminException.MESSAGE, studyId, memberId)); + .isInstanceOf(StudyMemberNotJoinedException.class) + .hasMessage(MessageFormat.format(StudyMemberNotJoinedException.MESSAGE, studyId, memberId)); } diff --git a/src/test/java/com/stumeet/server/stub/ActivityStub.java b/src/test/java/com/stumeet/server/stub/ActivityStub.java index bd7fbee1..1c04c8ca 100644 --- a/src/test/java/com/stumeet/server/stub/ActivityStub.java +++ b/src/test/java/com/stumeet/server/stub/ActivityStub.java @@ -19,8 +19,6 @@ public static ActivityCreateCommand getInvalidCreateActivity() { .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(); } @@ -45,8 +43,46 @@ public static ActivityCreateCommand getDefaultActivityCreateCommand() { .content("content") .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) .isNotice(false) + .participants(List.of(MemberStub.getMemberId())) + .build(); + } + + public static ActivityCreateCommand getMeetActivityCreateCommandLocationNull() { + return ActivityCreateCommand.builder() + .category("MEET") + .title("title") + .content("content") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .location(null) .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) + .isNotice(false) + .participants(List.of(MemberStub.getMemberId())) + .build(); + } + + public static ActivityCreateCommand getMeetActivityCreateCommandPeriodNull() { + return ActivityCreateCommand.builder() + .category("MEET") + .title("title") + .content("content") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .location("서울") + .isNotice(false) + .participants(List.of(MemberStub.getMemberId())) + .build(); + } + + public static ActivityCreateCommand getMeetActivityCreateCommandPeriodInvalid() { + return ActivityCreateCommand.builder() + .category("MEET") + .title("title") + .content("content") + .images(List.of("https://example.com/image1.png", "https://example.com/image2.png")) + .location("서울") + .startDate(LocalDateTime.parse("2024-05-02T00:00:00")) + .endDate(LocalDateTime.parse("2024-05-01T00:00:00")) + .isNotice(false) .participants(List.of(MemberStub.getMemberId())) .build(); } @@ -196,9 +232,6 @@ public static ActivityDetailResponse getActivityDetailResponse() { .author(getAuthorResponse()) .imageUrl(getActivityImageResponses()) .participants(List.of(getAuthorResponse(), getParticipantResponse())) - .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) - .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) - .location(null) .status(DefaultStatus.NONE.getDescription()) .build(); } @@ -211,9 +244,6 @@ public static ActivityDetailResponse getActivityDetailResponseForNotJoinedUser() .author(getAuthorResponse()) .imageUrl(getActivityImageResponses()) .participants(List.of(getAuthorResponse(), getParticipantResponse())) - .startDate(LocalDateTime.parse("2024-04-01T00:00:00")) - .endDate(LocalDateTime.parse("2050-05-01T00:00:00")) - .location(null) .status(NotJoinedStatus.NOT_JOINED.getDescription()) .build(); }