Skip to content
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 @@ -13,5 +13,5 @@ public class GroupDailyWorkoutBarResponseDTO {
private Long groupId;
private String groupName;
private LocalDate date;
private List<GroupMemberWorkoutBarResponseDTO> members;
private List<GroupExerciseBarSectionDTO> exercises;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.atmd.backend.domain.group.dto.response;

import com.atmd.backend.domain.fitness.enums.ExerciseType;
import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
public class GroupExerciseBarSectionDTO {

private ExerciseType type;
private String unit;
private long topValue;
private List<GroupExerciseMemberValueDTO> members;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.atmd.backend.domain.group.dto.response;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class GroupExerciseMemberValueDTO {

private Long userId;
private String nickname;
private boolean isOwner;
private long value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.atmd.backend.domain.group.dto.response;

import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;

@Getter
@Builder
public class GroupMemberDailyRecordDTO {

private LocalDate date;
private int exerciseTypeCount;
private long chairStandCount;
private long pushUpCount;
private long sitUpCount;
private long plankDurationMs;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.atmd.backend.domain.group.dto.response;

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
public class GroupMonthlyMemberSummaryDTO {

private int rank;
private Long userId;
private String nickname;
private boolean isOwner;
private double executionRate;
private int executedDays;
private int totalExerciseTypes;
private List<GroupMemberDailyRecordDTO> days;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ public class GroupMonthlyWorkoutBarResponseDTO {
private Long groupId;
private String groupName;
private YearMonth yearMonth;
private List<GroupMemberWorkoutBarResponseDTO> members;
private int daysInMonth;
private List<GroupMonthlyMemberSummaryDTO> members;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.atmd.backend.domain.group.repository;

import java.time.LocalDate;

public interface GroupDailyRecordItemProjection {
Long getUserId();
LocalDate getWorkoutDate();
Long getChairStandCount();
Long getPushUpCount();
Long getSitUpCount();
Long getPlankDurationMs();
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,18 @@ List<GroupMemberWorkoutAggregateProjection> aggregateByGroupAndDate(

@Query(value = """
SELECT
u.id AS userId,
u.nickname AS nickname,
(u.id = g.owner_id) AS isOwner,
COALESCE(SUM(r.chair_stand_count), 0) AS chairStandCount,
COALESCE(SUM(r.push_up_count), 0) AS pushUpCount,
COALESCE(SUM(r.sit_up_count), 0) AS sitUpCount,
COALESCE(SUM(r.plank_duration_ms), 0) AS plankDurationMs
FROM group_users gu
JOIN users u ON u.id = gu.user_id
JOIN groups g ON g.id = gu.group_id
LEFT JOIN group_daily_workout_records r
ON r.group_id = gu.group_id
AND r.user_id = gu.user_id
AND r.workout_date >= :startDate
AND r.workout_date < :endDateExclusive
WHERE gu.group_id = :groupId
GROUP BY u.id, u.nickname, g.owner_id
ORDER BY u.id
r.user_id AS userId,
r.workout_date AS workoutDate,
r.chair_stand_count AS chairStandCount,
r.push_up_count AS pushUpCount,
r.sit_up_count AS sitUpCount,
r.plank_duration_ms AS plankDurationMs
FROM group_daily_workout_records r
WHERE r.group_id = :groupId
AND r.workout_date >= :startDate
AND r.workout_date < :endDateExclusive
""", nativeQuery = true)
List<GroupMemberWorkoutAggregateProjection> aggregateByGroupAndDateRange(
List<GroupDailyRecordItemProjection> findItemsByGroupAndDateRange(
@Param("groupId") Long groupId,
@Param("startDate") LocalDate startDate,
@Param("endDateExclusive") LocalDate endDateExclusive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import com.atmd.backend.domain.fitness.enums.ExerciseType;
import com.atmd.backend.domain.group.dto.response.GroupDailyWorkoutBarResponseDTO;
import com.atmd.backend.domain.group.dto.response.GroupMemberWorkoutBarResponseDTO;
import com.atmd.backend.domain.group.dto.response.GroupExerciseBarSectionDTO;
import com.atmd.backend.domain.group.dto.response.GroupExerciseMemberValueDTO;
import com.atmd.backend.domain.group.dto.response.GroupMemberDailyRecordDTO;
import com.atmd.backend.domain.group.dto.response.GroupMonthlyMemberSummaryDTO;
import com.atmd.backend.domain.group.dto.response.GroupMonthlyWorkoutBarResponseDTO;
import com.atmd.backend.domain.group.entity.Group;
import com.atmd.backend.domain.group.entity.GroupUser;
import com.atmd.backend.domain.group.exception.GroupErrorCode;
import com.atmd.backend.domain.group.repository.GroupDailyRecordItemProjection;
import com.atmd.backend.domain.group.repository.GroupDailyWorkoutRecordRepository;
import com.atmd.backend.domain.group.repository.GroupMemberWorkoutAggregateProjection;
import com.atmd.backend.domain.group.repository.GroupRepository;
import com.atmd.backend.domain.group.repository.GroupUserRepository;
import com.atmd.backend.global.common.exception.GeneralException;
Expand All @@ -20,12 +25,19 @@
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.ToLongFunction;

@Service
@RequiredArgsConstructor
public class GroupWorkoutRecordService {
private static final ZoneId SERVER_ZONE = ZoneId.of("Asia/Seoul");
private static final String COUNT_UNIT = "COUNT";
private static final String MS_UNIT = "MS";

private final GroupRepository groupRepository;
private final GroupUserRepository groupUserRepository;
Expand Down Expand Up @@ -65,16 +77,22 @@ public void accumulateCompletedWorkout(
public GroupDailyWorkoutBarResponseDTO getDailyBar(Long groupId, Long viewerId, LocalDate date) {
Group group = findGroupAndCheckMembership(groupId, viewerId);
LocalDate targetDate = date != null ? date : LocalDate.now(SERVER_ZONE);
List<GroupMemberWorkoutBarResponseDTO> members = groupDailyWorkoutRecordRepository
.aggregateByGroupAndDate(groupId, targetDate)
.stream()
.map(GroupMemberWorkoutBarResponseDTO::from)
.toList();

List<GroupMemberWorkoutAggregateProjection> rows =
groupDailyWorkoutRecordRepository.aggregateByGroupAndDate(groupId, targetDate);

List<GroupExerciseBarSectionDTO> exercises = List.of(
buildExerciseSection(ExerciseType.CHAIR_STAND, COUNT_UNIT, rows, this::chairStand),
buildExerciseSection(ExerciseType.PUSH_UP, COUNT_UNIT, rows, this::pushUp),
buildExerciseSection(ExerciseType.SIT_UP, COUNT_UNIT, rows, this::sitUp),
buildExerciseSection(ExerciseType.PLANK, MS_UNIT, rows, this::plankMs)
);

return GroupDailyWorkoutBarResponseDTO.builder()
.groupId(group.getId())
.groupName(group.getName())
.date(targetDate)
.members(members)
.exercises(exercises)
.build();
}

Expand All @@ -84,19 +102,141 @@ public GroupMonthlyWorkoutBarResponseDTO getMonthlyBar(Long groupId, Long viewer
YearMonth targetMonth = yearMonth != null ? yearMonth : YearMonth.now(SERVER_ZONE);
LocalDate startDate = targetMonth.atDay(1);
LocalDate endDateExclusive = targetMonth.plusMonths(1).atDay(1);
List<GroupMemberWorkoutBarResponseDTO> members = groupDailyWorkoutRecordRepository
.aggregateByGroupAndDateRange(groupId, startDate, endDateExclusive)
.stream()
.map(GroupMemberWorkoutBarResponseDTO::from)
.toList();
int daysInMonth = targetMonth.lengthOfMonth();
Long ownerId = group.getOwner().getId();

List<GroupUser> memberships = groupUserRepository.findAllByGroupIdWithUser(groupId);
List<GroupDailyRecordItemProjection> items =
groupDailyWorkoutRecordRepository.findItemsByGroupAndDateRange(groupId, startDate, endDateExclusive);

Map<Long, List<GroupDailyRecordItemProjection>> itemsByUserId = new HashMap<>();
for (GroupDailyRecordItemProjection item : items) {
itemsByUserId.computeIfAbsent(item.getUserId(), k -> new ArrayList<>()).add(item);
}

List<GroupMonthlyMemberSummaryDTO> summaries = new ArrayList<>();
for (GroupUser membership : memberships) {
Long userId = membership.getUser().getId();
List<GroupDailyRecordItemProjection> userItems =
itemsByUserId.getOrDefault(userId, List.of());
summaries.add(buildMemberMonthlySummary(
membership, ownerId, userItems, daysInMonth));
}

summaries.sort(Comparator
.comparingDouble(GroupMonthlyMemberSummaryDTO::getExecutionRate).reversed()
.thenComparingInt(GroupMonthlyMemberSummaryDTO::getTotalExerciseTypes).reversed()
.thenComparing(GroupMonthlyMemberSummaryDTO::getUserId));

List<GroupMonthlyMemberSummaryDTO> ranked = new ArrayList<>(summaries.size());
for (int i = 0; i < summaries.size(); i++) {
GroupMonthlyMemberSummaryDTO s = summaries.get(i);
ranked.add(GroupMonthlyMemberSummaryDTO.builder()
.rank(i + 1)
.userId(s.getUserId())
.nickname(s.getNickname())
.isOwner(s.isOwner())
.executionRate(s.getExecutionRate())
.executedDays(s.getExecutedDays())
.totalExerciseTypes(s.getTotalExerciseTypes())
.days(s.getDays())
.build());
}

return GroupMonthlyWorkoutBarResponseDTO.builder()
.groupId(group.getId())
.groupName(group.getName())
.yearMonth(targetMonth)
.members(members)
.daysInMonth(daysInMonth)
.members(ranked)
.build();
}

private GroupExerciseBarSectionDTO buildExerciseSection(
ExerciseType type,
String unit,
List<GroupMemberWorkoutAggregateProjection> rows,
ToLongFunction<GroupMemberWorkoutAggregateProjection> valueFn
) {
List<GroupExerciseMemberValueDTO> values = new ArrayList<>(rows.size());
long topValue = 0L;
for (GroupMemberWorkoutAggregateProjection r : rows) {
long value = valueFn.applyAsLong(r);
if (value > topValue) topValue = value;
values.add(GroupExerciseMemberValueDTO.builder()
.userId(r.getUserId())
.nickname(r.getNickname())
.isOwner(Boolean.TRUE.equals(r.getIsOwner()))
.value(value)
.build());
}
values.sort(Comparator
.comparingLong(GroupExerciseMemberValueDTO::getValue).reversed()
.thenComparing(GroupExerciseMemberValueDTO::getUserId));
return GroupExerciseBarSectionDTO.builder()
.type(type)
.unit(unit)
.topValue(topValue)
.members(values)
.build();
}

private GroupMonthlyMemberSummaryDTO buildMemberMonthlySummary(
GroupUser membership,
Long ownerId,
List<GroupDailyRecordItemProjection> userItems,
int daysInMonth
) {
List<GroupMemberDailyRecordDTO> days = new ArrayList<>(userItems.size());
int executedDays = 0;
int totalExerciseTypes = 0;
for (GroupDailyRecordItemProjection item : userItems) {
long chair = zero(item.getChairStandCount());
long push = zero(item.getPushUpCount());
long sit = zero(item.getSitUpCount());
long plank = zero(item.getPlankDurationMs());
int typeCount = (chair > 0 ? 1 : 0) + (push > 0 ? 1 : 0)
+ (sit > 0 ? 1 : 0) + (plank > 0 ? 1 : 0);
if (typeCount == 0) continue;
executedDays++;
totalExerciseTypes += typeCount;
days.add(GroupMemberDailyRecordDTO.builder()
.date(item.getWorkoutDate())
.exerciseTypeCount(typeCount)
.chairStandCount(chair)
.pushUpCount(push)
.sitUpCount(sit)
.plankDurationMs(plank)
.build());
}
days.sort(Comparator.comparing(GroupMemberDailyRecordDTO::getDate));

double executionRate = daysInMonth == 0
? 0.0
: totalExerciseTypes * 100.0 / (daysInMonth * 4);

Long userId = membership.getUser().getId();
return GroupMonthlyMemberSummaryDTO.builder()
.rank(0)
.userId(userId)
.nickname(membership.getUser().getNickname())
.isOwner(userId.equals(ownerId))
.executionRate(round1(executionRate))
.executedDays(executedDays)
.totalExerciseTypes(totalExerciseTypes)
.days(days)
.build();
}

private long chairStand(GroupMemberWorkoutAggregateProjection r) { return zero(r.getChairStandCount()); }
private long pushUp(GroupMemberWorkoutAggregateProjection r) { return zero(r.getPushUpCount()); }
private long sitUp(GroupMemberWorkoutAggregateProjection r) { return zero(r.getSitUpCount()); }
private long plankMs(GroupMemberWorkoutAggregateProjection r) { return zero(r.getPlankDurationMs()); }

private long zero(Long v) { return v == null ? 0L : v; }

private double round1(double v) { return Math.round(v * 10.0) / 10.0; }

private Group findGroupAndCheckMembership(Long groupId, Long userId) {
Group group = groupRepository.findByIdAndIsDeletedFalse(groupId)
.orElseThrow(() -> new GeneralException(GroupErrorCode.GROUP_NOT_FOUND));
Expand Down
Loading