diff --git a/src/main/java/kr/re/etri/security/jwt/controllers/ProjectController.java b/src/main/java/kr/re/etri/security/jwt/controllers/ProjectController.java index 8a5141a..61011e9 100644 --- a/src/main/java/kr/re/etri/security/jwt/controllers/ProjectController.java +++ b/src/main/java/kr/re/etri/security/jwt/controllers/ProjectController.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.ModelAttribute; import java.util.List; @@ -28,6 +29,7 @@ public class ProjectController { public ResponseEntity> getAllProjects() { return ResponseEntity.ok(projectService.findAll()); } + @Operation(summary = "ID로 프로젝트 조회") @GetMapping("/{id}") public ResponseEntity getProjectById( @@ -41,7 +43,8 @@ public class ProjectController { @Operation(summary = "검색 및 페이지네이션 프로젝트 목록 조회") @GetMapping("/search") - public ResponseEntity> searchProjects(BaseSearchRequest request) { + public ResponseEntity> searchProjects( + @ModelAttribute BaseSearchRequest request) { Page page = projectService.search(request); return ResponseEntity.ok(page); } @@ -69,7 +72,6 @@ public class ProjectController { .orElse(ResponseEntity.notFound().build()); } - @Operation(summary = "프로젝트 삭제") @DeleteMapping("/{id}") public ResponseEntity deleteProject( diff --git a/src/main/java/kr/re/etri/security/jwt/entity/ProjectEntity.java b/src/main/java/kr/re/etri/security/jwt/entity/ProjectEntity.java index 3f61388..a7e5c1d 100644 --- a/src/main/java/kr/re/etri/security/jwt/entity/ProjectEntity.java +++ b/src/main/java/kr/re/etri/security/jwt/entity/ProjectEntity.java @@ -7,6 +7,8 @@ import org.hibernate.annotations.Comment; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; +import java.time.LocalDate; + @Schema(description = "프로젝트") @Comment("프로젝트") @Entity @@ -39,11 +41,11 @@ public class ProjectEntity { @Schema(description = "프로젝트 시작일", example = "2025-08-01") @Comment("프로젝트 시작일") - private String prjStartDt; + private LocalDate prjStartDt; @Schema(description = "프로젝트 종료일", example = "2025-12-31") @Comment("프로젝트 종료일") - private String prjEndDt; + private LocalDate prjEndDt; @Schema(description = "삭제 여부", example = "N") @Comment("삭제 여부") diff --git a/src/main/java/kr/re/etri/security/jwt/payload/request/BaseSearchRequest.java b/src/main/java/kr/re/etri/security/jwt/payload/request/BaseSearchRequest.java index bfc7f59..6e235b6 100644 --- a/src/main/java/kr/re/etri/security/jwt/payload/request/BaseSearchRequest.java +++ b/src/main/java/kr/re/etri/security/jwt/payload/request/BaseSearchRequest.java @@ -3,6 +3,7 @@ package kr.re.etri.security.jwt.payload.request; import lombok.Getter; import lombok.Setter; import org.springframework.format.annotation.DateTimeFormat; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; @@ -10,19 +11,27 @@ import java.time.LocalDate; @Setter public class BaseSearchRequest { - private int page = 0; // 페이지 번호 (0부터 시작) - private int size = 10; // 한 페이지당 출력 건수 + @Schema(description = "페이지 번호 (0부터 시작)", defaultValue = "0") + private int page = 0; - private String keyword; // 공통 키워드 검색 + @Schema(description = "한 페이지당 출력 건수", defaultValue = "10") + private int size = 10; - private String searchType; // 검색 유형 (예: "전체", "제목", "작성자" 등) + @Schema(description = "공통 키워드 검색") + private String keyword; - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) - private LocalDate startDate; // 등록일자 검색 시작 + @Schema(description = "검색 유형 (예: 전체, 제목, 작성자 등)", example = "전체") + private String searchType; - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) - private LocalDate endDate; // 등록일자 검색 종료 + @Schema(description = "등록일자 검색 시작", example = "2025-08-01") + private String startDate; - private String sortField = "id"; // 정렬 기준 필드명 - private String sortDirection = "DESC"; // 정렬 방향: ASC / DESC -} + @Schema(description = "등록일자 검색 종료", example = "2025-08-31") + private String endDate; + + @Schema(description = "정렬 기준 필드명", defaultValue = "id", example = "id") + private String sortField = "id"; + + @Schema(description = "정렬 방향: ASC / DESC", defaultValue = "DESC", allowableValues = {"ASC", "DESC"}) + private String sortDirection = "DESC"; +} \ No newline at end of file diff --git a/src/main/java/kr/re/etri/security/jwt/repository/ProjectRepository.java b/src/main/java/kr/re/etri/security/jwt/repository/ProjectRepository.java index 0145c9f..ac1e7d5 100644 --- a/src/main/java/kr/re/etri/security/jwt/repository/ProjectRepository.java +++ b/src/main/java/kr/re/etri/security/jwt/repository/ProjectRepository.java @@ -4,8 +4,9 @@ import kr.re.etri.security.jwt.entity.ProjectEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -public interface ProjectRepository extends JpaRepository { +public interface ProjectRepository extends JpaRepository, JpaSpecificationExecutor { boolean existsByPrjCd(String prjCd); Page findByPrjNmContainingIgnoreCase(String keyword, Pageable pageable); diff --git a/src/main/java/kr/re/etri/security/jwt/service/ProjectService.java b/src/main/java/kr/re/etri/security/jwt/service/ProjectService.java index 76e8187..391e0d3 100644 --- a/src/main/java/kr/re/etri/security/jwt/service/ProjectService.java +++ b/src/main/java/kr/re/etri/security/jwt/service/ProjectService.java @@ -4,12 +4,17 @@ import kr.re.etri.security.jwt.entity.ProjectEntity; import kr.re.etri.security.jwt.payload.request.BaseSearchRequest; import kr.re.etri.security.jwt.payload.request.ProjectRequest; import kr.re.etri.security.jwt.repository.ProjectRepository; +import kr.re.etri.security.jwt.specification.ProjectSpecification; 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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Optional; @@ -19,6 +24,7 @@ import java.util.Optional; public class ProjectService { private final ProjectRepository projectRepository; + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public List findAll() { return projectRepository.findAll(); @@ -35,10 +41,26 @@ public class ProjectService { Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortField()) ); - if (request.getKeyword() != null && !request.getKeyword().isEmpty()) { - return projectRepository.findByPrjNmContainingIgnoreCase(request.getKeyword(), pageable); - } else { - return projectRepository.findAll(pageable); + // String -> LocalDate 변환 + LocalDate startDate = parseDate(request.getStartDate()); + LocalDate endDate = parseDate(request.getEndDate()); + + Specification spec = ProjectSpecification.searchByConditions( + request.getSearchType(), + request.getKeyword(), + startDate, + endDate + ); + + return projectRepository.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/java/kr/re/etri/security/jwt/specification/ProjectSpecification.java b/src/main/java/kr/re/etri/security/jwt/specification/ProjectSpecification.java new file mode 100644 index 0000000..a02e6e6 --- /dev/null +++ b/src/main/java/kr/re/etri/security/jwt/specification/ProjectSpecification.java @@ -0,0 +1,51 @@ +package kr.re.etri.security.jwt.specification; + +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import kr.re.etri.security.jwt.entity.ProjectEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.domain.Specification; +import java.time.LocalDate; +import java.util.Set; + +@Slf4j +public class ProjectSpecification { + + public static Specification searchByConditions( + String searchType, String keyword, + LocalDate startDate, LocalDate endDate) { + + return (root, query, cb) -> { + Predicate predicate = cb.conjunction(); + + // 문자열 필드만 지정 (날짜 필드 제외) + Set stringFields = Set.of("prjNm", "prjDesc", "delYn"); + + if (keyword != null && !keyword.isEmpty()) { + if ("전체".equalsIgnoreCase(searchType) || "all".equalsIgnoreCase(searchType)) { + predicate = cb.and(predicate, cb.or( + cb.like(cb.lower(root.get("prjNm")), "%" + keyword.toLowerCase() + "%"), + cb.like(cb.lower(root.get("prjDesc")), "%" + keyword.toLowerCase() + "%"), + cb.like(cb.lower(root.get("delYn")), "%" + keyword.toLowerCase() + "%") + )); + } else if (stringFields.contains(searchType)) { + predicate = cb.and(predicate, + cb.like(cb.lower(root.get(searchType)), "%" + keyword.toLowerCase() + "%")); + } + // searchType이 날짜 컬럼이면 keyword 검색 안함 + } + + if (startDate != null) { + predicate = cb.and(predicate, + cb.greaterThanOrEqualTo(root.get("prjStartDt"), startDate)); + } + + if (endDate != null) { + predicate = cb.and(predicate, + cb.lessThanOrEqualTo(root.get("prjEndDt"), endDate)); + } + + return predicate; + }; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 807a35a..bae3b50 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,7 @@ spring.datasource.username=cuuva spring.datasource.password=cuuva spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect -spring.jpa.hibernate.ddl-auto= create-drop +spring.jpa.hibernate.ddl-auto=create-only spring.sql.init.mode=always diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 14e282c..3a4d598 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,5 +1,5 @@ -- src/main/resources/data.sql -INSERT INTO tb_role (id, name) VALUES (1, 'ROLE_USER'); -INSERT INTO tb_role (id, name) VALUES (2, 'ROLE_MODERATOR'); -INSERT INTO tb_role (id, name) VALUES (3, 'ROLE_ADMIN'); +# INSERT INTO tb_role (id, name) VALUES (1, 'ROLE_USER'); +# INSERT INTO tb_role (id, name) VALUES (2, 'ROLE_MODERATOR'); +# INSERT INTO tb_role (id, name) VALUES (3, 'ROLE_ADMIN');