[REMOVE] DynamicMinioAttachmentController, 관련 서비스 및 엔티티 클래스 삭제

main
bjkim 2 months ago
parent ffec74b7ee
commit 40d2cc9367

@ -23,9 +23,9 @@
- 워크플로우 생성 및 실행 관리 - 워크플로우 생성 및 실행 관리
### 4. 파일 관리 (File Management) ### 4. 파일 관리 (File Management)
- **MinIO** 및 **AWS S3** 연동을 통한 대용량 파일 저장 및 관리 - **AWS S3** 연동을 통한 대용량 파일 저장 및 관리
- 멀티파트(Multipart) 파일 업로드 지원 (최대 500MB) - 멀티파트(Multipart) 파일 업로드 지원 (최대 500MB)
- 동적 MinIO 첨부 파일 관리 시스템 - 동적 AWS S3 첨부 파일 관리 시스템
### 5. 외부 시스템 연동 (External Integrations) ### 5. 외부 시스템 연동 (External Integrations)
- **OTA (Over-The-Air)** 연동: 외부 인증 및 패키지 검색 API 연동 지원 - **OTA (Over-The-Air)** 연동: 외부 인증 및 패키지 검색 API 연동 지원
@ -40,7 +40,7 @@
- **Build Tool:** Gradle - **Build Tool:** Gradle
- **Database:** MariaDB (JPA / Hibernate) - **Database:** MariaDB (JPA / Hibernate)
- **Security:** Spring Security, JWT (jjwt 0.11.5) - **Security:** Spring Security, JWT (jjwt 0.11.5)
- **Storage:** MinIO, AWS S3 - **Storage:** AWS S3
- **Batch Processing:** Spring Batch - **Batch Processing:** Spring Batch
- **API Documentation:** Springdoc OpenAPI (Swagger UI) - **API Documentation:** Springdoc OpenAPI (Swagger UI)
- **Etc:** Lombok, Jsoup, Caffeine Cache, WebFlux - **Etc:** Lombok, Jsoup, Caffeine Cache, WebFlux
@ -52,7 +52,7 @@
### 사전 요구 사항 ### 사전 요구 사항
- Java 17 이상 설치 - Java 17 이상 설치
- MariaDB 설치 및 데이터베이스 생성 (`autoflow`) - MariaDB 설치 및 데이터베이스 생성 (`autoflow`)
- MinIO 또는 S3 접근 권한 필요 - AWS S3 접근 권한 필요
### 환경 설정 (`application.properties`) ### 환경 설정 (`application.properties`)
`src/main/resources/application.properties` 파일에서 다음 항목들을 설정해야 합니다: `src/main/resources/application.properties` 파일에서 다음 항목들을 설정해야 합니다:
@ -66,10 +66,11 @@ spring.datasource.password={PASSWORD}
# JWT 보안 설정 # JWT 보안 설정
cuuva.app.jwtSecret={YOUR_SECRET_KEY} cuuva.app.jwtSecret={YOUR_SECRET_KEY}
# MinIO 설정 # AWS S3 설정
minio.endpoint={MINIO_ENDPOINT} cloud.aws.s3.endpoint={AWS_S3_ENDPOINT}
minio.access-key={ACCESS_KEY} cloud.aws.credentials.access-key={AWS_ACCESS_KEY}
minio.secret-key={SECRET_KEY} cloud.aws.credentials.secret-key={AWS_SECRET_KEY}
cloud.aws.region.static={AWS_REGION}
# 외부 서비스 연동 # 외부 서비스 연동
kubeflow.url={KUBEFLOW_URL} kubeflow.url={KUBEFLOW_URL}

@ -2,11 +2,13 @@ package kr.re.etri.autoflow.common;
import io.minio.MinioClient; import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class MinIOConfig { public class StorageConfig {
@Value("${minio.endpoint}") @Value("${minio.endpoint}")
private String endpoint; private String endpoint;
@ -17,11 +19,11 @@ public class MinIOConfig {
private String secretKey; private String secretKey;
@Bean @Bean
@ConditionalOnProperty(name = "storage.provider", havingValue = "minio", matchIfMissing = true)
public MinioClient minioClient() { public MinioClient minioClient() {
return MinioClient.builder() return MinioClient.builder()
.endpoint(endpoint) .endpoint(endpoint)
.credentials(accessKey, secretKey) .credentials(accessKey, secretKey)
.build(); .build();
} }
} }

@ -3,9 +3,9 @@ package kr.re.etri.autoflow.controllers;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.service.DynamicMinioAttachmentService; import kr.re.etri.autoflow.service.DynamicStorageAttachmentService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -27,9 +27,9 @@ import java.util.Map;
@RestController @RestController
@RequestMapping("/api/minio") @RequestMapping("/api/minio")
@RequiredArgsConstructor @RequiredArgsConstructor
public class DynamicMinioAttachmentController { public class DynamicStorageAttachmentController {
private final DynamicMinioAttachmentService minioService; private final DynamicStorageAttachmentService minioService;
/** /**
* *
@ -48,7 +48,7 @@ public class DynamicMinioAttachmentController {
@RequestParam(defaultValue = "type1") String type @RequestParam(defaultValue = "type1") String type
) { ) {
try { try {
MinioAttachmentEntity attachment = minioService.uploadFile( StorageAttachmentEntity attachment = minioService.uploadFile(
file, path, refId, refType, title, description, version, regUserId, projectId, type file, path, refId, refType, title, description, version, regUserId, projectId, type
); );
@ -149,8 +149,8 @@ public class DynamicMinioAttachmentController {
* *
*/ */
@GetMapping("/list") @GetMapping("/list")
public ResponseEntity<List<MinioAttachmentEntity>> listAll() { public ResponseEntity<List<StorageAttachmentEntity>> listAll() {
List<MinioAttachmentEntity> list = minioService.findAll(); List<StorageAttachmentEntity> list = minioService.findAll();
return ResponseEntity.ok(list); return ResponseEntity.ok(list);
} }
@ -158,12 +158,12 @@ public class DynamicMinioAttachmentController {
* + * +
*/ */
@PostMapping("/search") @PostMapping("/search")
public ResponseEntity<Page<MinioAttachmentEntity>> search( public ResponseEntity<Page<StorageAttachmentEntity>> search(
@RequestBody ProjectBaseSearchRequest request, @RequestBody ProjectBaseSearchRequest request,
@RequestParam String refType, @RequestParam String refType,
@RequestParam Integer refId @RequestParam Integer refId
) { ) {
Page<MinioAttachmentEntity> page = minioService.search(request, refType, refId); Page<StorageAttachmentEntity> page = minioService.search(request, refType, refId);
return ResponseEntity.ok(page); return ResponseEntity.ok(page);
} }
} }

@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import kr.re.etri.autoflow.payload.request.EdgeSWVO; import kr.re.etri.autoflow.payload.request.EdgeSWVO;
import kr.re.etri.autoflow.service.DynamicMinioAttachmentService; import kr.re.etri.autoflow.service.DynamicStorageAttachmentService;
import kr.re.etri.autoflow.service.EdgeSWUploadService; import kr.re.etri.autoflow.service.EdgeSWUploadService;
import kr.re.etri.autoflow.service.ExternalAuthService; import kr.re.etri.autoflow.service.ExternalAuthService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -45,7 +45,7 @@ public class ExternalAuthController {
private final ExternalAuthService externalAuthService; private final ExternalAuthService externalAuthService;
private final EdgeSWUploadService edgeSWUploadService; private final EdgeSWUploadService edgeSWUploadService;
private final DynamicMinioAttachmentService minioService; private final DynamicStorageAttachmentService minioService;
private RestTemplate restTemplate; private RestTemplate restTemplate;

@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.service.DatasetService; import kr.re.etri.autoflow.service.DatasetService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -26,7 +26,7 @@ import java.util.Map;
public class ExternalDataSetController { public class ExternalDataSetController {
private final DatasetService datasetService; private final DatasetService datasetService;
private final kr.re.etri.autoflow.service.MinioAttachmentService minioAttachmentService; private final kr.re.etri.autoflow.service.StorageAttachmentService storageAttachmentService;
@Operation( @Operation(
summary = "데이터셋 목록 조회", summary = "데이터셋 목록 조회",
@ -64,13 +64,13 @@ public class ExternalDataSetController {
@RequestParam Long projectId @RequestParam Long projectId
) { ) {
try { try {
MinioAttachmentEntity saved = datasetService.downloadDataset( StorageAttachmentEntity saved = datasetService.downloadDataset(
datasetName, path, refId, refType, title, description, version, regUserId, projectId datasetName, path, refId, refType, title, description, version, regUserId, projectId
); );
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("attachment", saved); response.put("attachment", saved);
response.put("minioUrl", minioAttachmentService.getFileUrl(saved.getStoragePath())); response.put("minioUrl", storageAttachmentService.getFileUrl(saved.getStoragePath()));
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (Exception e) { } catch (Exception e) {

@ -6,10 +6,10 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.entity.WorkflowEntity; import kr.re.etri.autoflow.entity.WorkflowEntity;
import kr.re.etri.autoflow.payload.request.CreateRunRequest; import kr.re.etri.autoflow.payload.request.CreateRunRequest;
import kr.re.etri.autoflow.service.MinioAttachmentService; import kr.re.etri.autoflow.service.StorageAttachmentService;
import kr.re.etri.autoflow.service.PipelineUploadService; import kr.re.etri.autoflow.service.PipelineUploadService;
import kr.re.etri.autoflow.service.WorkFlowService; import kr.re.etri.autoflow.service.WorkFlowService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -36,7 +36,7 @@ public class PipelineUploadController {
private final PipelineUploadService pipelineUploadService; private final PipelineUploadService pipelineUploadService;
private final WorkFlowService workFlowService; private final WorkFlowService workFlowService;
private final MinioAttachmentService minioAttachmentService; private final StorageAttachmentService storageAttachmentService;
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, Object>> uploadPipeline( public ResponseEntity<Map<String, Object>> uploadPipeline(
@ -73,7 +73,7 @@ public class PipelineUploadController {
workFlowService.save(workflow); workFlowService.save(workflow);
// 2. MinIO 업로드 // 2. MinIO 업로드
MinioAttachmentEntity attachment = minioAttachmentService.uploadFile( StorageAttachmentEntity attachment = storageAttachmentService.uploadFile(
file, file,
"workflows/" + projectId, "workflows/" + projectId,
workflow.getId(), workflow.getId(),
@ -85,7 +85,7 @@ public class PipelineUploadController {
projectId projectId
); );
String minioUrl = minioAttachmentService.getFileUrl(attachment.getStoragePath()); String minioUrl = storageAttachmentService.getFileUrl(attachment.getStoragePath());
// 3. 최종 응답 // 3. 최종 응답
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();

@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.service.MinioAttachmentService; import kr.re.etri.autoflow.service.StorageAttachmentService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
@ -39,31 +39,31 @@ import java.util.*;
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@Tag(name = "첨부파일", description = "MinIO 첨부파일 관리 API") @Tag(name = "첨부파일", description = "MinIO 첨부파일 관리 API")
public class MinioAttachmentController { public class StorageAttachmentController {
private final MinioAttachmentService minioAttachmentService; private final StorageAttachmentService storageAttachmentService;
private final MinioClient minioClient; private final MinioClient minioClient;
@Operation(summary = "첨부파일 전체 조회") @Operation(summary = "첨부파일 전체 조회")
@GetMapping @GetMapping
public ResponseEntity<List<MinioAttachmentEntity>> getAll() { public ResponseEntity<List<StorageAttachmentEntity>> getAll() {
return ResponseEntity.ok(minioAttachmentService.findAll()); return ResponseEntity.ok(storageAttachmentService.findAll());
} }
@Operation(summary = "ID로 첨부파일 조회") @Operation(summary = "ID로 첨부파일 조회")
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<MinioAttachmentEntity> getById( public ResponseEntity<StorageAttachmentEntity> getById(
@Parameter(description = "첨부파일 ID", required = true) @Parameter(description = "첨부파일 ID", required = true)
@PathVariable("id") Long id) { @PathVariable("id") Long id) {
return minioAttachmentService.findById(id) return storageAttachmentService.findById(id)
.map(ResponseEntity::ok) .map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()); .orElse(ResponseEntity.notFound().build());
} }
@Operation(summary = "검색 및 페이지네이션 첨부파일 목록 조회") @Operation(summary = "검색 및 페이지네이션 첨부파일 목록 조회")
@GetMapping("/search") @GetMapping("/search")
public ResponseEntity<Page<MinioAttachmentEntity>> search( public ResponseEntity<Page<StorageAttachmentEntity>> search(
@ParameterObject @ModelAttribute ProjectBaseSearchRequest request, @ParameterObject @ModelAttribute ProjectBaseSearchRequest request,
@Parameter( @Parameter(
description = "첨부파일 구분자. 예: WORKFLOW_STEP, DATASET, TRAINING_SCRIPT", description = "첨부파일 구분자. 예: WORKFLOW_STEP, DATASET, TRAINING_SCRIPT",
@ -72,7 +72,7 @@ public class MinioAttachmentController {
@RequestParam(value = "refType", required = false) String refType, @RequestParam(value = "refType", required = false) String refType,
@RequestParam(value = "refId", required = false, defaultValue = "0") Integer refId @RequestParam(value = "refId", required = false, defaultValue = "0") Integer refId
) { ) {
Page<MinioAttachmentEntity> page = minioAttachmentService.search(request, refType, refId); Page<StorageAttachmentEntity> page = storageAttachmentService.search(request, refType, refId);
return ResponseEntity.ok(page); return ResponseEntity.ok(page);
} }
@ -81,7 +81,7 @@ public class MinioAttachmentController {
public ResponseEntity<Void> delete( public ResponseEntity<Void> delete(
@Parameter(description = "첨부파일 ID", required = true) @Parameter(description = "첨부파일 ID", required = true)
@PathVariable("id") Long id) { @PathVariable("id") Long id) {
if (minioAttachmentService.delete(id)) { if (storageAttachmentService.delete(id)) {
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@ -91,7 +91,7 @@ public class MinioAttachmentController {
@GetMapping("/download") @GetMapping("/download")
public ResponseEntity<byte[]> downloadFile(@RequestParam String objectName) { public ResponseEntity<byte[]> downloadFile(@RequestParam String objectName) {
try { try {
byte[] bytes = minioAttachmentService.downloadFile("mlpipeline", objectName); byte[] bytes = storageAttachmentService.downloadFile("mlpipeline", objectName);
String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8) String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); // 공백 처리 .replaceAll("\\+", "%20"); // 공백 처리
@ -139,7 +139,7 @@ public class MinioAttachmentController {
@GetMapping(value = "/readYamlText", produces = MediaType.TEXT_PLAIN_VALUE) @GetMapping(value = "/readYamlText", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> readYamlTextFromMinio(@RequestParam String objectName) { public ResponseEntity<String> readYamlTextFromMinio(@RequestParam String objectName) {
try { try {
String content = minioAttachmentService.readYamlText("mlpipeline", objectName); String content = storageAttachmentService.readYamlText("mlpipeline", objectName);
return ResponseEntity.ok(content); return ResponseEntity.ok(content);
} catch (Exception e) { } catch (Exception e) {
log.error("MinIO 파일 읽기 실패: {}", objectName, e); log.error("MinIO 파일 읽기 실패: {}", objectName, e);
@ -163,13 +163,13 @@ public class MinioAttachmentController {
) { ) {
try { try {
MinioAttachmentEntity saved = minioAttachmentService.uploadFile( StorageAttachmentEntity saved = storageAttachmentService.uploadFile(
file, path, refId, refType, title, description, version, regUserId, projectId file, path, refId, refType, title, description, version, regUserId, projectId
); );
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("attachment", saved); response.put("attachment", saved);
response.put("minioUrl", minioAttachmentService.getFileUrl(saved.getStoragePath())); response.put("minioUrl", storageAttachmentService.getFileUrl(saved.getStoragePath()));
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
@ -193,13 +193,13 @@ public class MinioAttachmentController {
@RequestParam(value = "regUserId") String regUserId @RequestParam(value = "regUserId") String regUserId
) { ) {
try { try {
MinioAttachmentEntity updated = minioAttachmentService.updateFile( StorageAttachmentEntity updated = storageAttachmentService.updateFile(
id, projectId, file, path, title, description, regUserId id, projectId, file, path, title, description, regUserId
); );
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("attachment", updated); response.put("attachment", updated);
response.put("minioUrl", minioAttachmentService.getFileUrl(updated.getStoragePath())); response.put("minioUrl", storageAttachmentService.getFileUrl(updated.getStoragePath()));
return ResponseEntity.ok(response); return ResponseEntity.ok(response);

@ -19,7 +19,7 @@ import java.time.LocalDateTime;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class MinioAttachmentEntity { public class StorageAttachmentEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

@ -1,16 +0,0 @@
package kr.re.etri.autoflow.repository;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface MinioAttachmentRepository extends JpaRepository<MinioAttachmentEntity, Long>, JpaSpecificationExecutor<MinioAttachmentEntity> {
//최신버전 파일 가져오기
Optional<MinioAttachmentEntity> findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType);
List<MinioAttachmentEntity> findAllByRefId(Long refId);
}

@ -0,0 +1,16 @@
package kr.re.etri.autoflow.repository;
import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface StorageAttachmentRepository extends JpaRepository<StorageAttachmentEntity, Long>, JpaSpecificationExecutor<StorageAttachmentEntity> {
//최신버전 파일 가져오기
Optional<StorageAttachmentEntity> findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType);
List<StorageAttachmentEntity> findAllByRefId(Long refId);
}

@ -3,12 +3,12 @@ package kr.re.etri.autoflow.service;
import io.minio.MinioClient; import io.minio.MinioClient;
import io.minio.RemoveObjectArgs; import io.minio.RemoveObjectArgs;
import kr.re.etri.autoflow.entity.DataGroupEntity; import kr.re.etri.autoflow.entity.DataGroupEntity;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseAndRefTypeRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseAndRefTypeRequest;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.payload.request.ProjectRequest; import kr.re.etri.autoflow.payload.request.ProjectRequest;
import kr.re.etri.autoflow.repository.DataGroupRepository; import kr.re.etri.autoflow.repository.DataGroupRepository;
import kr.re.etri.autoflow.repository.MinioAttachmentRepository; import kr.re.etri.autoflow.repository.StorageAttachmentRepository;
import kr.re.etri.autoflow.specification.DataGroupSpecification; import kr.re.etri.autoflow.specification.DataGroupSpecification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -36,7 +36,7 @@ public class DataGroupService {
private final DataGroupRepository dataGroupRepository; private final DataGroupRepository dataGroupRepository;
private final MinioAttachmentRepository minioAttachmentRepository; private final StorageAttachmentRepository storageAttachmentRepository;
private final DataGroupSpecification dataGroupSpecification; private final DataGroupSpecification dataGroupSpecification;
@ -133,11 +133,11 @@ public class DataGroupService {
} }
// 2. refId 기준으로 MinIO 첨부파일 조회 // 2. refId 기준으로 MinIO 첨부파일 조회
List<MinioAttachmentEntity> attachments = List<StorageAttachmentEntity> attachments =
minioAttachmentRepository.findAllByRefId(dataGroupId); storageAttachmentRepository.findAllByRefId(dataGroupId);
// 3. MinIO에서 파일 삭제 // 3. MinIO에서 파일 삭제
for (MinioAttachmentEntity attachment : attachments) { for (StorageAttachmentEntity attachment : attachments) {
try { try {
minioClient.removeObject( minioClient.removeObject(
RemoveObjectArgs.builder() RemoveObjectArgs.builder()
@ -146,7 +146,7 @@ public class DataGroupService {
.build() .build()
); );
// DB에서도 첨부파일 삭제 // DB에서도 첨부파일 삭제
minioAttachmentRepository.delete(attachment); storageAttachmentRepository.delete(attachment);
} catch (Exception e) { } catch (Exception e) {
log.error("MinIO 파일 삭제 실패: {}", attachment.getStoragePath(), e); log.error("MinIO 파일 삭제 실패: {}", attachment.getStoragePath(), e);
} }

@ -2,8 +2,8 @@ package kr.re.etri.autoflow.service;
import io.minio.MinioClient; import io.minio.MinioClient;
import io.minio.PutObjectArgs; import io.minio.PutObjectArgs;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.repository.MinioAttachmentRepository; import kr.re.etri.autoflow.repository.StorageAttachmentRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -33,7 +33,7 @@ public class DatasetService {
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final MinioAttachmentRepository minioAttachmentRepository; private final StorageAttachmentRepository storageAttachmentRepository;
private static final String BASE_URL = "http://52.14.11.43:18010"; private static final String BASE_URL = "http://52.14.11.43:18010";
@ -111,7 +111,7 @@ public class DatasetService {
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build(); .build();
public MinioAttachmentEntity downloadDataset( public StorageAttachmentEntity downloadDataset(
String datasetName, String datasetName,
String path, String path,
Long refId, Long refId,
@ -195,7 +195,7 @@ public class DatasetService {
latch.await(); latch.await();
// DB 저장 시 size 컬럼 필수 // DB 저장 시 size 컬럼 필수
MinioAttachmentEntity attachment = MinioAttachmentEntity.builder() StorageAttachmentEntity attachment = StorageAttachmentEntity.builder()
.refId(refId) .refId(refId)
.refType(refType) .refType(refType)
.originalName(datasetName + ".zip") .originalName(datasetName + ".zip")
@ -210,7 +210,7 @@ public class DatasetService {
.size(totalBytes[0]) .size(totalBytes[0])
.build(); .build();
return minioAttachmentRepository.save(attachment); return storageAttachmentRepository.save(attachment);
} catch (Exception e) { } catch (Exception e) {
log.error("외부 API 다운로드 및 MinIO 업로드 실패", e); log.error("외부 API 다운로드 및 MinIO 업로드 실패", e);

@ -1,285 +0,0 @@
package kr.re.etri.autoflow.service;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import jakarta.transaction.Transactional;
import kr.re.etri.autoflow.common.MinioType;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.repository.MinioAttachmentRepository;
import kr.re.etri.autoflow.specification.MinioAttachmentSpecification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class DynamicMinioAttachmentService {
private final MinioAttachmentRepository minioAttachmentRepository;
private final MinioAttachmentSpecification minioAttachmentSpecification;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/** MinioClient 생성 (type 기반) */
private MinioClient getClientByType(String type) {
MinioType config = MinioType.of(type);
return MinioClient.builder()
.endpoint(config.getEndpoint())
.credentials(config.getAccessKey(), config.getSecretKey())
.build();
}
/** Bucket 조회 (type 기반) */
private String getBucketByType(String type) {
return MinioType.of(type).getBucket();
}
/** 파일 업로드 & DB 저장 */
public MinioAttachmentEntity uploadFile(MultipartFile file,
String path,
Long refId,
String refType,
String title,
String description,
Integer version,
String regUserId,
Long projectId,
String type
) throws Exception {
MinioClient client = getClientByType(type);
String bucketName = getBucketByType(type);
try (InputStream is = file.getInputStream()) {
String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename();
String objectName = (path == null || path.isEmpty())
? storedName
: path + "/" + storedName;
// MinIO 업로드
client.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(is, is.available(), -1)
.contentType(file.getContentType())
.build()
);
// DB 저장
MinioAttachmentEntity attachment = MinioAttachmentEntity.builder()
.refId(refId)
.refType(refType)
.originalName(file.getOriginalFilename())
.storedName(storedName)
.contentType(file.getContentType())
.size(file.getSize())
.storagePath(objectName)
.title(title != null ? title : file.getOriginalFilename())
.version(version)
.description(description)
.regUserId(regUserId)
.projectId(projectId)
.build();
return minioAttachmentRepository.save(attachment);
}
}
/** 파일 다운로드 */
public byte[] downloadFile(String objectName, String type) {
MinioClient client = getClientByType(type);
String bucketName = getBucketByType(type);
try {
// 1. mlflow-artifacts:/ 접두어 제거
String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "");
// 2. 잘못된 슬래시 제거
cleanObjectName = cleanObjectName.replaceAll("^/+", "").replaceAll("/+$", "");
// 3. 파일 확장자가 없는 경우 (디렉터리 요청으로 추정)
// → MLflow 구조상 실제 파일은 artifacts/ 하위에 있으므로 경로 자동 보정
if (!cleanObjectName.matches(".*\\.[a-zA-Z0-9]+$")) {
throw new RuntimeException("요청된 객체가 파일이 아닙니다. 실제 파일 경로를 포함해야 합니다: " + cleanObjectName);
}
try (InputStream is = client.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(cleanObjectName)
.build()
)) {
return is.readAllBytes();
}
} catch (io.minio.errors.ErrorResponseException e) {
throw new RuntimeException(
"MinIO 서버가 요청을 거부했습니다: " + objectName +
", 코드=" + e.errorResponse().code() +
", 버킷이름=" + bucketName +
", 메시지=" + e.errorResponse().message() +
", 요청ID=" + e.errorResponse().requestId() +
", 호스트ID=" + e.errorResponse().hostId(), e);
} catch (Exception e) {
throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, e);
}
}
/**
* MinIO
* @param objectName MinIO
* @param type MinIO
* @param localPath (: downloads/temp)
* @return
*/
public File downloadFileToServer(String objectName, String type, String localPath) {
MinioClient client = getClientByType(type);
String bucketName = getBucketByType(type);
try {
// mlflow-artifacts:/ 접두어 제거
String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "")
.replaceAll("^/+", "").replaceAll("/+$", "");
// 파일 확장자 체크
if (!cleanObjectName.matches(".*\\.[a-zA-Z0-9]+$")) {
throw new RuntimeException("요청된 객체가 파일이 아닙니다: " + cleanObjectName);
}
// 파일명 추출
String fileName = Paths.get(cleanObjectName).getFileName().toString();
File localDir = new File(localPath);
if (!localDir.exists()) localDir.mkdirs();
File localFile = new File(localDir, fileName);
try (InputStream is = client.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(cleanObjectName)
.build()
);
FileOutputStream fos = new FileOutputStream(localFile)
) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
return localFile;
} catch (Exception e) {
throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName + ". 원인: " + e.getMessage(), e);
}
}
/** YAML 텍스트 읽기 */
public String readYamlText(String objectName, String type) {
MinioClient client = getClientByType(type);
String bucketName = getBucketByType(type);
try (InputStream is = client.getObject(
GetObjectArgs.builder().bucket(bucketName).object(objectName).build()
)) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("MinIO YAML 읽기 실패: " + objectName, e);
}
}
/** 파일 삭제 */
public boolean deleteFile(Long id, String type) {
Optional<MinioAttachmentEntity> attachmentOpt = minioAttachmentRepository.findById(id);
if (attachmentOpt.isEmpty()) return false;
MinioAttachmentEntity attachment = attachmentOpt.get();
MinioClient client = getClientByType(type);
String bucketName = getBucketByType(type);
try {
client.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(attachment.getStoragePath())
.build()
);
minioAttachmentRepository.deleteById(id);
return true;
} catch (Exception e) {
log.error("MinIO 파일 삭제 실패: " + attachment.getStoragePath(), e);
return false;
}
}
// 이하 기존 search, findById, update 등 기존 DB 관련 메서드는 그대로 재사용 가능
public List<MinioAttachmentEntity> findAll() {
return minioAttachmentRepository.findAll();
}
public Optional<MinioAttachmentEntity> findById(Long id) {
return minioAttachmentRepository.findById(id);
}
public Page<MinioAttachmentEntity> search(ProjectBaseSearchRequest request, String refType, Integer refId) {
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())
);
LocalDate startDate = parseDate(request.getStartDate());
LocalDate endDate = parseDate(request.getEndDate());
Specification<MinioAttachmentEntity> spec =
minioAttachmentSpecification.searchByConditions(
refType,
refId,
request.getSearchType(),
request.getKeyword(),
startDate,
endDate
);
if (request.getProjectId() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("projectId"), request.getProjectId())
);
}
return minioAttachmentRepository.findAll(spec, pageable);
}
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) return null;
try {
return LocalDate.parse(dateStr, formatter);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("날짜 형식이 잘못되었습니다. yyyy-MM-dd 형식이어야 합니다: " + dateStr);
}
}
}

@ -0,0 +1,173 @@
package kr.re.etri.autoflow.service;
import jakarta.transaction.Transactional;
import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.repository.StorageAttachmentRepository;
import kr.re.etri.autoflow.service.storage.StorageProvider;
import kr.re.etri.autoflow.specification.StorageAttachmentSpecification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class DynamicStorageAttachmentService {
private final StorageAttachmentRepository storageAttachmentRepository;
private final StorageAttachmentSpecification storageAttachmentSpecification;
private final StorageProvider storageProvider;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/** 파일 업로드 & DB 저장 */
public StorageAttachmentEntity uploadFile(MultipartFile file,
String path,
Long refId,
String refType,
String title,
String description,
Integer version,
String regUserId,
Long projectId,
String type
) throws Exception {
try (InputStream is = file.getInputStream()) {
String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename();
String objectName = (path == null || path.isEmpty())
? storedName
: path + "/" + storedName;
// 스토리지 업로드
storageProvider.uploadFile(null, objectName, is, file.getContentType(), file.getSize(), type);
// DB 저장
StorageAttachmentEntity attachment = StorageAttachmentEntity.builder()
.refId(refId)
.refType(refType)
.originalName(file.getOriginalFilename())
.storedName(storedName)
.contentType(file.getContentType())
.size(file.getSize())
.storagePath(objectName)
.title(title != null ? title : file.getOriginalFilename())
.version(version)
.description(description)
.regUserId(regUserId)
.projectId(projectId)
.build();
return storageAttachmentRepository.save(attachment);
}
}
/** 파일 다운로드 */
public byte[] downloadFile(String objectName, String type) {
try {
// mlflow-artifacts:/ 등 처리는 provider 단에서 혹은 여기서 할 수 있음.
// 현재 provider에 넣었으니 바로 위임
return storageProvider.downloadFile(null, objectName, type);
} catch (Exception e) {
throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName, e);
}
}
/**
*
*/
public File downloadFileToServer(String objectName, String type, String localPath) {
try {
return storageProvider.downloadFileToServer(null, objectName, localPath, type);
} catch (Exception e) {
throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName + ". 원인: " + e.getMessage(), e);
}
}
/** YAML 텍스트 읽기 */
public String readYamlText(String objectName, String type) {
try {
return storageProvider.readYamlText(null, objectName, type);
} catch (Exception e) {
throw new RuntimeException("스토리지 YAML 읽기 실패: " + objectName, e);
}
}
/** 파일 삭제 */
public boolean deleteFile(Long id, String type) {
Optional<StorageAttachmentEntity> attachmentOpt = storageAttachmentRepository.findById(id);
if (attachmentOpt.isEmpty()) return false;
StorageAttachmentEntity attachment = attachmentOpt.get();
try {
storageProvider.deleteFile(null, attachment.getStoragePath(), type);
storageAttachmentRepository.deleteById(id);
return true;
} catch (Exception e) {
log.error("스토리지 파일 삭제 실패: " + attachment.getStoragePath(), e);
return false;
}
}
public List<StorageAttachmentEntity> findAll() {
return storageAttachmentRepository.findAll();
}
public Optional<StorageAttachmentEntity> findById(Long id) {
return storageAttachmentRepository.findById(id);
}
public Page<StorageAttachmentEntity> search(ProjectBaseSearchRequest request, String refType, Integer refId) {
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())
);
LocalDate startDate = parseDate(request.getStartDate());
LocalDate endDate = parseDate(request.getEndDate());
Specification<StorageAttachmentEntity> spec =
storageAttachmentSpecification.searchByConditions(
refType,
refId,
request.getSearchType(),
request.getKeyword(),
startDate,
endDate
);
if (request.getProjectId() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("projectId"), request.getProjectId())
);
}
return storageAttachmentRepository.findAll(spec, pageable);
}
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) return null;
try {
return LocalDate.parse(dateStr, formatter);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("날짜 형식이 잘못되었습니다. yyyy-MM-dd 형식이어야 합니다: " + dateStr);
}
}
}

@ -1,26 +1,21 @@
package kr.re.etri.autoflow.service; package kr.re.etri.autoflow.service;
import io.minio.GetObjectArgs; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import jakarta.transaction.Transactional;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity;
import kr.re.etri.autoflow.payload.request.BaseSearchRequest; import kr.re.etri.autoflow.payload.request.BaseSearchRequest;
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
import kr.re.etri.autoflow.repository.MinioAttachmentRepository; import kr.re.etri.autoflow.repository.StorageAttachmentRepository;
import kr.re.etri.autoflow.specification.MinioAttachmentSpecification; import kr.re.etri.autoflow.service.storage.StorageProvider;
import kr.re.etri.autoflow.specification.StorageAttachmentSpecification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.*; import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
@ -32,24 +27,18 @@ import java.util.UUID;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional @Transactional
public class MinioAttachmentService { public class StorageAttachmentService {
private final MinioClient minioClient; private final StorageProvider storageProvider;
private final MinioAttachmentRepository minioAttachmentRepository; private final StorageAttachmentRepository storageAttachmentRepository;
private final MinioAttachmentSpecification minioAttachmentSpecification; private final StorageAttachmentSpecification storageAttachmentSpecification;
@Value("${minio.bucket}")
private String bucketName;
@Value("${minio.endpoint}")
private String minioEndpoint;
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/** /**
* & DB * & DB
*/ */
public MinioAttachmentEntity uploadFile(MultipartFile file, public StorageAttachmentEntity uploadFile(MultipartFile file,
String path, String path,
Long refId, Long refId,
String refType, String refType,
@ -66,18 +55,11 @@ public class MinioAttachmentService {
? storedName ? storedName
: path + "/" + storedName; : path + "/" + storedName;
// MinIO 업로드 // 스토리지 업로드
minioClient.putObject( storageProvider.uploadFileToDefault(objectName, is, file.getContentType(), file.getSize());
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(is, is.available(), -1)
.contentType(file.getContentType())
.build()
);
// DB 저장 // DB 저장
MinioAttachmentEntity attachment = MinioAttachmentEntity.builder() StorageAttachmentEntity attachment = StorageAttachmentEntity.builder()
.refId(refId) .refId(refId)
.refType(refType) .refType(refType)
.originalName(file.getOriginalFilename()) .originalName(file.getOriginalFilename())
@ -92,28 +74,28 @@ public class MinioAttachmentService {
.projectId(projectId) .projectId(projectId)
.build(); .build();
return minioAttachmentRepository.save(attachment); return storageAttachmentRepository.save(attachment);
} }
} }
/** /**
* *
*/ */
public List<MinioAttachmentEntity> findAll() { public List<StorageAttachmentEntity> findAll() {
return minioAttachmentRepository.findAll(); return storageAttachmentRepository.findAll();
} }
/** /**
* ID * ID
*/ */
public Optional<MinioAttachmentEntity> findById(Long id) { public Optional<StorageAttachmentEntity> findById(Long id) {
return minioAttachmentRepository.findById(id); return storageAttachmentRepository.findById(id);
} }
/** /**
* + * +
*/ */
public Page<MinioAttachmentEntity> search(ProjectBaseSearchRequest request, String refType, Integer refId) { public Page<StorageAttachmentEntity> search(ProjectBaseSearchRequest request, String refType, Integer refId) {
int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0;
Pageable pageable = PageRequest.of( Pageable pageable = PageRequest.of(
@ -125,8 +107,8 @@ public class MinioAttachmentService {
LocalDate startDate = parseDate(request.getStartDate()); LocalDate startDate = parseDate(request.getStartDate());
LocalDate endDate = parseDate(request.getEndDate()); LocalDate endDate = parseDate(request.getEndDate());
Specification<MinioAttachmentEntity> spec = Specification<StorageAttachmentEntity> spec =
minioAttachmentSpecification.searchByConditions( storageAttachmentSpecification.searchByConditions(
refType, refType,
refId, refId,
request.getSearchType(), request.getSearchType(),
@ -141,7 +123,7 @@ public class MinioAttachmentService {
); );
} }
return minioAttachmentRepository.findAll(spec, pageable); return storageAttachmentRepository.findAll(spec, pageable);
} }
private LocalDate parseDate(String dateStr) { private LocalDate parseDate(String dateStr) {
@ -154,28 +136,23 @@ public class MinioAttachmentService {
} }
public byte[] downloadFile(String bucketName, String objectName) { public byte[] downloadFile(String bucketName, String objectName) {
try (InputStream is = minioClient.getObject( try {
GetObjectArgs.builder().bucket(bucketName).object(objectName).build() return storageProvider.downloadFileFromDefault(objectName);
)) {
return is.readAllBytes();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, e); throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName, e);
} }
} }
// YAML 텍스트 읽기 // YAML 텍스트 읽기
public String readYamlText(String bucketName, String objectName) { public String readYamlText(String bucketName, String objectName) {
try (InputStream is = minioClient.getObject( try {
GetObjectArgs.builder().bucket(bucketName).object(objectName).build() return storageProvider.readYamlTextFromDefault(objectName);
)) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("MinIO YAML 읽기 실패: " + objectName, e); throw new RuntimeException("YAML 읽기 실패: " + objectName, e);
} }
} }
public MinioAttachmentEntity updateFile( public StorageAttachmentEntity updateFile(
Long id, Long id,
Long projectId, // 추가 Long projectId, // 추가
MultipartFile file, MultipartFile file,
@ -185,13 +162,13 @@ public class MinioAttachmentService {
String regUserId String regUserId
) throws Exception { ) throws Exception {
// 기존 엔티티 조회 // 기존 엔티티 조회
MinioAttachmentEntity existing = minioAttachmentRepository.findById(id) StorageAttachmentEntity existing = storageAttachmentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("첨부파일을 찾을 수 없습니다. ID=" + id)); .orElseThrow(() -> new IllegalArgumentException("첨부파일을 찾을 수 없습니다. ID=" + id));
// 최신 버전 조회 // 최신 버전 조회
Integer latestVersion = minioAttachmentRepository Integer latestVersion = storageAttachmentRepository
.findTopByRefIdAndRefTypeOrderByVersionDesc(existing.getRefId(), existing.getRefType()) .findTopByRefIdAndRefTypeOrderByVersionDesc(existing.getRefId(), existing.getRefType())
.map(MinioAttachmentEntity::getVersion) .map(StorageAttachmentEntity::getVersion)
.orElse(0); .orElse(0);
int newVersion = latestVersion + 1; int newVersion = latestVersion + 1;
@ -203,18 +180,11 @@ public class MinioAttachmentService {
: path + "/" + storedName; : path + "/" + storedName;
try (InputStream is = file.getInputStream()) { try (InputStream is = file.getInputStream()) {
minioClient.putObject( storageProvider.uploadFileToDefault(objectName, is, file.getContentType(), file.getSize());
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(is, is.available(), -1)
.contentType(file.getContentType())
.build()
);
} }
// 새로운 엔티티 생성 (이전 데이터는 그대로 두고, 새로운 버전 생성) // 새로운 엔티티 생성 (이전 데이터는 그대로 두고, 새로운 버전 생성)
MinioAttachmentEntity newAttachment = MinioAttachmentEntity.builder() StorageAttachmentEntity newAttachment = StorageAttachmentEntity.builder()
.projectId(projectId) // 추가 .projectId(projectId) // 추가
.refId(existing.getRefId()) .refId(existing.getRefId())
.refType(existing.getRefType()) .refType(existing.getRefType())
@ -229,24 +199,24 @@ public class MinioAttachmentService {
.regUserId(regUserId) .regUserId(regUserId)
.build(); .build();
return minioAttachmentRepository.save(newAttachment); return storageAttachmentRepository.save(newAttachment);
} }
/** /**
* (DB , ) * (DB , )
*/ */
public MinioAttachmentEntity create(MinioAttachmentEntity entity) { public StorageAttachmentEntity create(StorageAttachmentEntity entity) {
return minioAttachmentRepository.save(entity); return storageAttachmentRepository.save(entity);
} }
/** /**
* *
*/ */
public Optional<MinioAttachmentEntity> update(Long id, MinioAttachmentEntity dto) { public Optional<StorageAttachmentEntity> update(Long id, StorageAttachmentEntity dto) {
return minioAttachmentRepository.findById(id) return storageAttachmentRepository.findById(id)
.map(entity -> { .map(entity -> {
BeanUtils.copyProperties(dto, entity, "id", "regDt"); // ID, 등록일 제외 BeanUtils.copyProperties(dto, entity, "id", "regDt"); // ID, 등록일 제외
return minioAttachmentRepository.save(entity); return storageAttachmentRepository.save(entity);
}); });
} }
@ -255,35 +225,30 @@ public class MinioAttachmentService {
*/ */
@Transactional @Transactional
public boolean delete(Long id) { public boolean delete(Long id) {
Optional<MinioAttachmentEntity> attachmentOpt = minioAttachmentRepository.findById(id); Optional<StorageAttachmentEntity> attachmentOpt = storageAttachmentRepository.findById(id);
if (attachmentOpt.isEmpty()) { if (attachmentOpt.isEmpty()) {
return false; return false;
} }
MinioAttachmentEntity attachment = attachmentOpt.get(); StorageAttachmentEntity attachment = attachmentOpt.get();
try { try {
// MinIO 파일 삭제 // 스토리지 파일 삭제
minioClient.removeObject( storageProvider.deleteFileFromDefault(attachment.getStoragePath());
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(attachment.getStoragePath())
.build()
);
// DB에서 삭제 // DB에서 삭제
minioAttachmentRepository.deleteById(id); storageAttachmentRepository.deleteById(id);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("MinIO 파일 삭제 실패: " + attachment.getStoragePath(), e); log.error("스토리지 파일 삭제 실패: " + attachment.getStoragePath(), e);
return false; return false;
} }
} }
/** /**
* MinIO URL * URL
*/ */
public String getFileUrl(String objectName) { public String getFileUrl(String objectName) {
return String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); return storageProvider.getFileUrl(null, objectName, null);
} }
} }

@ -0,0 +1,155 @@
package kr.re.etri.autoflow.service.storage;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import kr.re.etri.autoflow.common.MinioType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
@Service("storageProvider")
@ConditionalOnProperty(name = "storage.provider", havingValue = "minio", matchIfMissing = true)
public class MinioStorageProvider implements StorageProvider {
@Value("${minio.bucket:mlpipeline}")
private String defaultBucket;
@Value("${minio.endpoint}")
private String minioEndpoint;
private final MinioClient defaultMinioClient;
public MinioStorageProvider(MinioClient minioClient) {
this.defaultMinioClient = minioClient;
}
private MinioClient getClientByType(String type) {
if (type == null) return defaultMinioClient;
MinioType config = MinioType.of(type);
return MinioClient.builder()
.endpoint(config.getEndpoint())
.credentials(config.getAccessKey(), config.getSecretKey())
.build();
}
private String getBucketByType(String bucketName, String type) {
if (bucketName != null && !bucketName.isEmpty()) return bucketName;
if (type != null) {
return MinioType.of(type).getBucket();
}
return defaultBucket;
}
@Override
public void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception {
MinioClient client = getClientByType(type);
String targetBucket = getBucketByType(bucketName, type);
client.putObject(
PutObjectArgs.builder()
.bucket(targetBucket)
.object(objectName)
.stream(is, size, -1)
.contentType(contentType)
.build()
);
}
@Override
public void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception {
uploadFile(null, objectName, is, contentType, size, null);
}
@Override
public byte[] downloadFile(String bucketName, String objectName, String type) throws Exception {
MinioClient client = getClientByType(type);
String targetBucket = getBucketByType(bucketName, type);
try (InputStream is = client.getObject(
GetObjectArgs.builder()
.bucket(targetBucket)
.object(objectName)
.build()
)) {
return is.readAllBytes();
}
}
@Override
public byte[] downloadFileFromDefault(String objectName) throws Exception {
return downloadFile(null, objectName, null);
}
@Override
public File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception {
MinioClient client = getClientByType(type);
String targetBucket = getBucketByType(bucketName, type);
String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "")
.replaceAll("^/+", "").replaceAll("/+$", "");
String fileName = Paths.get(cleanObjectName).getFileName().toString();
File localDir = new File(localPath);
if (!localDir.exists()) localDir.mkdirs();
File localFile = new File(localDir, fileName);
try (InputStream is = client.getObject(
GetObjectArgs.builder()
.bucket(targetBucket)
.object(cleanObjectName)
.build()
);
FileOutputStream fos = new FileOutputStream(localFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
return localFile;
}
@Override
public String readYamlText(String bucketName, String objectName, String type) throws Exception {
byte[] bytes = downloadFile(bucketName, objectName, type);
return new String(bytes, StandardCharsets.UTF_8);
}
@Override
public String readYamlTextFromDefault(String objectName) throws Exception {
return readYamlText(null, objectName, null);
}
@Override
public void deleteFile(String bucketName, String objectName, String type) throws Exception {
MinioClient client = getClientByType(type);
String targetBucket = getBucketByType(bucketName, type);
client.removeObject(
RemoveObjectArgs.builder()
.bucket(targetBucket)
.object(objectName)
.build()
);
}
@Override
public void deleteFileFromDefault(String objectName) throws Exception {
deleteFile(null, objectName, null);
}
@Override
public String getFileUrl(String bucketName, String objectName, String type) {
String targetBucket = getBucketByType(bucketName, type);
String endpoint = (type != null) ? MinioType.of(type).getEndpoint() : minioEndpoint;
return String.format("%s/%s/%s", endpoint, targetBucket, objectName);
}
}

@ -0,0 +1,139 @@
package kr.re.etri.autoflow.service.storage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.nio.file.Paths;
@Service("storageProvider")
@ConditionalOnProperty(name = "storage.provider", havingValue = "s3")
public class S3StorageProvider implements StorageProvider {
@Value("${cloud.aws.s3.bucket:mlpipeline}")
private String defaultBucket;
private final S3Client s3Client;
public S3StorageProvider(S3Client s3Client) {
this.s3Client = s3Client;
}
private String getBucketByType(String bucketName, String type) {
if (bucketName != null && !bucketName.isEmpty()) return bucketName;
// 향후 application.properties에서 type별 s3 bucket을 가져오는 로직 추가 가능
// 현재는 defaultBucket 사용 (type 파라미터 무시)
return defaultBucket;
}
@Override
public void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception {
String targetBucket = getBucketByType(bucketName, type);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(targetBucket)
.key(objectName)
.contentType(contentType)
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(is, size));
}
@Override
public void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception {
uploadFile(null, objectName, is, contentType, size, null);
}
@Override
public byte[] downloadFile(String bucketName, String objectName, String type) throws Exception {
String targetBucket = getBucketByType(bucketName, type);
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(targetBucket)
.key(objectName)
.build();
try (ResponseInputStream<GetObjectResponse> is = s3Client.getObject(getObjectRequest)) {
return is.readAllBytes();
}
}
@Override
public byte[] downloadFileFromDefault(String objectName) throws Exception {
return downloadFile(null, objectName, null);
}
@Override
public File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception {
String targetBucket = getBucketByType(bucketName, type);
String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "")
.replaceAll("^/+", "").replaceAll("/+$", "");
String fileName = Paths.get(cleanObjectName).getFileName().toString();
File localDir = new File(localPath);
if (!localDir.exists()) localDir.mkdirs();
File localFile = new File(localDir, fileName);
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(targetBucket)
.key(cleanObjectName)
.build();
try (ResponseInputStream<GetObjectResponse> is = s3Client.getObject(getObjectRequest);
FileOutputStream fos = new FileOutputStream(localFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
return localFile;
}
@Override
public String readYamlText(String bucketName, String objectName, String type) throws Exception {
byte[] bytes = downloadFile(bucketName, objectName, type);
return new String(bytes, StandardCharsets.UTF_8);
}
@Override
public String readYamlTextFromDefault(String objectName) throws Exception {
return readYamlText(null, objectName, null);
}
@Override
public void deleteFile(String bucketName, String objectName, String type) throws Exception {
String targetBucket = getBucketByType(bucketName, type);
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(targetBucket)
.key(objectName)
.build();
s3Client.deleteObject(deleteObjectRequest);
}
@Override
public void deleteFileFromDefault(String objectName) throws Exception {
deleteFile(null, objectName, null);
}
@Override
public String getFileUrl(String bucketName, String objectName, String type) {
String targetBucket = getBucketByType(bucketName, type);
// S3 URL 형식
return String.format("https://%s.s3.amazonaws.com/%s", targetBucket, objectName);
}
}

@ -0,0 +1,57 @@
package kr.re.etri.autoflow.service.storage;
import java.io.File;
import java.io.InputStream;
public interface StorageProvider {
/**
*
*/
void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception;
/**
*
*/
void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception;
/**
* (byte[])
*/
byte[] downloadFile(String bucketName, String objectName, String type) throws Exception;
/**
*
*/
byte[] downloadFileFromDefault(String objectName) throws Exception;
/**
*
*/
File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception;
/**
* YAML
*/
String readYamlText(String bucketName, String objectName, String type) throws Exception;
/**
* YAML
*/
String readYamlTextFromDefault(String objectName) throws Exception;
/**
*
*/
void deleteFile(String bucketName, String objectName, String type) throws Exception;
/**
*
*/
void deleteFileFromDefault(String objectName) throws Exception;
/**
* URL ( )
*/
String getFileUrl(String bucketName, String objectName, String type);
}

@ -6,7 +6,7 @@ import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.Metamodel;
import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import kr.re.etri.autoflow.entity.StorageAttachmentEntity;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
@ -20,7 +20,7 @@ import java.util.stream.Collectors;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MinioAttachmentSpecification { public class StorageAttachmentSpecification {
private final EntityManager entityManager; private final EntityManager entityManager;
@ -29,7 +29,7 @@ public class MinioAttachmentSpecification {
@PostConstruct @PostConstruct
public void init() { public void init() {
Metamodel metamodel = entityManager.getMetamodel(); Metamodel metamodel = entityManager.getMetamodel();
EntityType<MinioAttachmentEntity> entityType = metamodel.entity(MinioAttachmentEntity.class); EntityType<StorageAttachmentEntity> entityType = metamodel.entity(StorageAttachmentEntity.class);
// 문자열 타입 필드명만 추출 // 문자열 타입 필드명만 추출
stringFields = entityType.getAttributes().stream() stringFields = entityType.getAttributes().stream()
@ -37,10 +37,10 @@ public class MinioAttachmentSpecification {
.map(Attribute::getName) .map(Attribute::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
log.info("MinioAttachmentEntity string fields: {}", stringFields); log.info("StorageAttachmentEntity string fields: {}", stringFields);
} }
public Specification<MinioAttachmentEntity> searchByConditions( public Specification<StorageAttachmentEntity> searchByConditions(
String refType, String refType,
Integer refId, Integer refId,
String searchType, String searchType,

@ -50,6 +50,9 @@ spring.servlet.multipart.max-request-size=500MB
springdoc.swagger-ui.tags-sorter=alpha springdoc.swagger-ui.tags-sorter=alpha
# Storage Provider (minio or s3)
storage.provider=minio
# MinIO ?? # MinIO ??
minio.endpoint=http://192.168.10.135:31795 minio.endpoint=http://192.168.10.135:31795
minio.access-key=minio minio.access-key=minio
@ -78,3 +81,4 @@ external.auth.sw-search-url=https://a659120d3e2ff43ff94087b29396fd96-1057696791.
cloud.aws.region.static=ap-northeast-2 cloud.aws.region.static=ap-northeast-2
cloud.aws.credentials.access-key=AKIA2UC3EPERDDR4UOWN cloud.aws.credentials.access-key=AKIA2UC3EPERDDR4UOWN
cloud.aws.credentials.secret-key=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 cloud.aws.credentials.secret-key=Ps7ShmtcemhhTmZi+aUCpSpfZxjqFGyy51qgDSGD
cloud.aws.s3.bucket=mlpipeline
Loading…
Cancel
Save