diff --git a/backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java b/backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java index cf2de3fb..ac1326aa 100644 --- a/backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/activity/repository/ActivityLogRepository.java @@ -2,6 +2,7 @@ import org.sejongisc.backend.activity.entity.ActivityLog; import org.sejongisc.backend.activity.entity.ActivityType; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -15,9 +16,8 @@ public interface ActivityLogRepository extends JpaRepository { // 마이페이지 내 활동 조회 @Query("SELECT a FROM ActivityLog a WHERE a.userId = :userId " + - "AND a.activityType IN :activityTypes " + - "ORDER BY a.createdAt DESC") - List findByUserIdAndActivityTypesOrderByCreatedAtDesc(UUID userId, List activityTypes); + "AND a.activityType IN :activityTypes") + Page findByUserIdAndActivityTypeIn(@Param("userId") UUID userId, @Param("activityTypes") List activityTypes, Pageable pageable); @Query("SELECT COUNT(a) FROM ActivityLog a " + "WHERE a.activityType IN :types " + diff --git a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java index c7a725cd..3db8c3eb 100644 --- a/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java +++ b/backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java @@ -7,6 +7,7 @@ import org.sejongisc.backend.admin.dto.AdminUserResponse; import org.sejongisc.backend.admin.dto.ExcelSyncResponse; import org.sejongisc.backend.admin.service.AdminUserService; +import org.sejongisc.backend.user.entity.Grade; import org.sejongisc.backend.user.entity.Role; import org.sejongisc.backend.user.entity.UserStatus; import org.springframework.http.MediaType; @@ -60,9 +61,9 @@ public ResponseEntity> getAllUsers(@ModelAttribute Admin return ResponseEntity.ok(adminUserService.findAllUsers(request)); } - @Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, GRADUATED 등으로 상태를 변경합니다. (시스템 관리자용)") + @Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, OUT으로 상태를 변경합니다. (시스템 관리자용)") @PatchMapping("/{userId}/status") - @PreAuthorize("hasAnyRole('SYSTEM_ADMIN')") + @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") public ResponseEntity updateUserStatus( @PathVariable UUID userId, @RequestParam UserStatus status) { @@ -74,7 +75,7 @@ public ResponseEntity updateUserStatus( // TODO : 회장 권한 논의 필요 @Operation(summary = "회원 권한 변경", description = "특정 유저의 Role(PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)을 변경합니다. (시스템 관리자용)") @PatchMapping("/{userId}/role") - @PreAuthorize("hasRole('SYSTEM_ADMIN')") + @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") public ResponseEntity updateUserRole( @PathVariable UUID userId, @RequestParam Role role) { @@ -82,17 +83,28 @@ public ResponseEntity updateUserRole( return ResponseEntity.noContent().build(); } - @Operation(summary = "선배(SENIOR) 등급 변경", description = "특정 유저를 선배(SENIOR) 등급으로 변경합니다. (회장/관리자용)") - @PatchMapping("/{userId}/senior") + @Operation(summary = "회원 신분 변경", description = """ + 특정 유저의 신분(GRADE)을 변경합니다. (회장/관리자용) + + ## 요청방식 + ?grade=NEW_MEMBER + + ## 종류 + - NEW_MEMBER, // 신입부원 + - ASSOCIATE_MEMBER, // 준회원 + - REGULAR_MEMBER, // 정회원 + """) + @PatchMapping("/{userId}/grade") @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") - public ResponseEntity promoteToSenior(@PathVariable UUID userId) { - adminUserService.promoteToSenior(userId); + public ResponseEntity promoteToSenior(@PathVariable UUID userId, + @RequestParam Grade grade) { + adminUserService.updateUserGrade(userId, grade); return ResponseEntity.noContent().build(); } @Operation(summary = "회원 강제 탈퇴", description = "시스템에서 유저를 탈퇴 처리합니다. (시스템 관리자용)") @DeleteMapping("/{userId}") - @PreAuthorize("hasRole('SYSTEM_ADMIN')") + @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") public ResponseEntity forceDeleteUser(@PathVariable UUID userId) { adminUserService.deleteUser(userId); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java b/backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java index b0287d45..68e1444e 100644 --- a/backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java @@ -113,8 +113,8 @@ public void updateUserRole(UUID userId, Role role) { * 특정 사용자를 선배(SENIOR) 등급으로 변경 */ @Transactional - public void promoteToSenior(UUID userId) { - userService.promoteToSenior(userId); + public void updateUserGrade(UUID userId, Grade grade) { + userService.updateUserGrade(userId, grade); } /** diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java index 3ea0e321..6bd60eed 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceRoundController.java @@ -101,7 +101,7 @@ public ResponseEntity createRound( - **필요** ## 권한 - - **세션 MANAGER** 또는 **OWNER** + - **세션 OWNER** ## 경로 파라미터 - **`roundId`**: 삭제할 라운드 ID (`UUID`) @@ -137,7 +137,7 @@ public ResponseEntity deleteRound( ## 인증(JWT): **필요** ## 권한 - - **세션 MEMBER** + - **세션 MEMBER/MANAGER/OWNER** ## 동작 설명 - 특정 라운드 ID(`roundId`)를 통해 회차의 상세 정보 조회 diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java index 6504bfb5..b370e0f1 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/AttendanceSessionController.java @@ -52,6 +52,9 @@ public class AttendanceSessionController { - **`description`**: 세션 상세 설명 - **`allowedMinutes`**: 지각 처리 전 체크인 허용 시간 (분 단위) + ## 권한 + - positionName: **회장, 부회장, 팀장** + ## 동작 설명 - 새로운 출석 세션(Session) 엔티티 생성 - 세션 상태(`SessionStatus`)는 기본적으로 **OPEN**으로 설정 @@ -157,6 +160,9 @@ public ResponseEntity> getActiveSessions() { description = """ ## 인증(JWT): **필요** + ## 권한 + - **세션 OWNER** + ## 요청 파라미터 ( `AttendanceSessionRequest` ) - **`title`**: 세션 제목 - **`description`**: 세션 설명 @@ -189,7 +195,7 @@ public ResponseEntity updateSession( - **필요** ## 권한 - - **세션 MANAGER** 또는 **OWNER** + - **세션 OWNER** ## 경로 파라미터 - **`sessionId`**: 종료할 세션 ID (`UUID`) @@ -223,7 +229,7 @@ public ResponseEntity closeSession( - **필요** ## 권한 - - **세션 MANAGER** 또는 **OWNER** + - **세션 OWNER** ## 경로 파라미터 - **`sessionId`**: 삭제할 세션 ID (`UUID`) diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java b/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java index 7c1f6cd6..90b33d9e 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/controller/SessionUserController.java @@ -155,7 +155,7 @@ public ResponseEntity> getSessionUsers( """ ) @PostMapping("/{sessionId}/users/add-all") - @PreAuthorize("hasRole('PRESIDENT')") + @PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") public ResponseEntity addAllUsers( @PathVariable UUID sessionId, @AuthenticationPrincipal CustomUserDetails userDetails @@ -164,4 +164,52 @@ public ResponseEntity addAllUsers( sessionUserService.addAllUsers(sessionId, adminUserId); return ResponseEntity.ok().build(); } + + /** + * 세션 관리자(MANAGER) 권한 부여 + */ + @Operation( + summary = "세션 관리자 추가", + description = """ + ## 권한 + - **세션 OWNER** + + ## 동작 설명 + - 특정 사용자의 역할을 `MANAGER`로 격상시킵니다. + """) + @PostMapping("/{sessionId}/admins/{userId}") + public ResponseEntity addAdminToSession( + @PathVariable UUID sessionId, + @PathVariable UUID userId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + UUID adminUserId = requireUserId(userDetails); + // 서비스단에서 세션 소유자(OWNER)인지 검증하는 로직이 포함되어야 함 + sessionUserService.addAdmin(sessionId, userId, adminUserId); + return ResponseEntity.ok().build(); + } + + /** + * 세션 관리자(MANAGER) 권한 해제 + */ + @Operation( + summary = "세션 관리자 제거", + description = """ + ## 권한 + - **세션 OWNER** + + ## 동작 설명 + - 특정 사용자의 역할을 `PARTICIPANT`로 강등시킵니다. + - `OWNER`는 강등될 수 없습니다. + """) + @DeleteMapping("/{sessionId}/admins/{userId}") + public ResponseEntity removeAdminFromSession( + @PathVariable UUID sessionId, + @PathVariable UUID userId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + UUID adminUserId = requireUserId(userDetails); + sessionUserService.removeAdmin(sessionId, userId, adminUserId); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java index 5c0ce450..5a281d8b 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRepository.java @@ -7,6 +7,7 @@ import org.sejongisc.backend.attendance.entity.AttendanceRound; import org.sejongisc.backend.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -33,4 +34,10 @@ void deleteAllByAttendanceRound_AttendanceSession_AttendanceSessionIdAndUser_Use Optional findByAttendanceRound_RoundIdAndUser(@Param("roundId") UUID roundId, @Param("user") User user); List findAllByAttendanceRound(AttendanceRound round); + + @Modifying(clearAutomatically = true) // 벌크 연산 후 영속성 컨텍스트 동기화 + @Query("DELETE FROM Attendance a " + + "WHERE a.attendanceRound IN " + + "(SELECT r FROM AttendanceRound r WHERE r.attendanceSession.attendanceSessionId = :sessionId)") + void deleteBySessionId(@Param("sessionId") UUID sessionId); } diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java index 4ae9d0c7..242091d4 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/AttendanceRoundRepository.java @@ -31,14 +31,14 @@ public interface AttendanceRoundRepository extends JpaRepository 'CLOSED' - and r.closeAt <= :now -""") + update AttendanceRound r + set r.roundStatus = 'CLOSED' + where r.roundStatus <> 'CLOSED' + and r.closeAt <= :now + """) int closeDueRounds(LocalDateTime now); - List findByRoundStatusAndCloseAtBefore(RoundStatus status, LocalDateTime dateTime); + List findByRoundStatusAndCloseAtBefore(RoundStatus status, LocalDateTime dateTime); Optional findByQrSecret(String qrCode); List findByAttendanceSession_AttendanceSessionId(UUID sessionId); @@ -89,4 +89,8 @@ List findBySession_SessionIdAndRoundDateBefore( List findBySession_SessionIdAndRoundDateAfterOrEqual( @Param("sessionId") UUID sessionId, @Param("date") LocalDate date); + + @Modifying + @Query("DELETE FROM AttendanceRound a WHERE a.attendanceSession.attendanceSessionId = :sessionId") + void deleteBySessionId(@Param("sessionId") UUID sessionId); } diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java b/backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java index 2b3d511d..c3796fdc 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/repository/SessionUserRepository.java @@ -25,7 +25,9 @@ public interface SessionUserRepository extends JpaRepository List findByAttendanceSession_AttendanceSessionId(UUID sessionId); - + @Modifying + @Query("DELETE FROM SessionUser a WHERE a.attendanceSession.attendanceSessionId = :sessionId") + void deleteBySessionId(@Param("sessionId") UUID sessionId); /** diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java index 27a53263..efa6352e 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceRoundService.java @@ -167,7 +167,7 @@ public void deleteRound(UUID roundId, UUID userId) { .orElseThrow(() -> new CustomException(ErrorCode.ROUND_NOT_FOUND)); UUID sessionId = round.getAttendanceSession().getAttendanceSessionId(); - authorizationService.ensureAdmin(sessionId, userId); + authorizationService.ensureOwner(sessionId, userId); attendanceRoundRepository.delete(round); log.info("라운드 삭제 완료 - roundId: {}", roundId); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java index 12e018c1..8272defd 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceService.java @@ -113,6 +113,7 @@ public AttendanceResponse updateAttendanceStatusByRound( UUID targetUserId, AttendanceStatusUpdateRequest request ) { + // todo: activityLog 추가 String status = (request.getStatus() == null) ? null : request.getStatus().toString(); String reason = request.getReason(); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java index 358cd5f4..45f5bfbe 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/AttendanceSessionService.java @@ -8,6 +8,8 @@ import org.sejongisc.backend.attendance.entity.SessionRole; import org.sejongisc.backend.attendance.entity.SessionStatus; import org.sejongisc.backend.attendance.entity.SessionUser; +import org.sejongisc.backend.attendance.repository.AttendanceRepository; +import org.sejongisc.backend.attendance.repository.AttendanceRoundRepository; import org.sejongisc.backend.attendance.repository.AttendanceSessionRepository; import org.sejongisc.backend.attendance.repository.SessionUserRepository; import org.sejongisc.backend.common.exception.CustomException; @@ -27,6 +29,8 @@ @Slf4j public class AttendanceSessionService { + private final AttendanceRoundRepository attendanceRoundRepository; + private final AttendanceRepository attendanceRepository; private final AttendanceSessionRepository attendanceSessionRepository; private final UserRepository userRepository; private final SessionUserRepository sessionUserRepository; @@ -37,7 +41,11 @@ public class AttendanceSessionService { */ @Transactional public void createSession(UUID creatorId, AttendanceSessionRequest request) { - + User creator = userRepository.findById(creatorId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (!creator.isManagerPosition()) { + throw new CustomException(ErrorCode.NOT_MANAGER_POSITION); + } // 출석 세션 엔티티 생성 AttendanceSession attendanceSession = AttendanceSession.builder() .title(request.title()) @@ -48,8 +56,6 @@ public void createSession(UUID creatorId, AttendanceSessionRequest request) { AttendanceSession saved = attendanceSessionRepository.save(attendanceSession); - User creator = userRepository.findById(creatorId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); // 세션 생성자를 OWNER로 세션 사용자에 추가 SessionUser su = SessionUser.builder() @@ -108,7 +114,7 @@ public List getActiveSessions() { */ public void updateSession(UUID sessionId, AttendanceSessionRequest request, UUID userId) { // 권한 확인 - attendanceAuthorizationService.ensureAdmin(sessionId, userId); + attendanceAuthorizationService.ensureOwner(sessionId, userId); AttendanceSession session = attendanceSessionRepository.findById(sessionId) .orElseThrow(() -> new CustomException(ErrorCode.SESSION_NOT_FOUND)); @@ -128,11 +134,21 @@ public void updateSession(UUID sessionId, AttendanceSessionRequest request, UUID */ public void deleteSession(UUID sessionId, UUID userId) { // 권한 확인 - attendanceAuthorizationService.ensureAdmin(sessionId, userId); + attendanceAuthorizationService.ensureOwner(sessionId, userId); AttendanceSession session = attendanceSessionRepository.findById(sessionId) .orElseThrow(() -> new CustomException(ErrorCode.SESSION_NOT_FOUND)); + // 최하위 자식인 Attendance(출석기록) 삭제 + attendanceRepository.deleteBySessionId(sessionId); + + // 중간 자식인 AttendanceRound(라운드) 삭제 + attendanceRoundRepository.deleteBySessionId(sessionId); + + // 참여자 명단(SessionUser) 삭제 + sessionUserRepository.deleteBySessionId(sessionId); + + // session 삭제 attendanceSessionRepository.delete(session); log.info("출석 세션 삭제 완료: 세션ID={}", sessionId); } @@ -141,7 +157,7 @@ public void deleteSession(UUID sessionId, UUID userId) { * 세션 수동 종료(해당 세션 관리자용) - 세션 상태를 CLOSED로 변경 - 체크인 비활성화 */ public void closeSession(UUID sessionId, UUID userId) { - attendanceAuthorizationService.ensureAdmin(sessionId, userId); + attendanceAuthorizationService.ensureOwner(sessionId, userId); AttendanceSession session = attendanceSessionRepository.findById(sessionId) .orElseThrow(() -> new CustomException(ErrorCode.SESSION_NOT_FOUND)); diff --git a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java index ee87b4e8..ba250492 100644 --- a/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java +++ b/backend/src/main/java/org/sejongisc/backend/attendance/service/SessionUserService.java @@ -35,7 +35,7 @@ public class SessionUserService { public void addAllUsers(UUID sessionId, UUID userId) { // 권한 확인 - authorizationService.ensureAdmin(sessionId, userId); + authorizationService.ensureOwner(sessionId, userId); log.info("세션에 모든 사용자 추가 시작: 세션ID={}", sessionId); AttendanceSession session = attendanceSessionRepository.findById(sessionId) @@ -99,7 +99,7 @@ public SessionUserResponse addUserToSession(UUID sessionId, UUID targetUserId, U public void removeUserFromSession(UUID sessionId, UUID targetUserId, UUID actorUserId) { authorizationService.ensureOwner(sessionId, actorUserId); - AttendanceSession session = attendanceSessionRepository.findById(sessionId) + attendanceSessionRepository.findById(sessionId) .orElseThrow(() -> new CustomException(ErrorCode.SESSION_NOT_FOUND)); // SessionUser 삭제 @@ -124,11 +124,6 @@ public List getSessionUsers(UUID sessionId, UUID viewerUser return users.stream().map(SessionUserResponse::from).toList(); } - @Transactional(readOnly = true) - public boolean isUserInSession(UUID sessionId, UUID userId) { - return sessionUserRepository.existsByAttendanceSession_AttendanceSessionIdAndUser_UserId(sessionId, userId); - } - private void createAbsentForPastRounds(UUID sessionId, User user) { List pastRounds = attendanceRoundRepository .findByAttendanceSession_AttendanceSessionIdAndRoundDateBefore(sessionId, LocalDate.now()); @@ -150,47 +145,13 @@ private void createAbsentForPastRounds(UUID sessionId, User user) { } } - /** - * 세션 가입 - */ - @Transactional - public void joinSession(UUID sessionId, UUID userId) { - // 이미 가입했는지 체크 - boolean exists = sessionUserRepository.existsByAttendanceSession_AttendanceSessionIdAndUser_UserId(sessionId, - userId); - if (exists) { - throw new CustomException(ErrorCode.ALREADY_JOINED); - } - - AttendanceSession session = attendanceSessionRepository.findById(sessionId) - .orElseThrow(() -> new CustomException(ErrorCode.SESSION_NOT_FOUND)); - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - sessionUserRepository.save(SessionUser.builder() - .attendanceSession(session) - .user(user) - .sessionRole(SessionRole.PARTICIPANT) - .build()); - } - - /** - * 세션 탈퇴 - */ - @Transactional - public void leaveSession(UUID sessionId, UUID userId) { - SessionUser su = sessionUserRepository - .findByAttendanceSession_AttendanceSessionIdAndUser_UserId(sessionId, userId) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_SESSION_MEMBER)); - - sessionUserRepository.delete(su); - } /** * 세션 관리자 추가/제거 */ @Transactional - public void addAdmin(UUID sessionId, UUID targetUserId) { + public void addAdmin(UUID sessionId, UUID targetUserId, UUID actorUserId) { + authorizationService.ensureOwner(sessionId, actorUserId); SessionUser su = sessionUserRepository .findByAttendanceSession_AttendanceSessionIdAndUser_UserId(sessionId, targetUserId) .orElseThrow(() -> new CustomException(ErrorCode.TARGET_NOT_SESSION_MEMBER)); @@ -198,7 +159,8 @@ public void addAdmin(UUID sessionId, UUID targetUserId) { } @Transactional - public void removeAdmin(UUID sessionId, UUID targetUserId) { + public void removeAdmin(UUID sessionId, UUID targetUserId, UUID actorUserId) { + authorizationService.ensureOwner(sessionId, actorUserId); SessionUser su = sessionUserRepository .findByAttendanceSession_AttendanceSessionIdAndUser_UserId(sessionId, targetUserId) .orElseThrow(() -> new CustomException(ErrorCode.TARGET_NOT_SESSION_MEMBER)); diff --git a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java index 31ede69a..5219ac70 100644 --- a/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java +++ b/backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java @@ -89,30 +89,45 @@ public enum ErrorCode { // USER USER_NOT_FOUND(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."), + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."), + DUPLICATE_PHONE(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."), + DUPLICATE_USER(HttpStatus.CONFLICT, "이미 가입된 사용자입니다."), + INVALID_INPUT(HttpStatus.BAD_REQUEST, "입력값이 올바르지 않습니다."), + USER_WITHDRAWN(HttpStatus.FORBIDDEN, "탈퇴한 회원은 로그인할 수 없습니다."), + NEED_PENDING_APPROVAL(HttpStatus.FORBIDDEN, "가입 승인 대기 중입니다. 관리자의 확인 후 이용 가능합니다"), + NOT_MANAGER_POSITION(HttpStatus.FORBIDDEN, "관리자 포지션만 가능한 기능입니다. (팀장, 부회장, 회장)"), + // EXCEL INVALID_FILE_FORMAT(HttpStatus.BAD_REQUEST, "지원하지 않는 파일 형식입니다. .xlsx 파일을 업로드해주세요."), - INVALID_EXCEL_STRUCTURE(HttpStatus.UNPROCESSABLE_ENTITY, "엑셀 양식이 일치하지 않습니다. 필수 컬럼을 확인해주세요."), - EMPTY_FILE(HttpStatus.BAD_REQUEST, "업로드된 파일이 비어있습니다."), + INVALID_EXCEL_STRUCTURE(HttpStatus.UNPROCESSABLE_ENTITY, "엑셀 양식이 일치하지 않습니다. 필수 컬럼을 확인해주세요."), + EMPTY_FILE(HttpStatus.BAD_REQUEST, "업로드된 파일이 비어있습니다."), // BETTING STOCK_NOT_FOUND(HttpStatus.NOT_FOUND, "주식 종목이 존재하지 않습니다."), + BET_ROUND_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 라운드입니다."), + BET_DUPLICATE(HttpStatus.CONFLICT, "이미 이 라운드에 베팅했습니다."), + BET_ROUND_CLOSED(HttpStatus.CONFLICT, "베팅 가능 시간이 아닙니다."), + BET_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 베팅을 찾을 수 없습니다."), + BET_POINT_TOO_LOW(HttpStatus.CONFLICT, "베팅 포인트는 10 이상이어야 합니다."), + BET_ROUND_NOT_CLOSED(HttpStatus.CONFLICT, "닫히지 않은 배팅입니다."), + BET_ALREADY_PROCESSED(HttpStatus.CONFLICT, "이미 취소되었거나 처리된 베팅입니다."), // BOARD diff --git a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java index ca50e006..f6ab1ed0 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java +++ b/backend/src/main/java/org/sejongisc/backend/user/controller/UserController.java @@ -15,7 +15,8 @@ import org.sejongisc.backend.common.auth.service.RefreshTokenService; import org.sejongisc.backend.user.dto.*; import org.sejongisc.backend.user.service.UserService; -import org.springframework.data.domain.Slice; +import org.springframework.data.domain.*; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -58,15 +59,19 @@ public ResponseEntity updateUser(@RequestBody @Valid UserUpdateRequest req return ResponseEntity.ok().build(); } - @Operation(summary = "내 출석 로그 조회") + @Operation(summary = "내 출석 로그 조회", description = "?page=0&size=20 방식으로 페이지네이션 조회 (최신순)") @GetMapping("/logs/attendance") - public ResponseEntity> getAttendanceLogs(@AuthenticationPrincipal CustomUserDetails customUserDetails) { - return ResponseEntity.ok(userService.getAttendanceActivityLog(customUserDetails.getUserId())); + public ResponseEntity> getAttendanceLogs( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + return ResponseEntity.ok(userService.getAttendanceActivityLog(customUserDetails.getUserId(), pageable)); } - @Operation(summary = "내 활동 로그 조회") + @Operation(summary = "내 활동 로그 조회", description = "?page=0&size=20 방식으로 페이지네이션 조회 (최신순)") @GetMapping("/logs/board") - public ResponseEntity> getBoardLogs(@AuthenticationPrincipal CustomUserDetails customUserDetails) { - return ResponseEntity.ok(userService.getBoardActivityLog(customUserDetails.getUserId())); + public ResponseEntity> getBoardLogs( + @AuthenticationPrincipal CustomUserDetails customUserDetails, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + return ResponseEntity.ok(userService.getBoardActivityLog(customUserDetails.getUserId(), pageable)); } } diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/Grade.java b/backend/src/main/java/org/sejongisc/backend/user/entity/Grade.java index d405b49b..e33ceb6c 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/Grade.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/Grade.java @@ -4,14 +4,15 @@ public enum Grade { NEW_MEMBER, // 신입부원 ASSOCIATE_MEMBER, // 준회원 REGULAR_MEMBER, // 정회원 - SENIOR; // 선배/OB + //SENIOR // 선배/OB는 정회원으로 대체 + ; public static Grade fromString(String gradeStr) { if (gradeStr == null) return NEW_MEMBER; if (gradeStr.contains("정회원")) return REGULAR_MEMBER; if (gradeStr.contains("준회원")) return ASSOCIATE_MEMBER; - if (gradeStr.contains("선배") || gradeStr.contains("OB")) return SENIOR; + //if (gradeStr.contains("선배") || gradeStr.contains("OB")) return SENIOR; return NEW_MEMBER; } diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java index ff1ba46d..b045d173 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/User.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/User.java @@ -68,7 +68,7 @@ public class User extends BasePostgresEntity{ @Enumerated(EnumType.STRING) // 새 장부 업로드 시: 기존에 ACTIVE한 모든 인원을 INACTIVE로 일괄 업데이트 @Column(nullable = false) // 새 엑셀에 있는 studentId을 대조하여, 명단에 있는 사람만 다시 ACTIVE로 바꾸고 @Builder.Default // generation(기수)과 positionName(직위)을 최신화 - private UserStatus status = UserStatus.ACTIVE; // 활동 상태 (ACTIVE, INACTIVE, GRADUATED, OUT 등) + private UserStatus status = UserStatus.ACTIVE; // 활동 상태 (ACTIVE, INACTIVE, OUT 등) @Column(columnDefinition = "integer default 0",nullable = false) private Integer point; @@ -85,11 +85,12 @@ public class User extends BasePostgresEntity{ // 권한 확인용 편의 메서드 public boolean isManagerPosition() { - if (this.positionName == null) return false; + if (this.role == Role.TEAM_MEMBER || this.role == Role.PENDING_MEMBER ) return false; // 직위에 '팀장', '대표', '부대표' 등의 키워드가 있으면 운영진 권한 부여 후보 - return this.positionName.contains("팀장") || - this.positionName.contains("대표") || - this.positionName.contains("회장"); + return this.role == Role.SYSTEM_ADMIN || + this.role == Role.PRESIDENT || + this.role == Role.VICE_PRESIDENT || + this.role == Role.TEAM_LEADER; } // 기본값 지정 diff --git a/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java b/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java index 4882ea16..28790628 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java +++ b/backend/src/main/java/org/sejongisc/backend/user/entity/UserStatus.java @@ -9,7 +9,7 @@ public enum UserStatus { ACTIVE("활동 중"), INACTIVE("활동 중지"), - GRADUATED("졸업생"), + //GRADUATED("졸업생"), OUT("탈퇴"); private final String description; // 한글 명칭 diff --git a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java index f583d33b..7d408716 100644 --- a/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java +++ b/backend/src/main/java/org/sejongisc/backend/user/service/UserService.java @@ -15,6 +15,8 @@ import org.sejongisc.backend.user.entity.UserStatus; import org.sejongisc.backend.user.repository.UserRepository; import org.sejongisc.backend.user.util.PasswordPolicyValidator; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,15 +63,17 @@ public void updateUser(UUID userId, UserUpdateRequest request) { } @Transactional(readOnly = true) - public List getAttendanceActivityLog(UUID userId) { - return activityLogRepository.findByUserIdAndActivityTypesOrderByCreatedAtDesc(userId, - List.of(ActivityType.ATTENDANCE)); + public Page getAttendanceActivityLog(UUID userId, Pageable pageable) { + return activityLogRepository.findByUserIdAndActivityTypeIn(userId, + List.of(ActivityType.ATTENDANCE), + pageable); } @Transactional(readOnly = true) - public List getBoardActivityLog(UUID userId) { - return activityLogRepository.findByUserIdAndActivityTypesOrderByCreatedAtDesc(userId, - List.of(ActivityType.BOARD_LIKE, ActivityType.BOARD_POST, ActivityType.BOARD_COMMENT)); + public Page getBoardActivityLog(UUID userId, Pageable pageable) { + return activityLogRepository.findByUserIdAndActivityTypeIn(userId, + List.of(ActivityType.BOARD_LIKE, ActivityType.BOARD_POST, ActivityType.BOARD_COMMENT), + pageable); } @Transactional @@ -98,13 +102,9 @@ public void updateUserRole(UUID userId, Role role) { } @Transactional - public void promoteToSenior(UUID userId) { + public void updateUserGrade(UUID userId, Grade grade) { User user = findUser(userId); - - // grade 및 status 변경 - user.setGrade(Grade.SENIOR); - user.setStatus(UserStatus.GRADUATED); - + user.setGrade(grade); log.info("선배 등급 전환 완료: userId={}, 학번={}", userId, user.getStudentId()); }