diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java index 09985874..5c0ce450 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java @@ -13,19 +13,24 @@ @Repository public interface AttendanceRepository extends JpaRepository { - boolean existsByUserAndAttendanceRound( User user, AttendanceRound round); - boolean existsByUser_UserIdAndAttendanceRound_RoundId(UUID userId, UUID roundId); - List findByAttendanceRound_RoundId(UUID roundId); - void deleteAllByAttendanceRound_AttendanceSession_AttendanceSessionIdAndUser_UserId( - UUID sessionId, - UUID userId - ); + boolean existsByUserAndAttendanceRound(User user, AttendanceRound round); - // 사용자별 출석 이력 - List findByUserOrderByCheckedAtDesc(User user); + boolean existsByUser_UserIdAndAttendanceRound_RoundId(UUID userId, UUID roundId); - // 라운드별 특정 사용자 출석 확인 - @Query("SELECT a FROM Attendance a WHERE a.attendanceRound.roundId = :roundId AND a.user = :user") - Optional findByAttendanceRound_RoundIdAndUser(@Param("roundId") UUID roundId, @Param("user") User user); + List findByAttendanceRound_RoundId(UUID roundId); + + void deleteAllByAttendanceRound_AttendanceSession_AttendanceSessionIdAndUser_UserId( + UUID sessionId, + UUID userId + ); + + // 사용자별 출석 이력 + List findByUserOrderByCheckedAtDesc(User user); + + // 라운드별 특정 사용자 출석 확인 + @Query("SELECT a FROM Attendance a WHERE a.attendanceRound.roundId = :roundId AND a.user = :user") + Optional findByAttendanceRound_RoundIdAndUser(@Param("roundId") UUID roundId, @Param("user") User user); + + List findAllByAttendanceRound(AttendanceRound round); } diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java index c2af36b7..4ae9d0c7 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java @@ -1,25 +1,23 @@ package org.sejongisc.backend.attendance.repository; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import org.sejongisc.backend.attendance.entity.AttendanceRound; +import org.sejongisc.backend.attendance.entity.RoundStatus; 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 org.springframework.stereotype.Repository; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - @Repository public interface AttendanceRoundRepository extends JpaRepository { List findByAttendanceSession_AttendanceSessionIdAndRoundDateBefore(UUID sessionId, LocalDate date); - - // UPCOMING -> ACTIVE @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" @@ -40,8 +38,7 @@ public interface AttendanceRoundRepository extends JpaRepository findByRoundStatusAndCloseAtBefore(RoundStatus status, LocalDateTime dateTime); Optional findByQrSecret(String qrCode); List findByAttendanceSession_AttendanceSessionId(UUID sessionId); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java index 35bf359b..27a53263 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java @@ -2,17 +2,24 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.attendance.dto.AttendanceRoundQrTokenResponse; import org.sejongisc.backend.attendance.dto.AttendanceRoundRequest; import org.sejongisc.backend.attendance.dto.AttendanceRoundResponse; +import org.sejongisc.backend.attendance.entity.Attendance; import org.sejongisc.backend.attendance.entity.AttendanceRound; import org.sejongisc.backend.attendance.entity.AttendanceSession; +import org.sejongisc.backend.attendance.entity.AttendanceStatus; import org.sejongisc.backend.attendance.entity.RoundStatus; +import org.sejongisc.backend.attendance.entity.SessionUser; +import org.sejongisc.backend.attendance.repository.AttendanceRepository; import org.sejongisc.backend.attendance.repository.AttendanceRoundRepository; import org.sejongisc.backend.attendance.repository.AttendanceSessionRepository; +import org.sejongisc.backend.attendance.repository.SessionUserRepository; import org.sejongisc.backend.attendance.util.QrTokenUtil; import org.sejongisc.backend.attendance.util.RollingQrTokenUtil; import org.sejongisc.backend.common.exception.CustomException; @@ -31,6 +38,8 @@ public class AttendanceRoundService { private final AttendanceRoundRepository attendanceRoundRepository; private final AttendanceSessionRepository attendanceSessionRepository; + private final AttendanceRepository attendanceRepository; + private final SessionUserRepository sessionUserRepository; private final AttendanceAuthorizationService authorizationService; private final QrTokenStreamService qrTokenStreamService; @@ -193,11 +202,22 @@ public void openRound(UUID roundId, UUID userId) { @Transactional(propagation = Propagation.REQUIRES_NEW) public void runRoundStatusMaintenance() { LocalDateTime now = LocalDateTime.now(); - int closed = attendanceRoundRepository.closeDueRounds(now); + + List roundsToClose = attendanceRoundRepository + .findByRoundStatusAndCloseAtBefore(RoundStatus.ACTIVE, now); + + for (AttendanceRound round : roundsToClose) { + // 상태를 CLOSED로 변경 + round.changeStatus(RoundStatus.CLOSED); + + // 결석 처리 로직 실행 + processAbsentees(round); + } + int activated = attendanceRoundRepository.activateDueRounds(now); - if (activated > 0 || closed > 0) { - log.info("[Quartz] activated={}, closed={}", activated, closed); + if (activated > 0 || !roundsToClose.isEmpty()) { + log.info("[Quartz] activated={}, closed={}", activated, roundsToClose.size()); } } @@ -209,4 +229,38 @@ private void validateCreateRequest(AttendanceRoundRequest req) { throw new CustomException(ErrorCode.END_AT_MUST_BE_AFTER_START_AT); } } + + /** + * 결석자 일괄 처리 + * **/ + private void processAbsentees(AttendanceRound round) { + UUID sessionId = round.getAttendanceSession().getAttendanceSessionId(); + + // 해당 세션에 등록된 모든 유저 목록 조회 + List allSessionUsers = sessionUserRepository.findByAttendanceSession_AttendanceSessionId(sessionId); + + // 현재 라운드에 이미 출석 기록(PRESENT, LATE 등)이 있는 SessionUser의 ID 추출 + Set attendedUserIds = attendanceRepository.findAllByAttendanceRound(round) + .stream() + .map(a -> a.getUser().getUserId()) + .collect(Collectors.toSet()); + + // 출석 기록이 없는 SessionUser만 필터링하여 'ABSENT' 레코드 생성 + List absentRecords = allSessionUsers.stream() + .map(SessionUser::getUser) + .filter(user -> !attendedUserIds.contains(user.getUserId())) + .map(user -> Attendance.builder() + .attendanceRound(round) + .user(user) + .attendanceStatus(AttendanceStatus.ABSENT) + .note("시스템 자동 결석 처리") + .build()) + .toList(); + + // 결석 데이터 일괄 저장 + if (!absentRecords.isEmpty()) { + attendanceRepository.saveAll(absentRecords); + log.debug("[Round-{}] {}명의 SessionUser 결석 처리 완료", round.getRoundId(), absentRecords.size()); + } + } }