diff --git a/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceController.java b/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceController.java index 6e750a67..245a4e9f 100644 --- a/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceController.java +++ b/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceController.java @@ -1,6 +1,7 @@ package org.cotato.csquiz.api.attendance.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; @@ -8,16 +9,22 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.attendance.dto.AttendResponse; import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse; import org.cotato.csquiz.api.attendance.dto.AttendancesResponse; +import org.cotato.csquiz.api.attendance.dto.OfflineAttendanceRequest; +import org.cotato.csquiz.api.attendance.dto.OnlineAttendanceRequest; import org.cotato.csquiz.api.attendance.dto.UpdateAttendanceRequest; import org.cotato.csquiz.domain.attendance.service.AttendanceAdminService; +import org.cotato.csquiz.domain.attendance.service.AttendanceRecordService; import org.cotato.csquiz.domain.attendance.service.AttendanceService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -33,6 +40,7 @@ public class AttendanceController { private final AttendanceAdminService attendanceAdminService; private final AttendanceService attendanceService; + private final AttendanceRecordService attendanceRecordService; @Operation(summary = "출석 정보 변경 API") @PatchMapping @@ -62,4 +70,47 @@ public ResponseEntity> findAttendanceRecordsByAtt public ResponseEntity findAttendancesByGeneration(@RequestParam("generationId") Long generationId) { return ResponseEntity.ok().body(attendanceService.findAttendancesByGenerationId(generationId)); } + + @Operation(summary = "대면 출결 입력 API", + responses = { + @ApiResponse( + responseCode = "200", + description = "성공" + ), + @ApiResponse( + responseCode = "AT-301", + description = "이미 출석을 완료함" + ), + @ApiResponse( + responseCode = "AT-401", + description = "출석 시간이 아님" + ) + } + ) + @PostMapping(value = "/records/offline") + public ResponseEntity submitOfflineAttendanceRecord(@RequestBody OfflineAttendanceRequest request, + @AuthenticationPrincipal Long memberId) { + return ResponseEntity.ok().body(attendanceRecordService.submitRecord(request, memberId)); + } + + @Operation(summary = "비대면 출결 입력 API", + responses = { + @ApiResponse( + responseCode = "200", + description = "성공" + ), + @ApiResponse( + responseCode = "AT-301", + description = "이미 출석을 완료함" + ), + @ApiResponse( + responseCode = "AT-401", + description = "출석 시간이 아님" + ) + }) + @PostMapping(value = "/records/online") + public ResponseEntity submitOnlineAttendanceRecord(@RequestBody OnlineAttendanceRequest request, + @AuthenticationPrincipal Long memberId) { + return ResponseEntity.ok().body(attendanceRecordService.submitRecord(request, memberId)); + } } diff --git a/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceMemberController.java b/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceMemberController.java deleted file mode 100644 index 6f939bc2..00000000 --- a/src/main/java/org/cotato/csquiz/api/attendance/controller/AttendanceMemberController.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cotato.csquiz.api.attendance.controller; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequiredArgsConstructor -@RequestMapping("/v2/api/attendances/member") -public class AttendanceMemberController { -} diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendResponse.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendResponse.java new file mode 100644 index 00000000..e9095f81 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendResponse.java @@ -0,0 +1,15 @@ +package org.cotato.csquiz.api.attendance.dto; + +import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; + +public record AttendResponse( + AttendanceStatus status, + String message +) { + public static AttendResponse from(AttendanceStatus status) { + return new AttendResponse( + status, + status.getMessage() + ); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendanceParams.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendanceParams.java new file mode 100644 index 00000000..72c2b344 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/AttendanceParams.java @@ -0,0 +1,13 @@ +package org.cotato.csquiz.api.attendance.dto; + +import java.time.LocalDateTime; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; + +public interface AttendanceParams { + + AttendanceType attendanceType(); + + Long attendanceId(); + + LocalDateTime requestTime(); +} diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/OfflineAttendanceRequest.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/OfflineAttendanceRequest.java new file mode 100644 index 00000000..33221565 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/OfflineAttendanceRequest.java @@ -0,0 +1,40 @@ +package org.cotato.csquiz.api.attendance.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.cotato.csquiz.domain.attendance.embedded.Location; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; + +@Getter +@NoArgsConstructor +public class OfflineAttendanceRequest implements AttendanceParams { + + @Schema(description = "출석 PK") + @NotNull + private Long attendanceId; + + @Schema(description = "회원 요청 시간") + private LocalDateTime requestTime; + + @Schema(description = "사용자 요청 위치") + @NotNull + private Location location; + + @Override + public AttendanceType attendanceType() { + return AttendanceType.OFFLINE; + } + + @Override + public Long attendanceId() { + return attendanceId; + } + + @Override + public LocalDateTime requestTime() { + return requestTime; + } +} diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/OnlineAttendanceRequest.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/OnlineAttendanceRequest.java new file mode 100644 index 00000000..4de9ea4f --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/OnlineAttendanceRequest.java @@ -0,0 +1,34 @@ +package org.cotato.csquiz.api.attendance.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Getter; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; + +@Getter +@NotNull +public class OnlineAttendanceRequest implements AttendanceParams { + + @Schema(description = "출석 PK") + @NotNull + private Long attendanceId; + + @Schema(description = "회원 요청 시간") + private LocalDateTime requestTime; + + @Override + public AttendanceType attendanceType() { + return AttendanceType.ONLINE; + } + + @Override + public Long attendanceId() { + return attendanceId; + } + + @Override + public LocalDateTime requestTime() { + return requestTime; + } +} diff --git a/src/main/java/org/cotato/csquiz/api/attendance/dto/UpdateAttendanceRequest.java b/src/main/java/org/cotato/csquiz/api/attendance/dto/UpdateAttendanceRequest.java index 10a71e0a..6c2c498f 100644 --- a/src/main/java/org/cotato/csquiz/api/attendance/dto/UpdateAttendanceRequest.java +++ b/src/main/java/org/cotato/csquiz/api/attendance/dto/UpdateAttendanceRequest.java @@ -11,12 +11,12 @@ public record UpdateAttendanceRequest( @NotNull Long attendanceId, Location location, - AttendanceDeadLineDto attendanceDeadLine + AttendanceDeadLineDto attendTime ) { public UpdateAttendanceRequest { - if (Objects.isNull(attendanceDeadLine)) { - attendanceDeadLine = new AttendanceDeadLineDto(DEFAULT_ATTENDANCE_DEADLINE.getTime(), + if (Objects.isNull(attendTime)) { + attendTime = new AttendanceDeadLineDto(DEFAULT_ATTENDANCE_DEADLINE.getTime(), DEFAULT_LATE_DEADLINE.getTime()); } } 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 f94dc5b4..e93132bd 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -77,6 +77,12 @@ public enum ErrorCode { LAST_QUIZ_SCORER_NOT_EXIST(HttpStatus.CONFLICT, "W-201", "아직 마지막 문제 득점자가 없습니다"), WINNER_EXIST(HttpStatus.CONFLICT, "W-301", "이미 우승자가 존재합니다"), + + //출석 관련 AT + OFFLINE_ATTEND_FAIL(HttpStatus.BAD_REQUEST, "AT-101", "거리 부적합으로 인한 대면 출석 실패"), + ALREADY_ATTEND(HttpStatus.CONFLICT, "AT-301", "이미 해당 타입으로 출석한 기록이 있습니다."), + ATTENDANCE_CLOSED(HttpStatus.CONFLICT, "AT-401", "아직 출석 시간이 아닙니다."), + // 500 오류 -> 서버측에서 처리가 실패한 부분들 WEBSOCKET_SEND_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "소캣 메세지 전송 실패"), IMAGE_PROCESSING_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-002", "이미지 처리에 실패했습니다."), diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java b/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java index 14942f34..d693b503 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/embedded/Location.java @@ -19,4 +19,8 @@ private Location(Double latitude, Double longitude) { this.latitude = latitude; this.longitude = longitude; } + + public Double calculateAccuracy(Location location) { + return Math.pow(this.latitude - location.latitude, 2) + Math.pow(this.longitude - location.longitude, 2); + } } 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 6304edda..1694ee7c 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 @@ -8,9 +8,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; 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; @@ -18,7 +20,10 @@ import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; import org.cotato.csquiz.domain.attendance.enums.AttendanceType; -@Table(name = "attendance_record") +@Table(name = "attendance_record", + indexes = {@Index(name = "member_id_index", columnList = "member_id")}, + uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "attendance_id"})} +) @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -46,4 +51,43 @@ public class AttendanceRecord extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "attendance_id") private Attendance attendance; + + private AttendanceRecord(AttendanceType attendanceType, AttendanceStatus attendanceStatus, Double locationAccuracy, + Long memberId, Attendance attendance) { + this.attendanceType = attendanceType; + this.attendanceStatus = attendanceStatus; + this.locationAccuracy = locationAccuracy; + this.memberId = memberId; + this.attendance = attendance; + } + + public static AttendanceRecord onLineRecord(Attendance attendance, Long memberId, + AttendanceStatus attendanceStatus) { + return new AttendanceRecord( + AttendanceType.ONLINE, + attendanceStatus, + null, + memberId, + attendance + ); + } + + public static AttendanceRecord offlineRecord(Attendance attendance, Long memberId, Double locationAccuracy, + AttendanceStatus attendanceStatus) { + return new AttendanceRecord( + AttendanceType.OFFLINE, + attendanceStatus, + locationAccuracy, + memberId, + attendance + ); + } + + public void updateAttendanceType(AttendanceType attendanceType) { + this.attendanceType = attendanceType; + } + + public void updateLocationAccuracy(Double accuracy) { + this.locationAccuracy = accuracy; + } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceStatus.java b/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceStatus.java index 60c7afe2..a32875e4 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceStatus.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/enums/AttendanceStatus.java @@ -6,9 +6,11 @@ @Getter @AllArgsConstructor public enum AttendanceStatus { - PRESENT("출석"), - LATE("지각"), + PRESENT("출석", "출석에 성공하셨습니다."), + LATE("지각", "기준 시간을 지나 지각 처리 되었습니다."), + ABSENT("결석", "지각 마감 시간을 지나 결석 처리 되었습니다.") ; private final String description; + private final String message; } 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 c9cfc874..f6aec841 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 @@ -1,8 +1,10 @@ package org.cotato.csquiz.domain.attendance.repository; import java.util.List; +import java.util.Optional; import org.cotato.csquiz.domain.attendance.entity.Attendance; import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,4 +12,8 @@ public interface AttendanceRecordRepository extends JpaRepository { @Query("select a from AttendanceRecord a where a.attendance in :attendances") List findAllByAttendanceIdsInQuery(@Param("attendances") List attendances); + + boolean existsByAttendanceIdAndMemberIdAndAttendanceType(Long attendanceId, Long memberId, AttendanceType attendanceType); + + Optional findByMemberIdAndAttendanceId(Long memberId, Long attendanceId); } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java new file mode 100644 index 00000000..d304dea9 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendClient.java @@ -0,0 +1,12 @@ +package org.cotato.csquiz.domain.attendance.service; + +import org.cotato.csquiz.api.attendance.dto.AttendResponse; +import org.cotato.csquiz.api.attendance.dto.AttendanceParams; +import org.cotato.csquiz.domain.attendance.entity.Attendance; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; + +public interface AttendClient { + AttendanceType attendanceType(); + + AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance); +} 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 6cb0ab58..98babb20 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 @@ -25,6 +25,7 @@ @Slf4j public class AttendanceAdminService { + private static final int DEFAULT_ATTEND_SECOND = 59; private final AttendanceRepository attendanceRepository; private final AttendanceRecordService attendanceRecordService; private final SessionRepository sessionRepository; @@ -35,8 +36,10 @@ public void addAttendance(Session session, Location location, AttendanceDeadLine Attendance attendance = Attendance.builder() .session(session) .location(location) - .attendanceDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.attendanceDeadLine())) - .lateDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.lateDeadLine())) + .attendanceDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.attendanceDeadLine()) + .plusSeconds(DEFAULT_ATTEND_SECOND)) + .lateDeadLine(LocalDateTime.of(session.getSessionDate(), attendanceDeadLine.lateDeadLine()) + .plusSeconds(DEFAULT_ATTEND_SECOND)) .build(); attendanceRepository.save(attendance); @@ -54,9 +57,11 @@ public void updateAttendance(UpdateAttendanceRequest request) { throw new AppException(ErrorCode.SESSION_DATE_NOT_FOUND); } - attendance.updateDeadLine(LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine() - .attendanceDeadLine()), LocalDateTime.of(attendanceSession.getSessionDate(), request.attendanceDeadLine() - .lateDeadLine())); + attendance.updateDeadLine( + LocalDateTime.of(attendanceSession.getSessionDate(), request.attendTime().attendanceDeadLine()) + .plusSeconds(DEFAULT_ATTEND_SECOND), + LocalDateTime.of(attendanceSession.getSessionDate(), request.attendTime() + .lateDeadLine()).plusSeconds(DEFAULT_ATTEND_SECOND)); attendance.updateLocation(request.location()); } 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 af8688fc..a0676df2 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 @@ -1,28 +1,39 @@ package org.cotato.csquiz.domain.attendance.service; +import static org.cotato.csquiz.domain.attendance.util.AttendanceUtil.getAttendanceStatus; + +import jakarta.persistence.EntityNotFoundException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.attendance.dto.AttendResponse; +import org.cotato.csquiz.api.attendance.dto.AttendanceParams; import org.cotato.csquiz.api.attendance.dto.AttendanceRecordResponse; import org.cotato.csquiz.api.attendance.dto.AttendanceStatistic; +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.entity.AttendanceRecord; +import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository; +import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository; import org.cotato.csquiz.domain.auth.entity.Member; import org.cotato.csquiz.domain.auth.service.MemberService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) -@Slf4j public class AttendanceRecordService { private final AttendanceRecordRepository attendanceRecordRepository; + private final AttendanceRepository attendanceRepository; private final MemberService memberService; + private final RequestAttendanceService requestAttendanceService; public List generateAttendanceResponses(List attendances) { List records = attendanceRecordRepository.findAllByAttendanceIdsInQuery( @@ -40,4 +51,23 @@ public List generateAttendanceResponses(List new EntityNotFoundException("해당 출석이 존재하지 않습니다.")); + + // 해당 출석이 열려있는지 확인, 닫혀있으면 제외 + if (getAttendanceStatus(attendance, request.requestTime()) == AttendanceOpenStatus.CLOSED) { + throw new AppException(ErrorCode.ATTENDANCE_CLOSED); + } + + // 기존 출결 데이터가 존재하는지 확인 + if (attendanceRecordRepository.existsByAttendanceIdAndMemberIdAndAttendanceType(request.attendanceId(), + memberId, request.attendanceType())) { + throw new AppException(ErrorCode.ALREADY_ATTEND); + } + + return requestAttendanceService.attend(request, memberId, attendance); + } } diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java index c77de05f..1c2f6123 100644 --- a/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/AttendanceService.java @@ -1,8 +1,7 @@ package org.cotato.csquiz.domain.attendance.service; import jakarta.persistence.EntityNotFoundException; -import java.time.LocalDate; -import java.time.LocalTime; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -10,10 +9,8 @@ import lombok.RequiredArgsConstructor; import org.cotato.csquiz.api.attendance.dto.AttendanceResponse; import org.cotato.csquiz.api.attendance.dto.AttendancesResponse; -import org.cotato.csquiz.domain.attendance.entity.Attendance; -import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; -import org.cotato.csquiz.domain.attendance.enums.DeadLine; import org.cotato.csquiz.domain.attendance.repository.AttendanceRepository; +import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; import org.cotato.csquiz.domain.generation.entity.Generation; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.domain.generation.repository.GenerationRepository; @@ -43,12 +40,14 @@ public AttendancesResponse findAttendancesByGenerationId(final Long generationId .map(Session::getId) .toList(); + LocalDateTime currentTime = LocalDateTime.now(); + List attendances = attendanceRepository.findAllBySessionIdsInQuery(sessionIds).stream() .map(at -> AttendanceResponse.builder() .attendanceId(at.getId()) .sessionTitle(sessionMap.get(at.getSessionId()).getTitle()) .sessionDate(at.getAttendanceDeadLine().toLocalDate()) - .openStatus(getAttendanceStatus(at)) + .openStatus(AttendanceUtil.getAttendanceStatus(at, currentTime)) .build()) .toList(); @@ -58,32 +57,4 @@ public AttendancesResponse findAttendancesByGenerationId(final Long generationId .attendances(attendances) .build(); } - - private AttendanceOpenStatus getAttendanceStatus(Attendance attendance) { - if (!isToday(attendance) || !isStarted()) { - return AttendanceOpenStatus.CLOSED; - } - - LocalTime currentTime = LocalTime.now(); - - if (currentTime.isAfter(DeadLine.ATTENDANCE_START_TIME.getTime()) - && currentTime.isBefore(attendance.getAttendanceDeadLine().toLocalTime())) { - return AttendanceOpenStatus.OPEN; - } - - if (currentTime.isAfter(attendance.getAttendanceDeadLine().toLocalTime()) - && currentTime.isBefore(attendance.getLateDeadLine().toLocalTime())) { - return AttendanceOpenStatus.LATE; - } - - return AttendanceOpenStatus.ABSENT; - } - - private boolean isToday(Attendance attendance) { - return LocalDate.now().equals(attendance.getAttendanceDeadLine().toLocalDate()); - } - - private boolean isStarted() { - return LocalTime.now().isBefore(DeadLine.ATTENDANCE_START_TIME.getTime()); - } } 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 new file mode 100644 index 00000000..e037097a --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OfflineAttendClient.java @@ -0,0 +1,55 @@ +package org.cotato.csquiz.domain.attendance.service; + +import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.api.attendance.dto.AttendResponse; +import org.cotato.csquiz.api.attendance.dto.AttendanceParams; +import org.cotato.csquiz.api.attendance.dto.OfflineAttendanceRequest; +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.entity.AttendanceRecord; +import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; +import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository; +import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OfflineAttendClient implements AttendClient { + + private static final Double ACCURACY_STANDARD = 0.1; + private final AttendanceRecordRepository attendanceRecordRepository; + + @Override + public AttendanceType attendanceType() { + return AttendanceType.OFFLINE; + } + + @Override + public AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance) { + OfflineAttendanceRequest request = (OfflineAttendanceRequest) params; + + AttendanceStatus attendanceStatus = AttendanceUtil.calculateAttendanceStatus(attendance, params.requestTime()); + + Double accuracy = attendance.getLocation().calculateAccuracy(request.getLocation()); + validateAccuracy(accuracy); + + AttendanceRecord attendanceRecord = attendanceRecordRepository.findByMemberIdAndAttendanceId(memberId, + request.getAttendanceId()) + .orElseGet(() -> AttendanceRecord.offlineRecord(attendance, memberId, accuracy, attendanceStatus)); + + attendanceRecord.updateAttendanceType(request.attendanceType()); + attendanceRecord.updateLocationAccuracy(accuracy); + + attendanceRecordRepository.save(attendanceRecord); + + return AttendResponse.from(attendanceStatus); + } + + private void validateAccuracy(Double accuracy) { + if (accuracy >= ACCURACY_STANDARD) { + throw new AppException(ErrorCode.OFFLINE_ATTEND_FAIL); + } + } +} 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 new file mode 100644 index 00000000..fd6de449 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/OnlineAttendClient.java @@ -0,0 +1,33 @@ +package org.cotato.csquiz.domain.attendance.service; + +import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.api.attendance.dto.AttendResponse; +import org.cotato.csquiz.api.attendance.dto.AttendanceParams; +import org.cotato.csquiz.domain.attendance.entity.Attendance; +import org.cotato.csquiz.domain.attendance.entity.AttendanceRecord; +import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; +import org.cotato.csquiz.domain.attendance.repository.AttendanceRecordRepository; +import org.cotato.csquiz.domain.attendance.util.AttendanceUtil; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OnlineAttendClient implements AttendClient { + + private final AttendanceRecordRepository attendanceRecordRepository; + + @Override + public AttendanceType attendanceType() { + return AttendanceType.ONLINE; + } + + @Override + public AttendResponse request(AttendanceParams params, Long memberId, Attendance attendance) { + AttendanceStatus attendanceStatus = AttendanceUtil.calculateAttendanceStatus(attendance, params.requestTime()); + + attendanceRecordRepository.save(AttendanceRecord.onLineRecord(attendance, memberId, attendanceStatus)); + + return AttendResponse.from(attendanceStatus); + } +} \ No newline at end of file diff --git a/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java b/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java new file mode 100644 index 00000000..1639becf --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/attendance/service/RequestAttendanceService.java @@ -0,0 +1,30 @@ +package org.cotato.csquiz.domain.attendance.service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.cotato.csquiz.api.attendance.dto.AttendResponse; +import org.cotato.csquiz.api.attendance.dto.AttendanceParams; +import org.cotato.csquiz.domain.attendance.entity.Attendance; +import org.cotato.csquiz.domain.attendance.enums.AttendanceType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class RequestAttendanceService { + + private final Map clients; + + @Autowired + public RequestAttendanceService(List clients) { + this.clients = clients.stream().collect( + Collectors.toUnmodifiableMap(AttendClient::attendanceType, Function.identity()) + ); + } + + public AttendResponse attend(AttendanceParams params, Long memberId, Attendance attendance) { + AttendClient attendClient = clients.get(params.attendanceType()); + return attendClient.request(params, memberId, attendance); + } +} 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 new file mode 100644 index 00000000..0d324363 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtil.java @@ -0,0 +1,50 @@ +package org.cotato.csquiz.domain.attendance.util; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.cotato.csquiz.domain.attendance.entity.Attendance; +import org.cotato.csquiz.domain.attendance.enums.AttendanceOpenStatus; +import org.cotato.csquiz.domain.attendance.enums.AttendanceStatus; +import org.cotato.csquiz.domain.attendance.enums.DeadLine; + +public class AttendanceUtil { + + // 출석 시간에 따른 지각 여부 구분하기 + public static AttendanceStatus calculateAttendanceStatus(Attendance attendance, LocalDateTime attendTime){ + if (attendTime.isBefore(attendance.getAttendanceDeadLine())) { + return AttendanceStatus.PRESENT; + } if (attendTime.isBefore(attendance.getLateDeadLine())) { + return AttendanceStatus.LATE; + } + return AttendanceStatus.ABSENT; + } + + // 현재 시간을 기준으로 출석 open 상태를 반환한다. + public static AttendanceOpenStatus getAttendanceStatus(Attendance attendance, LocalDateTime currentDateTime) { + if (!isToday(attendance, currentDateTime) || !isStarted(currentDateTime.toLocalTime())) { + return AttendanceOpenStatus.CLOSED; + } + + LocalTime currentTime = currentDateTime.toLocalTime(); + + if (currentTime.isAfter(DeadLine.ATTENDANCE_START_TIME.getTime()) + && currentTime.isBefore(attendance.getAttendanceDeadLine().toLocalTime())) { + return AttendanceOpenStatus.OPEN; + } + + if (currentTime.isAfter(attendance.getAttendanceDeadLine().toLocalTime()) + && currentTime.isBefore(attendance.getLateDeadLine().toLocalTime())) { + return AttendanceOpenStatus.LATE; + } + + return AttendanceOpenStatus.ABSENT; + } + + private static boolean isToday(Attendance attendance, LocalDateTime currentDate) { + return currentDate.toLocalDate().equals(attendance.getAttendanceDeadLine().toLocalDate()); + } + + private static boolean isStarted(LocalTime currentTime) { + return currentTime.isAfter(DeadLine.ATTENDANCE_START_TIME.getTime()); + } +} diff --git a/src/test/java/org/cotato/csquiz/CsquizApplicationTests.java b/src/test/java/org/cotato/csquiz/CsquizApplicationTests.java new file mode 100644 index 00000000..bb897e31 --- /dev/null +++ b/src/test/java/org/cotato/csquiz/CsquizApplicationTests.java @@ -0,0 +1,8 @@ +package org.cotato.csquiz; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CsquizApplicationTests { + +} 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 new file mode 100644 index 00000000..a8e2a9cc --- /dev/null +++ b/src/test/java/org/cotato/csquiz/domain/attendance/util/AttendanceUtilTest.java @@ -0,0 +1,51 @@ +package org.cotato.csquiz.domain.attendance.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +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.Test; + +class AttendanceUtilTest { + + @Test + void 날짜가_다르면_출석이_닫혀있다(){ + //given + Attendance attendance = Attendance.builder() + .attendanceDeadLine(LocalDateTime.now()) + .lateDeadLine(LocalDateTime.now().plusMinutes(10)) + .session(Session.builder() + .build()) + .build(); + + //when + AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceStatus(attendance, + LocalDateTime.now().plusDays(1)); + + //then + assertEquals(attendanceStatus, AttendanceOpenStatus.CLOSED); + } + + @Test + void 기준시간_전이면_출석이_닫혀있다() { + //given + LocalDateTime attendanceDeadLine = LocalDateTime.now(); + + Attendance attendance = Attendance.builder() + .attendanceDeadLine(attendanceDeadLine) + .lateDeadLine(attendanceDeadLine.plusMinutes(10)) + .session(Session.builder() + .build()) + .build(); + + LocalDateTime beforeTime = attendanceDeadLine.minusMinutes(10); + + //when + AttendanceOpenStatus attendanceStatus = AttendanceUtil.getAttendanceStatus(attendance, beforeTime); + + //then + assertEquals(attendanceStatus, AttendanceOpenStatus.CLOSED); + } +} \ No newline at end of file