Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public ResponseEntity<AttendanceRoundResponse> createRound(
@Valid @RequestBody AttendanceRoundRequest request) {
log.info("📋 라운드 생성 요청 도착:");
log.info(" - sessionId: {}", sessionId);
log.info(" - roundDate: {} (타입: {})", request.getRoundDate(), request.getRoundDate() != null ? request.getRoundDate().getClass().getSimpleName() : "null");
log.info(" - date: {} (타입: {})", request.getDate(), request.getDate() != null ? request.getDate().getClass().getSimpleName() : "null");
log.info(" - startTime: {} (타입: {})", request.getStartTime(), request.getStartTime() != null ? request.getStartTime().getClass().getSimpleName() : "null");
log.info(" - allowedMinutes: {}", request.getAllowedMinutes());
log.info(" - availableMinutes: {}", request.getAvailableMinutes());

if (request.getStartTime() != null) {
log.info(" - startTime 상세: 시간={}, 분={}, 초={}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ public class AttendanceSessionController {
* 출석 세션 생성 (관리자용)
* - 6자리 랜덤 코드 자동 생성
* - GPS 위치 및 반경 설정
* - 시간 윈도우 설정
* - 기본 시작 시간 및 출석 인정 시간 설정
*/
@Operation(
summary = "출석 세션 생성",
description = "새로운 출석 세션을 생성합니다. (관리자 전용) " +
"6자리 랜덤 코드가 자동 생성되며, GPS 위치 정보, 시간 윈도우, " +
"보상 포인트 등을 설정할 수 있습니다."
"6자리 랜덤 코드가 자동 생성되며, GPS 위치 정보, 기본 시작 시간, " +
"출석 인정 시간, 보상 포인트 등을 설정할 수 있습니다."
)
@PostMapping
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
Expand Down Expand Up @@ -113,13 +113,13 @@ public ResponseEntity<List<AttendanceSessionResponse>> getPublicSessions() {

/**
* 현재 활성 세션 목록 조회
* - 체크인 가능한 세션들만 조회
* - 시작시간 ~ 종료 시간 범위 내
* - 상태가 OPEN인 세션들만 조회
* - 실제 체크인 가능 여부는 라운드의 상태로 판단
*/
@Operation(
summary = "활성 세션 목록 조회",
description = "현재 체크인이 가능한 활성 세션들을 조회합니다. " +
"세션 시작 시간부터 시간 윈도우 종료까지 범위 내인 세션들만 조회됩니다."
description = "현재 활성화된(OPEN 상태) 세션들을 조회합니다. " +
"실제 체크인 가능 여부는 세션 내 라운드의 시간 상태로 판단됩니다."
)
@GetMapping("/active")
public ResponseEntity<List<AttendanceSessionResponse>> getActiveSessions() {
Expand All @@ -132,13 +132,13 @@ public ResponseEntity<List<AttendanceSessionResponse>> getActiveSessions() {

/**
* 세션 정보 수정 (관리자용)
* - 제목, 시간, 위치, 반경 등 수정 가능
* - 제목, 기본 시간, 위치, 반경 등 수정 가능
* - 코드는 변경 불가
*/
@Operation(
summary = "세션 정보 수정",
description = "세션의 기본 정보를 수정합니다. (관리자 전용) " +
"제목, 태그, 시간, GPS 위치, 반경, 포인트 등을 수정할 수 있으며, " +
"제목, 기본 시작 시간, 출석 인정 시간, GPS 위치, 반경, 포인트 등을 수정할 수 있으며, " +
"6자리 코드는 변경할 수 없습니다."
)
@PutMapping("/{sessionId}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class AttendanceRoundRequest {
type = "string",
format = "date"
)
private LocalDate roundDate;
private LocalDate date;

@NotNull(message = "시작 시간은 필수입니다")
@Schema(
Expand All @@ -60,5 +60,5 @@ public class AttendanceRoundRequest {
minimum = "1",
maximum = "120"
)
private Integer allowedMinutes;
private Integer availableMinutes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class AttendanceRoundResponse {
format = "date"
)
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate roundDate;
private LocalDate date;

@Schema(
description = "라운드 출석 시작 시간",
Expand Down Expand Up @@ -69,7 +69,7 @@ public static AttendanceRoundResponse fromEntity(AttendanceRound round) {

return AttendanceRoundResponse.builder()
.roundId(round.getRoundId())
.roundDate(round.getRoundDate())
.date(round.getRoundDate())
.startTime(round.getStartTime())
.availableMinutes(round.getAllowedMinutes())
.status(statusString)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.sejongisc.backend.attendance.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.*;

import java.time.LocalDateTime;
import java.time.LocalTime;

@Getter
@Builder
Expand All @@ -19,33 +20,34 @@ public class AttendanceSessionRequest {

@Schema(
description = "세션의 제목/이름",
example = "2024년 10월 동아리 정기 모임",
example = "금융 IT팀 세션",
maxLength = 100
)
@NotBlank(message = "제목은 필수입니다")
@Size(max = 100, message = "제목은 100자 이하여야 합니다")
private String title;

@Schema(
description = "세션 시작 시간 (ISO 8601 형식). 현재 시간 이후여야 합니다.",
example = "2024-11-15T14:00:00",
description = "세션의 기본 시작 시간 (HH:MM:SS 형식). " +
"모든 라운드는 이 시간을 기본값으로 사용합니다.",
example = "18:30:00",
type = "string",
format = "date-time"
format = "time"
)
@NotNull(message = "시작 시간은 필수입니다")
@Future(message = "시작 시간은 현재 시간 이후여야 합니다")
private LocalDateTime startsAt;
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime defaultStartTime;
Comment on lines 21 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

@Future 검증 제거 및 날짜 정보 손실 확인 필요

LocalDateTime startsAt에서 LocalTime defaultStartTime으로 변경하면서 다음 사항들을 확인해주세요:

  1. 검증 로직 손실: 기존 @Future 어노테이션이 제거되어 과거 시간에 대한 검증이 불가능합니다. 날짜 정보 없이 시간만으로는 미래 여부를 판단할 수 없으므로, 서비스 레이어에서 날짜와 함께 검증하는 로직이 추가되었는지 확인이 필요합니다.

  2. 의미론적 변경: 절대 날짜시간에서 시간대(time-of-day)로의 변경은 세션이 "언제" 시작하는지가 아닌 "몇 시에" 시작하는지만 나타냅니다. 이것이 의도된 설계 변경인지 확인해주세요.

  3. 데이터베이스 마이그레이션: 기존 LocalDateTime 데이터가 LocalTime으로 올바르게 마이그레이션되었는지 확인이 필요합니다.

다음 스크립트로 관련 엔티티와 서비스 레이어의 검증 로직을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# AttendanceSession 엔티티에서 defaultStartTime 필드와 검증 로직 확인
ast-grep --pattern 'class AttendanceSession {
  $$$
  defaultStartTime
  $$$
}'

# 서비스 레이어에서 시간 검증 로직 검색
rg -n -C5 --type=java "defaultStartTime.*valid|validate.*defaultStartTime|Future.*defaultStartTime"

# 데이터베이스 마이그레이션 파일 확인
fd -e sql -e xml "migration|flyway|liquibase" --exec rg -l "defaultStartTime|startsAt"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find AttendanceSession entity file
echo "=== Finding AttendanceSession entity ==="
fd -e java --exec rg -l "class AttendanceSession" {} \;

# Find all files in attendance directory
echo -e "\n=== Attendance module structure ==="
fd "attendance" -type d

# Search for defaultStartTime definition
echo -e "\n=== Searching for defaultStartTime field ==="
rg -n "defaultStartTime" --type=java -B2 -A2

# Search for LocalTime usage in attendance module
echo -e "\n=== LocalTime usage in attendance ==="
rg -n "LocalTime" --type=java -B1 -A1 | head -50

# Check for validation-related annotations in attendance DTO/entity files
echo -e "\n=== Validation annotations in attendance ==="
rg -n "@Future|@Valid|@Validated" --type=java backend/src/main/java/org/sejongisc/backend/attendance

Length of output: 11423


🏁 Script executed:

#!/bin/bash
# Look for migration files
echo "=== Database migration files ==="
fd -e sql -e xml 

# Search for any validation logic on defaultStartTime in service
echo -e "\n=== Validation logic on defaultStartTime ==="
rg -n "defaultStartTime" --type=java -B5 -A5 backend/src/main/java/org/sejongisc/backend/attendance/service/

# Search for LocalTime validation/temporal logic
echo -e "\n=== LocalTime validation/temporal checks ==="
rg -n "LocalTime.*isAfter|LocalTime.*isBefore|LocalTime.*now\(\)|LocalTime.*compareTo" --type=java backend/src/main/java/org/sejongisc/backend/attendance/

# Check entire AttendanceSession entity for all fields and validations
echo -e "\n=== Full AttendanceSession entity ==="
cat backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java | head -80

Length of output: 7586


제거된 @Future 검증 대체 로직 미발견

검증 결과 다음 사항이 확인되었습니다:

  1. 검증 손실 확인: AttendanceSession 엔티티(line 32)와 AttendanceSessionRequest DTO(line 39) 모두에서 @Future 검증이 제거되었으며, 서비스 레이어에서도 대체 검증 로직이 없습니다.

  2. 시간만 저장: LocalTime 사용으로 인해 시간대(time-of-day)만 저장되고 날짜 정보가 제거되었습니다. 엔티티 주석도 "시간만"이라고 명시합니다.

  3. 검증 공백: 현재는 @NotNull 만 있어서 null 값만 방지하고, 과거 시간 설정을 막을 방법이 없습니다. 다른 레이어에서도 defaultStartTime에 대한 시간 유효성 검증이 발견되지 않습니다.

  4. 데이터베이스: 마이그레이션 파일이 없어서 기존 LocalDateTime 데이터의 마이그레이션 상태를 확인할 수 없습니다.

필수 검토 항목:

  • 서비스 레이어에서 defaultStartTime이 유효한(미래의) 시간인지 검증하는 로직 추가 필요
  • 데이터베이스 스키마 및 기존 데이터 마이그레이션 전략 확인 필요
🤖 Prompt for AI Agents
In
backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java
around lines 21-39, the @Future validation was removed and defaultStartTime is
now a LocalTime (time-of-day only), leaving no check that the configured start
is a future occurrence and no DB migration strategy; add service-layer
validation in the session create/update service to enforce a future occurrence
by either (A) changing the DTO/entity to LocalDateTime if a specific date+time
is required and validate @Future, or (B) if preserving LocalTime, implement
logic to resolve the next occurrence (combine LocalTime with LocalDate.now(), if
that datetime is before now use next day) and reject or adjust inputs
accordingly, and add unit tests; additionally, review database schema and
provide a migration plan to convert any existing LocalDateTime values to the new
shape or to store both date+time as needed, documenting the chosen approach.


@Schema(
description = "출석 체크인이 가능한 시간 윈도우 (초 단위). " +
"범위: 300초(5분) ~ 14400초(4시간)",
example = "1800",
minimum = "300",
maximum = "14400"
description = "출석 인정 시간 (분 단위). " +
"범위: 1분 ~ 240분(4시간)",
example = "30",
minimum = "1",
maximum = "240"
)
@Min(value = 300, message = "최소 5분 이상이어야 합니다")
@Max(value = 14400, message = "최대 4시간 설정 가능합니다")
private Integer windowSeconds;
@Min(value = 1, message = "최소 1분 이상이어야 합니다")
@Max(value = 240, message = "최대 240분(4시간) 설정 가능합니다")
private Integer defaultAvailableMinutes;
Comment on lines 41 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

최소 시간 제한 변경 검증 필요

출석 인정 시간의 단위가 초에서 분으로 변경되면서, 최소값이 300초(5분)에서 1분으로 감소했습니다:

  • 기존: 300초 ~ 14,400초 (5분 ~ 4시간)
  • 변경: 1분 ~ 240분 (1분 ~ 4시간)

1분의 출석 인정 시간은 다음과 같은 문제를 야기할 수 있습니다:

  • 앱 로딩 시간, 네트워크 지연으로 인한 체크인 실패
  • 사용자 경험 저하

최소 5분 정책이 비즈니스 요구사항이었다면, @Min(value = 5)로 유지하는 것을 권장합니다.

비즈니스 로직 검증을 위해 다음을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# 기존 windowSeconds 사용처에서 최소값 검증 로직 확인
rg -n -C3 --type=java "windowSeconds|availableMinutes" | rg -C3 "300|5.*minute"

# 테스트 코드에서 최소값 테스트 케이스 확인
rg -n --type=java "availableMinutes.*[1-5]" backend/src/test/

Length of output: 2041


Need to check if there are references to the old 5-minute constraint in the actual business logic.

Based on my verification, I need to gather more direct information from the codebase to understand if this change was intentional.


Based on the verification, here's the final rewritten review comment:

---

**출석 인정 시간 최소값 5분 → 1분 변경에 대한 검증 필요**

단위 변경(초 → 분)과 함께 최소값이 300초(5분)에서 1분으로 감소했습니다:

- **기존**: 300초 ~ 14,400초 (5분 ~ 4시간)  
- **변경**: 1분 ~ 240분

**문제점:**

1. **테스트 커버리지 부재** - 최소값(1분) 또는 최대값(240분)에 대한 경계값 테스트 케이스가 없음  
   현재 테스트는 30분, 45분만 사용 중

2. **비현실적인 최소값** - 1분 출석 창은 다음과 같은 실무 문제 야기:
   - 앱 로딩 시간 (보통 2-3초+)
   - 네트워크 지연
   - 사용자 체크인 실패 가능성 증가

3. **비즈니스 의도 불명확** - 최소값 변경 사유나 관련 주석 없음

**권장사항:**

- 1분 최소값이 의도된 것인지 확인 필요
- 비즈니스 요구사항이면 문서화 필수
- 그렇지 않으면 `@Min(value = 5)`로 유지 권장
- 최소/최대 경계값 테스트 케이스 추가 필수

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for service layer usage of availableMinutes
rg -n -B5 -A5 "availableMinutes" backend/src/main/java/org/sejongisc/backend/attendance/ --type=java | grep -v "test" | head -100

# Look for any migration or change notes
find backend -name "*.java" -o -name "*.md" | xargs rg -l "windowSeconds|availableMinutes|migration|change" 2>/dev/null | head -20

Length of output: 1679


Let me search with the correct directory structure:


🏁 Script executed:

#!/bin/bash
# Search for availableMinutes usage in service/controller/dto
rg -n -B3 -A3 "availableMinutes" git/backend/src/main/java/org/sejongisc/backend/attendance/ --type=java

# Look for any business logic with duration/time calculations
rg -n -B2 -A2 "availableMinutes|windowSeconds" git/backend/src/main/java/org/sejongisc/backend/attendance/service/ --type=java

Length of output: 207


Let me try a different approach to inspect the files directly:


🏁 Script executed:

#!/bin/bash
# Find and display the AttendanceRoundRequest.java file
find . -name "AttendanceRoundRequest.java" -type f

# Find and display the AttendanceRoundService.java file  
find . -name "AttendanceRoundService.java" -type f

# Find dto files related to attendance
find . -path "*/attendance/dto/*" -name "*.java" -type f

Length of output: 990



@Schema(
description = "출석 완료 시 지급할 포인트",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,21 @@ public class Attendance extends BasePostgresEntity {
// 지각 여부 계산 / 상태 업데이트

/**
/**
* 지각 여부 판단
* - 라운드의 시작 시간 이후에 체크인했으면 지각
*/
public boolean isLate() {
if (checkedAt == null || attendanceSession.getStartsAt() == null) {
if (checkedAt == null || attendanceRound == null) {
return false;
}
return checkedAt.isAfter(attendanceSession.getStartsAt());
// 라운드의 시작 시간(LocalTime)과 체크인 시간(LocalDateTime)을 비교
LocalDateTime roundStartDateTime = attendanceRound.getRoundDate()
.atTime(attendanceRound.getStartTime());
return checkedAt.isAfter(roundStartDateTime);
}
Comment on lines +67 to 79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

isLate() 로직이 잘못되었을 가능성이 있습니다.

현재 구현은 체크인 시간이 라운드 시작 시간 이후면 지각으로 판단합니다. 그러나 일반적으로 "지각"은 출석 인정 시간(availableMinutes) 이후에 체크인한 경우를 의미합니다.

예: 시작 시간 18:30, 출석 인정 30분이면 18:30~19:00 사이는 정상 출석, 19:00 이후가 지각이어야 합니다.

 public boolean isLate() {
     if (checkedAt == null || attendanceRound == null) {
         return false;
     }
-    // 라운드의 시작 시간(LocalTime)과 체크인 시간(LocalDateTime)을 비교
     LocalDateTime roundStartDateTime = attendanceRound.getRoundDate()
             .atTime(attendanceRound.getStartTime());
-    return checkedAt.isAfter(roundStartDateTime);
+    // 출석 인정 시간 이후에 체크인했으면 지각
+    LocalDateTime lateThreshold = roundStartDateTime
+            .plusMinutes(attendanceRound.getAvailableMinutes());
+    return checkedAt.isAfter(lateThreshold);
 }

🏁 Script executed:

#!/bin/bash
# Description: AttendanceRound 엔티티에서 availableMinutes 필드와 getter 확인

# availableMinutes 필드 확인
ast-grep --pattern $'class AttendanceRound {
  $$$
  availableMinutes
  $$$
}'

# getAvailableMinutes 메서드 또는 필드 검색
rg -n "availableMinutes" --type java

Length of output: 2812






🏁 Script executed:

#!/bin/bash
# Find and examine the AttendanceRound entity class definition

# Look for the AttendanceRound entity class
fd -t f "AttendanceRound.java" --exclude="*Test*" --exclude="*Request*" --exclude="*Response*"

# Search for the entity file path
rg -n "class AttendanceRound" --type java -A 30 --exclude="*Test*" --exclude="*Request*" --exclude="*Response*" | head -100

Length of output: 494


🏁 Script executed:

#!/bin/bash
# Get content of AttendanceRound entity to see all methods

cat backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceRound.java

Length of output: 3429


isLate() 로직이 잘못되었고, 제안된 수정 사항의 메서드명이 맞지 않습니다.

현재 구현은 체크인 시간이 라운드 시작 시간 이후면 지각으로 판단합니다. 일반적으로 "지각"은 출석 인정 시간(allowedMinutes) 이후에 체크인한 경우를 의미하므로, 라운드 시작 시간 + allowedMinutes 이후인지 확인해야 합니다.

예: 시작 시간 18:30, 출석 인정 30분이면 18:30~19:00 사이는 정상 출석, 19:00 이후가 지각입니다.

단, 제안된 수정에서 사용된 getAvailableMinutes() 메서드는 존재하지 않습니다. AttendanceRound 엔티티의 실제 메서드명은 getAllowedMinutes() 입니다.

 public boolean isLate() {
     if (checkedAt == null || attendanceRound == null) {
         return false;
     }
-    // 라운드의 시작 시간(LocalTime)과 체크인 시간(LocalDateTime)을 비교
     LocalDateTime roundStartDateTime = attendanceRound.getRoundDate()
             .atTime(attendanceRound.getStartTime());
-    return checkedAt.isAfter(roundStartDateTime);
+    // 출석 인정 시간 이후에 체크인했으면 지각
+    LocalDateTime lateThreshold = roundStartDateTime
+            .plusMinutes(attendanceRound.getAllowedMinutes());
+    return checkedAt.isAfter(lateThreshold);
 }

Committable suggestion skipped: line range outside the PR's diff.



/**
* 상태 업데이트 (관리자용)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package org.sejongisc.backend.attendance.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.*;
import org.sejongisc.backend.common.entity.postgres.BasePostgresEntity;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
Expand All @@ -24,13 +25,14 @@ public class AttendanceSession extends BasePostgresEntity {
private UUID attendanceSessionId;

@Column(nullable = false)
private String title; // "세투연 9/17"
private String title; // "금융 IT팀 세션"

@Column(name = "starts_at", nullable = false)
private LocalDateTime startsAt; // 세션 시작 시간
@Column(name = "default_start_time", nullable = false)
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime defaultStartTime; // 기본 시작 시간 (시간만) - 18:30:00

@Column(name = "window_seconds")
private Integer windowSeconds; // 체크인 가능 시간() - 1800 = 30분
@Column(name = "default_available_minutes")
private Integer defaultAvailableMinutes; // 출석 인정 시간() - 30분

@Column(unique = true, length = 6)
private String code; // 6자리 출석 코드 "942715"
Expand All @@ -55,46 +57,12 @@ public class AttendanceSession extends BasePostgresEntity {
private List<Attendance> attendances = new ArrayList<>();

/**
* 현재 세션 상태 계산
* 세션의 현재 상태 반환
* - 세션 상태는 수동으로 관리됨 (UPCOMING, OPEN, CLOSED)
* - 라운드의 상태는 라운드 엔티티에서 시간 기반으로 계산됨
*/
public SessionStatus calculateCurrentStatus() {
LocalDateTime now = LocalDateTime.now();

if (now.isBefore(startsAt)) {
return SessionStatus.UPCOMING;
} else if (now.isAfter(getEndsAt())) {
return SessionStatus.CLOSED;
} else {
return SessionStatus.OPEN;
}
}

/**
* 세션 종료 시간 계산
*/
public boolean isCheckInAvailable() {
LocalDateTime now = LocalDateTime.now();
return now.isAfter(startsAt) && now.isBefore(getEndsAt());
}

/**
* 세션 종료 시간 계산
*/
public LocalDateTime getEndsAt() {
return startsAt.plusSeconds(windowSeconds != null ? windowSeconds : 1800);
public SessionStatus getStatus() {
return this.status;
}

/**
* 남은 시간 계산 (초단위)
*/
public long getRemainingSeconds() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime endsAt = getEndsAt();

if (now.isAfter(endsAt)) {
return 0;
}

return java.time.Duration.between(now, endsAt).getSeconds();
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
package org.sejongisc.backend.attendance.repository;

import org.sejongisc.backend.attendance.entity.AttendanceSession;
import org.sejongisc.backend.attendance.entity.SessionStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface AttendanceSessionRepository extends JpaRepository<AttendanceSession, UUID> {

// 출석 코드로 세션 찾기 (학생 출석 체크)
Optional<AttendanceSession> findByCode(String code);

// 상태별 세션 조회
List<AttendanceSession> findByStatus(SessionStatus status);

// 모든 세션을 최신순으로 조회 (관리자용)
List<AttendanceSession> findAllByOrderByStartsAtDesc();

// 코드 중복 체크
boolean existsByCode(String code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,25 @@ public class AttendanceRoundService {
* 라운드 생성
*/
public AttendanceRoundResponse createRound(UUID sessionId, AttendanceRoundRequest request) {
log.info("📋 라운드 생성 요청: sessionId={}, roundDate={}, startTime={}, allowedMinutes={}",
sessionId, request.getRoundDate(), request.getStartTime(), request.getAllowedMinutes());
log.info("📋 라운드 생성 요청: sessionId={}, date={}, startTime={}, availableMinutes={}",
sessionId, request.getDate(), request.getStartTime(), request.getAvailableMinutes());

AttendanceSession session = attendanceSessionRepository.findById(sessionId)
.orElseThrow(() -> new IllegalArgumentException("세션을 찾을 수 없습니다: " + sessionId));

try {
// 클라이언트가 보낸 날짜 대신 서버의 현재 날짜를 사용하여 시간대 차이 방지
LocalDate roundDate = request.getRoundDate();
if (roundDate == null) {
roundDate = LocalDate.now();
}
// 클라이언트가 제공한 날짜를 사용하고, 없으면 서버의 현재 날짜를 기본값으로 사용
LocalDate roundDate = request.getDate() != null ? request.getDate() : LocalDate.now();
LocalTime requestStartTime = request.getStartTime();

log.info("📅 시간대 정보: 클라이언트 roundDate={}, 서버 today={}, 요청 startTime={}",
request.getRoundDate(), roundDate, requestStartTime);
log.info("📅 시간대 정보: 클라이언트 date={}, 사용할 roundDate={}, 요청 startTime={}",
request.getDate(), roundDate, requestStartTime);

AttendanceRound round = AttendanceRound.builder()
.attendanceSession(session)
.roundDate(roundDate)
.roundDate(roundDate) // 클라이언트 날짜를 우선 사용
.startTime(requestStartTime)
.allowedMinutes(request.getAllowedMinutes() != null ? request.getAllowedMinutes() : 30)
.allowedMinutes(request.getAvailableMinutes() != null ? request.getAvailableMinutes() : 30)
.roundStatus(RoundStatus.UPCOMING)
.build();

Expand Down Expand Up @@ -115,9 +112,9 @@ public AttendanceRoundResponse updateRound(UUID roundId, AttendanceRoundRequest
.orElseThrow(() -> new IllegalArgumentException("라운드를 찾을 수 없습니다: " + roundId));

round.updateRoundInfo(
request.getRoundDate(),
request.getDate(),
request.getStartTime(),
request.getAllowedMinutes()
request.getAvailableMinutes()
);

AttendanceRound updated = attendanceRoundRepository.save(round);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,27 @@ public AttendanceResponse checkIn(UUID sessionId, AttendanceRequest request, UUI
}


// 세션의 활성 라운드를 찾음
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(session.getStartsAt())) {
throw new IllegalStateException("아직 출석 시간이 아닙니다");
}

LocalDateTime endTime = session.getEndsAt();
if (now.isAfter(endTime)) {
throw new IllegalStateException("출석 시간이 종료되었습니다");
}
AttendanceRound activeRound = session.getRounds().stream()
.filter(round -> {
LocalDateTime roundStart = LocalDateTime.of(round.getRoundDate(), round.getStartTime());
LocalDateTime roundEnd = roundStart.plusMinutes(round.getAllowedMinutes());
return !now.isBefore(roundStart) && now.isBefore(roundEnd);
})
.findFirst()
.orElseThrow(() -> new IllegalStateException("현재 진행 중인 라운드가 없습니다"));

// 시작 후 5분 이내는 정상 출석, 이후는 지각
LocalDateTime lateThreshold = session.getStartsAt().plusMinutes(5);
LocalDateTime roundStart = LocalDateTime.of(activeRound.getRoundDate(), activeRound.getStartTime());
LocalDateTime lateThreshold = roundStart.plusMinutes(5);
AttendanceStatus status = now.isAfter(lateThreshold) ?
AttendanceStatus.LATE : AttendanceStatus.PRESENT;

Attendance attendance = Attendance.builder()
.user(user)
.attendanceSession(session)
.attendanceRound(activeRound)
.attendanceStatus(status)
.checkedAt(now)
.awardedPoints(session.getRewardPoints())
Expand All @@ -101,6 +104,7 @@ public AttendanceResponse checkIn(UUID sessionId, AttendanceRequest request, UUI
.deviceInfo(request.getDeviceInfo())
.build();


attendance = attendanceRepository.save(attendance);

log.info("출석 체크인 완료: 사용자={}, 상태={}", user.getName(), status);
Expand Down
Loading