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
@@ -1,5 +1,7 @@
package org.sejongisc.backend.attendance.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -20,6 +22,10 @@
@RequiredArgsConstructor
@RequestMapping("/api/attendance")
@Slf4j
@Tag(
name = "출석(Attendance) API",
description = "학생 출석 체크인 및 관리자 출석 현황 조회 관련 API"
)
public class AttendanceController {

private final AttendanceService attendanceService;
Expand All @@ -30,6 +36,12 @@ public class AttendanceController {
* - 위치 범위, 시간 위도우 검증 포함
* - 중복 출석 방지
*/
@Operation(
summary = "출석 체크인",
description = "학생이 출석 코드와 GPS 위치를 제시하여 출석 체크인을 진행합니다. " +
"세션의 지정된 위치 범위 내에 있어야 하며, 시간 윈도우 내에만 체크인이 가능합니다. " +
"시작 시간 5분 이내는 PRESENT, 이후는 LATE로 자동 판별됩니다."
)
@PostMapping("/sessions/{sessionId}/check-in")
public ResponseEntity<AttendanceResponse> checkIn(
@PathVariable UUID sessionId,
Expand All @@ -50,6 +62,11 @@ public ResponseEntity<AttendanceResponse> checkIn(
* - 특정 세션의 모든 출석 기록 조회
* - 출석 시간 순으로 정렬
*/
@Operation(
summary = "세션별 출석 목록 조회",
description = "특정 세션에 참가한 모든 학생의 출석 기록을 조회합니다. (관리자 전용) " +
"출석 시간 순으로 정렬되며, 각 학생의 상태, 체크인 시간, 포인트 등이 포함됩니다."
)
@GetMapping("/sessions/{sessionId}/attendances")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<List<AttendanceResponse>> getAttendancesBySession(@PathVariable UUID sessionId) {
Expand All @@ -65,6 +82,11 @@ public ResponseEntity<List<AttendanceResponse>> getAttendancesBySession(@PathVar
* - 로그인한 사용자의 모든 출석 기록 조회
* - 최신 순으로 정렬
*/
@Operation(
summary = "내 출석 기록 조회",
description = "로그인한 사용자의 모든 출석 기록을 최신 순으로 조회합니다. " +
"각 출석 기록에는 세션 정보, 출석 상태, 체크인 시간, 획득 포인트 등이 포함됩니다."
)
@GetMapping("/history")
public ResponseEntity<List<AttendanceResponse>> getMyAttendances(
@AuthenticationPrincipal CustomUserDetails userDetails) {
Expand All @@ -80,6 +102,12 @@ public ResponseEntity<List<AttendanceResponse>> getMyAttendances(
* - PRESENT/LATE/ABSENT 등으로 상태 변경
* - 수정 사유 기록 가능
*/
@Operation(
summary = "출석 상태 수정",
description = "특정 학생의 출석 상태를 변경합니다. (관리자 전용) " +
"PRESENT(출석), LATE(지각), ABSENT(결석), EXCUSED(사유결석) 등의 상태로 변경 가능하며, " +
"변경 사유를 함께 기록할 수 있습니다."
)
@PostMapping("/sessions/{sessionId}/attendances/{memberId}")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<AttendanceResponse> updateAttendanceStatus(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.sejongisc.backend.attendance.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -19,6 +21,10 @@
@RequiredArgsConstructor
@RequestMapping("/api/attendance/sessions")
@Slf4j
@Tag(
name = "출석 세션(Attendance Session) API",
description = "출석 세션 생성, 조회, 수정, 삭제 및 상태 관리 관련 API"
)
public class AttendanceSessionController {

private final AttendanceSessionService attendanceSessionService;
Expand All @@ -29,6 +35,12 @@ public class AttendanceSessionController {
* - GPS 위치 및 반경 설정
* - 시간 윈도우 설정
*/
@Operation(
summary = "출석 세션 생성",
description = "새로운 출석 세션을 생성합니다. (관리자 전용) " +
"6자리 랜덤 코드가 자동 생성되며, GPS 위치 정보, 시간 윈도우, " +
"보상 포인트 등을 설정할 수 있습니다."
)
@PostMapping
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<AttendanceSessionResponse> createSession(@Valid @RequestBody AttendanceSessionRequest request) {
Expand All @@ -46,6 +58,11 @@ public ResponseEntity<AttendanceSessionResponse> createSession(@Valid @RequestBo
* - 세션 ID로 상세 정보 조회
* - 남은 시간, 참여자 수 등 포함
*/
@Operation(
summary = "세션 상세 조회",
description = "세션 ID로 특정 세션의 상세 정보를 조회합니다. " +
"남은 체크인 시간, 현재 참여자 수, 세션 상태 등의 정보가 포함됩니다."
)
@GetMapping("/{sessionId}")
public ResponseEntity<AttendanceSessionResponse> getSession(@PathVariable UUID sessionId) {
log.info("출석 세션 조회: 세션ID={}", sessionId);
Expand All @@ -60,6 +77,11 @@ public ResponseEntity<AttendanceSessionResponse> getSession(@PathVariable UUID s
* - 학생이 출석 코드 입력 시 사용
* - 체크인 가능 여부 확인
*/
@Operation(
summary = "출석 코드로 세션 조회",
description = "6자리 출석 코드를 입력하여 해당 세션을 조회합니다. (학생용) " +
"체크인 가능 여부, 남은 시간 등의 정보를 확인할 수 있습니다."
)
@GetMapping("/code/{code}")
public ResponseEntity<AttendanceSessionResponse> getSessionByCode(@PathVariable String code) {
log.info("출석 코드로 세션 조회: 코드={}", code);
Expand All @@ -74,6 +96,11 @@ public ResponseEntity<AttendanceSessionResponse> getSessionByCode(@PathVariable
* - 최신 순으로 정렬
* - 공개/비공개 세션 모두 포함
*/
@Operation(
summary = "모든 세션 목록 조회",
description = "생성된 모든 출석 세션을 최신 순으로 조회합니다. " +
"공개/비공개 세션이 모두 포함되며, 관리자는 모든 세션을 볼 수 있습니다."
)
@GetMapping
public ResponseEntity<List<AttendanceSessionResponse>> getAllSessions() {
log.info("모든 출석 세션 조회");
Expand All @@ -88,6 +115,11 @@ public ResponseEntity<List<AttendanceSessionResponse>> getAllSessions() {
* - 학생들이 볼 수 있는 공개 세션만 조회
* - 최신 순으로 정렬
*/
@Operation(
summary = "공개 세션 목록 조회",
description = "학생들이 볼 수 있는 공개 세션들을 최신 순으로 조회합니다. " +
"비공개 세션은 제외됩니다."
)
@GetMapping("/public")
public ResponseEntity<List<AttendanceSessionResponse>> getPublicSessions() {
log.info("공개 출석 세션 조회");
Expand All @@ -102,6 +134,11 @@ public ResponseEntity<List<AttendanceSessionResponse>> getPublicSessions() {
* - 체크인 가능한 세션들만 조회
* - 시작시간 ~ 종료 시간 범위 내
*/
@Operation(
summary = "활성 세션 목록 조회",
description = "현재 체크인이 가능한 활성 세션들을 조회합니다. " +
"세션 시작 시간부터 시간 윈도우 종료까지 범위 내인 세션들만 조회됩니다."
)
@GetMapping("/active")
public ResponseEntity<List<AttendanceSessionResponse>> getActiveSessions() {
log.info("활성 출석 세션 조회");
Expand All @@ -115,6 +152,11 @@ public ResponseEntity<List<AttendanceSessionResponse>> getActiveSessions() {
* 태그별 세션 목록 조회
* - "금융IT", "동아리 전체" 등 태그로 필터링
*/
@Operation(
summary = "태그별 세션 목록 조회",
description = "특정 태그를 가진 세션들을 조회합니다. " +
"예: '금융IT', '동아리 전체', '스터디' 등으로 세션을 분류할 수 있습니다."
)
@GetMapping("/tag/{tag}")
public ResponseEntity<List<AttendanceSessionResponse>> getSessionsByTag(@PathVariable String tag) {
log.info("태그별 출석 세션 조회: 태그={}", tag);
Expand All @@ -128,6 +170,11 @@ public ResponseEntity<List<AttendanceSessionResponse>> getSessionsByTag(@PathVar
* 상태별 세션 목록 조회 (관리자용)
* - UPCOMING/OPEN/CLOSED 상태별 필터링
*/
@Operation(
summary = "상태별 세션 목록 조회",
description = "특정 상태의 세션들을 조회합니다. (관리자 전용) " +
"UPCOMING(예정), OPEN(진행중), CLOSED(종료) 상태별로 필터링할 수 있습니다."
)
@GetMapping("/status/{status}")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<List<AttendanceSessionResponse>> getSessionsByStatus(@PathVariable SessionStatus status) {
Expand All @@ -143,6 +190,12 @@ public ResponseEntity<List<AttendanceSessionResponse>> getSessionsByStatus(@Path
* - 제목, 시간, 위치, 반경 등 수정 가능
* - 코드는 변경 불가
*/
@Operation(
summary = "세션 정보 수정",
description = "세션의 기본 정보를 수정합니다. (관리자 전용) " +
"제목, 태그, 시간, GPS 위치, 반경, 포인트 등을 수정할 수 있으며, " +
"6자리 코드는 변경할 수 없습니다."
)
@PutMapping("/{sessionId}")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<AttendanceSessionResponse> updateSession(
Expand All @@ -163,6 +216,11 @@ public ResponseEntity<AttendanceSessionResponse> updateSession(
* - 세션 상태를 OPEN으로 변경
* - 체크인 수동 활성화
*/
@Operation(
summary = "세션 활성화",
description = "세션을 수동으로 활성화하여 즉시 체크인을 가능하게 합니다. (관리자 전용) " +
"세션 상태가 OPEN으로 변경되어 학생들이 체크인할 수 있습니다."
)
@PostMapping("/{sessionId}/activate")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<Void> activateSession(@PathVariable UUID sessionId) {
Expand All @@ -180,6 +238,11 @@ public ResponseEntity<Void> activateSession(@PathVariable UUID sessionId) {
* - 세션 상태를 CLOSED로 변경
* - 체크인 수동 종료
*/
@Operation(
summary = "세션 종료",
description = "세션을 종료합니다. (관리자 전용) " +
"세션 상태가 CLOSED로 변경되어 더 이상 체크인이 불가능합니다."
)
@PostMapping("/{sessionId}/close")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<Void> closeSession(@PathVariable UUID sessionId) {
Expand All @@ -197,6 +260,11 @@ public ResponseEntity<Void> closeSession(@PathVariable UUID sessionId) {
* - 세션 완전 삭제 (출석 기록도 함께 삭제)
* - 주의: 복구 불가
*/
@Operation(
summary = "세션 삭제",
description = "세션을 완전히 삭제합니다. (관리자 전용) " +
"⚠️ 주의: 해당 세션의 모든 출석 기록이 함께 삭제되며, 복구가 불가능합니다."
)
@DeleteMapping("/{sessionId}")
@PreAuthorize("hasRole('PRESIDENT') or hasRole('VICE_PRESIDENT')")
public ResponseEntity<Void> deleteSession(@PathVariable UUID sessionId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.sejongisc.backend.attendance.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -11,26 +12,58 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(
title = "출석 체크인 요청",
description = "학생이 출석 체크인 시 제출하는 요청 객체. 출석 코드, GPS 위치 정보를 포함합니다."
)
public class AttendanceRequest {

// === 출석 체크용 필드들 ===
@Schema(
description = "출석 세션의 6자리 코드. 관리자가 생성한 코드를 입력합니다.",
example = "ABC123",
minLength = 6,
maxLength = 6
)
@NotBlank(message = "출석 코드는 필수입니다")
@Size(min = 6, max = 6, message = "출석 코드는 6자리여야 합니다")
private String code;

@Schema(
description = "체크인 위치의 위도 (latitude). WGS84 좌표계 사용. 범위: -90 ~ 90",
example = "37.4979",
minimum = "-90.0",
maximum = "90.0"
)
@NotNull(message = "위도는 필수입니다")
@DecimalMin(value = "-90.0", message = "위도는 -90 이상이어야 합니다")
@DecimalMax(value = "90.0", message = "위도는 90 이하이어야 합니다")
private Double latitude;

@Schema(
description = "체크인 위치의 경도 (longitude). WGS84 좌표계 사용. 범위: -180 ~ 180",
example = "127.0276",
minimum = "-180.0",
maximum = "180.0"
)
@NotNull(message = "경도는 필수입니다")
@DecimalMin(value = "-180.0", message = "경도는 -180 이상이어야 합니다")
@DecimalMax(value = "180.0", message = "경도는 180 이하이어야 합니다")
private Double longitude;

@Schema(
description = "체크인 시 추가 메모. 선택 사항입니다.",
example = "교실 앞에서 체크인",
maxLength = 500
)
@Size(max = 500, message = "메모는 500자 이하여야 합니다")
private String note;

@Schema(
description = "체크인에 사용한 디바이스 정보. 예: 'iPhone 12', 'Android Pixel 6' 등",
example = "iPhone 14 Pro",
maxLength = 200
)
@Size(max = 200, message = "디바이스 정보는 200자 이하여야 합니다")
private String deviceInfo;

Expand Down
Loading