diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 10b02c43..a608e7ce 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -20,6 +20,7 @@ include::dailytodo/daily-todo-v1.adoc[] include::dailytodo/daily-todo-v2.adoc[] include::dailytodocertification/daily-todo-certification-v1.adoc[] include::memberactivity/member-activity-v1.adoc[] +include::memberactivity/member-activity-v2.adoc[] include::reminder/reminder-v1.adoc[] include::notification/notification-v1.adoc[] include::s3/s3-v1.adoc[] diff --git a/src/docs/asciidoc/memberactivity/member-activity-v2.adoc b/src/docs/asciidoc/memberactivity/member-activity-v2.adoc new file mode 100644 index 00000000..05ace72f --- /dev/null +++ b/src/docs/asciidoc/memberactivity/member-activity-v2.adoc @@ -0,0 +1,68 @@ +[[member-activity-v2-api]] +== [V2] 사용자 활동 API + +[[get-my-challenge-group-activity-summary-v2]] +=== 참여중인 특정 챌린지 그룹 활동 요약 조회 API + +==== 개발 상태 +|=== +| 환경 | 구현 여부 + +| 개발 +| O + +| 운영 +| X +|=== + +==== HTTP Request +include::{snippets}/member-activity-controller-v2-docs-test/get-my-challenge-group-activity-summary-v2/http-request.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-challenge-group-activity-summary-v2/path-parameters.adoc[] + +==== HTTP Response +include::{snippets}/member-activity-controller-v2-docs-test/get-my-challenge-group-activity-summary-v2/http-response.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-challenge-group-activity-summary-v2/response-fields.adoc[] + +[[get-my-certifications-v2]] +=== 사용자 인증 목록 전체 조회 API + +==== 개발 상태 +|=== +| 환경 | 구현 여부 + +| 개발 +| O + +| 운영 +| X +|=== + +==== HTTP Request +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certifications-v2/http-request.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certifications-v2/query-parameters.adoc[] + +==== HTTP Response +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certifications-v2/http-response.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certifications-v2/response-fields.adoc[] + +[[get-my-certification-stats-v2]] +=== 사용자 인증 통계 조회 API + +==== 개발 상태 +|=== +| 환경 | 구현 여부 + +| 개발 +| O + +| 운영 +| X +|=== + +==== HTTP Request +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certification-stats-v2/http-request.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certification-stats-v2/query-parameters.adoc[] + +==== HTTP Response +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certification-stats-v2/http-response.adoc[] +include::{snippets}/member-activity-controller-v2-docs-test/get-my-certification-stats-v2/response-fields.adoc[] diff --git a/src/main/java/site/dogether/memberactivity/controller/v1/MemberActivityControllerV1.java b/src/main/java/site/dogether/memberactivity/controller/v1/MemberActivityControllerV1.java index 216a83e1..f153cbcd 100644 --- a/src/main/java/site/dogether/memberactivity/controller/v1/MemberActivityControllerV1.java +++ b/src/main/java/site/dogether/memberactivity/controller/v1/MemberActivityControllerV1.java @@ -19,13 +19,11 @@ import site.dogether.memberactivity.controller.v1.dto.response.GetMyProfileApiResponseV1; import site.dogether.memberactivity.service.MemberActivityService; import site.dogether.memberactivity.service.dto.CertificationPeriodDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByCertificatedAtDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByGroupCreatedAtDto; import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationActivityDto; import site.dogether.memberactivity.service.dto.FindMyProfileDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; -import site.dogether.memberactivity.service.dto.MyCertificationStatsInChallengeGroupDto; import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; import java.util.List; @@ -46,7 +44,7 @@ public ResponseEntity final ChallengeGroupInfoDto challengeGroupInfo = memberActivityService.getChallengeGroupInfo(memberId, groupId); final List certificationPeriods = memberActivityService.getCertificationPeriods(memberId, groupId); final MyRankInChallengeGroupDto myRankInChallengeGroup = memberActivityService.getMyRankInChallengeGroup(memberId, groupId); - final MyCertificationStatsInChallengeGroupDto myCertificationStatsInChallengeGroup = memberActivityService.getMyCertificationStatsInChallengeGroup(memberId, groupId); + final MyCertificationStatsDto myCertificationStatsInChallengeGroup = memberActivityService.getMyCertificationStatsInChallengeGroup(memberId, groupId); return ResponseEntity.ok(success(new GetMyChallengeGroupActivityStatsApiResponseV1( GetMyChallengeGroupActivityStatsApiResponseV1.ChallengeGroupInfo.from(challengeGroupInfo), @@ -63,12 +61,11 @@ public ResponseEntity certifications = memberActivityService.getCertificationsByStatus(memberId, status, pageable); - // TODO: 추후 v2 올릴 때 'TODO_COMPLETED_AT'가 아닌 'CERTIFICATED_AT'으로 변경 필요 if (sortBy.equals("TODO_COMPLETED_AT")) { - final List groupedCertifications = memberActivityService.certificationsGroupedByCertificatedAt(certifications.getContent()); + final List groupedCertifications = memberActivityService.certificationsGroupedByCertificatedAt(certifications.getContent()); return ResponseEntity.ok(success(new GetMyActivityStatsAndCertificationsApiResponseV1( GetMyActivityStatsAndCertificationsApiResponseV1.MyCertificationStats.from(myCertificationStats), @@ -79,7 +76,7 @@ public ResponseEntity groupedCertifications = memberActivityService.certificationsGroupedByGroupCreatedAt(certifications.getContent()); + final List groupedCertifications = memberActivityService.certificationsGroupedByGroupCreatedAt(certifications.getContent()); return ResponseEntity.ok(success(new GetMyActivityStatsAndCertificationsApiResponseV1( GetMyActivityStatsAndCertificationsApiResponseV1.MyCertificationStats.from(myCertificationStats), @@ -96,7 +93,6 @@ public ResponseEntity> getMyG @RequestParam final String sortBy, @RequestParam(required = false) final String status ) { - // TODO: 추후 v2 올릴 때 'TODO_COMPLETED_AT'가 아닌 'CERTIFICATED_AT'으로 변경 필요 if (sortBy.equals("TODO_COMPLETED_AT")) { final List dailyTodoCertificationActivity = memberActivityService.getMyGroupCertificationsByCertificatedAt(memberId, todoId, status); diff --git a/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyActivityStatsAndCertificationsApiResponseV1.java b/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyActivityStatsAndCertificationsApiResponseV1.java index 456ae8b5..20ff380a 100644 --- a/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyActivityStatsAndCertificationsApiResponseV1.java +++ b/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyActivityStatsAndCertificationsApiResponseV1.java @@ -2,9 +2,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import org.springframework.data.domain.Slice; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByCertificatedAtDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByGroupCreatedAtDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationInfoDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; import java.util.List; @@ -12,7 +11,6 @@ public record GetMyActivityStatsAndCertificationsApiResponseV1( MyCertificationStats dailyTodoStats, - // TODO: 추후 v2 올릴 때 'TodoCompletedAt'가 아닌 'CertificatedAt'으로 변경 필요 @JsonInclude(JsonInclude.Include.NON_NULL) List certificationsGroupedByTodoCompletedAt, @@ -29,9 +27,9 @@ public record MyCertificationStats( ) { public static MyCertificationStats from(final MyCertificationStatsDto dto) { return new MyCertificationStats( - dto.totalCertificatedCount(), - dto.totalApprovedCount(), - dto.totalRejectedCount() + dto.certificatedCount(), + dto.approvedCount(), + dto.rejectedCount() ); } } @@ -40,15 +38,15 @@ public record CertificationsGroupedByCertificatedAt( String createdAt, List certificationInfo ) { - public static List fromList(final List dtoList) { + public static List fromList(final List dtoList) { return dtoList.stream() .map(CertificationsGroupedByCertificatedAt::from) .toList(); } - private static CertificationsGroupedByCertificatedAt from(final CertificationsGroupedByCertificatedAtDto dto) { + private static CertificationsGroupedByCertificatedAt from(final GroupedCertificationsDto dto) { return new CertificationsGroupedByCertificatedAt( - dto.createdAt(), + dto.groupedBy(), dto.certificationInfo().stream() .map(DailyTodoCertificationInfo::from) .toList() @@ -60,15 +58,15 @@ public record CertificationsGroupedByGroupCreatedAt( String groupName, List certificationInfo ) { - public static List fromList(final List dtoList) { + public static List fromList(final List dtoList) { return dtoList.stream() .map(CertificationsGroupedByGroupCreatedAt::from) .toList(); } - private static CertificationsGroupedByGroupCreatedAt from(final CertificationsGroupedByGroupCreatedAtDto dto) { + private static CertificationsGroupedByGroupCreatedAt from(final GroupedCertificationsDto dto) { return new CertificationsGroupedByGroupCreatedAt( - dto.groupName(), + dto.groupedBy(), dto.certificationInfo().stream() .map(DailyTodoCertificationInfo::from) .toList() diff --git a/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyChallengeGroupActivityStatsApiResponseV1.java b/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyChallengeGroupActivityStatsApiResponseV1.java index fcbe0d36..e8c8ab1f 100644 --- a/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyChallengeGroupActivityStatsApiResponseV1.java +++ b/src/main/java/site/dogether/memberactivity/controller/v1/dto/response/GetMyChallengeGroupActivityStatsApiResponseV1.java @@ -2,7 +2,7 @@ import site.dogether.memberactivity.service.dto.CertificationPeriodDto; import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; -import site.dogether.memberactivity.service.dto.MyCertificationStatsInChallengeGroupDto; +import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; import java.util.List; @@ -67,7 +67,7 @@ public record MyCertificationStatsInChallengeGroup( int approvedCount, int rejectedCount ) { - public static MyCertificationStatsInChallengeGroup from(final MyCertificationStatsInChallengeGroupDto dto) { + public static MyCertificationStatsInChallengeGroup from(final MyCertificationStatsDto dto) { return new MyCertificationStatsInChallengeGroup( dto.certificatedCount(), dto.approvedCount(), diff --git a/src/main/java/site/dogether/memberactivity/controller/v2/MemberActivityControllerV2.java b/src/main/java/site/dogether/memberactivity/controller/v2/MemberActivityControllerV2.java new file mode 100644 index 00000000..4f2fe3a9 --- /dev/null +++ b/src/main/java/site/dogether/memberactivity/controller/v2/MemberActivityControllerV2.java @@ -0,0 +1,71 @@ +package site.dogether.memberactivity.controller.v2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import site.dogether.auth.resolver.Authenticated; +import site.dogether.common.controller.dto.response.ApiResponse; +import site.dogether.memberactivity.controller.v2.dto.response.GetMyCertificationStatsApiResponseV2; +import site.dogether.memberactivity.controller.v2.dto.response.GetMyCertificationsApiResponseV2; +import site.dogether.memberactivity.controller.v2.dto.response.GetMyChallengeGroupActivitySummaryApiResponseV2; +import site.dogether.memberactivity.service.MemberActivityService; +import site.dogether.memberactivity.service.dto.CertificationPeriodDto; +import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsResultDto; +import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; +import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; + +import java.util.List; + +import static site.dogether.common.controller.dto.response.ApiResponse.success; + +@RequiredArgsConstructor +@RequestMapping("/api/v2/my") +@RestController +public class MemberActivityControllerV2 { + + private final MemberActivityService memberActivityService; + + @GetMapping("/groups/{groupId}/activity-summary") + public ResponseEntity> getMyChallengeGroupActivitySummary( + @Authenticated final Long memberId, @PathVariable final Long groupId + ) { + final ChallengeGroupInfoDto challengeGroupInfo = memberActivityService.getChallengeGroupInfo(memberId, groupId); + final List certificationPeriods = memberActivityService.getCertificationPeriods(memberId, groupId); + final MyRankInChallengeGroupDto myRankInChallengeGroup = memberActivityService.getMyRankInChallengeGroup(memberId, groupId); + + return ResponseEntity.ok(success(GetMyChallengeGroupActivitySummaryApiResponseV2.of( + challengeGroupInfo, + certificationPeriods, + myRankInChallengeGroup + ))); + } + + @GetMapping("/certifications") + public ResponseEntity> getMyCertifications( + @Authenticated final Long memberId, + @RequestParam final String sortBy, + @RequestParam(required = false) final String status, + @PageableDefault(size = 50) final Pageable pageable + ) { + final GroupedCertificationsResultDto certifications = memberActivityService.getCertifications(memberId, sortBy, status, pageable); + + return ResponseEntity.ok(success(GetMyCertificationsApiResponseV2.of(certifications))); + } + + @GetMapping("/certification-stats") + public ResponseEntity> getMyCertificationStats( + @Authenticated final Long memberId, + @RequestParam(required = false) final Long groupId + ) { + final MyCertificationStatsDto myCertificationStats = memberActivityService.getMyCertificationStats(memberId, groupId); + + return ResponseEntity.ok(success(GetMyCertificationStatsApiResponseV2.of(myCertificationStats))); + } +} diff --git a/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationStatsApiResponseV2.java b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationStatsApiResponseV2.java new file mode 100644 index 00000000..ab2504b5 --- /dev/null +++ b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationStatsApiResponseV2.java @@ -0,0 +1,20 @@ +package site.dogether.memberactivity.controller.v2.dto.response; + +import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; + +public record GetMyCertificationStatsApiResponseV2( + int certificatedCount, + int approvedCount, + int rejectedCount +) { + + public static GetMyCertificationStatsApiResponseV2 of( + final MyCertificationStatsDto dto + ) { + return new GetMyCertificationStatsApiResponseV2( + dto.certificatedCount(), + dto.approvedCount(), + dto.rejectedCount() + ); + } +} diff --git a/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationsApiResponseV2.java b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationsApiResponseV2.java new file mode 100644 index 00000000..af3a02eb --- /dev/null +++ b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyCertificationsApiResponseV2.java @@ -0,0 +1,75 @@ +package site.dogether.memberactivity.controller.v2.dto.response; + +import org.springframework.data.domain.Slice; +import site.dogether.memberactivity.service.dto.DailyTodoCertificationInfoDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsResultDto; + +import java.util.List; + +public record GetMyCertificationsApiResponseV2( + List certifications, + PageInfo pageInfo +) { + + public static GetMyCertificationsApiResponseV2 of( + final GroupedCertificationsResultDto dto + ) { + return new GetMyCertificationsApiResponseV2( + Certification.fromList(dto.certifications()), + PageInfo.from(dto.page()) + ); + } + + public record Certification( + String groupedBy, + List certificationInfo + ) { + public static List fromList(final List dtoList) { + return dtoList.stream() + .map(Certification::from) + .toList(); + } + + private static Certification from(final GroupedCertificationsDto dto) { + return new Certification( + dto.groupedBy(), + dto.certificationInfo().stream() + .map(CertificationInfo::from) + .toList() + ); + } + } + + public record CertificationInfo( + Long id, + String content, + String status, + String certificationContent, + String certificationMediaUrl, + String reviewFeedback + ) { + public static CertificationInfo from(final DailyTodoCertificationInfoDto dto) { + return new CertificationInfo( + dto.id(), + dto.content(), + dto.status(), + dto.certificationContent(), + dto.certificationMediaUrl(), + dto.reviewFeedback() + ); + } + } + + public record PageInfo( + int recentPageNumber, + boolean hasNext + ) { + public static PageInfo from(final Slice slice) { + return new PageInfo( + slice.getPageable().getPageNumber(), + slice.hasNext() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyChallengeGroupActivitySummaryApiResponseV2.java b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyChallengeGroupActivitySummaryApiResponseV2.java new file mode 100644 index 00000000..64e31a3a --- /dev/null +++ b/src/main/java/site/dogether/memberactivity/controller/v2/dto/response/GetMyChallengeGroupActivitySummaryApiResponseV2.java @@ -0,0 +1,74 @@ +package site.dogether.memberactivity.controller.v2.dto.response; + +import site.dogether.memberactivity.service.dto.CertificationPeriodDto; +import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; +import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; + +import java.util.List; + +public record GetMyChallengeGroupActivitySummaryApiResponseV2( + ChallengeGroupInfo groupInfo, + List certificationPeriods, + MyRankInChallengeGroup ranking +) { + + public static GetMyChallengeGroupActivitySummaryApiResponseV2 of( + final ChallengeGroupInfoDto groupInfoDto, + final List periodsDto, + final MyRankInChallengeGroupDto rankDto + ) { + return new GetMyChallengeGroupActivitySummaryApiResponseV2( + ChallengeGroupInfo.from(groupInfoDto), + CertificationPeriod.from(periodsDto), + MyRankInChallengeGroup.from(rankDto) + ); + } + + public record ChallengeGroupInfo( + String name, + int maximumMemberCount, + int currentMemberCount, + String joinCode, + String endAt + ) { + public static ChallengeGroupInfo from(final ChallengeGroupInfoDto dto) { + return new ChallengeGroupInfo( + dto.name(), + dto.maximumMemberCount(), + dto.currentMemberCount(), + dto.joinCode(), + dto.endAt() + ); + } + } + + public record CertificationPeriod( + int day, + int createdCount, + int certificatedCount, + int certificationRate + ) { + public static List from(final List dtos) { + return dtos.stream() + .map(dto -> new CertificationPeriod( + dto.day(), + dto.createdCount(), + dto.certificatedCount(), + dto.certificationRate() + )) + .toList(); + } + } + + public record MyRankInChallengeGroup( + int totalMemberCount, + int myRank + ) { + public static MyRankInChallengeGroup from(final MyRankInChallengeGroupDto dto) { + return new MyRankInChallengeGroup( + dto.totalMemberCount(), + dto.myRank() + ); + } + } +} diff --git a/src/main/java/site/dogether/memberactivity/service/MemberActivityService.java b/src/main/java/site/dogether/memberactivity/service/MemberActivityService.java index 83f1e1a7..bc0a7f6c 100644 --- a/src/main/java/site/dogether/memberactivity/service/MemberActivityService.java +++ b/src/main/java/site/dogether/memberactivity/service/MemberActivityService.java @@ -1,5 +1,6 @@ package site.dogether.memberactivity.service; +import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; @@ -26,14 +27,13 @@ import site.dogether.memberactivity.entity.DailyTodoStats; import site.dogether.memberactivity.repository.DailyTodoStatsRepository; import site.dogether.memberactivity.service.dto.CertificationPeriodDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByCertificatedAtDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByGroupCreatedAtDto; import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationActivityDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationInfoDto; import site.dogether.memberactivity.service.dto.FindMyProfileDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsResultDto; import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; -import site.dogether.memberactivity.service.dto.MyCertificationStatsInChallengeGroupDto; import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; import site.dogether.reminder.service.TodoActivityReminderService; @@ -185,7 +185,7 @@ public MyRankInChallengeGroupDto getMyRankInChallengeGroup(final Long memberId, return new MyRankInChallengeGroupDto(totalMemberCount, myRank); } - public MyCertificationStatsInChallengeGroupDto getMyCertificationStatsInChallengeGroup(final Long memberId, final Long groupId) { + public MyCertificationStatsDto getMyCertificationStatsInChallengeGroup(final Long memberId, final Long groupId) { final Member member = getMember(memberId); final ChallengeGroup challengeGroup = challengeGroupReader.getById(groupId); @@ -194,14 +194,14 @@ public MyCertificationStatsInChallengeGroupDto getMyCertificationStatsInChalleng final DailyTodoCertificationCount dailyTodoCertificationCount = dailyTodoCertificationRepository.countDailyTodoCertification(challengeGroup, member); - return new MyCertificationStatsInChallengeGroupDto( + return new MyCertificationStatsDto( dailyTodoCertificationCount.getTotalCount(), dailyTodoCertificationCount.getApprovedCount(), dailyTodoCertificationCount.getRejectedCount() ); } - public MyCertificationStatsDto getMyCertificationStats(final Long memberId) { + public MyCertificationStatsDto getMyTotalCertificationStats(final Long memberId) { final Member member = getMember(memberId); return dailyTodoStatsRepository.findByMember(member) @@ -224,12 +224,12 @@ public Slice getCertificationsByStatus(final Long member return dailyTodoCertificationRepository.findAllByDailyTodo_MemberOrderByCreatedAtDesc(member, pageable); } - public List certificationsGroupedByCertificatedAt(final List certifications) { + public List certificationsGroupedByCertificatedAt(final List certifications) { return certifications.stream() .collect(Collectors.groupingBy(certification -> certification.getCreatedAt().toLocalDate().format(DATE_FORMATTER))) .entrySet().stream() .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())) - .map(entry -> new CertificationsGroupedByCertificatedAtDto( + .map(entry -> new GroupedCertificationsDto( entry.getKey(), entry.getValue().stream() .sorted(Comparator.comparing(DailyTodoCertification::getCreatedAt).reversed()) @@ -252,12 +252,12 @@ private DailyTodoCertificationInfoDto certificationInfo(final DailyTodoCertifica ); } - public List certificationsGroupedByGroupCreatedAt(final List certifications) { + public List certificationsGroupedByGroupCreatedAt(final List certifications) { return certifications.stream() .collect(Collectors.groupingBy(certification -> certification.getDailyTodo().getChallengeGroup())) .entrySet().stream() .sorted(Comparator.comparing(entry -> entry.getKey().getCreatedAt(), Comparator.reverseOrder())) - .map(entry -> new CertificationsGroupedByGroupCreatedAtDto( + .map(entry -> new GroupedCertificationsDto( entry.getKey().getName(), entry.getValue().stream() .sorted(Comparator.comparing(DailyTodoCertification::getCreatedAt).reversed()) @@ -340,6 +340,22 @@ private List getCertificationsByGroupNameAndStatus(final return dailyTodoCertificationRepository.findAllByDailyTodo_MemberAndDailyTodo_ChallengeGroup_NameOrderByCreatedAtDesc(member, groupName); } + public GroupedCertificationsResultDto getCertifications(final Long memberId, final String sortBy, final String status, final Pageable pageable) { + final Slice certifications = getCertificationsByStatus(memberId, status, pageable); + + List groupedCertifications = new ArrayList<>(); + + if (sortBy.equals("CERTIFICATED_AT")) { + groupedCertifications = certificationsGroupedByCertificatedAt(certifications.getContent()); + } + + if (sortBy.equals("GROUP_CREATED_AT")) { + groupedCertifications = certificationsGroupedByGroupCreatedAt(certifications.getContent()); + } + + return new GroupedCertificationsResultDto(groupedCertifications, certifications); + } + public FindMyProfileDto getMyProfile(final Long memberId) { final Member member = getMember(memberId); @@ -348,4 +364,12 @@ public FindMyProfileDto getMyProfile(final Long memberId) { member.getProfileImageUrl() ); } + + public MyCertificationStatsDto getMyCertificationStats(final Long memberId, @Nullable final Long groupId) { + if (groupId != null) { + return getMyCertificationStatsInChallengeGroup(memberId, groupId); + } + + return getMyTotalCertificationStats(memberId); + } } diff --git a/src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByGroupCreatedAtDto.java b/src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByGroupCreatedAtDto.java deleted file mode 100644 index ad7e4eda..00000000 --- a/src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByGroupCreatedAtDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package site.dogether.memberactivity.service.dto; - -import java.util.List; - -public record CertificationsGroupedByGroupCreatedAtDto( - String groupName, - List certificationInfo -) { -} diff --git a/src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByCertificatedAtDto.java b/src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsDto.java similarity index 64% rename from src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByCertificatedAtDto.java rename to src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsDto.java index 56f913d4..8353a97f 100644 --- a/src/main/java/site/dogether/memberactivity/service/dto/CertificationsGroupedByCertificatedAtDto.java +++ b/src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsDto.java @@ -2,8 +2,8 @@ import java.util.List; -public record CertificationsGroupedByCertificatedAtDto( - String createdAt, +public record GroupedCertificationsDto( + String groupedBy, List certificationInfo ) { } diff --git a/src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsResultDto.java b/src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsResultDto.java new file mode 100644 index 00000000..fd095b93 --- /dev/null +++ b/src/main/java/site/dogether/memberactivity/service/dto/GroupedCertificationsResultDto.java @@ -0,0 +1,12 @@ +package site.dogether.memberactivity.service.dto; + +import org.springframework.data.domain.Slice; +import site.dogether.dailytodocertification.entity.DailyTodoCertification; + +import java.util.List; + +public record GroupedCertificationsResultDto( + List certifications, + Slice page +) { +} diff --git a/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsDto.java b/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsDto.java index fd0bf7bf..2a3dadb0 100644 --- a/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsDto.java +++ b/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsDto.java @@ -1,8 +1,8 @@ package site.dogether.memberactivity.service.dto; public record MyCertificationStatsDto( - int totalCertificatedCount, - int totalApprovedCount, - int totalRejectedCount + int certificatedCount, + int approvedCount, + int rejectedCount ) { } diff --git a/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsInChallengeGroupDto.java b/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsInChallengeGroupDto.java deleted file mode 100644 index 1ea165ed..00000000 --- a/src/main/java/site/dogether/memberactivity/service/dto/MyCertificationStatsInChallengeGroupDto.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.dogether.memberactivity.service.dto; - -public record MyCertificationStatsInChallengeGroupDto( - int certificatedCount, - int approvedCount, - int rejectedCount -) { -} diff --git a/src/test/java/site/dogether/docs/memberactivity/v1/MemberActivityControllerV1DocsTest.java b/src/test/java/site/dogether/docs/memberactivity/v1/MemberActivityControllerV1DocsTest.java index a7dcbd11..666c8954 100644 --- a/src/test/java/site/dogether/docs/memberactivity/v1/MemberActivityControllerV1DocsTest.java +++ b/src/test/java/site/dogether/docs/memberactivity/v1/MemberActivityControllerV1DocsTest.java @@ -13,14 +13,12 @@ import site.dogether.memberactivity.controller.v1.MemberActivityControllerV1; import site.dogether.memberactivity.service.MemberActivityService; import site.dogether.memberactivity.service.dto.CertificationPeriodDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByCertificatedAtDto; -import site.dogether.memberactivity.service.dto.CertificationsGroupedByGroupCreatedAtDto; import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationActivityDto; import site.dogether.memberactivity.service.dto.DailyTodoCertificationInfoDto; import site.dogether.memberactivity.service.dto.FindMyProfileDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; -import site.dogether.memberactivity.service.dto.MyCertificationStatsInChallengeGroupDto; import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; import java.util.List; @@ -64,7 +62,7 @@ void getGroupActivityStatV1() throws Exception { ); final MyRankInChallengeGroupDto myRankInChallengeGroup = new MyRankInChallengeGroupDto(10, 3); - final MyCertificationStatsInChallengeGroupDto myChallengeGroupStats = new MyCertificationStatsInChallengeGroupDto(123, 123, 123); + final MyCertificationStatsDto myChallengeGroupStats = new MyCertificationStatsDto(123, 123, 123); given(memberActivityService.getChallengeGroupInfo(any(), any())) .willReturn(challengeGroupInfo); @@ -154,8 +152,8 @@ void getMemberAllStatsSortedByTodoCompletedAtV1() throws Exception { final Slice slice = new SliceImpl<>(List.of(), PageRequest.of(0, 50), false); - final List certificationsGroupedByCertificatedAt = List.of( - new CertificationsGroupedByCertificatedAtDto( + final List certificationsGroupedByCertificatedAt = List.of( + new GroupedCertificationsDto( "2025.05.01", List.of( new DailyTodoCertificationInfoDto( @@ -168,7 +166,7 @@ void getMemberAllStatsSortedByTodoCompletedAtV1() throws Exception { ) ) ), - new CertificationsGroupedByCertificatedAtDto( + new GroupedCertificationsDto( "2025.05.02", List.of( new DailyTodoCertificationInfoDto( @@ -183,7 +181,7 @@ void getMemberAllStatsSortedByTodoCompletedAtV1() throws Exception { ) ); - given(memberActivityService.getMyCertificationStats(any())) + given(memberActivityService.getMyTotalCertificationStats(any())) .willReturn(stats); given(memberActivityService.getCertificationsByStatus(any(), any(), any())) @@ -285,8 +283,8 @@ void getMemberAllStatsSortedByGroupCreatedAtV1() throws Exception { final Slice slice = new SliceImpl<>(List.of(), PageRequest.of(0, 50), false); - final List certificationsGroupedByGroupCreatedAt = List.of( - new CertificationsGroupedByGroupCreatedAtDto( + final List certificationsGroupedByGroupCreatedAt = List.of( + new GroupedCertificationsDto( "스쿼트 챌린지", List.of( new DailyTodoCertificationInfoDto( @@ -299,7 +297,7 @@ void getMemberAllStatsSortedByGroupCreatedAtV1() throws Exception { ) ) ), - new CertificationsGroupedByGroupCreatedAtDto( + new GroupedCertificationsDto( "TIL 챌린지", List.of( new DailyTodoCertificationInfoDto( @@ -314,7 +312,7 @@ void getMemberAllStatsSortedByGroupCreatedAtV1() throws Exception { ) ); - given(memberActivityService.getMyCertificationStats(any())) + given(memberActivityService.getMyTotalCertificationStats(any())) .willReturn(stats); given(memberActivityService.getCertificationsByStatus(any(), any(), any())) diff --git a/src/test/java/site/dogether/docs/memberactivity/v2/MemberActivityControllerV2DocsTest.java b/src/test/java/site/dogether/docs/memberactivity/v2/MemberActivityControllerV2DocsTest.java new file mode 100644 index 00000000..0a72c07f --- /dev/null +++ b/src/test/java/site/dogether/docs/memberactivity/v2/MemberActivityControllerV2DocsTest.java @@ -0,0 +1,273 @@ +package site.dogether.docs.memberactivity.v2; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import site.dogether.dailytodocertification.entity.DailyTodoCertification; +import site.dogether.docs.util.RestDocsSupport; +import site.dogether.memberactivity.controller.v2.MemberActivityControllerV2; +import site.dogether.memberactivity.service.MemberActivityService; +import site.dogether.memberactivity.service.dto.CertificationPeriodDto; +import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; +import site.dogether.memberactivity.service.dto.DailyTodoCertificationInfoDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsDto; +import site.dogether.memberactivity.service.dto.GroupedCertificationsResultDto; +import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; +import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("사용자 활동 V2 API 문서화 테스트") +class MemberActivityControllerV2DocsTest extends RestDocsSupport { + + private final MemberActivityService memberActivityService = mock(MemberActivityService.class); + + @Override + protected Object initController() { + return new MemberActivityControllerV2(memberActivityService); + } + + @DisplayName("[V2] 참여중인 특정 챌린지 그룹 활동 요약 조회 API") + @Test + void getMyChallengeGroupActivitySummaryV2() throws Exception { + final ChallengeGroupInfoDto challengeGroupInfo = new ChallengeGroupInfoDto( + "그로밋과 함께하는 챌린지", + 10, + 6, + "123456", + "25.02.22" + ); + + final List certificationPeriods = List.of( + new CertificationPeriodDto(1, 8, 2, 25), + new CertificationPeriodDto(2, 6, 3, 50), + new CertificationPeriodDto(3, 6, 3, 50), + new CertificationPeriodDto(4, 3, 3, 100) + ); + + final MyRankInChallengeGroupDto myRankInChallengeGroup = new MyRankInChallengeGroupDto(10, 3); + + given(memberActivityService.getChallengeGroupInfo(any(), any())) + .willReturn(challengeGroupInfo); + + given(memberActivityService.getCertificationPeriods(any(), any())) + .willReturn(certificationPeriods); + + given(memberActivityService.getMyRankInChallengeGroup(any(), any())) + .willReturn(myRankInChallengeGroup); + + mockMvc.perform( + MockMvcRequestBuilders.get("/api/v2/my/groups/{groupId}/activity-summary", 1) + .header("Authorization", "Bearer access_token") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andDo(createDocument( + pathParameters( + parameterWithName("groupId") + .description("챌린지 그룹 id") + .attributes(constraints("존재하는 챌린지 그룹 id만 입력 가능"), pathVariableExample(1))), + responseFields( + fieldWithPath("code") + .description("응답 코드") + .type(JsonFieldType.STRING), + fieldWithPath("message") + .description("응답 메시지") + .type(JsonFieldType.STRING), + fieldWithPath("data.groupInfo.name") + .description("그룹 이름") + .type(JsonFieldType.STRING), + fieldWithPath("data.groupInfo.maximumMemberCount") + .description("그룹 참여 가능 인원수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.groupInfo.currentMemberCount") + .description("현재 그룹 인원수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.groupInfo.joinCode") + .description("초대 코드") + .type(JsonFieldType.STRING), + fieldWithPath("data.groupInfo.endAt") + .description("종료일") + .type(JsonFieldType.STRING), + fieldWithPath("data.certificationPeriods") + .description("인증한 기간 통계") + .optional() + .type(JsonFieldType.ARRAY), + fieldWithPath("data.certificationPeriods[].day") + .description("일차") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.certificationPeriods[].createdCount") + .description("작성한 투두 개수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.certificationPeriods[].certificatedCount") + .description("인증한 투두 개수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.certificationPeriods[].certificationRate") + .description("달성률") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.ranking.totalMemberCount") + .description("그룹원 수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.ranking.myRank") + .description("내 랭킹") + .type(JsonFieldType.NUMBER)))); + } + + @DisplayName("[V2] 사용자 인증 목록 전체 조회 API") + @Test + void getMyCertificationsV2() throws Exception { + final Slice slice = new SliceImpl<>(List.of(), PageRequest.of(0, 50), false); + + final List certifications = List.of( + new GroupedCertificationsDto( + "2025.05.01", + List.of( + new DailyTodoCertificationInfoDto( + 1L, + "운동 하기", + "APPROVE", + "운동 개조짐 ㅋㅋㅋㅋ", + "운동 조지는 짤.png", + "저도 같이 해요..." + ) + ) + ), + new GroupedCertificationsDto( + "2025.05.02", + List.of( + new DailyTodoCertificationInfoDto( + 2L, + "인강 듣기", + "APPROVE", + "인강 진짜 열심히 들었습니다. ㅎ", + "인강 달리는 짤.png", + "얼마나 더 똑똑해지려고 ㄷㄷ" + ) + ) + ) + ); + + final GroupedCertificationsResultDto groupedCertificationsResult = new GroupedCertificationsResultDto(certifications, slice); + + given(memberActivityService.getCertifications(any(), any(), any(), any())) + .willReturn(groupedCertificationsResult); + + mockMvc.perform( + MockMvcRequestBuilders.get("/api/v2/my/certifications") + .param("sortBy", "TODO_COMPLETED_AT") + .param("status", "APPROVE") + .param("page", "0") + .header("Authorization", "Bearer access_token") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andDo(createDocument( + queryParameters( + parameterWithName("sortBy") + .description("정렬 방식") + .attributes(constraints("옵션으로 정해진 값만 허용")) + .attributes(options("CERTIFICATED_AT(투두 인증일 순)", "GROUP_CREATED_AT(그룹 생성일 순)")), + parameterWithName("status") + .optional() + .description("데일리 투두 상태") + .attributes(constraints("옵션으로 정해진 값만 허용")) + .attributes(options("REVIEW_PENDING(검사 대기)", "APPROVE(인정)", "REJECT(노인정)")), + parameterWithName("page") + .description("페이지 번호") + .attributes(constraints("0부터 시작"))), + responseFields( + fieldWithPath("code") + .description("응답 코드") + .type(JsonFieldType.STRING), + fieldWithPath("message") + .description("응답 메시지") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications") + .description("인증한 투두 목록") + .optional() + .type(JsonFieldType.ARRAY), + fieldWithPath("data.certifications[].groupedBy") + .description("그룹핑 기준 값") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications[].certificationInfo") + .description("투두 인증 정보") + .type(JsonFieldType.ARRAY), + fieldWithPath("data.certifications[].certificationInfo[].id") + .description("데일리 투두 id") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.certifications[].certificationInfo[].content") + .description("데일리 투두 내용") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications[].certificationInfo[].status") + .description("데일리 투두 상태") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications[].certificationInfo[].certificationContent") + .description("데일리 투두 인증글 내용") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications[].certificationInfo[].certificationMediaUrl") + .description("데일리 투두 인증글 이미지 URL") + .type(JsonFieldType.STRING), + fieldWithPath("data.certifications[].certificationInfo[].reviewFeedback") + .description("데일리 투두 인증 검사 피드백") + .optional() + .type(JsonFieldType.STRING), + fieldWithPath("data.pageInfo.recentPageNumber") + .description("현재 페이지 번호") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.pageInfo.hasNext") + .description("다음 페이지 존재 여부") + .type(JsonFieldType.BOOLEAN) + ))); + } + + @DisplayName("[V2] 사용자 인증 통계 조회 API") + @Test + void getMyCertificationStatsV2() throws Exception { + final MyCertificationStatsDto challengeGroupInfo = new MyCertificationStatsDto(3, 2, 1); + + given(memberActivityService.getMyCertificationStats(any(), any())) + .willReturn(challengeGroupInfo); + + mockMvc.perform( + MockMvcRequestBuilders.get("/api/v2/my/certification-stats") + .param("groupId", "1") + .header("Authorization", "Bearer access_token") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andDo(createDocument( + queryParameters( + parameterWithName("groupId") + .optional() + .description("챌린지 그룹 id") + .attributes(constraints("존재하는 챌린지 그룹 id만 입력 가능"))), + responseFields( + fieldWithPath("code") + .description("응답 코드") + .type(JsonFieldType.STRING), + fieldWithPath("message") + .description("응답 메시지") + .type(JsonFieldType.STRING), + fieldWithPath("data.certificatedCount") + .description("인증한 투두 개수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.approvedCount") + .description("인정받은 투두 개수") + .type(JsonFieldType.NUMBER), + fieldWithPath("data.rejectedCount") + .description("노인정 받은 투두 개수") + .type(JsonFieldType.NUMBER)))); + } +} diff --git a/src/test/java/site/dogether/memberactivity/service/MemberActivityServiceTest.java b/src/test/java/site/dogether/memberactivity/service/MemberActivityServiceTest.java index 6de08b53..e0b7e8a2 100644 --- a/src/test/java/site/dogether/memberactivity/service/MemberActivityServiceTest.java +++ b/src/test/java/site/dogether/memberactivity/service/MemberActivityServiceTest.java @@ -16,7 +16,7 @@ import site.dogether.memberactivity.repository.DailyTodoStatsRepository; import site.dogether.memberactivity.service.dto.CertificationPeriodDto; import site.dogether.memberactivity.service.dto.ChallengeGroupInfoDto; -import site.dogether.memberactivity.service.dto.MyCertificationStatsInChallengeGroupDto; +import site.dogether.memberactivity.service.dto.MyCertificationStatsDto; import site.dogether.memberactivity.service.dto.MyRankInChallengeGroupDto; import java.time.LocalDate; @@ -169,7 +169,7 @@ void givenMemberIdAndGroupId_whenGetMyRankInChallengeGroup_thenReturnMyRank() { @DisplayName("유효한 값(memberId, groupId)이 들어오면 해당 그룹의 사용자 투두 인증 통계를 반환한다.") @Test - void givenMemberIdAndGroupId_whenGetMyCertificationStatsInChallengeGroup_thenReturnMyCertificationStats() { + void givenMemberIdAndGroupId_whenGetMyCertificationStatsInChallengeGroup_thenReturnMyTotalCertificationStats() { //Given final Member member = createMember("member1"); memberRepository.save(member); @@ -181,10 +181,10 @@ void givenMemberIdAndGroupId_whenGetMyCertificationStatsInChallengeGroup_thenRet challengeGroupMemberRepository.save(challengeGroupMember); //When - final MyCertificationStatsInChallengeGroupDto myCertificationStats = memberActivityService.getMyCertificationStatsInChallengeGroup(member.getId(), challengeGroup.getId()); + final MyCertificationStatsDto myCertificationStats = memberActivityService.getMyCertificationStatsInChallengeGroup(member.getId(), challengeGroup.getId()); //Then - final MyCertificationStatsInChallengeGroupDto expectedMyCertificationStats = new MyCertificationStatsInChallengeGroupDto(0, 0, 0); + final MyCertificationStatsDto expectedMyCertificationStats = new MyCertificationStatsDto(0, 0, 0); assertThat(myCertificationStats).isEqualTo(expectedMyCertificationStats); }