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,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;
Expand All @@ -15,9 +16,8 @@
public interface ActivityLogRepository extends JpaRepository<ActivityLog, Long> {
// 마이페이지 내 활동 조회
@Query("SELECT a FROM ActivityLog a WHERE a.userId = :userId " +
"AND a.activityType IN :activityTypes " +
"ORDER BY a.createdAt DESC")
List<ActivityLog> findByUserIdAndActivityTypesOrderByCreatedAtDesc(UUID userId, List<ActivityType> activityTypes);
"AND a.activityType IN :activityTypes")
Page<ActivityLog> findByUserIdAndActivityTypeIn(@Param("userId") UUID userId, @Param("activityTypes") List<ActivityType> activityTypes, Pageable pageable);

@Query("SELECT COUNT(a) FROM ActivityLog a " +
"WHERE a.activityType IN :types " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,9 +61,9 @@ public ResponseEntity<List<AdminUserResponse>> 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) {
Expand All @@ -74,25 +75,36 @@ 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) {
adminUserService.updateUserRole(userId, role);
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<Void> promoteToSenior(@PathVariable UUID userId) {
adminUserService.promoteToSenior(userId);
public ResponseEntity<Void> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public ResponseEntity<AttendanceRoundResponse> createRound(
- **필요**

## 권한
- **세션 MANAGER** 또는 **OWNER**
- **세션 OWNER**

## 경로 파라미터
- **`roundId`**: 삭제할 라운드 ID (`UUID`)
Expand Down Expand Up @@ -137,7 +137,7 @@ public ResponseEntity<Void> deleteRound(
## 인증(JWT): **필요**

## 권한
- **세션 MEMBER**
- **세션 MEMBER/MANAGER/OWNER**

## 동작 설명
- 특정 라운드 ID(`roundId`)를 통해 회차의 상세 정보 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class AttendanceSessionController {
- **`description`**: 세션 상세 설명
- **`allowedMinutes`**: 지각 처리 전 체크인 허용 시간 (분 단위)

## 권한
- positionName: **회장, 부회장, 팀장**

## 동작 설명
- 새로운 출석 세션(Session) 엔티티 생성
- 세션 상태(`SessionStatus`)는 기본적으로 **OPEN**으로 설정
Expand Down Expand Up @@ -157,6 +160,9 @@ public ResponseEntity<List<AttendanceSessionResponse>> getActiveSessions() {
description = """
## 인증(JWT): **필요**

## 권한
- **세션 OWNER**

## 요청 파라미터 ( `AttendanceSessionRequest` )
- **`title`**: 세션 제목
- **`description`**: 세션 설명
Expand Down Expand Up @@ -189,7 +195,7 @@ public ResponseEntity<Void> updateSession(
- **필요**

## 권한
- **세션 MANAGER** 또는 **OWNER**
- **세션 OWNER**

## 경로 파라미터
- **`sessionId`**: 종료할 세션 ID (`UUID`)
Expand Down Expand Up @@ -223,7 +229,7 @@ public ResponseEntity<Void> closeSession(
- **필요**

## 권한
- **세션 MANAGER** 또는 **OWNER**
- **세션 OWNER**

## 경로 파라미터
- **`sessionId`**: 삭제할 세션 ID (`UUID`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public ResponseEntity<List<SessionUserResponse>> getSessionUsers(
"""
)
@PostMapping("/{sessionId}/users/add-all")
@PreAuthorize("hasRole('PRESIDENT')")
@PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')")
public ResponseEntity<Void> addAllUsers(
@PathVariable UUID sessionId,
@AuthenticationPrincipal CustomUserDetails userDetails
Expand All @@ -164,4 +164,52 @@ public ResponseEntity<Void> addAllUsers(
sessionUserService.addAllUsers(sessionId, adminUserId);
return ResponseEntity.ok().build();
}

/**
* 세션 관리자(MANAGER) 권한 부여
*/
@Operation(
summary = "세션 관리자 추가",
description = """
## 권한
- **세션 OWNER**

## 동작 설명
- 특정 사용자의 역할을 `MANAGER`로 격상시킵니다.
""")
@PostMapping("/{sessionId}/admins/{userId}")
public ResponseEntity<Void> 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<Void> removeAdminFromSession(
@PathVariable UUID sessionId,
@PathVariable UUID userId,
@AuthenticationPrincipal CustomUserDetails userDetails) {

UUID adminUserId = requireUserId(userDetails);
sessionUserService.removeAdmin(sessionId, userId, adminUserId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,4 +34,10 @@ void deleteAllByAttendanceRound_AttendanceSession_AttendanceSessionIdAndUser_Use
Optional<Attendance> findByAttendanceRound_RoundIdAndUser(@Param("roundId") UUID roundId, @Param("user") User user);

List<Attendance> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ public interface AttendanceRoundRepository extends JpaRepository<AttendanceRound

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update AttendanceRound r
set r.roundStatus = 'CLOSED'
where r.roundStatus <> '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<AttendanceRound> findByRoundStatusAndCloseAtBefore(RoundStatus status, LocalDateTime dateTime);
List<AttendanceRound> findByRoundStatusAndCloseAtBefore(RoundStatus status, LocalDateTime dateTime);

Optional<AttendanceRound> findByQrSecret(String qrCode);
List<AttendanceRound> findByAttendanceSession_AttendanceSessionId(UUID sessionId);
Expand Down Expand Up @@ -89,4 +89,8 @@ List<AttendanceRound> findBySession_SessionIdAndRoundDateBefore(
List<AttendanceRound> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public interface SessionUserRepository extends JpaRepository<SessionUser, UUID>

List<SessionUser> findByAttendanceSession_AttendanceSessionId(UUID sessionId);


@Modifying
@Query("DELETE FROM SessionUser a WHERE a.attendanceSession.attendanceSessionId = :sessionId")
void deleteBySessionId(@Param("sessionId") UUID sessionId);


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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())
Expand All @@ -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()
Expand Down Expand Up @@ -108,7 +114,7 @@ public List<AttendanceSessionResponse> 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));
Expand All @@ -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);
}
Expand All @@ -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));
Expand Down
Loading