diff --git a/build.gradle.kts b/build.gradle.kts index 7997bb2..20a4644 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") - // + implementation("org.jsoup:jsoup:1.16.1") + + compileOnly("org.projectlombok:lombok:1.18.38") annotationProcessor("org.projectlombok:lombok:1.18.38") testCompileOnly("org.projectlombok:lombok:1.18.38") @@ -55,6 +57,11 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") + //배포시 주석 처리 해야함(sql 디버깅용) + implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.12.0") + + implementation("io.minio:minio:8.5.17") + } // Java 컴파일 인코딩 및 파라미터 리플렉션 지원 diff --git a/src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java b/src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java new file mode 100644 index 0000000..3c01687 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java @@ -0,0 +1,27 @@ +package kr.re.etri.autoflow.common; + +import io.minio.MinioClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MinIOConfig { + @Value("${minio.endpoint}") + private String endpoint; + + @Value("${minio.access-key}") + private String accessKey; + + @Value("${minio.secret-key}") + private String secretKey; + + @Bean + public MinioClient minioClient() { + return MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } + +} diff --git a/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java b/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java new file mode 100644 index 0000000..1d7884f --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java @@ -0,0 +1,172 @@ +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +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.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping("/api/minio") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "MinIO Controller", description = "MinIO 버킷/파일 관리 API") +public class MinIOController { + private final MinioClient minioClient; + + @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 버킷에 업로드합니다. path를 지정하면 하위 폴더에 저장 가능합니다.") + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public String uploadFile( + @Parameter(description = "업로드할 파일") + @RequestPart("file") MultipartFile file, + @Parameter(description = "저장 경로 (선택)") + @RequestPart(value = "path", required = false) String path + ) { + try (InputStream is = file.getInputStream()) { + String objectName = (path == null || path.isEmpty()) + ? file.getOriginalFilename() + : path + "/" + file.getOriginalFilename(); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .stream(is, is.available(), -1) + .contentType(file.getContentType()) + .build() + ); + + // MinIO 경로 URL 생성 (NodePort 기반) + String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); + + return "파일 업로드 성공: " + minioUrl; + } catch (Exception e) { + log.error("파일 업로드 실패", e); + return "파일 업로드 실패: " + e.getMessage(); + } + } + + @Operation(summary = "다중 파일 업로드", description = "MultipartFile 배열을 MinIO 버킷에 업로드합니다. path를 지정하면 하위 폴더에 저장 가능합니다.") + @PostMapping(value = "/upload-multiple", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public List uploadMultipleFiles( + @Parameter(description = "업로드할 파일들") + @RequestPart("files") MultipartFile[] files, + @Parameter(description = "저장 경로 (선택)") + @RequestPart(value = "path", required = false) String path + ) { + List uploadedUrls = new ArrayList<>(); + for (MultipartFile file : files) { + try (InputStream is = file.getInputStream()) { + String objectName = (path == null || path.isEmpty()) + ? file.getOriginalFilename() + : path + "/" + file.getOriginalFilename(); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .stream(is, is.available(), -1) + .contentType(file.getContentType()) + .build() + ); + + String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); + uploadedUrls.add(minioUrl); + + } catch (Exception e) { + log.error("파일 업로드 실패: {}", file.getOriginalFilename(), e); + uploadedUrls.add("업로드 실패: " + file.getOriginalFilename() + " (" + e.getMessage() + ")"); + } + } + return uploadedUrls; + } + + + @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(); + } + } +} \ No newline at end of file