diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/service/AlarmService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/service/AlarmService.java index 5fb03c03..37a42b9b 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/service/AlarmService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/service/AlarmService.java @@ -1,6 +1,7 @@ package DGU_AI_LAB.admin_be.domain.alarm.service; import DGU_AI_LAB.admin_be.domain.requests.entity.Request; +import DGU_AI_LAB.admin_be.domain.users.entity.User; import DGU_AI_LAB.admin_be.error.ErrorCode; import DGU_AI_LAB.admin_be.error.exception.BusinessException; import lombok.RequiredArgsConstructor; @@ -213,6 +214,38 @@ public void sendNewRequestNotification(Request request) { sendSlackAlert(message, targetWebhookUrl); } + /** + * 사용자에게 서버 사용 신청 승인 알림을 보냅니다. + * @param request 승인된 Request 엔티티 + */ + public void sendApprovalNotification(Request request) { + User user = request.getUser(); + String subject = "[DGU AI LAB] 서버 사용 신청이 승인되었습니다."; + String message = String.format( + """ + 🎉 %s님의 서버 사용 신청이 성공적으로 승인되었습니다! 🎉 + + 아래 정보를 사용하여 서버에 접속해 주세요. + ------------------------------------- + - Ubuntu 사용자 이름: %s + - 할당된 서버: %s + - 컨테이너 이미지: %s:%s + - 할당된 볼륨 크기: %d GiB + - 만료일: %s + ------------------------------------- + + 궁금한 점이 있다면 관리자에게 문의해 주세요. + """, + user.getName(), + request.getUbuntuUsername(), + request.getResourceGroup().getServerName(), + request.getContainerImage().getImageName(), + request.getContainerImage().getImageVersion(), + request.getVolumeSizeGiB(), + request.getExpiresAt().toLocalDate().toString() + ); + sendAllAlerts(user.getName(), user.getEmail(), subject, message); + } } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/gpus/entity/Gpu.java b/src/main/java/DGU_AI_LAB/admin_be/domain/gpus/entity/Gpu.java index 2d7a5d90..797f101d 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/gpus/entity/Gpu.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/gpus/entity/Gpu.java @@ -8,7 +8,6 @@ @Table(name = "gpus") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EqualsAndHashCode(of = "gpuId") public class Gpu { @Id diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/entity/Group.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/entity/Group.java index 00bb0a90..e3fc2a4e 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/entity/Group.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/entity/Group.java @@ -8,7 +8,7 @@ @Table(name = "`groups`") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EqualsAndHashCode(of = "groupId") +@EqualsAndHashCode(of = "ubuntuGid") public class Group { @Id diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java index a3aa40a5..2ab71d93 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java @@ -33,6 +33,16 @@ public ResponseEntity> getChangeRequests() { return SuccessResponse.ok(changeRequests); } + /** + * 모든 변경 요청 목록 조회 (관리자용) + * 모든 상태의 ChangeRequest 목록을 반환합니다. + */ + @GetMapping("/all") + public ResponseEntity> getAllChangeRequests() { + List changeRequests = adminRequestQueryService.getAllChangeRequests(); + return SuccessResponse.ok(changeRequests); + } + @PatchMapping("/approve") public ResponseEntity> approveModification( @AuthenticationPrincipal(expression = "userId") Long adminId, diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/RequestController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/RequestController.java index c0242524..580381ed 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/RequestController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/RequestController.java @@ -3,6 +3,7 @@ import DGU_AI_LAB.admin_be.domain.requests.controller.docs.RequestApi; import DGU_AI_LAB.admin_be.domain.requests.dto.request.SingleChangeRequestDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.SaveRequestRequestDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.service.RequestCommandService; import DGU_AI_LAB.admin_be.domain.requests.service.RequestQueryService; @@ -74,4 +75,13 @@ public ResponseEntity> getAllFulfilledUsernames() { List usernames = requestQueryService.getAllFulfilledUsernames(); return SuccessResponse.ok(usernames); } + + /** + * 나의 변경 요청 목록 조회 + */ + @GetMapping("/my/changes") + public ResponseEntity> getMyChangeRequests(@AuthenticationPrincipal CustomUserDetails user) { + List changeRequests = requestQueryService.getMyChangeRequests(user.getUserId()); + return SuccessResponse.ok(changeRequests); + } } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java index a9e7d8ed..47fe62ec 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java @@ -22,6 +22,13 @@ public interface AdminRequestChangeApi { }) ResponseEntity> getChangeRequests(); + @Operation(summary = "모든 변경 요청 목록 조회", description = "모든 상태의 변경 요청 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))) + }) + ResponseEntity> getAllChangeRequests(); + @Operation(summary = "변경 요청 승인", description = "PENDING 상태의 변경 요청을 승인하고 설정을 업데이트합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/RequestApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/RequestApi.java index 1a68b45e..0bad7452 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/RequestApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/RequestApi.java @@ -1,6 +1,7 @@ package DGU_AI_LAB.admin_be.domain.requests.controller.docs; import DGU_AI_LAB.admin_be.domain.requests.dto.request.SaveRequestRequestDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails; import DGU_AI_LAB.admin_be.global.common.SuccessResponse; @@ -78,4 +79,18 @@ ResponseEntity> getMyApprovedRequests( content = @Content(schema = @Schema(implementation = SuccessResponse.class)) ) ResponseEntity> getAllFulfilledUsernames(); + + @Operation( + summary = "내 변경 요청 목록 조회", + description = "로그인된 사용자가 제출한 모든 변경 요청 내역을 조회합니다. 모든 상태(PENDING, FULFILLED, DENIED)의 변경 요청이 포함됩니다." + ) + @ApiResponse( + responseCode = "200", + description = "조회 성공", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChangeRequestResponseDTO.class))) + ) + ResponseEntity> getMyChangeRequests( + @Parameter(hidden = true, description = "인증된 사용자") + CustomUserDetails user + ); } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/ChangeRequestResponseDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/ChangeRequestResponseDTO.java index 93bc46ec..111c6fd2 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/ChangeRequestResponseDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/ChangeRequestResponseDTO.java @@ -18,6 +18,7 @@ public record ChangeRequestResponseDTO( @JsonRawValue String newValue, String reason, Status status, + String adminComment, AdminUserInfo requestedBy, LocalDateTime createdAt ) { @@ -30,6 +31,7 @@ public static ChangeRequestResponseDTO fromEntity(ChangeRequest changeRequest) { .newValue(changeRequest.getNewValue()) .reason(changeRequest.getReason()) .status(changeRequest.getStatus()) + .adminComment(changeRequest.getAdminComment()) .requestedBy(AdminUserInfo.fromEntity(changeRequest.getRequestedBy())) .createdAt(changeRequest.getCreatedAt()) .build(); diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/Request.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/Request.java index ba8a3838..6c360c93 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/Request.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/Request.java @@ -19,6 +19,7 @@ @Table(name = "requests") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "ubuntuUsername", callSuper = false) public class Request extends BaseTimeEntity { @Id diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/RequestGroup.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/RequestGroup.java index 945e967c..d1263245 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/RequestGroup.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/RequestGroup.java @@ -8,18 +8,14 @@ @Entity @Table(name = "request_groups") -@Access(AccessType.FIELD) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EqualsAndHashCode(of = "id") +@EqualsAndHashCode(of = {"request", "group"}) public class RequestGroup { @EmbeddedId private RequestGroupId id; - @Column(name = "created_at", nullable = false) - private LocalDateTime createdAt; - @ManyToOne(fetch = FetchType.LAZY) @MapsId("requestId") @JoinColumn(name = "request_id", nullable = false) private Request request; @@ -28,6 +24,9 @@ public class RequestGroup { @JoinColumn(name = "ubuntu_gid", nullable = false) private Group group; + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + @Builder public RequestGroup(Request request, Group group) { this.request = request; @@ -36,10 +35,12 @@ public RequestGroup(Request request, Group group) { @PrePersist void onCreate() { - if (createdAt == null) createdAt = LocalDateTime.now(); - // @MapsId가 id를 자동으로 채워주지만 방어적으로 ID도 보완 - if (id == null && request != null && group != null) { - id = new RequestGroupId(request.getRequestId(), group.getUbuntuGid()); + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + + if (this.id == null) { + this.id = new RequestGroupId(this.request.getRequestId(), this.group.getUbuntuGid()); } } -} +} \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/ChangeRequestRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/ChangeRequestRepository.java index f82ec134..d58762c3 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/ChangeRequestRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/ChangeRequestRepository.java @@ -9,4 +9,5 @@ public interface ChangeRequestRepository extends JpaRepository { List findAllByStatus(Status status); List findAllByRequestedBy_UserIdAndStatus(Long userId, Status status); + List findAllByRequestedBy_UserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestCommandService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestCommandService.java index 175dcc56..a769d26e 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestCommandService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestCommandService.java @@ -1,5 +1,6 @@ package DGU_AI_LAB.admin_be.domain.requests.service; +import DGU_AI_LAB.admin_be.domain.alarm.service.AlarmService; import DGU_AI_LAB.admin_be.domain.containerImage.entity.ContainerImage; import DGU_AI_LAB.admin_be.domain.containerImage.repository.ContainerImageRepository; import DGU_AI_LAB.admin_be.domain.groups.entity.Group; @@ -42,6 +43,8 @@ @Transactional public class AdminRequestCommandService { + private final AlarmService alarmService; + private final RequestRepository requestRepository; private final UserRepository userRepository; private final ContainerImageRepository containerImageRepository; @@ -152,8 +155,16 @@ public SaveRequestResponseDTO approveRequest(ApproveRequestDTO dto) { ResourceGroup rg = resourceGroupRepository.findById(dto.resourceGroupId()) .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); request.approve(image, rg, dto.volumeSizeGiB(), dto.adminComment()); - requestRepository.flush(); + // requestRepository.flush(); + // 4. 사용자에게 승인 알림 발송 + try { + alarmService.sendApprovalNotification(request); + log.info("사용자 '{}'에게 승인 알림을 성공적으로 발송했습니다.", request.getUser().getName()); + } catch (Exception e) { + log.warn("사용자 '{}'에게 승인 알림을 보내는 데 실패했습니다. (RequestId: {})", + request.getUser().getName(), request.getRequestId(), e); + } return SaveRequestResponseDTO.fromEntity(request); } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestQueryService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestQueryService.java index daf801c3..9a40f678 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestQueryService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/AdminRequestQueryService.java @@ -64,4 +64,10 @@ public List getChangeRequests() { .map(ChangeRequestResponseDTO::fromEntity) .collect(Collectors.toList()); } + + public List getAllChangeRequests() { + return changeRequestRepository.findAll().stream() + .map(ChangeRequestResponseDTO::fromEntity) + .collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestQueryService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestQueryService.java index 07eb31ed..14fb12d2 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestQueryService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestQueryService.java @@ -1,12 +1,14 @@ package DGU_AI_LAB.admin_be.domain.requests.service; import DGU_AI_LAB.admin_be.domain.portRequests.service.PortRequestService; +import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.ContainerInfoDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.PortMappingDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.ResourceUsageDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.entity.Request; import DGU_AI_LAB.admin_be.domain.requests.entity.Status; +import DGU_AI_LAB.admin_be.domain.requests.repository.ChangeRequestRepository; import DGU_AI_LAB.admin_be.domain.requests.repository.RequestRepository; import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; import DGU_AI_LAB.admin_be.error.ErrorCode; @@ -25,6 +27,7 @@ public class RequestQueryService { private final RequestRepository requestRepository; + private final ChangeRequestRepository changeRequestRepository; private final UserRepository userRepository; private final PortRequestService portRequestService; @@ -79,4 +82,14 @@ public List getApprovedRequestsByUserId(Long userId) { .toList(); } + /** 내 변경 요청 목록 조회 */ + public List getMyChangeRequests(Long userId) { + if (!userRepository.existsById(userId)) { + throw new BusinessException(ErrorCode.USER_NOT_FOUND); + } + return changeRequestRepository.findAllByRequestedBy_UserId(userId).stream() + .map(ChangeRequestResponseDTO::fromEntity) + .toList(); + } + } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java index 4d468e24..594eaf62 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/entity/User.java @@ -8,6 +8,7 @@ @Table(name = "users") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "email", callSuper = false) public class User extends BaseTimeEntity { @Id