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 @@ -23,6 +23,7 @@
import com.atmd.backend.domain.fitness.pose.PoseFrameSmoother;
import com.atmd.backend.domain.fitness.pose.PoseFrameValidator;
import com.atmd.backend.domain.fitness.repository.ExerciseSessionRepository;
import com.atmd.backend.domain.group.service.GroupWorkoutRecordService;
import com.atmd.backend.domain.user.entity.User;
import com.atmd.backend.domain.user.repository.UserRepository;
import com.atmd.backend.global.common.exception.GeneralException;
Expand Down Expand Up @@ -58,6 +59,7 @@ public class ExerciseSessionService {
private final PushUpAnalyzer pushUpAnalyzer;
private final SitUpAnalyzer sitUpAnalyzer;
private final PlankAnalyzer plankAnalyzer;
private final GroupWorkoutRecordService groupWorkoutRecordService;

@Value("${fitness.exercise-session.idle-timeout-seconds:15}")
private long idleTimeoutSeconds;
Expand Down Expand Up @@ -349,6 +351,15 @@ private void completeRuntime(
runtime.validDurationMs(completedAt),
toLocalDateTime(completedAt)
);
if (session.getMode() == ExerciseSessionMode.WORKOUT) {
groupWorkoutRecordService.accumulateCompletedWorkout(
runtime.getUserId(),
runtime.getExerciseType(),
runtime.validCount(),
runtime.validDurationMs(completedAt),
completedAt
);
}
}
finalizeRuntimeAfterCommit(runtime);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.atmd.backend.domain.group.entity;

import com.atmd.backend.domain.user.entity.User;
import com.atmd.backend.global.common.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Getter
@Entity
@Table(
name = "group_daily_workout_records",
uniqueConstraints = @UniqueConstraint(
name = "uk_group_daily_workout_group_user_date",
columnNames = {"group_id", "user_id", "workout_date"}
)
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GroupDailyWorkoutRecord extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "group_id", nullable = false)
private Group group;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(name = "workout_date", nullable = false)
private LocalDate workoutDate;

@Column(name = "chair_stand_count", nullable = false)
private long chairStandCount;

@Column(name = "push_up_count", nullable = false)
private long pushUpCount;

@Column(name = "sit_up_count", nullable = false)
private long sitUpCount;

@Column(name = "plank_duration_ms", nullable = false)
private long plankDurationMs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.atmd.backend.domain.group.repository;

import com.atmd.backend.domain.group.entity.GroupDailyWorkoutRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDate;

public interface GroupDailyWorkoutRecordRepository extends JpaRepository<GroupDailyWorkoutRecord, Long> {

@Modifying
@Query(value = """
INSERT INTO group_daily_workout_records (
group_id, user_id, workout_date,
chair_stand_count, push_up_count, sit_up_count, plank_duration_ms,
created_at, updated_at
) VALUES (
:groupId, :userId, :workoutDate,
:chairStandCount, :pushUpCount, :sitUpCount, :plankDurationMs,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (group_id, user_id, workout_date)
DO UPDATE SET
chair_stand_count = group_daily_workout_records.chair_stand_count + EXCLUDED.chair_stand_count,
push_up_count = group_daily_workout_records.push_up_count + EXCLUDED.push_up_count,
sit_up_count = group_daily_workout_records.sit_up_count + EXCLUDED.sit_up_count,
plank_duration_ms = group_daily_workout_records.plank_duration_ms + EXCLUDED.plank_duration_ms,
updated_at = CURRENT_TIMESTAMP
""", nativeQuery = true)
void accumulate(
@Param("groupId") Long groupId,
@Param("userId") Long userId,
@Param("workoutDate") LocalDate workoutDate,
@Param("chairStandCount") long chairStandCount,
@Param("pushUpCount") long pushUpCount,
@Param("sitUpCount") long sitUpCount,
@Param("plankDurationMs") long plankDurationMs
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.atmd.backend.domain.group.service;

import com.atmd.backend.domain.fitness.enums.ExerciseType;
import com.atmd.backend.domain.group.entity.GroupUser;
import com.atmd.backend.domain.group.repository.GroupDailyWorkoutRecordRepository;
import com.atmd.backend.domain.group.repository.GroupUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;

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

private final GroupUserRepository groupUserRepository;
private final GroupDailyWorkoutRecordRepository groupDailyWorkoutRecordRepository;

public void accumulateCompletedWorkout(
Long userId,
ExerciseType exerciseType,
int validCount,
long validDurationMs,
Instant completedAt
) {
LocalDateTime completedDateTime = LocalDateTime.ofInstant(completedAt, SERVER_ZONE);
LocalDate workoutDate = completedDateTime.toLocalDate();
long chairStandCount = exerciseType == ExerciseType.CHAIR_STAND ? validCount : 0;
long pushUpCount = exerciseType == ExerciseType.PUSH_UP ? validCount : 0;
long sitUpCount = exerciseType == ExerciseType.SIT_UP ? validCount : 0;
long plankDurationMs = exerciseType == ExerciseType.PLANK ? validDurationMs : 0;

for (GroupUser membership : groupUserRepository.findAllByUserIdWithGroup(userId)) {
if (membership.getCreatedAt().isAfter(completedDateTime)) {
continue;
}
groupDailyWorkoutRecordRepository.accumulate(
membership.getGroup().getId(),
userId,
workoutDate,
chairStandCount,
pushUpCount,
sitUpCount,
plankDurationMs
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.atmd.backend.domain.fitness.repository.ExerciseSessionRepository;
import com.atmd.backend.domain.user.repository.UserRepository;
import com.atmd.backend.domain.user.entity.User;
import com.atmd.backend.domain.group.service.GroupWorkoutRecordService;
import com.atmd.backend.global.common.exception.GeneralException;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -73,7 +74,8 @@ private ExerciseSessionService service(
mock(ChairStandAnalyzer.class),
mock(PushUpAnalyzer.class),
mock(SitUpAnalyzer.class),
mock(PlankAnalyzer.class)
mock(PlankAnalyzer.class),
mock(GroupWorkoutRecordService.class)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.atmd.backend.domain.group.service;

import com.atmd.backend.domain.fitness.enums.ExerciseType;
import com.atmd.backend.domain.group.entity.Group;
import com.atmd.backend.domain.group.entity.GroupUser;
import com.atmd.backend.domain.group.repository.GroupDailyWorkoutRecordRepository;
import com.atmd.backend.domain.group.repository.GroupUserRepository;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class GroupWorkoutRecordServiceTest {
private final GroupUserRepository groupUserRepository = mock(GroupUserRepository.class);
private final GroupDailyWorkoutRecordRepository recordRepository =
mock(GroupDailyWorkoutRecordRepository.class);
private final GroupWorkoutRecordService service =
new GroupWorkoutRecordService(groupUserRepository, recordRepository);

@Test
void accumulatesSitUpCountForMembershipCreatedBeforeWorkout() {
GroupUser membership = membership(10L, LocalDateTime.of(2026, 9, 12, 8, 0));
when(groupUserRepository.findAllByUserIdWithGroup(1L)).thenReturn(List.of(membership));

service.accumulateCompletedWorkout(
1L, ExerciseType.SIT_UP, 6, 0,
Instant.parse("2026-09-12T01:00:00Z")
);

verify(recordRepository).accumulate(
10L, 1L, LocalDate.of(2026, 9, 12), 0, 0, 6, 0
);
}

@Test
void ignoresWorkoutCompletedBeforeMembership() {
GroupUser membership = membership(10L, LocalDateTime.of(2026, 9, 12, 11, 0));
when(groupUserRepository.findAllByUserIdWithGroup(1L)).thenReturn(List.of(membership));

service.accumulateCompletedWorkout(
1L, ExerciseType.SIT_UP, 6, 0,
Instant.parse("2026-09-12T01:00:00Z")
);

verify(recordRepository, never()).accumulate(
10L, 1L, LocalDate.of(2026, 9, 12), 0, 0, 6, 0
);
}

private GroupUser membership(Long groupId, LocalDateTime joinedAt) {
Group group = mock(Group.class);
when(group.getId()).thenReturn(groupId);
GroupUser membership = mock(GroupUser.class);
when(membership.getGroup()).thenReturn(group);
when(membership.getCreatedAt()).thenReturn(joinedAt);
return membership;
}
}
Loading