diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index e93132bd..c37ca1d3 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -80,6 +80,7 @@ public enum ErrorCode { //출석 관련 AT OFFLINE_ATTEND_FAIL(HttpStatus.BAD_REQUEST, "AT-101", "거리 부적합으로 인한 대면 출석 실패"), + INVALID_ATTEND_TIME(HttpStatus.BAD_REQUEST, "AT-102", "시간 입력 범위가 잘못되었습니다."), ALREADY_ATTEND(HttpStatus.CONFLICT, "AT-301", "이미 해당 타입으로 출석한 기록이 있습니다."), ATTENDANCE_CLOSED(HttpStatus.CONFLICT, "AT-401", "아직 출석 시간이 아닙니다."), diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java index 1694ee7c..2739c42f 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/entity/AttendanceRecord.java @@ -13,6 +13,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -52,34 +53,40 @@ public class AttendanceRecord extends BaseTimeEntity { @JoinColumn(name = "attendance_id") private Attendance attendance; + @Column(name = "attend_time", nullable = false) + private LocalDateTime attendTime; + private AttendanceRecord(AttendanceType attendanceType, AttendanceStatus attendanceStatus, Double locationAccuracy, - Long memberId, Attendance attendance) { + Long memberId, Attendance attendance, LocalDateTime attendTime) { this.attendanceType = attendanceType; this.attendanceStatus = attendanceStatus; this.locationAccuracy = locationAccuracy; this.memberId = memberId; this.attendance = attendance; + this.attendTime = attendTime; } - public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId, - AttendanceStatus attendanceStatus) { + public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId, AttendanceStatus attendanceStatus, + LocalDateTime attendTime) { return new AttendanceRecord( AttendanceType.ONLINE, attendanceStatus, null, memberId, - attendance + attendance, + attendTime ); } public static AttendanceRecord offlineRecord(Attendance attendance, Long memberId, Double locationAccuracy, - AttendanceStatus attendanceStatus) { + AttendanceStatus attendanceStatus, LocalDateTime attendTime) { return new AttendanceRecord( AttendanceType.OFFLINE, attendanceStatus, locationAccuracy, memberId, - attendance + attendance, + attendTime ); } @@ -90,4 +97,8 @@ public void updateAttendanceType(AttendanceType attendanceType) { public void updateLocationAccuracy(Double accuracy) { this.locationAccuracy = accuracy; } + + public void updateAttendanceStatus(AttendanceStatus attendanceStatus) { + this.attendanceStatus = attendanceStatus; + } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java b/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java index f6aec841..51390046 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/repository/AttendanceRecordRepository.java @@ -16,4 +16,6 @@ public interface AttendanceRecordRepository extends JpaRepository findByMemberIdAndAttendanceId(Long memberId, Long attendanceId); + + List findAllByAttendanceId(Long attendanceId); } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java index 98babb20..a11e87fc 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceAdminService.java @@ -14,6 +14,7 @@ import org.cotato.csquiz.domain.attendance.embedded.Location; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository; +import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.domain.generation.repository.SessionRepository; import org.springframework.stereotype.Service; @@ -32,6 +33,7 @@ public class AttendanceAdminService { @Transactional public void addAttendance(Session session, Location location, AttendanceDeadLineDto attendanceDeadLine) { + AttendanceUtil.validateAttendanceTime(attendanceDeadLine.attendanceDeadLine(), attendanceDeadLine.lateDeadLine()); Attendance attendance = Attendance.builder() .session(session) @@ -53,6 +55,8 @@ public void updateAttendance(UpdateAttendanceRequest request) { Session attendanceSession = sessionRepository.findById(attendance.getSessionId()) .orElseThrow(() -> new EntityNotFoundException("출석과 연결된 세션을 찾을 수 없습니다")); + AttendanceUtil.validateAttendanceTime(request.attendTime().attendanceDeadLine(), request.attendTime().lateDeadLine()); + if (attendanceSession.getSessionDate() == null) { throw new AppException(ErrorCode.SESSION_DATE_NOT_FOUND); } @@ -63,6 +67,8 @@ public void updateAttendance(UpdateAttendanceRequest request) { LocalDateTime.of(attendanceSession.getSessionDate(), request.attendTime() .lateDeadLine()).plusSeconds(DEFAULT_ATTEND_SECOND)); attendance.updateLocation(request.location()); + + attendanceRecordService.updateAttendanceStatus(attendance); } public List findAttendanceRecords(Long generationId, Integer month) { diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java index a0676df2..1c22b578 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceRecordService.java @@ -17,8 +17,10 @@ import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord; import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; +import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository; import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository; +import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; import org.cotato.csquiz.domain.auth.entity.Member; import org.cotato.csquiz.domain.auth.service.MemberService; import org.springframework.stereotype.Service; @@ -70,4 +72,16 @@ public AttendResponse submitRecord(AttendanceParams request, final Long memberId return requestAttendanceService.attend(request, memberId, attendance); } + + @Transactional + public void updateAttendanceStatus(Attendance attendance) { + List attendanceRecords = attendanceRecordRepository.findAllByAttendanceId(attendance.getId()); + + for (AttendanceRecord attendanceRecord : attendanceRecords) { + AttendanceStatus attendanceStatus = AttendanceUtil.calculateAttendanceStatus(attendance, attendanceRecord.getAttendTime()); + attendanceRecord.updateAttendanceStatus(attendanceStatus); + } + + attendanceRecordRepository.saveAll(attendanceRecords); + } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java index e037097a..b7121e7f 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java @@ -37,7 +37,7 @@ public AttendResponse request(AttendanceParams params, Long memberId, Attendance AttendanceRecord attendanceRecord = attendanceRecordRepository.findByMemberIdAndAttendanceId(memberId, request.getAttendanceId()) - .orElseGet(() -> AttendanceRecord.offlineRecord(attendance, memberId, accuracy, attendanceStatus)); + .orElseGet(() -> AttendanceRecord.offlineRecord(attendance, memberId, accuracy, attendanceStatus, request.getRequestTime())); attendanceRecord.updateAttendanceType(request.attendanceType()); attendanceRecord.updateLocationAccuracy(accuracy); diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java index fd6de449..50a432ea 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java @@ -26,7 +26,7 @@ public AttendanceType attendanceType() { public AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance) { AttendanceStatus attendanceStatus = AttendanceUtil.calculateAttendanceStatus(attendance, params.requestTime()); - attendanceRecordRepository.save(AttendanceRecord.onLineRecord(attendance, memberId, attendanceStatus)); + attendanceRecordRepository.save(AttendanceRecord.onLineRecord(attendance, memberId, attendanceStatus, params.requestTime())); return AttendResponse.from(attendanceStatus); } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java b/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java index 0d324363..232341ae 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java @@ -2,6 +2,8 @@ import java.time.LocalDateTime; import java.time.LocalTime; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; @@ -47,4 +49,18 @@ private static boolean isToday(Attendance attendance, LocalDateTime currentDate) private static boolean isStarted(LocalTime currentTime) { return currentTime.isAfter(DeadLine.ATTENDANCE_START_TIME.getTime()); } + + public static void validateAttendanceTime(LocalTime attendDeadLine, LocalTime lateDeadLine) { + if (!DeadLine.ATTENDANCE_START_TIME.getTime().isBefore(attendDeadLine)) { + throw new AppException(ErrorCode.INVALID_ATTEND_TIME); + } + + if (!attendDeadLine.isBefore(lateDeadLine)) { + throw new AppException(ErrorCode.INVALID_ATTEND_TIME); + } + + if (!lateDeadLine.isBefore(DeadLine.ATTENDANCE_END_TIME.getTime())) { + throw new AppException(ErrorCode.INVALID_ATTEND_TIME); + } + } } diff --git a/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java b/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java index a8e2a9cc..e1f7132c 100644 --- a/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java +++ b/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java @@ -1,17 +1,22 @@ package org.cotato.csquiz.domain.attendance.util; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDateTime; +import java.time.LocalTime; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; import org.cotato.csquiz.domain.generation.entity.Session; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class AttendanceUtilTest { @Test - void 날짜가_다르면_출석이_닫혀있다(){ + void 날짜가_다르면_출석이_닫혀있다() { //given Attendance attendance = Attendance.builder() .attendanceDeadLine(LocalDateTime.now()) @@ -48,4 +53,45 @@ class AttendanceUtilTest { //then assertEquals(attendanceStatus, AttendanceOpenStatus.CLOSED); } + + @Test + void 지각마감이_세션시작보다_빠를_수_없다() { + //given + LocalTime attendDeadline = LocalTime.of(18, 40, 0); + LocalTime lateDeadline = LocalTime.of(19, 20, 0); + + //when, then + assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) + .isInstanceOf(AppException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); + } + + @DisplayName(value = "지각마감이 출석보다 빠르면 예외를 발생한다.") + @Test + void 지각마감보다_출석마감이_빠르다() { + //given + LocalTime attendDeadline = LocalTime.of(19, 40, 0); + LocalTime lateDeadline = LocalTime.of(19, 20, 0); + + //when, then + assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) + .isInstanceOf(AppException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); + } + + @DisplayName(value = "지각 마감이 세션 종료보다 늦으면 예외를 발생한다.") + @Test + void 지각마감시간_검증_기능() { + //given + LocalTime attendDeadline = LocalTime.of(19, 40, 0); + LocalTime lateDeadline = LocalTime.of(20, 20, 0); + + //when, then + assertThatThrownBy(() -> AttendanceUtil.validateAttendanceTime(attendDeadline, lateDeadline)) + .isInstanceOf(AppException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.INVALID_ATTEND_TIME); + } } \ No newline at end of file