diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 86534a3f..68cbeb04 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -486,6 +486,8 @@ include::{snippets}/create-activity/fail/not-exists-study/response-fields.adoc[] 스터디 활동 상세 목록을 조회하는 API입니다. +* 정렬 기준: 생성일 + ==== GET /api/v1/studies/activities/detail ===== 요청 include::{snippets}/get-activity-details-by-condition/success/http-request.adoc[] @@ -507,6 +509,54 @@ include::{snippets}/get-activity-details-by-condition/fail/study-not-found/respo include::{snippets}/get-activity-details-by-condition/fail/study-not-found/response-fields.adoc[] +=== 스터디 활동 간략 목록 조회 + +스터디 활동 간략 목록을 조회하는 API입니다. 페이지네이션과 단순 조회를 모두 지원합니다. + +* 날짜 기준 +- 활동 유형 `MEET`: 활동 시작일 +- 활동 유형 `ASSIGNMENT`: 활동 종료일 +- 활동 유형 `DEFAULT`: 생성일 + +* 정렬 기준 +- 활동 유형 `MEET`: 활동 시작일 +- 활동 유형 `ASSIGNMENT`: 활동 종료일 +- 활동 유형 `DEFAULT`: 생성일 +- 활동 유형 조건이 없는 경우: 생성일 + +==== GET /api/v1/studies/activities/brief +===== 요청 +include::{snippets}/get-activity-briefs-by-condition/success/http-request.adoc[] +include::{snippets}/get-activity-briefs-by-condition/success/request-headers.adoc[] +include::{snippets}/get-activity-briefs-by-condition/success/query-parameters.adoc[] + +===== 응답 성공 (200) +include::{snippets}/get-activity-briefs-by-condition/success/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/success/response-fields.adoc[] + +===== 응답 실패 (400) +.불완전한 페이지네이션 요청변수로 요청하는 경우 +include::{snippets}/get-activity-briefs-by-condition/fail/pagination-incomplete/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/fail/pagination-incomplete/response-fields.adoc[] + +.0보다 작은 페이지네이션 요청변수로 요청하는 경우 +include::{snippets}/get-activity-briefs-by-condition/fail/pagination-invalid/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/fail/pagination-invalid/response-fields.adoc[] + +.유효하지 않은 활동 유형을 요청한 경우 +include::{snippets}/get-activity-briefs-by-condition/fail/activity-category-invalid/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/fail/activity-category-invalid/response-fields.adoc[] + +.시작기준일이 종료기준일보다 이후인 경우 +include::{snippets}/get-activity-briefs-by-condition/fail/period_invalid/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/fail/period_invalid/response-fields.adoc[] + +===== 응답 실패 (404) +.존재하지 않는 스터디 ID를 요청한 경우 +include::{snippets}/get-activity-briefs-by-condition/fail/study-not-found/response-body.adoc[] +include::{snippets}/get-activity-briefs-by-condition/fail/study-not-found/response-fields.adoc[] + + === 스터디 활동 단일 조회 스터디 활동을 단일 조회하는 API입니다. diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/ActivityQueryApi.java b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityQueryApi.java index ff112223..7cc3aa01 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/in/ActivityQueryApi.java +++ b/src/main/java/com/stumeet/server/activity/adapter/in/ActivityQueryApi.java @@ -1,19 +1,25 @@ package com.stumeet.server.activity.adapter.in; +import java.time.LocalDateTime; + import com.stumeet.server.activity.adapter.in.response.ActivityDetailResponse; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponses; import com.stumeet.server.activity.adapter.in.response.ActivityListDetailedPageResponses; import com.stumeet.server.activity.application.port.in.ActivityQueryUseCase; +import com.stumeet.server.activity.application.port.in.query.ActivityListBriefQuery; import com.stumeet.server.activity.application.port.in.query.ActivityListDetailedQuery; 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.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,35 +28,69 @@ @WebAdapter @RequestMapping("/api/v1") @RequiredArgsConstructor +@Validated public class ActivityQueryApi { private final ActivityQueryUseCase activityQueryUseCase; @GetMapping("/studies/{studyId}/activities/{activityId}") public ResponseEntity> getById( - @PathVariable Long studyId, - @PathVariable Long activityId, - @AuthenticationPrincipal LoginMember member + @PathVariable Long studyId, + @PathVariable Long activityId, + @AuthenticationPrincipal LoginMember member ) { ActivityDetailResponse response = activityQueryUseCase.getById(studyId, activityId, member.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_SUCCESS, response)); + .body(ApiResponse.success(SuccessCode.GET_SUCCESS, response)); } @GetMapping("/studies/activities/detail") public ResponseEntity> getDetailsByCondition( - @AuthenticationPrincipal LoginMember member, - @RequestParam Integer size, - @RequestParam Integer page, - @RequestParam(required = false) Boolean isNotice, - @RequestParam(required = false) Long studyId, - @RequestParam(required = false) String category + @AuthenticationPrincipal LoginMember member, + @RequestParam @Min(value = 0) Integer size, + @RequestParam @Min(value = 0) Integer page, + @RequestParam(required = false) Boolean isNotice, + @RequestParam(required = false) Long studyId, + @RequestParam(required = false) String category ) { - ActivityListDetailedQuery query = - ActivityListDetailedQuery.of(size, page, isNotice, member.getId(), studyId, category); + ActivityListDetailedQuery query = ActivityListDetailedQuery.builder() + .size(size) + .page(page) + .isNotice(isNotice) + .studyId(studyId) + .memberId(member.getId()) + .categoryName(category) + .build(); ActivityListDetailedPageResponses response = activityQueryUseCase.getDetails(query); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_SUCCESS, response)); + .body(ApiResponse.success(SuccessCode.GET_SUCCESS, response)); + } + + @GetMapping("/studies/activities/brief") + public ResponseEntity> getBriefsByCondition( + @AuthenticationPrincipal LoginMember member, + @RequestParam(required = false) @Min(value = 0) Integer size, + @RequestParam(required = false) @Min(value = 0) Integer page, + @RequestParam(required = false) Boolean isNotice, + @RequestParam(required = false) Long studyId, + @RequestParam(required = false) String category, + @RequestParam(required = false) LocalDateTime fromDate, + @RequestParam(required = false) LocalDateTime toDate + ) { + ActivityListBriefQuery query = ActivityListBriefQuery.builder() + .size(size) + .page(page) + .isNotice(isNotice) + .studyId(studyId) + .memberId(member.getId()) + .categoryName(category) + .fromDate(fromDate) + .toDate(toDate) + .build(); + ActivityListBriefResponses response = activityQueryUseCase.getBriefs(query); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.GET_SUCCESS, response)); } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponse.java b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponse.java new file mode 100644 index 00000000..a66d0b04 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponse.java @@ -0,0 +1,24 @@ +package com.stumeet.server.activity.adapter.in.response; + +import java.time.LocalDateTime; + +import com.querydsl.core.annotations.QueryProjection; +import com.stumeet.server.activity.domain.model.ActivityStatus; + +import lombok.Builder; + +@Builder +public record ActivityListBriefResponse( + Long id, + String category, + String title, + LocalDateTime startDate, + LocalDateTime endDate, + String location, + ActivityStatus status, + LocalDateTime createdAt +) { + @QueryProjection + public ActivityListBriefResponse { + } +} diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponses.java b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponses.java new file mode 100644 index 00000000..fd8b9a2d --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListBriefResponses.java @@ -0,0 +1,12 @@ +package com.stumeet.server.activity.adapter.in.response; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record ActivityListBriefResponses( + List items, + PageInfoResponse pageInfo +) { +} diff --git a/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListDetailedPageResponses.java b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListDetailedPageResponses.java index e2ad3f42..80e82a3a 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListDetailedPageResponses.java +++ b/src/main/java/com/stumeet/server/activity/adapter/in/response/ActivityListDetailedPageResponses.java @@ -6,7 +6,7 @@ @Builder public record ActivityListDetailedPageResponses( - List items, - PageInfoResponse pageInfo + List items, + PageInfoResponse pageInfo ) { } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityStatusConverter.java b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityStatusConverter.java index 0c032763..5835ec27 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityStatusConverter.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/mapper/ActivityStatusConverter.java @@ -1,6 +1,8 @@ package com.stumeet.server.activity.adapter.out.mapper; import com.stumeet.server.activity.domain.model.ActivityStatus; +import com.stumeet.server.activity.domain.model.CommonStatus; + import jakarta.persistence.AttributeConverter; public class ActivityStatusConverter implements AttributeConverter { @@ -11,6 +13,9 @@ public String convertToDatabaseColumn(ActivityStatus activityStatus) { @Override public ActivityStatus convertToEntityAttribute(String s) { + if (s == null) { + return CommonStatus.NON_PARTICIPATION; + } return ActivityStatus.findByStatus(s); } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java index d8e9bd5b..e595edaf 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/ActivityPersistenceAdapter.java @@ -1,8 +1,12 @@ package com.stumeet.server.activity.adapter.out.persistence; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.adapter.out.mapper.ActivityPersistenceMapper; import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; import com.stumeet.server.activity.application.port.out.ActivityCreatePort; @@ -31,17 +35,28 @@ public Activity create(Activity activity) { @Override public Activity getById(Long activityId) { ActivityJpaEntity entity = jpaActivityRepository.findById(activityId) - .orElseThrow(() -> new NotExistsActivityException(activityId)); + .orElseThrow(() -> new NotExistsActivityException(activityId)); return activityPersistenceMapper.toDomain(entity); } @Override - public Page getDetailPagesByCondition( - Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category) { + public Page getDetailPagesByCondition(Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category) { Page entities = - jpaActivityRepository.findDetailPagesByCondition(pageable, isNotice, studyId, category); + jpaActivityRepository.findDetailPagesByCondition(pageable, isNotice, studyId, category); return activityPersistenceMapper.toDomainPages(entities); } + + @Override + public Page getPaginatedBriefsByCondition(Pageable pageable, Boolean isNotice, Long memberId, + Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate) { + return jpaActivityRepository.findBriefsByConditionWithPagination(pageable, isNotice, memberId, studyId, category, startDate, endDate); + } + + @Override + public List getBriefsByCondition(Boolean isNotice, Long memberId, Long studyId, + ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate) { + return jpaActivityRepository.findBriefsByCondition(isNotice, memberId, studyId, category, startDate, endDate); + } } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustom.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustom.java index 8670abe9..9424ef49 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustom.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustom.java @@ -1,11 +1,19 @@ package com.stumeet.server.activity.adapter.out.persistence; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; import com.stumeet.server.activity.domain.model.ActivityCategory; public interface JpaActivityRepositoryCustom { Page findDetailPagesByCondition(Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category); + + Page findBriefsByConditionWithPagination(Pageable pageable, Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); + + List findBriefsByCondition(Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); } diff --git a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustomImpl.java b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustomImpl.java index ccc0a96e..92c95b9f 100644 --- a/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustomImpl.java +++ b/src/main/java/com/stumeet/server/activity/adapter/out/persistence/JpaActivityRepositoryCustomImpl.java @@ -1,47 +1,216 @@ package com.stumeet.server.activity.adapter.out.persistence; import static com.stumeet.server.activity.adapter.out.model.QActivityJpaEntity.*; +import static com.stumeet.server.activity.adapter.out.model.QActivityParticipantJpaEntity.*; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; +import com.stumeet.server.activity.adapter.in.response.QActivityListBriefResponse; import com.stumeet.server.activity.adapter.out.model.ActivityJpaEntity; import com.stumeet.server.activity.domain.model.ActivityCategory; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public class JpaActivityRepositoryCustomImpl implements JpaActivityRepositoryCustom{ - - private final JPAQueryFactory query; - - @Override - public Page findDetailPagesByCondition( - Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category) { - List content = query - .selectFrom(activityJpaEntity) - .where( - isNotice != null ? activityJpaEntity.isNotice.eq(isNotice) : null, - studyId != null ? activityJpaEntity.study.id.eq(studyId) : null, - category != null ? activityJpaEntity.category.eq(category) : null) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(activityJpaEntity.createdAt.desc()) - .fetch(); - - JPAQuery countQuery = query - .select(activityJpaEntity.count()) - .from(activityJpaEntity) - .where( - isNotice != null ? activityJpaEntity.isNotice.eq(isNotice) : null, - studyId != null ? activityJpaEntity.study.id.eq(studyId) : null, - category != null ? activityJpaEntity.category.eq(category) : null); - - return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); - } +public class JpaActivityRepositoryCustomImpl implements JpaActivityRepositoryCustom { + + private final JPAQueryFactory query; + + @Override + public Page findDetailPagesByCondition( + Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category) { + List content = query + .selectFrom(activityJpaEntity) + .where( + isNoticeEq(isNotice), + studyIdEq(studyId), + categoryEq(category) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(activityJpaEntity.createdAt.desc()) + .fetch(); + + JPAQuery countQuery = query + .select(activityJpaEntity.count()) + .from(activityJpaEntity) + .where( + isNoticeEq(isNotice), + studyIdEq(studyId), + categoryEq(category) + ); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + @Override + public Page findBriefsByConditionWithPagination(Pageable pageable, Boolean isNotice, + Long memberId, Long studyId, ActivityCategory category, LocalDateTime fromDate, LocalDateTime toDate) { + List content = query + .select( + new QActivityListBriefResponse( + activityJpaEntity.id, + activityJpaEntity.category.stringValue(), + activityJpaEntity.title, + activityJpaEntity.startDate, + activityJpaEntity.endDate, + activityJpaEntity.location, + activityParticipantJpaEntity.status, + activityJpaEntity.createdAt + ) + ) + .from(activityJpaEntity) + .leftJoin(activityParticipantJpaEntity) + .on( + activityJpaEntity.id.eq(activityParticipantJpaEntity.activity.id) + .and(activityParticipantJpaEntity.member.id.eq(memberId)) + ) + .where( + isNoticeEq(isNotice), + studyIdEq(studyId), + categoryEq(category), + dateBetweenByCategory(category, fromDate, toDate) + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(orderByCategory(category)) + .orderBy(activityJpaEntity.createdAt.desc()) + .fetch(); + + JPAQuery countQuery = query + .select(activityJpaEntity.count()) + .from(activityJpaEntity) + .leftJoin(activityParticipantJpaEntity) + .on( + activityJpaEntity.id.eq(activityParticipantJpaEntity.activity.id) + .and(activityParticipantJpaEntity.member.id.eq(memberId)) + ) + .where( + isNoticeEq(isNotice), + studyIdEq(studyId), + categoryEq(category), + dateBetweenByCategory(category, fromDate, toDate) + ); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + @Override + public List findBriefsByCondition(Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, + LocalDateTime fromDate, LocalDateTime toDate) { + return query + .select( + new QActivityListBriefResponse( + activityJpaEntity.id, + activityJpaEntity.category.stringValue(), + activityJpaEntity.title, + activityJpaEntity.startDate, + activityJpaEntity.endDate, + activityJpaEntity.location, + activityParticipantJpaEntity.status, + activityJpaEntity.createdAt + ) + ) + .from(activityJpaEntity) + .leftJoin(activityParticipantJpaEntity) + .on( + activityJpaEntity.id.eq(activityParticipantJpaEntity.activity.id) + .and(activityParticipantJpaEntity.member.id.eq(memberId)) + ) + .where( + isNoticeEq(isNotice), + studyIdEq(studyId), + categoryEq(category), + dateBetweenByCategory(category, fromDate, toDate) + ) + .orderBy(orderByCategory(category)) + .orderBy(activityJpaEntity.createdAt.desc()) + .fetch(); + } + + private BooleanExpression isNoticeEq(Boolean isNotice) { + return isNotice != null ? activityJpaEntity.isNotice.eq(isNotice) : null; + } + + private BooleanExpression studyIdEq(Long studyId) { + return studyId != null ? activityJpaEntity.study.id.eq(studyId) : null; + } + + private BooleanExpression categoryEq(ActivityCategory category) { + return category != null ? activityJpaEntity.category.eq(category) : null; + } + + private BooleanExpression startDateBetween(LocalDateTime fromDate, LocalDateTime toDate) { + return startDateGoe(fromDate).and(startDateLoe(toDate)); + } + + private BooleanExpression startDateGoe(LocalDateTime fromDate) { + return fromDate != null ? activityJpaEntity.startDate.goe(fromDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression startDateLoe(LocalDateTime toDate) { + return toDate != null ? activityJpaEntity.startDate.loe(toDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression endDateBetween(LocalDateTime fromDate, LocalDateTime toDate) { + return endDateGoe(fromDate).and(endDateLoe(toDate)); + } + + private BooleanExpression endDateGoe(LocalDateTime fromDate) { + return fromDate != null ? activityJpaEntity.endDate.goe(fromDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression endDateLoe(LocalDateTime toDate) { + return toDate != null ? activityJpaEntity.endDate.loe(toDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression createdAtBetween(LocalDateTime fromDate, LocalDateTime toDate) { + return createdAtGoe(fromDate).and(createdAtLoe(toDate)); + } + + private BooleanExpression createdAtGoe(LocalDateTime fromDate) { + return fromDate != null ? activityJpaEntity.createdAt.goe(fromDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression createdAtLoe(LocalDateTime toDate) { + return toDate != null ? activityJpaEntity.createdAt.loe(toDate) : Expressions.asBoolean(true).isTrue(); + } + + private BooleanExpression dateBetweenByCategory(ActivityCategory category, LocalDateTime fromDate, LocalDateTime toDate) { + if (category == null) { + return categoryEq(ActivityCategory.MEET).and(startDateBetween(fromDate, toDate)) + .or(categoryEq(ActivityCategory.ASSIGNMENT).and(endDateBetween(fromDate, toDate))) + .or(categoryEq(ActivityCategory.DEFAULT).and(createdAtBetween(fromDate, toDate))); + } + + return switch (category) { + case MEET -> startDateBetween(fromDate, toDate); + case ASSIGNMENT -> endDateBetween(fromDate, toDate); + case DEFAULT -> createdAtBetween(fromDate, toDate); + }; + } + + private OrderSpecifier orderByCategory(ActivityCategory category) { + if (category == null) { + return activityJpaEntity.createdAt.desc(); + } + + return switch (category) { + case MEET -> activityJpaEntity.startDate.desc(); + case ASSIGNMENT -> activityJpaEntity.endDate.desc(); + case DEFAULT -> activityJpaEntity.createdAt.desc(); + }; + } } + diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQuery.java b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQuery.java index 2960efaa..ca1f4925 100644 --- a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQuery.java +++ b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQuery.java @@ -1,8 +1,12 @@ package com.stumeet.server.activity.application.port.in; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.domain.model.Activity; import com.stumeet.server.activity.domain.model.ActivityCategory; @@ -10,4 +14,8 @@ public interface ActivityQuery { Activity getById(Long activityId); Page getDetailsByCondition(Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category); + + Page getPaginatedBriefsByCondition(Pageable pageable, Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); + + List getBriefsByCondition(Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); } diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQueryUseCase.java b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQueryUseCase.java index 5878413a..8bba32dd 100644 --- a/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQueryUseCase.java +++ b/src/main/java/com/stumeet/server/activity/application/port/in/ActivityQueryUseCase.java @@ -1,11 +1,15 @@ package com.stumeet.server.activity.application.port.in; import com.stumeet.server.activity.adapter.in.response.ActivityDetailResponse; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponses; import com.stumeet.server.activity.adapter.in.response.ActivityListDetailedPageResponses; +import com.stumeet.server.activity.application.port.in.query.ActivityListBriefQuery; import com.stumeet.server.activity.application.port.in.query.ActivityListDetailedQuery; public interface ActivityQueryUseCase { ActivityDetailResponse getById(Long studyId, Long activityId, Long memberId); ActivityListDetailedPageResponses getDetails(ActivityListDetailedQuery query); + + ActivityListBriefResponses getBriefs(ActivityListBriefQuery query); } 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 c6e26b12..db7c6d23 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 @@ -66,7 +66,7 @@ public ActivityDetailResponse toDetailResponse( .build(); } - public ActivityListDetailedPageResponse toListDetailResponse(Activity activity) { + private ActivityListDetailedPageResponse toListDetailedPageResponse(Activity activity) { return ActivityListDetailedPageResponse.builder() .id(activity.getId()) .category(activity.getCategory().name()) @@ -86,7 +86,7 @@ public ActivityListDetailedPageResponses toListDetailedPageResponses( ) { return ActivityListDetailedPageResponses.builder() .items(activities.stream() - .map(this::toListDetailResponse) + .map(this::toListDetailedPageResponse) .toList()) .pageInfo(pageInfoResponse) .build(); diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListBriefQuery.java b/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListBriefQuery.java new file mode 100644 index 00000000..436bd430 --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListBriefQuery.java @@ -0,0 +1,55 @@ +package com.stumeet.server.activity.application.port.in.query; + +import java.time.LocalDateTime; + +import com.stumeet.server.activity.domain.model.ActivityCategory; +import com.stumeet.server.common.exception.model.BadRequestException; +import com.stumeet.server.common.response.ErrorCode; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ActivityListBriefQuery { + private final Integer size; + private final Integer page; + private final Boolean isNotice; + private final Long memberId; + private final Long studyId; + private final ActivityCategory category; + private final LocalDateTime fromDate; + private final LocalDateTime toDate; + + @Builder + private ActivityListBriefQuery(Integer size, Integer page, Boolean isNotice, Long memberId, Long studyId, + String categoryName, LocalDateTime fromDate, LocalDateTime toDate) { + this(size, page, isNotice, memberId, studyId, + categoryName != null ? ActivityCategory.getByName(categoryName) : null, + fromDate, toDate); + validate(); + } + + private void validate() { + if (isIncompletePaginationRequest()) { + throw new BadRequestException(ErrorCode.INVALID_PAGINATION_PARAMETERS_EXCEPTION); + } + + if (isFromDateAfterToDate()) { + throw new BadRequestException(ErrorCode.INVALID_PERIOD_EXCEPTION); + } + } + + private boolean isIncompletePaginationRequest() { + return ((this.size != null) && (this.page == null)) || ((this.size == null) && (this.page != null)); + } + + private boolean isFromDateAfterToDate() { + return (fromDate != null && toDate != null) && (this.fromDate.isAfter(this.toDate)); + } + + public boolean isPaginationRequest() { + return (this.size != null) && (this.page != null); + } +} diff --git a/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListDetailedQuery.java b/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListDetailedQuery.java index 64bbf9d6..316f80f5 100644 --- a/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListDetailedQuery.java +++ b/src/main/java/com/stumeet/server/activity/application/port/in/query/ActivityListDetailedQuery.java @@ -2,17 +2,29 @@ import com.stumeet.server.activity.domain.model.ActivityCategory; -public record ActivityListDetailedQuery( - int size, - int page, - Boolean isNotice, - Long memberId, - Long studyId, - ActivityCategory category -) { - public static ActivityListDetailedQuery of( - int size, int page, Boolean isNotice, Long memberId, Long studyId, String categoryName) { - ActivityCategory category = categoryName != null ? ActivityCategory.getByName(categoryName) : null; - return new ActivityListDetailedQuery(size, page, isNotice, memberId, studyId, category); +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ActivityListDetailedQuery { + private final Integer size; + private final Integer page; + private final Boolean isNotice; + Long memberId; + Long studyId; + ActivityCategory category; + + @Builder + private ActivityListDetailedQuery(Integer size, Integer page, Boolean isNotice, Long memberId, Long studyId, String categoryName) { + this(size, page, isNotice, memberId, studyId, categoryName != null ? ActivityCategory.getByName(categoryName) : null); + } + + private ActivityListDetailedQuery(Integer size, Integer page, Boolean isNotice, Long memberId, Long studyId, ActivityCategory category) { + this.size = size; + this.page = page; + this.isNotice = isNotice; + this.memberId = memberId; + this.studyId = studyId; + this.category = category; } } 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 index 95768621..7f97f4e1 100644 --- 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 @@ -1,6 +1,5 @@ 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 { diff --git a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityQueryPort.java b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityQueryPort.java index 6a8587a8..0338f0cb 100644 --- a/src/main/java/com/stumeet/server/activity/application/port/out/ActivityQueryPort.java +++ b/src/main/java/com/stumeet/server/activity/application/port/out/ActivityQueryPort.java @@ -1,8 +1,12 @@ package com.stumeet.server.activity.application.port.out; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.domain.model.Activity; import com.stumeet.server.activity.domain.model.ActivityCategory; @@ -10,4 +14,8 @@ public interface ActivityQueryPort { Activity getById(Long activityId); Page getDetailPagesByCondition(Pageable pageable, Boolean isNotice, Long studyId, ActivityCategory category); + + Page getPaginatedBriefsByCondition(Pageable pageable, Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); + + List getBriefsByCondition(Boolean isNotice, Long memberId, Long studyId, ActivityCategory category, LocalDateTime startDate, LocalDateTime endDate); } diff --git a/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryFacade.java b/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryFacade.java index 3af3d482..74f22e5a 100644 --- a/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryFacade.java +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryFacade.java @@ -1,6 +1,8 @@ package com.stumeet.server.activity.application.service; import com.stumeet.server.activity.adapter.in.response.ActivityDetailResponse; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponses; import com.stumeet.server.activity.adapter.in.response.ActivityListDetailedPageResponses; import com.stumeet.server.activity.application.port.in.ActivityImageQuery; import com.stumeet.server.activity.application.port.in.ActivityParticipantQuery; @@ -10,6 +12,7 @@ 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.in.mapper.PageInfoUseCaseMapper; +import com.stumeet.server.activity.application.port.in.query.ActivityListBriefQuery; import com.stumeet.server.activity.application.port.in.query.ActivityListDetailedQuery; import com.stumeet.server.activity.domain.model.Activity; import com.stumeet.server.activity.domain.model.ActivityImage; @@ -68,17 +71,53 @@ public ActivityDetailResponse getById(Long studyId, Long activityId, Long member @Override public ActivityListDetailedPageResponses getDetails(ActivityListDetailedQuery query) { - if (query.studyId() != null) { - studyValidationUseCase.checkById(query.studyId()); + if (query.getStudyId() != null) { + studyValidationUseCase.checkById(query.getStudyId()); } Page activities = activityQuery.getDetailsByCondition( - PageRequest.of(query.page(), query.size()), - query.isNotice(), - query.studyId(), - query.category()); + PageRequest.of(query.getPage(), query.getSize()), + query.getIsNotice(), + query.getStudyId(), + query.getCategory()); return activityUseCaseMapper .toListDetailedPageResponses(activities, pageInfoUseCaseMapper.toPageInfoResponse(activities)); } + + @Override + public ActivityListBriefResponses getBriefs(ActivityListBriefQuery query) { + if (query.getStudyId() != null) { + studyValidationUseCase.checkById(query.getStudyId()); + } + + if (query.isPaginationRequest()) { + Page activities = activityQuery.getPaginatedBriefsByCondition( + PageRequest.of(query.getPage(), query.getSize()), + query.getIsNotice(), + query.getMemberId(), + query.getStudyId(), + query.getCategory(), + query.getFromDate(), + query.getToDate()); + + return ActivityListBriefResponses.builder() + .items(activities.toList()) + .pageInfo(pageInfoUseCaseMapper.toPageInfoResponse(activities)) + .build(); + } else { + List activities = activityQuery.getBriefsByCondition( + query.getIsNotice(), + query.getMemberId(), + query.getStudyId(), + query.getCategory(), + query.getFromDate(), + query.getToDate()); + + return ActivityListBriefResponses.builder() + .items(activities) + .pageInfo(null) + .build(); + } + } } diff --git a/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryService.java b/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryService.java index b4d64b1d..cc87a66f 100644 --- a/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryService.java +++ b/src/main/java/com/stumeet/server/activity/application/service/ActivityQueryService.java @@ -1,5 +1,9 @@ package com.stumeet.server.activity.application.service; +import java.time.LocalDateTime; +import java.util.List; + +import com.stumeet.server.activity.adapter.in.response.ActivityListBriefResponse; import com.stumeet.server.activity.application.port.in.ActivityQuery; import com.stumeet.server.activity.application.port.out.ActivityQueryPort; import com.stumeet.server.activity.domain.model.Activity; @@ -17,19 +21,43 @@ @Transactional(readOnly = true) public class ActivityQueryService implements ActivityQuery { - private final ActivityQueryPort activityQueryPort; - - @Override - public Activity getById(Long activityId) { - return activityQueryPort.getById(activityId); - } - - @Override - public Page getDetailsByCondition( - Pageable pageable, - Boolean isNotice, - Long studyId, - ActivityCategory category) { - return activityQueryPort.getDetailPagesByCondition(pageable, isNotice, studyId, category); - } + private final ActivityQueryPort activityQueryPort; + + @Override + public Activity getById(Long activityId) { + return activityQueryPort.getById(activityId); + } + + @Override + public Page getDetailsByCondition( + Pageable pageable, + Boolean isNotice, + Long studyId, + ActivityCategory category) { + return activityQueryPort.getDetailPagesByCondition(pageable, isNotice, studyId, category); + } + + @Override + public Page getPaginatedBriefsByCondition( + Pageable pageable, + Boolean isNotice, + Long memberId, + Long studyId, + ActivityCategory category, + LocalDateTime startDate, + LocalDateTime endDate) { + return activityQueryPort.getPaginatedBriefsByCondition(pageable, isNotice, memberId, studyId, category, + startDate, endDate); + } + + @Override + public List getBriefsByCondition( + Boolean isNotice, + Long memberId, + Long studyId, + ActivityCategory category, + LocalDateTime startDate, + LocalDateTime endDate) { + return activityQueryPort.getBriefsByCondition(isNotice, memberId, studyId, category, startDate, endDate); + } } diff --git a/src/main/java/com/stumeet/server/activity/domain/model/ActivityStatus.java b/src/main/java/com/stumeet/server/activity/domain/model/ActivityStatus.java index 9dea301d..4eea0e0d 100644 --- a/src/main/java/com/stumeet/server/activity/domain/model/ActivityStatus.java +++ b/src/main/java/com/stumeet/server/activity/domain/model/ActivityStatus.java @@ -1,5 +1,6 @@ package com.stumeet.server.activity.domain.model; +import com.fasterxml.jackson.annotation.JsonValue; import com.stumeet.server.activity.domain.exception.NotExistsActivityStatusException; import java.util.stream.Stream; @@ -7,6 +8,7 @@ public interface ActivityStatus { String getStatus(); + @JsonValue String getDescription(); static ActivityStatus findByStatus(String status) { @@ -16,5 +18,4 @@ static ActivityStatus findByStatus(String status) { .findAny() .orElseThrow(() -> new NotExistsActivityStatusException(status)); } - } diff --git a/src/main/java/com/stumeet/server/activity/domain/model/CommonStatus.java b/src/main/java/com/stumeet/server/activity/domain/model/CommonStatus.java new file mode 100644 index 00000000..d539e20e --- /dev/null +++ b/src/main/java/com/stumeet/server/activity/domain/model/CommonStatus.java @@ -0,0 +1,21 @@ +package com.stumeet.server.activity.domain.model; + +public enum CommonStatus implements ActivityStatus{ + NON_PARTICIPATION("미참여"); + + private final String description; + + CommonStatus(String description) { + this.description = description; + } + + @Override + public String getStatus() { + return this.name(); + } + + @Override + public String getDescription() { + return this.description; + } +} diff --git a/src/main/java/com/stumeet/server/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/stumeet/server/common/exception/handler/GlobalExceptionHandler.java index 7e035c42..fece01e9 100644 --- a/src/main/java/com/stumeet/server/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/stumeet/server/common/exception/handler/GlobalExceptionHandler.java @@ -1,14 +1,18 @@ package com.stumeet.server.common.exception.handler; +import java.util.List; +import java.util.stream.Collectors; + import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.stumeet.server.common.exception.model.BadRequestException; import com.stumeet.server.common.exception.model.BusinessException; -import com.stumeet.server.common.exception.model.InvalidStateException; -import com.stumeet.server.common.exception.model.NotExistsException; import com.stumeet.server.common.response.ErrorCode; import com.stumeet.server.common.model.ApiResponse; + +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; @@ -61,6 +65,20 @@ protected ResponseEntity handleBindException(final BindException e) .body(response); } + @ExceptionHandler(ConstraintViolationException.class) + protected ResponseEntity handleConstraintViolationException(final ConstraintViolationException e) { + log.warn(ERROR_LOG_MESSAGE, e.getClass().getSimpleName(), e.getMessage()); + + List errors = e.getConstraintViolations().stream() + .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) + .collect(Collectors.toList()); + + ApiResponse response = ApiResponse.fail(HttpStatus.BAD_REQUEST.value(), String.join(", ", errors)); + + return ResponseEntity.badRequest() + .body(response); + } + @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { log.warn(ERROR_LOG_MESSAGE, e.getClass().getSimpleName(), e.getMessage()); @@ -91,12 +109,8 @@ protected ResponseEntity handleMaxUploadSizeExceededException(final .body(response); } - @ExceptionHandler({ - BadRequestException.class, - // NotExistsException.class, - // InvalidStateException.class - }) - protected ResponseEntity handleCustomBadRequestException(final BusinessException e) { + @ExceptionHandler(BadRequestException.class) + protected ResponseEntity handleCustomBadRequestException(final BadRequestException e) { log.warn(ERROR_LOG_MESSAGE, e.getClass().getSimpleName(), e.getMessage()); String message = String.format("%s %s", e.getErrorCode().getMessage(), e.getMessage()); 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 67c08087..0862f201 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -19,6 +19,8 @@ public enum ErrorCode { INVALID_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않은 데이터입니다."), 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, "요청으로 전달한 토큰과 매칭되는 토큰이 없습니다."), @@ -30,7 +32,6 @@ public enum ErrorCode { INVALID_FILE_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 파일 확장자입니다."), INVALID_ACTIVITY_CATEGORY_EXCEPTION(HttpStatus.BAD_REQUEST, "유효하지 않은 활동 카테고리입니다."), - INVALID_STUDY_PERIOD_EXCEPTION(HttpStatus.BAD_REQUEST, "종료일이 시작일보다 빠릅니다."), START_DATE_NOT_YET_EXCEPTION(HttpStatus.BAD_REQUEST, "시작일 전에 스터디를 완료할 수 없습니다."), /* diff --git a/src/main/java/com/stumeet/server/study/domain/StudyPeriod.java b/src/main/java/com/stumeet/server/study/domain/StudyPeriod.java index dda1d9a8..4b1f8d26 100644 --- a/src/main/java/com/stumeet/server/study/domain/StudyPeriod.java +++ b/src/main/java/com/stumeet/server/study/domain/StudyPeriod.java @@ -23,7 +23,7 @@ private StudyPeriod(LocalDate startDate, LocalDate endDate) { private void validateDateRange(LocalDate startDate, LocalDate endDate) { if (isInvalidDateRange(startDate, endDate)) { - throw new BadRequestException(ErrorCode.INVALID_STUDY_PERIOD_EXCEPTION); + throw new BadRequestException(ErrorCode.INVALID_PERIOD_EXCEPTION); } } diff --git a/src/test/java/com/stumeet/server/activity/adapter/in/ActivityQueryApiTest.java b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityQueryApiTest.java index 2a652603..d0d5b3dd 100644 --- a/src/test/java/com/stumeet/server/activity/adapter/in/ActivityQueryApiTest.java +++ b/src/test/java/com/stumeet/server/activity/adapter/in/ActivityQueryApiTest.java @@ -28,254 +28,475 @@ class ActivityQueryApiTest extends ApiTest { - @Nested - @DisplayName("활동 단일 조회 API") - class GetById { + @Nested + @DisplayName("활동 단일 조회 API") + class GetById { - private static final String PATH = "/api/v1/studies/{studyId}/activities/{activityId}"; + private static final String PATH = "/api/v1/studies/{studyId}/activities/{activityId}"; - @Test - @WithMockMember - @DisplayName("[성공] 스터디 활동 단일 조회에 성공합니다.") - void successTest() throws Exception { - Long studyId = StudyStub.getStudyId(); - Long activityId = ActivityStub.getActivityId(); + @Test + @WithMockMember + @DisplayName("[성공] 스터디 활동 단일 조회에 성공한다.") + void successTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + Long activityId = ActivityStub.getActivityId(); - mockMvc.perform(get(PATH, studyId, activityId) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value(SuccessCode.GET_SUCCESS.getMessage())) - .andDo(document("get-activity-by-id/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - pathParameters( - parameterWithName("studyId").description("스터디 ID"), - parameterWithName("activityId").description("활동 ID") - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.id").description("활동 ID"), - fieldWithPath("data.category").description("활동 유형"), - fieldWithPath("data.title").description("활동 제목"), - fieldWithPath("data.content").description("활동 내용"), - fieldWithPath("data.imageUrl[].id").description("활동 이미지의 아이디"), - fieldWithPath("data.imageUrl[].imageUrl").description("활동 이미지의 URL"), - fieldWithPath("data.author.memberId").description("활동 작성자 ID"), - fieldWithPath("data.author.name").description("활동 작성자 이름"), - fieldWithPath("data.author.profileImageUrl").description("활동 작성자 프로필 이미지 URL"), - fieldWithPath("data.participants[].memberId").description("참여자 ID"), - fieldWithPath("data.participants[].name").description("참여자 이름"), - fieldWithPath("data.participants[].profileImageUrl").description("참여자 프로필 이미지 URL"), - fieldWithPath("data.status").description("나의 활동 상태"), - fieldWithPath("data.startDate").description("활동 시작일"), - fieldWithPath("data.endDate").description("활동 종료일"), - fieldWithPath("data.location").description("장소"), - fieldWithPath("data.createdAt").description("활동 생성일") - ) - )); - } + mockMvc.perform(get(PATH, studyId, activityId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(SuccessCode.GET_SUCCESS.getMessage())) + .andDo(document("get-activity-by-id/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.id").description("활동 ID"), + fieldWithPath("data.category").description("활동 유형"), + fieldWithPath("data.title").description("활동 제목"), + fieldWithPath("data.content").description("활동 내용"), + fieldWithPath("data.imageUrl[].id").description("활동 이미지의 아이디"), + fieldWithPath("data.imageUrl[].imageUrl").description("활동 이미지의 URL"), + fieldWithPath("data.author.memberId").description("활동 작성자 ID"), + fieldWithPath("data.author.name").description("활동 작성자 이름"), + fieldWithPath("data.author.profileImageUrl").description("활동 작성자 프로필 이미지 URL"), + fieldWithPath("data.participants[].memberId").description("참여자 ID"), + fieldWithPath("data.participants[].name").description("참여자 이름"), + fieldWithPath("data.participants[].profileImageUrl").description("참여자 프로필 이미지 URL"), + fieldWithPath("data.status").description("나의 활동 상태"), + fieldWithPath("data.startDate").description("활동 시작일"), + fieldWithPath("data.endDate").description("활동 종료일"), + fieldWithPath("data.location").description("장소"), + fieldWithPath("data.createdAt").description("활동 생성일") + ) + )); + } - @Test - @WithMockMember - @DisplayName("[실패] 스터디가 존재하지 않는 경우 예외가 발생합니다.") - void studyNotFoundTest() throws Exception { - Long studyId = StudyStub.getInvalidStudyId(); - Long activityId = ActivityStub.getActivityId(); + @Test + @WithMockMember + @DisplayName("[실패] 스터디가 존재하지 않는 경우 예외가 발생한다.") + void studyNotFoundTest() throws Exception { + Long studyId = StudyStub.getInvalidStudyId(); + Long activityId = ActivityStub.getActivityId(); - mockMvc.perform(get(PATH, studyId, activityId) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.message").value(ErrorCode.STUDY_NOT_FOUND.getMessage())) - .andDo(document("get-activity-by-id/fail/study-not-found", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - pathParameters( - parameterWithName("studyId").description("스터디 ID"), - parameterWithName("activityId").description("활동 ID") - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지") - ) - )); - } + mockMvc.perform(get(PATH, studyId, activityId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ErrorCode.STUDY_NOT_FOUND.getMessage())) + .andDo(document("get-activity-by-id/fail/study-not-found", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } - @Test - @WithMockMember(id = 3L) - @DisplayName("[실패] 스터디에 가입하지 않은 사용자인 경우 예외가 발생합니다.") - void notJoinedStudyTest() throws Exception { - Long studyId = StudyStub.getStudyId(); - Long activityId = ActivityStub.getActivityId(); + @Test + @WithMockMember(id = 3L) + @DisplayName("[실패] 스터디에 가입하지 않은 사용자인 경우 예외가 발생한다.") + void notJoinedStudyTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + Long activityId = ActivityStub.getActivityId(); - mockMvc.perform(get(PATH, studyId, activityId) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.message").value(ErrorCode.STUDY_MEMBER_NOT_JOINED_EXCEPTION.getMessage())) - .andDo(document("get-activity-by-id/fail/not-joined-study", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - pathParameters( - parameterWithName("studyId").description("스터디 ID"), - parameterWithName("activityId").description("활동 ID") - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지") - ) - )); - } + mockMvc.perform(get(PATH, studyId, activityId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").value(ErrorCode.STUDY_MEMBER_NOT_JOINED_EXCEPTION.getMessage())) + .andDo(document("get-activity-by-id/fail/not-joined-study", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } - @Test - @WithMockMember - @DisplayName("[실패] 활동이 존재하지 않는 경우 예외가 발생합니다.") - void notFoundActivityTest() throws Exception { - Long studyId = StudyStub.getStudyId(); - Long activityId = ActivityStub.getInvalidActivityId(); + @Test + @WithMockMember + @DisplayName("[실패] 활동이 존재하지 않는 경우 예외가 발생한다.") + void notFoundActivityTest() throws Exception { + Long studyId = StudyStub.getStudyId(); + Long activityId = ActivityStub.getInvalidActivityId(); - mockMvc.perform(get(PATH, studyId, activityId) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.message").value(ErrorCode.ACTIVITY_NOT_FOUND.getMessage())) - .andDo(document("get-activity-by-id/fail/activity-not-found", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - pathParameters( - parameterWithName("studyId").description("스터디 ID"), - parameterWithName("activityId").description("활동 ID") - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지") - ) - )); - } - } + mockMvc.perform(get(PATH, studyId, activityId) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value(ErrorCode.ACTIVITY_NOT_FOUND.getMessage())) + .andDo(document("get-activity-by-id/fail/activity-not-found", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + pathParameters( + parameterWithName("studyId").description("스터디 ID"), + parameterWithName("activityId").description("활동 ID") + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + } - @Nested - @DisplayName("활동 목록 상세 조회 API") - class GetDetailsByCondition { - private static final String PATH = "/api/v1/studies/activities/detail"; + @Nested + @DisplayName("활동 목록 상세 조회 API") + class GetDetailsByCondition { + private static final String PATH = "/api/v1/studies/activities/detail"; - @Test - @WithMockMember - @DisplayName("[성공] 스터디 활동 목록 상세 조회에 성공한다.") - void success() throws Exception { - mockMvc.perform(get(PATH) - .param("size", "2") - .param("page", "0") - .param("isNotice", "true") - .param("studyId", StudyStub.getStudyId().toString()) - .param("category", ActivityCategory.DEFAULT.name()) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isOk()) - .andDo(document("get-activity-details-by-condition/success", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - queryParameters( - parameterWithName("size").description("페이지당 결과 수"), - parameterWithName("page").description("조회할 페이지 번호"), - parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), - parameterWithName("studyId").description("특정 스터디 ID").optional(), - parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.items[]").description("활동 상세 목록"), - fieldWithPath("data.items[].id").description("활동 ID"), - fieldWithPath("data.items[].category").description("활동 유형"), - fieldWithPath("data.items[].title").description("활동 제목"), - fieldWithPath("data.items[].content").description("활동 내용"), - fieldWithPath("data.items[].startDate").description("활동 시작일"), - fieldWithPath("data.items[].endDate").description("활동 종료일"), - fieldWithPath("data.items[].location").description("장소"), - fieldWithPath("data.items[].author.memberId").description("활동 작성자 ID"), - fieldWithPath("data.items[].author.name").description("활동 작성자 이름"), - fieldWithPath("data.items[].author.profileImageUrl").description("활동 작성자 프로필 이미지 URL"), - fieldWithPath("data.items[].createdAt").description("활동 생성일"), - fieldWithPath("data.pageInfo").description("페이지 메타 정보"), - fieldWithPath("data.pageInfo.totalPages").description("전체 페이지 수"), - fieldWithPath("data.pageInfo.totalElements").description("전체 요소 수"), - fieldWithPath("data.pageInfo.currentPage").description("현재 페이지"), - fieldWithPath("data.pageInfo.pageSize").description("페이지 크기") - ) - )); - } + @Test + @WithMockMember + @DisplayName("[성공] 스터디 활동 목록 상세 조회에 성공한다.") + void success() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "100") + .param("page", "0") + .param("isNotice", "true") + .param("studyId", StudyStub.getStudyId().toString()) + .param("category", ActivityCategory.MEET.name()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andDo(document("get-activity-details-by-condition/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수"), + parameterWithName("page").description("조회할 페이지 번호"), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.items[]").description("활동 상세 목록"), + fieldWithPath("data.items[].id").description("활동 ID"), + fieldWithPath("data.items[].category").description("활동 유형"), + fieldWithPath("data.items[].title").description("활동 제목"), + fieldWithPath("data.items[].content").description("활동 내용"), + fieldWithPath("data.items[].startDate").description("활동 시작일"), + fieldWithPath("data.items[].endDate").description("활동 종료일"), + fieldWithPath("data.items[].location").description("장소"), + fieldWithPath("data.items[].author.memberId").description("활동 작성자 ID"), + fieldWithPath("data.items[].author.name").description("활동 작성자 이름"), + fieldWithPath("data.items[].author.profileImageUrl").description("활동 작성자 프로필 이미지 URL"), + fieldWithPath("data.items[].createdAt").description("활동 생성일"), + fieldWithPath("data.pageInfo").description("페이지 메타 정보"), + fieldWithPath("data.pageInfo.totalPages").description("전체 페이지 수"), + fieldWithPath("data.pageInfo.totalElements").description("전체 요소 수"), + fieldWithPath("data.pageInfo.currentPage").description("현재 페이지"), + fieldWithPath("data.pageInfo.pageSize").description("페이지 크기") + ) + )); + } - @Test - @WithMockMember - @DisplayName("[실패] 존재하지 않는 스터디 id로 요청하는 경우 활동 상세 조회에 실패한다.") - void fail_when_study_id_not_found() throws Exception{ - mockMvc.perform(get(PATH) - .param("size", "2") - .param("page", "0") - .param("studyId", StudyStub.getInvalidStudyId().toString()) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isNotFound()) - .andDo(document("get-activity-details-by-condition/fail/study-not-found", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - queryParameters( - parameterWithName("size").description("페이지당 결과 수"), - parameterWithName("page").description("조회할 페이지 번호"), - parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), - parameterWithName("studyId").description("특정 스터디 ID").optional(), - parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지") - ) - )); - } + @Test + @WithMockMember + @DisplayName("[실패] 존재하지 않는 스터디 id로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_study_id_not_found() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "2") + .param("page", "0") + .param("studyId", StudyStub.getInvalidStudyId().toString()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andDo(document("get-activity-details-by-condition/fail/study-not-found", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수"), + parameterWithName("page").description("조회할 페이지 번호"), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } - @Test - @WithMockMember - @DisplayName("[실패] 유효하지 않은 활동 유형으로 요청하는 경우 활동 상세 조회에 실패한다.") - void fail_when_activity_category_not_found() throws Exception{ - mockMvc.perform(get(PATH) - .param("size", "2") - .param("page", "0") - .param("category", ActivityStub.getInvalidActivityCategoryName()) - .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) - .andExpect(status().isBadRequest()) - .andDo(document("get-activity-details-by-condition/fail/activity-category-invalid", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()).description("서버로부터 전달받은 액세스 토큰") - ), - queryParameters( - parameterWithName("size").description("페이지당 결과 수"), - parameterWithName("page").description("조회할 페이지 번호"), - parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), - parameterWithName("studyId").description("특정 스터디 ID").optional(), - parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() - ), - responseFields( - fieldWithPath("code").description("응답 코드"), - fieldWithPath("message").description("응답 메시지") - ) - )); - } - } + @Test + @WithMockMember + @DisplayName("[실패] 유효하지 않은 활동 유형으로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_activity_category_not_found() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "2") + .param("page", "0") + .param("category", ActivityStub.getInvalidActivityCategoryName()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isBadRequest()) + .andDo(document("get-activity-details-by-condition/fail/activity-category-invalid", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수"), + parameterWithName("page").description("조회할 페이지 번호"), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + } + + @Nested + @DisplayName("활동 목록 간략 조회 API") + class GetBriefsByCondition { + private static final String PATH = "/api/v1/studies/activities/brief"; + + @Test + @WithMockMember + @DisplayName("[성공] 스터디 활동 목록 간략 조회에 성공한다.") + void success() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "100") + .param("page", "0") + .param("isNotice", "true") + .param("category", ActivityCategory.MEET.name()) + .param("studyId", StudyStub.getStudyId().toString()) + .param("fromDate", "2024-04-01T00:00:00") + .param("toDate", "2024-04-30T00:00:00") + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isOk()) + .andDo(document("get-activity-briefs-by-condition/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.items[]").description("활동 상세 목록"), + fieldWithPath("data.items[].id").description("활동 ID"), + fieldWithPath("data.items[].category").description("활동 유형"), + fieldWithPath("data.items[].title").description("활동 제목"), + fieldWithPath("data.items[].startDate").description("활동 시작일"), + fieldWithPath("data.items[].endDate").description("활동 종료일"), + fieldWithPath("data.items[].location").description("장소"), + fieldWithPath("data.items[].status").description("내 활동 상태"), + fieldWithPath("data.items[].createdAt").description("활동 생성일"), + fieldWithPath("data.pageInfo").description("페이지 메타 정보"), + fieldWithPath("data.pageInfo.totalPages").description("전체 페이지 수"), + fieldWithPath("data.pageInfo.totalElements").description("전체 요소 수"), + fieldWithPath("data.pageInfo.currentPage").description("현재 페이지"), + fieldWithPath("data.pageInfo.pageSize").description("페이지 크기") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 불완전한 페이지네이션 요청변수로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_pagination_parameters_is_incomplete() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "5") + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isBadRequest()) + .andDo(document("get-activity-briefs-by-condition/fail/pagination-incomplete", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 0보다 작은 페이지네이션 요청변수로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_pagination_parameters_is_smaller_than_zero() throws Exception { + mockMvc.perform(get(PATH) + .param("size", "-1") + .param("page", "-1") + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isBadRequest()) + .andDo(document("get-activity-briefs-by-condition/fail/pagination-invalid", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 존재하지 않는 스터디 id로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_study_id_not_found() throws Exception { + mockMvc.perform(get(PATH) + .param("studyId", StudyStub.getInvalidStudyId().toString()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isNotFound()) + .andDo(document("get-activity-briefs-by-condition/fail/study-not-found", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 유효하지 않은 활동 유형으로 요청하는 경우 활동 상세 조회에 실패한다.") + void fail_when_activity_category_not_found() throws Exception { + mockMvc.perform(get(PATH) + .param("category", ActivityStub.getInvalidActivityCategoryName()) + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isBadRequest()) + .andDo(document("get-activity-briefs-by-condition/fail/activity-category-invalid", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 시작기준일이 종료기준일보다 이후인 경우 활동 상세 조회에 실패한다.") + void fail_when_fromDate_is_after_toDate() throws Exception { + mockMvc.perform(get(PATH) + .param("fromDate", "2024-06-01T00:00:00") + .param("toDate", "2024-05-01T00:00:00") + .header(AuthenticationHeader.ACCESS_TOKEN.getName(), TokenStub.getMockAccessToken())) + .andExpect(status().isBadRequest()) + .andDo(document("get-activity-briefs-by-condition/fail/period_invalid", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AuthenticationHeader.ACCESS_TOKEN.getName()) + .description("서버로부터 전달받은 액세스 토큰") + ), + queryParameters( + parameterWithName("size").description("페이지당 결과 수").optional(), + parameterWithName("page").description("조회할 페이지 번호").optional(), + parameterWithName("isNotice").description("공지사항 여부 (true/false)").optional(), + parameterWithName("studyId").description("특정 스터디 ID").optional(), + parameterWithName("category").description("활동 유형 (DEFAULT/MEET/ASSIGNMENT)").optional(), + parameterWithName("fromDate").description("YYYY-MM-DDThh:mm:ss").optional(), + parameterWithName("toDate").description("YYYY-MM-DDThh:mm:ss").optional() + ), + responseFields( + fieldWithPath("code").description("응답 코드"), + fieldWithPath("message").description("응답 메시지") + ) + )); + } + } } \ No newline at end of file diff --git a/src/test/resources/db/setup.sql b/src/test/resources/db/setup.sql index a393f907..540ac472 100644 --- a/src/test/resources/db/setup.sql +++ b/src/test/resources/db/setup.sql @@ -43,7 +43,7 @@ VALUES (4, 'test4', 'http://localhost:4572/user/1/profile/2024030416531039839905 INSERT INTO study_member (member_id, study_id, is_admin, is_sent_grape) VALUES (4, 1, false, false); -- [TABLE: activity] --- 스터디 1 활동 (기본 활동 - 공지) +-- 스터디 1 (공지) 기본 활동 INSERT INTO activity(id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) VALUES (1, 1, 1, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); @@ -53,16 +53,16 @@ INSERT INTO activity_image (id, activity_id, image) VALUES (3, 1, 'https://examp INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (1, 1, 1, 'NONE'); INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (2, 1, 2, 'NONE'); --- 스터디 1 활동 (기본 활동) +-- 스터디 1 기본 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) -VALUES (2, 1, 1, 'DEFAULT', 'title', 'content', false, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); +VALUES (2, 1, 1, 'DEFAULT', 'title', 'content', false, '2024-04-08T00:00:00', '2050-05-01T00:00:00', null); INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (3, 2, 1, 'NONE'); INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (4, 2, 2, 'NONE'); --- 스터디 1 활동 (모임 활동) +-- 스터디 1 모임 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) -VALUES (3, 1, 1, 'MEET', 'title', 'content', false, '2024-04-01T00:00:00', '2050-05-01T00:00:00', '성신여대 카페구월'); +VALUES (3, 1, 1, 'MEET', 'title', 'content', false, '2024-04-15T00:00:00', '2024-05-01T00:00:00', '성신여대 카페구월'); INSERT INTO activity_image (id, activity_id, image) VALUES (7, 3, 'https://example.com/images/image1.png'); INSERT INTO activity_image (id, activity_id, image) VALUES (8, 3, 'https://example.com/images/image2.png'); @@ -70,22 +70,42 @@ INSERT INTO activity_image (id, activity_id, image) VALUES (9, 3, 'https://examp INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (5, 3, 1, 'ATTENDANCE'); INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (6, 3, 2, 'ACKNOWLEDGED_ABSENCE'); --- 스터디 1 활동 (과제 활동) +-- 스터디 1 (공지) 모임 활동 INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) -VALUES (4, 1, 1, 'ASSIGNMENT', 'title', 'content', true, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); +VALUES (4, 1, 1, 'MEET', 'title', 'content', true, '2024-04-22T00:00:00', '2024-04-23T00:00:00', '성신여대 카페구월'); INSERT INTO activity_image (id, activity_id, image) VALUES (10, 4, 'https://example.com/images/image1.png'); INSERT INTO activity_image (id, activity_id, image) VALUES (11, 4, 'https://example.com/images/image2.png'); INSERT INTO activity_image (id, activity_id, image) VALUES (12, 4, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (7, 4, 1, 'UNSUBMITTED'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (8, 4, 2, 'PERFORMED'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (7, 4, 1, 'ATTENDANCE'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (8, 4, 2, 'ACKNOWLEDGED_ABSENCE'); --- 스터디 2 활동 (기본 활동 - 공지) +-- 스터디 1 과제 활동 +INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) +VALUES (5, 1, 1, 'ASSIGNMENT', 'title', 'content', false, '2024-04-01T00:00:00', '2024-04-08T00:00:00', null); + +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (9, 5, 1, 'UNSUBMITTED'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (10, 5, 2, 'PERFORMED'); + +-- 스터디 1 (공지) 과제 활동 +INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) +VALUES (6, 1, 1, 'ASSIGNMENT', 'title', 'content', true, '2024-04-08T00:00:00', '2024-04-15T00:00:00', null); + +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (11, 6, 1, 'UNSUBMITTED'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (12, 6, 2, 'PERFORMED'); + +-- 스터디 1 미참여 과제 활동 +INSERT INTO activity (id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) +VALUES (7, 1, 1, 'ASSIGNMENT', 'title', 'content', false, '2024-04-08T00:00:00', '2024-04-15T00:00:00', null); + +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (13, 6, 2, 'PERFORMED'); + +-- 스터디 2 (공지) 기본 활동 INSERT INTO activity(id, study_id, author_id, category, title, content, is_notice, start_date, end_date, location) -VALUES (5, 2, 1, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2050-05-01T00:00:00', null); +VALUES (8, 2, 1, 'DEFAULT', 'title', 'content', true, '2024-04-01T00:00:00', '2024-04-08T00:00:00', '성신여대 카페구월'); -INSERT INTO activity_image (id, activity_id, image) VALUES (13, 5, 'https://example.com/images/image1.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (14, 5, 'https://example.com/images/image2.png'); -INSERT INTO activity_image (id, activity_id, image) VALUES (15, 5, 'https://example.com/images/image3.png'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (9, 5, 1, 'NONE'); -INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (10, 5, 2, 'NONE'); \ No newline at end of file +INSERT INTO activity_image (id, activity_id, image) VALUES (13, 8, 'https://example.com/images/image1.png'); +INSERT INTO activity_image (id, activity_id, image) VALUES (14, 8, 'https://example.com/images/image2.png'); +INSERT INTO activity_image (id, activity_id, image) VALUES (15, 8, 'https://example.com/images/image3.png'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (14, 8, 1, 'NONE'); +INSERT INTO activity_participant (id, activity_id, member_id, status) VALUES (15, 8, 2, 'NONE'); \ No newline at end of file