package kr.re.etri.autoflow.controllers; import io.minio.*; import io.minio.messages.Item; 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.DatasetAttachmentEntity; import kr.re.etri.autoflow.repository.DatasetAttachmentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @RestController @RequestMapping("/api/minio") @RequiredArgsConstructor @Slf4j @Tag(name = "MinIO Controller", description = "MinIO 버킷/파일 관리 API") public class MinIOController { private final MinioClient minioClient; private final DatasetAttachmentRepository datasetAttachmentRepository; @Value("${minio.bucket}") private String bucketName; @Value("${minio.endpoint}") private String minioEndpoint; @Operation(summary = "버킷 존재 여부 체크", description = "기본 버킷이 존재하는지 확인합니다.") @GetMapping("/bucket/check") public boolean bucketExists() { try { return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } catch (Exception e) { log.error("버킷 체크 실패", e); return false; } } @Operation(summary = "파일 목록 조회", description = "버킷 내 파일 목록 조회, recursive 옵션으로 하위 폴더 포함 여부 지정 가능") @GetMapping("/files") public List listFiles( @Parameter(description = "파일 경로 접두사") @RequestParam(required = false) String prefix, @Parameter(description = "하위 폴더 포함 여부") @RequestParam(required = false, defaultValue = "false") boolean recursive ) { List files = new ArrayList<>(); try { Iterable> results = minioClient.listObjects( ListObjectsArgs.builder() .bucket(bucketName) .prefix(prefix) .recursive(recursive) .build() ); for (Result result : results) { Item item = result.get(); files.add(item.objectName()); } } catch (Exception e) { log.error("파일 목록 조회 실패", e); } return files; } @Operation(summary = "파일 업로드", description = "MultipartFile을 MinIO 버킷에 업로드하고 DB에 기록합니다.") @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity uploadFile( @Parameter(description = "업로드할 파일") @RequestPart("file") MultipartFile file, @Parameter(description = "저장 경로 (선택)") @RequestPart(value = "path", required = false) String path, @RequestParam(value = "title", required = false, defaultValue = "배터리 퍼센트 데이터 셋") String title, @RequestParam(value = "description", required = false, defaultValue = "배터리 퍼센트 데이터 모음") String description, @RequestParam(value = "version", required = false, defaultValue = "1") Integer version, @RequestParam(value = "regUserId") String regUserId, @RequestParam(value = "refId") Long refId ) { try (InputStream is = file.getInputStream()) { String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename(); String objectName = (path == null || path.isEmpty()) ? storedName : path + "/" + storedName; // MinIO 업로드 minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(is, is.available(), -1) .contentType(file.getContentType()) .build() ); // DB에 저장 DatasetAttachmentEntity attachment = DatasetAttachmentEntity.builder() .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) .refId(refId) .build(); DatasetAttachmentEntity saved = datasetAttachmentRepository.save(attachment); return ResponseEntity.ok(saved); } catch (Exception e) { log.error("파일 업로드 실패", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @Operation(summary = "다중 파일 업로드", description = "MultipartFile들을 MinIO 버킷에 업로드하고 DB에 기록합니다.") @PostMapping(value = "/upload-multiple", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadFiles( @Parameter(description = "업로드할 파일들") @RequestPart("files") List files, @Parameter(description = "저장 경로 (선택)") @RequestPart(value = "path", required = false) String path, @RequestParam(value = "title", required = false, defaultValue = "배터리 퍼센트 데이터 셋") String title, @RequestParam(value = "description", required = false, defaultValue = "배터리 퍼센트 데이터 모음") String description, @RequestParam(value = "version", required = false, defaultValue = "1") Integer version, @RequestParam(value = "regUserId") String regUserId ) { List savedAttachments = new ArrayList<>(); for (MultipartFile file : files) { try (InputStream is = file.getInputStream()) { String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename(); String objectName = (path == null || path.isEmpty()) ? storedName : path + "/" + storedName; // MinIO 업로드 minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(is, is.available(), -1) .contentType(file.getContentType()) .build() ); // DB에 저장 DatasetAttachmentEntity attachment = DatasetAttachmentEntity.builder() .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) .regDt(LocalDateTime.now()) .build(); DatasetAttachmentEntity saved = datasetAttachmentRepository.save(attachment); savedAttachments.add(saved); } catch (Exception e) { log.error("파일 업로드 실패: " + file.getOriginalFilename(), e); } } if (savedAttachments.isEmpty()) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } return ResponseEntity.ok(savedAttachments); } @Operation(summary = "파일 다운로드", description = "MinIO에서 파일을 다운로드합니다.") @GetMapping("/download") public ResponseEntity downloadFile(@RequestParam String objectName) { try (InputStream is = minioClient.getObject( GetObjectArgs.builder().bucket(bucketName).object(objectName).build() )) { byte[] bytes = is.readAllBytes(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + objectName + "\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(bytes); } catch (Exception e) { log.error("파일 다운로드 실패", e); return ResponseEntity.internalServerError().build(); } } @Operation(summary = "파일 삭제", description = "MinIO 버킷에서 파일을 삭제합니다.") @DeleteMapping("/delete") public String deleteFile(@RequestParam String objectName) { try { minioClient.removeObject( RemoveObjectArgs.builder() .bucket(bucketName) .object(objectName) .build() ); return "삭제 성공: " + objectName; } catch (Exception e) { log.error("파일 삭제 실패", e); return "삭제 실패: " + e.getMessage(); } } }