Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fdf5a3d
[feat] #71 필요한 비즈니스 메서드 추가
saokiritoni Aug 12, 2025
2404ef5
[feat] #71 비밀번호/전화번호 변경 DTO 추가
saokiritoni Aug 12, 2025
14b2854
[feat] #71 비밀번호/전화번호 변경 서비스 개발
saokiritoni Aug 12, 2025
9764cf1
[feat] #71 비밀번호/전화번호 변경 API 추가
saokiritoni Aug 12, 2025
a7f5147
[feat] #71 에러 코드 정리
saokiritoni Aug 12, 2025
deaa8fd
[fix] #71 GPU id generation 지정
saokiritoni Aug 12, 2025
e139b98
[feat] #71 신청한 서버 정보 반환 DTO 생성
saokiritoni Aug 12, 2025
25db74a
[feat] #71 GPU 스펙별 반환 DTO 생성
saokiritoni Aug 12, 2025
29f728c
[feat] #71 리소스 그룹별 노드 조회 메서드 추가
saokiritoni Aug 12, 2025
07acd21
[feat] #71 리소스 그룹별 노드 조회 메서드 추가
saokiritoni Aug 12, 2025
ce3b108
[feat] #71 대시보드 신청서별 서버 조회 API
saokiritoni Aug 12, 2025
be820fe
[feat] #71 대시보드 신청서별 서버 조회 서비스
saokiritoni Aug 12, 2025
d6ad9ac
[feat] #71 GPU 조회 메서드 추가
saokiritoni Aug 12, 2025
605f3b7
[feat] #71 GPU 스펙별 리소스 정보 조회 API
saokiritoni Aug 12, 2025
e1c2f08
[feat] #71 GPU 스펙별 리소스 정보 조회용 DTO
saokiritoni Aug 12, 2025
359ea38
[feat] #71 GPU 스펙별 리소스 정보 조회 서비스
saokiritoni Aug 12, 2025
51e6219
Merge pull request #74 from CSID-DGU/feat/#73-DashboardResource
saokiritoni Aug 12, 2025
3c5c17f
[refactor] #71 import문 최적화
saokiritoni Aug 12, 2025
275c92c
[refactor] #71 fromQueryResult로 서비스 코드 간소화
saokiritoni Aug 12, 2025
de7b869
[refactor] #71 예외 처리
saokiritoni Aug 12, 2025
18f4182
[docs] #71 리소스 API 문서 작성
saokiritoni Aug 12, 2025
a636db3
[docs] #71 대시보드 API 문서 작성
saokiritoni Aug 12, 2025
e26ad2d
[docs] #71 코드오너 경로 수정 및 팀 멘션으로 수정
saokiritoni Aug 12, 2025
e925975
Merge pull request #75 from CSID-DGU/feat/#73-DashboardResource
saokiritoni Aug 13, 2025
574c094
Merge branch 'develop' into feat/#71-UserDetails
saokiritoni Aug 13, 2025
a62d4e5
Merge pull request #72 from CSID-DGU/feat/#71-UserDetails
saokiritoni Aug 13, 2025
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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# BE 리뷰어 지정
* @CSID-DGU/gpu_server_team
2 changes: 0 additions & 2 deletions .github/workflows/CODEOWNERS

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package DGU_AI_LAB.admin_be.domain.dashboard.controller;

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.UserServerResponseDTO;
import DGU_AI_LAB.admin_be.domain.requests.entity.Status;
import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails;
import DGU_AI_LAB.admin_be.global.common.SuccessResponse;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/dashboard")
public class DashboardController implements DashBoardApi {

private final DashboardService dashboardService;


/**
* 사용자 신청 현황 서버 목록 조회 API (대시보드 전용)
* GET /api/dashboard/me/servers
*
* @param principal 현재 로그인한 사용자의 인증 정보 (CustomUserDetails)
* @param status 조회할 서버 요청의 상태 (필수 값: PENDING, FULFILLED, DENIED 등)
* 사용자의 승인받은 서버 목록 또는 승인 대기중인 신청 목록을 필터링하여 반환합니다.
*/
@GetMapping("/me/servers")
public ResponseEntity<SuccessResponse<?>> getUserServers(
@AuthenticationPrincipal CustomUserDetails principal,
@RequestParam(name = "status") Status status
) {
List<UserServerResponseDTO> userServers = dashboardService.getUserServers(principal.getUserId(), status);
return SuccessResponse.ok(userServers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package DGU_AI_LAB.admin_be.domain.dashboard.controller.docs;

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.error.dto.ErrorResponse;
import DGU_AI_LAB.admin_be.global.common.SuccessResponse;
import DGU_AI_LAB.admin_be.global.auth.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.tags.Tag;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;


@Tag(name = "사용자 대시보드 API", description = "대시보드용 API")
@RequestMapping("/api/dashboard")
public interface DashBoardApi {

@Operation(
summary = "사용자 서버 신청 목록 조회",
description = "현재 로그인한 사용자의 서버 신청 목록을 상태별로 필터링하여 조회합니다.<br>" +
"JWT를 통해 사용자 ID를 식별합니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "성공적으로 서버 신청 목록을 조회했습니다.",
content = @Content(schema = @Schema(implementation = UserServerResponseDTO.class))
),
@ApiResponse(
responseCode = "401",
description = "인증되지 않은 사용자입니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "404",
description = "사용자를 찾을 수 없거나 해당 사용자의 신청 내역이 없습니다. (USER_NOT_FOUND)",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류 발생.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
}
)
@GetMapping("/me/servers")
ResponseEntity<SuccessResponse<?>> getUserServers(
@Parameter(hidden = true)
@AuthenticationPrincipal CustomUserDetails principal,
@Parameter(description = "조회할 서버 신청 상태 (PENDING, FULFILLED, DENIED 등)", required = true, example = "FULFILLED")
@RequestParam(name = "status") Status status
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package DGU_AI_LAB.admin_be.domain.dashboard.service;

import DGU_AI_LAB.admin_be.domain.nodes.entity.Node;
import DGU_AI_LAB.admin_be.domain.nodes.repository.NodeRepository;
import DGU_AI_LAB.admin_be.domain.requests.dto.response.UserServerResponseDTO;
import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
import DGU_AI_LAB.admin_be.domain.requests.entity.Status;
import DGU_AI_LAB.admin_be.domain.requests.repository.RequestRepository;
import DGU_AI_LAB.admin_be.domain.resourceGroups.entity.ResourceGroup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DashboardService {

private final RequestRepository requestRepository;
private final NodeRepository nodeRepository;

/**
* 사용자 대시보드 서버 목록 조회
* 승인받은 서버 및 승인 대기중인 신청 목록을 필터링하여 반환합니다.
*/
public List<UserServerResponseDTO> getUserServers(Long userId, Status status) {
log.info("[getUserServers] userId={}의 status={} 서버 목록 조회 시작", userId, status);

List<Request> requests = requestRepository.findByUserUserIdAndStatus(userId, status);

return requests.stream()
.map(request -> {
String serverAddress = (request.getStatus() == Status.FULFILLED) ? "TBD (서버 할당 후 표시)" : null;

Integer cpuCoreCount = null;
Integer memoryGB = null;
String resourceGroupName = null;

ResourceGroup resourceGroup = request.getResourceGroup();
if (resourceGroup != null) {
resourceGroupName = resourceGroup.getDescription();

List<Node> nodesInGroup = nodeRepository.findByRsgroupId(resourceGroup.getRsgroupId());
if (!nodesInGroup.isEmpty()) {
Node representativeNode = nodesInGroup.get(0);
cpuCoreCount = representativeNode.getCpuCoreCount();
memoryGB = representativeNode.getMemorySizeGB();
}
}

return UserServerResponseDTO.fromEntity(
request,
serverAddress,
cpuCoreCount,
memoryGB,
resourceGroupName
);
})
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package DGU_AI_LAB.admin_be.domain.gpus.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
@Schema(description = "GPU 기종별 리소스 정보 응답 DTO")
public record GpuTypeResponseDTO(

@Schema(description = "GPU 모델명", example = "RTX 3090 D6")
String gpuModel,

@Schema(description = "GPU RAM 크기 (GB)", example = "24")
Integer ramGb,

@Schema(description = "GPU가 속한 리소스 그룹명 (같은 스펙 묶음)", example = "RTX 3090")
String resourceGroupName,

@Schema(description = "사용 가능한 노드(서버) 개수", example = "5")
Long availableNodes,

@Schema(description = "현재 사용 가능 여부 (true: 사용 가능, false: 사용 불가능)", example = "true")
Boolean isAvailable // TODO: 현재는 항상 true로 가정, 이후에 논의 후 수정 필요
) {
// Object[] 형태의 쿼리 결과를 DTO로 변환하는 팩토리 메서드
public static GpuTypeResponseDTO fromQueryResult(Object[] queryResult) {
if (queryResult == null || queryResult.length < 4) {
throw new IllegalArgumentException("Invalid query result format for GpuTypeResponseDTO");
}
String gpuModel = (String) queryResult[0];
Integer ramGb = (Integer) queryResult[1];
String resourceGroupName = (String) queryResult[2];
Long availableNodes = ((Number) queryResult[3]).longValue();

return GpuTypeResponseDTO.builder()
.gpuModel(gpuModel)
.ramGb(ramGb)
.resourceGroupName(resourceGroupName)
.availableNodes(availableNodes)
.isAvailable(true) // 현재는 항상 true로 가정
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public class Gpu {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "gpu_id", nullable = false)
private Long gpuId;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package DGU_AI_LAB.admin_be.domain.gpus.repository;

import DGU_AI_LAB.admin_be.domain.gpus.entity.Gpu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface GpuRepository extends JpaRepository<Gpu, Long> {

@Query("SELECT g.gpuModel, g.ramGb, rg.description, COUNT(DISTINCT n.nodeId) " +
"FROM Gpu g JOIN g.node n JOIN ResourceGroup rg ON n.rsgroupId = rg.rsgroupId " +
"GROUP BY g.gpuModel, g.ramGb, rg.description")
List<Object[]> findGpuSummary();
@Query("SELECT DISTINCT n.cpuCoreCount, n.memorySizeGB " +
"FROM Gpu g JOIN g.node n " +
"WHERE g.gpuModel = :gpuModel")
List<Object[]> findNodeSpecsByGpuModel(String gpuModel);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface NodeRepository extends JpaRepository<Node, String> {
List<Node> findByRsgroupId(Integer rsgroupId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package DGU_AI_LAB.admin_be.domain.requests.dto.response;

import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
import DGU_AI_LAB.admin_be.domain.requests.entity.Status;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

import java.time.LocalDateTime;

@Builder
@Schema(description = "사용자 대시보드 서버 목록 응답 DTO")
public record UserServerResponseDTO(
@Schema(description = "서버 신청 고유 ID", example = "1")
Long requestId,
@Schema(description = "할당된 서버 주소 (승인 후 유효)", example = "192.168.1.100", nullable = true)
String serverAddress,
@Schema(description = "서버 사용 만료일", example = "2025-12-31T23:59:59")
LocalDateTime expiresAt,
@Schema(description = "할당된 볼륨 크기 (GB)", example = "100")
Long volumeSizeGB,
@Schema(description = "사용된 CUDA 버전", example = "12.0")
String cudaVersion,
@Schema(description = "할당된 CPU 코어 수", example = "8")
Integer cpuCoreCount,
@Schema(description = "할당된 메모리 크기 (GB)", example = "32")
Integer memoryGB,
@Schema(description = "할당된 리소스 그룹명 (GPU 스펙 묶음)", example = "RTX 3090 D6 24GB 그룹")
String resourceGroupName,
@Schema(description = "서버 신청 상태", example = "FULFILLED", allowableValues = {"PENDING", "FULFILLED", "DENIED", "MODIFICATION_REQUESTED", "MODIFICATION_APPROVED", "MODIFICATION_REJECTED"})
Status status
) {
public static UserServerResponseDTO fromEntity(Request request, String serverAddress, Integer cpuCoreCount, Integer memoryGB, String resourceGroupName) {
return UserServerResponseDTO.builder()
.requestId(request.getRequestId())
.serverAddress(serverAddress)
.expiresAt(request.getExpiresAt())
.volumeSizeGB(request.getVolumeSizeByte() / (1024L * 1024 * 1024))
.cudaVersion(request.getCudaVersion())
.cpuCoreCount(cpuCoreCount)
.memoryGB(memoryGB)
.resourceGroupName(resourceGroupName)
.status(request.getStatus())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ public interface RequestRepository extends JpaRepository<Request, Long> {
List<Request> findAllByUser(User user);
List<Request> findAllByUser_UserId(Long userId);
List<Request> findAllByStatus(Status status);

List<Request> findByUserUserIdAndStatus(Long userId, Status status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package DGU_AI_LAB.admin_be.domain.resourceGroups.controller;

import DGU_AI_LAB.admin_be.domain.gpus.dto.response.GpuTypeResponseDTO;
import DGU_AI_LAB.admin_be.domain.resourceGroups.controller.docs.ResourceGroupApi;
import DGU_AI_LAB.admin_be.domain.resourceGroups.service.ResourceGroupService;
import DGU_AI_LAB.admin_be.global.common.SuccessResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/resources")
public class ResourceGroupController implements ResourceGroupApi {

private final ResourceGroupService resourceGroupService;

/**
* GPU 기종별(리소스그룹) 리소스 정보 조회 API
* GET /api/dashboard/gpu-types
*
* 우선 모든 노드는 현재 사용 가능하다고 가정합니다.
*/
@GetMapping("/gpu-types")
public ResponseEntity<SuccessResponse<?>> getGpuTypeResources() {
List<GpuTypeResponseDTO> gpuTypes = resourceGroupService.getGpuTypeResources();
return SuccessResponse.ok(gpuTypes);
}

}
Loading