From ca85e556349ffaf75582492a08d41ae48c8c6fa4 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:53:18 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A2=85=EB=A3=8C=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=B3=B4=EA=B4=80=ED=95=A8=20API=20=EA=B5=AC=ED=98=84=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 종료미션 중간 커밋 2024.01.15 * feat: 종료미션 보관함 v1 * fix: long casting 이슈 해결 * feat: 2024.01.18 임시 커밋(스케줄러 작업 이후 진행) * fix: spotlessApply * fix: durationStatus 조건으로 변경 * fix: 종료미션 조건 수정 * fix: spotlessApply * feat: 종료미션 slice -> list * feat: 종료미션 달성률 포함 추가 및 개선 * fix: 날짜 between 계산 수정 --- .../domain/mission/api/MissionController.java | 7 +++ .../mission/application/MissionService.java | 39 ++++++++++++++++- .../mission/dao/MissionRepositoryCustom.java | 2 + .../mission/dao/MissionRepositoryImpl.java | 38 ++++++++++++++++ .../dto/response/FinishedMissionResponse.java | 43 +++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java diff --git a/src/main/java/com/depromeet/domain/mission/api/MissionController.java b/src/main/java/com/depromeet/domain/mission/api/MissionController.java index ac2b0047f..caa807264 100644 --- a/src/main/java/com/depromeet/domain/mission/api/MissionController.java +++ b/src/main/java/com/depromeet/domain/mission/api/MissionController.java @@ -3,6 +3,7 @@ import com.depromeet.domain.mission.application.MissionService; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.request.MissionUpdateRequest; +import com.depromeet.domain.mission.dto.response.FinishedMissionResponse; import com.depromeet.domain.mission.dto.response.FollowMissionFindAllResponse; import com.depromeet.domain.mission.dto.response.MissionCreateResponse; import com.depromeet.domain.mission.dto.response.MissionFindAllResponse; @@ -66,6 +67,12 @@ public MissionRecordSummaryResponse missionRecordFindSummary() { return missionService.findSummaryMissionRecord(); } + @Operation(summary = "종료미션 보관함", description = "종료된 미션 리스트를 조회합니다.") + @GetMapping("/finished") + public List missionFindAllFinished() { + return missionService.findAllFinishedMission(); + } + @Operation(summary = "번개 스택 조회", description = "완료한 미션 대상으로 번개 스택을 조회합니다.") @GetMapping("/symbol/{memberId}") public MissionSymbolStackResponse missionSymbolStackFind(@PathVariable Long memberId) { diff --git a/src/main/java/com/depromeet/domain/mission/application/MissionService.java b/src/main/java/com/depromeet/domain/mission/application/MissionService.java index d24edee7c..fd0ef7f8b 100644 --- a/src/main/java/com/depromeet/domain/mission/application/MissionService.java +++ b/src/main/java/com/depromeet/domain/mission/application/MissionService.java @@ -15,6 +15,7 @@ import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; +import com.depromeet.global.util.SecurityUtil; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -35,6 +36,7 @@ public class MissionService { private final MissionRecordTtlRepository missionRecordTtlRepository; private final MemberRelationRepository memberRelationRepository; private final MemberUtil memberUtil; + private final SecurityUtil securityUtil; public MissionCreateResponse createMission(MissionCreateRequest missionCreateRequest) { Mission mission = createMissionEntity(missionCreateRequest); @@ -116,8 +118,8 @@ public MissionRecordSummaryResponse findSummaryMissionRecord() { missions.stream() .mapToLong( mission -> - Duration.between(mission.getStartedAt().minusDays(1), today) - .toDays()) + Duration.between(mission.getStartedAt(), today).toDays() + + 1) .sum(); // Duration을 초로 바꾸고 합산 long sumDuration = @@ -201,6 +203,39 @@ public MissionSymbolStackResponse findMissionSymbolStack(Long memberId) { return MissionSymbolStackResponse.of(symbolStack); } + @Transactional(readOnly = true) + public List findAllFinishedMission() { + Long currentMemberId = securityUtil.getCurrentMemberId(); + + List finishedMissions = missionRepository.findAllFinishedMission(currentMemberId); + + return finishedMissions.stream() + .map( + mission -> { + long totalMissionDay = + Duration.between( + mission.getStartedAt(), + mission.getFinishedAt()) + .toDays() + + 1; + long completeCount = + mission.getMissionRecords().stream() + .filter( + missionRecord -> + missionRecord + .getUploadStatus() + .equals( + ImageUploadStatus + .COMPLETE)) + .count(); + + return FinishedMissionResponse.of( + mission, + calculateMissionAttainRate(completeCount, totalMissionDay)); + }) + .toList(); + } + public MissionUpdateResponse updateMission( MissionUpdateRequest missionUpdateRequest, Long missionId) { Mission mission = diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java index 211e83f2f..085b4e967 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java @@ -13,4 +13,6 @@ public interface MissionRepositoryCustom { List findMissionsWithRecordsByRelations(Long memberId, boolean existsMemberRelations); void updateFinishedDurationStatus(LocalDateTime today); + + List findAllFinishedMission(Long memberId); } diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java index 8eec6bf2d..6ce7a6f29 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java @@ -13,6 +13,9 @@ import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -70,6 +73,18 @@ public void updateFinishedDurationStatus(LocalDateTime today) { .execute(); } + @Override + public List findAllFinishedMission(Long memberId) { + return jpaQueryFactory + .selectFrom(mission) + .leftJoin(mission.missionRecords, missionRecord) + .fetchJoin() + .where(memberIdEq(memberId), durationStatusFinishedEq()) + .orderBy(mission.finishedAt.desc()) + .fetch(); + } + + // 미션의 사용자 id 조건 검증 메서드 private BooleanExpression memberIdEq(Long memberId) { return memberId == null ? null : mission.member.id.eq(memberId); } @@ -83,4 +98,27 @@ private BooleanExpression visibilityByRelations(boolean existsRelations) { private BooleanExpression durationStatusInProgress() { return mission.durationStatus.in(DurationStatus.IN_PROGRESS); } + + // lastId보다 작은 미션 id 찾는 조건 메서드 (lastId 가 있다면 마지막 요청) + private BooleanExpression ltMissionId(Long lastId) { + return lastId == null ? null : mission.id.lt(lastId); + } + + private BooleanExpression durationStatusFinishedEq() { + return mission.durationStatus.eq(DurationStatus.FINISHED); + } + + // 무한 스크롤 방식 처리하는 메서드 + private Slice checkLastPage(int size, List result) { + + boolean hasNext = false; + + // 조회한 결과 개수가 요청한 페이지 사이즈보다 크면 뒤에 더 있음, next = true + if (result.size() > size) { + hasNext = true; + result.remove(size); + } + Pageable pageable = Pageable.unpaged(); + return new SliceImpl<>(result, pageable, hasNext); + } } diff --git a/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java b/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java new file mode 100644 index 000000000..4a4d59dea --- /dev/null +++ b/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java @@ -0,0 +1,43 @@ +package com.depromeet.domain.mission.dto.response; + +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.mission.domain.MissionCategory; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +public record FinishedMissionResponse( + @Schema(description = "미션 ID", defaultValue = "1") Long missionId, + @Schema(description = "미션 이름", defaultValue = "default name") String name, + @Schema(description = "미션 내용", defaultValue = "default content") String content, + @Schema(description = "미션 카테고리", defaultValue = "STUDY") MissionCategory category, + @Schema(description = "미션 달성률", defaultValue = "1.1") double missionAttainRate, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + @Schema( + description = "미션 시작 시간", + defaultValue = "2024-01-01 00:34:00", + type = "string") + LocalDateTime startedAt, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + @Schema( + description = "미션 종료 시간", + defaultValue = "2024-01-15 00:34:00", + type = "string") + LocalDateTime finishedAt) { + public static FinishedMissionResponse of(Mission mission, double missionAttainRate) { + return new FinishedMissionResponse( + mission.getId(), + mission.getName(), + mission.getContent(), + mission.getCategory(), + missionAttainRate, + mission.getStartedAt(), + mission.getFinishedAt()); + } +}