diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java index 2401afb..103135c 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AttendanceController.java @@ -2,93 +2,88 @@ import backend.pirocheck.Attendance.dto.request.MarkAttendanceReq; import backend.pirocheck.Attendance.dto.response.ApiResponse; -import backend.pirocheck.Attendance.dto.response.AttendanceCodeResponse; +import backend.pirocheck.Attendance.dto.response.AttendanceMarkResponse; import backend.pirocheck.Attendance.dto.response.AttendanceSlotRes; import backend.pirocheck.Attendance.dto.response.AttendanceStatusRes; -import backend.pirocheck.Attendance.entity.AttendanceCode; import backend.pirocheck.Attendance.service.AttendanceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; import java.util.List; -import java.util.Optional; @RestController @RequiredArgsConstructor @RequestMapping("/api/attendance") +@Tag(name = "출석관리", description = "학생용 출석 관련 API") public class AttendanceController { private final AttendanceService attendanceService; // 특정 유저의 출석 정보 + @Operation(summary = "사용자 출석 정보 조회", description = "특정 사용자의 전체 출석 정보를 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") + }) @GetMapping("/user") - public ApiResponse> getAttendanceByUserId(@RequestParam Long userId) { + public ApiResponse> getAttendanceByUserId( + @Parameter(description = "사용자 ID", required = true) + @RequestParam Long userId) { return ApiResponse.success(attendanceService.findByUserId(userId)); } // 특정 유저의 특정 일자 출석 정보 + @Operation(summary = "특정 날짜 출석 정보 조회", description = "특정 사용자의 특정 날짜 출석 정보를 조회합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 또는 날짜 정보를 찾을 수 없음") + }) @GetMapping("/user/date") public ApiResponse> getAttendanceByUserIdAndDate( + @Parameter(description = "사용자 ID", required = true) @RequestParam Long userId, + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", required = true) @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date ) { return ApiResponse.success(attendanceService.findByUserIdAndDate(userId, date)); } - // 출석체크 시작 - @PostMapping("/start") - public ApiResponse postAttendance() { - AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(); - return ApiResponse.success(AttendanceCodeResponse.from(code)); - } - - // 현재 활성화된 출석코드 조회 - @GetMapping("/active-code") - public ApiResponse getActiveCode() { - Optional codeOpt = attendanceService.getActiveAttendanceCode(); - - if (codeOpt.isEmpty()) { - return ApiResponse.error("현재 활성화된 출석코드가 없습니다"); - } - - return ApiResponse.success(AttendanceCodeResponse.from(codeOpt.get())); - } - // 출석코드 비교 + @Operation(summary = "출석 체크", description = "출석 코드를 입력하여 출석을 체크합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 성공 또는 이미 출석 완료"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 출석 코드 또는 출석 체크 진행중이 아님") + }) @PostMapping("/mark") - public ApiResponse markAttendance(@RequestBody MarkAttendanceReq req) { - String result = attendanceService.markAttendance(req.getUserId(), req.getCode()); - - if (result.equals("출석이 성공적으로 처리되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } - } - - // 출석체크 종료 (코드 직접 전달) - @PutMapping("/expire") - public ApiResponse expireAttendance(@RequestParam String code) { - String result = attendanceService.exprireAttendanceCode(code); + public ApiResponse markAttendance( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "출석 체크 요청", required = true, + content = @Content(schema = @Schema(implementation = MarkAttendanceReq.class))) + @RequestBody MarkAttendanceReq req) { + AttendanceMarkResponse response = attendanceService.markAttendance(req.getUserId(), req.getCode()); - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } - } - - // 출석체크 종료 (가장 최근 활성화된 코드 자동 만료) - @PutMapping("/expire-latest") - public ApiResponse expireLatestAttendance() { - String result = attendanceService.expireLatestAttendanceCode(); + // statusCode가 SUCCESS 또는 ALREADY_MARKED인 경우 성공으로 처리 + boolean isSuccess = "SUCCESS".equals(response.getStatusCode()) || + "ALREADY_MARKED".equals(response.getStatusCode()); - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); + if (isSuccess) { + return ApiResponse.success(response); } else { - return ApiResponse.error(result); + // 그 외의 경우 (NO_ACTIVE_SESSION, CODE_EXPIRED, ERROR)는 오류로 처리 + return ApiResponse.builder() + .success(false) + .message(response.getMessage()) + .data(response) + .build(); } } } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java index 779ead3..450728e 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/MarkAttendanceReq.java @@ -1,9 +1,14 @@ package backend.pirocheck.Attendance.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(description = "출석 체크 요청") public class MarkAttendanceReq { + @Schema(description = "사용자 ID", example = "1") private Long userId; + + @Schema(description = "출석 코드", example = "1234") private String code; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java index 4fe0b37..ca3dc4b 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceCodeResponse.java @@ -1,6 +1,7 @@ package backend.pirocheck.Attendance.dto.response; import backend.pirocheck.Attendance.entity.AttendanceCode; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,10 +13,18 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@Schema(description = "출석 코드 응답") public class AttendanceCodeResponse { + @Schema(description = "출석 코드", example = "1234") private String code; + + @Schema(description = "출석 날짜", example = "2025-06-24") private LocalDate date; + + @Schema(description = "출석 차시 (1, 2, 3)", example = "1") private int order; + + @Schema(description = "만료 여부", example = "false") private boolean isExpired; public static AttendanceCodeResponse from(AttendanceCode attendanceCode) { diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java index aa1a5cc..8a804bd 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceSlotRes.java @@ -1,5 +1,6 @@ package backend.pirocheck.Attendance.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -7,8 +8,11 @@ @Getter @Setter @AllArgsConstructor +@Schema(description = "출석 차시별 상태") public class AttendanceSlotRes { + @Schema(description = "출석 차시 (1, 2, 3)", example = "1") private int order; + + @Schema(description = "출석 여부", example = "true") private boolean status; - } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java index 7a0cfa4..6716f8a 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/response/AttendanceStatusRes.java @@ -1,5 +1,6 @@ package backend.pirocheck.Attendance.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -8,7 +9,14 @@ @Getter @Setter +@Schema(description = "사용자 출석 상태") public class AttendanceStatusRes { + @Schema(description = "출석 날짜", example = "2025-06-24") private LocalDate date; + + @Schema(description = "주차", example = "1") + private int week; + + @Schema(description = "출석 차시별 상태 목록") private List slots; } diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java index de65f49..ae2d5a8 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java @@ -3,6 +3,7 @@ import backend.pirocheck.User.entity.Role; import backend.pirocheck.User.entity.User; import backend.pirocheck.User.repository.UserRepository; +import backend.pirocheck.Attendance.dto.response.AttendanceMarkResponse; import backend.pirocheck.Attendance.dto.response.AttendanceSlotRes; import backend.pirocheck.Attendance.dto.response.AttendanceStatusRes; import backend.pirocheck.Attendance.entity.Attendance; @@ -43,6 +44,11 @@ public AttendanceCode generateCodeAndCreateAttendances() { // 오늘 생성된 출석코드 개수 = 현재까지 생성된 차시 수 + 1 (MAX=3) int currentOrder = attendanceCodeRepository.countByDate(today) + 1; + + // 하루 최대 3회 출석 체크만 허용 + if (currentOrder > 3) { + throw new IllegalStateException("하루에 최대 3회까지만 출석 체크를 진행할 수 있습니다."); + } // 1. 출석 코드 생성 String code = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 10000)); @@ -119,25 +125,38 @@ public String exprireAttendanceCode(String code) { // 출석처리 함수 @Transactional - public String markAttendance(Long userId, String inputCode) { - // 1. 출석코드 일치 비교 - Optional validCodeOpt = attendanceCodeRepository.findByCodeAndDate(inputCode, LocalDate.now()); - + public AttendanceMarkResponse markAttendance(Long userId, String inputCode) { + // 오늘 날짜 + LocalDate today = LocalDate.now(); + + // 현재 활성화된 출석 코드가 있는지 확인 + List activeCodes = attendanceCodeRepository.findByDateAndIsExpiredFalse(today); + + // 활성화된 출석 코드가 없는 경우 + if (activeCodes.isEmpty()) { + return AttendanceMarkResponse.noActiveSession(); + } + + // 입력한 출석 코드와 일치하는 코드가 있는지 확인 + Optional validCodeOpt = attendanceCodeRepository.findByCodeAndDate(inputCode, today); + + // 입력한 출석 코드가 존재하지 않는 경우 if (validCodeOpt.isEmpty()) { - return "출석 코드가 존재하지 않습니다. 현재 출석 체크가 진행중이 아닙니다"; + return AttendanceMarkResponse.invalidCode(); } AttendanceCode code = validCodeOpt.get(); + // 입력한 출석 코드가 만료된 경우 if (code.isExpired()) { - return "출석 코드가 만료되었습니다"; + return AttendanceMarkResponse.codeExpired(); } // 2. 해당 유저의 출석 레코드 조회 Optional attendanceOpt = attendanceRepository.findByUserIdAndDateAndOrder(userId, code.getDate(), code.getOrder()); if (attendanceOpt.isEmpty()) { - return "출석 정보를 찾을 수 없습니다"; + return AttendanceMarkResponse.error("출석 정보를 찾을 수 없습니다"); } // 3. 출석 처리 @@ -145,13 +164,13 @@ public String markAttendance(Long userId, String inputCode) { // 이미 출석한 경우 if (attendance.isStatus()) { - return "이미 출석처리가 완료되었습니다"; + return AttendanceMarkResponse.alreadyMarked(); } attendance.setStatus(true); attendanceRepository.save(attendance); - return "출석이 성공적으로 처리되었습니다"; + return AttendanceMarkResponse.success(); } // 유저의 전체 출석 현황을 조회하는 함수 diff --git a/backend/pirocheck/src/main/resources/application.yml b/backend/pirocheck/src/main/resources/application.yml index b8b7096..bdef4aa 100644 --- a/backend/pirocheck/src/main/resources/application.yml +++ b/backend/pirocheck/src/main/resources/application.yml @@ -20,4 +20,20 @@ server: secure: false # HTTPS 전용 전송 (Https -> true로 바꿔야 함) same-site: Lax # CSRF 방지 timeout: 30m # 세션 타임아웃 30분 (30 minutes) - address: 0.0.0.0 \ No newline at end of file + address: 0.0.0.0 + +# Swagger 설정 +springdoc: + packages-to-scan: backend.pirocheck + default-consumes-media-type: application/json + default-produces-media-type: application/json + swagger-ui: + path: /swagger-ui.html + disable-swagger-default-url: true + display-request-duration: true + tags-sorter: alpha + operations-sorter: alpha + api-docs: + path: /api-docs + cache: + disabled: true \ No newline at end of file