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 @@ -557,7 +557,7 @@ private void validateDateTime(LocalDateTime startDate, LocalDateTime endDate) {
}

if (!startDate.isBefore(endDate)) {
throw new CustomException(ErrorCode.PLAN_INVALID_TIME_RANGE);
throw new CustomException(ErrorCode.INVALID_TIME_RANGE);
}
}
//시간 겹침 검증 메서드 (최적화된 DB 쿼리 + 가상 인스턴스 검증 조합)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.back.domain.study.record.controller;

import com.back.domain.study.record.dto.StudyRecordRequestDto;
import com.back.domain.study.record.dto.StudyRecordResponseDto;
import com.back.domain.study.record.service.StudyRecordService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/plans/records")
public class StudyRecordController {
private final StudyRecordService studyRecordService;

// ======================= 생성 ======================
// 학습 기록 생성
@PostMapping
public ResponseEntity<RsData<StudyRecordResponseDto>> createStudyRecord(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestBody StudyRecordRequestDto request
) {
Long userId = user.getUserId();
StudyRecordResponseDto response = studyRecordService.createStudyRecord(userId, request);
return ResponseEntity.ok(RsData.success("학습 기록이 생성되었습니다.", response));
}
// ======================= 조회 ======================
// 일별 학습 기록 조회
@GetMapping
public ResponseEntity<RsData<List<StudyRecordResponseDto>>> getDailyStudyRecord(
@AuthenticationPrincipal CustomUserDetails user,
@RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date
) {
Long userId = user.getUserId();
List<StudyRecordResponseDto> response = studyRecordService.getStudyRecordsByDate(userId, date);
return ResponseEntity.ok(RsData.success("일별 학습 기록 조회 성공", response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.back.domain.study.record.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
public class StudyRecordRequestDto {
private Long planId;
private Long roomId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Long duration;
private List<PauseInfoRequestDto> pauseInfos = new ArrayList<>();

@Getter
@NoArgsConstructor
public static class PauseInfoRequestDto {
private LocalDateTime pausedAt;
private LocalDateTime restartAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.back.domain.study.record.dto;

import com.back.domain.study.record.entity.PauseInfo;
import com.back.domain.study.record.entity.StudyRecord;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor
public class StudyRecordResponseDto {
private Long id;
private Long planId;
private Long roomId;
private LocalDateTime startTime;
private List<PauseInfoResponseDto> pauseInfos = new ArrayList<>();
private LocalDateTime endTime;
private Long duration;

public static StudyRecordResponseDto from(StudyRecord studyRecord) {
StudyRecordResponseDto dto = new StudyRecordResponseDto();
dto.id = studyRecord.getId();
dto.planId = studyRecord.getStudyPlan() != null ? studyRecord.getStudyPlan().getId() : null;
dto.roomId = studyRecord.getRoom() != null ? studyRecord.getRoom().getId() : null;
dto.startTime = studyRecord.getStartTime();
dto.endTime = studyRecord.getEndTime();
dto.duration = studyRecord.getDuration();
dto.pauseInfos = studyRecord.getPauseInfos().stream()
.map(PauseInfoResponseDto::from)
.collect(Collectors.toList());
return dto;
}

@Getter
@NoArgsConstructor
public static class PauseInfoResponseDto {
private LocalDateTime pausedAt;
private LocalDateTime restartAt;

public static PauseInfoResponseDto from(PauseInfo pauseInfo) {
PauseInfoResponseDto dto = new PauseInfoResponseDto();
dto.pausedAt = pauseInfo.getPausedAt();
dto.restartAt = pauseInfo.getRestartAt();
return dto;
}
}
}
42 changes: 42 additions & 0 deletions src/main/java/com/back/domain/study/record/entity/PauseInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.back.domain.study.record.entity;


import com.back.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.Getter;

import java.time.LocalDateTime;

@Entity
@Getter
@Table(name = "pause_infos")
public class PauseInfo {
// 일시정지 정보에 생성, 수정일은 필요 없을 것 같아서
// id만 별도로 생성
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "study_record_id", nullable = false)
private StudyRecord studyRecord;

@Column(name = "paused_at", nullable = false)
private LocalDateTime pausedAt;

@Column(name = "restart_at")
private LocalDateTime restartAt;

public static PauseInfo of(LocalDateTime pausedAt, LocalDateTime restartAt) {
PauseInfo pauseInfo = new PauseInfo();
pauseInfo.pausedAt = pausedAt;
pauseInfo.restartAt = restartAt;
return pauseInfo;
}

// 양방향 연관관계 설정
void assignStudyRecord(StudyRecord studyRecord) {
this.studyRecord = studyRecord;
}

}
65 changes: 59 additions & 6 deletions src/main/java/com/back/domain/study/record/entity/StudyRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,84 @@

import com.back.domain.study.plan.entity.StudyPlan;
import com.back.domain.studyroom.entity.Room;
import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@NoArgsConstructor
@Getter
public class StudyRecord extends BaseEntity {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "plan_id")
@JoinColumn(name = "plan_id", nullable = false)
private StudyPlan studyPlan;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id")
private Room room;

private int duration;
// 초 단위
@Column(nullable = false)
private Long duration;

private LocalDateTime startTime;

@OneToMany(mappedBy = "studyRecord", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PauseInfo> pauseInfos = new ArrayList<>();

private LocalDateTime endTime;

public static StudyRecord create(User user, StudyPlan studyPlan, Room room,
LocalDateTime startTime, LocalDateTime endTime,
List<PauseInfo> pauseInfos) {
StudyRecord record = new StudyRecord();
record.user = user;
record.studyPlan = studyPlan;
record.room = room;
record.startTime = startTime;
record.endTime = endTime;

// 일시정지 정보 추가
if (pauseInfos != null && !pauseInfos.isEmpty()) {
pauseInfos.forEach(record::addPauseInfo);
}
// 총 학습 시간 계산 (검증은 서비스에서)
record.calculateDuration();

return record;
}

// 일시정지 정보 추가
private void addPauseInfo(PauseInfo pauseInfo) {
this.pauseInfos.add(pauseInfo);
pauseInfo.assignStudyRecord(this);
}

// 실제 학습 시간 계산 (전체 시간 - 일시정지 시간)
private void calculateDuration() {
// 전체 시간 계산 (초)
long totalSeconds = Duration.between(startTime, endTime).getSeconds();

// 일시정지 시간 제외
long pausedSeconds = pauseInfos.stream()
.filter(pause -> pause.getPausedAt() != null && pause.getRestartAt() != null)
.mapToLong(pause -> Duration.between(pause.getPausedAt(), pause.getRestartAt()).getSeconds())
.sum();

this.duration = totalSeconds - pausedSeconds;;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.back.domain.study.record.repository;

import com.back.domain.study.record.entity.StudyRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

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

@Repository
public interface StudyRecordRepository extends JpaRepository<StudyRecord, Long> {
// 시작 시간을 기준으로만 조회
List<StudyRecord> findByUserIdAndStartTimeBetween(
Long userId, LocalDateTime start, LocalDateTime end);

// 시작 시간과 종료 시간을 둘 다 고려하여 조회
@Query("SELECT sr FROM StudyRecord sr " +
"WHERE sr.user.id = :userId " +
"AND (" +
" (sr.startTime >= :startOfDay AND sr.startTime < :endOfDay) OR " +
" (sr.endTime > :startOfDay AND sr.endTime <= :endOfDay) OR " +
" (sr.startTime < :startOfDay AND sr.endTime > :endOfDay)" +
") " +
"ORDER BY sr.startTime DESC")
List<StudyRecord> findByUserIdAndDateRange(
@Param("userId") Long userId,
@Param("startOfDay") LocalDateTime startOfDay,
@Param("endOfDay") LocalDateTime endOfDay);
}
Loading
Loading