Skip to content

Conversation

@SYDLK
Copy link

@SYDLK SYDLK commented Jan 22, 2026

#️⃣ 연관된 이슈

#34

#️⃣ 작업 내용

Explore API 목록 조회 기능에 대해 필터, 검색, 정렬, 페이징 로직을 QueryDSL 기반으로 확장 구현했습니다.
기존 Explore / 카테고리 / 필터 흐름을 하나의 API(/api/v1/apis)로 통합 처리하도록 구성했습니다.

또한 정렬 옵션(enum 기반)과 방향(enum 기반)을 추가하여 다양한 정렬 기준을 지원하도록 확장했습니다.
기존 코드와 main 브랜치 변경 사항을 병합하면서 충돌 해결 및 구조 분리(ApiDetailQueryService / ApiSearchQueryService)도 함께 진행했습니다.

  1. Explore 기준 통합 API 구현
    Explore / 카테고리 / 필터 / 검색 / 정렬을 하나의 엔드포인트로 통합했습니다.

📌 동작 방식

  • Explore
    GET /api/v1/apis?page=0&size=16&sort=LATEST&direction=ASC
    -> 전체 API 반환

  • naver 검색시
    GET /api/v1/apis?page=0&size=16&q=naver&sort=LATEST&direction=ASC

{
  "isSuccess": true,
  "code": "COMMON200",
  "message": "성공입니다.",
  "result": {
    "content": [
      {
        "apiId": 41,
        "name": "Naver Papago",
        "summary": "네이버 파파고 번역 API로 다양한 언어 번역",
        "avgRating": 0,
        "reviewCount": 0,
        "viewCounts": 0,
        "pricingType": "FREE",
        "authType": null,
        "providerCompany": "NAVER"
      }
    ],
    "totalPage": 1,
    "totalElements": 1,
    "listSize": 16,
    "currentPage": 1,
    "first": true,
    "last": true
  }
}
  • 카테고리 클릭 & 필터(가격 정책 - FREE 적용)
    GET /api/v1/apis?page=0&size=16&categoryId=5&sort=LATEST&direction=ASC&pricingTypes=FREE
{
  "isSuccess": true,
  "code": "COMMON200",
  "message": "성공입니다.",
  "result": {
    "content": [
      {
        "apiId": 24,
        "name": "DeepL API",
        "summary": "고품질 AI 기반 번역",
        "avgRating": 0,
        "reviewCount": 0,
        "viewCounts": 0,
        "pricingType": "FREE",
        "authType": null,
        "providerCompany": "DEEPL"
      },
      {
        "apiId": 41,
        "name": "Naver Papago",
        "summary": "네이버 파파고 번역 API로 다양한 언어 번역",
        "avgRating": 0,
        "reviewCount": 0,
        "viewCounts": 0,
        "pricingType": "FREE",
        "authType": null,
        "providerCompany": "NAVER"
      }
    ],
    "totalPage": 3,
    "totalElements": 35,
    "listSize": 16,
    "currentPage": 1,
    "first": true,
    "last": false
  }
}
  1. 필터 + 카테고리 조회 로직 구현 (QueryDSL)
    필터 조건이 존재하는 경우에만 WHERE 절에 반영되도록 BooleanBuilder 기반으로 구현했습니다.

지원 필터

  • providerCompany
  • authType
  • pricingType
  • minRating
  • categoryId
BooleanBuilder builder = new BooleanBuilder();

if (providerCompany != null)
    builder.and(api.providerCompany.eq(providerCompany));

if (authType != null)
    builder.and(api.authType.eq(authType));

if (pricingType != null)
    builder.and(api.pricingType.eq(pricingType));

if (minRating != null)
    builder.and(api.avgRating.goe(minRating));
  1. 검색(q) 기능 추가(대소문자 무시)
    CLOB 컬럼 제외 후 lower() 기반 LIKE 검색 적용
  • name
  • summary
  • enum 문자열 값(provider/auth/pricing)
if (q != null && !q.isBlank()) {
    String keyword = "%" + q.toLowerCase() + "%";

    builder.and(
        api.name.lower().like(keyword)
        .or(api.summary.lower().like(keyword))
        .or(Expressions.stringTemplate(
            "lower({0})", api.providerCompany.stringValue()
        ).like(keyword))
    );
}
  1. 정렬 옵션 enum 기반 기능 개발
    정렬 기준과 방향을 enum으로 분리하였습니다.
    지원 정렬
  • LATEST → createdAt
  • POPULAR → viewCounts
  • MOST_REVIEWED → reviewCount
boolean desc = direction == SortDirection.DESC;

OrderSpecifier<?> order =
    switch (sort) {
        case POPULAR -> desc ? api.viewCounts.desc() : api.viewCounts.asc();
        case MOST_REVIEWED -> desc ? review.id.count().desc() : review.id.count().asc();
        case LATEST -> desc ? api.createdAt.desc() : api.createdAt.asc();
    };
  1. 서비스 구조 분리
    기능별 QueryService 분리
  • ApiDetailQueryService → 상세 조회
  • ApiSearchQueryService → 검색/필터/정렬
    책임 분리 및 충돌 최소화를 위한 구조 개선
  1. main 브랜치 병합 및 충돌 해결
  • 기능별 QueryService 분리
  • ApiDetailQueryService → 상세 조회
  • ApiSearchQueryService → 검색/필터/정렬

#️⃣ 테스트 결과

  • Swagger 테스트 완료
  • 필터 조합 테스트 완료
  • API 상세조회 테스트 완료
  • 정렬 옵션별 테스트 완료

#️⃣ 변경 사항 체크리스트

  • 코드에 영향이 있는 모든 부분에 대한 테스트를 작성하고 실행했나요?
  • 문서를 작성하거나 수정했나요? (필요한 경우)
  • 코드 컨벤션에 따라 코드를 작성했나요?
  • 본 PR에서 발생할 수 있는 모든 의존성 문제가 해결되었나요?

#️⃣ 리뷰 요구사항 (선택)

  • main 브랜치 머지 과정에서 충돌 해결 및 구조 정리를 함께 진행했습니다.
  • 기능 동작이나 쿼리/정렬/필터 로직에 문제 없는지 한 번만 확인 부탁드립니다.
  • Explore 검색/필터/정렬 로직과 병합 결과에 문제 없는지 위주로 리뷰 부탁드립니다.

SYDLK added 7 commits January 22, 2026 19:57
[롬복 (Lombok) Java 21 + query dsl 조합시 롬복 오류로 인해 수정하였습니다.]
[- 향후 정렬(sort) 옵션 확장을 위한 코드 추가
  - popular: 조회수순
  - mostReviewed: 리뷰순
  - latest: 최신순

- 향후 검색(q) 기능 확장을 위한 스텁 코드 추가
  - name / summary / longDescription containsIgnoreCase 조건]
@SYDLK SYDLK linked an issue Jan 22, 2026 that may be closed by this pull request
6 tasks
@SYDLK SYDLK requested review from KJi0, dusvlf111 and seohyun27 January 22, 2026 17:19
@SYDLK SYDLK self-assigned this Jan 22, 2026
@SYDLK SYDLK moved this from Backlog to In Progress in umc 팀 프로젝트 허브 Jan 22, 2026
@dusvlf111 dusvlf111 requested a review from Copilot January 23, 2026 13:18
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

이 PR은 API 목록 조회 기능의 통합 엔드포인트(/apis)를 구현하여 Explore(전체 API 조회), 카테고리 필터링, 다중 필터 조건 조회를 단일 API로 제공합니다. QueryDSL을 활용한 동적 쿼리 구성을 통해 확장 가능한 필터링 구조를 구축했습니다.

Changes:

  • QueryDSL 의존성 추가 및 관련 설정 구성
  • API 목록 조회를 위한 DTO, Repository, Service, Controller 구현
  • 제공사, 인증 방식, 가격 유형, 최소 평점, 카테고리를 포함한 동적 필터링 로직 구현

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
build.gradle QueryDSL 의존성 추가 및 Lombok 버전 명시, QueryDSL Q클래스 생성 설정 추가
ApiDTO.java API 목록 미리보기를 위한 ApiPreview record DTO 정의
ApiRepository.java Api 엔티티를 위한 기본 JpaRepository 인터페이스 생성
ApiQueryService.java QueryDSL을 활용한 동적 필터링 및 페이징 조회 로직 구현
ApiController.java GET /apis 엔드포인트를 통한 통합 API 조회 컨트롤러 구현

Comment on lines 31 to 147
public Page<ApiDTO.ApiPreview> searchApis(
int page,
Integer size,
Long categoryId,
// String q,
// String sort,
ProviderCompany providerCompany,
AuthType authType,
PricingType pricingType,
BigDecimal minRating
) {

QApi api = QApi.api;
QApiReview review = QApiReview.apiReview;
QApiCategoriesMap map = QApiCategoriesMap.apiCategoriesMap;

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
BooleanBuilder builder = new BooleanBuilder();

// 필터 조건
if (providerCompany != null) builder.and(api.providerCompany.eq(providerCompany));
if (authType != null) builder.and(api.authType.eq(authType));
if (pricingType != null) builder.and(api.pricingType.eq(pricingType));
if (minRating != null) builder.and(api.avgRating.goe(minRating));

// 카테고리 필터 (exists 서브쿼리)
if (categoryId != null) {
builder.and(
JPAExpressions.selectOne()
.from(map)
.where(
map.api.id.eq(api.id)
.and(map.category.id.eq(categoryId))
)
.exists()
);
}

// 검색 조건
// if (q != null && !q.isBlank()) {
// builder.and(
// api.name.containsIgnoreCase(q)
// .or(api.summary.containsIgnoreCase(q))
// .or(api.longDescription.containsIgnoreCase(q))
// );
// }

int pageSize = (size != null) ? size : 16;
Pageable pageable = PageRequest.of(page, pageSize);

// reviewCount 서브쿼리
var reviewCountSubQuery =
JPAExpressions.select(review.count())
.from(review)
.where(review.api.id.eq(api.id));

// 정렬 옵션
// if (sort == null || sort.isBlank()) {
// sort = "latest";
// }

// var orderSpecifier = switch (sort) {
// case "popular" -> api.viewCounts.desc();
// case "mostReviewed" -> reviewCountSubQuery.desc();
// case "latest" -> api.createdAt.desc();
// default -> api.createdAt.desc(); // 잘못된 값 방어
// };

// 목록 조회
List<ApiDTO.ApiPreview> content = queryFactory
.select(Projections.constructor(
ApiDTO.ApiPreview.class,
api.id,
api.name,
api.summary,
api.avgRating,
reviewCountSubQuery,
api.viewCounts,
api.pricingType,
api.authType,
api.providerCompany
))
.from(api)
.where(builder)
.orderBy(api.createdAt.desc()) // 정렬 적용
// .orderBy(orderSpecifier) // 기존 createdAt.desc() → 동적 정렬
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

// total count
Long total = queryFactory
.select(api.count())
.from(api)
.where(builder)
.fetchOne();

long totalCount = (total != null) ? total : 0L;

// null 안전처리
content = content.stream()
.map(p -> new ApiDTO.ApiPreview(
p.apiId(),
p.name(),
p.summary(),
p.avgRating() != null ? p.avgRating() : BigDecimal.ZERO,
p.reviewCount() != null ? p.reviewCount() : 0L,
p.viewCounts() != null ? p.viewCounts() : 0L,
p.pricingType(),
p.authType(),
p.providerCompany()
))
.toList();

return new PageImpl<>(content, pageable, totalCount);
}
} No newline at end of file
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

새로 추가된 ApiQueryService와 ApiController에 대한 테스트 코드가 없습니다. 현재 테스트 디렉토리에는 ApiwikiBackendApplicationTests만 존재합니다.

주요 기능에 대한 테스트가 필요합니다:

  1. 필터 조건(providerCompany, authType, pricingType, minRating) 각각의 동작
  2. 카테고리 필터링 동작
  3. 페이징 처리
  4. null 값 처리
  5. 빈 결과 처리

최소한 단위 테스트(Service layer)와 통합 테스트(Controller layer)를 추가하는 것을 권장합니다.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 37

import java.math.BigDecimal;

@RestController
@RequiredArgsConstructor
@RequestMapping("/apis")
public class ApiController {

private final ApiQueryService apiQueryService;

@GetMapping
public ApiResponse<PageResponseDTO<ApiDTO.ApiPreview>> searchApis(
@RequestParam int page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) Long categoryId,
// @RequestParam(required = false) String q, // 검색어
// @RequestParam(required = false, defaultValue = "latest") String sort, // 정렬 옵션
@RequestParam(required = false, name = "providers") ProviderCompany providerCompany,
@RequestParam(required = false, name = "authTypes") AuthType authType,
@RequestParam(required = false, name = "pricingTypes") PricingType pricingType,
@RequestParam(required = false) BigDecimal minRating
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page 파라미터에 대한 입력 검증(validation)이 없습니다. 음수값이나 비정상적인 값이 전달될 경우 예기치 않은 동작이나 에러가 발생할 수 있습니다.

@RequestParam 파라미터에 대한 검증 추가를 권장합니다:

  • @min(0) 또는 @PositiveOrZero를 page에 추가
  • @positive를 size에 추가 (null이 아닌 경우)
  • @positive를 minRating에 추가하고 @max(5.0) 같은 최대값 제한도 고려

컨트롤러 클래스에 @validated 어노테이션도 추가해야 합니다.

Suggested change
import java.math.BigDecimal;
@RestController
@RequiredArgsConstructor
@RequestMapping("/apis")
public class ApiController {
private final ApiQueryService apiQueryService;
@GetMapping
public ApiResponse<PageResponseDTO<ApiDTO.ApiPreview>> searchApis(
@RequestParam int page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) Long categoryId,
// @RequestParam(required = false) String q, // 검색어
// @RequestParam(required = false, defaultValue = "latest") String sort, // 정렬 옵션
@RequestParam(required = false, name = "providers") ProviderCompany providerCompany,
@RequestParam(required = false, name = "authTypes") AuthType authType,
@RequestParam(required = false, name = "pricingTypes") PricingType pricingType,
@RequestParam(required = false) BigDecimal minRating
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import java.math.BigDecimal;
@RestController
@RequiredArgsConstructor
@RequestMapping("/apis")
@Validated
public class ApiController {
private final ApiQueryService apiQueryService;
@GetMapping
public ApiResponse<PageResponseDTO<ApiDTO.ApiPreview>> searchApis(
@RequestParam @PositiveOrZero int page,
@RequestParam(required = false) @Positive Integer size,
@RequestParam(required = false) Long categoryId,
// @RequestParam(required = false) String q, // 검색어
// @RequestParam(required = false, defaultValue = "latest") String sort, // 정렬 옵션
@RequestParam(required = false, name = "providers") ProviderCompany providerCompany,
@RequestParam(required = false, name = "authTypes") AuthType authType,
@RequestParam(required = false, name = "pricingTypes") PricingType pricingType,
@RequestParam(required = false) @Positive @DecimalMax("5.0") BigDecimal minRating

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 54
@RestController
@RequiredArgsConstructor
@RequestMapping("/apis")
public class ApiController {

private final ApiQueryService apiQueryService;

@GetMapping
public ApiResponse<PageResponseDTO<ApiDTO.ApiPreview>> searchApis(
@RequestParam int page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) Long categoryId,
// @RequestParam(required = false) String q, // 검색어
// @RequestParam(required = false, defaultValue = "latest") String sort, // 정렬 옵션
@RequestParam(required = false, name = "providers") ProviderCompany providerCompany,
@RequestParam(required = false, name = "authTypes") AuthType authType,
@RequestParam(required = false, name = "pricingTypes") PricingType pricingType,
@RequestParam(required = false) BigDecimal minRating
) {

Page<ApiDTO.ApiPreview> resultPage = apiQueryService.searchApis(
page,
size,
categoryId,
// q,
// sort,
providerCompany,
authType,
pricingType,
minRating
);

return ApiResponse.onPageSuccess(GeneralSuccessCode.OK, resultPage);
}
} No newline at end of file
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API 문서화가 누락되었습니다. 코드베이스의 UserController는 UserControllerDocs 인터페이스를 구현하여 Swagger @operation 어노테이션으로 API를 문서화하고 있습니다 (src/main/java/com/umc/apiwiki/domain/user/controller/UserControllerDocs.java 참조).

동일한 패턴으로 ApiControllerDocs 인터페이스를 생성하여 다음 내용을 문서화하는 것을 권장합니다:

  • API 엔드포인트의 목적과 동작
  • 쿼리 파라미터 설명 (page, size, categoryId, providers, authTypes, pricingTypes, minRating)
  • 응답 예시
  • 필터 조합 방식 설명

이는 코드베이스의 일관된 문서화 패턴을 따르는 것입니다.

Copilot uses AI. Check for mistakes.
Comment on lines 121 to 126
// total count
Long total = queryFactory
.select(api.count())
.from(api)
.where(builder)
.fetchOne();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두 번의 쿼리(content 조회 + total count 조회)가 같은 where 조건을 사용하고 있어 비효율적일 수 있습니다. 카테고리 필터의 경우 EXISTS 서브쿼리를 사용하므로 성능 영향이 있을 수 있습니다.

대안으로, total count가 필요 없는 경우(무한 스크롤 등)를 고려하여 선택적으로 count 쿼리를 실행하거나, Spring Data JPA의 Pageable과 함께 사용할 수 있는 최적화 방법을 고려해보세요. 또한 카테고리 필터는 JOIN으로 변경하는 것이 더 효율적일 수 있습니다.

Copilot uses AI. Check for mistakes.
Comment on lines 130 to 143
// null 안전처리
content = content.stream()
.map(p -> new ApiDTO.ApiPreview(
p.apiId(),
p.name(),
p.summary(),
p.avgRating() != null ? p.avgRating() : BigDecimal.ZERO,
p.reviewCount() != null ? p.reviewCount() : 0L,
p.viewCounts() != null ? p.viewCounts() : 0L,
p.pricingType(),
p.authType(),
p.providerCompany()
))
.toList();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null 안전 처리를 위해 모든 content를 스트림으로 재생성하는 로직은 비효율적입니다. QueryDSL의 Projections.constructor는 이미 값을 매핑하므로, null 값이 있다면 쿼리 레벨에서 COALESCE 함수를 사용하여 기본값을 설정하는 것이 더 효율적입니다.

예시:

api.avgRating.coalesce(BigDecimal.ZERO),
reviewCountSubQuery.coalesce(0L),
api.viewCounts.coalesce(0L)

이렇게 하면 131-143라인의 스트림 변환 로직을 제거할 수 있습니다.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 26
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

@Service
@RequiredArgsConstructor
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

읽기 전용 쿼리 서비스에 @transactional(readOnly = true)를 추가하는 것을 권장합니다. ApiQueryService는 조회만 수행하므로, readOnly 트랜잭션을 사용하면 성능 최적화와 데이터 일관성 보장에 도움이 됩니다.

코드베이스의 UserCommandService는 명령(Command) 작업이므로 @transactional을 사용하지만, 조회(Query) 서비스는 readOnly를 명시하는 것이 best practice입니다.

추가 권장:

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ApiQueryService {
Suggested change
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
@RequiredArgsConstructor
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)

Copilot uses AI. Check for mistakes.
QApiReview review = QApiReview.apiReview;
QApiCategoriesMap map = QApiCategoriesMap.apiCategoriesMap;

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPAQueryFactory를 매 요청마다 새로 생성하는 것은 비효율적입니다. JPAQueryFactory는 스레드 안전하며 재사용이 가능하므로, 빈으로 등록하거나 필드 레벨에서 한 번만 초기화하는 것이 권장됩니다. 현재 방식은 searchApis 메서드가 호출될 때마다 새로운 인스턴스를 생성하므로 불필요한 오버헤드가 발생합니다.

해결 방법:

  1. 생성자에서 초기화:
private final JPAQueryFactory queryFactory;

public ApiQueryService(EntityManager entityManager) {
    this.queryFactory = new JPAQueryFactory(entityManager);
}
  1. 또는 Configuration 클래스에서 빈으로 등록

Copilot uses AI. Check for mistakes.

@GetMapping
public ApiResponse<PageResponseDTO<ApiDTO.ApiPreview>> searchApis(
@RequestParam int page,
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지 번호 처리에 일관성 문제가 있습니다. 컨트롤러에서 받는 page 파라미터가 0-based인지 1-based인지 명확하지 않습니다. PageResponseDTO에서는 currentPage = page.getNumber() + 1로 1-based로 변환하는데, 컨트롤러에서는 page를 그대로 사용합니다.

PR 설명의 테스트 예시에서 page=0을 사용하고 있어 0-based로 보이지만, 응답에서 currentPage: 1로 반환되는 것을 보면 혼란을 줄 수 있습니다. API 문서나 주석으로 명확히 명시하거나, @min(0) 또는 @min(1) 같은 validation 어노테이션을 추가하는 것이 좋습니다.

Copilot uses AI. Check for mistakes.
Comment on lines 35 to 116
// String q,
// String sort,
ProviderCompany providerCompany,
AuthType authType,
PricingType pricingType,
BigDecimal minRating
) {

QApi api = QApi.api;
QApiReview review = QApiReview.apiReview;
QApiCategoriesMap map = QApiCategoriesMap.apiCategoriesMap;

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
BooleanBuilder builder = new BooleanBuilder();

// 필터 조건
if (providerCompany != null) builder.and(api.providerCompany.eq(providerCompany));
if (authType != null) builder.and(api.authType.eq(authType));
if (pricingType != null) builder.and(api.pricingType.eq(pricingType));
if (minRating != null) builder.and(api.avgRating.goe(minRating));

// 카테고리 필터 (exists 서브쿼리)
if (categoryId != null) {
builder.and(
JPAExpressions.selectOne()
.from(map)
.where(
map.api.id.eq(api.id)
.and(map.category.id.eq(categoryId))
)
.exists()
);
}

// 검색 조건
// if (q != null && !q.isBlank()) {
// builder.and(
// api.name.containsIgnoreCase(q)
// .or(api.summary.containsIgnoreCase(q))
// .or(api.longDescription.containsIgnoreCase(q))
// );
// }

int pageSize = (size != null) ? size : 16;
Pageable pageable = PageRequest.of(page, pageSize);

// reviewCount 서브쿼리
var reviewCountSubQuery =
JPAExpressions.select(review.count())
.from(review)
.where(review.api.id.eq(api.id));

// 정렬 옵션
// if (sort == null || sort.isBlank()) {
// sort = "latest";
// }

// var orderSpecifier = switch (sort) {
// case "popular" -> api.viewCounts.desc();
// case "mostReviewed" -> reviewCountSubQuery.desc();
// case "latest" -> api.createdAt.desc();
// default -> api.createdAt.desc(); // 잘못된 값 방어
// };

// 목록 조회
List<ApiDTO.ApiPreview> content = queryFactory
.select(Projections.constructor(
ApiDTO.ApiPreview.class,
api.id,
api.name,
api.summary,
api.avgRating,
reviewCountSubQuery,
api.viewCounts,
api.pricingType,
api.authType,
api.providerCompany
))
.from(api)
.where(builder)
.orderBy(api.createdAt.desc()) // 정렬 적용
// .orderBy(orderSpecifier) // 기존 createdAt.desc() → 동적 정렬
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 처리된 코드가 많이 포함되어 있습니다. 향후 확장을 위한 예비 코드라고 하셨지만, 프로덕션 코드에 주석 처리된 코드를 포함하는 것은 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.

대안:

  1. 별도의 브랜치나 feature flag로 관리
  2. TODO 주석과 함께 깔끔하게 정리
  3. 또는 완전히 제거하고 필요시 Git 히스토리에서 참조

특히 35-36, 44-45, 70-76, 88-97, 116번 라인의 주석 처리된 코드는 제거하는 것을 권장합니다.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@dusvlf111 dusvlf111 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다!

Copy link
Contributor

@seohyun27 seohyun27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! merge 해주시면 해당 기능들 바탕으로 좋아요 기능 추가하도록 하겠습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

[Feat] 정렬, 필터 기능 구현

4 participants