Skip to content
Merged
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 @@ -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<List<AttendanceStatusRes>> getAttendanceByUserId(@RequestParam Long userId) {
public ApiResponse<List<AttendanceStatusRes>> 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<List<AttendanceSlotRes>> 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<AttendanceCodeResponse> postAttendance() {
AttendanceCode code = attendanceService.generateCodeAndCreateAttendances();
return ApiResponse.success(AttendanceCodeResponse.from(code));
}

// 현재 활성화된 출석코드 조회
@GetMapping("/active-code")
public ApiResponse<AttendanceCodeResponse> getActiveCode() {
Optional<AttendanceCode> 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<Void> 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<Void> expireAttendance(@RequestParam String code) {
String result = attendanceService.exprireAttendanceCode(code);
public ApiResponse<AttendanceMarkResponse> 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<Void> 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.<AttendanceMarkResponse>builder()
.success(false)
.message(response.getMessage())
.data(response)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package backend.pirocheck.Attendance.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@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;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package backend.pirocheck.Attendance.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

Expand All @@ -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<AttendanceSlotRes> slots;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -119,39 +125,52 @@ public String exprireAttendanceCode(String code) {

// 출석처리 함수
@Transactional
public String markAttendance(Long userId, String inputCode) {
// 1. 출석코드 일치 비교
Optional<AttendanceCode> validCodeOpt = attendanceCodeRepository.findByCodeAndDate(inputCode, LocalDate.now());

public AttendanceMarkResponse markAttendance(Long userId, String inputCode) {
// 오늘 날짜
LocalDate today = LocalDate.now();

// 현재 활성화된 출석 코드가 있는지 확인
List<AttendanceCode> activeCodes = attendanceCodeRepository.findByDateAndIsExpiredFalse(today);

// 활성화된 출석 코드가 없는 경우
if (activeCodes.isEmpty()) {
return AttendanceMarkResponse.noActiveSession();
}

// 입력한 출석 코드와 일치하는 코드가 있는지 확인
Optional<AttendanceCode> 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<Attendance> attendanceOpt = attendanceRepository.findByUserIdAndDateAndOrder(userId, code.getDate(), code.getOrder());

if (attendanceOpt.isEmpty()) {
return "출석 정보를 찾을 수 없습니다";
return AttendanceMarkResponse.error("출석 정보를 찾을 수 없습니다");
}

// 3. 출석 처리
Attendance attendance = attendanceOpt.get();

// 이미 출석한 경우
if (attendance.isStatus()) {
return "이미 출석처리가 완료되었습니다";
return AttendanceMarkResponse.alreadyMarked();
}

attendance.setStatus(true);
attendanceRepository.save(attendance);

return "출석이 성공적으로 처리되었습니다";
return AttendanceMarkResponse.success();
}

// 유저의 전체 출석 현황을 조회하는 함수
Expand Down
18 changes: 17 additions & 1 deletion backend/pirocheck/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,20 @@ server:
secure: false # HTTPS 전용 전송 (Https -> true로 바꿔야 함)
same-site: Lax # CSRF 방지
timeout: 30m # 세션 타임아웃 30분 (30 minutes)
address: 0.0.0.0
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