Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.8.0 #326

Merged
merged 3 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ public MissionSummaryListResponse findSummaryList(LocalDate date) {

List<MissionSummaryItem> result =
missions.stream()
.map(mission -> getMissionSummaryItem(mission))
.map(mission -> getMissionSummaryItem(mission, date))
.sorted(
Comparator.comparing(MissionSummaryItem::missionStatus)
.reversed()
Expand All @@ -322,12 +322,6 @@ public MissionSummaryListResponse findSummaryList(LocalDate date) {
.reversed()))
.collect(Collectors.toList());

result.sort(
Comparator.comparing(MissionSummaryItem::missionStatus)
.reversed()
.thenComparing(
Comparator.comparing(MissionSummaryItem::finishedAt).reversed()));

long missionAllCount = missions.size();
long missionCompleteCount =
result.stream()
Expand All @@ -341,13 +335,17 @@ public MissionSummaryListResponse findSummaryList(LocalDate date) {
missionAllCount, missionCompleteCount, missionNoneCount, result);
}

private static MissionSummaryItem getMissionSummaryItem(Mission mission) {
private static MissionSummaryItem getMissionSummaryItem(Mission mission, LocalDate date) {
boolean isCompleted =
mission.getMissionRecords().stream()
.anyMatch(
missionRecord ->
missionRecord.getUploadStatus()
== ImageUploadStatus.COMPLETE);
== ImageUploadStatus.COMPLETE
&& missionRecord
.getStartedAt()
.toLocalDate()
.equals(date));
return isCompleted
? MissionSummaryItem.of(mission, MissionStatus.COMPLETED)
: MissionSummaryItem.of(mission, MissionStatus.NONE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ public List<Mission> findMissionsWithRecordsByRelations(
jpaQueryFactory
.selectFrom(mission)
.leftJoin(mission.missionRecords, missionRecord)
.where(memberIdEq(memberId), visibilityByRelations(existsMemberRelations))
.where(
memberIdEq(memberId),
durationStatusInProgress(),
visibilityByRelations(existsMemberRelations))
.orderBy(mission.id.desc())
.fetchJoin();
return query.fetch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ public ResponseEntity<Void> missionRecordInProgressDelete() {
missionRecordService.deleteInProgressMissionRecord();
return ResponseEntity.ok().build();
}

@Operation(summary = "미션별 상세 통계", description = "미션별 통계로 미션에 대한 현황을 파악합니다.")
@GetMapping("/statistics/{missionId}")
public MissionStatisticsResponse missionStatistics(@PathVariable Long missionId) {
return missionRecordService.findMissionStatistics(missionId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.depromeet.domain.missionRecord.application;

import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.application.MissionService;
import com.depromeet.domain.mission.dao.MissionRepository;
import com.depromeet.domain.mission.domain.Mission;
import com.depromeet.domain.missionRecord.dao.MissionRecordRepository;
Expand All @@ -11,6 +12,7 @@
import com.depromeet.domain.missionRecord.dto.request.MissionRecordCreateRequest;
import com.depromeet.domain.missionRecord.dto.request.MissionRecordUpdateRequest;
import com.depromeet.domain.missionRecord.dto.response.*;
import com.depromeet.domain.missionRecord.dto.response.MissionStatisticsResponse;
import com.depromeet.global.error.exception.CustomException;
import com.depromeet.global.error.exception.ErrorCode;
import com.depromeet.global.util.MemberUtil;
Expand All @@ -32,6 +34,7 @@ public class MissionRecordService {
private static final int DAYS_ADJUSTMENT = 1;

private final MemberUtil memberUtil;
private final MissionService missionService;
private final MissionRepository missionRepository;
private final MissionRecordRepository missionRecordRepository;
private final MissionRecordTtlRepository missionRecordTtlRepository;
Expand Down Expand Up @@ -165,6 +168,85 @@ public void deleteInProgressMissionRecord() {
}
}

@Transactional(readOnly = true)
public MissionStatisticsResponse findMissionStatistics(Long missionId) {
final Mission mission = findMissionById(missionId);
final LocalDateTime startedAt = mission.getStartedAt();
final LocalDateTime finishedAt = mission.getFinishedAt();
final LocalDateTime today = LocalDateTime.now();

List<MissionRecord> missionRecords =
missionRecordRepository.findAllByCompletedMission(missionId);
LocalDateTime endedAt = finishedAt.isBefore(today) ? finishedAt : today;

// 달성률
double totalMissionAttainRate =
calculateMissionAttainRate(missionRecords.size(), startedAt, endedAt);

// 시간표 생성
List<FocusMissionRecordItem> timeTable = generateRecordTimeTable(missionRecords);

// 최대 연속성 계산
long maxContinuousSuccessDay =
calculateMaxContinuousSuccessDay(startedAt, finishedAt, missionRecords);

long totalSymbolStack = 0;
long sumDuration = 0;
for (FocusMissionRecordItem timeOfDay : timeTable) {
totalSymbolStack += timeOfDay.symbolStack();
sumDuration += timeOfDay.durationMinute();
}

// 전체 수행 시간 (시간)
long totalMissionHour = sumDuration / 60;

// 전체 수행 시간 (분)
long totalMissionMinute = sumDuration % 60;

return MissionStatisticsResponse.of(
totalMissionHour,
totalMissionMinute,
totalSymbolStack,
maxContinuousSuccessDay,
missionRecords.size(),
totalMissionAttainRate,
startedAt,
finishedAt,
timeTable);
}

private List<FocusMissionRecordItem> generateRecordTimeTable(
List<MissionRecord> missionRecords) {
return missionRecords.stream().map(FocusMissionRecordItem::from).toList();
}

private long calculateMaxContinuousSuccessDay(
LocalDateTime startedAt, LocalDateTime finishedAt, List<MissionRecord> missionRecords) {
long continuousSuccessDay = 1;
long maxContinuousSuccessDay = 0;
LocalDate previousDate = null;

for (MissionRecord missionRecord : missionRecords) {
LocalDate currentDate = missionRecord.getStartedAt().toLocalDate();

// startedAt과 finishedAt 사이에 있는 일자일 때만 고려
if (!(currentDate.isAfter(startedAt.toLocalDate())
&& currentDate.isBefore(finishedAt.toLocalDate()))) {
continue;
}
if (previousDate != null && currentDate.minusDays(1).isEqual(previousDate)) {
continuousSuccessDay++;
} else {
continuousSuccessDay = 1; // 연속성이 깨진 경우 초기화
}

maxContinuousSuccessDay = Math.max(continuousSuccessDay, maxContinuousSuccessDay);

previousDate = currentDate;
}
return maxContinuousSuccessDay;
}

private void validateMissionRecordDuration(Duration duration) {
if (duration.getSeconds() > 3600L) {
throw new CustomException(ErrorCode.MISSION_RECORD_DURATION_OVERBALANCE);
Expand All @@ -176,4 +258,10 @@ private void validateMissionRecordUserMismatch(Mission mission, Member member) {
throw new CustomException(ErrorCode.MISSION_RECORD_USER_MISMATCH);
}
}

private double calculateMissionAttainRate(
long completeSize, LocalDateTime startedAt, LocalDateTime endedAt) {
long totalSize = Duration.between(startedAt, endedAt).toDays() + DAYS_ADJUSTMENT;
return Math.round((double) completeSize / totalSize * 1000) / 10.0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface MissionRecordRepositoryCustom {

List<MissionRecord> findAllByMissionIdAndYearMonth(Long missionId, YearMonth yearMonth);

List<MissionRecord> findAllByCompletedMission(Long missionId);

List<FeedOneResponse> findFeedAll(List<Member> members);

List<MissionRecord> findFeedAllByMemberId(Long memberId, List<MissionVisibility> visibilities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ public List<MissionRecord> findAllByMissionIdAndYearMonth(Long missionId, YearMo
.fetch();
}

@Override
public List<MissionRecord> findAllByCompletedMission(Long missionId) {
return jpaQueryFactory
.selectFrom(missionRecord)
.where(missionIdEq(missionId), uploadStatusCompleteEq())
.orderBy(missionRecord.startedAt.asc())
.fetch();
}

@Override
public boolean isCompletedMissionExistsToday(Long missionId) {
LocalDate now = LocalDate.now();
Expand Down Expand Up @@ -119,4 +128,8 @@ private BooleanExpression monthEq(int month) {
private BooleanExpression dayEq(int day) {
return missionRecord.startedAt.dayOfMonth().eq(day);
}

private BooleanExpression uploadStatusCompleteEq() {
return missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.depromeet.domain.missionRecord.dto.response;

import com.depromeet.domain.missionRecord.domain.MissionRecord;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;

public record FocusMissionRecordItem(
@Schema(description = "번개 수", defaultValue = "3") long symbolStack,
@Schema(description = "미션 수행 시간 (Minute)", defaultValue = "34") long durationMinute,
@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 FocusMissionRecordItem from(MissionRecord record) {
return new FocusMissionRecordItem(
record.getDuration().toMinutes() / 10,
record.getDuration().toMinutes(),
record.getStartedAt(),
record.getFinishedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.depromeet.domain.missionRecord.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;

public record MissionStatisticsResponse(
@Schema(description = "수행 시간(시간)", defaultValue = "3") long totalMissionHour,
@Schema(description = "수행 시간(분)", defaultValue = "8") long totalMissionMinute,
@Schema(description = "번개 수", defaultValue = "8") long totalSymbolStack,
@Schema(description = "연속 성공일", defaultValue = "4") long continuousSuccessDay,
@Schema(description = "총 성공일", defaultValue = "9") long totalSuccessDay,
@Schema(description = "달성률", defaultValue = "20.4") double totalMissionAttainRate,
@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,
@Schema(description = "미션 수행 타임 테이블") List<FocusMissionRecordItem> timeTable) {

public static MissionStatisticsResponse of(
long totalMissionHour,
long totalMissionMinute,
long totalSymbolStack,
long continuousSuccessDay,
long totalSuccessDay,
double totalMissionAttainRate,
LocalDateTime startedAt,
LocalDateTime finishedAt,
List<FocusMissionRecordItem> timeTable) {
return new MissionStatisticsResponse(
totalMissionHour,
totalMissionMinute,
totalSymbolStack,
continuousSuccessDay,
totalSuccessDay,
totalMissionAttainRate,
startedAt,
finishedAt,
timeTable);
}
}
Loading
Loading