-
Notifications
You must be signed in to change notification settings - Fork 0
✨Feat: 인기 전문가 칼럼 조회 Redis 캐시 도입 및 DTO 조회 구조 개선 #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d6826a0
0938457
c37359a
225eaad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,4 +10,4 @@ public record HomePostRow( | |
| LocalDateTime createdAt, | ||
| Long likeCount, | ||
| Long commentCount | ||
| ) {} | ||
| ) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.backend.farmon.dto.home; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public class PopularExpertPostRow { | ||
| private Long postId; | ||
| private String title; | ||
| private String content; | ||
| private String writer; | ||
| private String profileImageUrl; | ||
| private String firstImageStoredFileName; | ||
| } | ||
|
Comment on lines
+7
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 캐시 대상 DTO로 적절한 구조입니다.
불변성을 보장하려면 필드에 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,12 @@ | |
| import com.backend.farmon.apiPayload.exception.GeneralException; | ||
| import com.backend.farmon.domain.*; | ||
| import com.backend.farmon.dto.home.HomePostRow; | ||
| import com.backend.farmon.dto.home.PopularExpertPostRow; | ||
| import com.backend.farmon.dto.post.PostType; | ||
| import com.backend.farmon.repository.BoardRepository.BoardRepository; | ||
| import com.querydsl.core.BooleanBuilder; | ||
| import com.querydsl.core.types.Projections; | ||
| import com.querydsl.core.types.dsl.CaseBuilder; | ||
| import com.querydsl.core.types.dsl.Expressions; | ||
| import com.querydsl.jpa.JPAExpressions; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
|
|
@@ -20,8 +22,6 @@ | |
|
|
||
| import java.util.*; | ||
|
|
||
| import static com.backend.farmon.domain.QPostImg.postImg; | ||
|
|
||
| @Slf4j | ||
| @Repository | ||
| @RequiredArgsConstructor | ||
|
|
@@ -33,8 +33,10 @@ public class PostRepositoryImpl implements PostRepositoryCustom { | |
| private final QBoard board = QBoard.board; | ||
| private final QPost originalPost = new QPost("originalPost"); | ||
| private final QComment comment = QComment.comment; | ||
|
|
||
| private final QCrop crop = QCrop.crop; | ||
| private final QUser user = QUser.user; | ||
| private final QExpert expert = QExpert.expert; | ||
| private final QPostImg postImg = QPostImg.postImg; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * 공통 select (원본 게시글 기준) + 좋아요/댓글 count를 한 번에 가져오기 위한 프로젝션 | ||
|
|
@@ -142,33 +144,72 @@ public List<HomePostRow> findTopPostsByPostTypeWithCounts(PostType postType, int | |
|
|
||
| // 인기 전문가 칼럼 6개 조회 | ||
| @Override | ||
| public List<Post> findTop6ExpertColumnPostsByPostId(List<Long> popularPostsIdList) { | ||
| return queryFactory.select(post) | ||
| public List<PopularExpertPostRow> findTopExpertColumnRowsByPopularIds(List<Long> popularPostsIdList, int limit) { | ||
| QPostImg pi2 = new QPostImg("pi2"); | ||
|
|
||
| boolean hasPinned = popularPostsIdList != null && !popularPostsIdList.isEmpty(); | ||
|
|
||
| var query = queryFactory | ||
| .select(Projections.constructor( | ||
| PopularExpertPostRow.class, | ||
| post.id, | ||
| post.postTitle, | ||
| post.postContent, | ||
| user.userName, | ||
| expert.profileImageUrl, | ||
| postImg.storedFileName | ||
| )) | ||
| .from(post) | ||
| .join(post.board, board).fetchJoin() | ||
| .leftJoin(post.postlikes, likeCount) | ||
| .where( | ||
| board.postType.eq(PostType.EXPERT_COLUMN) // 전문가 칼럼 조건 | ||
| .and( | ||
| popularPostsIdList != null && !popularPostsIdList.isEmpty() | ||
| ? post.id.in(popularPostsIdList).or(post.id.notIn(popularPostsIdList)) | ||
| : null | ||
| ) | ||
| ) | ||
| .groupBy(post) | ||
| .orderBy( | ||
| // 인기 게시글 우선 정렬 | ||
| popularPostsIdList != null && !popularPostsIdList.isEmpty() | ||
| ? Expressions.stringTemplate("CASE WHEN {0} IN ({1}) THEN 1 ELSE 2 END", post.id, Expressions.constant(popularPostsIdList)).asc() | ||
| : null, | ||
| // popularPostsIdList 내부 정렬 | ||
| popularPostsIdList != null && !popularPostsIdList.isEmpty() | ||
| ? Expressions.stringTemplate("FIELD({0}, {1})", post.id, Expressions.constant(popularPostsIdList)).asc() | ||
| : null, | ||
| likeCount.count().desc(), // 좋아요 개수 내림차순 | ||
| post.createdAt.desc() // 작성일 내림차순 | ||
| ) | ||
| .limit(6) // 6개 제한 | ||
| .join(post.board, board) | ||
| .join(post.user, user) | ||
| .leftJoin(user.expert, expert) | ||
| .leftJoin(likeCount).on(likeCount.post.id.eq(post.id)) | ||
| // 첫 이미지 1개만 LEFT JOIN | ||
| .leftJoin(postImg).on(postImg.id.eq( | ||
| JPAExpressions.select(pi2.id.min()) | ||
| .from(pi2) | ||
| .where(pi2.post.id.eq(post.id)) | ||
| )) | ||
| .where(board.postType.eq(PostType.EXPERT_COLUMN)) | ||
| .groupBy( | ||
| post.id, | ||
| post.postTitle, | ||
| post.postContent, | ||
| user.userName, | ||
| expert.profileImageUrl, | ||
| postImg.storedFileName | ||
| ); | ||
|
|
||
| // QueryDSL orderBy에 null 전달 방지 + pinned 조건부 생성 | ||
| if (hasPinned) { | ||
| // pinned 우선 정렬(1) / 나머지(2) | ||
| var pinnedFirstOrderExpr = new CaseBuilder() | ||
| .when(post.id.in(popularPostsIdList)).then(1) | ||
| .otherwise(2); | ||
|
|
||
| // pinned 내부 순서 유지 (MySQL: FIELD) | ||
| var pinnedInnerOrderExpr = Expressions.numberTemplate( | ||
| Integer.class, | ||
| "FIELD({0}, {1})", | ||
| post.id, | ||
| Expressions.constant(popularPostsIdList) | ||
| ); | ||
|
Comment on lines
+191
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check for database configurations across the project
echo "=== Database Configuration Files ==="
fd -e properties -e yaml | head -20
echo -e "\n=== Searching for datasource and database configs ==="
rg "spring.datasource|spring.jpa|h2|mysql" -t yaml -t properties -A 2
echo -e "\n=== Checking pom.xml for database dependencies ==="
if [ -f "pom.xml" ]; then
rg "<artifactId>(h2|mysql|postgresql|mariadb|oracle)" pom.xml
fi
echo -e "\n=== Checking build.gradle for database dependencies ==="
if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
rg "(h2|mysql|postgresql|mariadb|oracle)" build.gradle* 2>/dev/null || true
fiRepository: mmije0ng/FarmON_BE_Refactor Length of output: 452 🏁 Script executed: #!/bin/bash
# Search more specifically for application configuration files in src directory
echo "=== Application configuration files in src ==="
find src -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" | sort
echo -e "\n=== Content of test application properties ==="
find src -path "*test*" -name "application*.properties" -o -path "*test*" -name "application*.yml" -o -path "*test*" -name "application*.yaml" | xargs cat 2>/dev/null || echo "No test configs found"
echo -e "\n=== Main application configurations ==="
find src/main -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" | xargs cat 2>/dev/null || echo "No main configs found"
echo -e "\n=== Checking if PostRepositoryImpl is tested ==="
fd -e java | xargs rg -l "PostRepositoryImpl|PostRepository" | grep -i test
echo -e "\n=== Check build.gradle for test profile ==="
cat build.gradle 2>/dev/null | grep -A 5 -B 5 "test\|h2" || echo "Could not read build.gradle"Repository: mmije0ng/FarmON_BE_Refactor Length of output: 2073 🏁 Script executed: #!/bin/bash
# Look at the PostRepositoryImpl file to understand the context better
echo "=== PostRepositoryImpl.java content around line 191-196 ==="
if [ -f "src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java" ]; then
cat -n "src/main/java/com/backend/farmon/repository/PostRepository/PostRepositoryImpl.java" | sed -n '180,210p'
else
echo "File not found, searching for it..."
find . -name "PostRepositoryImpl.java" -type f
fi
echo -e "\n=== Check if there are test files for PostRepository ==="
find . -path "*test*" -name "*PostRepository*Test*.java" -o -path "*test*" -name "*Post*RepositoryTest.java"
echo -e "\n=== Look for TestApplication or test config ==="
find . -path "*test*" -name "TestApplication.java" -o -path "*test*" -name "Test*.java" | head -10
echo -e "\n=== Check for @DataJpaTest or repository test annotations ==="
rg "@DataJpaTest|@SpringBootTest|testcontainers" -t java | head -20Repository: mmije0ng/FarmON_BE_Refactor Length of output: 1640 🏁 Script executed: #!/bin/bash
# Check if there's a default test configuration when no explicit config exists
echo "=== Check for application.yml or application.properties in test resources ==="
ls -la src/test/resources/ 2>/dev/null || echo "test/resources not found"
echo -e "\n=== Look for spring profiles in test ==="
rg "spring.profiles.active|@ActiveProfiles" -t java
echo -e "\n=== Check FarmonApplicationTests.java ==="
cat -n src/test/java/com/backend/farmon/FarmonApplicationTests.java
echo -e "\n=== Search for any test that calls PostRepository methods ==="
rg -r "getPostsWithPopularByCategory|popularPostsIdList" --type=java src/test/
echo -e "\n=== Check Spring Boot default test database behavior ==="
grep -i "test.database\|datasource.url" src/test/resources/*.properties src/test/resources/*.yml 2>/dev/null || echo "No test datasource config found"
echo -e "\n=== Overall test directory structure ==="
find src/test -type f -name "*.java" -o -name "*.properties" -o -name "*.yml" | head -20Repository: mmije0ng/FarmON_BE_Refactor Length of output: 815 MySQL 프로젝트의
🤖 Prompt for AI Agents |
||
|
|
||
| query.orderBy( | ||
| pinnedFirstOrderExpr.asc(), | ||
| pinnedInnerOrderExpr.asc(), | ||
| likeCount.id.countDistinct().desc(), | ||
| post.createdAt.desc() | ||
| ); | ||
| } else { | ||
| query.orderBy( | ||
| likeCount.id.countDistinct().desc(), | ||
| post.createdAt.desc() | ||
| ); | ||
| } | ||
|
|
||
| return query | ||
| .limit(limit) | ||
| .fetch(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
다중 캐시 TTL 구성이 적절합니다.
캐시별 TTL 분리(home:community 60초, home:popularExpertColumn 5분)가 데이터 특성에 맞게 잘 설정되었습니다.
다만,
base설정에 기본 TTL이 없어 명시적으로 등록되지 않은 캐시는 무제한 TTL을 갖게 됩니다. 향후 다른 캐시가 추가될 경우를 대비해 기본 TTL 설정을 고려해 보세요.🔧 기본 TTL 추가 제안
RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofMinutes(10)) // 기본 TTL .disableCachingNullValues() .serializeKeysWith(...) .serializeValuesWith(...);📝 Committable suggestion
🤖 Prompt for AI Agents