Conversation
Walkthrough출석 도메인에 PENDING 상태를 추가하고, 라운드 생성 시 또는 세션에 사용자 추가 시 모든 세션 사용자에 대해 PENDING 상태의 Attendance 레코드를 자동으로 미리 생성하는 기능을 구현합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Admin as 관리자
participant ARS as AttendanceRoundService
participant SUR as SessionUserRepository
participant AR as AttendanceRepository
participant DB as Database
Admin->>ARS: 출석 라운드 생성 요청
activate ARS
ARS->>DB: 라운드 저장
ARS->>SUR: 세션의 모든 사용자 조회
activate SUR
SUR->>DB: SessionUser 쿼리
SUR-->>ARS: 사용자 목록 반환
deactivate SUR
loop 각 세션 사용자별
ARS->>AR: PENDING 상태의 Attendance 레코드 생성
activate AR
AR->>DB: 레코드 저장
AR-->>ARS: 저장 완료
deactivate AR
ARS->>ARS: per-user 생성 로그
end
ARS->>ARS: 최종 요약 로그 (생성 개수)
ARS-->>Admin: 라운드 및 PENDING 레코드 생성 완료
deactivate ARS
sequenceDiagram
actor Admin as 관리자
participant SUS as SessionUserService
participant ARR as AttendanceRoundRepository
participant AR as AttendanceRepository
participant DB as Database
Admin->>SUS: 세션에 사용자 추가 요청
activate SUS
SUS->>DB: 과거 라운드에 대해 ABSENT 상태로 처리
SUS->>ARR: 오늘 이후의 미래 라운드 조회
activate ARR
ARR->>DB: roundDate >= today 쿼리
ARR-->>SUS: 미래 라운드 목록 반환
deactivate ARR
loop 각 미래 라운드별
SUS->>DB: 기존 Attendance 확인
alt 기존 레코드 없음
SUS->>AR: PENDING 상태의 Attendance 생성
activate AR
AR->>DB: 레코드 저장
AR-->>SUS: 저장 완료
deactivate AR
SUS->>SUS: per-record 생성 로그
end
end
SUS->>DB: 세션-사용자 추가
SUS-->>Admin: 사용자 추가 및 PENDING 레코드 생성 완료
deactivate SUS
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 추가 검토 필요 영역:
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java (2)
106-136: 성능 최적화 권장: 배치 저장 및 N+1 쿼리 개선현재 구현은 각 라운드마다 개별적으로
findByAttendanceRound_RoundIdAndUser를 호출하고save를 수행합니다. 라운드 수가 많아지면 성능 저하가 발생할 수 있습니다.- 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()); - } + if (!futureRounds.isEmpty()) { + log.info("📅 미래 라운드 PENDING 처리: 미래 라운드 수={}", futureRounds.size()); + + List<UUID> futureRoundIds = futureRounds.stream() + .map(AttendanceRound::getRoundId) + .collect(Collectors.toList()); + + // 기존 출석 기록이 있는 라운드 ID 조회 (한 번의 쿼리로 처리) + Set<UUID> existingRoundIds = attendanceRepository + .findExistingRoundIdsByUserAndRoundIds(user.getUserId(), futureRoundIds); + + List<Attendance> pendingRecords = futureRounds.stream() + .filter(round -> !existingRoundIds.contains(round.getRoundId())) + .map(round -> Attendance.builder() + .user(user) + .attendanceSession(session) + .attendanceRound(round) + .attendanceStatus(AttendanceStatus.PENDING) + .build()) + .collect(Collectors.toList()); + + attendanceRepository.saveAll(pendingRecords); + log.info("✅ 미래 라운드 PENDING 처리 완료: 생성된 레코드 수={}", pendingRecords.size()); + }
AttendanceRepository에 다음 메서드 추가가 필요합니다:@Query("SELECT a.attendanceRound.roundId FROM Attendance a WHERE a.user.userId = :userId AND a.attendanceRound.roundId IN :roundIds") Set<UUID> findExistingRoundIdsByUserAndRoundIds(@Param("userId") UUID userId, @Param("roundIds") List<UUID> roundIds);
72-104: 동일한 성능 최적화 패턴을 과거 라운드 처리에도 적용 권장미래 라운드 처리(lines 106-136)와 동일한 N+1 쿼리 패턴이 존재합니다. 배치 조회/저장 패턴을 적용하면 두 로직 모두 성능이 개선됩니다.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java (2)
80-98: 배치 저장으로 최적화 권장라운드 생성 시 각 사용자에 대해 개별
save호출이 발생합니다.saveAll을 사용하면 더 효율적입니다.- List<SessionUser> 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()); - } + List<SessionUser> sessionUsers = sessionUserRepository.findBySessionId(sessionId); + List<Attendance> pendingAttendances = sessionUsers.stream() + .map(sessionUser -> Attendance.builder() + .user(sessionUser.getUser()) + .attendanceSession(session) + .attendanceRound(saved) + .attendanceStatus(AttendanceStatus.PENDING) + .build()) + .collect(Collectors.toList()); + + attendanceRepository.saveAll(pendingAttendances); + log.debug("PENDING 출석 기록 생성 완료: 사용자 수={}", pendingAttendances.size());추가로, 개별 레코드 생성 로그는
log.debug로 변경하거나 제거하는 것을 권장합니다. 프로덕션에서 사용자가 많을 경우 로그가 과도하게 생성될 수 있습니다.
7-8: 와일드카드 import 사용 지양 권장
org.sejongisc.backend.attendance.entity.*대신 명시적 import를 사용하면 코드 가독성과 유지보수성이 향상됩니다.-import org.sejongisc.backend.attendance.entity.*; +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;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java(2 hunks)backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceCheckInRequest.java(2 hunks)backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/entity/SessionUser.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java(3 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java(5 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java(1 hunks)
🔇 Additional comments (9)
backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java (1)
39-40: LGTM!쿼리 단순화가 적절합니다.
COUNT(su) > 0은 JPQL에서 유효한 boolean 표현식이며, 이전의CASE WHEN구문보다 간결합니다.backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceCheckInRequest.java (1)
35-53: LGTM!위도/경도 필드에서
@NotNull제거 후에도 범위 검증(@DecimalMin,@DecimalMax)이 유지되어 값이 제공될 경우 유효성을 보장합니다. 조건부 필수 필드로의 변경이 적절합니다.backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java (1)
60-71: LGTM!미래 라운드 조회를 위한 새 쿼리 메서드가 적절하게 추가되었습니다.
>=연산자로 오늘 날짜를 포함하는 것이 PENDING 처리 로직과 일치합니다.backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java (1)
3-8: PENDING 상태 추가는 안전합니다AttendanceStatus 필드는
@Enumerated(EnumType.STRING)(Attendance.java 라인 42)로 설정되어 있어, enum 값이 이름으로 저장됩니다. 따라서 enum 순서 변경이 기존 데이터에 영향을 주지 않습니다. PENDING 상태 추가는 데이터 호환성 문제가 없습니다.Likely an incorrect or invalid review comment.
backend/src/main/java/org/sejongisc/backend/attendance/entity/SessionUser.java (1)
22-23: No action needed. The application uses Spring Data JPA withgenerate-ddl: trueinapplication.yml, which automatically manages schema creation from entity definitions. Hibernate will create theattendance_session_usertable when the application starts without requiring explicit SQL migration scripts. Additionally, no references to the old table namesession_userexist in the codebase, indicating this is likely a new entity or the table naming follows the current standard.Likely an incorrect or invalid review comment.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (2)
52-57: 날짜/시간 검증 및 remainingSeconds 계산은 일관성 있게 보입니다라운드 날짜 동일성 체크 후
startTime <= now < endTime윈도우 검증, 그리고Duration.between(checkTime, endTime)기반 남은 시간 계산 흐름이 서로 잘 맞습니다. 다만LocalDate.now()/LocalTime.now()가 서버 기본 타임존에 의존하므로, 운영 환경 타임존(KST 등)과 라운드 날짜/시간이 정의된 타임존이 항상 일치하는지 한 번만 점검해두면 좋겠습니다.Also applies to: 60-68, 70-72, 82-83, 156-159
85-95: Verify the PENDING status pre-creation flow and reconcile with duplicate attendance logicThe review raises a critical architectural concern: if
AttendanceRoundServiceandSessionUserServicepre-create attendance records in PENDING status (as mentioned in the PR description), the current duplicate check at lines 85-95 will reject all subsequent check-in attempts, making PENDING→PRESENT/LATE transitions impossible.The core issue is that
findByAttendanceRound_RoundIdAndUser()returns any existing record regardless of status, treating PENDING records identically to completed check-ins. To align with the pre-creation strategy, the logic should:
- Allow PENDING records to be updated: Check
existing.getAttendanceStatus() != AttendanceStatus.PENDINGbefore blocking- Update existing PENDING records instead of creating duplicates: Reuse the found record and update its status, timestamp, location, and points
- Only create new Attendance when no pre-existing record exists
This pattern should also apply to lines 134-142 and 152-155 for consistency. Before proceeding, confirm:
- Do
AttendanceRoundServiceandSessionUserServiceactually pre-create PENDING records?- Does
AttendanceStatusenum include PENDING status?- Are similar duplicate checks present at the other mentioned lines?
backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceController.java (2)
32-34: 세션별 조회 엔드포인트 deprecate 처리 적절함Javadoc, Swagger
@Operationdescription, 자바@Deprecated(since = "2.0", forRemoval = true)까지 일관되게 표시되어 있어, 클라이언트/문서 양쪽 모두에 라운드 기반 조회로의 마이그레이션 경로가 잘 안내됩니다. 시그니처나 보안 설정은 그대로 유지되어 있어 기존 소비자에게도 안전해 보입니다.Also applies to: 39-44, 47-48
79-80: 세션 기반 출석 상태 수정 엔드포인트 deprecate 처리도 괜찮습니다상단 주석과 Swagger description,
@Deprecated설정 모두 라운드 기반 수정 엔드포인트(PUT /api/attendance/rounds/{roundId}/attendances/{userId})로의 전환을 명확히 안내하고 있습니다. 런타임 로직 변경 없이 문서 수준에서만 경고를 주고 있어, 단계적 제거 전략으로 적절해 보입니다.Also applies to: 85-91, 94-95
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (1)
332-353: NPE 방지를 위한 null-safe 처리 완료이전 리뷰에서 지적된
attendance.getUser()null 가능성에 대한 처리가 완료되었습니다.userId와userName모두 null-safe하게 처리되고 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java(5 hunks)
🔇 Additional comments (4)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (4)
17-18: 시간 검증 로직 개선 확인
LocalDate,LocalTime을 사용한 중앙집중식 시간 검증 로직이 명확하게 구현되었습니다.lateThreshold를startTime.plusMinutes(5)로 설정하여 지각 기준이 일관되게 적용됩니다.Also applies to: 52-57
60-68: 날짜 검증 로직 추가 확인라운드 날짜와 현재 날짜가 일치하는지 검증하는 로직이 추가되었습니다. 날짜 불일치 시 명확한 실패 사유와 함께 로그를 남기고 있어 디버깅에 유용합니다.
70-83: 시간 범위 검증: 자정 경계 케이스 고려
startTime <= now < endTime검증은 일반적인 경우에 올바르게 동작합니다. 다만, 자정을 넘는 라운드(예: 23:00~01:00)가 있다면 현재 로직으로는 처리되지 않습니다.비즈니스 요구사항에 자정을 넘는 라운드가 없다면 무시해도 됩니다. 만약 있다면 별도 처리가 필요합니다.
126-131: 지각 판별 로직 명확화
lateThreshold변수를 사용하여 지각 기준이 명확해졌습니다. 로그에도 지각 기준 시간이 포함되어 디버깅이 용이합니다.
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java
Outdated
Show resolved
Hide resolved
737247b to
83dc1f8
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (1)
85-96: PENDING 레코드를 재사용하지 않고 새 Attendance를 생성해서 중복/예외가 발생할 수 있습니다
existingAttendance가PENDING인 경우에도 134~143라인에서 항상 새Attendance를 빌더로 생성하고 있습니다. 라운드/유저별로 PENDING 레코드를 미리 만들어두는 구조라면, 체크인 시에는 이 PENDING 레코드를 PRESENT/LATE로 업데이트하는 것이 의도에 더 가깝고, (roundId, user) 쌍당 하나의 Attendance만 유지할 수 있습니다.현재 구현처럼 새 레코드를 추가로 만들면 같은 라운드/유저에 대해 PENDING + PRESENT/LATE 두 건이 생길 수 있고, 이 경우
- DB에 (round_id, user_id) 유니크 제약이 있다면 제약 위반,
findByAttendanceRound_RoundIdAndUser처럼 단일 결과를 기대하는 조회에서 여러 건이 조회되어 예외가 날 위험
이 있어 보입니다. 이전 리뷰 코멘트에서 제안되었던 “기존 PENDING 레코드 업데이트” 부분이 아직 반영되지 않은 상태로 보입니다.기존 PENDING이 있을 때는 그 엔티티를 재사용해서 필드만 갱신하도록 아래와 같이 바꾸는 쪽을 권장합니다.
- // 5. 출석 기록 저장 - Attendance attendance = Attendance.builder() - .user(user) - .attendanceSession(session) - .attendanceRound(round) - .attendanceStatus(status) - .checkedAt(java.time.LocalDateTime.now()) - .awardedPoints(session.getRewardPoints()) - .checkInLocation(userLocation) - .build(); + // 5. 출석 기록 저장 (기존 PENDING 레코드가 있으면 해당 엔티티를 업데이트) + Attendance attendance = existingAttendance != null + ? existingAttendance + : Attendance.builder() + .user(user) + .attendanceSession(session) + .attendanceRound(round) + .build(); + + attendance.setAttendanceStatus(status); + attendance.setCheckedAt(java.time.LocalDateTime.now()); + attendance.setAwardedPoints(session.getRewardPoints()); + attendance.setCheckInLocation(userLocation);이렇게 하면 PENDING 선생성 전략과도 자연스럽게 맞고, 이후 관리자용 API(
updateAttendanceStatusByRound)에서 단일 Attendance를 전제로 조회하는 흐름과도 일관성이 맞습니다.Also applies to: 134-149
🧹 Nitpick comments (3)
backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java (1)
333-353: convertToResponse의 user null-safe 처리 적절합니다 (추가 아이디어 한 가지)
attendance.getUser()가 null인 경우를 삼항 연산자로 처리해서 기존에 지적되었던 NPE 가능성이 사라진 점 좋습니다. 현재는 user가 null이면 무조건"익명"으로 내려가는데, 도메인에 따라Attendance.anonymousUserName필드를 우선 사용하고, 없을 때만"익명"으로 fallback 하는 형태도 고려해 볼 수 있습니다. 그런 요구가 없다면 지금 구현만으로도 충분해 보입니다.backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java (1)
5-19: Attendance 엔티티 전체에 @Setter 부여 시 변경 범위 한 번 점검해 보시는 것을 권장합니다클래스 레벨에
@Setter가 추가되면서attendanceId, 연관 엔티티, 타임스탬프까지 모든 필드에 세터가 열려 있습니다. PENDING 선생성 후 상태/시간을 갱신하는 용도에는 편리하지만, 외부 코드에서 ID나 연관관계, 시간 정보 등을 임의로 변경하기 쉬워져 도메인 제약이 느슨해질 수 있습니다.실제로 세터가 필요한 필드만 선택적으로
@Setter를 붙이거나, ID/연관관계/생성일·업데이트일 같은 필드는@Setter(AccessLevel.NONE)등으로 막아 두는 방향도 한 번 고려해 보시면 좋겠습니다.backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java (1)
7-9: 라운드 생성 시 PENDING 출석 미리 생성 로직은 의도와 잘 맞습니다 (성능/중복 관점에서 확인 포인트)
createRound에서 세션의 모든SessionUser를 조회해 PENDINGAttendance를 선생성하는 흐름은 이후 라운드 기반 체크인 및 관리자 상태 변경 플로우와 잘 맞아 보입니다. 로그도 단계별로 잘 남겨서 운영 시 추적성이 좋을 것 같습니다.다만 세션 인원이 많은 경우 개별
attendanceRepository.save(...)호출이 상당히 늘어날 수 있으니, 필요 시saveAll(...)같은 배치 저장이나 별도 헬퍼 메서드로 분리하는 것도 고려해 볼 수 있겠습니다. 또, 세션에 유저를 추가할 때 미래 라운드에 대해 PENDING를 생성하는 로직(SessionUserService)과 조합될 때 동일 (round, user) 쌍에 대해 중복Attendance가 생기지 않는지(특히 동시성 상황에서) 한 번만 확인해 두시면 좋겠습니다.Also applies to: 11-11, 33-35, 80-98
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
backend/src/main/java/org/sejongisc/backend/attendance/entity/Attendance.java(2 hunks)backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java(1 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java(3 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java(2 hunks)backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceStatus.java
- backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java
🔇 Additional comments (1)
backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java (1)
61-71: 미래 라운드 조회용 쿼리 조건과 메소드명이 의도와 잘 맞습니다JPQL이
roundDate >= :date조건을 사용하고 메소드명이findBySession_SessionIdAndRoundDateAfterOrEqual이라서, “기준 날짜(예: 오늘) 포함 이후 라운드”를 조회하려는 용도와 일관성이 있습니다. 세션에 유저 추가 시 미래 라운드에 PENDING 출석을 미리 생성하는 시나리오에도 적절해 보입니다.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.