From 74aba15973578eca627884b0f65363544c88328d Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:27:17 +0900 Subject: [PATCH 01/23] =?UTF-8?q?[feat]=20#121=20ChangeType=20enum=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin_be/domain/requests/entity/ChangeType.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java index 424f107a..01eb4b1e 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java @@ -1,5 +1,9 @@ package DGU_AI_LAB.admin_be.domain.requests.entity; public enum ChangeType { - VOLUME_SIZE, EXPIRES_AT, RSGROUP_ID, IMAGE_ID, GROUPS + VOLUME_SIZE, + EXPIRES_AT, + GROUP, // 사용자가 속한 그룹 변경 + RESOURCE_GROUP, // 리소스 그룹 변경 + CONTAINER_IMAGE // 도커 이미지 변경 } From 1aad5fff3492fa448fa032c09c83d4acabea5113 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:27:30 +0900 Subject: [PATCH 02/23] =?UTF-8?q?[docs]=20#121=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/controller/RequestController.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 991be72b..a70aa6d6 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 @@ -23,6 +23,9 @@ public class RequestController implements RequestApi { private final RequestQueryService requestService; private final RequestCommandService requestCommandService; + /** + * 사용 신청 생성 + */ @PostMapping public ResponseEntity createRequest( @AuthenticationPrincipal(expression = "userId") Long userId, @@ -32,6 +35,9 @@ public ResponseEntity createRequest( return ResponseEntity.ok(body); } + /** + * 사용 신청 변경 (저장공간 크기, 만료기한, 사용자가 속한 그룹, 리소스 그룹, 도커 이미지) + */ @PostMapping("/{requestId}/change") public ResponseEntity createChangeRequest( @AuthenticationPrincipal(expression = "userId") Long userId, @@ -42,6 +48,9 @@ public ResponseEntity createChangeRequest( return ResponseEntity.ok().build(); } + /** + * 나의 사용 신청 조회 + */ @GetMapping("/my") public ResponseEntity> getMyRequests( @AuthenticationPrincipal CustomUserDetails user From 5a37ec28daa23786da43bb0e373789f66b44ffed Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:28:49 +0900 Subject: [PATCH 03/23] =?UTF-8?q?[feat]=20#121=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=A0=EC=B2=AD=20=ED=95=AD=EB=AA=A9?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/dto/request/ModifyRequestDTO.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java index 2760cc67..c89e1c98 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.Positive; import java.time.LocalDateTime; +import java.util.Set; public record ModifyRequestDTO( @NotBlank(message = "변경 사유는 필수입니다.") @@ -12,6 +13,13 @@ public record ModifyRequestDTO( @Positive(message = "볼륨 크기는 양수여야 합니다.") Long requestedVolumeSizeGiB, - LocalDateTime requestedExpiresAt + LocalDateTime requestedExpiresAt, + + Set requestedGroupIds, + + Integer requestedResourceGroupId, + + Long requestedContainerImageId + ) { } \ No newline at end of file From 90fe85600dd67c215cfd9c6ab8c19e8814454a9b Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:31:24 +0900 Subject: [PATCH 04/23] =?UTF-8?q?[feat]=20#121=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=A0=EC=B2=AD=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=9C=20=ED=95=AD=EB=AA=A9=EA=B0=92=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RequestCommandService.java | 87 +++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestCommandService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestCommandService.java index 674bdfdf..e2800612 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestCommandService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestCommandService.java @@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Set; +import java.util.stream.Collectors; @Slf4j @Service @@ -55,7 +56,7 @@ public void createModificationRequest(Long userId, Long requestId, ModifyRequest throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST); } - // FULFILLED 상태에서만 변경 요청 가능. + // FULFILLED 상태에서만 변경 요청 가능 if (originalRequest.getStatus() != Status.FULFILLED) { throw new BusinessException(ErrorCode.INVALID_REQUEST_STATUS); } @@ -63,36 +64,70 @@ public void createModificationRequest(Long userId, Long requestId, ModifyRequest User requestedBy = userRepository.findById(userId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + // 저장공간 크기 변경 if (dto.requestedVolumeSizeGiB() != null) { - try { - ChangeRequest changeRequest = ChangeRequest.builder() - .request(originalRequest) - .changeType(ChangeType.VOLUME_SIZE) - .oldValue(objectMapper.writeValueAsString(originalRequest.getVolumeSizeGiB())) - .newValue(objectMapper.writeValueAsString(dto.requestedVolumeSizeGiB())) - .reason(dto.reason()) - .requestedBy(requestedBy) - .build(); - changeRequestRepository.save(changeRequest); - } catch (Exception e) { - throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); - } + createAndSaveChangeRequest(originalRequest, requestedBy, ChangeType.VOLUME_SIZE, + originalRequest.getVolumeSizeGiB(), dto.requestedVolumeSizeGiB(), dto.reason()); } + // 만료 기한 변경 if (dto.requestedExpiresAt() != null) { - try { - ChangeRequest changeRequest = ChangeRequest.builder() - .request(originalRequest) - .changeType(ChangeType.EXPIRES_AT) - .oldValue(objectMapper.writeValueAsString(originalRequest.getExpiresAt().toString())) - .newValue(objectMapper.writeValueAsString(dto.requestedExpiresAt().toString())) - .reason(dto.reason()) - .requestedBy(requestedBy) - .build(); - changeRequestRepository.save(changeRequest); - } catch (Exception e) { - throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); + createAndSaveChangeRequest(originalRequest, requestedBy, ChangeType.EXPIRES_AT, + originalRequest.getExpiresAt().toString(), dto.requestedExpiresAt().toString(), dto.reason()); + } + + // 그룹 변경 + if (dto.requestedGroupIds() != null && !dto.requestedGroupIds().isEmpty()) { + // 변경 전 그룹 목록 조회 + Set oldGroupIds = originalRequest.getRequestGroups().stream() + .map(requestGroup -> requestGroup.getGroup().getUbuntuGid()) + .collect(Collectors.toSet()); + + // 변경 후 그룹 존재 여부 확인 + if (groupRepository.findAllByUbuntuGidIn(dto.requestedGroupIds()).size() != dto.requestedGroupIds().size()) { + throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND); } + + createAndSaveChangeRequest(originalRequest, requestedBy, ChangeType.GROUP, + oldGroupIds, dto.requestedGroupIds(), dto.reason()); + } + + // 리소스 그룹 변경 + if (dto.requestedResourceGroupId() != null) { + ResourceGroup newResourceGroup = resourceGroupRepository.findById(dto.requestedResourceGroupId()) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + + createAndSaveChangeRequest(originalRequest, requestedBy, ChangeType.RESOURCE_GROUP, + originalRequest.getResourceGroup().getRsgroupId(), dto.requestedResourceGroupId(), dto.reason()); + } + + // 도커 이미지 변경 + if (dto.requestedContainerImageId() != null) { + ContainerImage newImage = containerImageRepository.findById(dto.requestedContainerImageId()) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + + createAndSaveChangeRequest(originalRequest, requestedBy, ChangeType.CONTAINER_IMAGE, + originalRequest.getContainerImage().getImageId(), dto.requestedContainerImageId(), dto.reason()); + } + } + + + // 중복 코드 방지를 위한 헬퍼 메서드 + private void createAndSaveChangeRequest(Request originalRequest, User requestedBy, + ChangeType changeType, T oldValue, T newValue, String reason) { + try { + ChangeRequest changeRequest = ChangeRequest.builder() + .request(originalRequest) + .changeType(changeType) + .oldValue(objectMapper.writeValueAsString(oldValue)) + .newValue(objectMapper.writeValueAsString(newValue)) + .reason(reason) + .requestedBy(requestedBy) + .build(); + changeRequestRepository.save(changeRequest); + } catch (Exception e) { + log.error("Failed to create change request for type {}: {}", changeType, e.getMessage()); + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); } } From e5c24fdd1132f37f239c3efce815aac4b5856f39 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:53:51 +0900 Subject: [PATCH 05/23] =?UTF-8?q?[feat]=20#121=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=82=AC=EC=9A=A9=20=EB=B3=80=EA=B2=BD=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=8A=B9=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../groups/repository/GroupRepository.java | 2 + .../dto/request/RejectRequestDTO.java | 2 +- .../service/AdminRequestCommandService.java | 56 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java index 2a73c115..2bd99ad0 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java @@ -5,10 +5,12 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; import java.util.Set; @Repository public interface GroupRepository extends JpaRepository { List findAllByUbuntuGidIn(Set ubuntuGids); boolean existsByUbuntuGid(Long ubuntuGid); + Optional findByUbuntuGid(Long ubuntuGid); } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectRequestDTO.java index 8c4df972..e30d21b1 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectRequestDTO.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -@Schema(description = "1. 관리자용 요청 거절 요청 DTO") +@Schema(description = "관리자용 요청 거절 요청 DTO") public record RejectRequestDTO( @Schema(description = "거절할 요청 ID", example = "3") 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 b4a977d2..98ed7bc7 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 @@ -2,6 +2,8 @@ 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; +import DGU_AI_LAB.admin_be.domain.groups.repository.GroupRepository; import DGU_AI_LAB.admin_be.domain.requests.dto.request.*; import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.entity.ChangeRequest; @@ -16,6 +18,8 @@ import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; import DGU_AI_LAB.admin_be.error.ErrorCode; import DGU_AI_LAB.admin_be.error.exception.BusinessException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -27,7 +31,10 @@ import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; +import java.time.LocalDateTime; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Slf4j @Service @@ -41,6 +48,8 @@ public class AdminRequestCommandService { private final ResourceGroupRepository resourceGroupRepository; private final IdAllocationService idAllocationService; private final ChangeRequestRepository changeRequestRepository; + private final GroupRepository groupRepository; + private final ObjectMapper objectMapper; private final @Qualifier("pvcWebClient") WebClient pvcWebClient; private final @Qualifier("pvcWebClient") WebClient userCreationWebClient; @@ -155,16 +164,61 @@ public SaveRequestResponseDTO rejectRequest(RejectRequestDTO dto) { public void approveModification(Long adminId, ApproveModificationDTO dto) { ChangeRequest changeRequest = changeRequestRepository.findById(dto.changeRequestId()) .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + if (changeRequest.getStatus() != Status.PENDING) { throw new BusinessException(ErrorCode.INVALID_REQUEST_STATUS); } User admin = userRepository.findById(adminId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + Request originalRequest = changeRequest.getRequest(); if (originalRequest == null) { throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND); } - originalRequest.applyChange(changeRequest.getChangeType(), changeRequest.getNewValue()); + + try { + switch (changeRequest.getChangeType()) { + case VOLUME_SIZE: + Long newVolumeSize = objectMapper.readValue(changeRequest.getNewValue(), Long.class); + originalRequest.updateVolumeSize(newVolumeSize); + break; + case EXPIRES_AT: + LocalDateTime newExpiresAt = LocalDateTime.parse(objectMapper.readValue(changeRequest.getNewValue(), String.class)); + originalRequest.updateExpiresAt(newExpiresAt); + break; + case GROUP: + // 그룹 변경은 복잡하기 때문에, 엔티티가 아닌 서비스 레이어에서 처리합니다. + originalRequest.getRequestGroups().clear(); + Set newGroupIds = objectMapper.readValue(changeRequest.getNewValue(), Set.class); + Set newGroups = newGroupIds.stream() + .map(gid -> groupRepository.findByUbuntuGid(gid) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND))) + .collect(Collectors.toSet()); + + for (Group g : newGroups) { + originalRequest.addGroup(g); + } + break; + case RESOURCE_GROUP: + Integer newResourceGroupId = objectMapper.readValue(changeRequest.getNewValue(), Integer.class); + ResourceGroup newResourceGroup = resourceGroupRepository.findById(newResourceGroupId) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + originalRequest.updateResourceGroup(newResourceGroup); + break; + case CONTAINER_IMAGE: + Long newImageId = objectMapper.readValue(changeRequest.getNewValue(), Long.class); + ContainerImage newContainerImage = containerImageRepository.findById(newImageId) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + originalRequest.updateContainerImage(newContainerImage); + break; + default: + throw new BusinessException(ErrorCode.UNSUPPORTED_CHANGE_TYPE); + } + } catch (JsonProcessingException e) { + log.error("Failed to parse change request value: {}", e.getMessage()); + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); + } + changeRequest.approve(admin, dto.adminComment()); } } \ No newline at end of file From 3c4f78d0ff4133c8d10a3ecb89159546dbce9d55 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 16:54:58 +0900 Subject: [PATCH 06/23] =?UTF-8?q?[feat]=20#121=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B9=84?= =?UTF-8?q?=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/entity/Request.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) 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 c8270681..5f062e0d 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 @@ -87,20 +87,28 @@ public class Request extends BaseTimeEntity { /** * 변경 요청을 반영하여 엔티티의 속성을 업데이트합니다. */ - public void applyChange(ChangeType changeType, String newValue) { - if (this.status != Status.FULFILLED) { - throw new BusinessException(ErrorCode.INVALID_REQUEST_STATUS); + + public void updateVolumeSize(Long newVolumeSize) { + if (newVolumeSize != null) { + this.volumeSizeGiB = newVolumeSize; + } + } + + public void updateExpiresAt(LocalDateTime newExpiresAt) { + if (newExpiresAt != null) { + this.expiresAt = newExpiresAt; + } + } + + public void updateResourceGroup(ResourceGroup newResourceGroup) { + if (newResourceGroup != null) { + this.resourceGroup = newResourceGroup; } + } - switch (changeType) { - case VOLUME_SIZE: - this.volumeSizeGiB = Long.parseLong(newValue); - break; - case EXPIRES_AT: - this.expiresAt = LocalDateTime.parse(newValue); - break; - default: - throw new BusinessException(ErrorCode.UNSUPPORTED_CHANGE_TYPE); + public void updateContainerImage(ContainerImage newImage) { + if (newImage != null) { + this.containerImage = newImage; } } From 8afd72b14be3eb515ae88715e90da1216326719e Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:17:06 +0900 Subject: [PATCH 07/23] =?UTF-8?q?[docs]=20#112=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin_be/domain/requests/entity/ChangeType.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java index 01eb4b1e..531701d2 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/entity/ChangeType.java @@ -1,8 +1,8 @@ package DGU_AI_LAB.admin_be.domain.requests.entity; public enum ChangeType { - VOLUME_SIZE, - EXPIRES_AT, + VOLUME_SIZE, // 볼륨 사이즈 변경 + EXPIRES_AT, // 서버 사용 만료 기한 변경 GROUP, // 사용자가 속한 그룹 변경 RESOURCE_GROUP, // 리소스 그룹 변경 CONTAINER_IMAGE // 도커 이미지 변경 From eb883732de3a45e1efbb226c5cc57249e4937876 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:17:28 +0900 Subject: [PATCH 08/23] =?UTF-8?q?[docs]=20#112=20API=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20validation=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/docs/AdminRequestApi.java | 106 +++++++++--------- .../docs/AdminRequestChangeApi.java | 46 ++++++++ .../dto/request/ApproveModificationDTO.java | 9 +- .../dto/request/ModifyRequestDTO.java | 13 ++- .../dto/request/RejectModificationDTO.java | 15 +++ 5 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java create mode 100644 src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java index 811755b2..7a791950 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java @@ -1,74 +1,74 @@ package DGU_AI_LAB.admin_be.domain.requests.controller.docs; -import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveModificationDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveRequestDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectRequestDTO; -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.ResourceUsageDTO; -import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; +import DGU_AI_LAB.admin_be.error.dto.ErrorResponse; +import DGU_AI_LAB.admin_be.global.common.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; -import java.util.List; - -@Tag(name = "1. 관리자 서버 사용 신청 처리", description = "관리자용 서버 사용 신청 관리 API") +@Tag(name = "1. 관리자 - 서버 사용 신청 관리", description = "신규 신청 조회 및 승인/거절 API") public interface AdminRequestApi { - @Operation(summary = "변경 요청 승인", description = "사용자의 변경 요청을 승인합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "404", description = "변경 요청을 찾을 수 없음"), - @ApiResponse(responseCode = "409", description = "요청 상태가 PENDING이 아님") + @Operation(summary = "모든 요청 목록 조회", description = "모든 상태의 서버 사용 신청 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = SuccessResponse.class), + examples = @ExampleObject(value = "{\"status\": 200, \"message\": \"요청이 성공했습니다.\", \"data\": [...]}") + )), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class))) }) - @PatchMapping("/change/approve") - ResponseEntity approveModification( - @AuthenticationPrincipal(expression = "userId") Long adminId, - @RequestBody @Valid ApproveModificationDTO dto - ); - - @Operation(summary = "모든 리소스 사용량 조회", description = "현재 사용 중인 컨테이너들의 리소스 사용량을 조회합니다.") - @GetMapping("/usage") - ResponseEntity> getAllResourceUsage(); - - @Operation(summary = "모든 컨테이너 정보 조회", description = "현재 활성화된 모든 컨테이너의 상세 정보를 조회합니다.") - @GetMapping("/containers") - ResponseEntity> getAllActiveContainers(); + ResponseEntity> getAllRequests(); - @Operation(summary = "모든 요청 목록 조회", description = "모든 상태의 사용자 요청 목록을 조회합니다.") - @GetMapping - ResponseEntity> getAllRequests(); + @Operation(summary = "신규 신청 목록 조회", description = "PENDING 상태의 서버 사용 신청 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))) + }) + ResponseEntity> getNewRequests(); - @Operation(summary = "신규 신청 목록 조회", description = "PENDING 상태의 신규 사용자 신청 목록을 조회합니다.") - @GetMapping("/new") - ResponseEntity> getNewRequests(); + @Operation(summary = "전체 리소스 사용량 조회", description = "FULFILLED 상태인 모든 서버의 리소스 사용량 정보를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))) + }) + ResponseEntity> getAllResourceUsage(); - @Operation(summary = "변경 요청 목록 조회", description = "PENDING 상태의 사용자 변경 요청 목록을 조회합니다.") - @GetMapping("/change") - ResponseEntity> getChangeRequests(); + @Operation(summary = "모든 활성 컨테이너 조회", description = "현재 활성화된 모든 컨테이너 정보를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))) + }) + ResponseEntity> getAllActiveContainers(); - @Operation(summary = "신규 신청 승인", description = "신규 사용자 신청을 승인하고, 계정 및 리소스를 할당합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "404", description = "요청을 찾을 수 없음"), - @ApiResponse(responseCode = "409", description = "요청 상태가 PENDING이 아님"), - @ApiResponse(responseCode = "502", description = "외부 서버 오류") + @Operation(summary = "사용 신청 승인", description = "PENDING 상태의 사용 신청을 승인하고 사용자 PVC를 생성합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "409", description = "사용자명 중복", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @PatchMapping("/approval") - ResponseEntity approve(@RequestBody @Valid ApproveRequestDTO dto); + ResponseEntity> approveRequest(ApproveRequestDTO dto); - @Operation(summary = "신규 신청 거절", description = "신규 사용자 신청을 거절합니다.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "404", description = "요청을 찾을 수 없음"), - @ApiResponse(responseCode = "409", description = "요청 상태가 PENDING 또는 FULFILLED가 아님") + @Operation(summary = "사용 신청 거절", description = "PENDING 상태의 사용 신청을 거절합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) - @PatchMapping("/reject") - ResponseEntity reject(@RequestBody @Valid RejectRequestDTO dto); + ResponseEntity> rejectRequest(RejectRequestDTO dto); } \ 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 new file mode 100644 index 00000000..cc40a3af --- /dev/null +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestChangeApi.java @@ -0,0 +1,46 @@ +package DGU_AI_LAB.admin_be.domain.requests.controller.docs; + +import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveModificationDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectModificationDTO; +import DGU_AI_LAB.admin_be.error.dto.ErrorResponse; +import DGU_AI_LAB.admin_be.global.common.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "2. 관리자 - 서버 변경 요청 관리", description = "서버 설정 변경 요청 조회 및 승인/거절 API") +public interface AdminRequestChangeApi { + + @Operation(summary = "변경 요청 목록 조회", description = "PENDING 상태의 변경 요청 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))) + }) + ResponseEntity> getChangeRequests(); + + @Operation(summary = "변경 요청 승인", description = "PENDING 상태의 변경 요청을 승인하고 서버 설정을 업데이트합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 상태", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + ResponseEntity> approveModification(Long adminId, ApproveModificationDTO dto); + + @Operation(summary = "변경 요청 거절", description = "PENDING 상태의 변경 요청을 거절합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "404", description = "리소스 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "유효하지 않은 상태", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + ResponseEntity> rejectModification(Long adminId, RejectModificationDTO dto); +} \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ApproveModificationDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ApproveModificationDTO.java index 886f4315..10e1dc3b 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ApproveModificationDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ApproveModificationDTO.java @@ -1,12 +1,17 @@ package DGU_AI_LAB.admin_be.domain.requests.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +@Schema(description = "관리자용 변경 요청 승인 DTO") public record ApproveModificationDTO( + + @Schema(description = "승인할 변경 요청 ID", example = "5") @NotNull(message = "변경 요청 ID는 필수입니다.") Long changeRequestId, + + @Schema(description = "관리자 승인 코멘트", example = "변경 요청을 승인합니다.") @NotBlank(message = "승인 사유는 필수입니다.") String adminComment -) { -} \ No newline at end of file +) {} \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java index c89e1c98..90aeb4fa 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/ModifyRequestDTO.java @@ -1,25 +1,32 @@ package DGU_AI_LAB.admin_be.domain.requests.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import java.time.LocalDateTime; import java.util.Set; +@Schema(description = "사용자용 서버 설정 변경 요청 DTO") public record ModifyRequestDTO( + + @Schema(description = "변경 요청 사유 (필수)", example = "프로젝트 요구사항 변경으로 인한 용량 증설") @NotBlank(message = "변경 사유는 필수입니다.") String reason, + @Schema(description = "요청하는 새로운 저장 공간 크기(GiB)", example = "200") @Positive(message = "볼륨 크기는 양수여야 합니다.") Long requestedVolumeSizeGiB, + @Schema(description = "요청하는 새로운 만료 기한", example = "2026-09-02T10:00:00") LocalDateTime requestedExpiresAt, + @Schema(description = "요청하는 새로운 그룹 ID 목록", example = "[1001, 1002]") Set requestedGroupIds, + @Schema(description = "요청하는 새로운 리소스 그룹 ID", example = "2") Integer requestedResourceGroupId, + @Schema(description = "요청하는 새로운 도커 이미지 ID", example = "3") Long requestedContainerImageId - -) { -} \ No newline at end of file +) {} \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java new file mode 100644 index 00000000..f2df82dc --- /dev/null +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java @@ -0,0 +1,15 @@ +package DGU_AI_LAB.admin_be.domain.requests.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "관리자용 변경 요청 거절 DTO") +public record RejectModificationDTO( + + @NotNull(message = "변경 요청 ID는 필수입니다.") + Long changeRequestId, + @NotBlank(message = "거절 사유는 필수입니다.") + String adminComment +) { +} \ No newline at end of file From 20c9846a72a9a57ccbef68c7e7b919dd5ef81d4a Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:18:27 +0900 Subject: [PATCH 09/23] =?UTF-8?q?[refactor]=20#112=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=82=AC=EC=9A=A9=20=EC=8B=A0=EC=B2=AD=EA=B3=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminRequestChangeController.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java 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 new file mode 100644 index 00000000..f096f0be --- /dev/null +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestChangeController.java @@ -0,0 +1,55 @@ +package DGU_AI_LAB.admin_be.domain.requests.controller; + +import DGU_AI_LAB.admin_be.domain.requests.controller.docs.AdminRequestChangeApi; +import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveModificationDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectModificationDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; +import DGU_AI_LAB.admin_be.domain.requests.service.AdminRequestCommandService; +import DGU_AI_LAB.admin_be.domain.requests.service.AdminRequestQueryService; +import DGU_AI_LAB.admin_be.global.common.SuccessResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin/requests/change") +public class AdminRequestChangeController implements AdminRequestChangeApi { + + private final AdminRequestCommandService adminRequestCommandService; + private final AdminRequestQueryService adminRequestQueryService; + + /** + * 변경 요청 목록 조회 (관리자용) + * PENDING 상태의 ChangeRequest 목록을 반환합니다. + */ + @GetMapping("/change") + public ResponseEntity> getChangeRequests() { + List changeRequests = adminRequestQueryService.getChangeRequests(); + return ResponseEntity.ok((SuccessResponse) changeRequests); + } + + @PatchMapping("/change/approve") + public ResponseEntity> approveModification( + @AuthenticationPrincipal(expression = "userId") Long adminId, + @RequestBody @Valid ApproveModificationDTO dto + ) { + adminRequestCommandService.approveModification(adminId, dto); + return ResponseEntity.ok().build(); + } + + + @PatchMapping("/change/reject") + public ResponseEntity> rejectModification( + @AuthenticationPrincipal(expression = "userId") Long adminId, + @RequestBody @Valid RejectModificationDTO dto + ) { + adminRequestCommandService.rejectModification(adminId, dto); + return ResponseEntity.ok().build(); + } + +} From b743cbb97a3f04677f19851bb93a4b3a985bd7fa Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:18:39 +0900 Subject: [PATCH 10/23] =?UTF-8?q?[refactor]=20#112=20deny=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20reject=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminRequestCommandService.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 98ed7bc7..67abdce8 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 @@ -160,6 +160,22 @@ public SaveRequestResponseDTO rejectRequest(RejectRequestDTO dto) { return SaveRequestResponseDTO.fromEntity(request); } + + @Transactional + public void rejectModification(Long adminId, RejectModificationDTO dto) { + ChangeRequest changeRequest = changeRequestRepository.findById(dto.changeRequestId()) + .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND)); + + if (changeRequest.getStatus() != Status.PENDING) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_STATUS); + } + + User admin = userRepository.findById(adminId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + changeRequest.deny(admin, dto.adminComment()); + } + @Transactional public void approveModification(Long adminId, ApproveModificationDTO dto) { ChangeRequest changeRequest = changeRequestRepository.findById(dto.changeRequestId()) From 63daf48953fbc0adf878c3d9541e0cb27f0ce725 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:18:58 +0900 Subject: [PATCH 11/23] =?UTF-8?q?[refactor]=20#112=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=82=AC=EC=9A=A9=20=EC=8B=A0=EC=B2=AD=EA=B3=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminRequestController.java | 62 ++++++++----------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java index f5ebb5b7..33acdc83 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java @@ -3,6 +3,7 @@ import DGU_AI_LAB.admin_be.domain.requests.controller.docs.AdminRequestApi; import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveModificationDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveRequestDTO; +import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectModificationDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectRequestDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.ContainerInfoDTO; @@ -10,6 +11,7 @@ import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.service.AdminRequestCommandService; import DGU_AI_LAB.admin_be.domain.requests.service.AdminRequestQueryService; +import DGU_AI_LAB.admin_be.global.common.SuccessResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -26,32 +28,15 @@ public class AdminRequestController implements AdminRequestApi { private final AdminRequestCommandService adminRequestCommandService; private final AdminRequestQueryService adminRequestQueryService; - @PatchMapping("/change/approve") - public ResponseEntity approveModification( - @AuthenticationPrincipal(expression = "userId") Long adminId, - @RequestBody @Valid ApproveModificationDTO dto - ) { - adminRequestCommandService.approveModification(adminId, dto); - return ResponseEntity.ok().build(); - } - - @GetMapping("/usage") - public ResponseEntity> getAllResourceUsage() { - return ResponseEntity.ok(adminRequestQueryService.getAllFulfilledResourceUsage()); - } - - @GetMapping("/containers") - public ResponseEntity> getAllActiveContainers() { - return ResponseEntity.ok(adminRequestQueryService.getAllActiveContainers()); - } /** * 모든 요청 목록 조회 (관리자용) * 모든 상태의 Request 목록을 반환합니다. */ @GetMapping - public ResponseEntity> getAllRequests() { - return ResponseEntity.ok(adminRequestQueryService.getAllRequests()); + public ResponseEntity> getAllRequests() { + List requests = adminRequestQueryService.getAllRequests(); + return SuccessResponse.ok(requests); } /** @@ -59,28 +44,33 @@ public ResponseEntity> getAllRequests() { * PENDING 상태의 Request 목록을 반환합니다. */ @GetMapping("/new") - public ResponseEntity> getNewRequests() { + public ResponseEntity> getNewRequests() { List requests = adminRequestQueryService.getNewRequests(); - return ResponseEntity.ok(requests); + return SuccessResponse.ok(requests); } - /** - * 변경 요청 목록 조회 (관리자용) - * PENDING 상태의 ChangeRequest 목록을 반환합니다. - */ - @GetMapping("/change") - public ResponseEntity> getChangeRequests() { - List changeRequests = adminRequestQueryService.getChangeRequests(); - return ResponseEntity.ok(changeRequests); + @GetMapping("/usage") + public ResponseEntity> getAllResourceUsage() { + List usage = adminRequestQueryService.getAllFulfilledResourceUsage(); + return SuccessResponse.ok(usage); } - @PatchMapping("/approval") - public ResponseEntity approve(@RequestBody @Valid ApproveRequestDTO dto) { - return ResponseEntity.ok(adminRequestCommandService.approveRequest(dto)); + @GetMapping("/containers") + public ResponseEntity> getAllActiveContainers() { + List containers = adminRequestQueryService.getAllActiveContainers(); + return SuccessResponse.ok(containers); + } + + + @PatchMapping("/approve") + public ResponseEntity> approveRequest(@RequestBody @Valid ApproveRequestDTO dto) { + SaveRequestResponseDTO responseDto = adminRequestCommandService.approveRequest(dto); + return SuccessResponse.ok(responseDto); } @PatchMapping("/reject") - public ResponseEntity reject(@RequestBody @Valid RejectRequestDTO dto) { - return ResponseEntity.ok(adminRequestCommandService.rejectRequest(dto)); + public ResponseEntity> rejectRequest(@RequestBody @Valid RejectRequestDTO dto) { + SaveRequestResponseDTO responseDto = adminRequestCommandService.rejectRequest(dto); + return SuccessResponse.ok(responseDto); } -} \ No newline at end of file +} From b90d53b2a90edebcd93e42ad0fbe54d6f6b3b8b7 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:20:01 +0900 Subject: [PATCH 12/23] =?UTF-8?q?[docs]=20#112=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/controller/docs/AdminRequestChangeApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cc40a3af..98c3aa1d 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 @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -@Tag(name = "2. 관리자 - 서버 변경 요청 관리", description = "서버 설정 변경 요청 조회 및 승인/거절 API") +@Tag(name = "1. 관리자 - 서버 변경 요청 관리", description = "서버 설정 변경 요청 조회 및 승인/거절 API") public interface AdminRequestChangeApi { @Operation(summary = "변경 요청 목록 조회", description = "PENDING 상태의 변경 요청 목록을 조회합니다.") From 5f3434bb869d13e0de744569e7feb48669b1b3b0 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Thu, 4 Sep 2025 17:21:18 +0900 Subject: [PATCH 13/23] =?UTF-8?q?[docs]=20#112=20=EC=A7=9C=EC=9E=98?= =?UTF-8?q?=ED=95=9C=20=EB=A9=98=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/controller/docs/AdminRequestApi.java | 2 +- .../requests/controller/docs/AdminRequestChangeApi.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java index 7a791950..7e2b73e9 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/docs/AdminRequestApi.java @@ -13,7 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -@Tag(name = "1. 관리자 - 서버 사용 신청 관리", description = "신규 신청 조회 및 승인/거절 API") +@Tag(name = "1. 관리자 서버 사용 신청 관리", description = "서버 신규 신청 조회 및 승인/거절 API") public interface AdminRequestApi { @Operation(summary = "모든 요청 목록 조회", description = "모든 상태의 서버 사용 신청 목록을 조회합니다.") 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 98c3aa1d..a9e7d8ed 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 @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -@Tag(name = "1. 관리자 - 서버 변경 요청 관리", description = "서버 설정 변경 요청 조회 및 승인/거절 API") +@Tag(name = "1. 관리자 서버 변경 요청 관리", description = "서버 설정 변경 요청 조회 및 승인/거절 API") public interface AdminRequestChangeApi { @Operation(summary = "변경 요청 목록 조회", description = "PENDING 상태의 변경 요청 목록을 조회합니다.") @@ -22,7 +22,7 @@ public interface AdminRequestChangeApi { }) ResponseEntity> getChangeRequests(); - @Operation(summary = "변경 요청 승인", description = "PENDING 상태의 변경 요청을 승인하고 서버 설정을 업데이트합니다.") + @Operation(summary = "변경 요청 승인", description = "PENDING 상태의 변경 요청을 승인하고 설정을 업데이트합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), From ecb2d087a54a3b4c19463ff013dac31144fecd90 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Sun, 7 Sep 2025 00:25:23 +0900 Subject: [PATCH 14/23] =?UTF-8?q?[feat]=20#112=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/groups/service/GroupService.java | 102 ++++++++++++++---- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java index 96720a17..e9f6da75 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java @@ -4,31 +4,46 @@ import DGU_AI_LAB.admin_be.domain.groups.dto.response.GroupResponseDTO; import DGU_AI_LAB.admin_be.domain.groups.entity.Group; import DGU_AI_LAB.admin_be.domain.groups.repository.GroupRepository; +import DGU_AI_LAB.admin_be.domain.requests.repository.RequestRepository; import DGU_AI_LAB.admin_be.domain.usedIds.entity.UsedId; import DGU_AI_LAB.admin_be.domain.usedIds.repository.UsedIdRepository; +import DGU_AI_LAB.admin_be.domain.usedIds.service.IdAllocationService; import DGU_AI_LAB.admin_be.error.ErrorCode; import DGU_AI_LAB.admin_be.error.exception.BusinessException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Map; @Slf4j @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class GroupService { private final GroupRepository groupRepository; private final UsedIdRepository usedIdRepository; + private final RequestRepository requestRepository; + private final ObjectMapper objectMapper; + private final IdAllocationService idAllocationService; + private final @Qualifier("configWebClient") WebClient groupCreationWebClient; + /** * 모든 그룹 정보를 조회하는 API * GET /api/groups */ + @Transactional(readOnly = true) public List getAllGroups() { log.info("[getAllGroups] 모든 그룹 정보 조회 시작"); var groups = groupRepository.findAll(); @@ -51,35 +66,80 @@ public List getAllGroups() { * POST /api/groups */ @Transactional - public GroupResponseDTO createGroup(CreateGroupRequestDTO dto) { - log.info("[createGroup] 그룹 생성 요청 시작: groupName={}, ubuntuGid={}", dto.groupName(), dto.ubuntuGid()); - - // 1. 요청받은 GID로 UsedId가 이미 존재하는지 확인하고, 없으면 생성 - Optional existingUsedId = usedIdRepository.findById(dto.ubuntuGid()); - UsedId usedId; - if (existingUsedId.isPresent()) { - log.warn("[createGroup] 중복된 GID가 UsedId에 이미 존재합니다: {}", dto.ubuntuGid()); - usedId = existingUsedId.get(); - } else { - usedId = usedIdRepository.saveAndFlush(UsedId.builder().idValue(dto.ubuntuGid()).build()); - log.info("[createGroup] UsedId에 GID {} 할당 완료", dto.ubuntuGid()); + public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) { + + log.info("[createGroup] 그룹 생성 요청 시작: groupName={}, ubuntuUsername={}", dto.groupName(), dto.ubuntuUsername()); + + // 1. 요청된 ubuntuUsername이 로그인한 사용자의 계정인지 확인하는 유효성 검사 + if (!requestRepository.existsByUbuntuUsernameAndUser_UserId(dto.ubuntuUsername(), userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST); + } + + // 2. DB에서 그룹명 중복을 먼저 확인합니다. + if (groupRepository.existsByGroupName(dto.groupName())) { + log.warn("[createGroup] DB에 이미 존재하는 그룹명입니다: {}", dto.groupName()); + throw new BusinessException(ErrorCode.DUPLICATE_GROUP_NAME); + } + + // 3. 새로운 GID를 할당받습니다. + Long assignedGid = idAllocationService.allocateNewGid(); + log.info("[createGroup] 새로운 GID 할당 완료: {}", assignedGid); + + // 4. GID가 int 타입으로 변환 가능한지 확인 (오버플로우 방지) + if (assignedGid > Integer.MAX_VALUE || assignedGid < Integer.MIN_VALUE) { + log.error("[createGroup] 할당된 GID가 int 범위를 벗어납니다: {}", assignedGid); + throw new BusinessException(ErrorCode.GID_ALLOCATION_FAILED); } - // 2. 해당 GID의 그룹이 이미 존재하는지 확인 - if (groupRepository.existsByUbuntuGid(dto.ubuntuGid())) { - log.warn("[createGroup] 중복된 GID를 가진 그룹이 이미 존재합니다: {}", dto.ubuntuGid()); - throw new BusinessException(ErrorCode.DUPLICATE_GROUP_ID); + + // 5. 외부 API 호출을 위한 DTO를 준비하고 호출합니다. + Map apiDto = Map.of( + "name", dto.groupName(), + "gid", assignedGid.intValue(), + "members", List.of(dto.ubuntuUsername()) + ); + + try { + log.info("[createGroup] 외부 그룹 생성 API 호출 시작: name={}, gid={}, members={}", apiDto.get("name"), apiDto.get("gid"), apiDto.get("members")); + + groupCreationWebClient.post() + .uri("/accounts/addgroup") + .bodyValue(apiDto) + .retrieve() + .onStatus(HttpStatus.BAD_REQUEST::equals, clientResponse -> + Mono.error(new BusinessException(ErrorCode.GROUP_CREATION_FAILED)) + ) + .onStatus(HttpStatus.CONFLICT::equals, clientResponse -> + Mono.error(new BusinessException(ErrorCode.DUPLICATE_GROUP_ID)) + ) + .onStatus(HttpStatus.INTERNAL_SERVER_ERROR::equals, clientResponse -> + Mono.error(new BusinessException(ErrorCode.EXTERNAL_API_ERROR)) + ) + .bodyToMono(Map.class) + .block(); + + log.info("[createGroup] 외부 API 호출 성공"); + + } catch (WebClientResponseException e) { + log.error("[createGroup] 외부 API 호출 실패: 상태 코드={}, 응답={}", e.getStatusCode(), e.getResponseBodyAsString(), e); + throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED); + } catch (Exception e) { + log.error("[createGroup] 외부 API 호출 중 예상치 못한 오류 발생", e); + throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED); } - // 3. 그룹 엔티티 생성 및 저장 + // 4. API 호출이 성공한 후에만 로컬 DB에 그룹을 저장합니다. + UsedId usedId = usedIdRepository.findById(assignedGid) + .orElseGet(() -> usedIdRepository.saveAndFlush(UsedId.builder().idValue(assignedGid).build())); + Group group = Group.builder() .groupName(dto.groupName()) - .ubuntuGid(dto.ubuntuGid()) + .ubuntuGid(assignedGid) .usedId(usedId) .build(); group = groupRepository.save(group); - log.info("[createGroup] 그룹 생성 완료: id={}, name={}", group.getGroupId(), group.getGroupName()); + log.info("[createGroup] 그룹 생성 및 로컬 DB 저장 완료: id={}, name={}", group.getGroupId(), group.getGroupName()); return GroupResponseDTO.fromEntity(group); } From 293ef98714539ab36802ec9fdc7fe888c2bc4a64 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Sun, 7 Sep 2025 00:25:46 +0900 Subject: [PATCH 15/23] =?UTF-8?q?[feat]=20#112=20=EA=B7=B8=EB=A3=B9=20Id?= =?UTF-8?q?=20=ED=95=A0=EB=8B=B9=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../groups/repository/GroupRepository.java | 1 + .../repository/RequestRepository.java | 1 + .../usedIds/repository/UsedIdRepository.java | 4 +++ .../usedIds/service/IdAllocationService.java | 27 +++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java index 2bd99ad0..f0d47254 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/repository/GroupRepository.java @@ -13,4 +13,5 @@ public interface GroupRepository extends JpaRepository { List findAllByUbuntuGidIn(Set ubuntuGids); boolean existsByUbuntuGid(Long ubuntuGid); Optional findByUbuntuGid(Long ubuntuGid); + boolean existsByGroupName(String groupName); } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/RequestRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/RequestRepository.java index ee086bae..058c0507 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/RequestRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/repository/RequestRepository.java @@ -20,5 +20,6 @@ public interface RequestRepository extends JpaRepository { Optional findTopByUbuntuUsernameAndUbuntuUidIsNotNullOrderByApprovedAtDesc(String ubuntuUsername); boolean existsByUbuntuUsername(String ubuntuUsername); List findAllByUser_UserIdAndStatus(Long userId, Status status); + boolean existsByUbuntuUsernameAndUser_UserId(String ubuntuUsername, Long userId); } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java index 6fe93fbb..c99b4ccd 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java @@ -3,10 +3,14 @@ import DGU_AI_LAB.admin_be.domain.usedIds.entity.UsedId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface UsedIdRepository extends JpaRepository { @Query("SELECT MAX(u.idValue) FROM UsedId u") Optional findMaxIdValue(); + + @Query("SELECT MAX(u.idValue) FROM UsedId u WHERE u.idValue >= :startRange AND u.idValue <= :endRange") + Optional findMaxIdValueInRange(Long startRange, Long endRange); } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/service/IdAllocationService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/service/IdAllocationService.java index 94b19106..4aca4332 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/service/IdAllocationService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/service/IdAllocationService.java @@ -23,6 +23,8 @@ public class IdAllocationService { private static final long UID_BASE = 10_000L; private static final int MAX_RETRY = 5; + private static final long GID_BASE = 2_000L; + private static final long GID_MAX_VALUE = 65535L; private final UsedIdRepository usedIdRepository; private final GroupRepository groupRepository; @@ -66,6 +68,31 @@ private UsedId allocateNewUid() { throw new BusinessException(ErrorCode.UID_ALLOCATION_FAILED); } + /** + * 새로운 GID를 할당하고 UsedId 테이블에 저장합니다. + */ + @Transactional + public Long allocateNewGid() { + for (int i = 0; i < MAX_RETRY; i++) { + // GID 범위 내에서 최대값을 찾습니다. + Long currentMax = usedIdRepository.findMaxIdValueInRange(GID_BASE, GID_MAX_VALUE) + .orElse(GID_BASE - 1L); + + long candidate = Math.max(GID_BASE, currentMax + 1); + + if (candidate > GID_MAX_VALUE) { + throw new BusinessException(ErrorCode.GID_ALLOCATION_FAILED); + } + + try { + usedIdRepository.saveAndFlush(UsedId.builder().idValue(candidate).build()); + return candidate; + } catch (DataIntegrityViolationException ignore) {} + } + throw new BusinessException(ErrorCode.GID_ALLOCATION_FAILED); + } + + private Group createGroupWithSameId(String username, long uidValue) { Optional existing = groupRepository.findById(uidValue); if (existing.isPresent()) return existing.get(); From 3cbfaaf40173166bca4184f5273c492a32443b0b Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Sun, 7 Sep 2025 00:26:02 +0900 Subject: [PATCH 16/23] =?UTF-8?q?[feat]=20#112=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,=20DT?= =?UTF-8?q?O=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/groups/controller/GroupController.java | 13 +++++++++---- .../groups/dto/request/CreateGroupRequestDTO.java | 13 +++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java index 2784ff62..d30db172 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java @@ -4,11 +4,13 @@ import DGU_AI_LAB.admin_be.domain.groups.dto.request.CreateGroupRequestDTO; import DGU_AI_LAB.admin_be.domain.groups.dto.response.GroupResponseDTO; import DGU_AI_LAB.admin_be.domain.groups.service.GroupService; +import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails; import DGU_AI_LAB.admin_be.global.common.SuccessResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -17,7 +19,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/groups") -public class GroupController implements GroupApi { +public class GroupController { private final GroupService groupService; @@ -37,9 +39,12 @@ public ResponseEntity> getGroups() { * POST /api/groups */ @PostMapping - public ResponseEntity> createGroup(@RequestBody @Valid CreateGroupRequestDTO dto) { - log.info("[createGroup] 새로운 그룹 생성 요청 접수: {}", dto.groupName()); - GroupResponseDTO response = groupService.createGroup(dto); + public ResponseEntity> createGroup( + @RequestBody @Valid CreateGroupRequestDTO dto, + @AuthenticationPrincipal CustomUserDetails principal + ) { + log.info("[createGroup] 새로운 그룹 생성 요청 접수: groupName={}, ubuntuUsername={}", dto.groupName(), dto.ubuntuUsername()); + GroupResponseDTO response = groupService.createGroup(dto, principal.getUserId()); return SuccessResponse.created(response); } } diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java index ddb0baa7..03246486 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java @@ -6,15 +6,16 @@ import jakarta.validation.constraints.Positive; import lombok.Builder; -@Schema(description = "1. 그룹 생성 요청 DTO") +@Schema(description = "그룹 생성 요청 DTO") @Builder public record CreateGroupRequestDTO( - @Schema(description = "할당할 우분투 GID (Group ID)", example = "1001") - @NotNull @Positive - Long ubuntuGid, @Schema(description = "생성할 그룹명", example = "developers") - @NotBlank - String groupName + @NotBlank(message = "그룹명은 필수입니다.") + String groupName, + + @Schema(description = "그룹에 추가할 우분투 사용자 이름", example = "user1") + @NotBlank(message = "우분투 사용자 이름은 필수입니다.") + String ubuntuUsername ) { } \ No newline at end of file From 67f9b3e52dd8e361575b5ad73dd92f6597b2aa62 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Sun, 7 Sep 2025 00:26:21 +0900 Subject: [PATCH 17/23] =?UTF-8?q?[feat]=20#112=20WebClientConfig=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/requests/service/AdminRequestCommandService.java | 4 ++-- .../admin_be/domain/users/service/AdminUserService.java | 2 +- .../DGU_AI_LAB/admin_be/global/config/WebClientConfig.java | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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 67abdce8..3347386b 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 @@ -51,8 +51,8 @@ public class AdminRequestCommandService { private final GroupRepository groupRepository; private final ObjectMapper objectMapper; - private final @Qualifier("pvcWebClient") WebClient pvcWebClient; - private final @Qualifier("pvcWebClient") WebClient userCreationWebClient; + private final @Qualifier("configWebClient") WebClient pvcWebClient; + private final @Qualifier("configWebClient") WebClient userCreationWebClient; @Transactional public SaveRequestResponseDTO approveRequest(ApproveRequestDTO dto) { diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/AdminUserService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/AdminUserService.java index ca9c19d7..24942da0 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/AdminUserService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/AdminUserService.java @@ -30,7 +30,7 @@ public class AdminUserService { private final UserRepository userRepository; private final RequestRepository requestRepository; - private final @Qualifier("pvcWebClient") WebClient userWebClient; + private final @Qualifier("configWebClient") WebClient userWebClient; /** * 전체 유저 조회 diff --git a/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java b/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java index 01ef7c4f..18549f3c 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java +++ b/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java @@ -13,8 +13,8 @@ public class WebClientConfig { @Bean - public WebClient pvcWebClient(@Value("${pvc.base-url}") String baseUrl, - @Value("${pvc.timeout-seconds}") int timeout) { + public WebClient configWebClient(@Value("${config.base-url}") String baseUrl, + @Value("${config.timeout-seconds}") int timeout) { /** * 연결 풀 설정: HTTP 연결을 효율적으로 재사용하도록 한다. @@ -32,4 +32,6 @@ public WebClient pvcWebClient(@Value("${pvc.base-url}") String baseUrl, .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); } + + } \ No newline at end of file From c062df94626555f3a09359bfef956d2943e38a56 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Sun, 7 Sep 2025 00:26:35 +0900 Subject: [PATCH 18/23] =?UTF-8?q?[feat]=20#112=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20ErrorCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java index 636f75ec..1faec4a4 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java +++ b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java @@ -67,7 +67,7 @@ public enum ErrorCode { * 502 Bad Gateway */ SLACK_DM_CHANNEL_FAILED(HttpStatus.BAD_GATEWAY, "Slack DM 채널 열기를 실패하였습니다."), - EXTERNAL_API_FAILED(HttpStatus.BAD_GATEWAY, "외부 API 호출에 실패했습니다."), + EXTERNAL_API_ERROR(HttpStatus.BAD_GATEWAY, "외부 API 호출에 실패했습니다."), /** * 503 Service Unavailable @@ -98,7 +98,11 @@ public enum ErrorCode { * Group Error */ NO_AVAILABLE_GROUPS(HttpStatus.NOT_FOUND, "존재하는 그룹 정보가 없습니다."), - DUPLICATE_GROUP_ID(HttpStatus.CONFLICT, "중복된 그룹 ID를 할당할 수 없습니다."), + DUPLICATE_GROUP_ID(HttpStatus.CONFLICT, "외부 API: 그룹명 또는 GID 충돌"), + DUPLICATE_GROUP_NAME(HttpStatus.CONFLICT, "중복된 그룹 이름입니다."), + GROUP_CREATION_FAILED(HttpStatus.BAD_GATEWAY, "외부 API: 필수 필드 누락 또는 형식 오류 "), + GID_ALLOCATION_FAILED(HttpStatus.BAD_GATEWAY, "IdAllocationService에서 GID 할당에 실패했습니다. "), + FORBIDDEN_REQUEST(HttpStatus.BAD_REQUEST, "요청된 우분투 사용자 이름은 로그인한 사용자의 계정이 아닙니다."), /** @@ -121,7 +125,7 @@ public enum ErrorCode { * Request Error */ INVALID_REQUEST_STATUS(HttpStatus.CONFLICT, "이미 처리된 신청입니다."), - FORBIDDEN_REQUEST(HttpStatus.BAD_REQUEST, "본인의 신청만 변경 신청할 수 있습니다."), + //FORBIDDEN_REQUEST(HttpStatus.BAD_REQUEST, "본인의 신청만 변경 신청할 수 있습니다."), UNSUPPORTED_CHANGE_TYPE(HttpStatus.BAD_REQUEST, "지원되지 않는 요청 타입(enum)입니다."), /** From 40e16d67e7325a70a2d94d33dff64c743aa2c6c4 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Mon, 8 Sep 2025 13:36:12 +0900 Subject: [PATCH 19/23] =?UTF-8?q?[fix]=20#112=20=EB=82=B4=EB=B6=80=20DTO?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/groups/service/GroupService.java | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java index e9f6da75..faf832c4 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; @@ -24,6 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; @Slf4j @Service @@ -34,11 +36,9 @@ public class GroupService { private final GroupRepository groupRepository; private final UsedIdRepository usedIdRepository; private final RequestRepository requestRepository; - private final ObjectMapper objectMapper; private final IdAllocationService idAllocationService; private final @Qualifier("configWebClient") WebClient groupCreationWebClient; - /** * 모든 그룹 정보를 조회하는 API * GET /api/groups @@ -70,9 +70,11 @@ public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) { log.info("[createGroup] 그룹 생성 요청 시작: groupName={}, ubuntuUsername={}", dto.groupName(), dto.ubuntuUsername()); - // 1. 요청된 ubuntuUsername이 로그인한 사용자의 계정인지 확인하는 유효성 검사 - if (!requestRepository.existsByUbuntuUsernameAndUser_UserId(dto.ubuntuUsername(), userId)) { - throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST); + // 1. ubuntuUsername이 제공된 경우에만 유효성 검사 (필수 X) + if (StringUtils.hasText(dto.ubuntuUsername())) { + if (!requestRepository.existsByUbuntuUsernameAndUser_UserId(dto.ubuntuUsername(), userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST); + } } // 2. DB에서 그룹명 중복을 먼저 확인합니다. @@ -85,36 +87,30 @@ public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) { Long assignedGid = idAllocationService.allocateNewGid(); log.info("[createGroup] 새로운 GID 할당 완료: {}", assignedGid); - // 4. GID가 int 타입으로 변환 가능한지 확인 (오버플로우 방지) if (assignedGid > Integer.MAX_VALUE || assignedGid < Integer.MIN_VALUE) { log.error("[createGroup] 할당된 GID가 int 범위를 벗어납니다: {}", assignedGid); throw new BusinessException(ErrorCode.GID_ALLOCATION_FAILED); } + // 4. 외부 API 호출을 위한 ubuntuUser 멤버 리스트를 구성합니다. + List members = Optional.ofNullable(dto.ubuntuUsername()) + .filter(StringUtils::hasText) + .map(List::of) + .orElse(Collections.emptyList()); - // 5. 외부 API 호출을 위한 DTO를 준비하고 호출합니다. - Map apiDto = Map.of( - "name", dto.groupName(), - "gid", assignedGid.intValue(), - "members", List.of(dto.ubuntuUsername()) + ConfigServerGroupRequest apiDto = new ConfigServerGroupRequest( + dto.groupName(), + assignedGid.intValue(), + members ); try { - log.info("[createGroup] 외부 그룹 생성 API 호출 시작: name={}, gid={}, members={}", apiDto.get("name"), apiDto.get("gid"), apiDto.get("members")); + log.info("[createGroup] 외부 그룹 생성 API 호출 시작: {}", apiDto); groupCreationWebClient.post() .uri("/accounts/addgroup") .bodyValue(apiDto) .retrieve() - .onStatus(HttpStatus.BAD_REQUEST::equals, clientResponse -> - Mono.error(new BusinessException(ErrorCode.GROUP_CREATION_FAILED)) - ) - .onStatus(HttpStatus.CONFLICT::equals, clientResponse -> - Mono.error(new BusinessException(ErrorCode.DUPLICATE_GROUP_ID)) - ) - .onStatus(HttpStatus.INTERNAL_SERVER_ERROR::equals, clientResponse -> - Mono.error(new BusinessException(ErrorCode.EXTERNAL_API_ERROR)) - ) .bodyToMono(Map.class) .block(); @@ -122,13 +118,30 @@ public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) { } catch (WebClientResponseException e) { log.error("[createGroup] 외부 API 호출 실패: 상태 코드={}, 응답={}", e.getStatusCode(), e.getResponseBodyAsString(), e); + String responseBody = e.getResponseBodyAsString(); + + if (e.getStatusCode() == HttpStatus.BAD_REQUEST) { + if (responseBody.contains("invalid members")) { + throw new BusinessException(ErrorCode.INVALID_GROUP_MEMBER); + } + throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR); + } else if (e.getStatusCode() == HttpStatus.CONFLICT) { + if (responseBody.contains("group already exists")) { + throw new BusinessException(ErrorCode.DUPLICATE_GROUP_NAME); + } + throw new BusinessException(ErrorCode.DUPLICATE_GROUP_ID); + } else if (e.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) { + throw new BusinessException(ErrorCode.EXTERNAL_API_ERROR); + } + throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED); + } catch (Exception e) { log.error("[createGroup] 외부 API 호출 중 예상치 못한 오류 발생", e); throw new BusinessException(ErrorCode.GROUP_CREATION_FAILED); } - // 4. API 호출이 성공한 후에만 로컬 DB에 그룹을 저장합니다. + // 5. API 호출이 성공한 후에만 로컬 DB에 그룹을 저장합니다. UsedId usedId = usedIdRepository.findById(assignedGid) .orElseGet(() -> usedIdRepository.saveAndFlush(UsedId.builder().idValue(assignedGid).build())); @@ -143,4 +156,11 @@ public GroupResponseDTO createGroup(CreateGroupRequestDTO dto, Long userId) { return GroupResponseDTO.fromEntity(group); } + + // Group Service 내부적으로만 사용하는 DTO입니다. + private record ConfigServerGroupRequest( + String name, + int gid, + List members + ) {} } \ No newline at end of file From 2268fbd9e5465c2e9a54c46176ccf0ab4155191d Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Mon, 8 Sep 2025 13:36:20 +0900 Subject: [PATCH 20/23] =?UTF-8?q?[fix]=20#112=20ErrorCode=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java index 1faec4a4..0023d7e9 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java +++ b/src/main/java/DGU_AI_LAB/admin_be/error/ErrorCode.java @@ -103,6 +103,7 @@ public enum ErrorCode { GROUP_CREATION_FAILED(HttpStatus.BAD_GATEWAY, "외부 API: 필수 필드 누락 또는 형식 오류 "), GID_ALLOCATION_FAILED(HttpStatus.BAD_GATEWAY, "IdAllocationService에서 GID 할당에 실패했습니다. "), FORBIDDEN_REQUEST(HttpStatus.BAD_REQUEST, "요청된 우분투 사용자 이름은 로그인한 사용자의 계정이 아닙니다."), + INVALID_GROUP_MEMBER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다."), /** From 1a122d0a5b18cf0dd92871a6d2c08527c921cf78 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Mon, 8 Sep 2025 13:36:50 +0900 Subject: [PATCH 21/23] =?UTF-8?q?[fix]=20#112=20ubuntuUsername=20=ED=95=84?= =?UTF-8?q?=EC=88=98=20=ED=95=84=EB=93=9C=20=EC=95=84=EB=8B=88=EB=AF=80?= =?UTF-8?q?=EB=A1=9C=20annotation=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20swagg?= =?UTF-8?q?er=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/groups/dto/request/CreateGroupRequestDTO.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java index 03246486..74a67d1f 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java @@ -14,8 +14,7 @@ public record CreateGroupRequestDTO( @NotBlank(message = "그룹명은 필수입니다.") String groupName, - @Schema(description = "그룹에 추가할 우분투 사용자 이름", example = "user1") - @NotBlank(message = "우분투 사용자 이름은 필수입니다.") + @Schema(description = "그룹에 추가할 우분투 사용자 이름", example = "user1", required = false) String ubuntuUsername ) { } \ No newline at end of file From 4a33e7c15d3c3c5a7921ca69eaedcf80117281cd Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Mon, 8 Sep 2025 13:37:14 +0900 Subject: [PATCH 22/23] =?UTF-8?q?[refactor]=20#112=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/controller/DashboardController.java | 1 - .../admin_be/domain/groups/controller/GroupController.java | 1 - .../admin_be/domain/groups/controller/docs/GroupApi.java | 4 +++- .../domain/groups/dto/request/CreateGroupRequestDTO.java | 2 -- .../admin_be/domain/groups/service/GroupService.java | 2 -- .../domain/requests/controller/AdminRequestController.java | 4 ---- .../domain/requests/dto/request/RejectModificationDTO.java | 2 +- .../requests/dto/request/UserGroupUpdateRequestDTO.java | 1 + .../domain/requests/service/AdminRequestQueryService.java | 1 + .../domain/requests/service/ConfigRequestService.java | 1 - .../domain/requests/service/RequestQueryService.java | 5 ----- .../admin_be/domain/usedIds/repository/UsedIdRepository.java | 1 - .../admin_be/domain/users/controller/docs/AdminUserApi.java | 4 +++- .../admin_be/domain/users/controller/docs/UserApi.java | 4 +++- .../admin_be/domain/users/service/UserService.java | 4 ---- .../DGU_AI_LAB/admin_be/global/config/WebClientConfig.java | 1 + 16 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/dashboard/controller/DashboardController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/dashboard/controller/DashboardController.java index 6769e431..2de7b603 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/dashboard/controller/DashboardController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/dashboard/controller/DashboardController.java @@ -2,7 +2,6 @@ import DGU_AI_LAB.admin_be.domain.dashboard.controller.docs.DashBoardApi; import DGU_AI_LAB.admin_be.domain.dashboard.service.DashboardService; -import DGU_AI_LAB.admin_be.domain.requests.dto.response.ChangeRequestResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.UserServerResponseDTO; import DGU_AI_LAB.admin_be.domain.requests.entity.Status; import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java index d30db172..00de84bf 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java @@ -1,6 +1,5 @@ package DGU_AI_LAB.admin_be.domain.groups.controller; -import DGU_AI_LAB.admin_be.domain.groups.controller.docs.GroupApi; import DGU_AI_LAB.admin_be.domain.groups.dto.request.CreateGroupRequestDTO; import DGU_AI_LAB.admin_be.domain.groups.dto.response.GroupResponseDTO; import DGU_AI_LAB.admin_be.domain.groups.service.GroupService; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java index 402aac03..ad9a54a3 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java @@ -8,7 +8,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; @Tag(name = "2. 그룹 API", description = "사용자용 그룹 목록 조회, 그룹 생성 API") public interface GroupApi { diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java index 74a67d1f..5820ab52 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/dto/request/CreateGroupRequestDTO.java @@ -2,8 +2,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; import lombok.Builder; @Schema(description = "그룹 생성 요청 DTO") diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java index faf832c4..128651ec 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/service/GroupService.java @@ -10,7 +10,6 @@ import DGU_AI_LAB.admin_be.domain.usedIds.service.IdAllocationService; import DGU_AI_LAB.admin_be.error.ErrorCode; import DGU_AI_LAB.admin_be.error.exception.BusinessException; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -20,7 +19,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java index 33acdc83..8326148f 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/controller/AdminRequestController.java @@ -1,11 +1,8 @@ package DGU_AI_LAB.admin_be.domain.requests.controller; import DGU_AI_LAB.admin_be.domain.requests.controller.docs.AdminRequestApi; -import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveModificationDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.ApproveRequestDTO; -import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectModificationDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.request.RejectRequestDTO; -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.ResourceUsageDTO; import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO; @@ -15,7 +12,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java index f2df82dc..0a9c32a5 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/RejectModificationDTO.java @@ -1,8 +1,8 @@ package DGU_AI_LAB.admin_be.domain.requests.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "관리자용 변경 요청 거절 DTO") public record RejectModificationDTO( diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/UserGroupUpdateRequestDTO.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/UserGroupUpdateRequestDTO.java index eae1fc26..b26acd3b 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/UserGroupUpdateRequestDTO.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/UserGroupUpdateRequestDTO.java @@ -1,6 +1,7 @@ package DGU_AI_LAB.admin_be.domain.requests.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public record UserGroupUpdateRequestDTO( 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 6f04f00f..e72c26d6 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 @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java index 344095e7..9858b908 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java @@ -10,7 +10,6 @@ import DGU_AI_LAB.admin_be.error.exception.BusinessException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; 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 b43f7302..cb72989d 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,18 +1,13 @@ package DGU_AI_LAB.admin_be.domain.requests.service; -import DGU_AI_LAB.admin_be.domain.containerImage.repository.ContainerImageRepository; -import DGU_AI_LAB.admin_be.domain.groups.repository.GroupRepository; import DGU_AI_LAB.admin_be.domain.requests.dto.response.ContainerInfoDTO; 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.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.resourceGroups.repository.ResourceGroupRepository; import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; import DGU_AI_LAB.admin_be.error.ErrorCode; import DGU_AI_LAB.admin_be.error.exception.BusinessException; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java index c99b4ccd..1d5241be 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/usedIds/repository/UsedIdRepository.java @@ -3,7 +3,6 @@ import DGU_AI_LAB.admin_be.domain.usedIds.entity.UsedId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import java.util.Optional; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/AdminUserApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/AdminUserApi.java index bfa20296..3410512f 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/AdminUserApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/AdminUserApi.java @@ -7,7 +7,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; @Tag(name = "1. 관리자 유저 관리", description = "관리자용 사용자 계정 관리 API") public interface AdminUserApi { diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/UserApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/UserApi.java index b5bf75b2..fa142dc1 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/UserApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/controller/docs/UserApi.java @@ -12,7 +12,9 @@ import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; @Tag(name = "2. 사용자 정보 관리", description = "일반 사용자 본인 정보 관리 API") public interface UserApi { diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserService.java b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserService.java index bff40a2f..80cd22bb 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserService.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/users/service/UserService.java @@ -6,11 +6,9 @@ import DGU_AI_LAB.admin_be.domain.users.dto.request.PasswordUpdateRequestDTO; import DGU_AI_LAB.admin_be.domain.users.dto.request.PhoneUpdateRequestDTO; import DGU_AI_LAB.admin_be.domain.users.dto.request.UserAuthRequestDTO; -import DGU_AI_LAB.admin_be.domain.users.dto.request.UserUpdateRequestDTO; import DGU_AI_LAB.admin_be.domain.users.dto.response.MyInfoResponseDTO; import DGU_AI_LAB.admin_be.domain.users.dto.response.UserAuthResponseDTO; import DGU_AI_LAB.admin_be.domain.users.dto.response.UserResponseDTO; -import DGU_AI_LAB.admin_be.domain.users.dto.response.UserSummaryDTO; import DGU_AI_LAB.admin_be.domain.users.entity.User; import DGU_AI_LAB.admin_be.domain.users.repository.UserRepository; import DGU_AI_LAB.admin_be.error.ErrorCode; @@ -24,8 +22,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java b/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java index 18549f3c..b3bdcef9 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java +++ b/src/main/java/DGU_AI_LAB/admin_be/global/config/WebClientConfig.java @@ -7,6 +7,7 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; + import java.time.Duration; @Configuration From a94a99200da611b0f4e85a6fb65996ff1c7fc325 Mon Sep 17 00:00:00 2001 From: saokiritoni Date: Mon, 8 Sep 2025 13:42:24 +0900 Subject: [PATCH 23/23] =?UTF-8?q?[docs]=20#112=20swagger=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../groups/controller/GroupController.java | 3 ++- .../groups/controller/docs/GroupApi.java | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java index 00de84bf..6f6687e3 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/GroupController.java @@ -1,5 +1,6 @@ package DGU_AI_LAB.admin_be.domain.groups.controller; +import DGU_AI_LAB.admin_be.domain.groups.controller.docs.GroupApi; import DGU_AI_LAB.admin_be.domain.groups.dto.request.CreateGroupRequestDTO; import DGU_AI_LAB.admin_be.domain.groups.dto.response.GroupResponseDTO; import DGU_AI_LAB.admin_be.domain.groups.service.GroupService; @@ -18,7 +19,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/groups") -public class GroupController { +public class GroupController implements GroupApi { private final GroupService groupService; diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java index ad9a54a3..19a14974 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/groups/controller/docs/GroupApi.java @@ -1,18 +1,21 @@ package DGU_AI_LAB.admin_be.domain.groups.controller.docs; import DGU_AI_LAB.admin_be.domain.groups.dto.request.CreateGroupRequestDTO; +import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails; import DGU_AI_LAB.admin_be.global.common.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@Tag(name = "2. 그룹 API", description = "사용자용 그룹 목록 조회, 그룹 생성 API") +@Tag(name = "2. 사용자 그룹 API", description = "사용자용 우분투 그룹 관련 API") public interface GroupApi { @Operation(summary = "모든 그룹 목록 조회", description = "시스템에 등록된 모든 그룹의 정보를 조회합니다. 사용 신청 단계에서 이 목록을 조회하고, 그룹을 선택할 수 있습니다.") @@ -23,12 +26,18 @@ public interface GroupApi { @GetMapping ResponseEntity> getGroups(); - @Operation(summary = "새로운 그룹 생성", description = "새로운 그룹을 생성하고 DB에 저장합니다.") + @Operation(summary = "새로운 그룹 생성", description = "새로운 그룹을 생성하고 MySQL DB에 저장합니다. 이후 Config Server에 그룹 생성 API를 호출합니다. 그룹명과 사용자 이름은 필수값이지만, 사용자 이름은 생략 가능하며 이 경우 멤버 없는 그룹이 생성됩니다. " + + "사용 신청을 아직 생성하지 않았지만, 그룹을 먼저 생성해야 하는 경우를 고려했습니다.") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "생성 성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청 (필수 필드 누락 등)"), - @ApiResponse(responseCode = "409", description = "중복된 GID를 가진 그룹이 이미 존재함") + @ApiResponse(responseCode = "400", description = "잘못된 요청: 필수 필드 누락(groupName) 또는 잘못된 형식"), + @ApiResponse(responseCode = "403", description = "접근 금지: 요청한 ubuntuUsername이 로그인한 사용자와 일치하지 않음"), + @ApiResponse(responseCode = "409", description = "데이터 충돌: 동일한 그룹명이 이미 존재함"), + @ApiResponse(responseCode = "500", description = "서버 오류: GID 할당 실패 또는 외부 API 호출 실패") }) @PostMapping - ResponseEntity> createGroup(@RequestBody @Valid CreateGroupRequestDTO dto); + ResponseEntity> createGroup( + @RequestBody @Valid CreateGroupRequestDTO dto, + @AuthenticationPrincipal @Parameter(hidden = true) CustomUserDetails principal + ); } \ No newline at end of file