diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java b/backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java index 40b258cc..d87c9bb0 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java @@ -2,10 +2,7 @@ import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity; import org.sejongisc.backend.user.entity.User; @@ -15,6 +12,7 @@ @Entity @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java b/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java index cb26382f..8d8c208f 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java @@ -1,6 +1,7 @@ package org.sejongisc.backend.attendance.entity; public enum AttendanceStatus { + PENDING("미정"), // 라운드 예정 중 - 아직 체크인 안 됨 PRESENT("출석"), // 정상 출석 LATE("지각"), // 지각 출석 ABSENT("결석"), // 미출석 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 dc67527d..5b9ec305 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 @@ -57,4 +57,16 @@ public interface AttendanceRoundRepository extends JpaRepository findBySession_SessionIdAndRoundDateBefore( @Param("sessionId") UUID sessionId, @Param("date") LocalDate date); + + /** + * 세션의 특정 날짜 이후의 모든 라운드 조회 + * - 세션에 유저 추가 시, 미래 라운드들에 자동으로 PENDING 처리하기 위해 사용 + */ + @Query("SELECT r FROM AttendanceRound r " + + "WHERE r.attendanceSession.attendanceSessionId = :sessionId " + + "AND r.roundDate >= :date " + + "ORDER BY r.roundDate ASC") + List findBySession_SessionIdAndRoundDateAfterOrEqual( + @Param("sessionId") UUID sessionId, + @Param("date") LocalDate date); } 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 64c6fb9e..70265475 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 @@ -4,11 +4,11 @@ import lombok.extern.slf4j.Slf4j; import org.sejongisc.backend.attendance.dto.AttendanceRoundRequest; import org.sejongisc.backend.attendance.dto.AttendanceRoundResponse; -import org.sejongisc.backend.attendance.entity.AttendanceRound; -import org.sejongisc.backend.attendance.entity.AttendanceSession; -import org.sejongisc.backend.attendance.entity.RoundStatus; +import org.sejongisc.backend.attendance.entity.*; +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.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +30,9 @@ public class AttendanceRoundService { private final AttendanceRoundRepository attendanceRoundRepository; private final AttendanceSessionRepository attendanceSessionRepository; + private final SessionUserRepository sessionUserRepository; + private final AttendanceRepository attendanceRepository; + /** * 라운드 생성 @@ -74,8 +77,25 @@ public AttendanceRoundResponse createRound(UUID sessionId, AttendanceRoundReques // 양방향 관계를 DB에 반영하기 위해 세션도 저장 attendanceSessionRepository.save(session); - log.info("✅ 라운드 생성 완료 - sessionId: {}, roundId: {}, roundDate: {}, roundStatus: {}", - sessionId, saved.getRoundId(), saved.getRoundDate(), saved.getRoundStatus()); + // ⭐ 라운드 생성 시 세션의 모든 SessionUser에 대해 PENDING 상태의 Attendance 미리 생성 + log.info("📝 세션 사용자에 대한 PENDING 출석 기록 생성 시작: sessionId={}, roundId={}", + sessionId, saved.getRoundId()); + + List sessionUsers = sessionUserRepository.findBySessionId(sessionId); + for (SessionUser sessionUser : sessionUsers) { + Attendance pendingAttendance = Attendance.builder() + .user(sessionUser.getUser()) + .attendanceSession(session) + .attendanceRound(saved) + .attendanceStatus(AttendanceStatus.PENDING) + .build(); + attendanceRepository.save(pendingAttendance); + log.info(" ✓ PENDING 출석 기록 생성: userId={}, userName={}, roundId={}", + sessionUser.getUser().getUserId(), sessionUser.getUser().getName(), saved.getRoundId()); + } + + log.info("✅ 라운드 생성 완료 - sessionId: {}, roundId: {}, roundDate: {}, roundStatus: {}, 생성된PENDING개수: {}", + sessionId, saved.getRoundId(), saved.getRoundDate(), saved.getRoundStatus(), sessionUsers.size()); return AttendanceRoundResponse.fromEntity(saved); } catch (Exception e) { log.error("❌ 라운드 생성 중 오류 발생: sessionId={}, error={}", sessionId, e.getMessage(), e); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java index 43ec7764..d0f80bfa 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java @@ -82,11 +82,12 @@ public AttendanceCheckInResponse checkInByRound(AttendanceCheckInRequest request log.info("✅ 시간 검증 성공: 라운드ID={}, 사용자={}, 시간={}, 범위=[{}~{}]", request.getRoundId(), user.getName(), checkTime, startTime, endTime); - // 2. 중복 출석 확인 - boolean alreadyCheckedIn = attendanceRepository.findByAttendanceRound_RoundIdAndUser(request.getRoundId(), user) - .isPresent(); - if (alreadyCheckedIn) { - log.warn("중복 출석 시도: 라운드ID={}, 사용자={}", request.getRoundId(), user.getName()); + // 2. 기존 출석 기록 확인 (PENDING 제외하고 실제 체크인한 기록만 중복으로 취급) + Attendance existingAttendance = attendanceRepository.findByAttendanceRound_RoundIdAndUser(request.getRoundId(), user) + .orElse(null); + if (existingAttendance != null && existingAttendance.getAttendanceStatus() != AttendanceStatus.PENDING) { + log.warn("중복 출석 시도: 라운드ID={}, 사용자={}, 기존상태={}", + request.getRoundId(), user.getName(), existingAttendance.getAttendanceStatus()); return AttendanceCheckInResponse.builder() .roundId(request.getRoundId()) .success(false) @@ -332,8 +333,8 @@ public AttendanceResponse updateAttendanceStatusByRound(UUID roundId, UUID userI private AttendanceResponse convertToResponse(Attendance attendance) { return AttendanceResponse.builder() .attendanceId(attendance.getAttendanceId()) - .userId(attendance.getUser().getUserId()) - .userName(attendance.getUser().getName()) + .userId(attendance.getUser() != null ? attendance.getUser().getUserId() : null) + .userName(attendance.getUser() != null ? attendance.getUser().getName() : "익명") .attendanceSessionId(attendance.getAttendanceSession().getAttendanceSessionId()) .attendanceRoundId(attendance.getAttendanceRound() != null ? attendance.getAttendanceRound().getRoundId() : null) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java index b345b232..907a6b19 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java @@ -103,6 +103,38 @@ public SessionUserResponse addUserToSession(UUID sessionId, UUID userId) { log.info("✅ 과거 라운드 자동 결석 처리 완료: 처리된 라운드 수={}", pastRounds.size()); } + // 6. ⭐ 미래 라운드들에 대해 자동으로 PENDING 상태 처리 + List futureRounds = attendanceRoundRepository.findBySession_SessionIdAndRoundDateAfterOrEqual( + sessionId, + LocalDate.now() + ); + + if (!futureRounds.isEmpty()) { + log.info("📅 미래 라운드 PENDING 처리: 미래 라운드 수={}", futureRounds.size()); + + for (AttendanceRound round : futureRounds) { + // 이미 해당 라운드에 출석 기록이 있는지 확인 + boolean alreadyExists = attendanceRepository.findByAttendanceRound_RoundIdAndUser(round.getRoundId(), user) + .isPresent(); + + if (!alreadyExists) { + // 새로운 Attendance 레코드 생성 (PENDING 상태) + Attendance pendingRecord = Attendance.builder() + .user(user) + .attendanceSession(session) + .attendanceRound(round) + .attendanceStatus(AttendanceStatus.PENDING) + .build(); + + attendanceRepository.save(pendingRecord); + log.info(" - PENDING 기록 생성: roundId={}, date={}, userName={}", + round.getRoundId(), round.getRoundDate(), user.getName()); + } + } + + log.info("✅ 미래 라운드 PENDING 처리 완료: 처리된 라운드 수={}", futureRounds.size()); + } + log.info("✅ 세션에 사용자 추가 완료: sessionId={}, userId={}, userName={}", sessionId, userId, user.getName());