Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.UUID;

@Getter
@Builder
Expand All @@ -23,6 +24,15 @@
)
public class AttendanceRoundRequest {

@NotNull(message = "세션 ID는 필수입니다")
@Schema(
description = "회차가 속할 세션의 ID",
example = "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
type = "string",
format = "uuid"
)
private UUID sessionId;

@Schema(
description = "라운드 진행 날짜 (YYYY-MM-DD 형식)",
example = "2025-11-06",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.sejongisc.backend.attendance.entity.AttendanceRound;
import org.sejongisc.backend.attendance.entity.RoundStatus;
import org.sejongisc.backend.attendance.entity.AttendanceStatus;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.UUID;

@Getter
Expand All @@ -21,7 +18,7 @@
@AllArgsConstructor
@Schema(
title = "출석 라운드 응답",
description = "출석 라운드의 상세 정보. 라운드 상태, 시간, 출석 현황 통계를 포함합니다."
description = "출석 라운드의 상세 정보. 라운드 날짜, 시간, 상태를 포함합니다."
)
public class AttendanceRoundResponse {

Expand All @@ -42,107 +39,40 @@ public class AttendanceRoundResponse {

@Schema(
description = "라운드 출석 시작 시간",
example = "10:00",
example = "10:00:00",
type = "string",
format = "time"
)
@JsonFormat(pattern = "HH:mm")
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime startTime;

@Schema(
description = "라운드 출석 종료 시간 (startTime + allowedMinutes)",
example = "10:30",
type = "string",
format = "time"
)
@JsonFormat(pattern = "HH:mm")
private LocalTime endTime;

@Schema(
description = "출석 가능한 시간 (분단위)",
example = "30"
)
private Integer allowedMinutes;

@Schema(
description = "라운드의 현재 상태. UPCOMING(시작 전), ACTIVE(진행 중), CLOSED(종료됨)",
example = "ACTIVE",
implementation = RoundStatus.class
)
private RoundStatus roundStatus;

@Schema(
description = "라운드의 이름/제목. 예: 1주차, 2주차 등",
example = "1주차"
)
private String roundName;

@Schema(
description = "정시 출석자 수",
example = "20"
)
private Long presentCount;

@Schema(
description = "지각 출석자 수",
example = "5"
)
private Long lateCount;
private Integer availableMinutes;

@Schema(
description = "결석자 수",
example = "3"
description = "라운드의 현재 상태. (upcoming, active, closed 등)",
example = "active"
)
private Long absentCount;

@Schema(
description = "총 출석자 수",
example = "28"
)
private Long totalAttendees;
private String status;

/**
* 엔티티를 DTO로 변환
* roundStatus는 실시간으로 계산되어 반환됨
* 출석 통계는 단일 루프로 효율적으로 계산됨
* status는 실시간으로 계산되어 반환됨
*/
public static AttendanceRoundResponse fromEntity(AttendanceRound round) {
// attendances 리스트가 null일 수 있으므로 방어
var attendances = round.getAttendances();
if (attendances == null) {
attendances = List.of();
}

// 단일 루프로 모든 통계를 효율적으로 계산
long presentCount = 0;
long lateCount = 0;
long absentCount = 0;

for (var attendance : attendances) {
if (attendance.getAttendanceStatus() == AttendanceStatus.PRESENT) {
presentCount++;
} else if (attendance.getAttendanceStatus() == AttendanceStatus.LATE) {
lateCount++;
} else if (attendance.getAttendanceStatus() == AttendanceStatus.ABSENT) {
absentCount++;
}
}

// 현재 시간 기준으로 라운드 상태를 실시간 계산
RoundStatus currentStatus = round.calculateCurrentStatus();
// RoundStatus.getValue()를 사용하여 명시적이고 안전한 변환
String statusString = round.calculateCurrentStatus().getValue();

return AttendanceRoundResponse.builder()
.roundId(round.getRoundId())
.roundDate(round.getRoundDate())
.startTime(round.getStartTime())
.endTime(round.getEndTime())
.allowedMinutes(round.getAllowedMinutes())
.roundStatus(currentStatus)
.roundName(round.getRoundName())
.presentCount(presentCount)
.lateCount(lateCount)
.absentCount(absentCount)
.totalAttendees((long) attendances.size())
.availableMinutes(round.getAllowedMinutes())
.status(statusString)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.sejongisc.backend.attendance.entity.SessionStatus;
import org.sejongisc.backend.attendance.entity.SessionVisibility;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.UUID;

@Getter
Expand All @@ -15,7 +13,7 @@
@AllArgsConstructor
@Schema(
title = "출석 세션 응답",
description = "출석 세션의 상세 정보. 세션 설정, 상태, 남은 시간, 참여자 수 등을 포함합니다."
description = "출석 세션의 상세 정보. 세션 설정, 기본 시간, 위치 등을 포함합니다."
)
public class AttendanceSessionResponse {

Expand All @@ -27,84 +25,50 @@ public class AttendanceSessionResponse {

@Schema(
description = "세션의 제목/이름",
example = "2024년 10월 동아리 정기 모임"
example = "금융 IT팀 세션"
)
private String title;

@Schema(
description = "출석 체크인이 가능한 시간 윈도우 (초 단위)",
example = "1800"
description = "세션 개최 위치 정보",
example = "{\"lat\": 37.5499, \"lng\": 127.0751}"
)
private Integer windowSeconds;
private LocationInfo location;

@Schema(
description = "출석 완료 시 지급할 포인트",
example = "10"
description = "세션의 기본 시작 시간",
example = "18:30:00"
)
private Integer rewardPoints;
@JsonFormat(pattern = "HH:mm:ss")
private LocalTime defaultStartTime;

@Schema(
description = "세션 개최 위치의 위도",
example = "37.4979"
description = "출석 인정 시간 (분 단위)",
example = "30"
)
private Double latitude;
private Integer defaultAvailableMinutes;

@Schema(
description = "세션 개최 위치의 경도",
example = "127.0276"
)
private Double longitude;

@Schema(
description = "GPS 기반 위치 검증 반경 (미터 단위)",
description = "출석 완료 시 지급할 포인트",
example = "100"
)
private Integer radiusMeters;

@Schema(
description = "세션의 공개 범위. PUBLIC(공개) 또는 PRIVATE(비공개)",
example = "PUBLIC",
implementation = SessionVisibility.class
)
private SessionVisibility visibility;

@Schema(
description = "세션의 현재 상태. UPCOMING(예정), OPEN(진행중), CLOSED(종료)",
example = "OPEN",
implementation = SessionStatus.class
)
private SessionStatus status;

@Schema(
description = "세션 레코드 생성 시간",
example = "2024-10-31 10:00:00"
)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;

@Schema(
description = "세션 레코드 최종 수정 시간",
example = "2024-10-31 11:30:00"
)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;

@Schema(
description = "현재부터 체크인 마감까지 남은 시간 (초 단위). 음수이면 마감됨",
example = "1234"
)
private Long remainingSeconds;
private Integer rewardPoints;

@Schema(
description = "현재 체크인이 가능한 상태인지 여부. " +
"true면 지금 체크인 가능, false면 불가능",
description = "세션의 공개 여부. true(공개) 또는 false(비공개)",
example = "true"
)
private boolean checkInAvailable;

@Schema(
description = "현재 세션에 참여한 학생 수",
example = "25"
)
private Integer participantCount;
private Boolean isVisible;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class LocationInfo {
@Schema(description = "위도", example = "37.5499")
private Double lat;

@Schema(description = "경도", example = "127.0751")
private Double lng;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
* 라운드(주차) 상태
*/
public enum RoundStatus {
UPCOMING("진행 예정"),
ACTIVE("진행 중"),
CLOSED("마감됨");
UPCOMING("진행 예정", "upcoming"),
ACTIVE("진행 중", "active"),
CLOSED("마감됨", "closed");

private final String description;
private final String value;

RoundStatus(String description) {
RoundStatus(String description, String value) {
this.description = description;
this.value = value;
}

public String getDescription() {
return description;
}

/**
* API 응답에 사용할 문자열 값 반환
* toString().toLowerCase()와 달리 명시적이고 안전함
*
* @return API 응답용 상태값 (lowercase)
*/
public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public AttendanceRoundResponse createRound(UUID sessionId, AttendanceRoundReques

AttendanceRound saved = attendanceRoundRepository.save(round);
session.getRounds().add(saved);
// 양방향 관계를 DB에 반영하기 위해 세션도 저장
attendanceSessionRepository.save(session);

log.info("✅ 라운드 생성 완료 - sessionId: {}, roundId: {}, roundDate: {}, roundStatus: {}",
sessionId, saved.getRoundId(), saved.getRoundDate(), saved.getRoundStatus());
Expand Down
Loading