diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java index 99dddc0c..58e2e32a 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/controller/DataSourceController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.tuna.zoopzoop.backend.domain.datasource.dto.*; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService; import org.tuna.zoopzoop.backend.domain.datasource.service.PersonalDataSourceService; import org.tuna.zoopzoop.backend.global.rsData.RsData; @@ -212,8 +213,9 @@ public ResponseEntity>> search( @PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, @AuthenticationPrincipal CustomUserDetails user ) { + Category categoryEnum = category != null ? Category.from(category) : null; var cond = DataSourceSearchCondition.builder() - .title(title).summary(summary).category(category).folderId(folderId) + .title(title).summary(summary).category(categoryEnum).folderId(folderId) .folderName(folderName).isActive(isActive).keyword(keyword).build(); Page page = personalApp.search(user.getMember().getId(), cond, pageable); diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java index 20b7ad5f..4e2b5074 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/dto/DataSourceSearchCondition.java @@ -2,14 +2,15 @@ import lombok.Builder; import lombok.Getter; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; @Getter @Builder public class DataSourceSearchCondition { private final String title; private final String summary; - private final String category; - private final String Source; + private final Category category; +// private final String Source; private final Integer folderId; private final String folderName; private final Boolean isActive; diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/Category.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/Category.java index 3e6eb3bc..3776210e 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/Category.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/entity/Category.java @@ -1,5 +1,7 @@ package org.tuna.zoopzoop.backend.domain.datasource.entity; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum Category { POLITICS("정치"), ECONOMY("경제"), @@ -29,4 +31,22 @@ public boolean isBlank() { public String toUpperCase() { return this.name.toUpperCase(); } + + // JSON 문자열을 Category enum으로 변환 + @JsonCreator + public static Category from(String input) { + if (input == null || input.isBlank()) return null; + if (input.equalsIgnoreCase("IT")) return IT; // IT 예외 + + // 1) 한글 이름 매칭 + for (Category c : values()) { + if (c.getName().equalsIgnoreCase(input)) return c; + } + // 2) 영문 코드 fallback (e.g., "SPORTS") + try { + return Category.valueOf(input.toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("유효하지 않은 카테고리 값입니다: " + input); + } + } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java index a1478ff2..a652105a 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImpl.java @@ -17,6 +17,7 @@ import org.tuna.zoopzoop.backend.domain.archive.folder.entity.QFolder; import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchCondition; import org.tuna.zoopzoop.backend.domain.datasource.dto.DataSourceSearchItem; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; import org.tuna.zoopzoop.backend.domain.datasource.entity.QDataSource; import org.tuna.zoopzoop.backend.domain.datasource.entity.QTag; @@ -51,19 +52,28 @@ public Page search(Integer memberId, DataSourceSearchCondi if (hasText(cond.getTitle())) where.and(ds.title.containsIgnoreCase(cond.getTitle())); if (hasText(cond.getSummary())) where.and(ds.summary.containsIgnoreCase(cond.getSummary())); - if (hasText(cond.getCategory())) where.and(ds.category.stringValue().containsIgnoreCase(cond.getCategory())); - if (hasText(cond.getSource())) where.and(ds.source.containsIgnoreCase(cond.getSource())); + if (cond.getCategory() != null) where.and(ds.category.eq(cond.getCategory())); + // 키워드 검색 (카테고리는 "한글 라벨 정확 일치" + IT 예외만 허용) if (hasText(cond.getKeyword())) { String kw = cond.getKeyword(); + + BooleanBuilder categoryMatch = new BooleanBuilder(); + Category cat = resolveCategoryFromKoreanOrIT(kw); // 한글 또는 IT만 매칭 + if (cat != null) { + categoryMatch.or(ds.category.eq(cat)); + } + // 영어 ENUM 문자열로의 비교는 제거 (ds.category.stringValue().containsIgnoreCase(kw) 금지) + where.and( ds.title.containsIgnoreCase(kw) .or(ds.summary.containsIgnoreCase(kw)) .or(ds.source.containsIgnoreCase(kw)) - .or(ds.category.stringValue().containsIgnoreCase(kw)) + .or(categoryMatch) ); } + // 폴더 조건 if (hasText(cond.getFolderName())) where.and(ds.folder.name.eq(cond.getFolderName())); if (cond.getFolderId() != null) where.and(ds.folder.id.eq(cond.getFolderId())); @@ -156,33 +166,46 @@ public Page searchInArchive(Integer archiveId, DataSourceS QTag tag = QTag.tag; BooleanBuilder where = new BooleanBuilder(); + + // 활성/비활성 if (cond.getIsActive() == null || Boolean.TRUE.equals(cond.getIsActive())) where.and(ds.isActive.isTrue()); else where.and(ds.isActive.isFalse()); + // 개별 필드 필터 if (hasText(cond.getTitle())) where.and(ds.title.containsIgnoreCase(cond.getTitle())); if (hasText(cond.getSummary())) where.and(ds.summary.containsIgnoreCase(cond.getSummary())); - if (hasText(cond.getCategory())) where.and(ds.category.stringValue().containsIgnoreCase(cond.getCategory())); - if (hasText(cond.getSource())) where.and(ds.source.containsIgnoreCase(cond.getSource())); + if (cond.getCategory() != null) where.and(ds.category.eq(cond.getCategory())); + + // 키워드 검색 (카테고리는 "한글 라벨 정확 일치" + IT 예외만 허용) if (hasText(cond.getKeyword())) { String kw = cond.getKeyword(); + + BooleanBuilder categoryMatch = new BooleanBuilder(); + Category cat = resolveCategoryFromKoreanOrIT(kw); + if (cat != null) { + categoryMatch.or(ds.category.eq(cat)); + } + // 영어 ENUM 문자열 비교 제거 + where.and( ds.title.containsIgnoreCase(kw) .or(ds.summary.containsIgnoreCase(kw)) .or(ds.source.containsIgnoreCase(kw)) - .or(ds.category.stringValue().containsIgnoreCase(kw)) + .or(categoryMatch) ); } - if (hasText(cond.getFolderName())) where.and(ds.folder.name.eq(cond.getFolderName())); - if (cond.getFolderId() != null) where.and(ds.folder.id.eq(cond.getFolderId())); + // 아카이브 스코프 BooleanBuilder scope = new BooleanBuilder().and(folder.archive.id.eq(archiveId)); + // count JPAQuery countQuery = queryFactory .select(ds.id.countDistinct()) .from(ds) .join(ds.folder, folder) .where(where.and(scope)); + // content JPAQuery contentQuery = queryFactory .select(ds.id, ds.title, ds.dataCreatedDate, ds.summary, ds.source, ds.sourceUrl, ds.imageUrl, ds.category) .from(ds) @@ -220,10 +243,26 @@ public Page searchInArchive(Integer archiveId, DataSourceS row.get(ds.sourceUrl), row.get(ds.imageUrl), tagsById.getOrDefault(row.get(ds.id), List.of()), - row.get(ds.category).name() + row.get(ds.category).name() // 응답은 영문 코드 유지 )) .toList(); return new PageImpl<>(content, pageable, total); } + + /** + * 키워드가 "IT"(대소문자 무시)이면 IT를, 그 외에는 + * 카테고리의 한글 라벨과 "정확 일치"할 때만 해당 Enum을 반환. + * (예: "스포츠" -> SPORTS, "SPORTS" -> null) + */ + private Category resolveCategoryFromKoreanOrIT(String kw) { + if (kw == null || kw.isBlank()) return null; + if ("IT".equalsIgnoreCase(kw)) return Category.IT; // 유일한 영문 예외 + for (Category c : Category.values()) { + if (c != Category.IT && c.getName().equalsIgnoreCase(kw)) { + return c; // 한글 라벨 정확 일치 + } + } + return null; // 영문 코드 등은 매칭하지 않음 + } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceController.java index 7e4b9ed3..41f5575d 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveDataSourceController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.tuna.zoopzoop.backend.domain.datasource.dto.*; +import org.tuna.zoopzoop.backend.domain.datasource.entity.Category; import org.tuna.zoopzoop.backend.domain.datasource.service.DataSourceService; import org.tuna.zoopzoop.backend.domain.space.archive.service.SpaceDataSourceService; import org.tuna.zoopzoop.backend.global.rsData.RsData; @@ -239,8 +240,9 @@ public ResponseEntity>> search( @PageableDefault(size = 8, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, @AuthenticationPrincipal CustomUserDetails user ) { + Category categoryEnum = category != null ? Category.from(category) : null; var cond = DataSourceSearchCondition.builder() - .title(title).summary(summary).category(category).folderId(folderId) + .title(title).summary(summary).category(categoryEnum).folderId(folderId) .folderName(folderName).isActive(isActive).keyword(keyword).build(); Page page = spaceApp.search(user.getMember().getId(), spaceId, cond, pageable); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImplTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImplTest.java index fba0b4d0..0e746ccb 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImplTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/datasource/repository/DataSourceQRepositoryImplTest.java @@ -160,7 +160,7 @@ void filter_summary_contains() { void filter_category_contains() { Pageable pageable = PageRequest.of(0, 10); DataSourceSearchCondition cond = DataSourceSearchCondition.builder() - .category("it") + .category(Category.IT) .build(); Page page = dataSourceQRepository.search(memberId, cond, pageable);