From 250e01a0c1252ac216d215d81c0c5abbdfd34c7f Mon Sep 17 00:00:00 2001 From: bjkim Date: Wed, 24 Sep 2025 12:34:40 +0900 Subject: [PATCH] =?UTF-8?q?[ADD]=20Kubeflow=20Run=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=20=EB=B0=8F=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Controller,=20Serv?= =?UTF-8?q?ice,=20Repository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../batch/KubeflowRunBatchConfig.java | 4 +- .../KubeflowRunsNewController.java | 54 +++++++ .../autoflow/entity/KubeflowRunEntity.java | 37 ++++- .../payload/request/BaseSearchRequest.java | 4 +- .../request/KubeflowRunSearchRequest.java | 13 ++ .../repository/KubeflowRunRepository.java | 4 +- .../autoflow/service/KubeflowRunService.java | 38 +++++ .../KubeflowRunSpecification.java | 146 ++++++++++++++++++ .../etri/autoflow/swagger/OpenAPIConfig.java | 4 +- 9 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 src/main/java/kr/re/etri/autoflow/controllers/KubeflowRunsNewController.java create mode 100644 src/main/java/kr/re/etri/autoflow/payload/request/KubeflowRunSearchRequest.java create mode 100644 src/main/java/kr/re/etri/autoflow/service/KubeflowRunService.java create mode 100644 src/main/java/kr/re/etri/autoflow/specification/KubeflowRunSpecification.java diff --git a/src/main/java/kr/re/etri/autoflow/batch/KubeflowRunBatchConfig.java b/src/main/java/kr/re/etri/autoflow/batch/KubeflowRunBatchConfig.java index 915ab96..485a4b5 100644 --- a/src/main/java/kr/re/etri/autoflow/batch/KubeflowRunBatchConfig.java +++ b/src/main/java/kr/re/etri/autoflow/batch/KubeflowRunBatchConfig.java @@ -97,8 +97,8 @@ public class KubeflowRunBatchConfig { @Bean public ItemProcessor runProcessor() { return dto -> { - // DB에서 이미 존재하는 runId 체크 - if (kubeflowRunRepository.existsById(dto.getRun_id())) { + // runId 기준 중복 체크 + if (kubeflowRunRepository.existsByRunId(dto.getRun_id())) { return null; // 중복이면 skip } diff --git a/src/main/java/kr/re/etri/autoflow/controllers/KubeflowRunsNewController.java b/src/main/java/kr/re/etri/autoflow/controllers/KubeflowRunsNewController.java new file mode 100644 index 0000000..1ecaa25 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/KubeflowRunsNewController.java @@ -0,0 +1,54 @@ +package kr.re.etri.autoflow.controllers; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.re.etri.autoflow.entity.KubeflowRunEntity; +import kr.re.etri.autoflow.payload.request.KubeflowRunSearchRequest; +import kr.re.etri.autoflow.repository.KubeflowRunRepository; +import kr.re.etri.autoflow.service.KubeflowRunService; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "Kubeflow Run", description = "Kubeflow Run 조회 API") +@RestController +@RequestMapping("/api/kubeflow/runs") +@RequiredArgsConstructor +public class KubeflowRunsNewController { + + private final KubeflowRunRepository runRepository; + + private final KubeflowRunService kubeflowRunService; + + @Operation(summary = "모든 Kubeflow Run 조회") + @GetMapping + public ResponseEntity> getAllRuns() { + List runs = runRepository.findAll(); + return ResponseEntity.ok(runs); + } + + @Operation(summary = "Kubeflow Run 단건 조회") + @GetMapping("/{runId}") + public ResponseEntity getRun( + @Parameter(description = "Kubeflow Run ID", example = "ad980d7f-050a-4c59-a775-94394befad40") + @PathVariable("runId") String runId) { + + return runRepository.findById(runId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation(summary = "Kubeflow Run 검색 및 페이지네이션 조회") + @GetMapping("/search") + public ResponseEntity> searchRuns( + @ParameterObject @ModelAttribute KubeflowRunSearchRequest request) { + + Page page = kubeflowRunService.search(request); + return ResponseEntity.ok(page); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/entity/KubeflowRunEntity.java b/src/main/java/kr/re/etri/autoflow/entity/KubeflowRunEntity.java index eb5979e..620fcfc 100644 --- a/src/main/java/kr/re/etri/autoflow/entity/KubeflowRunEntity.java +++ b/src/main/java/kr/re/etri/autoflow/entity/KubeflowRunEntity.java @@ -1,34 +1,63 @@ package kr.re.etri.autoflow.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.time.Instant; -// Entity (DB 저장용) +/** + * Kubeflow Run 정보를 저장하는 엔티티 + * DB 테이블: tb_runs + */ @Entity @Table(name = "tb_runs") @Data @NoArgsConstructor @AllArgsConstructor public class KubeflowRunEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "자동 증가 내부 ID (DB 관리용)", example = "1") + private Long Id; + + @Column(unique = true, nullable = false) + @Schema(description = "Kubeflow에서 제공하는 Run ID", example = "12345-abcdef") private String runId; + @Schema(description = "Run이 속한 Experiment ID", example = "exp-001") private String experimentId; + + @Schema(description = "Run의 표시 이름", example = "My First Run") private String displayName; + + @Schema(description = "Run의 저장 상태 (예: AVAILABLE, ARCHIVED)", example = "AVAILABLE") private String storageState; + + @Schema(description = "Run 설명", example = "This run tests the new pipeline") private String description; + + @Schema(description = "연결된 Pipeline ID", example = "pipeline-001") private String pipelineId; + + @Schema(description = "연결된 Pipeline Version ID", example = "v1.0") private String pipelineVersionId; + + @Schema(description = "실행 서비스 계정", example = "default") private String serviceAccount; + @Schema(description = "Run 생성 시각", example = "2025-09-24T08:30:00Z") private Instant createdAt; + + @Schema(description = "Run 예약 시각", example = "2025-09-24T08:45:00Z") private Instant scheduledAt; + + @Schema(description = "Run 완료 시각", example = "2025-09-24T09:00:00Z") private Instant finishedAt; + + @Schema(description = "Run 상태 (예: RUNNING, COMPLETED, FAILED)", example = "COMPLETED") private String state; } diff --git a/src/main/java/kr/re/etri/autoflow/payload/request/BaseSearchRequest.java b/src/main/java/kr/re/etri/autoflow/payload/request/BaseSearchRequest.java index ff452bd..5eff810 100644 --- a/src/main/java/kr/re/etri/autoflow/payload/request/BaseSearchRequest.java +++ b/src/main/java/kr/re/etri/autoflow/payload/request/BaseSearchRequest.java @@ -23,10 +23,10 @@ public class BaseSearchRequest { @Schema(description = "검색 유형 (예: 전체, 제목, 작성자 등)", example = "전체") private String searchType; - @Schema(description = "등록일자 검색 시작", example = "2025-08-01") + @Schema(description = "등록일자 검색 시작", example = "1970-08-01") private String startDate; - @Schema(description = "등록일자 검색 종료", example = "2025-08-31") + @Schema(description = "등록일자 검색 종료", example = "2050-08-31") private String endDate; @Schema(description = "정렬 기준 필드명", defaultValue = "id", example = "id") diff --git a/src/main/java/kr/re/etri/autoflow/payload/request/KubeflowRunSearchRequest.java b/src/main/java/kr/re/etri/autoflow/payload/request/KubeflowRunSearchRequest.java new file mode 100644 index 0000000..c57989c --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/payload/request/KubeflowRunSearchRequest.java @@ -0,0 +1,13 @@ +package kr.re.etri.autoflow.payload.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class KubeflowRunSearchRequest extends BaseSearchRequest { + @Schema(description = "실험 ID", example = "1") + private String experimentId; +} + diff --git a/src/main/java/kr/re/etri/autoflow/repository/KubeflowRunRepository.java b/src/main/java/kr/re/etri/autoflow/repository/KubeflowRunRepository.java index f9c9072..a03597c 100644 --- a/src/main/java/kr/re/etri/autoflow/repository/KubeflowRunRepository.java +++ b/src/main/java/kr/re/etri/autoflow/repository/KubeflowRunRepository.java @@ -2,6 +2,8 @@ package kr.re.etri.autoflow.repository; import kr.re.etri.autoflow.entity.KubeflowRunEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface KubeflowRunRepository extends JpaRepository { +public interface KubeflowRunRepository extends JpaRepository, JpaSpecificationExecutor { + boolean existsByRunId(String runId); } diff --git a/src/main/java/kr/re/etri/autoflow/service/KubeflowRunService.java b/src/main/java/kr/re/etri/autoflow/service/KubeflowRunService.java new file mode 100644 index 0000000..effe70c --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/KubeflowRunService.java @@ -0,0 +1,38 @@ +package kr.re.etri.autoflow.service; + +import kr.re.etri.autoflow.entity.KubeflowRunEntity; +import kr.re.etri.autoflow.payload.request.KubeflowRunSearchRequest; +import kr.re.etri.autoflow.repository.KubeflowRunRepository; +import kr.re.etri.autoflow.specification.KubeflowRunSpecification; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class KubeflowRunService { + + private final KubeflowRunRepository runRepository; + private final KubeflowRunSpecification runSpecification; + + @Transactional(readOnly = true) + public Page search(KubeflowRunSearchRequest request) { + int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; + + Pageable pageable = PageRequest.of( + pageIndex, + request.getSize(), + Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortField()) + ); + + Specification spec = runSpecification.searchByConditions( + request.getExperimentId(), // experimentId는 필수 + request.getSearchType(), + request.getKeyword() + ); + + return runRepository.findAll(spec, pageable); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/specification/KubeflowRunSpecification.java b/src/main/java/kr/re/etri/autoflow/specification/KubeflowRunSpecification.java new file mode 100644 index 0000000..0c2a7eb --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/specification/KubeflowRunSpecification.java @@ -0,0 +1,146 @@ +//package kr.re.etri.autoflow.specification; +// +//import jakarta.annotation.PostConstruct; +//import jakarta.persistence.EntityManager; +//import jakarta.persistence.metamodel.Attribute; +//import jakarta.persistence.metamodel.EntityType; +//import jakarta.persistence.metamodel.Metamodel; +//import kr.re.etri.autoflow.entity.KubeflowRunEntity; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.data.jpa.domain.Specification; +//import org.springframework.stereotype.Component; +// +//import jakarta.persistence.criteria.Predicate; +//import java.util.Set; +//import java.util.stream.Collectors; +// +//@Slf4j +//@Component +//public class KubeflowRunSpecification { +// +// private final EntityManager entityManager; +// private Set stringFields; +// +// public KubeflowRunSpecification(EntityManager entityManager) { +// this.entityManager = entityManager; +// } +// +// @PostConstruct +// public void init() { +// Metamodel metamodel = entityManager.getMetamodel(); +// EntityType entityType = metamodel.entity(KubeflowRunEntity.class); +// +// stringFields = entityType.getAttributes().stream() +// .filter(attr -> attr.getJavaType().equals(String.class)) +// .map(Attribute::getName) +// .collect(Collectors.toSet()); +// +// log.info("KubeflowRunEntity string fields: {}", stringFields); +// } +// +// public Specification searchByConditions( +// String experimentId, String searchType, String keyword) { +// +// return (root, query, cb) -> { +// Predicate predicate = cb.conjunction(); +// +// // experimentId는 항상 조건으로 추가 +// predicate = cb.and(predicate, +// cb.equal(root.get("experimentId"), experimentId)); +// +// if (keyword != null && !keyword.isEmpty()) { +// if (searchType == null || searchType.isEmpty() || +// "전체".equalsIgnoreCase(searchType) || "all".equalsIgnoreCase(searchType)) { +// +// Predicate orPredicate = cb.disjunction(); +// for (String field : stringFields) { +// orPredicate = cb.or(orPredicate, +// cb.like(cb.lower(root.get(field)), "%" + keyword.toLowerCase() + "%")); +// } +// predicate = cb.and(predicate, orPredicate); +// +// } else if (stringFields.contains(searchType)) { +// predicate = cb.and(predicate, +// cb.like(cb.lower(root.get(searchType)), "%" + keyword.toLowerCase() + "%")); +// } +// } +// +// return predicate; +// }; +// } +//} + + +package kr.re.etri.autoflow.specification; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.Metamodel; +import kr.re.etri.autoflow.entity.ProjectEntity; +import kr.re.etri.autoflow.entity.KubeflowRunEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +import jakarta.persistence.criteria.Predicate; +import java.time.LocalDate; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class KubeflowRunSpecification { + + private final EntityManager entityManager; + + private Set stringFields; + + public KubeflowRunSpecification(EntityManager entityManager) { + this.entityManager = entityManager; + } + + // 스프링 빈 초기화 후 실행 + @PostConstruct + public void init() { + Metamodel metamodel = entityManager.getMetamodel(); + EntityType entityType = metamodel.entity(KubeflowRunEntity.class); + + // 문자열 타입 필드명만 추출 + stringFields = entityType.getAttributes().stream() + .filter(attr -> attr.getJavaType().equals(String.class)) + .map(Attribute::getName) + .collect(Collectors.toSet()); + + log.info("KubeflowRunEntity string fields: {}", stringFields); + } + + public Specification searchByConditions( + String experimentId, String searchType, String keyword) { + + return (root, query, cb) -> { + Predicate predicate = cb.conjunction(); + + predicate = cb.and(predicate, cb.equal(root.get("experimentId"), experimentId)); + + if (keyword != null && !keyword.isEmpty()) { + if (searchType == null || searchType.isEmpty() || + "전체".equalsIgnoreCase(searchType) || "all".equalsIgnoreCase(searchType)) { + Predicate orPredicate = cb.disjunction(); + for (String field : stringFields) { + orPredicate = cb.or(orPredicate, + cb.like(cb.lower(root.get(field)), "%" + keyword.toLowerCase() + "%")); + } + predicate = cb.and(predicate, orPredicate); + + } else if (stringFields.contains(searchType)) { + predicate = cb.and(predicate, + cb.like(cb.lower(root.get(searchType)), "%" + keyword.toLowerCase() + "%")); + } + } + + return predicate; + }; + } +} diff --git a/src/main/java/kr/re/etri/autoflow/swagger/OpenAPIConfig.java b/src/main/java/kr/re/etri/autoflow/swagger/OpenAPIConfig.java index f33dbac..c5a7af8 100644 --- a/src/main/java/kr/re/etri/autoflow/swagger/OpenAPIConfig.java +++ b/src/main/java/kr/re/etri/autoflow/swagger/OpenAPIConfig.java @@ -23,8 +23,8 @@ public class OpenAPIConfig { private static final String SECURITY_SCHEME_ACCESS = "cuuva-jwt-cookie"; private static final String SECURITY_SCHEME_REFRESH = "cuuva-jwt-refresh-cookie"; - private static final String PRODUCTION_SERVER_URL = "http://cuuva.com:2480/autoflow"; - private static final String LOCAL_SERVER_URL = "http://localhost:80"; + private static final String PRODUCTION_SERVER_URL = "http://cuuva.com:2481/autoflow"; + private static final String LOCAL_SERVER_URL = "http://localhost:8080"; @Bean public OpenAPI customOpenAPI() {