From 0a59aa1523f2782d4b356907980a2ea3652a574a Mon Sep 17 00:00:00 2001 From: bjkim Date: Mon, 20 Oct 2025 16:57:22 +0900 Subject: [PATCH] =?UTF-8?q?[ADD]=20MinIO=20=ED=99=9C=EC=9A=A9=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C,=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EA=B4=80=EB=A0=A8=20Service=20=EB=B0=8F?= =?UTF-8?q?=20Controller=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84,=20appl?= =?UTF-8?q?ication.properties=20=ED=8C=8C=EC=9D=BC=EC=9D=98=20MLflow=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/re/etri/autoflow/common/MinioType.java | 27 +++ .../DynamicMinioAttachmentController.java | 136 ++++++++++++ .../controllers/MlflowController.java | 2 +- .../DynamicMinioAttachmentService.java | 205 ++++++++++++++++++ src/main/resources/application.properties | 2 +- 5 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/main/java/kr/re/etri/autoflow/common/MinioType.java create mode 100644 src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java create mode 100644 src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java diff --git a/src/main/java/kr/re/etri/autoflow/common/MinioType.java b/src/main/java/kr/re/etri/autoflow/common/MinioType.java new file mode 100644 index 0000000..308b217 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/common/MinioType.java @@ -0,0 +1,27 @@ +package kr.re.etri.autoflow.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; + +@AllArgsConstructor +@Getter +public enum MinioType { + TYPE1("mlpipeline", "http://192.168.10.135:31795", "minio", "minio123"), + TYPE2("mlflow-artifacts", "http://192.168.10.135:31000", "admin", "YAt76ajBtr"); + + private final String bucket; + private final String endpoint; + private final String accessKey; + private final String secretKey; + + public static final Map TYPE_MAP = Map.of( + "type1", TYPE1, + "type2", TYPE2 + ); + + public static MinioType of(String type) { + return TYPE_MAP.getOrDefault(type.toLowerCase(), TYPE1); + } +} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java new file mode 100644 index 0000000..d376dc2 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java @@ -0,0 +1,136 @@ +package kr.re.etri.autoflow.controllers; + +import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; +import kr.re.etri.autoflow.service.DynamicMinioAttachmentService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/minio") +@RequiredArgsConstructor +public class DynamicMinioAttachmentController { + + private final DynamicMinioAttachmentService minioService; + + /** + * 파일 업로드 + */ + @PostMapping("/upload") + public ResponseEntity> uploadFile( + @RequestParam MultipartFile file, + @RequestParam(required = false) String path, + @RequestParam Long refId, + @RequestParam(defaultValue = "DATASET") String refType, + @RequestParam(required = false) String title, + @RequestParam(required = false) String description, + @RequestParam(defaultValue = "1") Integer version, + @RequestParam String regUserId, + @RequestParam Long projectId, + @RequestParam(defaultValue = "type1") String type + ) { + try { + MinioAttachmentEntity attachment = minioService.uploadFile( + file, path, refId, refType, title, description, version, regUserId, projectId, type + ); + + Map response = new HashMap<>(); + response.put("attachment", attachment); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("파일 업로드 실패", e); + return ResponseEntity.internalServerError() + .body(Map.of("error", e.getMessage())); + } + } + + /** + * 파일 다운로드 + */ + @GetMapping("/download") + public ResponseEntity downloadFile( + @RequestParam String objectName, + @RequestParam(defaultValue = "type1") String type + ) { + try { + byte[] bytes = minioService.downloadFile(objectName, type); + + String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } catch (Exception e) { + log.error("파일 다운로드 실패", e); + return ResponseEntity.internalServerError().build(); + } + } + + /** + * YAML 텍스트 읽기 + */ + @GetMapping(value = "/readYamlText", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity readYamlText( + @RequestParam String objectName, + @RequestParam(defaultValue = "type1") String type + ) { + try { + String content = minioService.readYamlText(objectName, type); + return ResponseEntity.ok(content); + } catch (Exception e) { + log.error("MinIO YAML 읽기 실패: {}", objectName, e); + return ResponseEntity.internalServerError() + .body("Error reading file: " + e.getMessage()); + } + } + + /** + * 파일 삭제 + */ + @DeleteMapping("/delete") + public ResponseEntity> deleteFile( + @RequestParam Long id, + @RequestParam(defaultValue = "type1") String type + ) { + boolean result = minioService.deleteFile(id, type); + return ResponseEntity.ok(Map.of("deleted", result)); + } + + /** + * 전체 조회 + */ + @GetMapping("/list") + public ResponseEntity> listAll() { + List list = minioService.findAll(); + return ResponseEntity.ok(list); + } + + /** + * 검색 + 페이지네이션 + */ + @PostMapping("/search") + public ResponseEntity> search( + @RequestBody ProjectBaseSearchRequest request, + @RequestParam String refType, + @RequestParam Integer refId + ) { + Page page = minioService.search(request, refType, refId); + return ResponseEntity.ok(page); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java b/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java index 7c9a452..4e118f8 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java @@ -23,7 +23,7 @@ public class MlflowController { public MlflowController() { this.webClient = WebClient.builder() .baseUrl("http://192.168.10.135:30128/api/2.0/mlflow") - .defaultHeaders(headers -> headers.setBasicAuth("user", "aBZ1RzEDGutI")) + .defaultHeaders(headers -> headers.setBasicAuth("user", "WjWjIi13KEkO")) .build(); } diff --git a/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java new file mode 100644 index 0000000..2eb3823 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java @@ -0,0 +1,205 @@ +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.InputStream; +import java.nio.charset.StandardCharsets; +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 (InputStream is = client.getObject( + GetObjectArgs.builder().bucket(bucketName).object(objectName).build() + )) { + return is.readAllBytes(); + } catch (Exception e) { + throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, 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 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 findAll() { + return minioAttachmentRepository.findAll(); + } + + public Optional findById(Long id) { + return minioAttachmentRepository.findById(id); + } + + public Page 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 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); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b91dde8..488d8a1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -63,7 +63,7 @@ kubeflow.url=http://192.168.10.135:32473/ # MLflow mlflow.url=http://192.168.10.135:30128/ mlflow.user=user -mlflow.password=aBZ1RzEDGutI +mlflow.password=WjWjIi13KEkO server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true