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

[Feature] 몰입도 기록 API 구현 #80

Merged
merged 9 commits into from
Aug 14, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ public enum ErrorStatus implements BaseErrorCode {
// 할 일 관련
NO_TODO_FOUND(HttpStatus.NOT_FOUND, "TODO4040", "해당 할 일을 찾지 못하였습니다."),
MOVED_SUBJECT_RESTRICTION(HttpStatus.BAD_REQUEST, "TODO4040", "보관함에 이동된 항목은 삭제 및 조회만 가능합니다."),
NOT_TODO_OWNER(HttpStatus.BAD_REQUEST, "TODO4009", "해당 회원의 할 일이 아닙니다.");
NOT_TODO_OWNER(HttpStatus.BAD_REQUEST, "TODO4009", "해당 회원의 할 일이 아닙니다."),

// 몰입도 관련


NOT_STUDY_TIME_OWNER(HttpStatus.BAD_REQUEST, "STUDY_TIME4001", "해당 회원의 몰입도 기록이 아닙니다."),
OVERLAP_STUDY_TIME(HttpStatus.BAD_REQUEST, "STUDY_TIME4002", "겹치는 기록 시간이 존재합니다."),
NOT_POSSIBLE_STUDY_TIME(HttpStatus.BAD_REQUEST, "STUDY_TIME4003", "시간 설정이 올바르지 않습니다."),
NO_STUDY_TIME_FOUND(HttpStatus.NOT_FOUND, "STUDY_TIME4040", "해당 몰입도 기록을 찾지 못하였습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package timify.com.common.apiPayload.exception.handler;

import timify.com.common.apiPayload.code.BaseErrorCode;
import timify.com.common.apiPayload.exception.GeneralException;

public class StudyTimeHandler extends GeneralException {
public StudyTimeHandler(BaseErrorCode code) {
super(code);
}
}
4 changes: 2 additions & 2 deletions src/main/java/timify/com/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import timify.com.domain.MemberMission;
import timify.com.studytime.domain.StudyTime;
import timify.com.todo.domain.Todo;
import timify.com.domain.common.BaseDateTimeEntity;
import timify.com.study.domain.StudyMethod;
import timify.com.study.domain.StudyPlace;
import timify.com.study.domain.StudyType;
import timify.com.studytime.domain.StudyTime;
import timify.com.subject.domain.Subject;
import timify.com.todo.domain.Todo;

@Entity
@Getter
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/timify/com/studytime/StudyTimeConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package timify.com.studytime;

import static timify.com.utils.DateTimeUtil.stringToLocalTime;

import java.time.Duration;
import timify.com.studytime.domain.StudyTime;
import timify.com.studytime.dto.StudyTimeRequest.studyTimeRequest;
import timify.com.studytime.dto.StudyTimeResponse;

public class StudyTimeConverter {

public static StudyTime toStudyTime(studyTimeRequest request, double temp) {

return StudyTime.builder()
.startTime(stringToLocalTime(request.getStartTime()))
.endTime(stringToLocalTime(request.getEndTime()))
.grade(request.getGrade())
.temp(temp)
.build();

}

public static StudyTimeResponse.studyTimeDto toStudyTimeDto(StudyTime studyTime) {
return StudyTimeResponse.studyTimeDto.builder()
.studyTimeId(studyTime.getId())
.startTime(studyTime.getStartTime())
.endTime(studyTime.getEndTime())
.totalTime((int) Duration.between(studyTime.getStartTime(), studyTime.getEndTime()).toMinutes())
.temp(studyTime.getTemp())
.grade(studyTime.getGrade())
.build();
}

}
257 changes: 257 additions & 0 deletions src/main/java/timify/com/studytime/StudyTimeService.java
BaekJaehyuk marked this conversation as resolved.
Show resolved Hide resolved
BaekJaehyuk marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package timify.com.studytime;

import static timify.com.common.apiPayload.code.status.ErrorStatus.NOT_POSSIBLE_STUDY_TIME;
import static timify.com.common.apiPayload.code.status.ErrorStatus.NOT_STUDY_TIME_OWNER;
import static timify.com.common.apiPayload.code.status.ErrorStatus.NOT_TODO_OWNER;
import static timify.com.common.apiPayload.code.status.ErrorStatus.NO_TODO_FOUND;
import static timify.com.common.apiPayload.code.status.ErrorStatus.OVERLAP_STUDY_TIME;
import static timify.com.utils.DateTimeUtil.stringToLocalTime;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import timify.com.common.apiPayload.exception.handler.StudyHandler;
import timify.com.common.apiPayload.exception.handler.StudyTimeHandler;
import timify.com.common.apiPayload.exception.handler.TodoHandler;
import timify.com.member.domain.Member;
import timify.com.studytime.domain.StudyTime;
import timify.com.studytime.domain.StudyTimeGrade;
import timify.com.studytime.dto.StudyTimeRequest.studyTimeRequest;
import timify.com.studytime.repository.StudyTimeRepository;
import timify.com.todo.domain.Todo;
import timify.com.todo.repository.TodoRepository;

@Service
@RequiredArgsConstructor
public class StudyTimeService {

private final static double DEFAULT_TEMP = 50d;
private final StudyTimeRepository studyTimeRepository;
private final TodoRepository todoRepository;

@Transactional
public List<StudyTime> recordStudyTime(Member member, Long todoId, studyTimeRequest request) {
Todo todo = validateTodoOwner(member, todoId);

LocalDateTime startTime = stringToLocalTime(request.getStartTime());
LocalDateTime endTime = stringToLocalTime(request.getEndTime());

validateStudyTimeRange(todo, startTime, endTime);

// if (isInvalidStudyTime(startTime, endTime)) {
// throw new StudyTimeHandler(NOT_POSSIBLE_STUDY_TIME);
// }

List<StudyTime> overlappingTimes = studyTimeRepository
.findByMemberAndStartTimeLessThanEqualAndEndTimeGreaterThanEqual(member, endTime,
startTime);

if (!overlappingTimes.isEmpty()) {
throw new StudyTimeHandler(OVERLAP_STUDY_TIME);
}

List<StudyTime> studyTimes = new ArrayList<>();

// 4 AM 기준 시간 설정
LocalTime boundaryTime = LocalTime.of(4, 0);
LocalDateTime boundaryDateTime = LocalDateTime.of(startTime.toLocalDate(), boundaryTime);

// 4 AM 넘는 경우
if (isCrossingBoundary(startTime, endTime, boundaryDateTime)) {
studyTimes.add(saveStudyTimePart(member, todo, startTime, boundaryDateTime, request));
studyTimes.add(
saveStudyTimePartForNextDay(member, todo, boundaryDateTime, endTime, request));
return studyTimes;
}

studyTimes.add(saveStudyTime(member, todo, startTime, endTime, request));
return studyTimes;
}

@Transactional
public void deleteStudyTime(Member member, Long studyTimeId) {

StudyTime studyTime = validateStudyTimeOwner(member, studyTimeId);

if (studyTime.getTodo().getStudyTimeList().size() == 1) {
StudyTime firstStudyTime = studyTimeRepository.findByTodoOrderByStartTimeAsc(
studyTime.getTodo()).get(0);
double newTemp = firstStudyTime.getTemp() + DEFAULT_TEMP;
firstStudyTime.updateTemp(newTemp);
}

studyTime.disassociateMember(member);
studyTime.disassociateSubject(studyTime.getTodo().getSubject());
studyTime.disassociateTodo(studyTime.getTodo());
studyTimeRepository.delete(studyTime);
}

@Transactional(readOnly = true)
public List<StudyTime> getStudyTimes(Member member, Long todoId) {
return studyTimeRepository.findByMemberAndTodoId(member, todoId);
}

@Transactional
public List<StudyTime> updateStudyTime(Member member, Long studyTimeId,
studyTimeRequest request) {

StudyTime studyTime = validateStudyTimeOwner(member, studyTimeId);

LocalDateTime startTime = stringToLocalTime(request.getStartTime());
LocalDateTime endTime = stringToLocalTime(request.getEndTime());

validateStudyTimeRange(studyTime.getTodo(), startTime, endTime);

if (isInvalidStudyTime(startTime, endTime)) {
throw new StudyTimeHandler(NOT_POSSIBLE_STUDY_TIME);
}

studyTimeRepository.delete(studyTime);

List<StudyTime> overlappingTimes = studyTimeRepository
.findByMemberAndStartTimeLessThanEqualAndEndTimeGreaterThanEqual(member, endTime,
startTime);

if (!overlappingTimes.isEmpty()) {
throw new StudyTimeHandler(OVERLAP_STUDY_TIME);
}

List<StudyTime> studyTimes = new ArrayList<>();

// 4 AM 기준
LocalTime boundaryTime = LocalTime.of(4, 0);
LocalDateTime boundaryDateTime = LocalDateTime.of(startTime.toLocalDate(), boundaryTime);

// 4 AM 넘는 경우
if (isCrossingBoundary(startTime, endTime, boundaryDateTime)) {
studyTimes.add(
saveStudyTimePart(member, studyTime.getTodo(), startTime, boundaryDateTime,
request));
studyTimes.add(
saveStudyTimePartForNextDay(member, studyTime.getTodo(), boundaryDateTime, endTime,
request));
return studyTimes;
}

studyTimes.add(saveStudyTime(member, studyTime.getTodo(), startTime, endTime, request));
BaekJaehyuk marked this conversation as resolved.
Show resolved Hide resolved
return studyTimes;
}

private void validateStudyTimeRange(Todo todo, LocalDateTime startTime, LocalDateTime endTime) {
LocalDate todoDate = todo.getDate();

LocalDateTime startOfDay = LocalDateTime.of(todoDate, LocalTime.of(4, 0));
LocalDateTime endOfDay = LocalDateTime.of(todoDate.plusDays(2), LocalTime.of(4, 0));

if (startTime.isBefore(startOfDay) || endTime.isAfter(endOfDay)) {
throw new StudyTimeHandler(NOT_POSSIBLE_STUDY_TIME);
}
}

private boolean isInvalidStudyTime(LocalDateTime startTime, LocalDateTime endTime) {
LocalDateTime now = LocalDateTime.now();

return endTime.isBefore(startTime) || (startTime.isAfter(now) || endTime.isAfter(now));
}

private boolean isCrossingBoundary(LocalDateTime startTime, LocalDateTime endTime,
LocalDateTime boundaryDateTime) {
return endTime.isAfter(boundaryDateTime) && startTime.isBefore(boundaryDateTime);
}

private StudyTime saveStudyTimePart(Member member, Todo todo, LocalDateTime startTime,
LocalDateTime boundaryDateTime, studyTimeRequest request) {
StudyTime studyTime = StudyTimeConverter.toStudyTime(
new studyTimeRequest(request.getStartTime(),
boundaryDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")),
request.getGrade()),
calculateTemp(todo, startTime, boundaryDateTime, request.getGrade()));
studyTime.associateMember(member);
studyTime.associateSubject(todo.getSubject());
studyTime.associateTodo(todo);
return studyTimeRepository.save(studyTime);
}

private StudyTime saveStudyTimePartForNextDay(Member member, Todo todo,
LocalDateTime boundaryDateTime, LocalDateTime endTime, studyTimeRequest request) {

Todo nextDayTodo = Todo.builder()
.content(todo.getContent())
.date(boundaryDateTime.toLocalDate())
.studyType(todo.getStudyType())
.studyMethod(todo.getStudyMethod())
.studyPlace(todo.getStudyPlace())
.status(todo.getStatus())
.studyTimeList(new ArrayList<>())
.build();

nextDayTodo.associateMember(todo.getMember());
nextDayTodo.associateSubject(todo.getSubject());
nextDayTodo = todoRepository.save(nextDayTodo);

StudyTime studyTime = StudyTimeConverter.toStudyTime(
new studyTimeRequest(
boundaryDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")),
request.getEndTime(), request.getGrade()),
calculateTemp(nextDayTodo, boundaryDateTime, endTime, request.getGrade())
);

studyTime.associateMember(member);
studyTime.associateSubject(nextDayTodo.getSubject());
studyTime.associateTodo(nextDayTodo);
return studyTimeRepository.save(studyTime);
}

private StudyTime saveStudyTime(Member member, Todo todo, LocalDateTime startTime,
LocalDateTime endTime, studyTimeRequest request) {
StudyTime studyTime = StudyTimeConverter.toStudyTime(request,
calculateTemp(todo, startTime, endTime, request.getGrade()));

studyTime.associateMember(member);
studyTime.associateSubject(todo.getSubject());
studyTime.associateTodo(todo);
return studyTimeRepository.save(studyTime);
}

private double calculateTemp(Todo todo, LocalDateTime startTime, LocalDateTime endTime,
StudyTimeGrade grade) {

int minutes = (int) Duration.between(startTime, endTime).toMinutes();
long countStudyTime = todo.getStudyTimeList().size();

if (countStudyTime == 0) {
return DEFAULT_TEMP + (minutes * grade.getScore());
}

return minutes * grade.getScore();
}

private StudyTime validateStudyTimeOwner(Member member, Long studyTimeId) {
StudyTime studyTime = studyTimeRepository.findById(studyTimeId)
.orElseThrow(() -> new StudyTimeHandler(NOT_STUDY_TIME_OWNER));

if (!studyTime.getMember().equals(member)) {
throw new StudyHandler(NOT_TODO_OWNER);
}

return studyTime;
}

private Todo validateTodoOwner(Member member, Long todoId) {
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new TodoHandler(NO_TODO_FOUND));

if (!todo.getMember().equals(member)) {
throw new TodoHandler(NOT_TODO_OWNER);
}

return todo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package timify.com.studytime.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import timify.com.auth.annotation.AuthMember;
import timify.com.common.apiPayload.ApiResponse;
import timify.com.member.domain.Member;
import timify.com.studytime.dto.StudyTimeRequest.studyTimeRequest;
import timify.com.studytime.dto.StudyTimeResponse.studyTimeDto;

@Tag(name = "StudyTime", description = "StudyTime 관련 API")
public interface StudyTimeController {

@Operation(summary = "몰입시간 기록 API", description = "몰입시간 기록하는 API 입니다. "
+ "등록할 시간은 yyyyMMddHHmm 형식, Grade에는 EXCELLENT, GOOD, AVERAGE, BELOW_AVERAGE, POOR 중 하나를 입력해 주세요. )")
@Parameters(value = {
@Parameter(name = "todoId", description = "몰입도를 기록할 할 일에 해당하는 todoId 을 입력해 주세요.")
})
ApiResponse<List<studyTimeDto>> recordStudyTime(@AuthMember Member member,
@PathVariable Long todoId,
@RequestBody studyTimeRequest request);

@Operation(summary = "시간 기록 삭제 API", description = "시간 기록 삭제 API 입니다.")
@Parameters(value = {
@Parameter(name = "studyTimeId", description = "기록을 삭제할 StudyTimeId 을 입력해 주세요.")
})
ApiResponse<String> deleteStudyTime(@AuthMember Member member,
@PathVariable Long studyTimeId);

@Operation(summary = "몰입 시간 조회 API", description = "몰입 시간을 조회하는 API 입니다.")
@Parameters(value = {
@Parameter(name = "todoId", description = "시간 기록을 조회할 할 일에 해당하는 todoId 을 입력해 주세요.")
})
ApiResponse<List<studyTimeDto>> getStudyTimes(@AuthMember Member member,
@PathVariable Long todoId);

@Operation(summary = "몰입 시간 수정 API", description = "몰입 시간을 수정하는 API 입니다.")
@Parameters(value = {
@Parameter(name = "studyTimeId", description = "몰입 시간을 수정할 studyTimeId 을 입력해 주세요.")
})
ApiResponse<List<studyTimeDto>> updateStudyTime(@AuthMember Member member,
@PathVariable Long studyTimeId,
@RequestBody studyTimeRequest request);

}
Loading