diff --git a/src/main/java/kr/re/etri/autoflow/controllers/AttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/AttachmentController.java new file mode 100644 index 0000000..2db0877 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/AttachmentController.java @@ -0,0 +1,99 @@ +package kr.re.etri.autoflow.controllers; + +import io.minio.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.payload.request.BaseSearchRequest; + +import kr.re.etri.autoflow.service.MinioAttachmentService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +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.util.*; + +@RestController +@RequestMapping("/api/attachments") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "첨부파일", description = "MinIO 첨부파일 관리 API") +public class AttachmentController { + + private final MinioAttachmentService minioAttachmentService; + + @Operation(summary = "첨부파일 전체 조회") + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok(minioAttachmentService.findAll()); + } + + @Operation(summary = "ID로 첨부파일 조회") + @GetMapping("/{id}") + public ResponseEntity getById( + @Parameter(description = "첨부파일 ID", required = true) + @PathVariable("id") Long id) { + return minioAttachmentService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation(summary = "검색 및 페이지네이션 첨부파일 목록 조회") + @GetMapping("/search") + public ResponseEntity> search( + @ParameterObject @ModelAttribute BaseSearchRequest request, + @RequestParam(value = "refType", required = false) String refType) { + Page page = minioAttachmentService.search(request, refType); + return ResponseEntity.ok(page); + } + + @Operation(summary = "첨부파일 삭제") + @DeleteMapping("/{id}") + public ResponseEntity delete( + @Parameter(description = "첨부파일 ID", required = true) + @PathVariable("id") Long id) { + if (minioAttachmentService.delete(id)) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.notFound().build(); + } + + @Operation(summary = "파일 업로드", description = "MultipartFile을 MinIO 버킷에 업로드하고 DB에 기록합니다.") + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> uploadFile( + @Parameter(description = "업로드할 파일") @RequestPart("file") MultipartFile file, + @RequestPart(value = "path", required = false) String path, + @RequestParam(value = "refId", required = false) Long refId, + @RequestParam(value = "refType", required = false, defaultValue = "TRAINING_SCRIPT") String refType, + @RequestParam(value = "title", required = false) String title, + @RequestParam(value = "description", required = false) String description, + @RequestParam(value = "version", required = false, defaultValue = "1") Integer version, + @RequestParam(value = "regUserId") String regUserId + ) { + try { + MinioAttachmentEntity saved = minioAttachmentService.uploadFile( + file, path, refId, refType, title, description, version, regUserId + ); + + Map response = new HashMap<>(); + response.put("attachment", saved); + response.put("minioUrl", minioAttachmentService.getFileUrl(saved.getStoragePath())); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + log.error("파일 업로드 실패", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java b/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java deleted file mode 100644 index ac3fc00..0000000 --- a/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java +++ /dev/null @@ -1,134 +0,0 @@ -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.enums.ParameterIn; -import io.swagger.v3.oas.annotations.tags.Tag; -import kr.re.etri.autoflow.entity.DatasetAttachmentEntity; -import kr.re.etri.autoflow.repository.DatasetAttachmentRepository; -import kr.re.etri.autoflow.service.DataGroupService; -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.*; - -@RestController -@RequestMapping("/api/dataset") -@RequiredArgsConstructor -@Slf4j -@Tag(name = "데이터셋 업로드", description = "MinIO 버킷/파일 관리 API") -public class DatasetController { - private final MinioClient minioClient; - private final DatasetAttachmentRepository datasetAttachmentRepository; - private final DataGroupService dataGroupService; - - - @Value("${minio.bucket}") - private String bucketName; - - @Value("${minio.endpoint}") - private String minioEndpoint; - - @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); - - // MinIO URL 생성 - String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); - - Map response = new HashMap<>(); - response.put("attachment", saved); - response.put("minioUrl", minioUrl); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("파일 업로드 실패", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "데이터셋 삭제") - @DeleteMapping("/{id}") - public ResponseEntity deleteDataset( - @Parameter(description = "삭제할 데이터셋 ID", required = true, in = ParameterIn.PATH) - @PathVariable("id") Long id) { - try { - // 1. DB에서 데이터셋 조회 - Optional optional = datasetAttachmentRepository.findById(id); - if (optional.isEmpty()) { - return ResponseEntity.notFound().build(); - } - DatasetAttachmentEntity attachment = optional.get(); - - // 2. MinIO에서 실제 파일 삭제 - minioClient.removeObject( - RemoveObjectArgs.builder() - .bucket(bucketName) - .object(attachment.getStoragePath()) - .build() - ); - - // 3. DB 레코드 삭제 - datasetAttachmentRepository.deleteById(id); - - return ResponseEntity.noContent().build(); - - } catch (Exception e) { - log.error("데이터셋 삭제 실패", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } -} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java b/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java deleted file mode 100644 index 1336942..0000000 --- a/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java +++ /dev/null @@ -1,231 +0,0 @@ -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.*; - -@RestController -@RequestMapping("/api/minio") -@RequiredArgsConstructor -@Slf4j -@Tag(name = "MinIO 스토리지 관련", 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); - - // MinIO URL 생성 - String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); - - Map response = new HashMap<>(); - response.put("attachment", saved); - response.put("minioUrl", minioUrl); - - return ResponseEntity.ok(response); - - } 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(); - } - } -} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/controllers/TrainingScriptAttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/TrainingScriptAttachmentController.java deleted file mode 100644 index 815f285..0000000 --- a/src/main/java/kr/re/etri/autoflow/controllers/TrainingScriptAttachmentController.java +++ /dev/null @@ -1,140 +0,0 @@ -package kr.re.etri.autoflow.controllers; - -import io.minio.*; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.tags.Tag; -import kr.re.etri.autoflow.entity.TrainingScriptAttachmentEntity; -import kr.re.etri.autoflow.payload.request.BaseSearchRequest; -import kr.re.etri.autoflow.repository.TrainingScriptAttachmentRepository; -import kr.re.etri.autoflow.service.TrainingScriptAttachmentService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -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.util.*; - -@RestController -@RequestMapping("/api/trainingscript") -@RequiredArgsConstructor -@Slf4j -@Tag(name = "트레이닝 스크립트", description = "트레이닝 스크립트 MinIO 버킷/파일 관리 API") -public class TrainingScriptAttachmentController { - private final MinioClient minioClient; - private final TrainingScriptAttachmentRepository trainingScriptAttachmentRepository; - private final TrainingScriptAttachmentService trainingScriptAttachmentService; - - - - @Value("${minio.bucket}") - private String bucketName; - - @Value("${minio.endpoint}") - private String minioEndpoint; - - - - @Operation(summary = "전체 트레이닝 스크립트 목록 조회") - @GetMapping - public ResponseEntity> getAllTrainingScripts() { - return ResponseEntity.ok(trainingScriptAttachmentService.findAll()); - } - - @Operation(summary = "ID로 트레이닝 스크립트 조회") - @GetMapping("/{id}") - public ResponseEntity getTrainingScriptById( - @Parameter(description = "조회할 트레이닝 스크립트 ID", required = true, in = ParameterIn.PATH) - @PathVariable("id") Long id) { - - return trainingScriptAttachmentService.findById(id) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - @Operation(summary = "검색 및 페이지네이션 트레이닝 스크립트 목록 조회") - @GetMapping("/search") - public ResponseEntity> searchTrainingScripts( - @ParameterObject @ModelAttribute BaseSearchRequest request) { - Page page = trainingScriptAttachmentService.search(request); - return ResponseEntity.ok(page); - } - - @Operation(summary = "트레이닝 스크립트 삭제") - @DeleteMapping("/{id}") - public ResponseEntity deleteTrainingScript( - @Parameter(description = "삭제할 트레이닝 스크립트 ID", required = true, in = ParameterIn.PATH) - @PathVariable("id") Long id) { - if (trainingScriptAttachmentService.delete(id)) { - return ResponseEntity.noContent().build(); - } - return ResponseEntity.notFound().build(); - } - - - @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 - ) { - 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에 저장 - TrainingScriptAttachmentEntity attachment = TrainingScriptAttachmentEntity.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) - .build(); - - TrainingScriptAttachmentEntity saved = trainingScriptAttachmentRepository.save(attachment); - - // MinIO URL 생성 - String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); - - Map response = new HashMap<>(); - response.put("attachment", saved); - response.put("minioUrl", minioUrl); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - log.error("파일 업로드 실패", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } -} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/entity/DataGroupEntity.java b/src/main/java/kr/re/etri/autoflow/entity/DataGroupEntity.java index 759042d..31160c6 100644 --- a/src/main/java/kr/re/etri/autoflow/entity/DataGroupEntity.java +++ b/src/main/java/kr/re/etri/autoflow/entity/DataGroupEntity.java @@ -69,8 +69,4 @@ public class DataGroupEntity { @Schema(description = "프로젝트 아이디", example = "1", defaultValue = "0") @Column(nullable = false) private Long projectId; - - @OneToMany - @JoinColumn(name = "refId", referencedColumnName = "id", insertable = false, updatable = false) - private List files = new ArrayList<>(); } diff --git a/src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java b/src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java similarity index 86% rename from src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java rename to src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java index 35cd5b6..eb26c10 100644 --- a/src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java +++ b/src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java @@ -1,6 +1,5 @@ package kr.re.etri.autoflow.entity; -import com.fasterxml.jackson.annotation.JsonManagedReference; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import lombok.*; @@ -9,11 +8,9 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -@Schema(description = "MinIO 전용 첨부파일") -@Comment("MinIO 전용 첨부파일") +@Schema(description = "MinIO 첨부파일 (Dataset/TrainingScript 통합)") +@Comment("MinIO 첨부파일") @Entity @EntityListeners(AuditingEntityListener.class) @Table(name = "tb_minio_attachment") @@ -22,7 +19,7 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder -public class DatasetAttachmentEntity { +public class MinioAttachmentEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,9 +28,15 @@ public class DatasetAttachmentEntity { private Long id; @Schema(description = "연관 엔티티 ID", example = "1") + @Comment("연관 엔티티 ID") @Column(nullable = false) private Long refId; + @Schema(description = "첨부파일 종류 (DATASET / SCRIPT)", example = "DATASET") + @Comment("첨부파일 종류") + @Column(nullable = false, length = 50) + private String refType; // 구분자 (예: DATASET, TRAINING_SCRIPT) + @Schema(description = "원본 파일명", example = "step1.yaml") @Comment("원본 파일명") @Column(nullable = false, length = 255) diff --git a/src/main/java/kr/re/etri/autoflow/entity/TrainingScriptAttachmentEntity.java b/src/main/java/kr/re/etri/autoflow/entity/TrainingScriptAttachmentEntity.java deleted file mode 100644 index 46833d0..0000000 --- a/src/main/java/kr/re/etri/autoflow/entity/TrainingScriptAttachmentEntity.java +++ /dev/null @@ -1,80 +0,0 @@ -package kr.re.etri.autoflow.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.Comment; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Schema(description = "MinIO 전용 첨부파일") -@Comment("MinIO 전용 첨부파일") -@Entity -@EntityListeners(AuditingEntityListener.class) -@Table(name = "tb_trainingscript_attachment") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class TrainingScriptAttachmentEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "첨부파일 ID", example = "1") - @Comment("첨부파일 ID") - private Long id; - - @Schema(description = "원본 파일명", example = "step1.yaml") - @Comment("원본 파일명") - @Column(nullable = false, length = 255) - private String originalName; - - @Schema(description = "저장된 파일명(UUID + ver)", example = "a1b2c3d4-step1-ver.1.yaml") - @Comment("저장된 파일명") - @Column(nullable = false, length = 255) - private String storedName; - - @Schema(description = "MIME 타입", example = "application/x-yaml") - @Comment("MIME 타입") - @Column(nullable = false, length = 100) - private String contentType; - - @Schema(description = "파일 크기(byte)", example = "2048") - @Comment("파일 크기") - @Column(nullable = false) - private Long size; - - @Schema(description = "스토리지 경로", example = "/uploads/step1-ver.1.yaml") - @Comment("스토리지 경로") - @Column(nullable = false, length = 500) - private String storagePath; - - @Schema(description = "업로더 ID", example = "admin") - @Comment("업로더 ID") - @Column(nullable = false, length = 50) - private String regUserId; - - @Schema(description = "업로드 일시", example = "2025-09-17T15:00:00") - @CreatedDate - @Comment("업로드 일시") - @Column(nullable = false, updatable = false) - private LocalDateTime regDt; - - @Schema(description = "파일 제목", example = "자율주행차량 데이터 셋") - @Comment("파일 제목") - @Column(length = 200) - private String title; - - @Schema(description = "파일 버전", example = "1") - @Comment("파일 버전") - @Column(nullable = false) - private Integer version; - - @Schema(description = "파일 설명", example = "자율주행차량 데이터 모음집입니다.") - @Comment("파일 설명") - @Column(length = 1000) - private String description; -} diff --git a/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java deleted file mode 100644 index d308048..0000000 --- a/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package kr.re.etri.autoflow.repository; - -import kr.re.etri.autoflow.entity.DatasetAttachmentEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface DatasetAttachmentRepository extends JpaRepository { -} diff --git a/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java new file mode 100644 index 0000000..2290520 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java @@ -0,0 +1,13 @@ +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; + +@Repository +public interface MinioAttachmentRepository extends JpaRepository, JpaSpecificationExecutor { + +} diff --git a/src/main/java/kr/re/etri/autoflow/repository/TrainingScriptAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/TrainingScriptAttachmentRepository.java deleted file mode 100644 index 7714b74..0000000 --- a/src/main/java/kr/re/etri/autoflow/repository/TrainingScriptAttachmentRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package kr.re.etri.autoflow.repository; -import kr.re.etri.autoflow.entity.TrainingScriptAttachmentEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; - -public interface TrainingScriptAttachmentRepository extends JpaRepository, JpaSpecificationExecutor { -} diff --git a/src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java new file mode 100644 index 0000000..a7c86d9 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java @@ -0,0 +1,182 @@ +package kr.re.etri.autoflow.service; + +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import jakarta.transaction.Transactional; +import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.payload.request.BaseSearchRequest; +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.beans.factory.annotation.Value; +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.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 MinioAttachmentService { + + private final MinioClient minioClient; + private final MinioAttachmentRepository minioAttachmentRepository; + private final MinioAttachmentSpecification minioAttachmentSpecification; + + @Value("${minio.bucket}") + private String bucketName; + + @Value("${minio.endpoint}") + private String minioEndpoint; + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * 파일 업로드 & DB 저장 + */ + public MinioAttachmentEntity uploadFile(MultipartFile file, + String path, + Long refId, + String refType, + String title, + String description, + Integer version, + String regUserId) throws Exception { + 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 저장 + 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) + .build(); + + return minioAttachmentRepository.save(attachment); + } + } + + /** + * 전체 조회 + */ + @Transactional + public List findAll() { + return minioAttachmentRepository.findAll(); + } + + /** + * ID 조회 + */ + @Transactional + public Optional findById(Long id) { + return minioAttachmentRepository.findById(id); + } + + /** + * 검색 + 페이지네이션 + */ + public Page search(BaseSearchRequest request, String refType) { + 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, + request.getSearchType(), + request.getKeyword(), + startDate, + endDate + ); + + 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); + } + } + + /** + * 생성 (DB만 저장, 파일은 따로 업로드된 경우) + */ + @Transactional + public MinioAttachmentEntity create(MinioAttachmentEntity entity) { + return minioAttachmentRepository.save(entity); + } + + /** + * 업데이트 + */ + @Transactional + public Optional update(Long id, MinioAttachmentEntity dto) { + return minioAttachmentRepository.findById(id) + .map(entity -> { + BeanUtils.copyProperties(dto, entity, "id", "regDt"); // ID, 등록일 제외 + return minioAttachmentRepository.save(entity); + }); + } + + /** + * 삭제 + */ + @Transactional + public boolean delete(Long id) { + if (!minioAttachmentRepository.existsById(id)) { + return false; + } + minioAttachmentRepository.deleteById(id); + return true; + } + + /** + * MinIO URL 반환 + */ + public String getFileUrl(String objectName) { + return String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/service/TrainingScriptAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/TrainingScriptAttachmentService.java deleted file mode 100644 index 51ce149..0000000 --- a/src/main/java/kr/re/etri/autoflow/service/TrainingScriptAttachmentService.java +++ /dev/null @@ -1,96 +0,0 @@ -package kr.re.etri.autoflow.service; - -import kr.re.etri.autoflow.entity.TrainingScriptAttachmentEntity; -import kr.re.etri.autoflow.payload.request.BaseSearchRequest; -import kr.re.etri.autoflow.payload.request.ProjectRequest; -import kr.re.etri.autoflow.repository.TrainingScriptAttachmentRepository; -import kr.re.etri.autoflow.specification.TrainingScriptAttachmentSpecification; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.BeanUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class TrainingScriptAttachmentService { - - private final TrainingScriptAttachmentRepository trainingScriptAttachmentRepository; - - private final TrainingScriptAttachmentSpecification trainingScriptAttachmentSpecification; - - private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - public List findAll() { - return trainingScriptAttachmentRepository.findAll(); - } - - public Optional findById(Long id) { - return trainingScriptAttachmentRepository.findById(id); - } - - public Page search(BaseSearchRequest request) { - int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; - - Pageable pageable = PageRequest.of( - pageIndex, - request.getSize(), - Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortField()) - ); - - LocalDate startDate = parseDate(request.getStartDate()); - LocalDate endDate = parseDate(request.getEndDate()); - - Specification spec = trainingScriptAttachmentSpecification.searchByConditions( - request.getSearchType(), - request.getKeyword(), - startDate, - endDate - ); - - return trainingScriptAttachmentRepository.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); - } - } - - @Transactional - public TrainingScriptAttachmentEntity create(TrainingScriptAttachmentEntity project) { - return trainingScriptAttachmentRepository.save(project); - } - - @Transactional - public Optional update(Long id, ProjectRequest dto) { - return trainingScriptAttachmentRepository.findById(id) - .map(project -> { - BeanUtils.copyProperties(dto, project); - return trainingScriptAttachmentRepository.save(project); - }); - } - - @Transactional - public boolean delete(Long id) { - if (!trainingScriptAttachmentRepository.existsById(id)) { - return false; - } - trainingScriptAttachmentRepository.deleteById(id); - return true; - } -} diff --git a/src/main/java/kr/re/etri/autoflow/specification/TrainingScriptAttachmentSpecification.java b/src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java similarity index 60% rename from src/main/java/kr/re/etri/autoflow/specification/TrainingScriptAttachmentSpecification.java rename to src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java index b927984..d9df7a0 100644 --- a/src/main/java/kr/re/etri/autoflow/specification/TrainingScriptAttachmentSpecification.java +++ b/src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java @@ -6,7 +6,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; -import kr.re.etri.autoflow.entity.TrainingScriptAttachmentEntity; +import kr.re.etri.autoflow.entity.MinioAttachmentEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; @@ -17,21 +17,20 @@ import java.util.stream.Collectors; @Slf4j @Component -public class TrainingScriptAttachmentSpecification { +public class MinioAttachmentSpecification { private final EntityManager entityManager; private Set stringFields; - public TrainingScriptAttachmentSpecification(EntityManager entityManager) { + public MinioAttachmentSpecification(EntityManager entityManager) { this.entityManager = entityManager; } - // 스프링 빈 초기화 후 실행 @PostConstruct public void init() { Metamodel metamodel = entityManager.getMetamodel(); - EntityType entityType = metamodel.entity(TrainingScriptAttachmentEntity.class); + EntityType entityType = metamodel.entity(MinioAttachmentEntity.class); // 문자열 타입 필드명만 추출 stringFields = entityType.getAttributes().stream() @@ -39,39 +38,50 @@ public class TrainingScriptAttachmentSpecification { .map(Attribute::getName) .collect(Collectors.toSet()); - log.info("TrainingScriptAttachmentEntity string fields: {}", stringFields); + log.info("MinioAttachmentEntity string fields: {}", stringFields); } - public Specification searchByConditions( - String searchType, String keyword, - LocalDate startDate, LocalDate endDate) { - + public Specification searchByConditions( + String refType, + String searchType, + String keyword, + LocalDate startDate, + LocalDate endDate + ) { return (root, query, cb) -> { Predicate predicate = cb.conjunction(); + // refType 조건 추가 + if (refType != null && !refType.isBlank()) { + predicate = cb.and(predicate, cb.equal(root.get("refType"), refType)); + } + + // keyword 검색 if (keyword != null && !keyword.isEmpty()) { - if (searchType == null || searchType.isEmpty() || - "전체".equalsIgnoreCase(searchType) || "all".equalsIgnoreCase(searchType)) { + if (searchType == null || searchType.isEmpty() + || "전체".equalsIgnoreCase(searchType) + || "all".equalsIgnoreCase(searchType)) { Predicate orPredicate = cb.disjunction(); for (String field : stringFields) { orPredicate = cb.or(orPredicate, cb.like(cb.lower(root.get(field)), "%" + keyword.toLowerCase() + "%")); } predicate = cb.and(predicate, orPredicate); - } else if (stringFields.contains(searchType)) { predicate = cb.and(predicate, cb.like(cb.lower(root.get(searchType)), "%" + keyword.toLowerCase() + "%")); } - // 날짜 타입 필드 검색 시 무시 } + // 날짜 검색 if (startDate != null) { - predicate = cb.and(predicate, cb.greaterThanOrEqualTo(root.get("regDt"), startDate.atStartOfDay())); + predicate = cb.and(predicate, + cb.greaterThanOrEqualTo(root.get("regDt"), startDate.atStartOfDay())); } if (endDate != null) { - predicate = cb.and(predicate, cb.lessThanOrEqualTo(root.get("regDt"), endDate.atTime(23, 59, 59))); + predicate = cb.and(predicate, + cb.lessThanOrEqualTo(root.get("regDt"), endDate.atTime(23, 59, 59))); } return predicate;