diff --git a/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java b/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java new file mode 100644 index 0000000..a45ccc2 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/controllers/DatasetController.java @@ -0,0 +1,113 @@ +package kr.re.etri.autoflow.controllers; + +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.DatasetEntity; +import kr.re.etri.autoflow.entity.ProjectEntity; +import kr.re.etri.autoflow.payload.request.BaseSearchRequest; +import kr.re.etri.autoflow.payload.request.DatasetRequest; +import kr.re.etri.autoflow.service.DatasetService; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Tag(name = "데이터셋 API", description = "Dataset CRUD 기능 제공") +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/datasets") +@RequiredArgsConstructor +public class DatasetController { + + private final DatasetService datasetService; + + @Operation(summary = "전체 데이터셋 목록 조회") + @GetMapping + public ResponseEntity> getAllDatasets() { + return ResponseEntity.ok(datasetService.findAll()); + } + + @Operation(summary = "ID로 데이터셋 조회") + @GetMapping("/{id}") + public ResponseEntity getDatasetById( + @Parameter(description = "조회할 데이터셋 ID", required = true, in = ParameterIn.PATH) + @PathVariable("id") Long id) { + + return datasetService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @Operation(summary = "검색 및 페이지네이션 데이터셋 목록 조회") + @GetMapping("/search") + public ResponseEntity> searchDatasets( + @ParameterObject @ModelAttribute BaseSearchRequest request) { + Page page = datasetService.search(request); + return ResponseEntity.ok(page); + } + + @Operation(summary = "데이터셋 생성 (Swagger 전용)") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity createDataset( + @Parameter(description = "데이터셋 정보", required = true) + @ModelAttribute DatasetRequest request, + @Parameter(description = "첨부파일", required = false) + @RequestPart(value = "files", required = false) List files) { + + // DTO → Entity 변환 + DatasetEntity dataset = DatasetEntity.builder() + .dsNm(request.getDsNm()) + .dsDesc(request.getDsDesc()) + .delYn(request.getDelYn()) + .regUserId(request.getRegUserId()) + .regUserNm(request.getRegUserNm()) + .build(); + + System.out.println("DatasetController.createDataset: " + dataset.toString()); + + DatasetEntity saved = datasetService.createWithFiles(dataset, files); + return ResponseEntity.ok(saved); + } + +// +// @Operation(summary = "데이터셋 생성") +// @PostMapping +// public ResponseEntity createDataset(@RequestBody DatasetEntity dataset) { +// try { +// DatasetEntity saved = datasetService.create(dataset); +// return ResponseEntity.ok(saved); +// } catch (IllegalArgumentException e) { +// return ResponseEntity.badRequest().body(e.getMessage()); +// } +// } + +// @Operation(summary = "데이터셋 수정") +// @PutMapping("/{id}") +// public ResponseEntity updateDataset( +// @Parameter(description = "수정할 데이터셋 ID", required = true, in = ParameterIn.PATH) +// @PathVariable("id") Long id, +// @RequestBody DatasetEntity dataset) { // DTO 없이 엔티티 사용 +// return datasetService.update(id, dataset) +// .map(ResponseEntity::ok) +// .orElse(ResponseEntity.notFound().build()); +// } + + + @Operation(summary = "데이터셋 삭제") + @DeleteMapping("/{id}") + public ResponseEntity deleteDataset( + @Parameter(description = "삭제할 데이터셋 ID", required = true, in = ParameterIn.PATH) + @PathVariable("id") Long id) { + if (datasetService.delete(id)) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.notFound().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 index 1d7884f..7ecb6e6 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/MinIOController.java @@ -5,18 +5,23 @@ 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") @@ -25,6 +30,8 @@ import java.util.List; @Tag(name = "MinIO Controller", description = "MinIO 버킷/파일 관리 API") public class MinIOController { private final MinioClient minioClient; + private final DatasetAttachmentRepository datasetAttachmentRepository; + @Value("${minio.bucket}") private String bucketName; @@ -68,19 +75,25 @@ public class MinIOController { return files; } - @Operation(summary = "파일 업로드", description = "MultipartFile을 MinIO 버킷에 업로드합니다. path를 지정하면 하위 폴더에 저장 가능합니다.") + @Operation(summary = "파일 업로드", description = "MultipartFile을 MinIO 버킷에 업로드하고 DB에 기록합니다.") @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public String uploadFile( + public ResponseEntity uploadFile( @Parameter(description = "업로드할 파일") @RequestPart("file") MultipartFile file, @Parameter(description = "저장 경로 (선택)") - @RequestPart(value = "path", required = false) String path + @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()) - ? file.getOriginalFilename() - : path + "/" + file.getOriginalFilename(); + ? storedName + : path + "/" + storedName; + // MinIO 업로드 minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) @@ -90,16 +103,29 @@ public class MinIOController { .build() ); - // MinIO 경로 URL 생성 (NodePort 기반) - String minioUrl = String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); - - return "파일 업로드 성공: " + minioUrl; + // 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) + .build(); + + DatasetAttachmentEntity saved = datasetAttachmentRepository.save(attachment); + + return ResponseEntity.ok(saved); } catch (Exception e) { log.error("파일 업로드 실패", e); - return "파일 업로드 실패: " + e.getMessage(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + @Operation(summary = "다중 파일 업로드", description = "MultipartFile 배열을 MinIO 버킷에 업로드합니다. path를 지정하면 하위 폴더에 저장 가능합니다.") @PostMapping(value = "/upload-multiple", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public List uploadMultipleFiles( diff --git a/src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java b/src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java new file mode 100644 index 0000000..f8d61e2 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/entity/DatasetAttachmentEntity.java @@ -0,0 +1,83 @@ +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.*; +import org.hibernate.annotations.Comment; +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 전용 첨부파일") +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "tb_minio_attachment") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DatasetAttachmentEntity { + + @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/entity/DatasetEntity.java b/src/main/java/kr/re/etri/autoflow/entity/DatasetEntity.java new file mode 100644 index 0000000..8a5022e --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/entity/DatasetEntity.java @@ -0,0 +1,72 @@ +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.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "데이터셋") +@Comment("데이터셋") +@Entity +@Table(name = "tb_dataset") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DatasetEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "ID", example = "null") + @Comment("ID") + private Long id; + + @Schema(description = "데이터셋 이름", example = "배터리 상태 데이터 셋") + @Comment("데이터셋 이름") + private String dsNm; + + @Schema(description = "데이터셋 설명", example = "EV6 차량의 배터리 상태 모음") + @Comment("데이터셋 설명") + private String dsDesc; + + @Schema(description = "삭제 여부", example = "N") + @Comment("삭제 여부") + private String delYn; + + @CreatedDate + @Schema(description = "등록 일자") + @Comment("등록 일자") + private LocalDateTime regDate; + + @Schema(description = "등록 유저 ID", example = "system") + @Comment("등록 유저 ID") + private String regUserId; + + @Schema(description = "등록 유저 이름", example = "시스템") + @Comment("등록 유저 이름") + private String regUserNm; + + @LastModifiedDate + @Schema(description = "수정 일자") + @Comment("수정 일자") + private LocalDateTime modDate; + + @Schema(description = "수정 유저 ID", example = "system") + @Comment("수정 유저 ID") + private String modUserId; + + @Schema(description = "수정 유저 이름", example = "시스템") + @Comment("수정 유저 이름") + private String modUserNm; +// +// @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/payload/request/DatasetRequest.java b/src/main/java/kr/re/etri/autoflow/payload/request/DatasetRequest.java new file mode 100644 index 0000000..fecd353 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/payload/request/DatasetRequest.java @@ -0,0 +1,15 @@ +package kr.re.etri.autoflow.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Data +public class DatasetRequest { + private String dsNm; + private String dsDesc; + private String delYn; + private String regUserId; + private String regUserNm; +} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java new file mode 100644 index 0000000..d308048 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/repository/DatasetAttachmentRepository.java @@ -0,0 +1,11 @@ +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/DatasetRepository.java b/src/main/java/kr/re/etri/autoflow/repository/DatasetRepository.java new file mode 100644 index 0000000..0bee698 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/repository/DatasetRepository.java @@ -0,0 +1,8 @@ +package kr.re.etri.autoflow.repository; + +import kr.re.etri.autoflow.entity.DatasetEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface DatasetRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/src/main/java/kr/re/etri/autoflow/repository/FileUploadRepository.java b/src/main/java/kr/re/etri/autoflow/repository/FileUploadRepository.java index 07e7125..42442e9 100644 --- a/src/main/java/kr/re/etri/autoflow/repository/FileUploadRepository.java +++ b/src/main/java/kr/re/etri/autoflow/repository/FileUploadRepository.java @@ -8,13 +8,4 @@ import java.util.List; @Repository public interface FileUploadRepository extends JpaRepository { - - // 연관 엔티티 타입 + ID 기준 조회 - List findByRefTypeAndRefId(String refType, Long refId); - - // 업로더 기준 조회 - List findByRegUserId(String regUserId); - - // Dataset 제목 기준 조회 (optional) - List findByDatasetTitleContaining(String datasetTitle); } diff --git a/src/main/java/kr/re/etri/autoflow/security/WebSecurityConfig.java b/src/main/java/kr/re/etri/autoflow/security/WebSecurityConfig.java index 30c824b..6f79fd6 100644 --- a/src/main/java/kr/re/etri/autoflow/security/WebSecurityConfig.java +++ b/src/main/java/kr/re/etri/autoflow/security/WebSecurityConfig.java @@ -83,39 +83,39 @@ public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { // http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // } - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> {}) // ← CORS 설정 추가! - .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> - auth.requestMatchers("/api/auth/**").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/test/**").permitAll() - .anyRequest().authenticated() - ); - - http.authenticationProvider(authenticationProvider()); - http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - // 임시 설정 -// @Bean -// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -// http.csrf(AbstractHttpConfigurer::disable) -// .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) -// .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) -// .authorizeHttpRequests(auth -> -// auth.anyRequest().permitAll() // 모든 요청 허용 -// ); +// @Bean +// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { +// http +// .csrf(AbstractHttpConfigurer::disable) +// .cors(cors -> {}) // ← CORS 설정 추가! +// .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) +// .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) +// .authorizeHttpRequests(auth -> +// auth.requestMatchers("/api/auth/**").permitAll() +// .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() +// .requestMatchers("/api/test/**").permitAll() +// .anyRequest().authenticated() +// ); // -// http.authenticationProvider(authenticationProvider()); -// http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); +// http.authenticationProvider(authenticationProvider()); +// http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // -// return http.build(); -// } +// return http.build(); +// } + + // 임시 설정 + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> + auth.anyRequest().permitAll() // 모든 요청 허용 + ); + + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } diff --git a/src/main/java/kr/re/etri/autoflow/service/DatasetService.java b/src/main/java/kr/re/etri/autoflow/service/DatasetService.java new file mode 100644 index 0000000..1aa9b69 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/DatasetService.java @@ -0,0 +1,134 @@ +package kr.re.etri.autoflow.service; + +import kr.re.etri.autoflow.entity.DatasetAttachmentEntity; +import kr.re.etri.autoflow.entity.DatasetEntity; +import kr.re.etri.autoflow.payload.request.BaseSearchRequest; +import kr.re.etri.autoflow.repository.DatasetAttachmentRepository; +import kr.re.etri.autoflow.repository.DatasetRepository; +import kr.re.etri.autoflow.specification.DatasetSpecification; +import lombok.RequiredArgsConstructor; +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.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DatasetService { + + private final DatasetRepository datasetRepository; + private final DatasetAttachmentRepository datasetAttachmentRepository; + + private final DatasetSpecification datasetSpecification; + + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public List findAll() { + return datasetRepository.findAll(); + } + + public Optional findById(Long id) { + return datasetRepository.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 = datasetSpecification.searchByConditions( + request.getSearchType(), + request.getKeyword(), + startDate, + endDate + ); + + return datasetRepository.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 DatasetEntity create(DatasetEntity dataset) { + return datasetRepository.save(dataset); + } + + @Transactional + public Optional update(Long id) { + return datasetRepository.findById(id); + } + + @Transactional + public boolean delete(Long id) { + if (!datasetRepository.existsById(id)) { + return false; + } + datasetRepository.deleteById(id); + return true; + } + + @Transactional + public DatasetEntity createWithFiles(DatasetEntity dataset, List files) { + DatasetEntity saved = datasetRepository.save(dataset); + + if (files != null && !files.isEmpty()) { + List attachments = new ArrayList<>(); + for (MultipartFile file : files) { + DatasetAttachmentEntity attachment = DatasetAttachmentEntity.builder() + .originalName(file.getOriginalFilename()) + .storedName(storeFile(file)) // 실제 파일 저장 로직 필요 + .contentType(file.getContentType()) + .size(file.getSize()) + .regUserId(dataset.getRegUserId()) + .regDt(LocalDateTime.now()) + .title(file.getOriginalFilename()) + .version(1) + .description("") + .build(); + attachments.add(attachment); + } + // attachment 저장 + datasetAttachmentRepository.saveAll(attachments); + + // 엔티티에 첨부파일 리스트 설정 + //saved.setFiles(attachments); + } + + return saved; + } + + // 파일 저장 예시 + private String storeFile(MultipartFile file) { + // TODO: MinIO 또는 로컬 스토리지에 실제 저장 후 경로 반환 + String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename(); + // file.transferTo(new File(uploadPath + storedName)); + return storedName; + } + +} diff --git a/src/main/java/kr/re/etri/autoflow/specification/DatasetSpecification.java b/src/main/java/kr/re/etri/autoflow/specification/DatasetSpecification.java new file mode 100644 index 0000000..cdcfc19 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/specification/DatasetSpecification.java @@ -0,0 +1,80 @@ +package kr.re.etri.autoflow.specification; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +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.DatasetEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class DatasetSpecification { + + private final EntityManager entityManager; + + private Set stringFields; + + public DatasetSpecification(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @PostConstruct + public void init() { + Metamodel metamodel = entityManager.getMetamodel(); + EntityType entityType = metamodel.entity(DatasetEntity.class); + + // 문자열 타입 필드명만 추출 + stringFields = entityType.getAttributes().stream() + .filter(attr -> attr.getJavaType().equals(String.class)) + .map(Attribute::getName) + .collect(Collectors.toSet()); + + log.info("DatasetEntity string fields: {}", stringFields); + } + + public Specification searchByConditions( + String searchType, String keyword, + LocalDate startDate, LocalDate endDate) { + + return (root, query, cb) -> { + Predicate predicate = cb.conjunction(); + + if (keyword != null && !keyword.isEmpty()) { + 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("regDate"), startDate.atStartOfDay())); + } + + if (endDate != null) { + predicate = cb.and(predicate, + cb.lessThanOrEqualTo(root.get("regDate"), endDate.atTime(23, 59, 59))); + } + + return predicate; + }; + } +}