Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package DGU_AI_LAB.admin_be.domain.requests.controller;

import DGU_AI_LAB.admin_be.domain.requests.controller.docs.RequestApi;
import DGU_AI_LAB.admin_be.domain.requests.dto.request.ModifyRequestDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.request.SingleChangeRequestDTO;
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removed import for ModifyRequestDTO suggests this class may no longer be used. Verify that ModifyRequestDTO can be safely removed from the codebase to avoid dead code.

Copilot uses AI. Check for mistakes.
import DGU_AI_LAB.admin_be.domain.requests.dto.request.SaveRequestRequestDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO;
import DGU_AI_LAB.admin_be.domain.requests.service.RequestCommandService;
Expand Down Expand Up @@ -36,14 +36,14 @@ public ResponseEntity<SuccessResponse<?>> createRequest(@AuthenticationPrincipal
}

/**
* ์‚ฌ์šฉ ์‹ ์ฒญ ๋ณ€๊ฒฝ (์ €์žฅ๊ณต๊ฐ„ ํฌ๊ธฐ, ๋งŒ๋ฃŒ๊ธฐํ•œ, ์‚ฌ์šฉ์ž๊ฐ€ ์†ํ•œ ๊ทธ๋ฃน, ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน, ๋„์ปค ์ด๋ฏธ์ง€)
* ์‚ฌ์šฉ ์‹ ์ฒญ ๋ณ€๊ฒฝ (๋‹จ์ผ ๋ณ€๊ฒฝ ์š”์ฒญ)
*/
@PostMapping("/{requestId}/change")
public ResponseEntity<SuccessResponse<?>> createChangeRequest(@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable Long requestId,
@RequestBody @Valid ModifyRequestDTO dto
@RequestBody @Valid SingleChangeRequestDTO dto
) {
requestCommandService.createModificationRequest(userId, requestId, dto);
requestCommandService.createSingleChangeRequest(userId, requestId, dto);
return SuccessResponse.ok(null);
}

Expand All @@ -57,6 +57,15 @@ public ResponseEntity<SuccessResponse<?>> getMyRequests(@AuthenticationPrincipal
return SuccessResponse.ok(body);
}

/**
* ๋‚˜์˜ ์Šน์ธ ์™„๋ฃŒ๋œ ์‚ฌ์šฉ ์‹ ์ฒญ ์กฐํšŒ
*/
@GetMapping("/my/approved")
public ResponseEntity<SuccessResponse<?>> getMyApprovedRequests(@AuthenticationPrincipal CustomUserDetails user) {
List<SaveRequestResponseDTO> body = requestQueryService.getApprovedRequestsByUserId(user.getUserId());
return SuccessResponse.ok(body);
}

/**
* ๋‚˜์˜ ์‚ฌ์šฉ ์‹ ์ฒญ์— ๋Œ€ํ•œ ๋ชจ๋“  ubuntu_username ์กฐํšŒ
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ ResponseEntity<SuccessResponse<?>> getMyRequests(
@Parameter(hidden = true, description = "์ธ์ฆ๋œ ์‚ฌ์šฉ์ž")
CustomUserDetails user
);

@Operation(
summary = "๋‚ด ์Šน์ธ ์™„๋ฃŒ๋œ ์‹ ์ฒญ ๋ชฉ๋ก ์กฐํšŒ",
description = "๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž์˜ ์Šน์ธ ์™„๋ฃŒ(FULFILLED) ์ƒํƒœ์ธ ์‹ ์ฒญ ๋‚ด์—ญ๋งŒ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค."
)
@ApiResponse(
responseCode = "200",
description = "์กฐํšŒ ์„ฑ๊ณต",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SaveRequestResponseDTO.class)))
)
ResponseEntity<SuccessResponse<?>> getMyApprovedRequests(
@Parameter(hidden = true, description = "์ธ์ฆ๋œ ์‚ฌ์šฉ์ž")
CustomUserDetails user
);

@Operation(
summary = "์Šน์ธ ์™„๋ฃŒ๋œ ๋ชจ๋“  Ubuntu ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์กฐํšŒ",
description = "[๊ทธ๋ฃน ์ƒ์„ฑ ์‹œ ์‚ฌ์šฉ] ํ˜„์žฌ ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉ ์Šน์ธ(FULFILLED)์ด ์™„๋ฃŒ๋œ ๋ชจ๋“  ์š”์ฒญ์˜ Ubuntu ์‚ฌ์šฉ์ž ์ด๋ฆ„ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package DGU_AI_LAB.admin_be.domain.requests.dto.request;

import DGU_AI_LAB.admin_be.domain.requests.entity.ChangeRequest;
import DGU_AI_LAB.admin_be.domain.requests.entity.ChangeType;
import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
import DGU_AI_LAB.admin_be.domain.users.entity.User;
import DGU_AI_LAB.admin_be.error.ErrorCode;
import DGU_AI_LAB.admin_be.error.exception.BusinessException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import DGU_AI_LAB.admin_be.domain.requests.dto.response.PortMappingDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.request.PortRequestDTO;
import DGU_AI_LAB.admin_be.domain.portRequests.service.PortRequestService;

@Slf4j
@Schema(description = "๋‹จ์ผ ๋ณ€๊ฒฝ ์š”์ฒญ DTO")
public record SingleChangeRequestDTO(

@Schema(description = "๋ณ€๊ฒฝ ํƒ€์ž…", example = "VOLUME_SIZE")
@NotNull(message = "๋ณ€๊ฒฝ ํƒ€์ž…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
ChangeType changeType,

@Schema(description = "์ƒˆ๋กœ์šด ๊ฐ’", example = "100")
@NotBlank(message = "์ƒˆ๋กœ์šด ๊ฐ’์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
String newValue,

@Schema(description = "๋ณ€๊ฒฝ ์š”์ฒญ ์‚ฌ์œ ", example = "ํ”„๋กœ์ ํŠธ ์š”๊ตฌ์‚ฌํ•ญ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ์šฉ๋Ÿ‰ ์ฆ์„ค")
@NotBlank(message = "๋ณ€๊ฒฝ ์‚ฌ์œ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
String reason
) {

/**
* ๊ธฐ์กด Request์—์„œ oldValue๋ฅผ ์ถ”์ถœํ•˜๊ณ  ChangeRequest ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ
*/
public static ChangeRequest toEntity(SingleChangeRequestDTO dto, Request originalRequest, User requestedBy, ObjectMapper objectMapper, PortRequestService portRequestService) {
try {
String oldValue = extractOldValue(originalRequest, dto.changeType(), objectMapper, portRequestService);

return ChangeRequest.builder()
.request(originalRequest)
.changeType(dto.changeType())
.oldValue(oldValue)
.newValue(dto.newValue())
.reason(dto.reason())
.requestedBy(requestedBy)
.build();
} catch (Exception e) {
log.error("Failed to create ChangeRequest entity: {}", e.getMessage());
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

/**
* ๋ณ€๊ฒฝ ํƒ€์ž…์— ๋”ฐ๋ผ ๊ธฐ์กด ๊ฐ’์„ ์ถ”์ถœ
*/
private static String extractOldValue(Request originalRequest, ChangeType changeType, ObjectMapper objectMapper, PortRequestService portRequestService) {
try {
return switch (changeType) {
case VOLUME_SIZE -> objectMapper.writeValueAsString(originalRequest.getVolumeSizeGiB());
case EXPIRES_AT -> objectMapper.writeValueAsString(originalRequest.getExpiresAt());
case GROUP -> {
Set<Long> oldGroupIds = originalRequest.getRequestGroups().stream()
.map(requestGroup -> requestGroup.getGroup().getUbuntuGid())
.collect(Collectors.toSet());
yield objectMapper.writeValueAsString(oldGroupIds);
}
case RESOURCE_GROUP -> objectMapper.writeValueAsString(originalRequest.getResourceGroup().getRsgroupId());
case CONTAINER_IMAGE -> objectMapper.writeValueAsString(originalRequest.getContainerImage().getImageId());
case PORT -> {
// Get existing port requests for this request
List<PortMappingDTO> existingPorts = originalRequest.getRequestId() != null
? portRequestService.getPortRequestsByRequestId(originalRequest.getRequestId())
.stream()
.map(PortMappingDTO::fromEntity)
.collect(Collectors.toList())
: List.of();
yield objectMapper.writeValueAsString(existingPorts);
}
};
} catch (Exception e) {
log.error("Failed to extract old value for change type {}: {}", changeType, e.getMessage());
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

/**
* DTO ๋‚ด๋ถ€์—์„œ ์ž์ฒด์ ์œผ๋กœ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กด์žฌ ์—ฌ๋ถ€๊นŒ์ง€ ํ™•์ธํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ
*/
public static ChangeRequest createValidatedChangeRequest(SingleChangeRequestDTO dto, Request originalRequest, User requestedBy, ObjectMapper objectMapper, PortRequestService portRequestService) {
dto.validateAndCheckExistence(originalRequest);
return toEntity(dto, originalRequest, requestedBy, objectMapper, portRequestService);
}

/**
* ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
*/
private void validateAndCheckExistence(Request originalRequest) {
validateBasicFormat();
validateValueFormat();
// ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กด์žฌ ์—ฌ๋ถ€ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
}

/**
* ๊ธฐ๋ณธ ํ˜•์‹ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
*/
private void validateBasicFormat() {
if (changeType == null) {
throw new BusinessException("๋ณ€๊ฒฝ ํƒ€์ž…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}

if (newValue == null || newValue.trim().isEmpty()) {
throw new BusinessException("์ƒˆ๋กœ์šด ๊ฐ’์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}

if (reason == null || reason.trim().isEmpty()) {
throw new BusinessException("๋ณ€๊ฒฝ ์‚ฌ์œ ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}

/**
* ๋ณ€๊ฒฝ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๊ฐ’ ํ˜•์‹ ๊ฒ€์ฆ
*/
private void validateValueFormat() {
try {
switch (changeType) {
case VOLUME_SIZE -> {
Long volumeSize = Long.parseLong(newValue.trim());
if (volumeSize <= 0) {
throw new BusinessException("๋ณผ๋ฅจ ํฌ๊ธฐ๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}
case EXPIRES_AT -> {
LocalDateTime.parse(newValue.trim());
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using LocalDateTime.parse() without specifying a DateTimeFormatter may cause parsing failures if the input format doesn't match ISO-8601. Consider using a specific formatter or documenting the expected format.

Copilot uses AI. Check for mistakes.
}
case GROUP -> {
ObjectMapper mapper = new ObjectMapper();
Set<Long> groupIds = mapper.readValue(newValue, mapper.getTypeFactory().constructCollectionType(Set.class, Long.class));
if (groupIds.isEmpty()) {
throw new BusinessException("๊ทธ๋ฃน ID ๋ชฉ๋ก์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}
case RESOURCE_GROUP -> {
Integer resourceGroupId = Integer.parseInt(newValue.trim());
if (resourceGroupId <= 0) {
throw new BusinessException("๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ID๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}
case CONTAINER_IMAGE -> {
Long imageId = Long.parseLong(newValue.trim());
if (imageId <= 0) {
throw new BusinessException("์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€ ID๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}
case PORT -> {
ObjectMapper mapper = new ObjectMapper();
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating new ObjectMapper instances in validation methods is inefficient. Consider injecting the existing ObjectMapper instance or creating a static final instance to reuse.

Copilot uses AI. Check for mistakes.
List<PortRequestDTO> portRequests = mapper.readValue(newValue,
mapper.getTypeFactory().constructCollectionType(List.class, PortRequestDTO.class));

// Validate each port request
for (PortRequestDTO portRequest : portRequests) {
if (portRequest.internalPort() == null || portRequest.internalPort() < 1 || portRequest.internalPort() > 65535) {
throw new BusinessException("๋‚ด๋ถ€ ํฌํŠธ๋Š” 1-65535 ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
if (portRequest.usagePurpose() == null || portRequest.usagePurpose().trim().isEmpty()) {
throw new BusinessException("ํฌํŠธ ์‚ฌ์šฉ ๋ชฉ์ ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", ErrorCode.INVALID_INPUT_VALUE);
}
}
}
}
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException("์ƒˆ๋กœ์šด ๊ฐ’์˜ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: " + e.getMessage(), ErrorCode.INVALID_INPUT_VALUE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
public enum ChangeType {
VOLUME_SIZE, // ๋ณผ๋ฅจ ์‚ฌ์ด์ฆˆ ๋ณ€๊ฒฝ
EXPIRES_AT, // ์„œ๋ฒ„ ์‚ฌ์šฉ ๋งŒ๋ฃŒ ๊ธฐํ•œ ๋ณ€๊ฒฝ
GROUP, // ์‚ฌ์šฉ์ž๊ฐ€ ์†ํ•œ ๊ทธ๋ฃน ๋ณ€๊ฒฝ
RESOURCE_GROUP, // ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ๋ณ€๊ฒฝ
CONTAINER_IMAGE // ๋„์ปค ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ
CONTAINER_IMAGE, // ๋„์ปค ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ
GROUP, // ์‚ฌ์šฉ์ž๊ฐ€ ์†ํ•œ ๊ทธ๋ฃน ๋ณ€๊ฒฝ
PORT,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.ModifyRequestDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.request.SingleChangeRequestDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.request.SaveRequestRequestDTO;
import DGU_AI_LAB.admin_be.domain.requests.dto.response.SaveRequestResponseDTO;
import DGU_AI_LAB.admin_be.domain.requests.entity.ChangeRequest;
Expand Down Expand Up @@ -115,6 +116,32 @@ public void createModificationRequest(Long userId, Long requestId, ModifyRequest
}
}

/**
* ๋‹จ์ผ ๋ณ€๊ฒฝ ์š”์ฒญ ์ƒ์„ฑ - DTO๊ฐ€ ๋ชจ๋“  ๊ฒ€์ฆ์„ ๋‹ด๋‹นํ•˜๋ฏ€๋กœ ์„œ๋น„์Šค๋Š” ๋‹จ์ˆœํžˆ ์ฒ˜๋ฆฌ๋งŒ ํ•จ
*/
@Transactional
public void createSingleChangeRequest(Long userId, Long requestId, SingleChangeRequestDTO dto) {
Request originalRequest = requestRepository.findById(requestId)
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND));

// ์š”์ฒญ์ž๊ฐ€ ์›๋ณธ ์š”์ฒญ์˜ ์†Œ์œ ์ž์ธ์ง€ ํ™•์ธ
if (!originalRequest.getUser().getUserId().equals(userId)) {
throw new BusinessException(ErrorCode.FORBIDDEN_REQUEST);
}

// FULFILLED ์ƒํƒœ์—์„œ๋งŒ ๋ณ€๊ฒฝ ์š”์ฒญ ๊ฐ€๋Šฅ
if (originalRequest.getStatus() != Status.FULFILLED) {
throw new BusinessException(ErrorCode.INVALID_REQUEST_STATUS);
}

User requestedBy = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));

// DTO ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์ฆ๋œ ChangeRequest ์ƒ์„ฑ ๋ฐ ์ €์žฅ
ChangeRequest changeRequest = SingleChangeRequestDTO.createValidatedChangeRequest(
dto, originalRequest, requestedBy, objectMapper, portRequestService);
changeRequestRepository.save(changeRequest);
}

// ์ค‘๋ณต ์ฝ”๋“œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ํ—ฌํผ ๋ฉ”์„œ๋“œ
private <T> void createAndSaveChangeRequest(Request originalRequest, User requestedBy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,14 @@ public List<String> getAllFulfilledUsernames() {
return requestRepository.findUbuntuUsernamesByStatus(Status.FULFILLED);
}

/** ๋‚ด ์‹ ์ฒญ ๋ชฉ๋ก ์ค‘ ์Šน์ธ ์™„๋ฃŒ(FULFILLED) ์ƒํƒœ๋งŒ ์กฐํšŒ */
public List<SaveRequestResponseDTO> getApprovedRequestsByUserId(Long userId) {
if (!userRepository.existsById(userId)) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return requestRepository.findAllByUser_UserIdAndStatus(userId, Status.FULFILLED).stream()
.map(this::createResponseDTOWithPortMappings)
.toList();
}

}