diff --git a/src/main/java/kr/re/etri/autoflow/controllers/WorkflowStepController.java b/src/main/java/kr/re/etri/autoflow/controllers/WorkflowStepController.java new file mode 100644 index 0000000..2062d68 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/WorkflowStepController.java @@ -0,0 +1,78 @@ +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.WorkflowStepEntity; +import kr.re.etri.autoflow.payload.request.WorkFlowStepRequest; +import kr.re.etri.autoflow.service.WorkFlowStepService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "워크플로우 스텝", description = "워크플로우 스텝 API") +@RestController +@RequestMapping("/api/workflow-steps") +@RequiredArgsConstructor +public class WorkflowStepController { + + private final WorkFlowStepService workflowStepService; + + @Operation(summary = "모든 워크플로우 스텝 조회") + @GetMapping + public ResponseEntity> getAllWorkflowSteps() { + return ResponseEntity.ok(workflowStepService.findAll()); + } + + @Operation(summary = "워크플로우 스텝 단건 조회") + @GetMapping("/{id}") + public ResponseEntity getWorkflowStep( + @Parameter(description = "워크플로우 스텝 ID", example = "1") @PathVariable("id") Long id) { + + return workflowStepService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation(summary = "워크플로우 스텝 검색 및 페이지네이션") + @GetMapping("/search") + public ResponseEntity> searchWorkflowSteps( + @ModelAttribute WorkFlowStepRequest request) { + Page page = workflowStepService.search(request); + return ResponseEntity.ok(page); + } + + @Operation(summary = "워크플로우 스텝 등록") + @PostMapping + public ResponseEntity createWorkflowStep(@RequestBody WorkflowStepEntity workflowStep) { + return ResponseEntity.ok(workflowStepService.save(workflowStep)); + } + + @Operation(summary = "워크플로우 스텝 수정") + @PutMapping("/{id}") + public ResponseEntity updateWorkflowStep( + @Parameter(description = "워크플로우 스텝 ID", example = "1") @PathVariable("id") Long id, + @RequestBody WorkflowStepEntity workflowStep) { + + return workflowStepService.findById(id) + .map(existing -> { + workflowStep.setId(id); + return ResponseEntity.ok(workflowStepService.save(workflowStep)); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation(summary = "워크플로우 스텝 삭제") + @DeleteMapping("/{id}") + public ResponseEntity deleteWorkflowStep( + @Parameter(description = "워크플로우 스텝 ID", example = "1") @PathVariable("id") Long id) { + + if (workflowStepService.deleteById(id)) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.notFound().build(); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/entity/FileUploadEntity.java b/src/main/java/kr/re/etri/autoflow/entity/FileUploadEntity.java new file mode 100644 index 0000000..826d3cb --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/entity/FileUploadEntity.java @@ -0,0 +1,62 @@ +package kr.re.etri.autoflow.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "tb_attachment") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "범용 첨부파일 엔티티") +public class FileUploadEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "첨부파일 ID", example = "1") + private Long id; + + // 연관 엔티티 ex) orkflow_step, projectentity + @Schema(description = "연관 엔티티 타입", example = "workflow_step") + @Column(nullable = false, length = 50) + private String refType; + + @Schema(description = "연관 엔티티 ID", example = "1") + @Column(nullable = false) + private Long refId; + + @Schema(description = "원본 파일명", example = "step1.yaml") + @Column(nullable = false, length = 255) + private String originalName; + + @Schema(description = "저장된 파일명(UUID)", example = "a1b2c3d4-step1.yaml") + @Column(nullable = false, length = 255) + private String storedName; + + @Schema(description = "MIME 타입", example = "application/x-yaml") + @Column(nullable = false, length = 100) + private String contentType; + + @Schema(description = "파일 크기(byte)", example = "2048") + @Column(nullable = false) + private Long size; + + @Schema(description = "스토리지 경로", example = "/uploads/workflow/step1.yaml") + @Column(nullable = false, length = 500) + private String storagePath; + + @Schema(description = "업로더 ID", example = "admin") + @Column(nullable = false, length = 50) + private String regUserId; + + @Schema(description = "업로드 일시", example = "2025-09-11T10:00:00") + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime regDt; +} diff --git a/src/main/java/kr/re/etri/autoflow/entity/WorkflowStepEntity.java b/src/main/java/kr/re/etri/autoflow/entity/WorkflowStepEntity.java new file mode 100644 index 0000000..3bea6b4 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/entity/WorkflowStepEntity.java @@ -0,0 +1,73 @@ +package kr.re.etri.autoflow.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Comment; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "tb_workflow_steps") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "워크플로우 스텝 엔티티") +public class WorkflowStepEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "워크플로우 스텝 ID", example = "null", defaultValue = "null") + private Long id; + + @Schema(description = "스텝 이름", example = "데이터 전처리") + @Column(nullable = false, length = 100) + private String stepName; + + @Schema(description = "스텝 상태", example = "Running") + @Column(nullable = false, length = 50) + private String status; + + @Schema(description = "생성자 ID", example = "admin") + @Column(nullable = false, length = 50) + private String regUserId; + + @Schema(description = "생성 일시", example = "2025-09-11T10:00:00") + @Column(nullable = false) + @CreatedDate + private LocalDateTime regDt; + + @Schema(description = "버전", example = "1") + @Column(nullable = false) + private Long version; + + /** Kubeflow에서 가져와야하는 항목 **/ + + @Schema(description = "Kubeflow Pipeline ID", example = "b8c5edc5-6feb-4f9a-8c02-0927e8f864bd") + @Column(length = 100) + private String pipelineId; // Kubeflow Pipeline 식별자 + + @Schema(description = "Kubeflow 시작 시각", example = "2025-09-11T10:00:00") + private LocalDateTime startTime; + + @Schema(description = "Kubeflow 종료 시각", example = "2025-09-11T10:30:00") + private LocalDateTime endTime; + + @Schema(description = "Kubeflow 실행 로그 경로", example = "/logs/workflow/step1.log") + @Column(length = 500) + private String logPath; + + @Schema(description = "프로젝트 아이디", example = "1", defaultValue = "0") + @Column(nullable = false) + private Long projectId; + + @OneToMany + @JoinColumn(name = "refId", referencedColumnName = "id", insertable = false, updatable = false) + private List files = new ArrayList<>(); + +} diff --git a/src/main/java/kr/re/etri/autoflow/payload/request/WorkFlowStepRequest.java b/src/main/java/kr/re/etri/autoflow/payload/request/WorkFlowStepRequest.java new file mode 100644 index 0000000..21d7f89 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/payload/request/WorkFlowStepRequest.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 WorkFlowStepRequest extends BaseSearchRequest { + @Schema(description = "프로젝트 ID", example = "1") + private Long projectId; +} + diff --git a/src/main/java/kr/re/etri/autoflow/repository/WorkflowStepRepository.java b/src/main/java/kr/re/etri/autoflow/repository/WorkflowStepRepository.java new file mode 100644 index 0000000..1287456 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/repository/WorkflowStepRepository.java @@ -0,0 +1,10 @@ +package kr.re.etri.autoflow.repository; + +import kr.re.etri.autoflow.entity.WorkflowEntity; +import kr.re.etri.autoflow.entity.WorkflowStepEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface WorkflowStepRepository extends JpaRepository, JpaSpecificationExecutor { + Long findVersionById(Long id); +} diff --git a/src/main/java/kr/re/etri/autoflow/service/WorkFlowStepService.java b/src/main/java/kr/re/etri/autoflow/service/WorkFlowStepService.java new file mode 100644 index 0000000..4669906 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/WorkFlowStepService.java @@ -0,0 +1,81 @@ +package kr.re.etri.autoflow.service; + +import jakarta.transaction.Transactional; +import kr.re.etri.autoflow.entity.WorkflowStepEntity; +import kr.re.etri.autoflow.payload.request.WorkFlowStepRequest; +import kr.re.etri.autoflow.repository.WorkflowStepRepository; +import kr.re.etri.autoflow.specification.WorkflowStepSpecification; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class WorkFlowStepService { + + private final WorkflowStepRepository workflowstepRepository; + private final WorkflowStepSpecification workflowstepSpecification; + + + public List findAll() { + return workflowstepRepository.findAll(); + } + + public Optional findById(Long id) { + return workflowstepRepository.findById(id); + } + + @Transactional + public WorkflowStepEntity save(WorkflowStepEntity workflowstep) { + if (workflowstep.getId() == null) { + // 신규 생성 + workflowstep.setVersion(1L); + } else { + // 업데이트 시 기존 max 버전 + 1 +// int maxVersion = Math.toIntExact(workflowstepRepository.findVersionById(workflowstep.getId())); +// workflowstep.setVersion((long) (maxVersion+1)); + } + return workflowstepRepository.save(workflowstep); + } + + @Transactional + public boolean deleteById(Long id) { + if (workflowstepRepository.existsById(id)) { + workflowstepRepository.deleteById(id); + return true; + } + return false; + } + + @Transactional + public Page search(WorkFlowStepRequest 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 = workflowstepSpecification.searchByConditions( + request.getSearchType(), + request.getKeyword() + ); + + // projectId가 있으면 조건 추가 (권장) + if (request.getProjectId() != null) { + spec = spec.and((root, query, cb) -> + cb.equal(root.get("projectId"), request.getProjectId()) + ); + } + + return workflowstepRepository.findAll(spec, pageable); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/specification/WorkflowStepSpecification.java b/src/main/java/kr/re/etri/autoflow/specification/WorkflowStepSpecification.java new file mode 100644 index 0000000..732f259 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/specification/WorkflowStepSpecification.java @@ -0,0 +1,70 @@ +package kr.re.etri.autoflow.specification; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.Metamodel; +import kr.re.etri.autoflow.entity.WorkflowStepEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class WorkflowStepSpecification { + + private final EntityManager entityManager; + + private Set stringFields; + + public WorkflowStepSpecification(EntityManager entityManager) { + this.entityManager = entityManager; + } + + // 스프링 빈 초기화 후 실행 + @PostConstruct + public void init() { + Metamodel metamodel = entityManager.getMetamodel(); + EntityType entityType = metamodel.entity(WorkflowStepEntity.class); + + // 문자열 타입 필드명만 추출 + stringFields = entityType.getAttributes().stream() + .filter(attr -> attr.getJavaType().equals(String.class)) + .map(Attribute::getName) + .collect(Collectors.toSet()); + + log.info("ProjectEntity string fields: {}", stringFields); + } + + public Specification searchByConditions( + String searchType, String keyword) { + + return (root, query, cb) -> { + Predicate predicate = cb.conjunction(); + + 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; + }; + } +}