diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java index 2fcf5959..58efd94c 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java @@ -55,9 +55,9 @@ public ResponseEntity createRound( @Valid @RequestBody AttendanceRoundRequest request) { log.info("πŸ“‹ λΌμš΄λ“œ 생성 μš”μ²­ 도착:"); log.info(" - sessionId: {}", sessionId); - log.info(" - date: {} (νƒ€μž…: {})", request.getDate(), request.getDate() != null ? request.getDate().getClass().getSimpleName() : "null"); + log.info(" - roundDate: {} (νƒ€μž…: {})", request.getRoundDate(), request.getRoundDate() != null ? request.getRoundDate().getClass().getSimpleName() : "null"); log.info(" - startTime: {} (νƒ€μž…: {})", request.getStartTime(), request.getStartTime() != null ? request.getStartTime().getClass().getSimpleName() : "null"); - log.info(" - availableMinutes: {}", request.getAvailableMinutes()); + log.info(" - allowedMinutes: {}", request.getAllowedMinutes()); if (request.getStartTime() != null) { log.info(" - startTime 상세: μ‹œκ°„={}, λΆ„={}, 초={}", diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java index 289dae1d..fa45b245 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java @@ -34,13 +34,13 @@ public class AttendanceSessionController { * μΆœμ„ μ„Έμ…˜ 생성 (κ΄€λ¦¬μžμš©) * - 6자리 랜덀 μ½”λ“œ μžλ™ 생성 * - GPS μœ„μΉ˜ 및 반경 μ„€μ • - * - κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„ 및 μΆœμ„ 인정 μ‹œκ°„ μ„€μ • + * - μ‹œκ°„ μœˆλ„μš° μ„€μ • */ @Operation( summary = "μΆœμ„ μ„Έμ…˜ 생성", description = "μƒˆλ‘œμš΄ μΆœμ„ μ„Έμ…˜μ„ μƒμ„±ν•©λ‹ˆλ‹€. (κ΄€λ¦¬μž μ „μš©) " + - "6자리 랜덀 μ½”λ“œκ°€ μžλ™ μƒμ„±λ˜λ©°, GPS μœ„μΉ˜ 정보, κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„, " + - "μΆœμ„ 인정 μ‹œκ°„, 보상 포인트 등을 μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€." + "6자리 랜덀 μ½”λ“œκ°€ μžλ™ μƒμ„±λ˜λ©°, GPS μœ„μΉ˜ 정보, μ‹œκ°„ μœˆλ„μš°, " + + "보상 포인트 등을 μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€." ) @PostMapping @PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')") @@ -132,13 +132,13 @@ public ResponseEntity> getActiveSessions() { /** * μ„Έμ…˜ 정보 μˆ˜μ • (κ΄€λ¦¬μžμš©) - * - 제λͺ©, κΈ°λ³Έ μ‹œκ°„, μœ„μΉ˜, 반경 λ“± μˆ˜μ • κ°€λŠ₯ + * - 제λͺ©, μ‹œκ°„, μœ„μΉ˜, 반경 λ“± μˆ˜μ • κ°€λŠ₯ * - μ½”λ“œλŠ” λ³€κ²½ λΆˆκ°€ */ @Operation( summary = "μ„Έμ…˜ 정보 μˆ˜μ •", description = "μ„Έμ…˜μ˜ κΈ°λ³Έ 정보λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€. (κ΄€λ¦¬μž μ „μš©) " + - "제λͺ©, κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„, μΆœμ„ 인정 μ‹œκ°„, GPS μœ„μΉ˜, 반경, 포인트 등을 μˆ˜μ •ν•  수 있으며, " + + "제λͺ©, νƒœκ·Έ, μ‹œκ°„, GPS μœ„μΉ˜, 반경, 포인트 등을 μˆ˜μ •ν•  수 있으며, " + "6자리 μ½”λ“œλŠ” λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€." ) @PutMapping("/{sessionId}") diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundRequest.java b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundRequest.java index 73ec590d..8ea59e6f 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundRequest.java @@ -39,7 +39,7 @@ public class AttendanceRoundRequest { type = "string", format = "date" ) - private LocalDate date; + private LocalDate roundDate; @NotNull(message = "μ‹œμž‘ μ‹œκ°„μ€ ν•„μˆ˜μž…λ‹ˆλ‹€") @Schema( @@ -60,5 +60,5 @@ public class AttendanceRoundRequest { minimum = "1", maximum = "120" ) - private Integer availableMinutes; + private Integer allowedMinutes; } diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundResponse.java b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundResponse.java index 8aaf9124..3ba856f4 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundResponse.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceRoundResponse.java @@ -35,7 +35,7 @@ public class AttendanceRoundResponse { format = "date" ) @JsonFormat(pattern = "yyyy-MM-dd") - private LocalDate date; + private LocalDate roundDate; @Schema( description = "λΌμš΄λ“œ μΆœμ„ μ‹œμž‘ μ‹œκ°„", @@ -69,7 +69,7 @@ public static AttendanceRoundResponse fromEntity(AttendanceRound round) { return AttendanceRoundResponse.builder() .roundId(round.getRoundId()) - .date(round.getRoundDate()) + .roundDate(round.getRoundDate()) .startTime(round.getStartTime()) .availableMinutes(round.getAllowedMinutes()) .status(statusString) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java index a63fee2a..d3a01520 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java @@ -1,11 +1,10 @@ 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.LocalTime; +import java.time.LocalDateTime; @Getter @Builder @@ -20,7 +19,7 @@ public class AttendanceSessionRequest { @Schema( description = "μ„Έμ…˜μ˜ 제λͺ©/이름", - example = "금육 ITνŒ€ μ„Έμ…˜", + example = "2024λ…„ 10μ›” 동아리 μ •κΈ° λͺ¨μž„", maxLength = 100 ) @NotBlank(message = "제λͺ©μ€ ν•„μˆ˜μž…λ‹ˆλ‹€") @@ -28,26 +27,25 @@ public class AttendanceSessionRequest { private String title; @Schema( - description = "μ„Έμ…˜μ˜ κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„ (HH:MM:SS ν˜•μ‹). " + - "λͺ¨λ“  λΌμš΄λ“œλŠ” 이 μ‹œκ°„μ„ κΈ°λ³Έκ°’μœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.", - example = "18:30:00", + description = "μ„Έμ…˜ μ‹œμž‘ μ‹œκ°„ (ISO 8601 ν˜•μ‹). ν˜„μž¬ μ‹œκ°„ 이후여야 ν•©λ‹ˆλ‹€.", + example = "2024-11-15T14:00:00", type = "string", - format = "time" + format = "date-time" ) @NotNull(message = "μ‹œμž‘ μ‹œκ°„μ€ ν•„μˆ˜μž…λ‹ˆλ‹€") - @JsonFormat(pattern = "HH:mm:ss") - private LocalTime defaultStartTime; + @Future(message = "μ‹œμž‘ μ‹œκ°„μ€ ν˜„μž¬ μ‹œκ°„ 이후여야 ν•©λ‹ˆλ‹€") + private LocalDateTime startsAt; @Schema( - description = "μΆœμ„ 인정 μ‹œκ°„ (λΆ„ λ‹¨μœ„). " + - "λ²”μœ„: 1λΆ„ ~ 240λΆ„(4μ‹œκ°„)", - example = "30", - minimum = "1", - maximum = "240" + description = "μΆœμ„ 체크인이 κ°€λŠ₯ν•œ μ‹œκ°„ μœˆλ„μš° (초 λ‹¨μœ„). " + + "λ²”μœ„: 300초(5λΆ„) ~ 14400초(4μ‹œκ°„)", + example = "1800", + minimum = "300", + maximum = "14400" ) - @Min(value = 1, message = "μ΅œμ†Œ 1λΆ„ 이상이어야 ν•©λ‹ˆλ‹€") - @Max(value = 240, message = "μ΅œλŒ€ 240λΆ„(4μ‹œκ°„) μ„€μ • κ°€λŠ₯ν•©λ‹ˆλ‹€") - private Integer defaultAvailableMinutes; + @Min(value = 300, message = "μ΅œμ†Œ 5λΆ„ 이상이어야 ν•©λ‹ˆλ‹€") + @Max(value = 14400, message = "μ΅œλŒ€ 4μ‹œκ°„ μ„€μ • κ°€λŠ₯ν•©λ‹ˆλ‹€") + private Integer windowSeconds; @Schema( description = "μΆœμ„ μ™„λ£Œ μ‹œ μ§€κΈ‰ν•  포인트", 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 c2d6649c..e5459e01 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 @@ -65,25 +65,12 @@ public class Attendance extends BasePostgresEntity { /** * 지각 μ—¬λΆ€ νŒλ‹¨ - * - λΌμš΄λ“œ 기반: λΌμš΄λ“œ μ‹œμž‘ μ‹œκ°„ 이후이면 지각 - * - μ„Έμ…˜ 기반: λΌμš΄λ“œ 없이 μ§„ν–‰λ˜λŠ” κ²½μš°λŠ” 확인 λΆˆκ°€ */ public boolean isLate() { - if (checkedAt == null) { + if (checkedAt == null || attendanceSession.getStartsAt() == null) { return false; } - - // λΌμš΄λ“œ 기반 체크인인 경우 - if (attendanceRound != null) { - LocalDateTime roundStartTime = LocalDateTime.of( - attendanceRound.getRoundDate(), - attendanceRound.getStartTime() - ); - return checkedAt.isAfter(roundStartTime); - } - - // λΌμš΄λ“œ μ—†λŠ” 경우 지각 νŒλ‹¨ λΆˆκ°€ - return false; + return checkedAt.isAfter(attendanceSession.getStartsAt()); } /** diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java b/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java index bdde7b49..83452dd6 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java @@ -1,12 +1,11 @@ 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.LocalTime; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -25,14 +24,13 @@ public class AttendanceSession extends BasePostgresEntity { private UUID attendanceSessionId; @Column(nullable = false) - private String title; // "금육 ITνŒ€ μ„Έμ…˜" + private String title; // "μ„Ένˆ¬μ—° 9/17" - @Column(name = "default_start_time", nullable = false) - @JsonFormat(pattern = "HH:mm:ss") - private LocalTime defaultStartTime; // κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„ (μ‹œκ°„λ§Œ) - 18:30:00 + @Column(name = "starts_at", nullable = false) + private LocalDateTime startsAt; // μ„Έμ…˜ μ‹œμž‘ μ‹œκ°„ - @Column(name = "default_available_minutes") - private Integer defaultAvailableMinutes; // μΆœμ„ 인정 μ‹œκ°„(λΆ„) - 30λΆ„ + @Column(name = "window_seconds") + private Integer windowSeconds; // 체크인 κ°€λŠ₯ μ‹œκ°„(초) - 1800 = 30λΆ„ @Column(unique = true, length = 6) private String code; // 6자리 μΆœμ„ μ½”λ“œ "942715" @@ -57,19 +55,46 @@ public class AttendanceSession extends BasePostgresEntity { private List attendances = new ArrayList<>(); /** - * μ„Έμ…˜ μƒνƒœλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. - * - UPCOMING: ν™œμ„±ν™”λ˜μ§€ μ•Šμ€ μƒνƒœ - * - OPEN: κ΄€λ¦¬μžκ°€ ν™œμ„±ν™”ν•œ μƒνƒœ (체크인 κ°€λŠ₯) - * - CLOSED: κ΄€λ¦¬μžκ°€ μ’…λ£Œν•œ μƒνƒœ (체크인 λΆˆκ°€) + * ν˜„μž¬ μ„Έμ…˜ μƒνƒœ 계산 */ - public SessionStatus getCurrentStatus() { - return this.status; + 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() { - return SessionStatus.OPEN.equals(this.status); + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(startsAt) && now.isBefore(getEndsAt()); + } + + /** + * μ„Έμ…˜ μ’…λ£Œ μ‹œκ°„ 계산 + */ + public LocalDateTime getEndsAt() { + return startsAt.plusSeconds(windowSeconds != null ? windowSeconds : 1800); + } + + /** + * 남은 μ‹œκ°„ 계산 (μ΄ˆλ‹¨μœ„) + */ + public long getRemainingSeconds() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime endsAt = getEndsAt(); + + if (now.isAfter(endsAt)) { + return 0; + } + + return java.time.Duration.between(now, endsAt).getSeconds(); } } 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 c634b7f8..64c6fb9e 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 @@ -35,25 +35,28 @@ public class AttendanceRoundService { * λΌμš΄λ“œ 생성 */ public AttendanceRoundResponse createRound(UUID sessionId, AttendanceRoundRequest request) { - log.info("πŸ“‹ λΌμš΄λ“œ 생성 μš”μ²­: sessionId={}, date={}, startTime={}, availableMinutes={}", - sessionId, request.getDate(), request.getStartTime(), request.getAvailableMinutes()); + log.info("πŸ“‹ λΌμš΄λ“œ 생성 μš”μ²­: sessionId={}, roundDate={}, startTime={}, allowedMinutes={}", + sessionId, request.getRoundDate(), request.getStartTime(), request.getAllowedMinutes()); AttendanceSession session = attendanceSessionRepository.findById(sessionId) .orElseThrow(() -> new IllegalArgumentException("μ„Έμ…˜μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: " + sessionId)); try { - // ν΄λΌμ΄μ–ΈνŠΈκ°€ μ œκ³΅ν•œ λ‚ μ§œλ₯Ό μ‚¬μš©ν•˜κ³ , μ—†μœΌλ©΄ μ„œλ²„μ˜ ν˜„μž¬ λ‚ μ§œλ₯Ό κΈ°λ³Έκ°’μœΌλ‘œ μ‚¬μš© - LocalDate roundDate = request.getDate() != null ? request.getDate() : LocalDate.now(); + // ν΄λΌμ΄μ–ΈνŠΈκ°€ 보낸 λ‚ μ§œ λŒ€μ‹  μ„œλ²„μ˜ ν˜„μž¬ λ‚ μ§œλ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹œκ°„λŒ€ 차이 λ°©μ§€ + LocalDate roundDate = request.getRoundDate(); + if (roundDate == null) { + roundDate = LocalDate.now(); + } LocalTime requestStartTime = request.getStartTime(); - log.info("πŸ“… μ‹œκ°„λŒ€ 정보: ν΄λΌμ΄μ–ΈνŠΈ date={}, μ‚¬μš©ν•  roundDate={}, μš”μ²­ startTime={}", - request.getDate(), roundDate, requestStartTime); + log.info("πŸ“… μ‹œκ°„λŒ€ 정보: ν΄λΌμ΄μ–ΈνŠΈ roundDate={}, μ„œλ²„ today={}, μš”μ²­ startTime={}", + request.getRoundDate(), roundDate, requestStartTime); AttendanceRound round = AttendanceRound.builder() .attendanceSession(session) - .roundDate(roundDate) // ν΄λΌμ΄μ–ΈνŠΈ λ‚ μ§œλ₯Ό μš°μ„  μ‚¬μš© + .roundDate(roundDate) .startTime(requestStartTime) - .allowedMinutes(request.getAvailableMinutes() != null ? request.getAvailableMinutes() : 30) + .allowedMinutes(request.getAllowedMinutes() != null ? request.getAllowedMinutes() : 30) .roundStatus(RoundStatus.UPCOMING) .build(); @@ -112,9 +115,9 @@ public AttendanceRoundResponse updateRound(UUID roundId, AttendanceRoundRequest .orElseThrow(() -> new IllegalArgumentException("λΌμš΄λ“œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: " + roundId)); round.updateRoundInfo( - request.getDate(), + request.getRoundDate(), request.getStartTime(), - request.getAvailableMinutes() + request.getAllowedMinutes() ); AttendanceRound updated = attendanceRoundRepository.save(round); 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 0ad2b717..df5a32c2 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 @@ -75,27 +75,24 @@ public AttendanceResponse checkIn(UUID sessionId, AttendanceRequest request, UUI } - // μ„Έμ…˜μ˜ ν™œμ„± λΌμš΄λ“œλ₯Ό 찾음 LocalDateTime now = LocalDateTime.now(); - 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("ν˜„μž¬ μ§„ν–‰ 쀑인 λΌμš΄λ“œκ°€ μ—†μŠ΅λ‹ˆλ‹€")); + if (now.isBefore(session.getStartsAt())) { + throw new IllegalStateException("아직 μΆœμ„ μ‹œκ°„μ΄ μ•„λ‹™λ‹ˆλ‹€"); + } + + LocalDateTime endTime = session.getEndsAt(); + if (now.isAfter(endTime)) { + throw new IllegalStateException("μΆœμ„ μ‹œκ°„μ΄ μ’…λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€"); + } // μ‹œμž‘ ν›„ 5λΆ„ μ΄λ‚΄λŠ” 정상 μΆœμ„, μ΄ν›„λŠ” 지각 - LocalDateTime roundStart = LocalDateTime.of(activeRound.getRoundDate(), activeRound.getStartTime()); - LocalDateTime lateThreshold = roundStart.plusMinutes(5); + LocalDateTime lateThreshold = session.getStartsAt().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()) @@ -104,7 +101,6 @@ public AttendanceResponse checkIn(UUID sessionId, AttendanceRequest request, UUI .deviceInfo(request.getDeviceInfo()) .build(); - attendance = attendanceRepository.save(attendance); log.info("μΆœμ„ 체크인 μ™„λ£Œ: μ‚¬μš©μž={}, μƒνƒœ={}", user.getName(), status); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java index 2f06c8a4..ba31eaf7 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java @@ -13,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -33,7 +35,7 @@ public class AttendanceSessionService { * - κΈ°λ³Έ μƒνƒœ UPCOMING 으둜 μ„€μ • */ public AttendanceSessionResponse createSession(AttendanceSessionRequest request) { - log.info("μΆœμ„ μ„Έμ…˜ 생성 μ‹œμž‘: 제λͺ©={}, κΈ°λ³Έμ‹œκ°„={}", request.getTitle(), request.getDefaultStartTime()); + log.info("μΆœμ„ μ„Έμ…˜ 생성 μ‹œμž‘: 제λͺ©={}", request.getTitle()); String code = generateUniqueCode(); Location location = null; @@ -48,8 +50,8 @@ public AttendanceSessionResponse createSession(AttendanceSessionRequest request) AttendanceSession session = AttendanceSession.builder() .title(request.getTitle()) - .defaultStartTime(request.getDefaultStartTime()) - .defaultAvailableMinutes(request.getDefaultAvailableMinutes()) + .startsAt(request.getStartsAt()) + .windowSeconds(request.getWindowSeconds()) .code(code) .rewardPoints(request.getRewardPoints()) .location(location) @@ -78,14 +80,13 @@ public AttendanceSessionResponse getSessionById(UUID sessionId) { /** * λͺ¨λ“  μ„Έμ…˜ λͺ©λ‘ 쑰회 * - κ΄€λ¦¬μžμš©, 곡개/λΉ„κ³΅κ°œ λͺ¨λ‘ 포함 - * - 생성 λ‚ μ§œ μ—­μˆœμœΌλ‘œ μ •λ ¬ + * - μ΅œμ‹  순으둜 μ •λ ¬ */ @Transactional(readOnly = true) public List getAllSessions() { - List sessions = attendanceSessionRepository.findAll(); + List sessions = attendanceSessionRepository.findAllByOrderByStartsAtDesc(); return sessions.stream() - .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) .map(this::convertToResponse) .collect(Collectors.toList()); } @@ -93,14 +94,14 @@ public List getAllSessions() { /** * 곡개 μ„Έμ…˜ λͺ©λ‘ 쑰회 * - 학생듀이 λ³Ό 수 μžˆλŠ” λͺ¨λ“  μ„Έμ…˜λ§Œ 쑰회 - * - 생성 λ‚ μ§œ μ—­μˆœμœΌλ‘œ μ •λ ¬ + * - μ΅œμ‹  순으둜 μ •λ ¬ */ @Transactional(readOnly = true) public List getPublicSessions() { - List sessions = attendanceSessionRepository.findAll(); + List sessions = attendanceSessionRepository + .findAllByOrderByStartsAtDesc(); return sessions.stream() - .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) .map(this::convertToResponse) .collect(Collectors.toList()); } @@ -108,22 +109,28 @@ public List getPublicSessions() { /** * ν™œμ„± μ„Έμ…˜ λͺ©λ‘ 쑰회 * - ν˜„μž¬ 체크인 κ°€λŠ₯ν•œ μ„Έμ…˜λ“€λ§Œ 필터링 - * - μƒνƒœκ°€ OPEN인 μ„Έμ…˜λ§Œ λ°˜ν™˜ + * - μ‹œμž‘ μ‹œκ°„ ~ μ’…λ£Œ μ‹œκ°„ λ²”μœ„ λ‚΄ μ„Έμ…˜ */ @Transactional(readOnly = true) public List getActiveSessions() { - List allSessions = attendanceSessionRepository.findAll(); + LocalDateTime now = LocalDateTime.now(); + List allSessions = attendanceSessionRepository.findAllByOrderByStartsAtDesc(); return allSessions.stream() - .filter(session -> SessionStatus.OPEN.equals(session.getStatus())) - .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) - .map(this::convertToResponse) - .collect(Collectors.toList()); + .filter(session -> { + if (session.getStatus() != SessionStatus.OPEN) { + return false; + } + LocalDateTime endTime = session.getStartsAt().plusSeconds(session.getWindowSeconds()); + return !now.isBefore(session.getStartsAt()) && now.isBefore(endTime); + }) + .map(this::convertToResponse) + .collect(Collectors.toList()); } /** * μ„Έμ…˜ 정보 μˆ˜μ • - * - 제λͺ©, κΈ°λ³Έ μ‹œκ°„, μœ„μΉ˜, 반경 λ“± μˆ˜μ • κ°€λŠ₯ + * - 제λͺ©, μ‹œκ°„, μœ„μΉ˜, 반경 λ“± μˆ˜μ • κ°€λŠ₯ * - μ½”λ“œλŠ” λ³€κ²½λ˜μ§€ μ•ŠμŒ (λ³΄μ•ˆμƒ 이유) */ public AttendanceSessionResponse updateSession(UUID sessionId, AttendanceSessionRequest request) { @@ -145,8 +152,8 @@ public AttendanceSessionResponse updateSession(UUID sessionId, AttendanceSession session = session.toBuilder() .title(request.getTitle()) - .defaultStartTime(request.getDefaultStartTime()) - .defaultAvailableMinutes(request.getDefaultAvailableMinutes()) + .startsAt(request.getStartsAt()) + .windowSeconds(request.getWindowSeconds()) .rewardPoints(request.getRewardPoints()) .location(location) .build(); @@ -177,7 +184,7 @@ public void deleteSession(UUID sessionId) { /** * μ„Έμ…˜ μˆ˜λ™ ν™œμ„±ν™” * - μ„Έμ…˜ μƒνƒœλ₯Ό OPEN으둜 λ³€κ²½ - * - 체크인 ν™œμ„±ν™” + * - μ‹œκ°„κ³Ό 관계없이 체크인 ν™œμ„±ν™” */ public void activateSession(UUID sessionId) { log.info("μΆœμ„ μ„Έμ…˜ ν™œμ„±ν™” μ‹œμž‘: μ„Έμ…˜ID={}", sessionId); @@ -187,6 +194,7 @@ public void activateSession(UUID sessionId) { session = session.toBuilder() .status(SessionStatus.OPEN) + .startsAt(LocalDateTime.now()) .build(); attendanceSessionRepository.save(session); @@ -271,8 +279,9 @@ private String generateRandomCode() { /** * AttendanceSession μ—”ν‹°ν‹°λ₯Ό Response DTO둜 λ³€ν™˜ - * - κΈ°λ³Έ μ„Έμ…˜ 정보: 제λͺ©, κΈ°λ³Έ μ‹œμž‘ μ‹œκ°„, μΆœμ„ 인정 μ‹œκ°„, 보상 포인트 + * - κΈ°λ³Έ μ„Έμ…˜ 정보: 제λͺ©, μ‹œμž‘ μ‹œκ°„, μΆœμ„ 인정 μ‹œκ°„, 보상 포인트 * - μœ„μΉ˜ 정보: location 객체 (lat, lng) + * - 곡개 μ—¬λΆ€: isVisible boolean */ private AttendanceSessionResponse convertToResponse(AttendanceSession session) { // μœ„μΉ˜ 정보 λ³€ν™˜ (location이 μ‘΄μž¬ν•˜λ©΄ LocationInfo 객체 생성, μ—†μœΌλ©΄ null) @@ -284,15 +293,21 @@ private AttendanceSessionResponse convertToResponse(AttendanceSession session) { .build(); } + // defaultAvailableMinutes: windowSecondsλ₯Ό λΆ„ λ‹¨μœ„λ‘œ λ³€ν™˜ + Integer defaultAvailableMinutes = (int) (session.getWindowSeconds() / 60); + + // defaultStartTime: startsAtμ—μ„œ μ‹œκ°„ λΆ€λΆ„λ§Œ μΆ”μΆœ + LocalTime defaultStartTime = session.getStartsAt().toLocalTime(); + return AttendanceSessionResponse.builder() .attendanceSessionId(session.getAttendanceSessionId()) .title(session.getTitle()) .location(location) - .defaultStartTime(session.getDefaultStartTime()) - .defaultAvailableMinutes(session.getDefaultAvailableMinutes()) + .defaultStartTime(defaultStartTime) + .defaultAvailableMinutes(defaultAvailableMinutes) .rewardPoints(session.getRewardPoints()) .build(); - } + } } diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java index 93e3487a..58fc298a 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/controller/AttendanceRoundControllerTest.java @@ -70,14 +70,14 @@ void createRound_success() throws Exception { LocalTime startTime = LocalTime.of(14, 0); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(roundDate) + .roundDate(roundDate) .startTime(startTime) - .availableMinutes(30) + .allowedMinutes(30) .build(); AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .date(roundDate) + .roundDate(roundDate) .startTime(startTime) .availableMinutes(30) .status("upcoming") @@ -92,7 +92,7 @@ void createRound_success() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.roundId").value(roundId.toString())) - .andExpect(jsonPath("$.date").value(roundDate.toString())) + .andExpect(jsonPath("$.roundDate").value(roundDate.toString())) .andExpect(jsonPath("$.startTime").value("14:00:00")) .andExpect(jsonPath("$.availableMinutes").value(30)) .andExpect(jsonPath("$.status").value("upcoming")); @@ -104,9 +104,9 @@ void createRound_success() throws Exception { void createRound_fail_noPermission() throws Exception { // given AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(LocalDate.now()) + .roundDate(LocalDate.now()) .startTime(LocalTime.of(14, 0)) - .availableMinutes(30) + .allowedMinutes(30) .build(); // when & then @@ -126,7 +126,7 @@ void getRound_success() throws Exception { AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .date(roundDate) + .roundDate(roundDate) .startTime(startTime) .availableMinutes(30) .status("active") @@ -152,7 +152,7 @@ void getRoundsBySession_success() throws Exception { AttendanceRoundResponse round1 = AttendanceRoundResponse.builder() .roundId(UUID.randomUUID()) - .date(roundDate) + .roundDate(roundDate) .startTime(startTime) .availableMinutes(30) .status("active") @@ -160,7 +160,7 @@ void getRoundsBySession_success() throws Exception { AttendanceRoundResponse round2 = AttendanceRoundResponse.builder() .roundId(UUID.randomUUID()) - .date(roundDate.plusDays(7)) + .roundDate(roundDate.plusDays(7)) .startTime(startTime) .availableMinutes(30) .status("upcoming") @@ -186,14 +186,14 @@ void updateRound_success() throws Exception { LocalTime newStartTime = LocalTime.of(15, 0); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(newDate) + .roundDate(newDate) .startTime(newStartTime) - .availableMinutes(45) + .allowedMinutes(45) .build(); AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .date(newDate) + .roundDate(newDate) .startTime(newStartTime) .availableMinutes(45) .status("upcoming") @@ -207,7 +207,7 @@ void updateRound_success() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.date").value(newDate.toString())) + .andExpect(jsonPath("$.roundDate").value(newDate.toString())) .andExpect(jsonPath("$.startTime").value("15:00:00")) .andExpect(jsonPath("$.availableMinutes").value(45)); } @@ -320,7 +320,7 @@ void getRoundByDate_success() throws Exception { AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .date(targetDate) + .roundDate(targetDate) .startTime(startTime) .availableMinutes(30) .status("active") diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundServiceTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundServiceTest.java index d97a41e1..b57c36c1 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceRoundServiceTest.java @@ -50,9 +50,9 @@ void createRound_success() { LocalTime startTime = LocalTime.of(14, 0); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(roundDate) + .roundDate(roundDate) .startTime(startTime) - .availableMinutes(30) + .allowedMinutes(30) .build(); AttendanceSession session = AttendanceSession.builder() @@ -80,7 +80,7 @@ void createRound_success() { // then assertAll( () -> assertThat(response.getRoundId()).isNotNull(), - () -> assertThat(response.getDate()).isEqualTo(roundDate), + () -> assertThat(response.getRoundDate()).isEqualTo(roundDate), () -> assertThat(response.getStartTime()).isEqualTo(startTime), () -> assertThat(response.getAvailableMinutes()).isEqualTo(30) ); @@ -95,9 +95,9 @@ void createRound_fail_sessionNotFound() { // given UUID sessionId = UUID.randomUUID(); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(LocalDate.now().plusDays(1)) + .roundDate(LocalDate.now().plusDays(1)) .startTime(LocalTime.of(14, 0)) - .availableMinutes(30) + .allowedMinutes(30) .build(); when(attendanceSessionRepository.findById(sessionId)).thenReturn(Optional.empty()); @@ -132,7 +132,7 @@ void getRound_success() { // then assertAll( () -> assertThat(response.getRoundId()).isEqualTo(roundId), - () -> assertThat(response.getDate()).isEqualTo(roundDate), + () -> assertThat(response.getRoundDate()).isEqualTo(roundDate), () -> assertThat(response.getStartTime()).isEqualTo(startTime) ); } @@ -185,8 +185,8 @@ void getRoundsBySession_success() { // then assertAll( () -> assertThat(responses).hasSize(2), - () -> assertThat(responses.get(0).getDate()).isEqualTo(date1), - () -> assertThat(responses.get(1).getDate()).isEqualTo(date2) + () -> assertThat(responses.get(0).getRoundDate()).isEqualTo(date1), + () -> assertThat(responses.get(1).getRoundDate()).isEqualTo(date2) ); } @@ -199,9 +199,9 @@ void updateRound_success() { LocalTime newTime = LocalTime.of(15, 0); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .date(newDate) + .roundDate(newDate) .startTime(newTime) - .availableMinutes(45) + .allowedMinutes(45) .build(); AttendanceRound existingRound = AttendanceRound.builder() @@ -226,7 +226,7 @@ void updateRound_success() { // then assertAll( - () -> assertThat(response.getDate()).isEqualTo(newDate), + () -> assertThat(response.getRoundDate()).isEqualTo(newDate), () -> assertThat(response.getStartTime()).isEqualTo(newTime), () -> assertThat(response.getAvailableMinutes()).isEqualTo(45) ); @@ -285,7 +285,7 @@ void getRoundByDate_success() { // then assertAll( - () -> assertThat(response.getDate()).isEqualTo(targetDate), + () -> assertThat(response.getRoundDate()).isEqualTo(targetDate), () -> assertThat(response.getStartTime()).isEqualTo(LocalTime.of(14, 0)) ); }