11package org .sejongisc .backend .attendance .controller ;
22
3+ import static org .sejongisc .backend .attendance .util .AuthUserUtil .requireUserId ;
4+
35import io .swagger .v3 .oas .annotations .Operation ;
46import io .swagger .v3 .oas .annotations .tags .Tag ;
57import jakarta .validation .Valid ;
1416import org .sejongisc .backend .common .auth .dto .CustomUserDetails ;
1517import org .springframework .http .ResponseEntity ;
1618import org .springframework .security .core .annotation .AuthenticationPrincipal ;
17- import org .springframework .web .bind .annotation .*;
19+ import org .springframework .web .bind .annotation .GetMapping ;
20+ import org .springframework .web .bind .annotation .PathVariable ;
21+ import org .springframework .web .bind .annotation .PostMapping ;
22+ import org .springframework .web .bind .annotation .PutMapping ;
23+ import org .springframework .web .bind .annotation .RequestBody ;
24+ import org .springframework .web .bind .annotation .RequestMapping ;
25+ import org .springframework .web .bind .annotation .RestController ;
1826
1927@ RestController
2028@ RequestMapping ("/api/attendance" )
@@ -30,21 +38,68 @@ public class AttendanceController {
3038 * POST /api/attendance/check-in
3139 * body: { "qrToken": "..." }
3240 */
33- @ Operation (summary = "체크인" , description = "qrToken으로 출석 체크인합니다. (세션 멤버)" )
41+ @ Operation (
42+ summary = "체크인" ,
43+ description = """
44+ ## 인증(JWT): **필요**
45+
46+ ## 요청 바디 ( `AttendanceRoundQrTokenRequest` )
47+ - **`qrToken`**: QR 토큰
48+
49+ ## 동작 설명
50+ - qrToken이 유효한지 검증
51+ - 출석 라운드가 ACTIVE 상태인지 검증
52+ - 해당 멤버가 출석 세션의 멤버인지 검증
53+ - 해당 세션의 allowedMinutes 내에 출석체크하면 AttendanceStatus가 PRESENT
54+ - allowedMinutes가 지난 후에 출석체크하면 LATE
55+
56+ ## 응답
57+ - **`200 OK`**
58+
59+ ## 에러코드
60+ - **`QR_TOKEN_MALFORMED`** : QR 토큰 형식이 올바르지 않습니다.
61+ - **`ROUND_NOT_FOUND`** : 해당 출석 라운드가 존재하지 않습니다.
62+ - **`ROUND_NOT_ACTIVE`** : 출석 라운드가 진행 중이 아닙니다.
63+ - **`NOT_SESSION_MEMBER`** : 출석 세션의 멤버가 아닙니다.
64+ - **`ALREADY_CHECKED_IN`** : 이미 출석 체크되었습니다.
65+
66+ """ )
3467 @ PostMapping ("/check-in" )
3568 public ResponseEntity <Void > checkIn (
3669 @ AuthenticationPrincipal CustomUserDetails userDetails ,
3770 @ RequestBody AttendanceRoundQrTokenRequest request
3871 ) {
3972 UUID userId = requireUserId (userDetails );
40- attendanceService .checkIn (userId , userDetails .getName (), request );
73+ attendanceService .checkIn (userId , userDetails .getUsername (), request );
4174 return ResponseEntity .ok ().build ();
4275 }
4376
4477 /**
4578 * 라운드별 출석 명단 조회(관리자/OWNER)
4679 */
47- @ Operation (summary = "라운드 출석 명단 조회" , description = "특정 라운드의 출석 기록을 조회합니다. (관리자/OWNER)" )
80+ @ Operation (
81+ summary = "라운드 출석 명단 조회" ,
82+ description = """
83+ ## 인증(JWT)
84+ - **필요**
85+
86+ ## 권한
87+ - 세션 **MANAGER** 또는 **OWNER**
88+
89+ ## 동작 설명
90+ - 특정 출석 라운드(`roundId`)에 기록된 모든 출석 데이터를 리스트로 반환
91+
92+ ## 응답 바디 ( `List<AttendanceResponse>` )
93+ - **유저 정보**: `userId`, `userName`(이름)
94+ - **세션/라운드 정보**: 세션 제목, 라운드 이름, 장소, 시작 시간 등
95+ - **출석 상태**: `attendanceStatus` (PENDING, PRESENT, LATE, ABSENT, EXCUSED)
96+ - **상세 기록**: `checkedAt`(체크인 시각), `note`(비고), `checkInLatitude/Longitude`(위치 정보)
97+
98+ ## 에러 코드
99+ - **`ROUND_NOT_FOUND`**: 해당 출석 라운드가 존재하지 않습니다.
100+ - **`NOT_SESSION_ADMIN`**: 세션 관리자 권한이 없습니다.
101+
102+ """ )
48103 @ GetMapping ("/rounds/{roundId}/records" )
49104 public ResponseEntity <List <AttendanceResponse >> getAttendancesByRound (
50105 @ PathVariable UUID roundId ,
@@ -55,38 +110,36 @@ public ResponseEntity<List<AttendanceResponse>> getAttendancesByRound(
55110 }
56111
57112 /**
58- * 라운드 내 특정 유저 출석 상태 수정(관리자/OWNER)
59- * PUT /api/attendance/rounds/{roundId}/users/{userId}
113+ * 라운드 내 특정 유저 출석 상태 수정(관리자/OWNER) PUT /api/attendance/rounds/{roundId}/users/{userId}
60114 */
61115 @ Operation (
62116 summary = "출석 상태 수정" ,
63117 description = """
64-
65- ## 인증(JWT): **필요**
66-
67-
68- ## 권한
69- - **세션 관리자 / OWNER**
70-
71- ## 경로 파라미터
72- - **`roundId`**: 출석 상태를 수정할 라운드 ID (`UUID`)
73- - **`userId`**: 출석 상태를 수정할 대상 사용자 ID (`UUID`)
74-
75- ## 요청 바디 ( `AttendanceStatusUpdateRequest` )
76- - **`status`**: 출석 상태 (필수)
77- - 허용값 예시: `PRESENT`, `LATE`, `ABSENT`, `EXCUSED`
78- - **`reason`**: 상태 수정 사유 (선택)
79- - 예: 지각 사유, 공결 사유 등
80-
81- ## 동작 설명
82- - 특정 라운드에서 특정 사용자의 출석 상태를 수정합니다.
83- - 요청한 사용자가 해당 세션의 관리자/OWNER인지 검증합니다.
84- - `status` 값과 `reason` 값을 기반으로 출석 상태를 반영합니다.
85-
86- ## 응답
87- - **200 OK**
88- - 수정된 출석 정보 (`AttendanceResponse`)
89- """ )
118+
119+ ## 인증(JWT): **필요**
120+
121+ ## 권한
122+ - 세션 **MANAGER** 또는 **OWNER**
123+
124+ ## 경로 파라미터
125+ - **`roundId`**: 출석 상태를 수정할 라운드 ID (`UUID`)
126+ - **`userId`**: 출석 상태를 수정할 대상 사용자 ID (`UUID`)
127+
128+ ## 요청 바디 ( `AttendanceStatusUpdateRequest` )
129+ - **`status`**: 출석 상태 (필수)
130+ - 허용값 예시: `PRESENT`, `LATE`, `ABSENT`, `EXCUSED`
131+ - **`reason`**: 상태 수정 사유 (선택)
132+ - 예: 지각 사유, 공결 사유 등
133+
134+ ## 동작 설명
135+ - 특정 라운드에서 특정 사용자의 출석 상태를 수정
136+ - 요청한 사용자가 해당 세션의 관리자/OWNER인지 검증
137+ - `status` 값과 `reason` 값을 기반으로 출석 상태 반영
138+
139+ ## 응답
140+ - **200 OK**
141+ - 수정된 출석 정보 (`AttendanceResponse`)
142+ """ )
90143 @ PutMapping ("/rounds/{roundId}/users/{userId}" )
91144 public ResponseEntity <AttendanceResponse > updateAttendanceStatus (
92145 @ PathVariable UUID roundId ,
@@ -95,35 +148,38 @@ public ResponseEntity<AttendanceResponse> updateAttendanceStatus(
95148 @ Valid @ RequestBody AttendanceStatusUpdateRequest request
96149 ) {
97150 UUID adminUserId = requireUserId (userDetails );
98-
99- // status가 enum이든 string이든 안전하게 문자열로 변환
100- String statusStr = String .valueOf (request .getStatus ());
101- String reason = request .getReason ();
102-
103151 AttendanceResponse response =
104- attendanceService .updateAttendanceStatusByRound (adminUserId , roundId , userId , statusStr , reason );
152+ attendanceService .updateAttendanceStatusByRound (adminUserId , roundId , userId , request );
105153
106154 return ResponseEntity .ok (response );
107155 }
108156
109157 /**
110- * (옵션) 내 출석 이력 조회
111- * GET /api/attendance/me
158+ * (옵션) 내 출석 이력 조회 GET /api/attendance/me
112159 */
113- @ Operation (summary = "내 출석 이력 조회" , description = "로그인한 사용자의 출석 이력을 조회합니다." )
160+ @ Operation (
161+ summary = "내 출석 이력 조회" ,
162+ description = """
163+ ## 인증(JWT): **필요**
164+
165+ ## 동작 설명
166+ - 현재 로그인한 사용자가 참여한 모든 세션 및 라운드의 출석 기록을 최신순으로 조회
167+
168+ ## 응답 바디 ( `List<AttendanceResponse>` )
169+ - **유저 정보**: `userId`, `userName`(이름)
170+ - **세션/라운드 정보**: 세션 제목, 라운드 이름, 장소, 시작 시간 등
171+ - **출석 상태**: `attendanceStatus` (PENDING, PRESENT, LATE, ABSENT, EXCUSED)
172+ - **상세 기록**: `checkedAt`(체크인 시각), `note`(비고), `checkInLatitude/Longitude`(위치 정보)
173+
174+ ## 에러 코드
175+ - **`USER_NOT_FOUND`**: 유저 정보를 찾을 수 없습니다.
176+
177+ """ )
114178 @ GetMapping ("/me" )
115179 public ResponseEntity <List <AttendanceResponse >> getMyAttendances (
116180 @ AuthenticationPrincipal CustomUserDetails userDetails
117181 ) {
118182 UUID userId = requireUserId (userDetails );
119183 return ResponseEntity .ok (attendanceService .getAttendancesByUser (userId ));
120184 }
121-
122-
123-
124- // ------- helper -------
125- private UUID requireUserId (CustomUserDetails userDetails ) {
126- if (userDetails == null ) throw new IllegalStateException ("UNAUTHENTICATED" );
127- return userDetails .getUserId ();
128- }
129185}
0 commit comments