From 54281c539e43fa4e13d91e1057fc515128771b14 Mon Sep 17 00:00:00 2001 From: chanhyeok oh <125783546+ochanhyeok@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:48:45 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=EC=B6=9C=EC=84=9D=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceSessionController.java | 10 +-- .../dto/AttendanceSessionRequest.java | 32 +++++----- .../backend/attendance/entity/Attendance.java | 17 +++++- .../attendance/entity/AttendanceSession.java | 57 +++++------------ .../attendance/service/AttendanceService.java | 22 ++++--- .../service/AttendanceSessionService.java | 61 +++++++------------ 6 files changed, 89 insertions(+), 110 deletions(-) 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 fa45b245..289dae1d 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/AttendanceSessionRequest.java b/backend/src/main/java/org/sejongisc/backend/attendance/dto/AttendanceSessionRequest.java index d3a01520..a63fee2a 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,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 @@ -19,7 +20,7 @@ public class AttendanceSessionRequest { @Schema( description = "세션의 제목/이름", - example = "2024년 10월 동아리 정기 모임", + example = "금융 IT팀 세션", maxLength = 100 ) @NotBlank(message = "제목은 필수입니다") @@ -27,25 +28,26 @@ public class AttendanceSessionRequest { 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; @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; @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 e5459e01..c2d6649c 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,12 +65,25 @@ public class Attendance extends BasePostgresEntity { /** * 지각 여부 판단 + * - 라운드 기반: 라운드 시작 시간 이후이면 지각 + * - 세션 기반: 라운드 없이 진행되는 경우는 확인 불가 */ public boolean isLate() { - if (checkedAt == null || attendanceSession.getStartsAt() == null) { + if (checkedAt == null) { return false; } - return checkedAt.isAfter(attendanceSession.getStartsAt()); + + // 라운드 기반 체크인인 경우 + if (attendanceRound != null) { + LocalDateTime roundStartTime = LocalDateTime.of( + attendanceRound.getRoundDate(), + attendanceRound.getStartTime() + ); + return checkedAt.isAfter(roundStartTime); + } + + // 라운드 없는 경우 지각 판단 불가 + return false; } /** 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 83452dd6..bdde7b49 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,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; @@ -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" @@ -55,46 +57,19 @@ public class AttendanceSession extends BasePostgresEntity { private List 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 SessionStatus getCurrentStatus() { + return this.status; } /** - * 세션 종료 시간 계산 + * 체크인이 가능한 상태인지 확인합니다. */ 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 long getRemainingSeconds() { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime endsAt = getEndsAt(); - - if (now.isAfter(endsAt)) { - return 0; - } - - return java.time.Duration.between(now, endsAt).getSeconds(); + return SessionStatus.OPEN.equals(this.status); } } 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 df5a32c2..0ad2b717 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,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()) @@ -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); 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 ba31eaf7..2f06c8a4 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,8 +13,6 @@ 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; @@ -35,7 +33,7 @@ public class AttendanceSessionService { * - 기본 상태 UPCOMING 으로 설정 */ public AttendanceSessionResponse createSession(AttendanceSessionRequest request) { - log.info("출석 세션 생성 시작: 제목={}", request.getTitle()); + log.info("출석 세션 생성 시작: 제목={}, 기본시간={}", request.getTitle(), request.getDefaultStartTime()); String code = generateUniqueCode(); Location location = null; @@ -50,8 +48,8 @@ public AttendanceSessionResponse createSession(AttendanceSessionRequest request) AttendanceSession session = AttendanceSession.builder() .title(request.getTitle()) - .startsAt(request.getStartsAt()) - .windowSeconds(request.getWindowSeconds()) + .defaultStartTime(request.getDefaultStartTime()) + .defaultAvailableMinutes(request.getDefaultAvailableMinutes()) .code(code) .rewardPoints(request.getRewardPoints()) .location(location) @@ -80,13 +78,14 @@ public AttendanceSessionResponse getSessionById(UUID sessionId) { /** * 모든 세션 목록 조회 * - 관리자용, 공개/비공개 모두 포함 - * - 최신 순으로 정렬 + * - 생성 날짜 역순으로 정렬 */ @Transactional(readOnly = true) public List getAllSessions() { - List sessions = attendanceSessionRepository.findAllByOrderByStartsAtDesc(); + List sessions = attendanceSessionRepository.findAll(); return sessions.stream() + .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) .map(this::convertToResponse) .collect(Collectors.toList()); } @@ -94,14 +93,14 @@ public List getAllSessions() { /** * 공개 세션 목록 조회 * - 학생들이 볼 수 있는 모든 세션만 조회 - * - 최신 순으로 정렬 + * - 생성 날짜 역순으로 정렬 */ @Transactional(readOnly = true) public List getPublicSessions() { - List sessions = attendanceSessionRepository - .findAllByOrderByStartsAtDesc(); + List sessions = attendanceSessionRepository.findAll(); return sessions.stream() + .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) .map(this::convertToResponse) .collect(Collectors.toList()); } @@ -109,28 +108,22 @@ public List getPublicSessions() { /** * 활성 세션 목록 조회 * - 현재 체크인 가능한 세션들만 필터링 - * - 시작 시간 ~ 종료 시간 범위 내 세션 + * - 상태가 OPEN인 세션만 반환 */ @Transactional(readOnly = true) public List getActiveSessions() { - LocalDateTime now = LocalDateTime.now(); - List allSessions = attendanceSessionRepository.findAllByOrderByStartsAtDesc(); + List allSessions = attendanceSessionRepository.findAll(); return allSessions.stream() - .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()); + .filter(session -> SessionStatus.OPEN.equals(session.getStatus())) + .sorted((a, b) -> b.getCreatedDate().compareTo(a.getCreatedDate())) + .map(this::convertToResponse) + .collect(Collectors.toList()); } /** * 세션 정보 수정 - * - 제목, 시간, 위치, 반경 등 수정 가능 + * - 제목, 기본 시간, 위치, 반경 등 수정 가능 * - 코드는 변경되지 않음 (보안상 이유) */ public AttendanceSessionResponse updateSession(UUID sessionId, AttendanceSessionRequest request) { @@ -152,8 +145,8 @@ public AttendanceSessionResponse updateSession(UUID sessionId, AttendanceSession session = session.toBuilder() .title(request.getTitle()) - .startsAt(request.getStartsAt()) - .windowSeconds(request.getWindowSeconds()) + .defaultStartTime(request.getDefaultStartTime()) + .defaultAvailableMinutes(request.getDefaultAvailableMinutes()) .rewardPoints(request.getRewardPoints()) .location(location) .build(); @@ -184,7 +177,7 @@ public void deleteSession(UUID sessionId) { /** * 세션 수동 활성화 * - 세션 상태를 OPEN으로 변경 - * - 시간과 관계없이 체크인 활성화 + * - 체크인 활성화 */ public void activateSession(UUID sessionId) { log.info("출석 세션 활성화 시작: 세션ID={}", sessionId); @@ -194,7 +187,6 @@ public void activateSession(UUID sessionId) { session = session.toBuilder() .status(SessionStatus.OPEN) - .startsAt(LocalDateTime.now()) .build(); attendanceSessionRepository.save(session); @@ -279,9 +271,8 @@ private String generateRandomCode() { /** * AttendanceSession 엔티티를 Response DTO로 변환 - * - 기본 세션 정보: 제목, 시작 시간, 출석 인정 시간, 보상 포인트 + * - 기본 세션 정보: 제목, 기본 시작 시간, 출석 인정 시간, 보상 포인트 * - 위치 정보: location 객체 (lat, lng) - * - 공개 여부: isVisible boolean */ private AttendanceSessionResponse convertToResponse(AttendanceSession session) { // 위치 정보 변환 (location이 존재하면 LocationInfo 객체 생성, 없으면 null) @@ -293,21 +284,15 @@ 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(defaultStartTime) - .defaultAvailableMinutes(defaultAvailableMinutes) + .defaultStartTime(session.getDefaultStartTime()) + .defaultAvailableMinutes(session.getDefaultAvailableMinutes()) .rewardPoints(session.getRewardPoints()) .build(); - } + } From 655b373768033050c2c35be7810e0861bda2e314 Mon Sep 17 00:00:00 2001 From: chanhyeok oh <125783546+ochanhyeok@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:54:43 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20roundDto=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AttendanceRoundController.java | 4 ++-- .../dto/AttendanceRoundRequest.java | 4 ++-- .../dto/AttendanceRoundResponse.java | 4 ++-- .../service/AttendanceRoundService.java | 23 ++++++++----------- 4 files changed, 16 insertions(+), 19 deletions(-) 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 58efd94c..2fcf5959 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(" - 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 상세: 시간={}, 분={}, 초={}", 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 8ea59e6f..73ec590d 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 roundDate; + private LocalDate date; @NotNull(message = "시작 시간은 필수입니다") @Schema( @@ -60,5 +60,5 @@ public class AttendanceRoundRequest { minimum = "1", maximum = "120" ) - private Integer allowedMinutes; + private Integer availableMinutes; } 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 3ba856f4..8aaf9124 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 roundDate; + private LocalDate date; @Schema( description = "라운드 출석 시작 시간", @@ -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) 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..c634b7f8 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,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(); @@ -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); From cd5c8fd05feb7095112ea1fc939de7d19b7da994 Mon Sep 17 00:00:00 2001 From: chanhyeok oh <125783546+ochanhyeok@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:57:41 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test:=20test=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceRoundControllerTest.java | 28 +++++++++---------- .../service/AttendanceRoundServiceTest.java | 24 ++++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) 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 58fc298a..93e3487a 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() - .roundDate(roundDate) + .date(roundDate) .startTime(startTime) - .allowedMinutes(30) + .availableMinutes(30) .build(); AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .roundDate(roundDate) + .date(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("$.roundDate").value(roundDate.toString())) + .andExpect(jsonPath("$.date").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() - .roundDate(LocalDate.now()) + .date(LocalDate.now()) .startTime(LocalTime.of(14, 0)) - .allowedMinutes(30) + .availableMinutes(30) .build(); // when & then @@ -126,7 +126,7 @@ void getRound_success() throws Exception { AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .roundDate(roundDate) + .date(roundDate) .startTime(startTime) .availableMinutes(30) .status("active") @@ -152,7 +152,7 @@ void getRoundsBySession_success() throws Exception { AttendanceRoundResponse round1 = AttendanceRoundResponse.builder() .roundId(UUID.randomUUID()) - .roundDate(roundDate) + .date(roundDate) .startTime(startTime) .availableMinutes(30) .status("active") @@ -160,7 +160,7 @@ void getRoundsBySession_success() throws Exception { AttendanceRoundResponse round2 = AttendanceRoundResponse.builder() .roundId(UUID.randomUUID()) - .roundDate(roundDate.plusDays(7)) + .date(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() - .roundDate(newDate) + .date(newDate) .startTime(newStartTime) - .allowedMinutes(45) + .availableMinutes(45) .build(); AttendanceRoundResponse response = AttendanceRoundResponse.builder() .roundId(roundId) - .roundDate(newDate) + .date(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("$.roundDate").value(newDate.toString())) + .andExpect(jsonPath("$.date").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) - .roundDate(targetDate) + .date(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 b57c36c1..d97a41e1 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() - .roundDate(roundDate) + .date(roundDate) .startTime(startTime) - .allowedMinutes(30) + .availableMinutes(30) .build(); AttendanceSession session = AttendanceSession.builder() @@ -80,7 +80,7 @@ void createRound_success() { // then assertAll( () -> assertThat(response.getRoundId()).isNotNull(), - () -> assertThat(response.getRoundDate()).isEqualTo(roundDate), + () -> assertThat(response.getDate()).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() - .roundDate(LocalDate.now().plusDays(1)) + .date(LocalDate.now().plusDays(1)) .startTime(LocalTime.of(14, 0)) - .allowedMinutes(30) + .availableMinutes(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.getRoundDate()).isEqualTo(roundDate), + () -> assertThat(response.getDate()).isEqualTo(roundDate), () -> assertThat(response.getStartTime()).isEqualTo(startTime) ); } @@ -185,8 +185,8 @@ void getRoundsBySession_success() { // then assertAll( () -> assertThat(responses).hasSize(2), - () -> assertThat(responses.get(0).getRoundDate()).isEqualTo(date1), - () -> assertThat(responses.get(1).getRoundDate()).isEqualTo(date2) + () -> assertThat(responses.get(0).getDate()).isEqualTo(date1), + () -> assertThat(responses.get(1).getDate()).isEqualTo(date2) ); } @@ -199,9 +199,9 @@ void updateRound_success() { LocalTime newTime = LocalTime.of(15, 0); AttendanceRoundRequest request = AttendanceRoundRequest.builder() - .roundDate(newDate) + .date(newDate) .startTime(newTime) - .allowedMinutes(45) + .availableMinutes(45) .build(); AttendanceRound existingRound = AttendanceRound.builder() @@ -226,7 +226,7 @@ void updateRound_success() { // then assertAll( - () -> assertThat(response.getRoundDate()).isEqualTo(newDate), + () -> assertThat(response.getDate()).isEqualTo(newDate), () -> assertThat(response.getStartTime()).isEqualTo(newTime), () -> assertThat(response.getAvailableMinutes()).isEqualTo(45) ); @@ -285,7 +285,7 @@ void getRoundByDate_success() { // then assertAll( - () -> assertThat(response.getRoundDate()).isEqualTo(targetDate), + () -> assertThat(response.getDate()).isEqualTo(targetDate), () -> assertThat(response.getStartTime()).isEqualTo(LocalTime.of(14, 0)) ); } From 0e04aa806e147a329ad1682634ec91351075ce09 Mon Sep 17 00:00:00 2001 From: chanhyeok oh <125783546+ochanhyeok@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:12:57 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EB=B3=80=EC=88=98=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AttendanceSessionRepository.java | 12 ------------ .../service/AttendanceRoundServiceTest.java | 1 - .../attendance/service/AttendanceServiceTest.java | 2 -- .../service/SessionLocationUpdateTest.java | 6 ------ 4 files changed, 21 deletions(-) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceSessionRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceSessionRepository.java index dff2c1de..9579e348 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceSessionRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceSessionRepository.java @@ -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 { - // 출석 코드로 세션 찾기 (학생 출석 체크) - Optional findByCode(String code); - - // 상태별 세션 조회 - List findByStatus(SessionStatus status); - - // 모든 세션을 최신순으로 조회 (관리자용) - List findAllByOrderByStartsAtDesc(); - // 코드 중복 체크 boolean existsByCode(String code); } 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..c4b8dc2e 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 @@ -59,7 +59,6 @@ void createRound_success() { .attendanceSessionId(sessionId) .title("테스트 세션") .code("123456") - .startsAt(LocalDateTime.now().plusDays(1)) .build(); AttendanceRound savedRound = AttendanceRound.builder() diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java index 57c55d76..3f6de440 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/service/AttendanceServiceTest.java @@ -66,8 +66,6 @@ public class AttendanceServiceTest { .attendanceSessionId(sessionId) .title("세투연 정기모임") .code(code) - .startsAt(now.minusMinutes(5)) - .windowSeconds(1800) .rewardPoints(10) .location(sessionLocation) .status(SessionStatus.OPEN) diff --git a/backend/src/test/java/org/sejongisc/backend/attendance/service/SessionLocationUpdateTest.java b/backend/src/test/java/org/sejongisc/backend/attendance/service/SessionLocationUpdateTest.java index a9219edc..4f63991f 100644 --- a/backend/src/test/java/org/sejongisc/backend/attendance/service/SessionLocationUpdateTest.java +++ b/backend/src/test/java/org/sejongisc/backend/attendance/service/SessionLocationUpdateTest.java @@ -59,8 +59,6 @@ void updateSessionLocation_success_withExistingLocation() { .attendanceSessionId(sessionId) .title("테스트 세션") .code("123456") - .startsAt(LocalDateTime.now().plusHours(1)) - .windowSeconds(1800) .location(existingLocation) .status(SessionStatus.UPCOMING) .build(); @@ -105,8 +103,6 @@ void updateSessionLocation_success_withoutExistingLocation() { .attendanceSessionId(sessionId) .title("테스트 세션") .code("123456") - .startsAt(LocalDateTime.now().plusHours(1)) - .windowSeconds(1800) .location(null) // 기존 위치 없음 .status(SessionStatus.UPCOMING) .build(); @@ -174,8 +170,6 @@ void updateSessionLocation_verifyRadiusPreservation() { .attendanceSessionId(sessionId) .title("테스트 세션") .code("123456") - .startsAt(LocalDateTime.now().plusHours(1)) - .windowSeconds(1800) .location(existingLocation) .status(SessionStatus.UPCOMING) .build(); From a05230da6bc4415feba6ac886f2d2d291d55773f Mon Sep 17 00:00:00 2001 From: chanhyeok oh <125783546+ochanhyeok@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:03:00 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EB=B3=80=EC=88=98=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceSessionController.java | 8 +++---- .../backend/attendance/entity/Attendance.java | 23 +++++++------------ .../attendance/entity/AttendanceSession.java | 15 ++++-------- 3 files changed, 16 insertions(+), 30 deletions(-) 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..d3af7fd3 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 @@ -113,13 +113,13 @@ public ResponseEntity> getPublicSessions() { /** * 현재 활성 세션 목록 조회 - * - 체크인 가능한 세션들만 조회 - * - 시작시간 ~ 종료 시간 범위 내 + * - 상태가 OPEN인 세션들만 조회 + * - 실제 체크인 가능 여부는 라운드의 상태로 판단 */ @Operation( summary = "활성 세션 목록 조회", - description = "현재 체크인이 가능한 활성 세션들을 조회합니다. " + - "세션 시작 시간부터 시간 윈도우 종료까지 범위 내인 세션들만 조회됩니다." + description = "현재 활성화된(OPEN 상태) 세션들을 조회합니다. " + + "실제 체크인 가능 여부는 세션 내 라운드의 시간 상태로 판단됩니다." ) @GetMapping("/active") public ResponseEntity> getActiveSessions() { 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..3bd82bef 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 @@ -64,28 +64,21 @@ public class Attendance extends BasePostgresEntity { // 지각 여부 계산 / 상태 업데이트 /** + /** * 지각 여부 판단 - * - 라운드 기반: 라운드 시작 시간 이후이면 지각 - * - 세션 기반: 라운드 없이 진행되는 경우는 확인 불가 + * - 라운드의 시작 시간 이후에 체크인했으면 지각 */ public boolean isLate() { - if (checkedAt == null) { + if (checkedAt == null || attendanceRound == null) { return false; } - - // 라운드 기반 체크인인 경우 - if (attendanceRound != null) { - LocalDateTime roundStartTime = LocalDateTime.of( - attendanceRound.getRoundDate(), - attendanceRound.getStartTime() - ); - return checkedAt.isAfter(roundStartTime); - } - - // 라운드 없는 경우 지각 판단 불가 - return false; + // 라운드의 시작 시간(LocalTime)과 체크인 시간(LocalDateTime)을 비교 + LocalDateTime roundStartDateTime = attendanceRound.getRoundDate() + .atTime(attendanceRound.getStartTime()); + return checkedAt.isAfter(roundStartDateTime); } + /** * 상태 업데이트 (관리자용) */ 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..41bddd7b 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 @@ -57,19 +57,12 @@ public class AttendanceSession extends BasePostgresEntity { private List attendances = new ArrayList<>(); /** - * 세션 상태를 반환합니다. - * - UPCOMING: 활성화되지 않은 상태 - * - OPEN: 관리자가 활성화한 상태 (체크인 가능) - * - CLOSED: 관리자가 종료한 상태 (체크인 불가) + * 세션의 현재 상태 반환 + * - 세션 상태는 수동으로 관리됨 (UPCOMING, OPEN, CLOSED) + * - 라운드의 상태는 라운드 엔티티에서 시간 기반으로 계산됨 */ - public SessionStatus getCurrentStatus() { + public SessionStatus getStatus() { return this.status; } - /** - * 체크인이 가능한 상태인지 확인합니다. - */ - public boolean isCheckInAvailable() { - return SessionStatus.OPEN.equals(this.status); - } }