Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;

import org.ezcode.codetest.domain.problem.model.entity.Problem;
import org.ezcode.codetest.domain.problem.model.entity.ProblemSearchDocument;

import lombok.Builder;
Expand Down Expand Up @@ -36,4 +37,17 @@ public static ProblemSearchResponse from(ProblemSearchDocument document) {
.score(document.getScore())
.build();
}

public static ProblemSearchResponse from(Problem problem) {

return ProblemSearchResponse.builder()
.id(problem.getId())
.title(problem.getTitle())
.category(null) // TODO: 카테고리 입력해줘야 함
.difficulty(problem.getDifficulty().getDifficulty())
.reference(problem.getReference().toString())
.description(problem.getDescription())
.score(problem.getScore())
.build();
}
Comment on lines +41 to +52
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ProblemSearchResponse usages and how the category field is accessed

# Find files that import or use ProblemSearchResponse
rg -nP --type=java 'ProblemSearchResponse' -B2 -A5 | head -100

# Also specifically look for category field access
echo "=== Searching for category field/method access on ProblemSearchResponse ==="
rg -nP --type=java '\.getCategory\(\)|\.category' -B3 -A3

Repository: ezcode-my/backend

Length of output: 16749


🏁 Script executed:

#!/bin/bash
# Search for tests or client code that use ProblemSearchResponse
rg -nP --type=java '\.category|ProblemSearchResponse' src/test -B2 -A2 | head -80

# Also check for frontend files that might consume this API
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) | grep -i problem | head -20

Repository: ezcode-my/backend

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Read the frontend file to see how category is used
cat src/main/resources/static/js/problem-search.js

Repository: ezcode-my/backend

Length of output: 4297


The category field must be populated before sending the response to clients.

Line 46 sets category to null with a TODO comment. The frontend code (problem-search.js, line 72-73) directly renders this field without null checks: <div><b>Category:</b> ${item.category}</div>. When category is null, the UI will display "Category: null", degrading user experience.

Additionally, there's an inconsistency: the from(ProblemSearchDocument) factory method (line 33) populates category with document.getCategories(), while from(Problem) (line 46) leaves it null. API consumers will receive inconsistent response structures.

Populate the category field using Problem's category relationship before the TODO is resolved, or remove the field temporarily if it cannot be populated reliably.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/problem/dto/response/ProblemSearchResponse.java
around lines 41-52, the from(Problem) factory sets category to null which causes
the frontend to render "null" and creates inconsistency with the
from(ProblemSearchDocument) factory that uses document.getCategories(); replace
the null assignment by populating category from the Problem entity (match the
same shape as the other factory: e.g. use problem.getCategories() or map
problem.getCategory() to the same type), and guard against null by defaulting to
an empty list/string as appropriate so the response always contains a non-null
category value.

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package org.ezcode.codetest.application.problem.service;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.ezcode.codetest.application.problem.dto.response.ProblemSearchResponse;
import org.ezcode.codetest.domain.problem.model.entity.ProblemSearchDocument;
import org.ezcode.codetest.domain.problem.service.ProblemSearchDomainService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
Expand All @@ -20,48 +16,18 @@ public class ProblemSearchService {

private final ProblemSearchDomainService searchDomainService;

// Redis 캐시 적용 (Cache Name: suggestions, Key: keyword)
@Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()")
public Set<String> getProblemSuggestions(String keyword) {

return searchDomainService
.getSuggestionsByKeyword(keyword)
.stream()
.flatMap(doc -> {
Stream.Builder<String> searchedKeyword = Stream.builder();

Optional.ofNullable(doc.getTitle()).ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getReference())
.map(Enum::toString)
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDifficulty())
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDescription())
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDifficultyEn())
.map(Enum::toString)
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getReferenceKor())
.ifPresent(searchedKeyword::add);

Optional.ofNullable(doc.getCategories())
.stream()
.flatMap(Collection::stream)
.forEach(searchedKeyword::add);

Optional.ofNullable(doc.getCategoriesKor())
.stream()
.flatMap(Collection::stream)
.forEach(searchedKeyword::add);

return searchedKeyword.build();
})
.map(String::toUpperCase)
.collect(Collectors.toSet());
return null;
}
Comment on lines +19 to 24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

null 반환으로 인한 NullPointerException 위험.

이 메서드는 다음과 같은 심각한 문제들이 있습니다:

  1. 반환 타입은 Set<String>인데 null을 반환하면 호출자에서 NullPointerException이 발생합니다.
  2. unless = "#result.isEmpty()"null에 대해 작동하지 않습니다. null.isEmpty()를 평가하려다 예외가 발생하거나, 캐시 조건이 제대로 작동하지 않습니다.
  3. 결과적으로 null 값이 캐시에 저장될 수 있습니다.

자동완성 기능을 비활성화하려면 빈 Set을 반환하거나, 메서드 시그니처를 변경하는 것이 안전합니다.

다음 수정을 적용하세요:

 @Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()")
 public Set<String> getProblemSuggestions(String keyword) {
-	return null;
+	return Set.of();  // 빈 Set 반환으로 NPE 방지
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Redis 캐시 적용 (Cache Name: suggestions, Key: keyword)
@Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()")
public Set<String> getProblemSuggestions(String keyword) {
return searchDomainService
.getSuggestionsByKeyword(keyword)
.stream()
.flatMap(doc -> {
Stream.Builder<String> searchedKeyword = Stream.builder();
Optional.ofNullable(doc.getTitle()).ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getReference())
.map(Enum::toString)
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDifficulty())
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDescription())
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getDifficultyEn())
.map(Enum::toString)
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getReferenceKor())
.ifPresent(searchedKeyword::add);
Optional.ofNullable(doc.getCategories())
.stream()
.flatMap(Collection::stream)
.forEach(searchedKeyword::add);
Optional.ofNullable(doc.getCategoriesKor())
.stream()
.flatMap(Collection::stream)
.forEach(searchedKeyword::add);
return searchedKeyword.build();
})
.map(String::toUpperCase)
.collect(Collectors.toSet());
return null;
}
// Redis 캐시 적용 (Cache Name: suggestions, Key: keyword)
@Cacheable(value = "suggestions", key = "#keyword", unless = "#result.isEmpty()")
public Set<String> getProblemSuggestions(String keyword) {
return Set.of(); // 빈 Set 반환으로 NPE 방지
}
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/problem/service/ProblemSearchService.java
around lines 19 to 24, the method currently returns null which risks
NullPointerException and breaks the Cacheable unless expression; change the
method to never return null (return an empty Set when no suggestions, e.g.
Collections.emptySet()) and update the cache condition to safely handle null by
using unless = "#result == null || #result.isEmpty()" (or keep unless =
"#result.isEmpty()" if you ensure non-null returns), ensuring no null values are
stored in the cache.


public List<ProblemSearchResponse> getProblemSearchResult(String keyword) {

List<ProblemSearchDocument> results = searchDomainService.searchByKeywordMatch(keyword);

return results.stream().map(ProblemSearchResponse::from).toList();
return searchDomainService.searchByKeywordMatch(keyword)
.stream()
.map(ProblemSearchResponse::from)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.ezcode.codetest.domain.problem.repository;

import java.util.List;

import org.ezcode.codetest.domain.problem.model.entity.Problem;

public interface ProblemSearchRepository {

// 검색용 (검색 전용 DTO 또는 엔티티 리턴)
List<Problem> searchProblems(String keyword);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.ezcode.codetest.domain.problem.service;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.ezcode.codetest.domain.problem.model.entity.ProblemSearchDocument;
import org.ezcode.codetest.domain.problem.repository.ProblemDocumentRepository;
import org.ezcode.codetest.domain.problem.model.entity.Problem;
import org.ezcode.codetest.domain.problem.repository.ProblemSearchRepository;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
Expand All @@ -13,15 +14,11 @@
@RequiredArgsConstructor
public class ProblemSearchDomainService {

private final ProblemDocumentRepository searchRepository;
// private final ProblemDocumentRepository searchRepository;
private final ProblemSearchRepository searchRepository;

public Set<ProblemSearchDocument> getSuggestionsByKeyword(String keyword) {
public List<Problem> searchByKeywordMatch(String keyword) {

return searchRepository.findDocumentContainingKeyword(keyword);
}

public List<ProblemSearchDocument> searchByKeywordMatch(String keyword) {

return searchRepository.findProblemsByKeyword(keyword);
return searchRepository.searchProblems(keyword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.ezcode.codetest.infrastructure.persistence.repository.problem;

import java.util.List;

import org.ezcode.codetest.domain.problem.model.entity.Problem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProblemSearchJpaRepository extends JpaRepository<Problem, Long> {

// TODO: FULLTEXT 인덱스 추가
@Query(value = "SELECT * FROM problem WHERE title LIKE CONCAT('%', :keyword, '%') OR description LIKE CONCAT('%', :keyword, '%')", nativeQuery = true)
List<Problem> searchByKeyword(@Param("keyword") String keyword);
Comment on lines +12 to +14
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

LIKE 패턴의 성능 문제와 결과 제한 필요.

Line 13의 쿼리에 다음 문제들이 있습니다:

  1. Leading wildcard (%keyword%)는 인덱스를 사용할 수 없어 전체 테이블 스캔이 발생합니다. 데이터가 많아질수록 심각한 성능 저하가 예상됩니다.
  2. 결과 개수 제한이 없어 대량의 결과가 반환될 수 있습니다.
  3. TODO에서 언급된 FULLTEXT 인덱스를 추가하면 성능이 개선되지만, 현재는 구현되지 않았습니다.

임시 코드라고 하더라도 프로덕션 환경에서 성능 문제를 일으킬 수 있습니다.

다음 개선 방안을 제안합니다:

  1. 결과에 LIMIT 절을 추가하여 최대 반환 개수를 제한
  2. FULLTEXT 인덱스 구현 이슈를 생성하여 추적

개선된 쿼리를 생성하거나 FULLTEXT 인덱스 구현 이슈를 생성하시겠습니까?

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.ezcode.codetest.infrastructure.persistence.repository.problem;

import java.util.List;

import org.ezcode.codetest.domain.problem.model.entity.Problem;
import org.ezcode.codetest.domain.problem.repository.ProblemSearchRepository;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class ProblemSearchRepositoryImpl implements ProblemSearchRepository {

private final ProblemSearchJpaRepository searchJpaRepository;

@Override
public List<Problem> searchProblems(String keyword) {
return searchJpaRepository.searchByKeyword(keyword);
}
}