From ca38ffbc288dff39f1d5eef49ce34526b94126ac Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 14:56:36 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20post=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=B5=9C=EC=83=81=EC=9C=84=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=8A=B9=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/BookmarkCommandService.java | 4 +- .../query/BookmarkQueryService.java | 2 +- .../activity/bookmark/domain/Bookmark.java | 2 +- .../infrastructure/BookmarkRepository.java | 2 +- .../command/ReadPostCommandService.java | 6 +- .../query/ReadPostQueryService.java | 2 +- .../readpost/domain/FirstReadPost.java | 2 +- .../activity/readpost/domain/ReadPost.java | 2 +- .../domain/ReadPostFirstReadPolicy.java | 2 +- .../PersonalizationProfileService.java | 2 +- .../converter/RecommendationConverter.java | 72 +- .../dto/RecommendationListResponse.java | 26 +- .../entity/RecommendationHistory.java | 2 +- .../entity/RecommendedPost.java | 2 +- .../service/LlmRecommendationService.java | 6 +- .../search/service/SearchServiceImpl.java | 6 +- .../domain/source/batch/PostBatchWriter.java | 114 +-- .../domain/source/batch/RssFeedReader.java | 694 +++++++++--------- .../source/batch/RssToPostProcessor.java | 58 +- .../source/config/RssCrawlingJobConfig.java | 424 +++++------ .../config/ElasticsearchCacheManager.java | 2 +- .../batch/PostEmbeddingProcessor.java | 140 ++-- .../batch/PostEmbeddingReader.java | 14 +- .../batch/PostEmbeddingWriter.java | 154 ++-- .../batch/PostSummaryProcessor.java | 58 +- .../application}/batch/PostSummaryReader.java | 72 +- .../application}/batch/PostSummaryWriter.java | 218 +++--- .../command}/PostCommandService.java | 4 +- .../application}/converter/PostConverter.java | 126 ++-- .../application}/dto/CompanyDto.java | 2 +- .../application}/dto/CompanyListResponse.java | 2 +- .../application}/dto/PostDetailDto.java | 2 +- .../application}/dto/PostInfoDto.java | 2 +- .../application}/dto/PostListResponse.java | 2 +- .../dto/SummaryWithKeywordsDto.java | 20 +- .../query}/PostKeywordLookupService.java | 6 +- .../application/query}/PostLookupService.java | 8 +- .../application/query}/PostQueryService.java | 14 +- .../support}/ContentChunkerService.java | 400 +++++----- .../support}/SummaryExtractionService.java | 184 ++--- .../post/entity => post/domain}/Post.java | 218 +++--- .../entity => post/domain}/PostKeyword.java | 74 +- .../domain}/enums/EPostSortType.java | 26 +- .../domain}/exception/PostErrorCode.java | 48 +- .../domain/projection}/ContentChunk.java | 62 +- .../domain/projection}/PostDocument.java | 198 ++--- .../PostDocumentRepository.java | 26 +- .../PostKeywordRepository.java | 26 +- .../infrastructure}/PostRepository.java | 274 +++---- .../presentation}/PostController.java | 188 ++--- .../presentation}/PostControllerV2.java | 10 +- 51 files changed, 2005 insertions(+), 2005 deletions(-) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingProcessor.java (88%) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingReader.java (91%) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingWriter.java (92%) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostSummaryProcessor.java (82%) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostSummaryReader.java (84%) rename src/main/java/com/techfork/{domain/post => post/application}/batch/PostSummaryWriter.java (93%) rename src/main/java/com/techfork/{domain/post/service => post/application/command}/PostCommandService.java (92%) rename src/main/java/com/techfork/{domain/post => post/application}/converter/PostConverter.java (93%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/CompanyDto.java (85%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/CompanyListResponse.java (88%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/PostDetailDto.java (91%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/PostInfoDto.java (92%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/PostListResponse.java (89%) rename src/main/java/com/techfork/{domain/post => post/application}/dto/SummaryWithKeywordsDto.java (76%) rename src/main/java/com/techfork/{domain/post/service => post/application/query}/PostKeywordLookupService.java (84%) rename src/main/java/com/techfork/{domain/post/service => post/application/query}/PostLookupService.java (72%) rename src/main/java/com/techfork/{domain/post/service => post/application/query}/PostQueryService.java (95%) rename src/main/java/com/techfork/{domain/post/service => post/application/support}/ContentChunkerService.java (96%) rename src/main/java/com/techfork/{domain/post/service => post/application/support}/SummaryExtractionService.java (95%) rename src/main/java/com/techfork/{domain/post/entity => post/domain}/Post.java (96%) rename src/main/java/com/techfork/{domain/post/entity => post/domain}/PostKeyword.java (92%) rename src/main/java/com/techfork/{domain/post => post/domain}/enums/EPostSortType.java (80%) rename src/main/java/com/techfork/{domain/post => post/domain}/exception/PostErrorCode.java (90%) rename src/main/java/com/techfork/{domain/post/document => post/domain/projection}/ContentChunk.java (91%) rename src/main/java/com/techfork/{domain/post/document => post/domain/projection}/PostDocument.java (94%) rename src/main/java/com/techfork/{domain/post/repository => post/infrastructure}/PostDocumentRepository.java (74%) rename src/main/java/com/techfork/{domain/post/repository => post/infrastructure}/PostKeywordRepository.java (79%) rename src/main/java/com/techfork/{domain/post/repository => post/infrastructure}/PostRepository.java (86%) rename src/main/java/com/techfork/{domain/post/controller => post/presentation}/PostController.java (91%) rename src/main/java/com/techfork/{domain/post/controller => post/presentation}/PostControllerV2.java (95%) diff --git a/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java b/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java index 4b080fa0..6292acf4 100644 --- a/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java +++ b/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java @@ -3,8 +3,8 @@ import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.domain.BookmarkErrorCode; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.service.PostLookupService; +import com.techfork.post.domain.Post; +import com.techfork.post.application.query.PostLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.exception.GeneralException; diff --git a/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java b/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java index 77710efb..88c8d5e2 100644 --- a/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java +++ b/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java @@ -2,7 +2,7 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkQueryRow; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.service.PostKeywordLookupService; +import com.techfork.post.application.query.PostKeywordLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; diff --git a/src/main/java/com/techfork/activity/bookmark/domain/Bookmark.java b/src/main/java/com/techfork/activity/bookmark/domain/Bookmark.java index 27fc606f..cbaaac03 100644 --- a/src/main/java/com/techfork/activity/bookmark/domain/Bookmark.java +++ b/src/main/java/com/techfork/activity/bookmark/domain/Bookmark.java @@ -1,6 +1,6 @@ package com.techfork.activity.bookmark.domain; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepository.java b/src/main/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepository.java index 2071f93c..f6588ee9 100644 --- a/src/main/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepository.java +++ b/src/main/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepository.java @@ -1,7 +1,7 @@ package com.techfork.activity.bookmark.infrastructure; import com.techfork.activity.bookmark.domain.Bookmark; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java index 29589952..92a9204b 100644 --- a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java +++ b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java @@ -4,9 +4,9 @@ import com.techfork.activity.readpost.domain.ReadPostErrorCode; import com.techfork.activity.readpost.domain.ReadPostFirstReadPolicy; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.service.PostCommandService; -import com.techfork.domain.post.service.PostLookupService; +import com.techfork.post.domain.Post; +import com.techfork.post.application.command.PostCommandService; +import com.techfork.post.application.query.PostLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.exception.GeneralException; diff --git a/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java b/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java index 30eb1001..44095026 100644 --- a/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java +++ b/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java @@ -3,7 +3,7 @@ import com.techfork.activity.bookmark.application.query.lookup.BookmarkLookupService; import com.techfork.activity.readpost.infrastructure.ReadPostQueryRow; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.service.PostKeywordLookupService; +import com.techfork.post.application.query.PostKeywordLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/techfork/activity/readpost/domain/FirstReadPost.java b/src/main/java/com/techfork/activity/readpost/domain/FirstReadPost.java index a99c0258..afa12d5d 100644 --- a/src/main/java/com/techfork/activity/readpost/domain/FirstReadPost.java +++ b/src/main/java/com/techfork/activity/readpost/domain/FirstReadPost.java @@ -1,6 +1,6 @@ package com.techfork.activity.readpost.domain; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.BaseEntity; import jakarta.persistence.Column; diff --git a/src/main/java/com/techfork/activity/readpost/domain/ReadPost.java b/src/main/java/com/techfork/activity/readpost/domain/ReadPost.java index cc0fd422..36ebe383 100644 --- a/src/main/java/com/techfork/activity/readpost/domain/ReadPost.java +++ b/src/main/java/com/techfork/activity/readpost/domain/ReadPost.java @@ -1,6 +1,6 @@ package com.techfork.activity.readpost.domain; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.BaseEntity; import jakarta.persistence.Column; diff --git a/src/main/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicy.java b/src/main/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicy.java index 961ed586..ceed5567 100644 --- a/src/main/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicy.java +++ b/src/main/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicy.java @@ -1,7 +1,7 @@ package com.techfork.activity.readpost.domain; import com.techfork.activity.readpost.infrastructure.FirstReadPostRepository; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java b/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java index c91c103c..a756cf0b 100644 --- a/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java +++ b/src/main/java/com/techfork/domain/personalization/service/PersonalizationProfileService.java @@ -6,7 +6,7 @@ import com.techfork.activity.readhistory.infrastructure.SearchHistoryRepository; import com.techfork.activity.readpost.domain.ReadPost; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.post.domain.PostKeyword; import com.techfork.domain.recommendation.service.RecommendationService; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import com.techfork.domain.useraccount.entity.User; diff --git a/src/main/java/com/techfork/domain/recommendation/converter/RecommendationConverter.java b/src/main/java/com/techfork/domain/recommendation/converter/RecommendationConverter.java index 4f94b133..b49479bf 100644 --- a/src/main/java/com/techfork/domain/recommendation/converter/RecommendationConverter.java +++ b/src/main/java/com/techfork/domain/recommendation/converter/RecommendationConverter.java @@ -1,16 +1,16 @@ -package com.techfork.domain.recommendation.converter; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; +package com.techfork.domain.recommendation.converter; + +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; import com.techfork.domain.recommendation.dto.RecommendationListResponse; import com.techfork.domain.recommendation.dto.RecommendedPostDto; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; - -import java.util.List; - + +import java.util.List; + @Component @RequiredArgsConstructor public class RecommendationConverter { @@ -18,27 +18,27 @@ public class RecommendationConverter { private final CloudflareThirdPartyThumbnailOptimizer thumbnailOptimizer; public RecommendationListResponse toRecommendationListResponse(List recommendedPosts) { - List dtos = recommendedPosts.stream() - .map(this::toRecommendedPostDto) - .toList(); - - return RecommendationListResponse.builder() - .recommendations(dtos) - .totalCount(dtos.size()) - .build(); - } - - public RecommendedPostDto toRecommendedPostDto(RecommendedPost recommendedPost) { - Post post = recommendedPost.getPost(); - - List keywords = post.getKeywords().stream() - .map(PostKeyword::getKeyword) - .toList(); - - return RecommendedPostDto.builder() - .id(recommendedPost.getId()) - .postId(post.getId()) - .title(post.getTitle()) + List dtos = recommendedPosts.stream() + .map(this::toRecommendedPostDto) + .toList(); + + return RecommendationListResponse.builder() + .recommendations(dtos) + .totalCount(dtos.size()) + .build(); + } + + public RecommendedPostDto toRecommendedPostDto(RecommendedPost recommendedPost) { + Post post = recommendedPost.getPost(); + + List keywords = post.getKeywords().stream() + .map(PostKeyword::getKeyword) + .toList(); + + return RecommendedPostDto.builder() + .id(recommendedPost.getId()) + .postId(post.getId()) + .title(post.getTitle()) .shortSummary(post.getShortSummary()) .company(post.getCompany()) .url(post.getUrl()) @@ -47,11 +47,11 @@ public RecommendedPostDto toRecommendedPostDto(RecommendedPost recommendedPost) .viewCount(post.getViewCount()) .isBookmarked(null) // Will be set later in service layer .publishedAt(post.getPublishedAt()) - .keywords(keywords) - .similarityScore(recommendedPost.getSimilarityScore()) - .mmrScore(recommendedPost.getMmrScore()) - .rank(recommendedPost.getRankOrder()) - .recommendedAt(recommendedPost.getRecommendedAt()) - .build(); - } -} + .keywords(keywords) + .similarityScore(recommendedPost.getSimilarityScore()) + .mmrScore(recommendedPost.getMmrScore()) + .rank(recommendedPost.getRankOrder()) + .recommendedAt(recommendedPost.getRecommendedAt()) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java b/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java index 93e975ac..59e20fb8 100644 --- a/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java +++ b/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java @@ -1,13 +1,13 @@ -package com.techfork.domain.recommendation.dto; - -import com.techfork.domain.post.dto.PostInfoDto; -import lombok.Builder; - -import java.util.List; - -@Builder -public record RecommendationListResponse( - List recommendations, - int totalCount -) { -} +package com.techfork.domain.recommendation.dto; + +import com.techfork.post.application.dto.PostInfoDto; +import lombok.Builder; + +import java.util.List; + +@Builder +public record RecommendationListResponse( + List recommendations, + int totalCount +) { +} diff --git a/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java b/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java index 6cff4381..a681071b 100644 --- a/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java +++ b/src/main/java/com/techfork/domain/recommendation/entity/RecommendationHistory.java @@ -1,6 +1,6 @@ package com.techfork.domain.recommendation.entity; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java b/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java index 039a12a1..4e092b71 100644 --- a/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java +++ b/src/main/java/com/techfork/domain/recommendation/entity/RecommendedPost.java @@ -1,6 +1,6 @@ package com.techfork.domain.recommendation.entity; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.global.common.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java index 32297b53..3d228d02 100644 --- a/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java +++ b/src/main/java/com/techfork/domain/recommendation/service/LlmRecommendationService.java @@ -7,9 +7,9 @@ import co.elastic.clients.elasticsearch.core.search.Hit; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.global.elasticsearch.query.VectorQueryBuilder; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.recommendation.config.RecommendationProperties; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.recommendation.entity.RecommendationHistory; diff --git a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java index e4c5baff..a3ae9adc 100644 --- a/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java +++ b/src/main/java/com/techfork/domain/search/service/SearchServiceImpl.java @@ -7,9 +7,9 @@ import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.dto.SearchResult; diff --git a/src/main/java/com/techfork/domain/source/batch/PostBatchWriter.java b/src/main/java/com/techfork/domain/source/batch/PostBatchWriter.java index 782d5bb7..acc20e3c 100644 --- a/src/main/java/com/techfork/domain/source/batch/PostBatchWriter.java +++ b/src/main/java/com/techfork/domain/source/batch/PostBatchWriter.java @@ -1,57 +1,57 @@ -package com.techfork.domain.source.batch; - -import com.techfork.domain.post.entity.Post; -import com.techfork.global.util.JdbcBatchExecutor; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * Post를 배치로 저장하는 Writer - * Processor에서 중복 체크가 완료되므로 여기서는 단순 저장만 수행 - * JDBC Bulk Insert를 사용하여 성능 최적화 - */ -@Slf4j -@Component -@StepScope -@RequiredArgsConstructor -public class PostBatchWriter implements ItemWriter { - - private final JdbcBatchExecutor jdbcBatchExecutor; - - private static final String INSERT_SQL = """ - INSERT INTO posts - (title, full_content, plain_content, company, url, logo_url, thumbnail_url, published_at, crawled_at, view_count, tech_blog_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """; - - @Override - public void write(Chunk chunk) { - if (chunk.isEmpty()) { - return; - } - - List items = chunk.getItems(); - - int inserted = jdbcBatchExecutor.batchExecute(INSERT_SQL, items, (ps, post, i) -> { - ps.setString(1, post.getTitle()); - ps.setString(2, post.getFullContent()); - ps.setString(3, post.getPlainContent()); - ps.setString(4, post.getCompany()); - ps.setString(5, post.getUrl()); - ps.setString(6, post.getLogoUrl()); - ps.setString(7, post.getThumbnailUrl()); - ps.setTimestamp(8, JdbcBatchExecutor.toTimestamp(post.getPublishedAt())); - ps.setTimestamp(9, JdbcBatchExecutor.toTimestamp(post.getCrawledAt())); - ps.setLong(10, 0L); - ps.setLong(11, post.getTechBlog().getId()); - }); - - log.info("{}개 게시글 Bulk Insert 완료", inserted); - } -} +package com.techfork.domain.source.batch; + +import com.techfork.post.domain.Post; +import com.techfork.global.util.JdbcBatchExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Post를 배치로 저장하는 Writer + * Processor에서 중복 체크가 완료되므로 여기서는 단순 저장만 수행 + * JDBC Bulk Insert를 사용하여 성능 최적화 + */ +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class PostBatchWriter implements ItemWriter { + + private final JdbcBatchExecutor jdbcBatchExecutor; + + private static final String INSERT_SQL = """ + INSERT INTO posts + (title, full_content, plain_content, company, url, logo_url, thumbnail_url, published_at, crawled_at, view_count, tech_blog_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + @Override + public void write(Chunk chunk) { + if (chunk.isEmpty()) { + return; + } + + List items = chunk.getItems(); + + int inserted = jdbcBatchExecutor.batchExecute(INSERT_SQL, items, (ps, post, i) -> { + ps.setString(1, post.getTitle()); + ps.setString(2, post.getFullContent()); + ps.setString(3, post.getPlainContent()); + ps.setString(4, post.getCompany()); + ps.setString(5, post.getUrl()); + ps.setString(6, post.getLogoUrl()); + ps.setString(7, post.getThumbnailUrl()); + ps.setTimestamp(8, JdbcBatchExecutor.toTimestamp(post.getPublishedAt())); + ps.setTimestamp(9, JdbcBatchExecutor.toTimestamp(post.getCrawledAt())); + ps.setLong(10, 0L); + ps.setLong(11, post.getTechBlog().getId()); + }); + + log.info("{}개 게시글 Bulk Insert 완료", inserted); + } +} diff --git a/src/main/java/com/techfork/domain/source/batch/RssFeedReader.java b/src/main/java/com/techfork/domain/source/batch/RssFeedReader.java index 06352705..e9fc566c 100644 --- a/src/main/java/com/techfork/domain/source/batch/RssFeedReader.java +++ b/src/main/java/com/techfork/domain/source/batch/RssFeedReader.java @@ -1,348 +1,348 @@ -package com.techfork.domain.source.batch; - -import com.rometools.modules.mediarss.MediaEntryModule; -import com.rometools.modules.mediarss.types.MediaContent; -import com.rometools.rome.feed.synd.SyndEnclosure; -import com.rometools.rome.feed.synd.SyndEntry; -import com.rometools.rome.feed.synd.SyndFeed; -import com.rometools.rome.io.SyndFeedInput; -import com.rometools.rome.io.XmlReader; -import com.techfork.domain.post.repository.PostRepository; -import com.techfork.domain.source.dto.RssFeedItem; -import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.global.util.ContentCleaner; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -@Component -@StepScope -@Slf4j -public class RssFeedReader implements ItemReader { - - private static final int RSS_FETCH_TASK_TIMEOUT_SECONDS = 45; - - private final TechBlogRepository techBlogRepository; - private final PostRepository postRepository; - private final WebClient webClient; - @Qualifier("rssFetchTaskExecutor") - private final AsyncTaskExecutor rssFetchTaskExecutor; - private final int rssFetchTaskTimeoutSeconds; - - private List items; - private int currentIndex = 0; - - @Autowired - public RssFeedReader( - TechBlogRepository techBlogRepository, - PostRepository postRepository, - WebClient webClient, - @Qualifier("rssFetchTaskExecutor") AsyncTaskExecutor rssFetchTaskExecutor - ) { - this( - techBlogRepository, - postRepository, - webClient, - rssFetchTaskExecutor, - RSS_FETCH_TASK_TIMEOUT_SECONDS - ); - } - - RssFeedReader( - TechBlogRepository techBlogRepository, - PostRepository postRepository, - WebClient webClient, - @Qualifier("rssFetchTaskExecutor") AsyncTaskExecutor rssFetchTaskExecutor, - int rssFetchTaskTimeoutSeconds - ) { - this.techBlogRepository = techBlogRepository; - this.postRepository = postRepository; - this.webClient = webClient; - this.rssFetchTaskExecutor = rssFetchTaskExecutor; - this.rssFetchTaskTimeoutSeconds = rssFetchTaskTimeoutSeconds; - } - - @Override - public RssFeedItem read() { - if (items == null) { - initializeItems(); - } - - if (currentIndex >= items.size()) { - log.info("모든 RSS 피드 수집 완료: 총 {}개", items.size()); - return null; - } - - return items.get(currentIndex++); - } - - private void initializeItems() { - List techBlogs = techBlogRepository.findAll(); - log.info("총 {}개 테크 블로그 RSS 수집 시작", techBlogs.size()); - - List fetchTasks = techBlogs.stream() - .map(this::submitFetchTask) - .toList(); - - List allItems = fetchTasks.stream() - .flatMap(this::collectFeedItems) - .toList(); - - Set existingUrls = postRepository.findExistingUrls( - allItems.stream().map(RssFeedItem::url).toList() - ); - - Map uniqueItemsByUrl = new LinkedHashMap<>(); - for (RssFeedItem item : allItems) { - if (!existingUrls.contains(item.url())) { - uniqueItemsByUrl.putIfAbsent(item.url(), item); - } - } - - items = List.copyOf(uniqueItemsByUrl.values()); - - log.info("RSS 수집 초기화 완료: 총 {}개 아이템", items.size()); - } - - private FeedFetchTask submitFetchTask(TechBlog techBlog) { - Future> future = rssFetchTaskExecutor.submit(() -> fetchFeedSafely(techBlog)); - return new FeedFetchTask(techBlog, future); - } - - private List fetchFeedSafely(TechBlog techBlog) { - try { - List feedItems = fetchRssFeed(techBlog); - log.info("[{}] RSS 수집 성공: {}개", techBlog.getCompanyName(), feedItems.size()); - return feedItems; - } catch (Exception e) { - log.error("[{}] RSS 수집 실패: {}", techBlog.getCompanyName(), e.getMessage()); - return List.of(); - } - } - - private Stream collectFeedItems(FeedFetchTask fetchTask) { - try { - return fetchTask.future() - .get(rssFetchTaskTimeoutSeconds, TimeUnit.SECONDS) - .stream(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("[{}] RSS 수집 대기 중 인터럽트 발생", fetchTask.techBlog().getCompanyName(), e); - return Stream.empty(); - } catch (TimeoutException e) { - boolean cancelled = fetchTask.future().cancel(true); - log.error("[{}] RSS 수집 타임아웃: {}초 (cancelled={})", - fetchTask.techBlog().getCompanyName(), - rssFetchTaskTimeoutSeconds, - cancelled); - return Stream.empty(); - } catch (ExecutionException e) { - Throwable cause = e.getCause() != null ? e.getCause() : e; - log.error("[{}] RSS 수집 Future 처리 실패: {}", fetchTask.techBlog().getCompanyName(), cause.getMessage()); - return Stream.empty(); - } - } - - private List fetchRssFeed(TechBlog techBlog) throws Exception { - // WebClient로 RSS 피드 다운로드 - byte[] responseBytes = webClient.get() - .uri(techBlog.getRssUrl()) - .retrieve() - .bodyToMono(byte[].class) - .block(); // 동기 처리 (Spring Batch에서는 동기 필요) - - if (responseBytes == null || responseBytes.length == 0) { - throw new Exception("Empty response from RSS feed"); - } - - // RSS 파싱 - try (InputStream inputStream = new ByteArrayInputStream(responseBytes); - XmlReader reader = new XmlReader(inputStream)) { - - SyndFeedInput input = new SyndFeedInput(); - SyndFeed feed = input.build(reader); - - return feed.getEntries().stream() - .map(entry -> convertToFeedItem(entry, techBlog)) - .toList(); - } - } - - /** - * SyndEntry를 RssFeedItem으로 변환 - */ - private RssFeedItem convertToFeedItem(SyndEntry entry, TechBlog techBlog) { - // 본문 추출 (description 또는 content 중 더 긴 것 사용) - String content = extractContent(entry); - - // HTML 태그 및 마크다운 제거한 plain text 생성 - String plainContent = ContentCleaner.clean(content); - - // 발행일 변환 - LocalDateTime publishedAt = convertToLocalDateTime(entry.getPublishedDate()); - - // 썸네일 이미지 추출 - String thumbnailUrl = extractThumbnailUrl(entry, content); - - return RssFeedItem.builder() - .title(entry.getTitle()) - .url(entry.getLink()) - .content(content) - .plainContent(plainContent) - .publishedAt(publishedAt) - .company(techBlog.getCompanyName()) - .logoUrl(techBlog.getLogoUrl()) - .thumbnailUrl(thumbnailUrl) - .techBlogId(techBlog.getId()) - .build(); - } - - /** - * RSS entry에서 본문 추출 - * description과 content:encoded 중 더 긴 것을 선택 - */ - private String extractContent(SyndEntry entry) { - String description = entry.getDescription() != null - ? entry.getDescription().getValue() - : ""; - - String content = ""; - if (entry.getContents() != null && !entry.getContents().isEmpty()) { - content = entry.getContents().get(0).getValue(); - } - - // 더 긴 것을 선택 (보통 content:encoded가 전체 본문) - return content.length() > description.length() ? content : description; - } - - /** - * Date를 LocalDateTime으로 변환 - * publishedDate가 없으면 crawledAt 시점을 사용 - */ - private LocalDateTime convertToLocalDateTime(Date date) { - if (date == null) { - return LocalDateTime.now(); - } - return date.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - } - - /** - * RSS 피드에서 썸네일 이미지 URL 추출 - * 1. Media RSS 모듈 (media:thumbnail, media:content) - * 2. Enclosure (주로 이미지/오디오 첨부파일) - * 3. 본문 HTML에서 첫 번째 img 태그 추출 - */ - private String extractThumbnailUrl(SyndEntry entry, String content) { - // 1. Media RSS 모듈에서 추출 시도 - String mediaThumbnail = extractFromMediaModule(entry); - if (mediaThumbnail != null) { - return mediaThumbnail; - } - - // 2. Enclosure에서 이미지 추출 시도 - String enclosureImage = extractFromEnclosure(entry); - if (enclosureImage != null) { - return enclosureImage; - } - - // 3. 본문 HTML에서 첫 번째 이미지 추출 - return extractImageFromHtml(content); - } - - /** - * Media RSS 모듈에서 이미지 추출 - */ - private String extractFromMediaModule(SyndEntry entry) { - try { - Object mediaModule = entry.getModule("http://search.yahoo.com/mrss/"); - if (mediaModule != null) { - // Rome Tools의 Media RSS 모듈 사용 - MediaEntryModule media = - (MediaEntryModule) mediaModule; - - if (media.getMediaContents() != null && media.getMediaContents().length > 0) { - MediaContent mediaContent = media.getMediaContents()[0]; - if (mediaContent.getReference() != null) { - return mediaContent.getReference().toString(); - } - } - - if (media.getMetadata() != null && media.getMetadata().getThumbnail() != null - && media.getMetadata().getThumbnail().length > 0) { - return media.getMetadata().getThumbnail()[0].getUrl().toString(); - } - } - } catch (Exception e) { - log.debug("Media RSS 모듈에서 이미지 추출 실패: {}", e.getMessage()); - } - return null; - } - - /** - * Enclosure에서 이미지 추출 - */ - private String extractFromEnclosure(SyndEntry entry) { - if (entry.getEnclosures() != null && !entry.getEnclosures().isEmpty()) { - for (SyndEnclosure enclosure : entry.getEnclosures()) { - String type = enclosure.getType(); - if (type != null && type.startsWith("image/")) { - return enclosure.getUrl(); - } - } - } - return null; - } - - /** - * HTML 본문에서 첫 번째 img 태그의 src 추출 - */ - private String extractImageFromHtml(String htmlContent) { - if (htmlContent == null || htmlContent.isEmpty()) { - return null; - } - - // img 태그의 src 속성을 추출하는 정규식 - Pattern pattern = Pattern.compile("]+src=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE); - Matcher matcher = pattern.matcher(htmlContent); - - if (matcher.find()) { - String imageUrl = matcher.group(1); - // 상대 URL이 아닌 절대 URL만 반환 (http:// 또는 https://로 시작) - if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { - return imageUrl; - } - } - - return null; - } - - private record FeedFetchTask(TechBlog techBlog, Future> future) { - } +package com.techfork.domain.source.batch; + +import com.rometools.modules.mediarss.MediaEntryModule; +import com.rometools.modules.mediarss.types.MediaContent; +import com.rometools.rome.feed.synd.SyndEnclosure; +import com.rometools.rome.feed.synd.SyndEntry; +import com.rometools.rome.feed.synd.SyndFeed; +import com.rometools.rome.io.SyndFeedInput; +import com.rometools.rome.io.XmlReader; +import com.techfork.post.infrastructure.PostRepository; +import com.techfork.domain.source.dto.RssFeedItem; +import com.techfork.domain.source.entity.TechBlog; +import com.techfork.domain.source.repository.TechBlogRepository; +import com.techfork.global.util.ContentCleaner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +@Component +@StepScope +@Slf4j +public class RssFeedReader implements ItemReader { + + private static final int RSS_FETCH_TASK_TIMEOUT_SECONDS = 45; + + private final TechBlogRepository techBlogRepository; + private final PostRepository postRepository; + private final WebClient webClient; + @Qualifier("rssFetchTaskExecutor") + private final AsyncTaskExecutor rssFetchTaskExecutor; + private final int rssFetchTaskTimeoutSeconds; + + private List items; + private int currentIndex = 0; + + @Autowired + public RssFeedReader( + TechBlogRepository techBlogRepository, + PostRepository postRepository, + WebClient webClient, + @Qualifier("rssFetchTaskExecutor") AsyncTaskExecutor rssFetchTaskExecutor + ) { + this( + techBlogRepository, + postRepository, + webClient, + rssFetchTaskExecutor, + RSS_FETCH_TASK_TIMEOUT_SECONDS + ); + } + + RssFeedReader( + TechBlogRepository techBlogRepository, + PostRepository postRepository, + WebClient webClient, + @Qualifier("rssFetchTaskExecutor") AsyncTaskExecutor rssFetchTaskExecutor, + int rssFetchTaskTimeoutSeconds + ) { + this.techBlogRepository = techBlogRepository; + this.postRepository = postRepository; + this.webClient = webClient; + this.rssFetchTaskExecutor = rssFetchTaskExecutor; + this.rssFetchTaskTimeoutSeconds = rssFetchTaskTimeoutSeconds; + } + + @Override + public RssFeedItem read() { + if (items == null) { + initializeItems(); + } + + if (currentIndex >= items.size()) { + log.info("모든 RSS 피드 수집 완료: 총 {}개", items.size()); + return null; + } + + return items.get(currentIndex++); + } + + private void initializeItems() { + List techBlogs = techBlogRepository.findAll(); + log.info("총 {}개 테크 블로그 RSS 수집 시작", techBlogs.size()); + + List fetchTasks = techBlogs.stream() + .map(this::submitFetchTask) + .toList(); + + List allItems = fetchTasks.stream() + .flatMap(this::collectFeedItems) + .toList(); + + Set existingUrls = postRepository.findExistingUrls( + allItems.stream().map(RssFeedItem::url).toList() + ); + + Map uniqueItemsByUrl = new LinkedHashMap<>(); + for (RssFeedItem item : allItems) { + if (!existingUrls.contains(item.url())) { + uniqueItemsByUrl.putIfAbsent(item.url(), item); + } + } + + items = List.copyOf(uniqueItemsByUrl.values()); + + log.info("RSS 수집 초기화 완료: 총 {}개 아이템", items.size()); + } + + private FeedFetchTask submitFetchTask(TechBlog techBlog) { + Future> future = rssFetchTaskExecutor.submit(() -> fetchFeedSafely(techBlog)); + return new FeedFetchTask(techBlog, future); + } + + private List fetchFeedSafely(TechBlog techBlog) { + try { + List feedItems = fetchRssFeed(techBlog); + log.info("[{}] RSS 수집 성공: {}개", techBlog.getCompanyName(), feedItems.size()); + return feedItems; + } catch (Exception e) { + log.error("[{}] RSS 수집 실패: {}", techBlog.getCompanyName(), e.getMessage()); + return List.of(); + } + } + + private Stream collectFeedItems(FeedFetchTask fetchTask) { + try { + return fetchTask.future() + .get(rssFetchTaskTimeoutSeconds, TimeUnit.SECONDS) + .stream(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("[{}] RSS 수집 대기 중 인터럽트 발생", fetchTask.techBlog().getCompanyName(), e); + return Stream.empty(); + } catch (TimeoutException e) { + boolean cancelled = fetchTask.future().cancel(true); + log.error("[{}] RSS 수집 타임아웃: {}초 (cancelled={})", + fetchTask.techBlog().getCompanyName(), + rssFetchTaskTimeoutSeconds, + cancelled); + return Stream.empty(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + log.error("[{}] RSS 수집 Future 처리 실패: {}", fetchTask.techBlog().getCompanyName(), cause.getMessage()); + return Stream.empty(); + } + } + + private List fetchRssFeed(TechBlog techBlog) throws Exception { + // WebClient로 RSS 피드 다운로드 + byte[] responseBytes = webClient.get() + .uri(techBlog.getRssUrl()) + .retrieve() + .bodyToMono(byte[].class) + .block(); // 동기 처리 (Spring Batch에서는 동기 필요) + + if (responseBytes == null || responseBytes.length == 0) { + throw new Exception("Empty response from RSS feed"); + } + + // RSS 파싱 + try (InputStream inputStream = new ByteArrayInputStream(responseBytes); + XmlReader reader = new XmlReader(inputStream)) { + + SyndFeedInput input = new SyndFeedInput(); + SyndFeed feed = input.build(reader); + + return feed.getEntries().stream() + .map(entry -> convertToFeedItem(entry, techBlog)) + .toList(); + } + } + + /** + * SyndEntry를 RssFeedItem으로 변환 + */ + private RssFeedItem convertToFeedItem(SyndEntry entry, TechBlog techBlog) { + // 본문 추출 (description 또는 content 중 더 긴 것 사용) + String content = extractContent(entry); + + // HTML 태그 및 마크다운 제거한 plain text 생성 + String plainContent = ContentCleaner.clean(content); + + // 발행일 변환 + LocalDateTime publishedAt = convertToLocalDateTime(entry.getPublishedDate()); + + // 썸네일 이미지 추출 + String thumbnailUrl = extractThumbnailUrl(entry, content); + + return RssFeedItem.builder() + .title(entry.getTitle()) + .url(entry.getLink()) + .content(content) + .plainContent(plainContent) + .publishedAt(publishedAt) + .company(techBlog.getCompanyName()) + .logoUrl(techBlog.getLogoUrl()) + .thumbnailUrl(thumbnailUrl) + .techBlogId(techBlog.getId()) + .build(); + } + + /** + * RSS entry에서 본문 추출 + * description과 content:encoded 중 더 긴 것을 선택 + */ + private String extractContent(SyndEntry entry) { + String description = entry.getDescription() != null + ? entry.getDescription().getValue() + : ""; + + String content = ""; + if (entry.getContents() != null && !entry.getContents().isEmpty()) { + content = entry.getContents().get(0).getValue(); + } + + // 더 긴 것을 선택 (보통 content:encoded가 전체 본문) + return content.length() > description.length() ? content : description; + } + + /** + * Date를 LocalDateTime으로 변환 + * publishedDate가 없으면 crawledAt 시점을 사용 + */ + private LocalDateTime convertToLocalDateTime(Date date) { + if (date == null) { + return LocalDateTime.now(); + } + return date.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } + + /** + * RSS 피드에서 썸네일 이미지 URL 추출 + * 1. Media RSS 모듈 (media:thumbnail, media:content) + * 2. Enclosure (주로 이미지/오디오 첨부파일) + * 3. 본문 HTML에서 첫 번째 img 태그 추출 + */ + private String extractThumbnailUrl(SyndEntry entry, String content) { + // 1. Media RSS 모듈에서 추출 시도 + String mediaThumbnail = extractFromMediaModule(entry); + if (mediaThumbnail != null) { + return mediaThumbnail; + } + + // 2. Enclosure에서 이미지 추출 시도 + String enclosureImage = extractFromEnclosure(entry); + if (enclosureImage != null) { + return enclosureImage; + } + + // 3. 본문 HTML에서 첫 번째 이미지 추출 + return extractImageFromHtml(content); + } + + /** + * Media RSS 모듈에서 이미지 추출 + */ + private String extractFromMediaModule(SyndEntry entry) { + try { + Object mediaModule = entry.getModule("http://search.yahoo.com/mrss/"); + if (mediaModule != null) { + // Rome Tools의 Media RSS 모듈 사용 + MediaEntryModule media = + (MediaEntryModule) mediaModule; + + if (media.getMediaContents() != null && media.getMediaContents().length > 0) { + MediaContent mediaContent = media.getMediaContents()[0]; + if (mediaContent.getReference() != null) { + return mediaContent.getReference().toString(); + } + } + + if (media.getMetadata() != null && media.getMetadata().getThumbnail() != null + && media.getMetadata().getThumbnail().length > 0) { + return media.getMetadata().getThumbnail()[0].getUrl().toString(); + } + } + } catch (Exception e) { + log.debug("Media RSS 모듈에서 이미지 추출 실패: {}", e.getMessage()); + } + return null; + } + + /** + * Enclosure에서 이미지 추출 + */ + private String extractFromEnclosure(SyndEntry entry) { + if (entry.getEnclosures() != null && !entry.getEnclosures().isEmpty()) { + for (SyndEnclosure enclosure : entry.getEnclosures()) { + String type = enclosure.getType(); + if (type != null && type.startsWith("image/")) { + return enclosure.getUrl(); + } + } + } + return null; + } + + /** + * HTML 본문에서 첫 번째 img 태그의 src 추출 + */ + private String extractImageFromHtml(String htmlContent) { + if (htmlContent == null || htmlContent.isEmpty()) { + return null; + } + + // img 태그의 src 속성을 추출하는 정규식 + Pattern pattern = Pattern.compile("]+src=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(htmlContent); + + if (matcher.find()) { + String imageUrl = matcher.group(1); + // 상대 URL이 아닌 절대 URL만 반환 (http:// 또는 https://로 시작) + if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { + return imageUrl; + } + } + + return null; + } + + private record FeedFetchTask(TechBlog techBlog, Future> future) { + } } \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/source/batch/RssToPostProcessor.java b/src/main/java/com/techfork/domain/source/batch/RssToPostProcessor.java index 9d716c7b..2eb0f97f 100644 --- a/src/main/java/com/techfork/domain/source/batch/RssToPostProcessor.java +++ b/src/main/java/com/techfork/domain/source/batch/RssToPostProcessor.java @@ -1,29 +1,29 @@ -package com.techfork.domain.source.batch; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.source.dto.RssFeedItem; -import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.source.repository.TechBlogRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.stereotype.Component; - -/** - * RssFeedItem을 Post 엔티티로 변환하는 Processor - */ -@Slf4j -@Component -@StepScope -@RequiredArgsConstructor -public class RssToPostProcessor implements ItemProcessor { - - private final TechBlogRepository techBlogRepository; - - @Override - public Post process(RssFeedItem item) { - TechBlog techBlog = techBlogRepository.getReferenceById(item.techBlogId()); - return Post.create(item, techBlog); - } -} +package com.techfork.domain.source.batch; + +import com.techfork.post.domain.Post; +import com.techfork.domain.source.dto.RssFeedItem; +import com.techfork.domain.source.entity.TechBlog; +import com.techfork.domain.source.repository.TechBlogRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +/** + * RssFeedItem을 Post 엔티티로 변환하는 Processor + */ +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class RssToPostProcessor implements ItemProcessor { + + private final TechBlogRepository techBlogRepository; + + @Override + public Post process(RssFeedItem item) { + TechBlog techBlog = techBlogRepository.getReferenceById(item.techBlogId()); + return Post.create(item, techBlog); + } +} diff --git a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java index ff3cf8f3..c40e5c7b 100644 --- a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java +++ b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java @@ -1,212 +1,212 @@ -package com.techfork.domain.source.config; - -import com.techfork.domain.post.batch.*; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.source.batch.PostBatchWriter; -import com.techfork.domain.source.batch.RssFeedReader; -import com.techfork.domain.source.batch.RssToPostProcessor; -import com.techfork.domain.source.dto.RssFeedItem; -import com.techfork.domain.source.listener.RssCrawlingJobListener; -import com.techfork.global.filter.MdcTaskDecorator; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.batch.integration.async.AsyncItemProcessor; -import org.springframework.batch.integration.async.AsyncItemWriter; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.ItemWriter; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.transaction.PlatformTransactionManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.Future; -import java.util.concurrent.ThreadPoolExecutor; - -/** - * RSS 크롤링 Job 설정 - * - * Step 1: RSS 피드 수집 및 저장 - * - Reader: RSS 피드를 블로그별로 수집 - * - Processor: 중복 체크 및 Post 엔티티 변환 - * - Writer: Post를 DB에 저장 (Bulk Insert) - * - * Step 2: 요약 추출 (비동기 처리) - * - Reader: 요약이 없는 Post 조회 (단일 스레드) - * - Processor: GPT API로 구조화된 요약 추출 (멀티 스레드) - * - Writer: PostSummary 저장 - * - * Step 3: 임베딩 생성 및 Elasticsearch 저장 (비동기 처리) - * - Reader: 요약이 완료되고 임베딩되지 않은 Post 조회 (단일 스레드) - * - Processor: Chunk 분할 + OpenAI 임베딩 생성 (멀티 스레드) - * - Writer: Elasticsearch에 PostDocument 저장 - */ -@Slf4j -@Configuration -@RequiredArgsConstructor -public class RssCrawlingJobConfig { - - private final JobRepository jobRepository; - private final PlatformTransactionManager transactionManager; - - private final RssFeedReader rssFeedReader; - private final RssToPostProcessor rssToPostProcessor; - private final PostBatchWriter postBatchWriter; - - private final PostSummaryReader postSummaryReader; - private final PostSummaryProcessor postSummaryProcessor; - private final PostSummaryWriter postSummaryWriter; - - private final PostEmbeddingReader postEmbeddingReader; - private final PostEmbeddingProcessor postEmbeddingProcessor; - private final PostEmbeddingWriter postEmbeddingWriter; - - private final RssCrawlingJobListener rssCrawlingJobListener; - - @Bean - public Job rssCrawlingJob() { - return new JobBuilder("rssCrawlingJob", jobRepository) - .listener(rssCrawlingJobListener) - .start(fetchAndSaveRssStep()) - .next(extractSummaryStep()) - .next(embedAndIndexStep()) - .build(); - } - - @Bean - public Job summaryAndEmbeddingJob() { - return new JobBuilder("summaryAndEmbeddingJob", jobRepository) - .start(extractSummaryStep()) - .next(embedAndIndexStep()) - .build(); - } - - @Bean - public Step fetchAndSaveRssStep() { - return new StepBuilder("fetchAndSaveRssStep", jobRepository) - .chunk(10, transactionManager) - .reader(rssFeedReader) - .processor(rssToPostProcessor) - .writer(postBatchWriter) - .faultTolerant() - // 건너뛰기 정책: 최대 10개 아이템까지 건너뛰기 허용 - .skipLimit(10) - .skip(Exception.class) - .noSkip(IllegalStateException.class) - .build(); - } - - @Bean - public Step extractSummaryStep() { - return new StepBuilder("extractSummaryStep", jobRepository) - .>chunk(5, transactionManager) // Rate Limiter(15/min)를 고려한 chunk size - .reader(postSummaryReader) - .processor(asyncSummaryProcessor()) - .writer(asyncSummaryWriter()) - // Resilience4j가 Retry를 담당 - // Skip 로직만 유지: 실패한 아이템을 건너뛰고 다음 아이템 처리 - .faultTolerant() - .skipLimit(10) // 실패 허용 개수 증가 - .skip(Exception.class) - .build(); - } - - @Bean - public Step embedAndIndexStep() { - return new StepBuilder("embedAndIndexStep", jobRepository) - .>chunk(20, transactionManager) // 5개씩 배치 처리 - .reader(postEmbeddingReader) - .processor(asyncEmbeddingProcessor()) - .writer(asyncEmbeddingWriter()) - // Resilience4j가 Retry를 담당 - // Skip 로직만 유지: 실패한 아이템을 건너뛰고 다음 아이템 처리 - .faultTolerant() - .skipLimit(20) // 임베딩 실패 허용 개수 - .skip(Exception.class) - .build(); - } - - - @Bean - public ItemProcessor> asyncSummaryProcessor() { - AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); - asyncItemProcessor.setDelegate(postSummaryProcessor); - asyncItemProcessor.setTaskExecutor(summaryTaskExecutor()); - return asyncItemProcessor; - } - - @Bean - public ItemWriter> asyncSummaryWriter() { - AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); - asyncItemWriter.setDelegate(postSummaryWriter); - return asyncItemWriter; - } - - @Bean - public ItemProcessor> asyncEmbeddingProcessor() { - AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); - asyncItemProcessor.setDelegate(postEmbeddingProcessor); - asyncItemProcessor.setTaskExecutor(embeddingTaskExecutor()); - return asyncItemProcessor; - } - - @Bean - public ItemWriter> asyncEmbeddingWriter() { - AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); - asyncItemWriter.setDelegate(postEmbeddingWriter); - return asyncItemWriter; - } - - @Bean(name = "rssFetchTaskExecutor") - public AsyncTaskExecutor rssFetchTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(20); - executor.setMaxPoolSize(20); - executor.setQueueCapacity(32); - executor.setThreadNamePrefix("rss-fetch-"); - executor.setTaskDecorator(new MdcTaskDecorator()); - executor.setWaitForTasksToCompleteOnShutdown(true); - executor.setAwaitTerminationSeconds(60); - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); - executor.initialize(); - return executor; - } - - @Bean - public TaskExecutor summaryTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(2); - executor.setMaxPoolSize(2); - executor.setQueueCapacity(10); - executor.setThreadNamePrefix("summary-"); - executor.setTaskDecorator(new MdcTaskDecorator()); - executor.setWaitForTasksToCompleteOnShutdown(true); - executor.setAwaitTerminationSeconds(60); - executor.initialize(); - return executor; - } - - @Bean - public TaskExecutor embeddingTaskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(20); - executor.setQueueCapacity(50); - executor.setThreadNamePrefix("embedding-"); - executor.setTaskDecorator(new MdcTaskDecorator()); - executor.setWaitForTasksToCompleteOnShutdown(true); - executor.setAwaitTerminationSeconds(60); - executor.initialize(); - return executor; - } - -} +package com.techfork.domain.source.config; + +import com.techfork.post.application.batch.*; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.domain.source.batch.PostBatchWriter; +import com.techfork.domain.source.batch.RssFeedReader; +import com.techfork.domain.source.batch.RssToPostProcessor; +import com.techfork.domain.source.dto.RssFeedItem; +import com.techfork.domain.source.listener.RssCrawlingJobListener; +import com.techfork.global.filter.MdcTaskDecorator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.integration.async.AsyncItemProcessor; +import org.springframework.batch.integration.async.AsyncItemWriter; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemWriter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * RSS 크롤링 Job 설정 + * + * Step 1: RSS 피드 수집 및 저장 + * - Reader: RSS 피드를 블로그별로 수집 + * - Processor: 중복 체크 및 Post 엔티티 변환 + * - Writer: Post를 DB에 저장 (Bulk Insert) + * + * Step 2: 요약 추출 (비동기 처리) + * - Reader: 요약이 없는 Post 조회 (단일 스레드) + * - Processor: GPT API로 구조화된 요약 추출 (멀티 스레드) + * - Writer: PostSummary 저장 + * + * Step 3: 임베딩 생성 및 Elasticsearch 저장 (비동기 처리) + * - Reader: 요약이 완료되고 임베딩되지 않은 Post 조회 (단일 스레드) + * - Processor: Chunk 분할 + OpenAI 임베딩 생성 (멀티 스레드) + * - Writer: Elasticsearch에 PostDocument 저장 + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class RssCrawlingJobConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + + private final RssFeedReader rssFeedReader; + private final RssToPostProcessor rssToPostProcessor; + private final PostBatchWriter postBatchWriter; + + private final PostSummaryReader postSummaryReader; + private final PostSummaryProcessor postSummaryProcessor; + private final PostSummaryWriter postSummaryWriter; + + private final PostEmbeddingReader postEmbeddingReader; + private final PostEmbeddingProcessor postEmbeddingProcessor; + private final PostEmbeddingWriter postEmbeddingWriter; + + private final RssCrawlingJobListener rssCrawlingJobListener; + + @Bean + public Job rssCrawlingJob() { + return new JobBuilder("rssCrawlingJob", jobRepository) + .listener(rssCrawlingJobListener) + .start(fetchAndSaveRssStep()) + .next(extractSummaryStep()) + .next(embedAndIndexStep()) + .build(); + } + + @Bean + public Job summaryAndEmbeddingJob() { + return new JobBuilder("summaryAndEmbeddingJob", jobRepository) + .start(extractSummaryStep()) + .next(embedAndIndexStep()) + .build(); + } + + @Bean + public Step fetchAndSaveRssStep() { + return new StepBuilder("fetchAndSaveRssStep", jobRepository) + .chunk(10, transactionManager) + .reader(rssFeedReader) + .processor(rssToPostProcessor) + .writer(postBatchWriter) + .faultTolerant() + // 건너뛰기 정책: 최대 10개 아이템까지 건너뛰기 허용 + .skipLimit(10) + .skip(Exception.class) + .noSkip(IllegalStateException.class) + .build(); + } + + @Bean + public Step extractSummaryStep() { + return new StepBuilder("extractSummaryStep", jobRepository) + .>chunk(5, transactionManager) // Rate Limiter(15/min)를 고려한 chunk size + .reader(postSummaryReader) + .processor(asyncSummaryProcessor()) + .writer(asyncSummaryWriter()) + // Resilience4j가 Retry를 담당 + // Skip 로직만 유지: 실패한 아이템을 건너뛰고 다음 아이템 처리 + .faultTolerant() + .skipLimit(10) // 실패 허용 개수 증가 + .skip(Exception.class) + .build(); + } + + @Bean + public Step embedAndIndexStep() { + return new StepBuilder("embedAndIndexStep", jobRepository) + .>chunk(20, transactionManager) // 5개씩 배치 처리 + .reader(postEmbeddingReader) + .processor(asyncEmbeddingProcessor()) + .writer(asyncEmbeddingWriter()) + // Resilience4j가 Retry를 담당 + // Skip 로직만 유지: 실패한 아이템을 건너뛰고 다음 아이템 처리 + .faultTolerant() + .skipLimit(20) // 임베딩 실패 허용 개수 + .skip(Exception.class) + .build(); + } + + + @Bean + public ItemProcessor> asyncSummaryProcessor() { + AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); + asyncItemProcessor.setDelegate(postSummaryProcessor); + asyncItemProcessor.setTaskExecutor(summaryTaskExecutor()); + return asyncItemProcessor; + } + + @Bean + public ItemWriter> asyncSummaryWriter() { + AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); + asyncItemWriter.setDelegate(postSummaryWriter); + return asyncItemWriter; + } + + @Bean + public ItemProcessor> asyncEmbeddingProcessor() { + AsyncItemProcessor asyncItemProcessor = new AsyncItemProcessor<>(); + asyncItemProcessor.setDelegate(postEmbeddingProcessor); + asyncItemProcessor.setTaskExecutor(embeddingTaskExecutor()); + return asyncItemProcessor; + } + + @Bean + public ItemWriter> asyncEmbeddingWriter() { + AsyncItemWriter asyncItemWriter = new AsyncItemWriter<>(); + asyncItemWriter.setDelegate(postEmbeddingWriter); + return asyncItemWriter; + } + + @Bean(name = "rssFetchTaskExecutor") + public AsyncTaskExecutor rssFetchTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(20); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(32); + executor.setThreadNamePrefix("rss-fetch-"); + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + @Bean + public TaskExecutor summaryTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(2); + executor.setQueueCapacity(10); + executor.setThreadNamePrefix("summary-"); + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.initialize(); + return executor; + } + + @Bean + public TaskExecutor embeddingTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("embedding-"); + executor.setTaskDecorator(new MdcTaskDecorator()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + executor.initialize(); + return executor; + } + +} diff --git a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java index b76462ef..138fd4a1 100644 --- a/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java +++ b/src/main/java/com/techfork/global/config/ElasticsearchCacheManager.java @@ -3,7 +3,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.KnnSearch; import co.elastic.clients.json.JsonData; -import com.techfork.domain.post.document.PostDocument; +import com.techfork.post.domain.projection.PostDocument; import com.techfork.domain.recommendation.config.RecommendationProperties; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingProcessor.java b/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java similarity index 88% rename from src/main/java/com/techfork/domain/post/batch/PostEmbeddingProcessor.java rename to src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java index 712f8e88..31d7efb6 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingProcessor.java +++ b/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java @@ -1,71 +1,71 @@ -package com.techfork.domain.post.batch; - -import com.techfork.domain.post.document.ContentChunk; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.service.ContentChunkerService; -import com.techfork.global.llm.EmbeddingClient; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class PostEmbeddingProcessor implements ItemProcessor { - - private final ContentChunkerService contentChunkerService; - private final EmbeddingClient embeddingClient; - - @Override - public PostDocument process(Post post) { - log.info("임베딩 처리 시작: Post ID={}, Title={}", post.getId(), post.getTitle()); - - if (post.getTitle() == null || post.getTitle().isBlank()) { - log.warn("Post ID={}의 제목이 비어있어 임베딩을 스킵합니다.", post.getId()); - return null; - } - if (post.getSummary() == null || post.getSummary().isBlank()) { - log.warn("Post ID={}의 요약이 비어있어 임베딩을 스킵합니다.", post.getId()); - return null; - } - - List titleEmbedding = embeddingClient.embed(post.getTitle()); - List summaryEmbedding = embeddingClient.embed(post.getSummary()); - - List rawChunks = contentChunkerService.chunkContent(post.getFullContent()); - - List validChunks = rawChunks.stream() - .filter(chunk -> chunk != null && !chunk.isBlank()) - .toList(); - - log.info("Post ID={} 청크 개수: (원본: {}, 유효: {})", post.getId(), rawChunks.size(), validChunks.size()); - - if (validChunks.isEmpty()) { - log.warn("Post ID={}의 본문에서 유효한 텍스트 청크를 찾을 수 없어 스킵합니다.", post.getId()); - return null; - } - - List> chunkEmbeddings = embeddingClient.embedBatch(validChunks); - - List contentChunks = new ArrayList<>(); - for (int i = 0; i < validChunks.size(); i++) { - ContentChunk chunk = ContentChunk.create(i, validChunks.get(i), chunkEmbeddings.get(i)); - contentChunks.add(chunk); - } - - PostDocument postDocument = PostDocument.create( - post, - titleEmbedding, - summaryEmbedding, - contentChunks - ); - - log.info("임베딩 처리 완료: Post ID={}, Chunks={}", post.getId(), contentChunks.size()); - return postDocument; - } +package com.techfork.post.application.batch; + +import com.techfork.post.domain.projection.ContentChunk; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.application.support.ContentChunkerService; +import com.techfork.global.llm.EmbeddingClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PostEmbeddingProcessor implements ItemProcessor { + + private final ContentChunkerService contentChunkerService; + private final EmbeddingClient embeddingClient; + + @Override + public PostDocument process(Post post) { + log.info("임베딩 처리 시작: Post ID={}, Title={}", post.getId(), post.getTitle()); + + if (post.getTitle() == null || post.getTitle().isBlank()) { + log.warn("Post ID={}의 제목이 비어있어 임베딩을 스킵합니다.", post.getId()); + return null; + } + if (post.getSummary() == null || post.getSummary().isBlank()) { + log.warn("Post ID={}의 요약이 비어있어 임베딩을 스킵합니다.", post.getId()); + return null; + } + + List titleEmbedding = embeddingClient.embed(post.getTitle()); + List summaryEmbedding = embeddingClient.embed(post.getSummary()); + + List rawChunks = contentChunkerService.chunkContent(post.getFullContent()); + + List validChunks = rawChunks.stream() + .filter(chunk -> chunk != null && !chunk.isBlank()) + .toList(); + + log.info("Post ID={} 청크 개수: (원본: {}, 유효: {})", post.getId(), rawChunks.size(), validChunks.size()); + + if (validChunks.isEmpty()) { + log.warn("Post ID={}의 본문에서 유효한 텍스트 청크를 찾을 수 없어 스킵합니다.", post.getId()); + return null; + } + + List> chunkEmbeddings = embeddingClient.embedBatch(validChunks); + + List contentChunks = new ArrayList<>(); + for (int i = 0; i < validChunks.size(); i++) { + ContentChunk chunk = ContentChunk.create(i, validChunks.get(i), chunkEmbeddings.get(i)); + contentChunks.add(chunk); + } + + PostDocument postDocument = PostDocument.create( + post, + titleEmbedding, + summaryEmbedding, + contentChunks + ); + + log.info("임베딩 처리 완료: Post ID={}, Chunks={}", post.getId(), contentChunks.size()); + return postDocument; + } } \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingReader.java b/src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java similarity index 91% rename from src/main/java/com/techfork/domain/post/batch/PostEmbeddingReader.java rename to src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java index 5dcf7e99..29a8d0a7 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingReader.java +++ b/src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java @@ -1,7 +1,7 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -12,10 +12,10 @@ import java.util.Iterator; import java.util.List; - -/** - * 요약이 완료되고 임베딩이 필요한 Post를 읽어오는 Reader - */ + +/** + * 요약이 완료되고 임베딩이 필요한 Post를 읽어오는 Reader + */ @Slf4j @Component @StepScope diff --git a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingWriter.java b/src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java similarity index 92% rename from src/main/java/com/techfork/domain/post/batch/PostEmbeddingWriter.java rename to src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java index 255a996e..f2a7272d 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostEmbeddingWriter.java +++ b/src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java @@ -1,77 +1,77 @@ -package com.techfork.domain.post.batch; - -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch.core.BulkResponse; -import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.repository.PostRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class PostEmbeddingWriter implements ItemWriter { - - private final ElasticsearchClient elasticsearchClient; - private final PostRepository postRepository; - - @Override - public void write(Chunk chunk) throws Exception { - var documents = chunk.getItems(); - - if (documents.isEmpty()) { - return; - } - - BulkResponse bulkResponse = elasticsearchClient.bulk(b -> b - .index("posts") - .operations(documents.stream() - .map(doc -> BulkOperation.of(op -> op - .index(i -> i - .id(doc.getId()) - .document(doc) - ) - )) - .toList() - ) - ); - - if (bulkResponse == null) { - log.error("Bulk 응답이 null입니다."); - return; - } - - if (bulkResponse.errors()) { - log.warn("Bulk 인덱싱 중 일부 실패 발생"); - } - - List successPostIds = bulkResponse.items().stream() - .filter(item -> item.error() == null) // 에러 없는 것만 - .map(item -> Long.parseLong(item.id())) - .toList(); - - int failureCount = documents.size() - successPostIds.size(); - - log.info("Elasticsearch Bulk Insert 완료: 성공={}, 실패={}", successPostIds.size(), failureCount); - - if (failureCount > 0) { - log.warn("실패한 문서 ID 목록:"); - bulkResponse.items().stream() - .filter(item -> item.error() != null) - .forEach(item -> log.warn("ID={}, Error={}", item.id(), item.error().reason())); - } - - if (!successPostIds.isEmpty()) { - postRepository.bulkUpdateEmbeddedAt(successPostIds, java.time.LocalDateTime.now()); - log.info("Post embeddedAt Bulk 업데이트 완료: {} 개", successPostIds.size()); - } else { - log.warn("ES 저장에 성공한 문서가 없어 embeddedAt 업데이트를 건너뜁니다."); - } - } -} +package com.techfork.post.application.batch; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.BulkResponse; +import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.infrastructure.PostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PostEmbeddingWriter implements ItemWriter { + + private final ElasticsearchClient elasticsearchClient; + private final PostRepository postRepository; + + @Override + public void write(Chunk chunk) throws Exception { + var documents = chunk.getItems(); + + if (documents.isEmpty()) { + return; + } + + BulkResponse bulkResponse = elasticsearchClient.bulk(b -> b + .index("posts") + .operations(documents.stream() + .map(doc -> BulkOperation.of(op -> op + .index(i -> i + .id(doc.getId()) + .document(doc) + ) + )) + .toList() + ) + ); + + if (bulkResponse == null) { + log.error("Bulk 응답이 null입니다."); + return; + } + + if (bulkResponse.errors()) { + log.warn("Bulk 인덱싱 중 일부 실패 발생"); + } + + List successPostIds = bulkResponse.items().stream() + .filter(item -> item.error() == null) // 에러 없는 것만 + .map(item -> Long.parseLong(item.id())) + .toList(); + + int failureCount = documents.size() - successPostIds.size(); + + log.info("Elasticsearch Bulk Insert 완료: 성공={}, 실패={}", successPostIds.size(), failureCount); + + if (failureCount > 0) { + log.warn("실패한 문서 ID 목록:"); + bulkResponse.items().stream() + .filter(item -> item.error() != null) + .forEach(item -> log.warn("ID={}, Error={}", item.id(), item.error().reason())); + } + + if (!successPostIds.isEmpty()) { + postRepository.bulkUpdateEmbeddedAt(successPostIds, java.time.LocalDateTime.now()); + log.info("Post embeddedAt Bulk 업데이트 완료: {} 개", successPostIds.size()); + } else { + log.warn("ES 저장에 성공한 문서가 없어 embeddedAt 업데이트를 건너뜁니다."); + } + } +} diff --git a/src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java similarity index 82% rename from src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java rename to src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java index 1b6ea8d9..3899970d 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java +++ b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java @@ -1,32 +1,32 @@ -package com.techfork.domain.post.batch; - -import com.techfork.domain.post.dto.SummaryWithKeywordsDto; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.service.SummaryExtractionService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemProcessor; -import org.springframework.stereotype.Component; - -/** - * Post에서 LLM을 사용하여 요약과 키워드를 추출하고 Post 엔티티에 저장하는 Processor - */ -@Slf4j -@Component -@StepScope -@RequiredArgsConstructor -public class PostSummaryProcessor implements ItemProcessor { - - private final SummaryExtractionService summaryExtractionService; - - @Override - public Post process(Post post) { - log.debug("요약 및 키워드 추출 중: {}", post.getTitle()); - - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary( - post.getTitle(), - post.getPlainContent() +package com.techfork.post.application.batch; + +import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.domain.Post; +import com.techfork.post.application.support.SummaryExtractionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.stereotype.Component; + +/** + * Post에서 LLM을 사용하여 요약과 키워드를 추출하고 Post 엔티티에 저장하는 Processor + */ +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class PostSummaryProcessor implements ItemProcessor { + + private final SummaryExtractionService summaryExtractionService; + + @Override + public Post process(Post post) { + log.debug("요약 및 키워드 추출 중: {}", post.getTitle()); + + SummaryWithKeywordsDto result = summaryExtractionService.extractSummary( + post.getTitle(), + post.getPlainContent() ); post.updateSummaries(result.summary(), result.shortSummary()); diff --git a/src/main/java/com/techfork/domain/post/batch/PostSummaryReader.java b/src/main/java/com/techfork/post/application/batch/PostSummaryReader.java similarity index 84% rename from src/main/java/com/techfork/domain/post/batch/PostSummaryReader.java rename to src/main/java/com/techfork/post/application/batch/PostSummaryReader.java index 2469fab0..4ca38159 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostSummaryReader.java +++ b/src/main/java/com/techfork/post/application/batch/PostSummaryReader.java @@ -1,36 +1,36 @@ -package com.techfork.domain.post.batch; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.ItemReader; -import org.springframework.stereotype.Component; - -import java.util.Iterator; -import java.util.List; - -/** - * summary가 null이거나 빈 문자열인 Post들을 읽어오는 Reader - */ -@Slf4j -@Component -@StepScope -@RequiredArgsConstructor -public class PostSummaryReader implements ItemReader { - - private final PostRepository postRepository; - private Iterator postIterator; - - @Override - public Post read() { - if (postIterator == null) { - List posts = postRepository.findWithKeywordsBySummaryIsNull(); - log.info("요약이 없거나 비어있는 게시글 {}개 발견", posts.size()); - postIterator = posts.iterator(); - } - - return postIterator.hasNext() ? postIterator.next() : null; - } -} +package com.techfork.post.application.batch; + +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.ItemReader; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.List; + +/** + * summary가 null이거나 빈 문자열인 Post들을 읽어오는 Reader + */ +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class PostSummaryReader implements ItemReader { + + private final PostRepository postRepository; + private Iterator postIterator; + + @Override + public Post read() { + if (postIterator == null) { + List posts = postRepository.findWithKeywordsBySummaryIsNull(); + log.info("요약이 없거나 비어있는 게시글 {}개 발견", posts.size()); + postIterator = posts.iterator(); + } + + return postIterator.hasNext() ? postIterator.next() : null; + } +} diff --git a/src/main/java/com/techfork/domain/post/batch/PostSummaryWriter.java b/src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java similarity index 93% rename from src/main/java/com/techfork/domain/post/batch/PostSummaryWriter.java rename to src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java index d705ddb5..07da15b1 100644 --- a/src/main/java/com/techfork/domain/post/batch/PostSummaryWriter.java +++ b/src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java @@ -1,110 +1,110 @@ -package com.techfork.domain.post.batch; - -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.global.util.JdbcBatchExecutor; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.configuration.annotation.StepScope; -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 요약이 추가된 Post를 저장하는 Writer - */ -@Slf4j -@Component -@StepScope -@RequiredArgsConstructor -public class PostSummaryWriter implements ItemWriter { - - private final JdbcBatchExecutor jdbcBatchExecutor; - - @PersistenceContext - private EntityManager entityManager; - - @Override - public void write(Chunk chunk) { - List posts = chunk.getItems(); - if (posts.isEmpty()) { - return; - } - - updatePostSummaries(posts); - deleteOldKeywords(posts); - insertNewKeywords(posts); - - log.info("PostSummaryWriter: {}개 게시글 처리 완료", posts.size()); - - entityManager.clear(); - } - - private void updatePostSummaries(List posts) { - String sql = "UPDATE posts SET summary = ?, short_summary = ? WHERE id = ?"; - - @SuppressWarnings("unchecked") - List postList = (List) posts; - - int totalUpdated = jdbcBatchExecutor.batchExecute(sql, postList, (ps, post, i) -> { - ps.setString(1, post.getSummary()); - ps.setString(2, post.getShortSummary()); - ps.setLong(3, post.getId()); - }); - - log.debug("UPDATE posts: {}개 업데이트", totalUpdated); - } - - private void deleteOldKeywords(List posts) { - List postIds = posts.stream() - .map(Post::getId) - .collect(Collectors.toList()); - - if (postIds.isEmpty()) { - return; - } - - String sql = "DELETE FROM post_keywords WHERE post_id = ?"; - - int deletedCount = jdbcBatchExecutor.batchExecute(sql, postIds, (ps, id, i) -> - ps.setLong(1, id) - ); - log.debug("DELETE post_keywords: {}개 삭제", deletedCount); - } - - private void insertNewKeywords(List posts) { - // Post에서 모든 PostKeyword를 평탄화 - List keywordDtos = new ArrayList<>(); - for (Post post : posts) { - for (PostKeyword keyword : post.getKeywords()) { - keywordDtos.add(new KeywordInsertDto(keyword.getKeyword(), post.getId())); - } - } - - if (keywordDtos.isEmpty()) { - log.debug("INSERT post_keywords: 삽입할 키워드 없음"); - return; - } - - String sql = "INSERT INTO post_keywords (keyword, post_id) VALUES (?, ?)"; - - int inserted = jdbcBatchExecutor.batchExecute(sql, keywordDtos, (ps, dto, i) -> { - ps.setString(1, dto.keyword); - ps.setLong(2, dto.postId); - }); - - log.debug("INSERT post_keywords: {}개 삽입", inserted); - } - - /** - * 키워드 삽입을 위한 DTO - */ - private record KeywordInsertDto(String keyword, Long postId) { - } +package com.techfork.post.application.batch; + +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; +import com.techfork.global.util.JdbcBatchExecutor; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 요약이 추가된 Post를 저장하는 Writer + */ +@Slf4j +@Component +@StepScope +@RequiredArgsConstructor +public class PostSummaryWriter implements ItemWriter { + + private final JdbcBatchExecutor jdbcBatchExecutor; + + @PersistenceContext + private EntityManager entityManager; + + @Override + public void write(Chunk chunk) { + List posts = chunk.getItems(); + if (posts.isEmpty()) { + return; + } + + updatePostSummaries(posts); + deleteOldKeywords(posts); + insertNewKeywords(posts); + + log.info("PostSummaryWriter: {}개 게시글 처리 완료", posts.size()); + + entityManager.clear(); + } + + private void updatePostSummaries(List posts) { + String sql = "UPDATE posts SET summary = ?, short_summary = ? WHERE id = ?"; + + @SuppressWarnings("unchecked") + List postList = (List) posts; + + int totalUpdated = jdbcBatchExecutor.batchExecute(sql, postList, (ps, post, i) -> { + ps.setString(1, post.getSummary()); + ps.setString(2, post.getShortSummary()); + ps.setLong(3, post.getId()); + }); + + log.debug("UPDATE posts: {}개 업데이트", totalUpdated); + } + + private void deleteOldKeywords(List posts) { + List postIds = posts.stream() + .map(Post::getId) + .collect(Collectors.toList()); + + if (postIds.isEmpty()) { + return; + } + + String sql = "DELETE FROM post_keywords WHERE post_id = ?"; + + int deletedCount = jdbcBatchExecutor.batchExecute(sql, postIds, (ps, id, i) -> + ps.setLong(1, id) + ); + log.debug("DELETE post_keywords: {}개 삭제", deletedCount); + } + + private void insertNewKeywords(List posts) { + // Post에서 모든 PostKeyword를 평탄화 + List keywordDtos = new ArrayList<>(); + for (Post post : posts) { + for (PostKeyword keyword : post.getKeywords()) { + keywordDtos.add(new KeywordInsertDto(keyword.getKeyword(), post.getId())); + } + } + + if (keywordDtos.isEmpty()) { + log.debug("INSERT post_keywords: 삽입할 키워드 없음"); + return; + } + + String sql = "INSERT INTO post_keywords (keyword, post_id) VALUES (?, ?)"; + + int inserted = jdbcBatchExecutor.batchExecute(sql, keywordDtos, (ps, dto, i) -> { + ps.setString(1, dto.keyword); + ps.setLong(2, dto.postId); + }); + + log.debug("INSERT post_keywords: {}개 삽입", inserted); + } + + /** + * 키워드 삽입을 위한 DTO + */ + private record KeywordInsertDto(String keyword, Long postId) { + } } \ No newline at end of file diff --git a/src/main/java/com/techfork/domain/post/service/PostCommandService.java b/src/main/java/com/techfork/post/application/command/PostCommandService.java similarity index 92% rename from src/main/java/com/techfork/domain/post/service/PostCommandService.java rename to src/main/java/com/techfork/post/application/command/PostCommandService.java index c0d78a58..aae68af0 100644 --- a/src/main/java/com/techfork/domain/post/service/PostCommandService.java +++ b/src/main/java/com/techfork/post/application/command/PostCommandService.java @@ -1,6 +1,6 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.command; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/techfork/domain/post/converter/PostConverter.java b/src/main/java/com/techfork/post/application/converter/PostConverter.java similarity index 93% rename from src/main/java/com/techfork/domain/post/converter/PostConverter.java rename to src/main/java/com/techfork/post/application/converter/PostConverter.java index 395ff094..fedb48a5 100644 --- a/src/main/java/com/techfork/domain/post/converter/PostConverter.java +++ b/src/main/java/com/techfork/post/application/converter/PostConverter.java @@ -1,63 +1,63 @@ -package com.techfork.domain.post.converter; - -import com.techfork.domain.post.dto.*; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.util.List; - -@Component -public class PostConverter { - - public CompanyListResponse toCompanyListResponse(List companies) { - return CompanyListResponse.builder() - .companies(companies) - .build(); - } - - public CompanyListResponse toCompanyListResponseV2(List companies) { - return CompanyListResponse.builder() - .totalNumber(companies.size()) - .companies(companies) - .build(); - } - - public PostListResponse toPostListResponse(List posts, int requestedSize) { - boolean hasNext = posts.size() > requestedSize; - List content = hasNext ? posts.subList(0, requestedSize) : posts; - - Long lastPostId = null; - Long lastViewCount = null; - LocalDateTime lastPublishedAt = null; - - if (!content.isEmpty()) { - PostInfoDto lastPost = content.get(content.size() - 1); - lastPostId = lastPost.id(); - lastViewCount = lastPost.viewCount(); - lastPublishedAt = lastPost.publishedAt(); - } - - return PostListResponse.builder() - .posts(content) - .lastPostId(lastPostId) - .lastViewCount(lastViewCount) - .lastPublishedAt(lastPublishedAt) - .hasNext(hasNext) - .build(); - } - - public PostDetailDto toPostDetailDto(PostDetailDto baseDto, List keywords, Boolean isBookmarked) { - return PostDetailDto.builder() - .id(baseDto.id()) - .title(baseDto.title()) - .summary(baseDto.summary()) - .company(baseDto.company()) - .url(baseDto.url()) - .logoUrl(baseDto.logoUrl()) - .publishedAt(baseDto.publishedAt()) - .viewCount(baseDto.viewCount()) - .keywords(keywords) - .isBookmarked(isBookmarked) - .build(); - } -} +package com.techfork.post.application.converter; + +import com.techfork.post.application.dto.*; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +public class PostConverter { + + public CompanyListResponse toCompanyListResponse(List companies) { + return CompanyListResponse.builder() + .companies(companies) + .build(); + } + + public CompanyListResponse toCompanyListResponseV2(List companies) { + return CompanyListResponse.builder() + .totalNumber(companies.size()) + .companies(companies) + .build(); + } + + public PostListResponse toPostListResponse(List posts, int requestedSize) { + boolean hasNext = posts.size() > requestedSize; + List content = hasNext ? posts.subList(0, requestedSize) : posts; + + Long lastPostId = null; + Long lastViewCount = null; + LocalDateTime lastPublishedAt = null; + + if (!content.isEmpty()) { + PostInfoDto lastPost = content.get(content.size() - 1); + lastPostId = lastPost.id(); + lastViewCount = lastPost.viewCount(); + lastPublishedAt = lastPost.publishedAt(); + } + + return PostListResponse.builder() + .posts(content) + .lastPostId(lastPostId) + .lastViewCount(lastViewCount) + .lastPublishedAt(lastPublishedAt) + .hasNext(hasNext) + .build(); + } + + public PostDetailDto toPostDetailDto(PostDetailDto baseDto, List keywords, Boolean isBookmarked) { + return PostDetailDto.builder() + .id(baseDto.id()) + .title(baseDto.title()) + .summary(baseDto.summary()) + .company(baseDto.company()) + .url(baseDto.url()) + .logoUrl(baseDto.logoUrl()) + .publishedAt(baseDto.publishedAt()) + .viewCount(baseDto.viewCount()) + .keywords(keywords) + .isBookmarked(isBookmarked) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/post/dto/CompanyDto.java b/src/main/java/com/techfork/post/application/dto/CompanyDto.java similarity index 85% rename from src/main/java/com/techfork/domain/post/dto/CompanyDto.java rename to src/main/java/com/techfork/post/application/dto/CompanyDto.java index a19dc198..891e7c11 100644 --- a/src/main/java/com/techfork/domain/post/dto/CompanyDto.java +++ b/src/main/java/com/techfork/post/application/dto/CompanyDto.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.dto; +package com.techfork.post.application.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java b/src/main/java/com/techfork/post/application/dto/CompanyListResponse.java similarity index 88% rename from src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java rename to src/main/java/com/techfork/post/application/dto/CompanyListResponse.java index 6efa74c7..c49b4028 100644 --- a/src/main/java/com/techfork/domain/post/dto/CompanyListResponse.java +++ b/src/main/java/com/techfork/post/application/dto/CompanyListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.dto; +package com.techfork.post.application.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/post/dto/PostDetailDto.java b/src/main/java/com/techfork/post/application/dto/PostDetailDto.java similarity index 91% rename from src/main/java/com/techfork/domain/post/dto/PostDetailDto.java rename to src/main/java/com/techfork/post/application/dto/PostDetailDto.java index d03ba22b..8fe2a5c1 100644 --- a/src/main/java/com/techfork/domain/post/dto/PostDetailDto.java +++ b/src/main/java/com/techfork/post/application/dto/PostDetailDto.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.dto; +package com.techfork.post.application.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/post/dto/PostInfoDto.java b/src/main/java/com/techfork/post/application/dto/PostInfoDto.java similarity index 92% rename from src/main/java/com/techfork/domain/post/dto/PostInfoDto.java rename to src/main/java/com/techfork/post/application/dto/PostInfoDto.java index 94ab95c5..2da79c0e 100644 --- a/src/main/java/com/techfork/domain/post/dto/PostInfoDto.java +++ b/src/main/java/com/techfork/post/application/dto/PostInfoDto.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.dto; +package com.techfork.post.application.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/post/dto/PostListResponse.java b/src/main/java/com/techfork/post/application/dto/PostListResponse.java similarity index 89% rename from src/main/java/com/techfork/domain/post/dto/PostListResponse.java rename to src/main/java/com/techfork/post/application/dto/PostListResponse.java index 431d3080..f38db0ee 100644 --- a/src/main/java/com/techfork/domain/post/dto/PostListResponse.java +++ b/src/main/java/com/techfork/post/application/dto/PostListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.dto; +package com.techfork.post.application.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/domain/post/dto/SummaryWithKeywordsDto.java b/src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java similarity index 76% rename from src/main/java/com/techfork/domain/post/dto/SummaryWithKeywordsDto.java rename to src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java index 18c3e966..fbe12c73 100644 --- a/src/main/java/com/techfork/domain/post/dto/SummaryWithKeywordsDto.java +++ b/src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java @@ -1,10 +1,10 @@ -package com.techfork.domain.post.dto; - -import java.util.List; - -public record SummaryWithKeywordsDto( - String summary, - String shortSummary, - List keywords -) { -} +package com.techfork.post.application.dto; + +import java.util.List; + +public record SummaryWithKeywordsDto( + String summary, + String shortSummary, + List keywords +) { +} diff --git a/src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java b/src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java similarity index 84% rename from src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java rename to src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java index 0bfcfd33..a1c8c6a9 100644 --- a/src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java +++ b/src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java @@ -1,7 +1,7 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.repository.PostKeywordRepository; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.infrastructure.PostKeywordRepository; import java.util.List; import java.util.Map; import java.util.stream.Collectors; diff --git a/src/main/java/com/techfork/domain/post/service/PostLookupService.java b/src/main/java/com/techfork/post/application/query/PostLookupService.java similarity index 72% rename from src/main/java/com/techfork/domain/post/service/PostLookupService.java rename to src/main/java/com/techfork/post/application/query/PostLookupService.java index 99fa7f25..b9c18a2b 100644 --- a/src/main/java/com/techfork/domain/post/service/PostLookupService.java +++ b/src/main/java/com/techfork/post/application/query/PostLookupService.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.exception.PostErrorCode; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.exception.PostErrorCode; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/techfork/domain/post/service/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java similarity index 95% rename from src/main/java/com/techfork/domain/post/service/PostQueryService.java rename to src/main/java/com/techfork/post/application/query/PostQueryService.java index 401ecb11..57b65d01 100644 --- a/src/main/java/com/techfork/domain/post/service/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -1,12 +1,12 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.converter.PostConverter; -import com.techfork.domain.post.dto.*; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.enums.EPostSortType; -import com.techfork.domain.post.repository.PostKeywordRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.application.converter.PostConverter; +import com.techfork.post.application.dto.*; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.domain.enums.EPostSortType; +import com.techfork.post.infrastructure.PostKeywordRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.global.exception.CommonErrorCode; import com.techfork.global.exception.GeneralException; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; diff --git a/src/main/java/com/techfork/domain/post/service/ContentChunkerService.java b/src/main/java/com/techfork/post/application/support/ContentChunkerService.java similarity index 96% rename from src/main/java/com/techfork/domain/post/service/ContentChunkerService.java rename to src/main/java/com/techfork/post/application/support/ContentChunkerService.java index 0c4e49b5..97f840af 100644 --- a/src/main/java/com/techfork/domain/post/service/ContentChunkerService.java +++ b/src/main/java/com/techfork/post/application/support/ContentChunkerService.java @@ -1,200 +1,200 @@ -package com.techfork.domain.post.service; - -import com.techfork.global.util.ContentCleaner; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * HTML 및 Markdown 기반 fullContent를 의미 있는 청크로 분할하는 서비스 - * - RSS 피드는 대부분 HTML 형식 - * -

~

태그로 섹션 분할 - * -

태그로 세부 분할 - */ -@Slf4j -@Service -public class ContentChunkerService { - - private static final int MAX_CHUNK_SIZE = 4000; - private static final int MIN_CHUNK_SIZE = 500; - private static final int OVERLAP_SIZE = 400; - - - // HTML 헤더 패턴 (h1~h6 태그로 섹션 시작 위치 찾기) - private static final Pattern HTML_HEADER = Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); - - // 마크다운 헤더 패턴 (#, ## 등) - // (m) 플래그: ^가 각 줄의 시작과 일치하도록 함 - private static final Pattern MARKDOWN_HEADER = Pattern.compile("(?m)^\\s*#{1,6}\\s+"); - - /** - * HTML 컨텐츠를 청크로 분할 - */ - public List chunkContent(String content) { - if (content == null || content.isBlank()) { - return List.of(); - } - - List chunks = chunkByStructure(content); - - // 청크 크기 조정 및 오버랩 추가 - return adjustChunkSizes(chunks); - } - - /** - * HTML 구조 기반 청크 분할 - * 1.

~

태그로 섹션 분할 - * 2. HTML/Markdown 태그 제거하여 순수 텍스트 추출 - */ - private List chunkByStructure(String content) { - List chunks = new ArrayList<>(); - - Set startSet = new TreeSet<>(); - startSet.add(0); - - // 1. HTML 헤더 태그(

~

)로 시작 위치 찾기 - Matcher headerMatcher = HTML_HEADER.matcher(content); - while (headerMatcher.find()) { - startSet.add(headerMatcher.start()); - } - - // 2. Markdown 헤더 위치 찾기 - Matcher mdMatcher = MARKDOWN_HEADER.matcher(content); - while (mdMatcher.find()) { - startSet.add(mdMatcher.start()); - } - - startSet.add(content.length()); - - List sectionStarts = new ArrayList<>(startSet); - - // 각 섹션 추출 - for (int i = 0; i < sectionStarts.size() - 1; i++) { - int start = sectionStarts.get(i); - int end = sectionStarts.get(i + 1); - String sectionMarkup = content.substring(start, end).trim(); - - if (!sectionMarkup.isEmpty()) { - // HTML 및 마크다운 태그 제거하여 순수 텍스트로 변환 - String cleanText = ContentCleaner.clean(sectionMarkup); - if (!cleanText.isBlank()) { - chunks.add(cleanText); - } - } - } - - // 헤더가 없는 경우 (헤더 없이 바로 본문 시작) - if (chunks.isEmpty()) { - String cleanContent = ContentCleaner.clean(content); - if (!cleanContent.isBlank()) { - chunks.add(cleanContent); - } - } - - return chunks; - } - - /** - * 청크 크기 조정 및 오버랩 추가 - */ - private List adjustChunkSizes(List chunks) { - List adjustedChunks = new ArrayList<>(); - - for (String chunk : chunks) { - // 청크가 최대 크기보다 크면 분할 - if (chunk.length() > MAX_CHUNK_SIZE) { - adjustedChunks.addAll(splitLargeChunk(chunk)); - } else if (chunk.length() >= MIN_CHUNK_SIZE) { - adjustedChunks.add(chunk); - } else { - // 너무 작은 청크는 이전 청크와 병합 - if (!adjustedChunks.isEmpty()) { - int lastIndex = adjustedChunks.size() - 1; - String merged = adjustedChunks.get(lastIndex) + "\n\n" + chunk; - - // 병합된 쳥크가 최대 크기를 넘지 않도록 방어 로직 추가 - if (merged.length() <= MAX_CHUNK_SIZE * 1.1) { // 약간의 여유 허용 - adjustedChunks.set(lastIndex, merged); - } - } else { - adjustedChunks.add(chunk); - } - } - } - - return adjustedChunks; - } - - /** - * 큰 청크를 여러 개로 분할 - */ - private List splitLargeChunk(String chunk) { - List splits = new ArrayList<>(); - String[] paragraphs = chunk.split("\n\n+"); - - StringBuilder currentChunk = new StringBuilder(); - for (String paragraph : paragraphs) { - if (currentChunk.length() + paragraph.length() > MAX_CHUNK_SIZE) { - if (currentChunk.length() > 0) { - splits.add(currentChunk.toString().trim()); - // 오버랩을 위해 마지막 부분 일부 유지 - currentChunk = new StringBuilder(getOverlapText(currentChunk.toString())); - currentChunk.append("\n\n"); - } - } - - // 만약 단락 자체가 최대 크기보다 크다면, 강제로 분할 (간단한 처리) - if (paragraph.length() > MAX_CHUNK_SIZE) { - // 현재 청크가 있다면 먼저 추가 - if (currentChunk.length() > 0) { - splits.add(currentChunk.toString().trim()); - currentChunk = new StringBuilder(); - } - // 큰 단락을 분할해서 추가 - splits.addAll(forceSplitText(paragraph, MAX_CHUNK_SIZE, OVERLAP_SIZE)); - } else { - currentChunk.append(paragraph).append("\n\n"); - } - } - - if (currentChunk.length() > 0) { - splits.add(currentChunk.toString().trim()); - } - - return splits; - } - - /** - * 오버랩용 텍스트 추출 (마지막 N자) - */ - private String getOverlapText(String text) { - if (text.length() <= OVERLAP_SIZE) { - return text; - } - return text.substring(text.length() - OVERLAP_SIZE); - } - - private List forceSplitText(String text, int size, int overlap) { - List parts = new ArrayList<>(); - int length = text.length(); - int start = 0; - - while (start < length) { - int end = Math.min(start + size, length); - parts.add(text.substring(start, end)); - start += (size - overlap); - if (start >= length) break; - // 오버랩이 너무 커서 무한 루프 도는 것 방지 - if (start <= end - size + overlap) { - start = end; // 오버랩 없이 다음으로 넘어감 - } - } - return parts; - } -} +package com.techfork.post.application.support; + +import com.techfork.global.util.ContentCleaner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML 및 Markdown 기반 fullContent를 의미 있는 청크로 분할하는 서비스 + * - RSS 피드는 대부분 HTML 형식 + * -

~

태그로 섹션 분할 + * -

태그로 세부 분할 + */ +@Slf4j +@Service +public class ContentChunkerService { + + private static final int MAX_CHUNK_SIZE = 4000; + private static final int MIN_CHUNK_SIZE = 500; + private static final int OVERLAP_SIZE = 400; + + + // HTML 헤더 패턴 (h1~h6 태그로 섹션 시작 위치 찾기) + private static final Pattern HTML_HEADER = Pattern.compile("]*>", Pattern.CASE_INSENSITIVE); + + // 마크다운 헤더 패턴 (#, ## 등) + // (m) 플래그: ^가 각 줄의 시작과 일치하도록 함 + private static final Pattern MARKDOWN_HEADER = Pattern.compile("(?m)^\\s*#{1,6}\\s+"); + + /** + * HTML 컨텐츠를 청크로 분할 + */ + public List chunkContent(String content) { + if (content == null || content.isBlank()) { + return List.of(); + } + + List chunks = chunkByStructure(content); + + // 청크 크기 조정 및 오버랩 추가 + return adjustChunkSizes(chunks); + } + + /** + * HTML 구조 기반 청크 분할 + * 1.

~

태그로 섹션 분할 + * 2. HTML/Markdown 태그 제거하여 순수 텍스트 추출 + */ + private List chunkByStructure(String content) { + List chunks = new ArrayList<>(); + + Set startSet = new TreeSet<>(); + startSet.add(0); + + // 1. HTML 헤더 태그(

~

)로 시작 위치 찾기 + Matcher headerMatcher = HTML_HEADER.matcher(content); + while (headerMatcher.find()) { + startSet.add(headerMatcher.start()); + } + + // 2. Markdown 헤더 위치 찾기 + Matcher mdMatcher = MARKDOWN_HEADER.matcher(content); + while (mdMatcher.find()) { + startSet.add(mdMatcher.start()); + } + + startSet.add(content.length()); + + List sectionStarts = new ArrayList<>(startSet); + + // 각 섹션 추출 + for (int i = 0; i < sectionStarts.size() - 1; i++) { + int start = sectionStarts.get(i); + int end = sectionStarts.get(i + 1); + String sectionMarkup = content.substring(start, end).trim(); + + if (!sectionMarkup.isEmpty()) { + // HTML 및 마크다운 태그 제거하여 순수 텍스트로 변환 + String cleanText = ContentCleaner.clean(sectionMarkup); + if (!cleanText.isBlank()) { + chunks.add(cleanText); + } + } + } + + // 헤더가 없는 경우 (헤더 없이 바로 본문 시작) + if (chunks.isEmpty()) { + String cleanContent = ContentCleaner.clean(content); + if (!cleanContent.isBlank()) { + chunks.add(cleanContent); + } + } + + return chunks; + } + + /** + * 청크 크기 조정 및 오버랩 추가 + */ + private List adjustChunkSizes(List chunks) { + List adjustedChunks = new ArrayList<>(); + + for (String chunk : chunks) { + // 청크가 최대 크기보다 크면 분할 + if (chunk.length() > MAX_CHUNK_SIZE) { + adjustedChunks.addAll(splitLargeChunk(chunk)); + } else if (chunk.length() >= MIN_CHUNK_SIZE) { + adjustedChunks.add(chunk); + } else { + // 너무 작은 청크는 이전 청크와 병합 + if (!adjustedChunks.isEmpty()) { + int lastIndex = adjustedChunks.size() - 1; + String merged = adjustedChunks.get(lastIndex) + "\n\n" + chunk; + + // 병합된 쳥크가 최대 크기를 넘지 않도록 방어 로직 추가 + if (merged.length() <= MAX_CHUNK_SIZE * 1.1) { // 약간의 여유 허용 + adjustedChunks.set(lastIndex, merged); + } + } else { + adjustedChunks.add(chunk); + } + } + } + + return adjustedChunks; + } + + /** + * 큰 청크를 여러 개로 분할 + */ + private List splitLargeChunk(String chunk) { + List splits = new ArrayList<>(); + String[] paragraphs = chunk.split("\n\n+"); + + StringBuilder currentChunk = new StringBuilder(); + for (String paragraph : paragraphs) { + if (currentChunk.length() + paragraph.length() > MAX_CHUNK_SIZE) { + if (currentChunk.length() > 0) { + splits.add(currentChunk.toString().trim()); + // 오버랩을 위해 마지막 부분 일부 유지 + currentChunk = new StringBuilder(getOverlapText(currentChunk.toString())); + currentChunk.append("\n\n"); + } + } + + // 만약 단락 자체가 최대 크기보다 크다면, 강제로 분할 (간단한 처리) + if (paragraph.length() > MAX_CHUNK_SIZE) { + // 현재 청크가 있다면 먼저 추가 + if (currentChunk.length() > 0) { + splits.add(currentChunk.toString().trim()); + currentChunk = new StringBuilder(); + } + // 큰 단락을 분할해서 추가 + splits.addAll(forceSplitText(paragraph, MAX_CHUNK_SIZE, OVERLAP_SIZE)); + } else { + currentChunk.append(paragraph).append("\n\n"); + } + } + + if (currentChunk.length() > 0) { + splits.add(currentChunk.toString().trim()); + } + + return splits; + } + + /** + * 오버랩용 텍스트 추출 (마지막 N자) + */ + private String getOverlapText(String text) { + if (text.length() <= OVERLAP_SIZE) { + return text; + } + return text.substring(text.length() - OVERLAP_SIZE); + } + + private List forceSplitText(String text, int size, int overlap) { + List parts = new ArrayList<>(); + int length = text.length(); + int start = 0; + + while (start < length) { + int end = Math.min(start + size, length); + parts.add(text.substring(start, end)); + start += (size - overlap); + if (start >= length) break; + // 오버랩이 너무 커서 무한 루프 도는 것 방지 + if (start <= end - size + overlap) { + start = end; // 오버랩 없이 다음으로 넘어감 + } + } + return parts; + } +} diff --git a/src/main/java/com/techfork/domain/post/service/SummaryExtractionService.java b/src/main/java/com/techfork/post/application/support/SummaryExtractionService.java similarity index 95% rename from src/main/java/com/techfork/domain/post/service/SummaryExtractionService.java rename to src/main/java/com/techfork/post/application/support/SummaryExtractionService.java index 6f283e9f..af2d11c7 100644 --- a/src/main/java/com/techfork/domain/post/service/SummaryExtractionService.java +++ b/src/main/java/com/techfork/post/application/support/SummaryExtractionService.java @@ -1,105 +1,105 @@ -package com.techfork.domain.post.service; - +package com.techfork.post.application.support; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.domain.post.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.dto.SummaryWithKeywordsDto; import com.techfork.global.llm.LlmClient; import com.techfork.global.llm.exception.LlmException; import com.techfork.global.util.ContentCleaner; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -/** - * LLM을 사용한 게시글 요약 추출 서비스 - * 의미 기반 검색 최적화를 위한 요약 생성 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class SummaryExtractionService { - - private final LlmClient llmClient; - private final ObjectMapper objectMapper; - - private static final String SYSTEM_PROMPT = """ - 너는 기술 블로그 분석 전문가야. - 주어진 기술 블로그 글을 분석하고, 요약과 핵심 키워드를 추출해줘. - - 응답은 반드시 아래 JSON 형식으로만 작성해: - { - "summary": "상세 요약 내용", - "shortSummary": "짧은 요약 내용", - "keywords": ["키워드1", "키워드2", ...] - } - """; - - public SummaryWithKeywordsDto extractSummary(String title, String content) { - String processedContent = content; - if (content != null && content.length() > 50000) { - processedContent = ContentCleaner.cleanAndLimit(content, 50000); - log.debug("콘텐츠가 너무 길어 50,000자로 제한: {} (원본: {}자 -> 정제 후: {}자)", - title, content.length(), processedContent.length()); - } - - String userPrompt = buildUserPrompt(title, processedContent); - String response = llmClient.call(SYSTEM_PROMPT, userPrompt); - - log.debug("LLM API 응답 (제목: {}): {}", title, response); - +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * LLM을 사용한 게시글 요약 추출 서비스 + * 의미 기반 검색 최적화를 위한 요약 생성 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SummaryExtractionService { + + private final LlmClient llmClient; + private final ObjectMapper objectMapper; + + private static final String SYSTEM_PROMPT = """ + 너는 기술 블로그 분석 전문가야. + 주어진 기술 블로그 글을 분석하고, 요약과 핵심 키워드를 추출해줘. + + 응답은 반드시 아래 JSON 형식으로만 작성해: + { + "summary": "상세 요약 내용", + "shortSummary": "짧은 요약 내용", + "keywords": ["키워드1", "키워드2", ...] + } + """; + + public SummaryWithKeywordsDto extractSummary(String title, String content) { + String processedContent = content; + if (content != null && content.length() > 50000) { + processedContent = ContentCleaner.cleanAndLimit(content, 50000); + log.debug("콘텐츠가 너무 길어 50,000자로 제한: {} (원본: {}자 -> 정제 후: {}자)", + title, content.length(), processedContent.length()); + } + + String userPrompt = buildUserPrompt(title, processedContent); + String response = llmClient.call(SYSTEM_PROMPT, userPrompt); + + log.debug("LLM API 응답 (제목: {}): {}", title, response); + // JSON 응답 파싱 (파싱 실패 시 예외 전파) return parseResponse(response.trim()); } - - private SummaryWithKeywordsDto parseResponse(String response) { - try { - JsonNode jsonNode = objectMapper.readTree(response); - String summary = jsonNode.get("summary").asText(); - String shortSummary = jsonNode.has("shortSummary") ? jsonNode.get("shortSummary").asText() : ""; - List keywords = new ArrayList<>(); - - JsonNode keywordsNode = jsonNode.get("keywords"); - if (keywordsNode != null && keywordsNode.isArray()) { - keywordsNode.forEach(node -> keywords.add(node.asText())); - } - - return new SummaryWithKeywordsDto(summary, shortSummary, keywords); + + private SummaryWithKeywordsDto parseResponse(String response) { + try { + JsonNode jsonNode = objectMapper.readTree(response); + String summary = jsonNode.get("summary").asText(); + String shortSummary = jsonNode.has("shortSummary") ? jsonNode.get("shortSummary").asText() : ""; + List keywords = new ArrayList<>(); + + JsonNode keywordsNode = jsonNode.get("keywords"); + if (keywordsNode != null && keywordsNode.isArray()) { + keywordsNode.forEach(node -> keywords.add(node.asText())); + } + + return new SummaryWithKeywordsDto(summary, shortSummary, keywords); } catch (Exception e) { log.error("JSON 응답 파싱 실패: {}", response, e); throw new LlmException("LLM summary response parsing failed", e); } } - - private String buildUserPrompt(String title, String content) { - return String.format(""" - 다음 기술 블로그 글을 분석해서 요약과 키워드를 추출해줘: - - 제목: %s - 내용: %s - - 상세 요약(summary) 작성 가이드: - - 글의 전체 맥락과 흐름을 포함 (300-500자) - - 글에서 다루는 핵심 문제와 해결 방법을 구체적으로 기술 - - 사용된 기술 스택, 도구, 프레임워크를 정확한 명칭으로 명시 - - 자연스러운 한국어 문장으로 작성 - - 마크다운이나 특수 기호 없이 순수 텍스트로만 작성 - - 짧은 요약(shortSummary) 작성 가이드: - - 웹 UI에 표시될 짧은 요약 (150-200자) - - 글의 핵심 주제와 내용을 한두 문장으로 간결하게 표현 - - 사용자가 빠르게 글의 내용을 파악할 수 있도록 작성 - - 자연스러운 한국어 문장으로 작성 - - 키워드 추출 가이드: - - 10-15개의 핵심 키워드 추출 - - 기술 스택 (예: Spring Boot, React, Kubernetes) - - 주제/개념 (예: 성능 최적화, 마이크로서비스, CI/CD) - - 방법론 (예: TDD, DDD, 애자일) - - 영문과 한글 키워드 모두 포함 - - 너무 일반적인 키워드(예: "개발", "코딩")는 제외 - """, title, content != null ? content : ""); - } -} + + private String buildUserPrompt(String title, String content) { + return String.format(""" + 다음 기술 블로그 글을 분석해서 요약과 키워드를 추출해줘: + + 제목: %s + 내용: %s + + 상세 요약(summary) 작성 가이드: + - 글의 전체 맥락과 흐름을 포함 (300-500자) + - 글에서 다루는 핵심 문제와 해결 방법을 구체적으로 기술 + - 사용된 기술 스택, 도구, 프레임워크를 정확한 명칭으로 명시 + - 자연스러운 한국어 문장으로 작성 + - 마크다운이나 특수 기호 없이 순수 텍스트로만 작성 + + 짧은 요약(shortSummary) 작성 가이드: + - 웹 UI에 표시될 짧은 요약 (150-200자) + - 글의 핵심 주제와 내용을 한두 문장으로 간결하게 표현 + - 사용자가 빠르게 글의 내용을 파악할 수 있도록 작성 + - 자연스러운 한국어 문장으로 작성 + + 키워드 추출 가이드: + - 10-15개의 핵심 키워드 추출 + - 기술 스택 (예: Spring Boot, React, Kubernetes) + - 주제/개념 (예: 성능 최적화, 마이크로서비스, CI/CD) + - 방법론 (예: TDD, DDD, 애자일) + - 영문과 한글 키워드 모두 포함 + - 너무 일반적인 키워드(예: "개발", "코딩")는 제외 + """, title, content != null ? content : ""); + } +} diff --git a/src/main/java/com/techfork/domain/post/entity/Post.java b/src/main/java/com/techfork/post/domain/Post.java similarity index 96% rename from src/main/java/com/techfork/domain/post/entity/Post.java rename to src/main/java/com/techfork/post/domain/Post.java index 40872a95..50791f0e 100644 --- a/src/main/java/com/techfork/domain/post/entity/Post.java +++ b/src/main/java/com/techfork/post/domain/Post.java @@ -1,112 +1,112 @@ -package com.techfork.domain.post.entity; - -import com.techfork.domain.source.dto.RssFeedItem; -import com.techfork.domain.source.entity.TechBlog; -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.BatchSize; -import org.springframework.data.annotation.PersistenceCreator; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "posts", indexes = { - @Index(name = "idx_post_published_at_id", columnList = "publishedAt, id"), - @Index(name = "idx_post_view_count_id", columnList = "viewCount, id"), - @Index(name = "idx_post_company_published_at_id", columnList = "company, publishedAt, id") -}) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Post extends BaseEntity { - - @Column(nullable = false, length = 500) - private String title; - - @Column(columnDefinition = "MEDIUMTEXT") - private String fullContent; - - @Column(columnDefinition = "MEDIUMTEXT") - private String plainContent; - - @Column(columnDefinition = "TEXT") - private String summary; - - @Column(columnDefinition = "TEXT") - private String shortSummary; - - @Column(nullable = false) - private String company; - - @Column(length = 500) - private String logoUrl; - - @Column(length = 1000) - private String thumbnailUrl; - - @Column(unique = true, nullable = false, length = 1000) - private String url; - - @Column(nullable = false) - private LocalDateTime publishedAt; - - @Column(nullable = false) - private LocalDateTime crawledAt; - - @Column - private LocalDateTime embeddedAt; - - @Column(nullable = false) - private Long viewCount = 0L; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "tech_blog_id", nullable = false) - private TechBlog techBlog; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - @BatchSize(size = 100) - private List keywords = new ArrayList<>(); - - @PersistenceCreator - @Builder - Post(String title, String fullContent, String plainContent, String summary, String shortSummary, String company, String logoUrl, String thumbnailUrl, - String url, LocalDateTime publishedAt, LocalDateTime crawledAt, LocalDateTime embeddedAt, TechBlog techBlog) { - this.title = title; - this.fullContent = fullContent; - this.plainContent = plainContent; - this.summary = summary; - this.shortSummary = shortSummary; - this.company = company; - this.logoUrl = logoUrl; - this.thumbnailUrl = thumbnailUrl; - this.url = url; - this.publishedAt = publishedAt; - this.crawledAt = crawledAt; - this.embeddedAt = embeddedAt; - this.techBlog = techBlog; - } - - public static Post create(RssFeedItem item, TechBlog techBlog) { - return Post.builder() - .title(item.title()) - .fullContent(item.content()) - .plainContent(item.plainContent()) - .company(item.company()) - .logoUrl(item.logoUrl()) - .thumbnailUrl(item.thumbnailUrl()) - .url(item.url()) - .publishedAt(item.publishedAt()) - .crawledAt(LocalDateTime.now()) - .techBlog(techBlog) - .build(); - } - - +package com.techfork.post.domain; + +import com.techfork.domain.source.dto.RssFeedItem; +import com.techfork.domain.source.entity.TechBlog; +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; +import org.springframework.data.annotation.PersistenceCreator; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "posts", indexes = { + @Index(name = "idx_post_published_at_id", columnList = "publishedAt, id"), + @Index(name = "idx_post_view_count_id", columnList = "viewCount, id"), + @Index(name = "idx_post_company_published_at_id", columnList = "company, publishedAt, id") +}) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Post extends BaseEntity { + + @Column(nullable = false, length = 500) + private String title; + + @Column(columnDefinition = "MEDIUMTEXT") + private String fullContent; + + @Column(columnDefinition = "MEDIUMTEXT") + private String plainContent; + + @Column(columnDefinition = "TEXT") + private String summary; + + @Column(columnDefinition = "TEXT") + private String shortSummary; + + @Column(nullable = false) + private String company; + + @Column(length = 500) + private String logoUrl; + + @Column(length = 1000) + private String thumbnailUrl; + + @Column(unique = true, nullable = false, length = 1000) + private String url; + + @Column(nullable = false) + private LocalDateTime publishedAt; + + @Column(nullable = false) + private LocalDateTime crawledAt; + + @Column + private LocalDateTime embeddedAt; + + @Column(nullable = false) + private Long viewCount = 0L; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tech_blog_id", nullable = false) + private TechBlog techBlog; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @BatchSize(size = 100) + private List keywords = new ArrayList<>(); + + @PersistenceCreator + @Builder + Post(String title, String fullContent, String plainContent, String summary, String shortSummary, String company, String logoUrl, String thumbnailUrl, + String url, LocalDateTime publishedAt, LocalDateTime crawledAt, LocalDateTime embeddedAt, TechBlog techBlog) { + this.title = title; + this.fullContent = fullContent; + this.plainContent = plainContent; + this.summary = summary; + this.shortSummary = shortSummary; + this.company = company; + this.logoUrl = logoUrl; + this.thumbnailUrl = thumbnailUrl; + this.url = url; + this.publishedAt = publishedAt; + this.crawledAt = crawledAt; + this.embeddedAt = embeddedAt; + this.techBlog = techBlog; + } + + public static Post create(RssFeedItem item, TechBlog techBlog) { + return Post.builder() + .title(item.title()) + .fullContent(item.content()) + .plainContent(item.plainContent()) + .company(item.company()) + .logoUrl(item.logoUrl()) + .thumbnailUrl(item.thumbnailUrl()) + .url(item.url()) + .publishedAt(item.publishedAt()) + .crawledAt(LocalDateTime.now()) + .techBlog(techBlog) + .build(); + } + + public void updateSummaries(String summary, String shortSummary) { this.summary = summary; this.shortSummary = shortSummary; diff --git a/src/main/java/com/techfork/domain/post/entity/PostKeyword.java b/src/main/java/com/techfork/post/domain/PostKeyword.java similarity index 92% rename from src/main/java/com/techfork/domain/post/entity/PostKeyword.java rename to src/main/java/com/techfork/post/domain/PostKeyword.java index 1621229d..272cbe3e 100644 --- a/src/main/java/com/techfork/domain/post/entity/PostKeyword.java +++ b/src/main/java/com/techfork/post/domain/PostKeyword.java @@ -1,37 +1,37 @@ -package com.techfork.domain.post.entity; - -import com.techfork.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.PersistenceCreator; - -@Entity -@Table(name = "post_keywords") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PostKeyword extends BaseEntity { - - @Column(nullable = false, length = 50) - private String keyword; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id", nullable = false) - private Post post; - - @PersistenceCreator - @Builder - PostKeyword(String keyword, Post post) { - this.keyword = keyword; - this.post = post; - } - - public static PostKeyword create(String keyword, Post post) { - return PostKeyword.builder() - .keyword(keyword) - .post(post) - .build(); - } -} +package com.techfork.post.domain; + +import com.techfork.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.PersistenceCreator; + +@Entity +@Table(name = "post_keywords") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostKeyword extends BaseEntity { + + @Column(nullable = false, length = 50) + private String keyword; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @PersistenceCreator + @Builder + PostKeyword(String keyword, Post post) { + this.keyword = keyword; + this.post = post; + } + + public static PostKeyword create(String keyword, Post post) { + return PostKeyword.builder() + .keyword(keyword) + .post(post) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/post/enums/EPostSortType.java b/src/main/java/com/techfork/post/domain/enums/EPostSortType.java similarity index 80% rename from src/main/java/com/techfork/domain/post/enums/EPostSortType.java rename to src/main/java/com/techfork/post/domain/enums/EPostSortType.java index ec0c0186..eac57d46 100644 --- a/src/main/java/com/techfork/domain/post/enums/EPostSortType.java +++ b/src/main/java/com/techfork/post/domain/enums/EPostSortType.java @@ -1,13 +1,13 @@ -package com.techfork.domain.post.enums; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum EPostSortType { - LATEST("최신순"), - POPULAR("인기순"); - - private final String description; -} +package com.techfork.post.domain.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EPostSortType { + LATEST("최신순"), + POPULAR("인기순"); + + private final String description; +} diff --git a/src/main/java/com/techfork/domain/post/exception/PostErrorCode.java b/src/main/java/com/techfork/post/domain/exception/PostErrorCode.java similarity index 90% rename from src/main/java/com/techfork/domain/post/exception/PostErrorCode.java rename to src/main/java/com/techfork/post/domain/exception/PostErrorCode.java index bc0ea5cb..ba47249a 100644 --- a/src/main/java/com/techfork/domain/post/exception/PostErrorCode.java +++ b/src/main/java/com/techfork/post/domain/exception/PostErrorCode.java @@ -1,24 +1,24 @@ -package com.techfork.domain.post.exception; - -import com.techfork.global.common.code.BaseCode; -import com.techfork.global.response.ReasonDTO; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; - -@RequiredArgsConstructor -public enum PostErrorCode implements BaseCode { - POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST404_1", "게시글을 찾을 수 없습니다."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public ReasonDTO getReason() { - return ReasonDTO.builder() - .httpStatus(httpStatus) - .code(code) - .message(message) - .build(); - } -} +package com.techfork.post.domain.exception; + +import com.techfork.global.common.code.BaseCode; +import com.techfork.global.response.ReasonDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum PostErrorCode implements BaseCode { + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST404_1", "게시글을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .httpStatus(httpStatus) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/post/document/ContentChunk.java b/src/main/java/com/techfork/post/domain/projection/ContentChunk.java similarity index 91% rename from src/main/java/com/techfork/domain/post/document/ContentChunk.java rename to src/main/java/com/techfork/post/domain/projection/ContentChunk.java index 183b8174..0a36666e 100644 --- a/src/main/java/com/techfork/domain/post/document/ContentChunk.java +++ b/src/main/java/com/techfork/post/domain/projection/ContentChunk.java @@ -1,31 +1,31 @@ -package com.techfork.domain.post.document; - -import lombok.*; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -import java.util.List; - -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class ContentChunk { - - @Field(type = FieldType.Integer) - private Integer chunkOrder; - - @Field(type = FieldType.Text) - private String chunkText; - - @Field(type = FieldType.Dense_Vector, dims = 3072) - private List embedding; - - public static ContentChunk create(int order, String text, List embedding) { - return ContentChunk.builder() - .chunkOrder(order) - .chunkText(text) - .embedding(embedding) - .build(); - } -} +package com.techfork.post.domain.projection; + +import lombok.*; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ContentChunk { + + @Field(type = FieldType.Integer) + private Integer chunkOrder; + + @Field(type = FieldType.Text) + private String chunkText; + + @Field(type = FieldType.Dense_Vector, dims = 3072) + private List embedding; + + public static ContentChunk create(int order, String text, List embedding) { + return ContentChunk.builder() + .chunkOrder(order) + .chunkText(text) + .embedding(embedding) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/post/document/PostDocument.java b/src/main/java/com/techfork/post/domain/projection/PostDocument.java similarity index 94% rename from src/main/java/com/techfork/domain/post/document/PostDocument.java rename to src/main/java/com/techfork/post/domain/projection/PostDocument.java index e3ecdd5e..7ca52ff3 100644 --- a/src/main/java/com/techfork/domain/post/document/PostDocument.java +++ b/src/main/java/com/techfork/post/domain/projection/PostDocument.java @@ -1,99 +1,99 @@ -package com.techfork.domain.post.document; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.techfork.domain.post.entity.Post; -import com.techfork.global.util.StringToLocalDateTimeConverter; -import lombok.*; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -import java.time.LocalDateTime; -import java.util.List; - -@Document(indexName = "posts") -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@JsonIgnoreProperties(ignoreUnknown = true) -public class PostDocument { - - @Id - private String id; - - @Field(type = FieldType.Long) - private Long postId; - - @Field(type = FieldType.Text) - private String title; - - @Field(type = FieldType.Text) - private String summary; - - @Field(type = FieldType.Text) - private String shortSummary; - - @Field(type = FieldType.Keyword) - private String company; - - @Field(type = FieldType.Keyword) - private String url; - - @Field(type = FieldType.Keyword) - private String logoUrl; - - @Field(type = FieldType.Keyword) - private String thumbnailUrl; - - @Field(type = FieldType.Keyword, name = "publishedAt") - @JsonProperty("publishedAt") - @Getter(AccessLevel.NONE) - private String publishedAtString; - - @Field(type = FieldType.Dense_Vector, dims = 3072) - private List titleEmbedding; - - @Field(type = FieldType.Dense_Vector, dims = 3072) - private List summaryEmbedding; - - @Field(type = FieldType.Nested) - private List contentChunks; - - @JsonIgnore - public LocalDateTime getPublishedAt() { - if (publishedAtString == null) { - return null; - } - return new StringToLocalDateTimeConverter().convert(publishedAtString); - } - - // Jackson 역직렬화를 위한 setter - @JsonProperty("publishedAt") - private void setPublishedAtString(String publishedAtString) { - this.publishedAtString = publishedAtString; - } - - public static PostDocument create(Post post, List titleEmbedding, - List summaryEmbedding, - List contentChunks) { - return PostDocument.builder() - .id(String.valueOf(post.getId())) - .postId(post.getId()) - .title(post.getTitle()) - .summary(post.getSummary()) - .shortSummary(post.getShortSummary()) - .company(post.getCompany()) - .url(post.getUrl()) - .logoUrl(post.getLogoUrl()) - .thumbnailUrl(post.getThumbnailUrl()) - .publishedAtString(post.getPublishedAt() != null ? post.getPublishedAt().toString() : null) - .titleEmbedding(titleEmbedding) - .summaryEmbedding(summaryEmbedding) - .contentChunks(contentChunks) - .build(); - } -} +package com.techfork.post.domain.projection; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.techfork.post.domain.Post; +import com.techfork.global.util.StringToLocalDateTimeConverter; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.time.LocalDateTime; +import java.util.List; + +@Document(indexName = "posts") +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@JsonIgnoreProperties(ignoreUnknown = true) +public class PostDocument { + + @Id + private String id; + + @Field(type = FieldType.Long) + private Long postId; + + @Field(type = FieldType.Text) + private String title; + + @Field(type = FieldType.Text) + private String summary; + + @Field(type = FieldType.Text) + private String shortSummary; + + @Field(type = FieldType.Keyword) + private String company; + + @Field(type = FieldType.Keyword) + private String url; + + @Field(type = FieldType.Keyword) + private String logoUrl; + + @Field(type = FieldType.Keyword) + private String thumbnailUrl; + + @Field(type = FieldType.Keyword, name = "publishedAt") + @JsonProperty("publishedAt") + @Getter(AccessLevel.NONE) + private String publishedAtString; + + @Field(type = FieldType.Dense_Vector, dims = 3072) + private List titleEmbedding; + + @Field(type = FieldType.Dense_Vector, dims = 3072) + private List summaryEmbedding; + + @Field(type = FieldType.Nested) + private List contentChunks; + + @JsonIgnore + public LocalDateTime getPublishedAt() { + if (publishedAtString == null) { + return null; + } + return new StringToLocalDateTimeConverter().convert(publishedAtString); + } + + // Jackson 역직렬화를 위한 setter + @JsonProperty("publishedAt") + private void setPublishedAtString(String publishedAtString) { + this.publishedAtString = publishedAtString; + } + + public static PostDocument create(Post post, List titleEmbedding, + List summaryEmbedding, + List contentChunks) { + return PostDocument.builder() + .id(String.valueOf(post.getId())) + .postId(post.getId()) + .title(post.getTitle()) + .summary(post.getSummary()) + .shortSummary(post.getShortSummary()) + .company(post.getCompany()) + .url(post.getUrl()) + .logoUrl(post.getLogoUrl()) + .thumbnailUrl(post.getThumbnailUrl()) + .publishedAtString(post.getPublishedAt() != null ? post.getPublishedAt().toString() : null) + .titleEmbedding(titleEmbedding) + .summaryEmbedding(summaryEmbedding) + .contentChunks(contentChunks) + .build(); + } +} diff --git a/src/main/java/com/techfork/domain/post/repository/PostDocumentRepository.java b/src/main/java/com/techfork/post/infrastructure/PostDocumentRepository.java similarity index 74% rename from src/main/java/com/techfork/domain/post/repository/PostDocumentRepository.java rename to src/main/java/com/techfork/post/infrastructure/PostDocumentRepository.java index 3925b6b2..3c936c1a 100644 --- a/src/main/java/com/techfork/domain/post/repository/PostDocumentRepository.java +++ b/src/main/java/com/techfork/post/infrastructure/PostDocumentRepository.java @@ -1,13 +1,13 @@ -package com.techfork.domain.post.repository; - -import com.techfork.domain.post.document.PostDocument; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface PostDocumentRepository extends ElasticsearchRepository { - - Optional findByPostId(Long postId); -} +package com.techfork.post.infrastructure; + +import com.techfork.post.domain.projection.PostDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PostDocumentRepository extends ElasticsearchRepository { + + Optional findByPostId(Long postId); +} diff --git a/src/main/java/com/techfork/domain/post/repository/PostKeywordRepository.java b/src/main/java/com/techfork/post/infrastructure/PostKeywordRepository.java similarity index 79% rename from src/main/java/com/techfork/domain/post/repository/PostKeywordRepository.java rename to src/main/java/com/techfork/post/infrastructure/PostKeywordRepository.java index fb4fc7f7..ce5249b4 100644 --- a/src/main/java/com/techfork/domain/post/repository/PostKeywordRepository.java +++ b/src/main/java/com/techfork/post/infrastructure/PostKeywordRepository.java @@ -1,13 +1,13 @@ -package com.techfork.domain.post.repository; - -import com.techfork.domain.post.entity.PostKeyword; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface PostKeywordRepository extends JpaRepository { - @Query("SELECT pk FROM PostKeyword pk WHERE pk.post.id IN :postIds") - List findByPostIdIn(@Param("postIds") List postIds); -} +package com.techfork.post.infrastructure; + +import com.techfork.post.domain.PostKeyword; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface PostKeywordRepository extends JpaRepository { + @Query("SELECT pk FROM PostKeyword pk WHERE pk.post.id IN :postIds") + List findByPostIdIn(@Param("postIds") List postIds); +} diff --git a/src/main/java/com/techfork/domain/post/repository/PostRepository.java b/src/main/java/com/techfork/post/infrastructure/PostRepository.java similarity index 86% rename from src/main/java/com/techfork/domain/post/repository/PostRepository.java rename to src/main/java/com/techfork/post/infrastructure/PostRepository.java index 4078a972..848b91cf 100644 --- a/src/main/java/com/techfork/domain/post/repository/PostRepository.java +++ b/src/main/java/com/techfork/post/infrastructure/PostRepository.java @@ -1,33 +1,33 @@ -package com.techfork.domain.post.repository; - -import com.techfork.domain.post.dto.CompanyDto; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostInfoDto; -import com.techfork.domain.post.entity.Post; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -public interface PostRepository extends JpaRepository { - @Query("SELECT p.url FROM Post p WHERE p.url IN :urls") - Set findExistingUrls(@Param("urls") List urls); - - @Query(""" - SELECT DISTINCT p FROM Post p - LEFT JOIN FETCH p.keywords - WHERE p.summary IS NULL OR p.summary = '' - """) - List findWithKeywordsBySummaryIsNull(); - +package com.techfork.post.infrastructure; + +import com.techfork.post.application.dto.CompanyDto; +import com.techfork.post.application.dto.PostDetailDto; +import com.techfork.post.application.dto.PostInfoDto; +import com.techfork.post.domain.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface PostRepository extends JpaRepository { + @Query("SELECT p.url FROM Post p WHERE p.url IN :urls") + Set findExistingUrls(@Param("urls") List urls); + + @Query(""" + SELECT DISTINCT p FROM Post p + LEFT JOIN FETCH p.keywords + WHERE p.summary IS NULL OR p.summary = '' + """) + List findWithKeywordsBySummaryIsNull(); + @Query(""" SELECT p FROM Post p WHERE p.summary IS NOT NULL @@ -46,110 +46,110 @@ AND LENGTH(TRIM(p.summary)) > 0 @Query("SELECT DISTINCT p.company FROM Post p ORDER BY p.company") List findDistinctCompanies(); - - @Query(""" - SELECT new com.techfork.domain.post.dto.CompanyDto( - p.company, - (COUNT(CASE WHEN p.publishedAt >= CURRENT_DATE THEN 1 END) > 0), - MAX(p.logoUrl) - ) - FROM Post p - GROUP BY p.company - ORDER BY MAX(p.publishedAt) DESC - """) - List findCompaniesWithDetails(); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE (:company IS NULL OR p.company = :company) - AND (:lastPostId IS NULL OR p.id < :lastPostId) - ORDER BY p.publishedAt DESC - """) - List findByCompanyWithCursor( - @Param("company") String company, - @Param("lastPostId") Long lastPostId, - Pageable pageable - ); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE (:companies IS NULL OR p.company IN :companies) - AND ( - :lastPublishedAt IS NULL OR - p.publishedAt < :lastPublishedAt OR - (p.publishedAt = :lastPublishedAt AND p.id < :lastPostId) - ) - ORDER BY p.publishedAt DESC, p.id DESC - """) - List findByCompanyNamesWithCursor(List companies, LocalDateTime lastPublishedAt, Long lastPostId, PageRequest pageRequest); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE :lastPostId IS NULL OR p.id < :lastPostId - ORDER BY p.publishedAt DESC - """) - List findRecentPostsWithCursor( - @Param("lastPostId") Long lastPostId, - Pageable pageable - ); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE ( - :lastPublishedAt IS NULL OR - p.publishedAt < :lastPublishedAt OR - (p.publishedAt = :lastPublishedAt AND p.id < :lastPostId) - ) - ORDER BY p.publishedAt DESC, p.id DESC - """) - List findRecentPostsWithCursorV2( - @Param("lastPublishedAt") LocalDateTime lastPublishedAt, - @Param("lastPostId") Long lastPostId, - Pageable pageable - ); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE :lastPostId IS NULL OR p.id < :lastPostId - ORDER BY p.viewCount DESC, p.publishedAt DESC - """) - List findPopularPostsWithCursor( - @Param("lastPostId") Long lastPostId, - Pageable pageable - ); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostInfoDto( - p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE ( - :lastViewCount IS NULL OR - p.viewCount < :lastViewCount OR - (p.viewCount = :lastViewCount AND p.id < :lastPostId) - ) - ORDER BY p.viewCount DESC, p.id DESC - """) - List findPopularPostsWithCursorV2( - @Param("lastViewCount") Integer lastViewCount, - @Param("lastPostId") Long lastPostId, - Pageable pageable - ); - - @Query(""" - SELECT new com.techfork.domain.post.dto.PostDetailDto( - p.id, p.title, p.summary, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null, null) - FROM Post p - WHERE p.id = :id - """) - Optional findByIdWithTechBlog(@Param("id") Long id); -} + + @Query(""" + SELECT new com.techfork.post.application.dto.CompanyDto( + p.company, + (COUNT(CASE WHEN p.publishedAt >= CURRENT_DATE THEN 1 END) > 0), + MAX(p.logoUrl) + ) + FROM Post p + GROUP BY p.company + ORDER BY MAX(p.publishedAt) DESC + """) + List findCompaniesWithDetails(); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE (:company IS NULL OR p.company = :company) + AND (:lastPostId IS NULL OR p.id < :lastPostId) + ORDER BY p.publishedAt DESC + """) + List findByCompanyWithCursor( + @Param("company") String company, + @Param("lastPostId") Long lastPostId, + Pageable pageable + ); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE (:companies IS NULL OR p.company IN :companies) + AND ( + :lastPublishedAt IS NULL OR + p.publishedAt < :lastPublishedAt OR + (p.publishedAt = :lastPublishedAt AND p.id < :lastPostId) + ) + ORDER BY p.publishedAt DESC, p.id DESC + """) + List findByCompanyNamesWithCursor(List companies, LocalDateTime lastPublishedAt, Long lastPostId, PageRequest pageRequest); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE :lastPostId IS NULL OR p.id < :lastPostId + ORDER BY p.publishedAt DESC + """) + List findRecentPostsWithCursor( + @Param("lastPostId") Long lastPostId, + Pageable pageable + ); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE ( + :lastPublishedAt IS NULL OR + p.publishedAt < :lastPublishedAt OR + (p.publishedAt = :lastPublishedAt AND p.id < :lastPostId) + ) + ORDER BY p.publishedAt DESC, p.id DESC + """) + List findRecentPostsWithCursorV2( + @Param("lastPublishedAt") LocalDateTime lastPublishedAt, + @Param("lastPostId") Long lastPostId, + Pageable pageable + ); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE :lastPostId IS NULL OR p.id < :lastPostId + ORDER BY p.viewCount DESC, p.publishedAt DESC + """) + List findPopularPostsWithCursor( + @Param("lastPostId") Long lastPostId, + Pageable pageable + ); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostInfoDto( + p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE ( + :lastViewCount IS NULL OR + p.viewCount < :lastViewCount OR + (p.viewCount = :lastViewCount AND p.id < :lastPostId) + ) + ORDER BY p.viewCount DESC, p.id DESC + """) + List findPopularPostsWithCursorV2( + @Param("lastViewCount") Integer lastViewCount, + @Param("lastPostId") Long lastPostId, + Pageable pageable + ); + + @Query(""" + SELECT new com.techfork.post.application.dto.PostDetailDto( + p.id, p.title, p.summary, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null, null) + FROM Post p + WHERE p.id = :id + """) + Optional findByIdWithTechBlog(@Param("id") Long id); +} diff --git a/src/main/java/com/techfork/domain/post/controller/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java similarity index 91% rename from src/main/java/com/techfork/domain/post/controller/PostController.java rename to src/main/java/com/techfork/post/presentation/PostController.java index f7843ea0..51a00b0c 100644 --- a/src/main/java/com/techfork/domain/post/controller/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -1,94 +1,94 @@ -package com.techfork.domain.post.controller; - -import com.techfork.domain.post.dto.CompanyListResponse; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostListResponse; -import com.techfork.domain.post.enums.EPostSortType; -import com.techfork.domain.post.service.PostQueryService; -import com.techfork.global.common.code.SuccessCode; -import com.techfork.global.response.BaseResponse; -import com.techfork.global.security.oauth.UserPrincipal; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "Post", description = "게시글 API") -@Slf4j -@RestController -@RequestMapping("/api/v1/posts") -@RequiredArgsConstructor -public class PostController { - - private final PostQueryService postQueryService; - - @Operation( - summary = "게시글이 있는 회사 목록 조회", - description = "게시글이 존재하는 회사명 목록을 조회합니다. (필터링 칩용)" - ) - @GetMapping("/companies") - public ResponseEntity> getCompanies() { - CompanyListResponse response = postQueryService.getCompanies(); - return BaseResponse.of(SuccessCode.OK, response); - } - - @Operation( - summary = "기업별 게시글 조회", - description = "특정 기업의 게시글을 무한 스크롤 방식으로 조회합니다. company 파라미터가 없으면 전체 게시글을 조회합니다. 로그인 시 북마크 여부가 포함됩니다." - ) - @GetMapping("/by-company") - public ResponseEntity> getPostsByCompany( - @Parameter(description = "회사명 필터 (선택, 없으면 전체 조회)") - @RequestParam(required = false) String company, - @Parameter(description = "마지막 게시글 ID (커서, 선택)") - @RequestParam(required = false) Long lastPostId, - @Parameter(description = "페이지 크기 (기본값: 20)") - @RequestParam(defaultValue = "20") int size, - @Parameter(hidden = true) - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getPostsByCompany(company, lastPostId, size, userId); - return BaseResponse.of(SuccessCode.OK, response); - } - - @Operation( - summary = "최근 게시글 조회", - description = "최근 생성된 게시글을 무한 스크롤 방식으로 조회합니다. sortBy로 정렬 기준을 선택할 수 있습니다. 로그인 시 북마크 여부가 포함됩니다." - ) - @GetMapping("/recent") - public ResponseEntity> getRecentPosts( - @Parameter(description = "정렬 기준 (LATEST: 최신순, POPULAR: 인기순, 기본값: LATEST)") - @RequestParam(defaultValue = "LATEST") EPostSortType sortBy, - @Parameter(description = "마지막 게시글 ID (커서, 선택)") - @RequestParam(required = false) Long lastPostId, - @Parameter(description = "페이지 크기 (기본값: 20)") - @RequestParam(defaultValue = "20") int size, - @Parameter(hidden = true) - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); - return BaseResponse.of(SuccessCode.OK, response); - } - - @Operation( - summary = "게시글 상세 조회", - description = "특정 게시글의 상세 정보를 조회합니다. 로그인 시 북마크 여부가 포함됩니다." - ) - @GetMapping("/{postId}") - public ResponseEntity> getPostDetail( - @Parameter(description = "게시글 ID") - @PathVariable Long postId, - @Parameter(hidden = true) - @AuthenticationPrincipal UserPrincipal userPrincipal - ) { - Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostDetailDto response = postQueryService.getPostDetail(postId, userId); - return BaseResponse.of(SuccessCode.OK, response); - } -} +package com.techfork.post.presentation; + +import com.techfork.post.application.dto.CompanyListResponse; +import com.techfork.post.application.dto.PostDetailDto; +import com.techfork.post.application.dto.PostListResponse; +import com.techfork.post.domain.enums.EPostSortType; +import com.techfork.post.application.query.PostQueryService; +import com.techfork.global.common.code.SuccessCode; +import com.techfork.global.response.BaseResponse; +import com.techfork.global.security.oauth.UserPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "Post", description = "게시글 API") +@Slf4j +@RestController +@RequestMapping("/api/v1/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostQueryService postQueryService; + + @Operation( + summary = "게시글이 있는 회사 목록 조회", + description = "게시글이 존재하는 회사명 목록을 조회합니다. (필터링 칩용)" + ) + @GetMapping("/companies") + public ResponseEntity> getCompanies() { + CompanyListResponse response = postQueryService.getCompanies(); + return BaseResponse.of(SuccessCode.OK, response); + } + + @Operation( + summary = "기업별 게시글 조회", + description = "특정 기업의 게시글을 무한 스크롤 방식으로 조회합니다. company 파라미터가 없으면 전체 게시글을 조회합니다. 로그인 시 북마크 여부가 포함됩니다." + ) + @GetMapping("/by-company") + public ResponseEntity> getPostsByCompany( + @Parameter(description = "회사명 필터 (선택, 없으면 전체 조회)") + @RequestParam(required = false) String company, + @Parameter(description = "마지막 게시글 ID (커서, 선택)") + @RequestParam(required = false) Long lastPostId, + @Parameter(description = "페이지 크기 (기본값: 20)") + @RequestParam(defaultValue = "20") int size, + @Parameter(hidden = true) + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + Long userId = userPrincipal != null ? userPrincipal.getId() : null; + PostListResponse response = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + return BaseResponse.of(SuccessCode.OK, response); + } + + @Operation( + summary = "최근 게시글 조회", + description = "최근 생성된 게시글을 무한 스크롤 방식으로 조회합니다. sortBy로 정렬 기준을 선택할 수 있습니다. 로그인 시 북마크 여부가 포함됩니다." + ) + @GetMapping("/recent") + public ResponseEntity> getRecentPosts( + @Parameter(description = "정렬 기준 (LATEST: 최신순, POPULAR: 인기순, 기본값: LATEST)") + @RequestParam(defaultValue = "LATEST") EPostSortType sortBy, + @Parameter(description = "마지막 게시글 ID (커서, 선택)") + @RequestParam(required = false) Long lastPostId, + @Parameter(description = "페이지 크기 (기본값: 20)") + @RequestParam(defaultValue = "20") int size, + @Parameter(hidden = true) + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + Long userId = userPrincipal != null ? userPrincipal.getId() : null; + PostListResponse response = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + return BaseResponse.of(SuccessCode.OK, response); + } + + @Operation( + summary = "게시글 상세 조회", + description = "특정 게시글의 상세 정보를 조회합니다. 로그인 시 북마크 여부가 포함됩니다." + ) + @GetMapping("/{postId}") + public ResponseEntity> getPostDetail( + @Parameter(description = "게시글 ID") + @PathVariable Long postId, + @Parameter(hidden = true) + @AuthenticationPrincipal UserPrincipal userPrincipal + ) { + Long userId = userPrincipal != null ? userPrincipal.getId() : null; + PostDetailDto response = postQueryService.getPostDetail(postId, userId); + return BaseResponse.of(SuccessCode.OK, response); + } +} diff --git a/src/main/java/com/techfork/domain/post/controller/PostControllerV2.java b/src/main/java/com/techfork/post/presentation/PostControllerV2.java similarity index 95% rename from src/main/java/com/techfork/domain/post/controller/PostControllerV2.java rename to src/main/java/com/techfork/post/presentation/PostControllerV2.java index a02e4317..77f7b55d 100644 --- a/src/main/java/com/techfork/domain/post/controller/PostControllerV2.java +++ b/src/main/java/com/techfork/post/presentation/PostControllerV2.java @@ -1,9 +1,9 @@ -package com.techfork.domain.post.controller; +package com.techfork.post.presentation; -import com.techfork.domain.post.dto.CompanyListResponse; -import com.techfork.domain.post.dto.PostListResponse; -import com.techfork.domain.post.enums.EPostSortType; -import com.techfork.domain.post.service.PostQueryService; +import com.techfork.post.application.dto.CompanyListResponse; +import com.techfork.post.application.dto.PostListResponse; +import com.techfork.post.domain.enums.EPostSortType; +import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; import com.techfork.global.response.BaseResponse; import com.techfork.global.security.oauth.UserPrincipal; From b9ab4eb52c3ae9f46d6175e602fc465ad6e99932 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 14:56:54 +0900 Subject: [PATCH 02/13] =?UTF-8?q?test:=20Post=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B5=9C=EC=83=81=EC=9C=84=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EB=A1=9C=20=EC=8A=B9=EA=B2=A9=ED=95=98=EB=A9=B0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=8F=84=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/BookmarkCommandServiceTest.java | 6 +++--- .../query/BookmarkQueryServiceTest.java | 2 +- .../activity/bookmark/domain/BookmarkTest.java | 2 +- .../infrastructure/BookmarkRepositoryTest.java | 4 ++-- .../integration/BookmarkIntegrationTest.java | 4 ++-- .../command/ReadPostCommandServiceTest.java | 8 ++++---- .../query/ReadPostQueryServiceTest.java | 2 +- .../domain/ReadPostFirstReadPolicyTest.java | 2 +- .../activity/readpost/domain/ReadPostTest.java | 2 +- .../FirstReadPostRepositoryTest.java | 4 ++-- .../infrastructure/ReadPostRepositoryTest.java | 4 ++-- ...mmandServiceConcurrencyIntegrationTest.java | 4 ++-- .../integration/ReadPostIntegrationTest.java | 4 ++-- .../PersonalizationProfileServiceTest.java | 4 ++-- ...ecommendationControllerIntegrationTest.java | 4 ++-- .../converter/RecommendationConverterTest.java | 2 +- .../RecommendationQueryServiceTest.java | 2 +- .../source/batch/PostBatchWriterTest.java | 2 +- .../domain/source/batch/RssFeedReaderTest.java | 2 +- .../source/batch/RssToPostProcessorTest.java | 2 +- .../config/RssCrawlingJobIntegrationTest.java | 18 +++++++++--------- .../source/fixture/SourcePostFixture.java | 2 +- .../repository/UserRepositoryTest.java | 4 ++-- .../RecommendationEvaluationService.java | 4 ++-- .../recommendation/RecommendationTestBase.java | 2 +- .../recommendation/setup/PostDataExporter.java | 8 ++++---- .../setup/UserDataSetupAndExporter.java | 2 +- .../setup/components/GroundTruthGenerator.java | 6 +++--- .../setup/components/PostMatcher.java | 4 ++-- .../setup/components/TestDataGenerator.java | 4 ++-- .../setup/components/UserTestDataBuilder.java | 2 +- .../util/EvaluationFixtureLoader.java | 10 +++++----- .../search/SearchEvaluationTestBase.java | 2 +- .../setup/SearchGroundTruthGenerator.java | 4 ++-- .../common/MySqlRedisIntegrationTestBase.java | 2 +- .../batch/PostEmbeddingProcessorTest.java | 12 ++++++------ .../batch/PostEmbeddingReaderDataJpaTest.java | 6 +++--- .../batch/PostEmbeddingReaderTest.java | 8 ++++---- .../batch/PostEmbeddingWriterTest.java | 12 ++++++------ .../batch/PostSummaryProcessorTest.java | 12 ++++++------ .../batch/PostSummaryReaderDataJpaTest.java | 6 +++--- .../batch/PostSummaryReaderTest.java | 8 ++++---- .../batch/PostSummaryWriterDataJpaTest.java | 6 +++--- .../batch/PostSummaryWriterTest.java | 8 ++++---- .../command}/PostCommandServiceTest.java | 4 ++-- .../query}/PostKeywordLookupServiceTest.java | 8 ++++---- .../query}/PostLookupServiceTest.java | 8 ++++---- .../query}/PostQueryServiceTest.java | 14 +++++++------- .../support}/SummaryExtractionServiceTest.java | 4 ++-- .../post/entity => post/domain}/PostTest.java | 4 ++-- .../{domain => }/post/fixture/PostFixture.java | 4 ++-- .../infrastructure}/PostRepositoryTest.java | 10 +++++----- .../PostControllerIntegrationTest.java | 10 +++++----- .../PostControllerV2IntegrationTest.java | 6 +++--- 54 files changed, 145 insertions(+), 145 deletions(-) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingProcessorTest.java (96%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingReaderDataJpaTest.java (97%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingReaderTest.java (95%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostEmbeddingWriterTest.java (96%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostSummaryProcessorTest.java (93%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostSummaryReaderDataJpaTest.java (97%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostSummaryReaderTest.java (93%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostSummaryWriterDataJpaTest.java (97%) rename src/test/java/com/techfork/{domain/post => post/application}/batch/PostSummaryWriterTest.java (97%) rename src/test/java/com/techfork/{domain/post/service => post/application/command}/PostCommandServiceTest.java (94%) rename src/test/java/com/techfork/{domain/post/service => post/application/query}/PostKeywordLookupServiceTest.java (90%) rename src/test/java/com/techfork/{domain/post/service => post/application/query}/PostLookupServiceTest.java (88%) rename src/test/java/com/techfork/{domain/post/service => post/application/query}/PostQueryServiceTest.java (99%) rename src/test/java/com/techfork/{domain/post/service => post/application/support}/SummaryExtractionServiceTest.java (98%) rename src/test/java/com/techfork/{domain/post/entity => post/domain}/PostTest.java (97%) rename src/test/java/com/techfork/{domain => }/post/fixture/PostFixture.java (97%) rename src/test/java/com/techfork/{domain/post/repository => post/infrastructure}/PostRepositoryTest.java (99%) rename src/test/java/com/techfork/{domain/post/controller => post/presentation}/PostControllerIntegrationTest.java (98%) rename src/test/java/com/techfork/{domain/post/controller => post/presentation}/PostControllerV2IntegrationTest.java (99%) diff --git a/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java b/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java index 7d026622..e492a313 100644 --- a/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java +++ b/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java @@ -3,9 +3,9 @@ import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.domain.BookmarkErrorCode; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.exception.PostErrorCode; -import com.techfork.domain.post.service.PostLookupService; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.exception.PostErrorCode; +import com.techfork.post.application.query.PostLookupService; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; diff --git a/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java b/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java index d8d49657..e027fa4d 100644 --- a/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java +++ b/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java @@ -2,7 +2,7 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkQueryRow; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.service.PostKeywordLookupService; +import com.techfork.post.application.query.PostKeywordLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; import com.techfork.domain.useraccount.service.UserLookupService; diff --git a/src/test/java/com/techfork/activity/bookmark/domain/BookmarkTest.java b/src/test/java/com/techfork/activity/bookmark/domain/BookmarkTest.java index 01c74929..db0da07a 100644 --- a/src/test/java/com/techfork/activity/bookmark/domain/BookmarkTest.java +++ b/src/test/java/com/techfork/activity/bookmark/domain/BookmarkTest.java @@ -1,6 +1,6 @@ package com.techfork.activity.bookmark.domain; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepositoryTest.java b/src/test/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepositoryTest.java index d53607ad..16b736e5 100644 --- a/src/test/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepositoryTest.java +++ b/src/test/java/com/techfork/activity/bookmark/infrastructure/BookmarkRepositoryTest.java @@ -1,8 +1,8 @@ package com.techfork.activity.bookmark.infrastructure; import com.techfork.activity.bookmark.domain.Bookmark; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/activity/bookmark/integration/BookmarkIntegrationTest.java b/src/test/java/com/techfork/activity/bookmark/integration/BookmarkIntegrationTest.java index 10a349f1..a81158f1 100644 --- a/src/test/java/com/techfork/activity/bookmark/integration/BookmarkIntegrationTest.java +++ b/src/test/java/com/techfork/activity/bookmark/integration/BookmarkIntegrationTest.java @@ -4,8 +4,8 @@ import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.activity.bookmark.presentation.BookmarkRequest; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java index 919d770e..44afd453 100644 --- a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java +++ b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java @@ -4,10 +4,10 @@ import com.techfork.activity.readpost.domain.ReadPostErrorCode; import com.techfork.activity.readpost.domain.ReadPostFirstReadPolicy; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.service.PostCommandService; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.exception.PostErrorCode; -import com.techfork.domain.post.service.PostLookupService; +import com.techfork.post.application.command.PostCommandService; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.exception.PostErrorCode; +import com.techfork.post.application.query.PostLookupService; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; diff --git a/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java b/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java index 2019c607..d7c03959 100644 --- a/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java +++ b/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java @@ -3,7 +3,7 @@ import com.techfork.activity.bookmark.application.query.lookup.BookmarkLookupService; import com.techfork.activity.readpost.infrastructure.ReadPostQueryRow; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.service.PostKeywordLookupService; +import com.techfork.post.application.query.PostKeywordLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import java.time.LocalDateTime; import java.util.Arrays; diff --git a/src/test/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicyTest.java b/src/test/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicyTest.java index d3ee2c39..8b607d2d 100644 --- a/src/test/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicyTest.java +++ b/src/test/java/com/techfork/activity/readpost/domain/ReadPostFirstReadPolicyTest.java @@ -1,7 +1,7 @@ package com.techfork.activity.readpost.domain; import com.techfork.activity.readpost.infrastructure.FirstReadPostRepository; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/activity/readpost/domain/ReadPostTest.java b/src/test/java/com/techfork/activity/readpost/domain/ReadPostTest.java index 40599e1f..74300b85 100644 --- a/src/test/java/com/techfork/activity/readpost/domain/ReadPostTest.java +++ b/src/test/java/com/techfork/activity/readpost/domain/ReadPostTest.java @@ -1,6 +1,6 @@ package com.techfork.activity.readpost.domain; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/techfork/activity/readpost/infrastructure/FirstReadPostRepositoryTest.java b/src/test/java/com/techfork/activity/readpost/infrastructure/FirstReadPostRepositoryTest.java index d141871f..08492350 100644 --- a/src/test/java/com/techfork/activity/readpost/infrastructure/FirstReadPostRepositoryTest.java +++ b/src/test/java/com/techfork/activity/readpost/infrastructure/FirstReadPostRepositoryTest.java @@ -1,7 +1,7 @@ package com.techfork.activity.readpost.infrastructure; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/activity/readpost/infrastructure/ReadPostRepositoryTest.java b/src/test/java/com/techfork/activity/readpost/infrastructure/ReadPostRepositoryTest.java index e72ed638..eb0539bb 100644 --- a/src/test/java/com/techfork/activity/readpost/infrastructure/ReadPostRepositoryTest.java +++ b/src/test/java/com/techfork/activity/readpost/infrastructure/ReadPostRepositoryTest.java @@ -1,8 +1,8 @@ package com.techfork.activity.readpost.infrastructure; import com.techfork.activity.readpost.domain.ReadPost; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/activity/readpost/integration/ReadPostCommandServiceConcurrencyIntegrationTest.java b/src/test/java/com/techfork/activity/readpost/integration/ReadPostCommandServiceConcurrencyIntegrationTest.java index 93c9d575..c7ae2450 100644 --- a/src/test/java/com/techfork/activity/readpost/integration/ReadPostCommandServiceConcurrencyIntegrationTest.java +++ b/src/test/java/com/techfork/activity/readpost/integration/ReadPostCommandServiceConcurrencyIntegrationTest.java @@ -4,8 +4,8 @@ import com.techfork.activity.readpost.application.command.SaveReadPostCommand; import com.techfork.activity.readpost.infrastructure.FirstReadPostRepository; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/activity/readpost/integration/ReadPostIntegrationTest.java b/src/test/java/com/techfork/activity/readpost/integration/ReadPostIntegrationTest.java index 47a17420..da56061b 100644 --- a/src/test/java/com/techfork/activity/readpost/integration/ReadPostIntegrationTest.java +++ b/src/test/java/com/techfork/activity/readpost/integration/ReadPostIntegrationTest.java @@ -8,8 +8,8 @@ import com.techfork.activity.readpost.infrastructure.FirstReadPostRepository; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.activity.readpost.presentation.ReadPostRequest; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java b/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java index a41699ae..2fed5490 100644 --- a/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java +++ b/src/test/java/com/techfork/domain/personalization/service/PersonalizationProfileServiceTest.java @@ -6,8 +6,8 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.activity.readhistory.infrastructure.SearchHistoryRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; import com.techfork.domain.recommendation.service.RecommendationService; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java b/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java index 7e7f3bd4..9d0e47fb 100644 --- a/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java +++ b/src/test/java/com/techfork/domain/recommendation/controller/RecommendationControllerIntegrationTest.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.recommendation.repository.RecommendedPostRepository; import com.techfork.domain.source.entity.TechBlog; diff --git a/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java b/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java index 3231136c..cf52f9ef 100644 --- a/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java +++ b/src/test/java/com/techfork/domain/recommendation/converter/RecommendationConverterTest.java @@ -1,6 +1,6 @@ package com.techfork.domain.recommendation.converter; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.recommendation.dto.RecommendedPostDto; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.source.entity.TechBlog; diff --git a/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java b/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java index 1155bddf..7883716f 100644 --- a/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java +++ b/src/test/java/com/techfork/domain/recommendation/service/RecommendationQueryServiceTest.java @@ -1,7 +1,7 @@ package com.techfork.domain.recommendation.service; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.recommendation.converter.RecommendationConverter; import com.techfork.domain.recommendation.dto.RecommendationListResponse; import com.techfork.domain.recommendation.dto.RecommendedPostDto; diff --git a/src/test/java/com/techfork/domain/source/batch/PostBatchWriterTest.java b/src/test/java/com/techfork/domain/source/batch/PostBatchWriterTest.java index 0e9c1734..1372d482 100644 --- a/src/test/java/com/techfork/domain/source/batch/PostBatchWriterTest.java +++ b/src/test/java/com/techfork/domain/source/batch/PostBatchWriterTest.java @@ -1,6 +1,6 @@ package com.techfork.domain.source.batch; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.global.util.JdbcBatchExecutor; diff --git a/src/test/java/com/techfork/domain/source/batch/RssFeedReaderTest.java b/src/test/java/com/techfork/domain/source/batch/RssFeedReaderTest.java index d44f3f42..76491285 100644 --- a/src/test/java/com/techfork/domain/source/batch/RssFeedReaderTest.java +++ b/src/test/java/com/techfork/domain/source/batch/RssFeedReaderTest.java @@ -1,6 +1,6 @@ package com.techfork.domain.source.batch; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; diff --git a/src/test/java/com/techfork/domain/source/batch/RssToPostProcessorTest.java b/src/test/java/com/techfork/domain/source/batch/RssToPostProcessorTest.java index edd599a7..77f91d4b 100644 --- a/src/test/java/com/techfork/domain/source/batch/RssToPostProcessorTest.java +++ b/src/test/java/com/techfork/domain/source/batch/RssToPostProcessorTest.java @@ -1,6 +1,6 @@ package com.techfork.domain.source.batch; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; diff --git a/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java b/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java index 4098b6ad..d0ec40bb 100644 --- a/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java +++ b/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java @@ -1,14 +1,14 @@ package com.techfork.domain.source.config; -import com.techfork.domain.post.batch.PostEmbeddingProcessor; -import com.techfork.domain.post.batch.PostEmbeddingReader; -import com.techfork.domain.post.batch.PostEmbeddingWriter; -import com.techfork.domain.post.batch.PostSummaryProcessor; -import com.techfork.domain.post.batch.PostSummaryReader; -import com.techfork.domain.post.batch.PostSummaryWriter; -import com.techfork.domain.post.document.ContentChunk; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.application.batch.PostEmbeddingProcessor; +import com.techfork.post.application.batch.PostEmbeddingReader; +import com.techfork.post.application.batch.PostEmbeddingWriter; +import com.techfork.post.application.batch.PostSummaryProcessor; +import com.techfork.post.application.batch.PostSummaryReader; +import com.techfork.post.application.batch.PostSummaryWriter; +import com.techfork.post.domain.projection.ContentChunk; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; import com.techfork.domain.source.batch.PostBatchWriter; import com.techfork.domain.source.batch.RssFeedReader; import com.techfork.domain.source.batch.RssToPostProcessor; diff --git a/src/test/java/com/techfork/domain/source/fixture/SourcePostFixture.java b/src/test/java/com/techfork/domain/source/fixture/SourcePostFixture.java index e398f5a8..1142d3ee 100644 --- a/src/test/java/com/techfork/domain/source/fixture/SourcePostFixture.java +++ b/src/test/java/com/techfork/domain/source/fixture/SourcePostFixture.java @@ -1,6 +1,6 @@ package com.techfork.domain.source.fixture; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import org.springframework.test.util.ReflectionTestUtils; diff --git a/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java b/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java index e8776088..50c380ab 100644 --- a/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java +++ b/src/test/java/com/techfork/domain/useraccount/repository/UserRepositoryTest.java @@ -6,8 +6,8 @@ import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.activity.readhistory.infrastructure.SearchHistoryRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java index 44913d72..61bc2748 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationEvaluationService.java @@ -6,8 +6,8 @@ import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch._types.KnnSearch; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.recommendation.config.RecommendationProperties; import com.techfork.domain.recommendation.entity.RecommendedPost; import com.techfork.domain.recommendation.repository.RecommendationHistoryRepository; diff --git a/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java b/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java index d40fc050..e83fac51 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java +++ b/src/test/java/com/techfork/evaluation/recommendation/RecommendationTestBase.java @@ -2,7 +2,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.repository.PostDocumentRepository; +import com.techfork.post.infrastructure.PostDocumentRepository; import com.techfork.domain.recommendation.config.RecommendationProperties; import com.techfork.evaluation.recommendation.util.EvaluationFixtureLoader; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/PostDataExporter.java b/src/test/java/com/techfork/evaluation/recommendation/setup/PostDataExporter.java index 895b9ff6..10e81cf8 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/PostDataExporter.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/PostDataExporter.java @@ -1,9 +1,9 @@ package com.techfork.evaluation.recommendation.setup; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostDocumentRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostDocumentRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.evaluation.recommendation.setup.components.FileExporter; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Disabled; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java index 32da51fc..4b65ec8e 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/UserDataSetupAndExporter.java @@ -2,7 +2,7 @@ import com.techfork.activity.readpost.domain.ReadPost; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.evaluation.recommendation.setup.components.FileExporter; import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator; import com.techfork.evaluation.recommendation.setup.components.TestDataGenerator.UserCreationResult; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java index d9ecbeb2..0f474d92 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/GroundTruthGenerator.java @@ -1,8 +1,8 @@ package com.techfork.evaluation.recommendation.setup.components; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostDocumentRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostDocumentRepository; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import com.techfork.global.llm.LlmClient; import lombok.RequiredArgsConstructor; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java index ec79ace5..302b76b0 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/PostMatcher.java @@ -1,7 +1,7 @@ package com.techfork.evaluation.recommendation.setup.components; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.useraccount.enums.EInterestCategory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java index 624b1a63..843b225d 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/TestDataGenerator.java @@ -1,7 +1,7 @@ package com.techfork.evaluation.recommendation.setup.components; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.enums.EInterestCategory; diff --git a/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java b/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java index 955b7f1b..803c026e 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java +++ b/src/test/java/com/techfork/evaluation/recommendation/setup/components/UserTestDataBuilder.java @@ -6,7 +6,7 @@ import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.activity.readhistory.infrastructure.SearchHistoryRepository; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.entity.UserInterestCategory; import com.techfork.domain.useraccount.entity.UserInterestKeyword; diff --git a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java index 0d4dcba6..90281ffe 100644 --- a/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java +++ b/src/test/java/com/techfork/evaluation/recommendation/util/EvaluationFixtureLoader.java @@ -5,11 +5,11 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.techfork.activity.readpost.domain.ReadPost; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.domain.post.document.ContentChunk; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostDocumentRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.ContentChunk; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostDocumentRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.personalization.document.PersonalizationProfileDocument; diff --git a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java index ecd2e63b..bfe51e86 100644 --- a/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java +++ b/src/test/java/com/techfork/evaluation/search/SearchEvaluationTestBase.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.search.dto.SearchResult; import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchService; diff --git a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java index 6204fe9d..2c8fe73f 100644 --- a/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java +++ b/src/test/java/com/techfork/evaluation/search/setup/SearchGroundTruthGenerator.java @@ -3,8 +3,8 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.SearchResponse; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.search.dto.SearchResult; import com.techfork.domain.search.config.GeneralSearchProperties; import com.techfork.domain.search.service.SearchServiceImpl; diff --git a/src/test/java/com/techfork/global/common/MySqlRedisIntegrationTestBase.java b/src/test/java/com/techfork/global/common/MySqlRedisIntegrationTestBase.java index 2e0c74ca..506db1c0 100644 --- a/src/test/java/com/techfork/global/common/MySqlRedisIntegrationTestBase.java +++ b/src/test/java/com/techfork/global/common/MySqlRedisIntegrationTestBase.java @@ -2,7 +2,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import com.techfork.domain.personalization.repository.PersonalizationProfileDocumentRepository; -import com.techfork.domain.post.repository.PostDocumentRepository; +import com.techfork.post.infrastructure.PostDocumentRepository; import com.techfork.evaluation.recommendation.RecommendationEvaluationService; import com.techfork.global.configuration.MySqlRedisIntegrationTestConfig; import org.junit.jupiter.api.Tag; diff --git a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingProcessorTest.java b/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java similarity index 96% rename from src/test/java/com/techfork/domain/post/batch/PostEmbeddingProcessorTest.java rename to src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java index 9177537b..bac4c921 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingProcessorTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java @@ -1,10 +1,10 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.document.ContentChunk; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.fixture.PostFixture; -import com.techfork.domain.post.service.ContentChunkerService; +import com.techfork.post.domain.projection.ContentChunk; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.fixture.PostFixture; +import com.techfork.post.application.support.ContentChunkerService; import com.techfork.global.llm.EmbeddingClient; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderDataJpaTest.java b/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java similarity index 97% rename from src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderDataJpaTest.java rename to src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java index 2f5447cc..bf5af791 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderDataJpaTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java @@ -1,7 +1,7 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; diff --git a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderTest.java b/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java similarity index 95% rename from src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderTest.java rename to src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java index 0cc3a9d3..2472f3f2 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingReaderTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.fixture.PostFixture; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.fixture.PostFixture; +import com.techfork.post.infrastructure.PostRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingWriterTest.java b/src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java similarity index 96% rename from src/test/java/com/techfork/domain/post/batch/PostEmbeddingWriterTest.java rename to src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java index eb9b92f5..f1ee1ce7 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostEmbeddingWriterTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java @@ -1,4 +1,4 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ErrorCause; @@ -7,11 +7,11 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.bulk.OperationType; import co.elastic.clients.util.ObjectBuilder; -import com.techfork.domain.post.document.ContentChunk; -import com.techfork.domain.post.document.PostDocument; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.fixture.PostFixture; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.projection.ContentChunk; +import com.techfork.post.domain.projection.PostDocument; +import com.techfork.post.domain.Post; +import com.techfork.post.fixture.PostFixture; +import com.techfork.post.infrastructure.PostRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/batch/PostSummaryProcessorTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java similarity index 93% rename from src/test/java/com/techfork/domain/post/batch/PostSummaryProcessorTest.java rename to src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java index 080f3e40..5ddf4c8b 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostSummaryProcessorTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java @@ -1,10 +1,10 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.dto.SummaryWithKeywordsDto; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.fixture.PostFixture; -import com.techfork.domain.post.service.SummaryExtractionService; +import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.fixture.PostFixture; +import com.techfork.post.application.support.SummaryExtractionService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/batch/PostSummaryReaderDataJpaTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java similarity index 97% rename from src/test/java/com/techfork/domain/post/batch/PostSummaryReaderDataJpaTest.java rename to src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java index d667f2f2..e7b7c9a7 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostSummaryReaderDataJpaTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java @@ -1,10 +1,10 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceUnitUtil; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/techfork/domain/post/batch/PostSummaryReaderTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java similarity index 93% rename from src/test/java/com/techfork/domain/post/batch/PostSummaryReaderTest.java rename to src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java index 5e78f3f2..8b2e2b25 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostSummaryReaderTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.fixture.PostFixture; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.fixture.PostFixture; +import com.techfork.post.infrastructure.PostRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/batch/PostSummaryWriterDataJpaTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java similarity index 97% rename from src/test/java/com/techfork/domain/post/batch/PostSummaryWriterDataJpaTest.java rename to src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java index 18e026d8..a9ddcac7 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostSummaryWriterDataJpaTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java @@ -1,10 +1,10 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.global.util.JdbcBatchExecutor; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/techfork/domain/post/batch/PostSummaryWriterTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java similarity index 97% rename from src/test/java/com/techfork/domain/post/batch/PostSummaryWriterTest.java rename to src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java index 2d10b9b6..23ff9ad6 100644 --- a/src/test/java/com/techfork/domain/post/batch/PostSummaryWriterTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.batch; +package com.techfork.post.application.batch; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.fixture.PostFixture; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.fixture.PostFixture; import com.techfork.global.util.JdbcBatchExecutor; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/post/service/PostCommandServiceTest.java b/src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java similarity index 94% rename from src/test/java/com/techfork/domain/post/service/PostCommandServiceTest.java rename to src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java index 2052067b..359221e4 100644 --- a/src/test/java/com/techfork/domain/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java @@ -1,6 +1,6 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.command; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.infrastructure.PostRepository; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/service/PostKeywordLookupServiceTest.java b/src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java similarity index 90% rename from src/test/java/com/techfork/domain/post/service/PostKeywordLookupServiceTest.java rename to src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java index ff613a5b..92e36adf 100644 --- a/src/test/java/com/techfork/domain/post/service/PostKeywordLookupServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.repository.PostKeywordRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.infrastructure.PostKeywordRepository; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/post/service/PostLookupServiceTest.java b/src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java similarity index 88% rename from src/test/java/com/techfork/domain/post/service/PostLookupServiceTest.java rename to src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java index 503290ac..c214261e 100644 --- a/src/test/java/com/techfork/domain/post/service/PostLookupServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.exception.PostErrorCode; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.exception.PostErrorCode; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.global.exception.GeneralException; import java.util.Optional; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java similarity index 99% rename from src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java rename to src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index 747fe0da..d4a54df1 100644 --- a/src/test/java/com/techfork/domain/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -1,12 +1,12 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.converter.PostConverter; -import com.techfork.domain.post.dto.*; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.enums.EPostSortType; -import com.techfork.domain.post.repository.PostKeywordRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.application.converter.PostConverter; +import com.techfork.post.application.dto.*; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.domain.enums.EPostSortType; +import com.techfork.post.infrastructure.PostKeywordRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.global.exception.GeneralException; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/techfork/domain/post/service/SummaryExtractionServiceTest.java b/src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java similarity index 98% rename from src/test/java/com/techfork/domain/post/service/SummaryExtractionServiceTest.java rename to src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java index 1df2dbc9..299b854b 100644 --- a/src/test/java/com/techfork/domain/post/service/SummaryExtractionServiceTest.java +++ b/src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java @@ -1,7 +1,7 @@ -package com.techfork.domain.post.service; +package com.techfork.post.application.support; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.domain.post.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.dto.SummaryWithKeywordsDto; import com.techfork.global.llm.LlmClient; import com.techfork.global.llm.exception.LlmException; import com.techfork.global.util.ContentCleaner; diff --git a/src/test/java/com/techfork/domain/post/entity/PostTest.java b/src/test/java/com/techfork/post/domain/PostTest.java similarity index 97% rename from src/test/java/com/techfork/domain/post/entity/PostTest.java rename to src/test/java/com/techfork/post/domain/PostTest.java index 651ea408..706396a9 100644 --- a/src/test/java/com/techfork/domain/post/entity/PostTest.java +++ b/src/test/java/com/techfork/post/domain/PostTest.java @@ -1,8 +1,8 @@ -package com.techfork.domain.post.entity; +package com.techfork.post.domain; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; -import com.techfork.domain.post.fixture.PostFixture; +import com.techfork.post.fixture.PostFixture; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/domain/post/fixture/PostFixture.java b/src/test/java/com/techfork/post/fixture/PostFixture.java similarity index 97% rename from src/test/java/com/techfork/domain/post/fixture/PostFixture.java rename to src/test/java/com/techfork/post/fixture/PostFixture.java index 5abe4dbf..5fe098cc 100644 --- a/src/test/java/com/techfork/domain/post/fixture/PostFixture.java +++ b/src/test/java/com/techfork/post/fixture/PostFixture.java @@ -1,6 +1,6 @@ -package com.techfork.domain.post.fixture; +package com.techfork.post.fixture; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; import com.techfork.domain.source.entity.TechBlog; import org.springframework.test.util.ReflectionTestUtils; diff --git a/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java b/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java similarity index 99% rename from src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java rename to src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java index 0f47e56d..c0288635 100644 --- a/src/test/java/com/techfork/domain/post/repository/PostRepositoryTest.java +++ b/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java @@ -1,9 +1,9 @@ -package com.techfork.domain.post.repository; +package com.techfork.post.infrastructure; -import com.techfork.domain.post.dto.CompanyDto; -import com.techfork.domain.post.dto.PostDetailDto; -import com.techfork.domain.post.dto.PostInfoDto; -import com.techfork.domain.post.entity.Post; +import com.techfork.post.application.dto.CompanyDto; +import com.techfork.post.application.dto.PostDetailDto; +import com.techfork.post.application.dto.PostInfoDto; +import com.techfork.post.domain.Post; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import jakarta.persistence.EntityManager; diff --git a/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java b/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java similarity index 98% rename from src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java rename to src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java index e4dda4b9..69baae1a 100644 --- a/src/test/java/com/techfork/domain/post/controller/PostControllerIntegrationTest.java +++ b/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java @@ -1,11 +1,11 @@ -package com.techfork.domain.post.controller; +package com.techfork.post.presentation; import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.entity.PostKeyword; -import com.techfork.domain.post.repository.PostKeywordRepository; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.domain.PostKeyword; +import com.techfork.post.infrastructure.PostKeywordRepository; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; diff --git a/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java b/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java similarity index 99% rename from src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java rename to src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java index 18f673ee..abfe5b99 100644 --- a/src/test/java/com/techfork/domain/post/controller/PostControllerV2IntegrationTest.java +++ b/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java @@ -1,9 +1,9 @@ -package com.techfork.domain.post.controller; +package com.techfork.post.presentation; import com.techfork.activity.bookmark.domain.Bookmark; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.domain.post.entity.Post; -import com.techfork.domain.post.repository.PostRepository; +import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.PostRepository; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; import com.techfork.domain.useraccount.entity.User; From a63d9201d47b1ae48660c25dd79d69befb127efd Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:16:36 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20batch=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20DDD=20=EA=B3=84=EC=B8=B5=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/source/config/RssCrawlingJobConfig.java | 7 ++++++- .../techfork/post/application/query/PostQueryService.java | 2 +- .../batch/PostEmbeddingReader.java | 2 +- .../batch/PostEmbeddingWriter.java | 2 +- .../batch/PostSummaryReader.java | 2 +- .../batch/PostSummaryWriter.java | 2 +- .../converter => presentation}/PostConverter.java | 2 +- .../source/config/RssCrawlingJobIntegrationTest.java | 8 ++++---- .../post/application/query/PostQueryServiceTest.java | 2 +- .../batch/PostEmbeddingReaderDataJpaTest.java | 2 +- .../batch/PostEmbeddingReaderTest.java | 2 +- .../batch/PostEmbeddingWriterTest.java | 2 +- .../batch/PostSummaryReaderDataJpaTest.java | 2 +- .../batch/PostSummaryReaderTest.java | 2 +- .../batch/PostSummaryWriterDataJpaTest.java | 2 +- .../batch/PostSummaryWriterTest.java | 2 +- 16 files changed, 24 insertions(+), 19 deletions(-) rename src/main/java/com/techfork/post/{application => infrastructure}/batch/PostEmbeddingReader.java (97%) rename src/main/java/com/techfork/post/{application => infrastructure}/batch/PostEmbeddingWriter.java (98%) rename src/main/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryReader.java (95%) rename src/main/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryWriter.java (98%) rename src/main/java/com/techfork/post/{application/converter => presentation}/PostConverter.java (97%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostEmbeddingReaderDataJpaTest.java (99%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostEmbeddingReaderTest.java (98%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostEmbeddingWriterTest.java (99%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryReaderDataJpaTest.java (99%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryReaderTest.java (98%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryWriterDataJpaTest.java (99%) rename src/test/java/com/techfork/post/{application => infrastructure}/batch/PostSummaryWriterTest.java (99%) diff --git a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java index c40e5c7b..9d110c23 100644 --- a/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java +++ b/src/main/java/com/techfork/domain/source/config/RssCrawlingJobConfig.java @@ -1,8 +1,13 @@ package com.techfork.domain.source.config; -import com.techfork.post.application.batch.*; +import com.techfork.post.application.batch.PostEmbeddingProcessor; +import com.techfork.post.application.batch.PostSummaryProcessor; import com.techfork.post.domain.projection.PostDocument; import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.batch.PostEmbeddingReader; +import com.techfork.post.infrastructure.batch.PostEmbeddingWriter; +import com.techfork.post.infrastructure.batch.PostSummaryReader; +import com.techfork.post.infrastructure.batch.PostSummaryWriter; import com.techfork.domain.source.batch.PostBatchWriter; import com.techfork.domain.source.batch.RssFeedReader; import com.techfork.domain.source.batch.RssToPostProcessor; diff --git a/src/main/java/com/techfork/post/application/query/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java index 57b65d01..8cee54e0 100644 --- a/src/main/java/com/techfork/post/application/query/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -1,12 +1,12 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.converter.PostConverter; import com.techfork.post.application.dto.*; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; import com.techfork.post.infrastructure.PostRepository; +import com.techfork.post.presentation.PostConverter; import com.techfork.global.exception.CommonErrorCode; import com.techfork.global.exception.GeneralException; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; diff --git a/src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java b/src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingReader.java similarity index 97% rename from src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java rename to src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingReader.java index 29a8d0a7..a356996c 100644 --- a/src/main/java/com/techfork/post/application/batch/PostEmbeddingReader.java +++ b/src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingReader.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.infrastructure.PostRepository; diff --git a/src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java b/src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriter.java similarity index 98% rename from src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java rename to src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriter.java index f2a7272d..64c81ea0 100644 --- a/src/main/java/com/techfork/post/application/batch/PostEmbeddingWriter.java +++ b/src/main/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriter.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.BulkResponse; diff --git a/src/main/java/com/techfork/post/application/batch/PostSummaryReader.java b/src/main/java/com/techfork/post/infrastructure/batch/PostSummaryReader.java similarity index 95% rename from src/main/java/com/techfork/post/application/batch/PostSummaryReader.java rename to src/main/java/com/techfork/post/infrastructure/batch/PostSummaryReader.java index 4ca38159..f025bab9 100644 --- a/src/main/java/com/techfork/post/application/batch/PostSummaryReader.java +++ b/src/main/java/com/techfork/post/infrastructure/batch/PostSummaryReader.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.infrastructure.PostRepository; diff --git a/src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java b/src/main/java/com/techfork/post/infrastructure/batch/PostSummaryWriter.java similarity index 98% rename from src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java rename to src/main/java/com/techfork/post/infrastructure/batch/PostSummaryWriter.java index 07da15b1..4a05c038 100644 --- a/src/main/java/com/techfork/post/application/batch/PostSummaryWriter.java +++ b/src/main/java/com/techfork/post/infrastructure/batch/PostSummaryWriter.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.domain.PostKeyword; diff --git a/src/main/java/com/techfork/post/application/converter/PostConverter.java b/src/main/java/com/techfork/post/presentation/PostConverter.java similarity index 97% rename from src/main/java/com/techfork/post/application/converter/PostConverter.java rename to src/main/java/com/techfork/post/presentation/PostConverter.java index fedb48a5..9d72de2e 100644 --- a/src/main/java/com/techfork/post/application/converter/PostConverter.java +++ b/src/main/java/com/techfork/post/presentation/PostConverter.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.converter; +package com.techfork.post.presentation; import com.techfork.post.application.dto.*; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java b/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java index d0ec40bb..52ba90a8 100644 --- a/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java +++ b/src/test/java/com/techfork/domain/source/config/RssCrawlingJobIntegrationTest.java @@ -1,11 +1,11 @@ package com.techfork.domain.source.config; import com.techfork.post.application.batch.PostEmbeddingProcessor; -import com.techfork.post.application.batch.PostEmbeddingReader; -import com.techfork.post.application.batch.PostEmbeddingWriter; +import com.techfork.post.infrastructure.batch.PostEmbeddingReader; +import com.techfork.post.infrastructure.batch.PostEmbeddingWriter; import com.techfork.post.application.batch.PostSummaryProcessor; -import com.techfork.post.application.batch.PostSummaryReader; -import com.techfork.post.application.batch.PostSummaryWriter; +import com.techfork.post.infrastructure.batch.PostSummaryReader; +import com.techfork.post.infrastructure.batch.PostSummaryWriter; import com.techfork.post.domain.projection.ContentChunk; import com.techfork.post.domain.projection.PostDocument; import com.techfork.post.domain.Post; diff --git a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index d4a54df1..1252125b 100644 --- a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -1,8 +1,8 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.converter.PostConverter; import com.techfork.post.application.dto.*; +import com.techfork.post.presentation.PostConverter; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; diff --git a/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderDataJpaTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderDataJpaTest.java index bf5af791..46f3803a 100644 --- a/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderDataJpaTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderDataJpaTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.infrastructure.PostRepository; diff --git a/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderTest.java similarity index 98% rename from src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderTest.java index 2472f3f2..64cfc456 100644 --- a/src/test/java/com/techfork/post/application/batch/PostEmbeddingReaderTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingReaderTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.fixture.PostFixture; diff --git a/src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriterTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriterTest.java index f1ee1ce7..2c523906 100644 --- a/src/test/java/com/techfork/post/application/batch/PostEmbeddingWriterTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostEmbeddingWriterTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ErrorCause; diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderDataJpaTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderDataJpaTest.java index e7b7c9a7..3850db2d 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryReaderDataJpaTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderDataJpaTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderTest.java similarity index 98% rename from src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderTest.java index 8b2e2b25..9f65f21d 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryReaderTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryReaderTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.fixture.PostFixture; diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterDataJpaTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterDataJpaTest.java index a9ddcac7..86404880 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryWriterDataJpaTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterDataJpaTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.domain.source.dto.RssFeedItem; diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java rename to src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterTest.java index 23ff9ad6..23b40b29 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryWriterTest.java +++ b/src/test/java/com/techfork/post/infrastructure/batch/PostSummaryWriterTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.batch; +package com.techfork.post.infrastructure.batch; import com.techfork.post.domain.Post; import com.techfork.post.domain.PostKeyword; From 2de896169856590c6ebcdd1588c5cbfdd554bcc5 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:25:07 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20PostCommandService=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=AA=85=ED=99=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tactical-design.md | 6 +++--- docs/ubiquitous-language/activity.md | 14 +++++++------- docs/ubiquitous-language/post-content.md | 12 ++++++------ .../command/ReadPostCommandService.java | 6 +++--- ...e.java => PostViewCountCommandService.java} | 2 +- .../command/ReadPostCommandServiceTest.java | 18 +++++++++--------- ...va => PostViewCountCommandServiceTest.java} | 8 ++++---- 7 files changed, 33 insertions(+), 33 deletions(-) rename src/main/java/com/techfork/post/application/command/{PostCommandService.java => PostViewCountCommandService.java} (96%) rename src/test/java/com/techfork/post/application/command/{PostCommandServiceTest.java => PostViewCountCommandServiceTest.java} (84%) diff --git a/docs/tactical-design.md b/docs/tactical-design.md index c6a20166..5583864f 100644 --- a/docs/tactical-design.md +++ b/docs/tactical-design.md @@ -13,7 +13,7 @@ | 컨텍스트 | 애그리거트 루트 | 내부 엔티티 / 값 객체 / Projection | 트랜잭션 내 보장 불변식 | 현재 코드 평가 | |---|---|---|---|---| | Source / Ingestion | `TechBlog` | `RssFeedItem`은 DTO/ACL 결과 | `blogUrl`과 `rssUrl`은 유일해야 한다. `lastCrawledAt`은 `markCrawled(LocalDateTime)`으로만 갱신된다. 기술 블로그는 RSS 수집 대상의 기준이다. | `TechBlog`가 Source 컨텍스트의 루트로 적절하다. **`markCrawled(LocalDateTime)` 도메인 메서드 누락** — 현재 Anemic Model 위험. | -| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. 조회수 증가는 `PostCommandService`/`PostRepository`의 SQL atomic UPDATE 경로로만 처리한다. | `Post`가 핵심 애그리거트 루트다. `PostKeyword`는 `Post` 내부 컬렉션으로 보는 것이 자연스럽다. 조회수 증가는 aggregate 필드 증가가 아니라 command/repository 경로를 canonical write path로 본다 (§1.2 참조). | +| Post / Content | `Post` | `PostKeyword`, `PostDocument`, `ContentChunk` | URL은 유일해야 한다. 요약/짧은 요약은 `updateSummaries()`로만 교체된다. 키워드는 `clearKeywords() + addKeyword()` 조합으로만 교체된다. 임베딩 완료 시각은 `markAsEmbedded(LocalDateTime)`으로만 기록된다. 조회수 증가는 `PostViewCountCommandService`/`PostRepository`의 SQL atomic UPDATE 경로로만 처리한다. | `Post`가 핵심 애그리거트 루트다. `PostKeyword`는 `Post` 내부 컬렉션으로 보는 것이 자연스럽다. 조회수 증가는 aggregate 필드 증가가 아니라 command/repository 경로를 canonical write path로 본다 (§1.2 참조). | | User Account | `User` | `UserInterestCategory`, `UserInterestKeyword` | `socialType + socialId` 조합은 유일해야 한다. 상태 전이는 `PENDING → ACTIVE → WITHDRAWN → PENDING(재활성화)` 경로만 허용된다. 관심 키워드는 반드시 선택된 관심 카테고리에 속해야 한다. 관심사 교체는 `replaceInterests()`로 단일 트랜잭션 내 불변식 검증과 함께 처리된다. | `User`가 루트다. 계정/온보딩/관심사 불변식을 소유한다. **`replaceInterests()` 도메인 메서드 누락** — 불변식 검증이 서비스 레이어에 산재. | | Personalization Profile | 명시적 쓰기 애그리거트 없음 | `PersonalizationProfileDocument`, `UserActivityData` | 같은 `userId` 기준 현재 개인화 프로필 projection은 하나만 유지된다. 프로필 텍스트, 벡터, 핵심 키워드는 함께 재생성된다. | Personalization Profile은 aggregate보다 read model / application service 중심 컨텍스트다. 현재 `PersonalizationProfileService`가 생성 책임을 가진다. | | Activity | `ReadPost`, `Bookmark`, `SearchHistory` | `FirstReadPost` | `Bookmark`는 `userId + postId` 조합이 유일해야 한다. `ReadPost`는 같은 사용자+게시글 중복 저장을 허용한다. `FirstReadPost`는 `userId + postId` 유니크 제약으로 "조회수 증가 자격"을 한 번만 부여하는 dedupe ledger 역할을 한다. `SearchHistory`는 같은 검색어를 중복 저장한다 (동일 검색어의 반복 횟수 자체가 개인화 관심 신호가 된다). 행동 기록은 삭제되지 않고 보존된다 (북마크 제외). | 각 행동 기록이 독립 record aggregate처럼 동작한다. 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `activity/` 아래에서 `presentation / application / domain / infrastructure` 구조로 정리되었다. `ReadPost`는 `SaveReadPostCommand`, `GetReadPostsQuery`, `ReadPostConverter`, `BookmarkLookupService`를 통해 저장/조회/북마크 여부 조합을 분담하고, `ReadPostFirstReadPolicy.markFirstRead()` + `first_read_posts` 유니크 제약으로 최초 조회수 증가를 보호한다. 목록 조회 `size`는 HTTP layer에서 `1..100`으로 검증한다. `SearchHistory`는 `SearchHistoryRequest`, `SaveSearchHistoryCommand`, `ReadHistoryCommandService`로 저장 흐름을 분리했다. 또한 Activity application 서비스의 cross-context 조회는 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`를 통해 application 간 의존으로 정리되었다. aggregate/value object 강화, hexagonal port/adaptor 적용, `ManyToOne -> id reference` 같은 경계 재설계는 후속 단계로 미룬다. | @@ -54,7 +54,7 @@ int incrementViewCount(@Param("id") Long id); ``` - `@Version` 낙관적 락은 재시도 비용이 발생하고 조회수 같은 통계성 필드에는 부적합하므로 채택하지 않는다. -- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostCommandService`/`PostRepository` 경로를 canonical write path로 둔다. +- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostViewCountCommandService`/`PostRepository` 경로를 canonical write path로 둔다. - 사용자별 중복 카운트 방지는 `activity/readpost`의 `first_read_posts(user_id, post_id)` 유니크 제약으로 처리한다. - `ReadPostCommandService`는 `ReadPostFirstReadPolicy.markFirstRead()`가 성공한 경우에만 조회수를 증가시킨다. - `markFirstRead()`는 duplicate key만 "이미 읽음"으로 번역하고, 조회수 증가가 실패하면 `ReadPostErrorCode.READ_POST_VIEW_COUNT_INCREMENT_FAILED`를 던져 `first_read_posts` 마킹과 `read_posts` 저장이 함께 롤백되도록 한다. @@ -161,7 +161,7 @@ createSocialUser() → PENDING | P0 | 개인화 프로필이 생성됨 | `PersonalizedProfileGenerated` | `PersonalizationProfileService.generatePersonalizationProfileSync` | 추천 생성, 개인화 검색 준비 완료 | 현재 `PersonalizationProfileService`가 추천 생성을 직접 호출한다. 이벤트 분리 우선순위가 높다. | | P0 | 추천이 생성됨 | `RecommendationsGenerated` | `LlmRecommendationService.generateRecommendationsForUser` | Notification, Analytics | 사용자에게 보여줄 현재 추천 목록이 바뀌는 핵심 이벤트다. | | P1 | 기술 게시글을 읽음 | `TechnicalPostRead` | `ReadPostCommandService.saveReadPost` | 개인화 프로필 갱신, 추천 정책 | 읽기 행동은 개인화 프로필과 읽은 게시글 제외 정책의 핵심 입력이다. | -| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.markFirstRead` + `PostCommandService.incrementViewCount` | 인기순 정렬, 분석 | `first_read_posts` dedupe ledger를 통과한 최초 읽기에서만 발생하며 조회수 증가와 인기순 정렬에 직접 연결된다. | +| P1 | 기술 게시글을 처음 읽음 | `TechnicalPostFirstRead` | `ReadPostFirstReadPolicy.markFirstRead` + `PostViewCountCommandService.incrementViewCount` | 인기순 정렬, 분석 | `first_read_posts` dedupe ledger를 통과한 최초 읽기에서만 발생하며 조회수 증가와 인기순 정렬에 직접 연결된다. | | P1 | 기술 게시글을 북마크함 | `TechnicalPostBookmarked` | `BookmarkCommandService.addBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 강한 선호 신호로 개인화 품질에 중요하다. | | P1 | 북마크가 해제됨 | `BookmarkRemoved` | `BookmarkCommandService.deleteBookmark` | 개인화 프로필 갱신, 추천 튜닝 | 선호 신호 제거로 볼 수 있다. | | P1 | 검색어가 기록됨 | `SearchQueryRecorded` | `saveSearchHistory` | 개인화 프로필 갱신, 검색 분석 | 검색 의도는 개인화 프로필의 주요 입력이다. | diff --git a/docs/ubiquitous-language/activity.md b/docs/ubiquitous-language/activity.md index a27182ad..c8103751 100644 --- a/docs/ubiquitous-language/activity.md +++ b/docs/ubiquitous-language/activity.md @@ -34,7 +34,7 @@ |---|---|---| | 읽기 기록 | `ReadPost` | 사용자와 기술 게시글의 읽기 이벤트 레코드 | | 최초 읽기 마킹 | `ReadPostFirstReadPolicy.markFirstRead` | `first_read_posts` 유니크 제약을 이용해 조회수 증가 여부를 결정하는 정책 | -| 조회수 증가 위임 | `PostCommandService.incrementViewCount` | 첫 읽기일 때 Post 컨텍스트에 조회수 증가를 위임하는 command 경로 | +| 조회수 증가 위임 | `PostViewCountCommandService.incrementViewCount` | 첫 읽기일 때 Post 컨텍스트에 조회수 증가를 위임하는 command 경로 | | 최초 읽기 ledger | `FirstReadPost`, `first_read_posts` | 사용자별 게시글 조회수 dedupe를 담당하는 보조 record | | 북마크 레코드 | `Bookmark` (legacy name: `ScrabPost`) | 북마크 저장 레코드의 현재 표준 이름과 과거 이름을 함께 설명한다 | | 검색 기록 레코드 | `SearchHistory` | 사용자 검색어를 시간순으로 남기는 레코드 | @@ -58,13 +58,13 @@ ## 현재 구조 메모 - 현재 브랜치 기준으로 `Bookmark`, `ReadPost`, `SearchHistory`는 모두 `presentation / application / domain / infrastructure` 기준으로 정리되어 있다. -- `ReadPost` 저장은 `UserLookupService`, `PostLookupService`, `ReadPostFirstReadPolicy`, `PostCommandService`를 조합해 읽기 이력 저장과 조회수 증가를 분리한다. +- `ReadPost` 저장은 `UserLookupService`, `PostLookupService`, `ReadPostFirstReadPolicy`, `PostViewCountCommandService`를 조합해 읽기 이력 저장과 조회수 증가를 분리한다. - `ReadPostFirstReadPolicy.markFirstRead()`는 `first_read_posts(user_id, post_id)` 유니크 제약을 first-read 판정의 단일 진실 원천으로 사용한다. -- `ReadPostCommandService`는 first-read 마킹이 성공했을 때만 `PostCommandService.incrementViewCount()`를 호출하고, 조회수 증가 실패 시 예외를 던져 전체 트랜잭션을 롤백한다. +- `ReadPostCommandService`는 first-read 마킹이 성공했을 때만 `PostViewCountCommandService.incrementViewCount()`를 호출하고, 조회수 증가 실패 시 예외를 던져 전체 트랜잭션을 롤백한다. - `ReadPost` 조회는 `bookmark.infrastructure.BookmarkRepository`를 직접 참조하지 않고 `bookmark.application.query.lookup.BookmarkLookupService`를 통해 북마크 여부를 조합한다. - `ReadPost` 목록 조회 `size`는 HTTP layer에서 `1..100`으로 검증한다. - `SearchHistory` 저장은 `SearchHistoryRequest -> SaveSearchHistoryCommand -> ReadHistoryCommandService` 흐름을 따른다. -- Activity application 서비스의 cross-context 조회/명령 의존은 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`, `PostCommandService`를 통해 application 간 의존으로 정리되어 있다. +- Activity application 서비스의 cross-context 조회/명령 의존은 `UserLookupService`, `PostLookupService`, `PostKeywordLookupService`, `BookmarkLookupService`, `PostViewCountCommandService`를 통해 application 간 의존으로 정리되어 있다. - aggregate/value object 강화, hexagonal architecture(포트/어댑터), `ManyToOne -> ID reference` 전환은 후속 정리 범위다. ## 주요 근거 파일 @@ -90,9 +90,9 @@ - `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkConverter.java` - `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkController.java` - `src/main/java/com/techfork/domain/useraccount/service/UserLookupService.java` -- `src/main/java/com/techfork/domain/post/service/PostLookupService.java` -- `src/main/java/com/techfork/domain/post/service/PostCommandService.java` -- `src/main/java/com/techfork/domain/post/service/PostKeywordLookupService.java` +- `src/main/java/com/techfork/post/application/query/PostLookupService.java` +- `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java` +- `src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java` - `src/main/java/com/techfork/activity/readhistory/application/command/ReadHistoryCommandService.java` - `src/main/java/com/techfork/activity/readhistory/application/command/SaveSearchHistoryCommand.java` - `src/main/java/com/techfork/activity/readhistory/presentation/SearchHistoryRequest.java` diff --git a/docs/ubiquitous-language/post-content.md b/docs/ubiquitous-language/post-content.md index 62eb2e01..c4257db3 100644 --- a/docs/ubiquitous-language/post-content.md +++ b/docs/ubiquitous-language/post-content.md @@ -21,7 +21,7 @@ | 수집일 | `crawledAt` | TechFork가 해당 기술 게시글을 수집한 시각 | | 임베딩 완료일 | `embeddedAt` | 기술 게시글이 임베딩되어 Elasticsearch에 색인된 시각 | | 조회수 | `viewCount` | 사용자가 처음 읽은 경우 증가하는 popularity 지표 | -| 조회수 증가 경로 | `PostCommandService.incrementViewCount()` | production에서 조회수를 증가시키는 canonical command 경로 | +| 조회수 증가 경로 | `PostViewCountCommandService.incrementViewCount()` | production에서 조회수를 증가시키는 canonical command 경로 | | 검색 문서 | `PostDocument` | Elasticsearch `posts` 인덱스에 저장되는 기술 게시글 projection | | 콘텐츠 청크 | `ContentChunk` | 긴 본문을 임베딩 검색용으로 분할한 단위 | | 출처명 | `Post.company`, `TechBlog.companyName` | 기술 게시글이 어느 기술 블로그/회사에서 왔는지 표시하기 위한 이름 | @@ -32,9 +32,9 @@ - 도메인/기획 문서에서는 `Post`를 **기술 게시글**로 부른다. - `PostDocument`, `ContentChunk`는 aggregate가 아니라 **검색/추천용 projection**이다. - `Post.company`는 Source 컨텍스트의 출처명을 복사한 조회용 스냅샷이다. -- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostCommandService`/`PostRepository`의 SQL atomic update를 canonical write path로 둔다. -- Activity 컨텍스트에서는 `first_read_posts(user_id, post_id)` dedupe ledger를 통과한 최초 읽기에서만 `PostCommandService.incrementViewCount()`를 호출한다. -- `PostCommandService.incrementViewCount()`는 DB 값을 원자적으로 증가시키지만, 이미 로드된 managed `Post`의 `viewCount`를 같은 트랜잭션 안에서 최신 상태로 동기화하지는 않는다. +- production 경로에서는 `Post.incrementViewCount()` 같은 엔티티 필드 증가를 사용하지 않고 `PostViewCountCommandService`/`PostRepository`의 SQL atomic update를 canonical write path로 둔다. +- Activity 컨텍스트에서는 `first_read_posts(user_id, post_id)` dedupe ledger를 통과한 최초 읽기에서만 `PostViewCountCommandService.incrementViewCount()`를 호출한다. +- `PostViewCountCommandService.incrementViewCount()`는 DB 값을 원자적으로 증가시키지만, 이미 로드된 managed `Post`의 `viewCount`를 같은 트랜잭션 안에서 최신 상태로 동기화하지는 않는다. - `EDifficultyLevel`은 실제 사용처가 없어 제거되었다. 필요해지면 정책과 함께 재도입한다. ## 내부 glossary @@ -47,7 +47,7 @@ | 청크 projection | `ContentChunk` | 긴 본문을 분할한 임베딩 검색 단위 | | 임베딩 완료 시각 | `embeddedAt` | 검색/추천용 색인 준비 완료 시각 | | 인기 지표 | `viewCount` | 조회수 기반 popularity 지표 | -| 조회수 증가 command | `PostCommandService.incrementViewCount` | 조회수 증가를 DB atomic update로 위임하는 application command | +| 조회수 증가 command | `PostViewCountCommandService.incrementViewCount` | 조회수 증가를 DB atomic update로 위임하는 application command | ## 혼동 금지 @@ -68,7 +68,7 @@ - `src/main/java/com/techfork/domain/post/entity/Post.java` - `src/main/java/com/techfork/domain/post/entity/PostKeyword.java` -- `src/main/java/com/techfork/domain/post/service/PostCommandService.java` +- `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java` - `src/main/java/com/techfork/domain/post/repository/PostRepository.java` - `src/main/java/com/techfork/domain/post/document/PostDocument.java` - `src/main/java/com/techfork/domain/post/document/ContentChunk.java` diff --git a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java index 92a9204b..7d30ea84 100644 --- a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java +++ b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java @@ -5,7 +5,7 @@ import com.techfork.activity.readpost.domain.ReadPostFirstReadPolicy; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.post.domain.Post; -import com.techfork.post.application.command.PostCommandService; +import com.techfork.post.application.command.PostViewCountCommandService; import com.techfork.post.application.query.PostLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; @@ -23,7 +23,7 @@ public class ReadPostCommandService { private final ReadPostRepository readPostRepository; private final PostLookupService postLookupService; - private final PostCommandService postCommandService; + private final PostViewCountCommandService postViewCountCommandService; private final UserLookupService userLookupService; private final ReadPostFirstReadPolicy readPostFirstReadPolicy; @@ -34,7 +34,7 @@ public void saveReadPost(SaveReadPostCommand command) { boolean firstReadMarked = readPostFirstReadPolicy.markFirstRead(user, post, command.readAt()); boolean viewCountIncremented = false; if (firstReadMarked) { - viewCountIncremented = postCommandService.incrementViewCount(post.getId()); + viewCountIncremented = postViewCountCommandService.incrementViewCount(post.getId()); if (!viewCountIncremented) { throw new GeneralException(ReadPostErrorCode.READ_POST_VIEW_COUNT_INCREMENT_FAILED); } diff --git a/src/main/java/com/techfork/post/application/command/PostCommandService.java b/src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java similarity index 96% rename from src/main/java/com/techfork/post/application/command/PostCommandService.java rename to src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java index aae68af0..ed7cbe0f 100644 --- a/src/main/java/com/techfork/post/application/command/PostCommandService.java +++ b/src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java @@ -10,7 +10,7 @@ @Service @RequiredArgsConstructor @Transactional -public class PostCommandService { +public class PostViewCountCommandService { private final PostRepository postRepository; diff --git a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java index 44afd453..ecc4fa79 100644 --- a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java +++ b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java @@ -4,7 +4,7 @@ import com.techfork.activity.readpost.domain.ReadPostErrorCode; import com.techfork.activity.readpost.domain.ReadPostFirstReadPolicy; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.post.application.command.PostCommandService; +import com.techfork.post.application.command.PostViewCountCommandService; import com.techfork.post.domain.Post; import com.techfork.post.domain.exception.PostErrorCode; import com.techfork.post.application.query.PostLookupService; @@ -43,7 +43,7 @@ class ReadPostCommandServiceTest { private PostLookupService postLookupService; @Mock - private PostCommandService postCommandService; + private PostViewCountCommandService postViewCountCommandService; @Mock private UserLookupService userLookupService; @@ -92,7 +92,7 @@ void saveReadPost_FirstRead_IncrementViewCountAndPreserveState() { given(userLookupService.getUserOrThrow(userId)).willReturn(mockUser); given(postLookupService.getPostOrThrow(postId)).willReturn(mockPost); given(readPostFirstReadPolicy.markFirstRead(mockUser, mockPost, readAt)).willReturn(true); - given(postCommandService.incrementViewCount(postId)).willReturn(true); + given(postViewCountCommandService.incrementViewCount(postId)).willReturn(true); given(readPostRepository.save(any(ReadPost.class))).willReturn(mock(ReadPost.class)); Long beforeViewCount = mockPost.getViewCount(); @@ -101,7 +101,7 @@ void saveReadPost_FirstRead_IncrementViewCountAndPreserveState() { ArgumentCaptor readPostCaptor = ArgumentCaptor.forClass(ReadPost.class); verify(readPostFirstReadPolicy, times(1)).markFirstRead(mockUser, mockPost, readAt); - verify(postCommandService, times(1)).incrementViewCount(postId); + verify(postViewCountCommandService, times(1)).incrementViewCount(postId); verify(readPostRepository, times(1)).save(readPostCaptor.capture()); ReadPost savedReadPost = readPostCaptor.getValue(); @@ -149,7 +149,7 @@ void saveReadPost_AlreadyRead_NoIncrementViewCount() { ArgumentCaptor readPostCaptor = ArgumentCaptor.forClass(ReadPost.class); verify(readPostFirstReadPolicy, times(1)).markFirstRead(mockUser, mockPost, readAt); - verify(postCommandService, never()).incrementViewCount(any()); + verify(postViewCountCommandService, never()).incrementViewCount(any()); verify(readPostRepository, times(1)).save(readPostCaptor.capture()); assertThat(mockPost.getViewCount()).isEqualTo(beforeViewCount); @@ -185,14 +185,14 @@ void saveReadPost_FirstRead_ViewCountIncrementFailed_ThrowException() { given(userLookupService.getUserOrThrow(userId)).willReturn(mockUser); given(postLookupService.getPostOrThrow(postId)).willReturn(mockPost); given(readPostFirstReadPolicy.markFirstRead(mockUser, mockPost, readAt)).willReturn(true); - given(postCommandService.incrementViewCount(postId)).willReturn(false); + given(postViewCountCommandService.incrementViewCount(postId)).willReturn(false); assertThatThrownBy(() -> readPostCommandService.saveReadPost(command)) .isInstanceOf(GeneralException.class) .hasFieldOrPropertyWithValue("code", ReadPostErrorCode.READ_POST_VIEW_COUNT_INCREMENT_FAILED); verify(readPostFirstReadPolicy, times(1)).markFirstRead(mockUser, mockPost, readAt); - verify(postCommandService, times(1)).incrementViewCount(postId); + verify(postViewCountCommandService, times(1)).incrementViewCount(postId); verify(readPostRepository, never()).save(any()); } } @@ -215,7 +215,7 @@ void saveReadPost_Fail_UserNotFound() { .hasFieldOrPropertyWithValue("code", UserErrorCode.USER_NOT_FOUND); verify(postLookupService, never()).getPostOrThrow(any()); - verify(postCommandService, never()).incrementViewCount(any()); + verify(postViewCountCommandService, never()).incrementViewCount(any()); verify(readPostFirstReadPolicy, never()).markFirstRead(any(), any(), any()); verify(readPostRepository, never()).save(any()); } @@ -236,7 +236,7 @@ void saveReadPost_Fail_PostNotFound() { .isInstanceOf(GeneralException.class) .hasFieldOrPropertyWithValue("code", PostErrorCode.POST_NOT_FOUND); - verify(postCommandService, never()).incrementViewCount(any()); + verify(postViewCountCommandService, never()).incrementViewCount(any()); verify(readPostFirstReadPolicy, never()).markFirstRead(any(), any(), any()); verify(readPostRepository, never()).save(any()); } diff --git a/src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java b/src/test/java/com/techfork/post/application/command/PostViewCountCommandServiceTest.java similarity index 84% rename from src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java rename to src/test/java/com/techfork/post/application/command/PostViewCountCommandServiceTest.java index 359221e4..43c4d464 100644 --- a/src/test/java/com/techfork/post/application/command/PostCommandServiceTest.java +++ b/src/test/java/com/techfork/post/application/command/PostViewCountCommandServiceTest.java @@ -15,13 +15,13 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -class PostCommandServiceTest { +class PostViewCountCommandServiceTest { @Mock private PostRepository postRepository; @InjectMocks - private PostCommandService postCommandService; + private PostViewCountCommandService postViewCountCommandService; @Nested @DisplayName("incrementViewCount") @@ -33,7 +33,7 @@ void incrementViewCount_ReturnsTrue_WhenSingleRowUpdated() { Long postId = 100L; given(postRepository.incrementViewCount(postId)).willReturn(1); - boolean incremented = postCommandService.incrementViewCount(postId); + boolean incremented = postViewCountCommandService.incrementViewCount(postId); assertThat(incremented).isTrue(); verify(postRepository, times(1)).incrementViewCount(postId); @@ -45,7 +45,7 @@ void incrementViewCount_ReturnsFalse_WhenUpdatedCountIsNotOne() { Long postId = 100L; given(postRepository.incrementViewCount(postId)).willReturn(0); - boolean incremented = postCommandService.incrementViewCount(postId); + boolean incremented = postViewCountCommandService.incrementViewCount(postId); assertThat(incremented).isFalse(); verify(postRepository, times(1)).incrementViewCount(postId); From 4cfb5d759a3ff30fc6c8e24e62f760b35c904a8e Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:29:25 +0900 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=20support=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=8C=80=EC=8B=A0=20application=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=ED=95=98=EC=9C=84=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../techfork/post/application/batch/PostEmbeddingProcessor.java | 2 +- .../techfork/post/application/batch/PostSummaryProcessor.java | 2 +- .../{support => embedding}/ContentChunkerService.java | 2 +- .../{support => summary}/SummaryExtractionService.java | 2 +- .../post/application/batch/PostEmbeddingProcessorTest.java | 2 +- .../post/application/batch/PostSummaryProcessorTest.java | 2 +- .../{support => summary}/SummaryExtractionServiceTest.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/com/techfork/post/application/{support => embedding}/ContentChunkerService.java (99%) rename src/main/java/com/techfork/post/application/{support => summary}/SummaryExtractionService.java (98%) rename src/test/java/com/techfork/post/application/{support => summary}/SummaryExtractionServiceTest.java (99%) diff --git a/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java b/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java index 31d7efb6..119c308c 100644 --- a/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java +++ b/src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java @@ -3,7 +3,7 @@ import com.techfork.post.domain.projection.ContentChunk; import com.techfork.post.domain.projection.PostDocument; import com.techfork.post.domain.Post; -import com.techfork.post.application.support.ContentChunkerService; +import com.techfork.post.application.embedding.ContentChunkerService; import com.techfork.global.llm.EmbeddingClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java index 3899970d..e9ef6eb1 100644 --- a/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java +++ b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java @@ -2,7 +2,7 @@ import com.techfork.post.application.dto.SummaryWithKeywordsDto; import com.techfork.post.domain.Post; -import com.techfork.post.application.support.SummaryExtractionService; +import com.techfork.post.application.summary.SummaryExtractionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.configuration.annotation.StepScope; diff --git a/src/main/java/com/techfork/post/application/support/ContentChunkerService.java b/src/main/java/com/techfork/post/application/embedding/ContentChunkerService.java similarity index 99% rename from src/main/java/com/techfork/post/application/support/ContentChunkerService.java rename to src/main/java/com/techfork/post/application/embedding/ContentChunkerService.java index 97f840af..8788d2aa 100644 --- a/src/main/java/com/techfork/post/application/support/ContentChunkerService.java +++ b/src/main/java/com/techfork/post/application/embedding/ContentChunkerService.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.support; +package com.techfork.post.application.embedding; import com.techfork.global.util.ContentCleaner; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/techfork/post/application/support/SummaryExtractionService.java b/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java similarity index 98% rename from src/main/java/com/techfork/post/application/support/SummaryExtractionService.java rename to src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java index af2d11c7..2bb68783 100644 --- a/src/main/java/com/techfork/post/application/support/SummaryExtractionService.java +++ b/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.support; +package com.techfork.post.application.summary; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java b/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java index bac4c921..ba9cab5e 100644 --- a/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostEmbeddingProcessorTest.java @@ -4,7 +4,7 @@ import com.techfork.post.domain.projection.PostDocument; import com.techfork.post.domain.Post; import com.techfork.post.fixture.PostFixture; -import com.techfork.post.application.support.ContentChunkerService; +import com.techfork.post.application.embedding.ContentChunkerService; import com.techfork.global.llm.EmbeddingClient; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java index 5ddf4c8b..98e8f535 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java @@ -4,7 +4,7 @@ import com.techfork.post.domain.Post; import com.techfork.post.domain.PostKeyword; import com.techfork.post.fixture.PostFixture; -import com.techfork.post.application.support.SummaryExtractionService; +import com.techfork.post.application.summary.SummaryExtractionService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java b/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java similarity index 99% rename from src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java rename to src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java index 299b854b..d23484bc 100644 --- a/src/test/java/com/techfork/post/application/support/SummaryExtractionServiceTest.java +++ b/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.support; +package com.techfork.post.application.summary; import com.fasterxml.jackson.databind.ObjectMapper; import com.techfork.post.application.dto.SummaryWithKeywordsDto; From 40617212e5722f871edf176eb854e30f999f0d76 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:33:05 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20lookup=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EB=AC=B6=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ubiquitous-language/activity.md | 4 ++-- .../bookmark/application/command/BookmarkCommandService.java | 2 +- .../bookmark/application/query/BookmarkQueryService.java | 2 +- .../readpost/application/command/ReadPostCommandService.java | 2 +- .../readpost/application/query/ReadPostQueryService.java | 2 +- .../query/{ => lookup}/PostKeywordLookupService.java | 2 +- .../application/query/{ => lookup}/PostLookupService.java | 2 +- .../application/command/BookmarkCommandServiceTest.java | 2 +- .../bookmark/application/query/BookmarkQueryServiceTest.java | 2 +- .../application/command/ReadPostCommandServiceTest.java | 2 +- .../readpost/application/query/ReadPostQueryServiceTest.java | 2 +- .../query/{ => lookup}/PostKeywordLookupServiceTest.java | 2 +- .../application/query/{ => lookup}/PostLookupServiceTest.java | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) rename src/main/java/com/techfork/post/application/query/{ => lookup}/PostKeywordLookupService.java (94%) rename src/main/java/com/techfork/post/application/query/{ => lookup}/PostLookupService.java (93%) rename src/test/java/com/techfork/post/application/query/{ => lookup}/PostKeywordLookupServiceTest.java (97%) rename src/test/java/com/techfork/post/application/query/{ => lookup}/PostLookupServiceTest.java (97%) diff --git a/docs/ubiquitous-language/activity.md b/docs/ubiquitous-language/activity.md index c8103751..d9e2a160 100644 --- a/docs/ubiquitous-language/activity.md +++ b/docs/ubiquitous-language/activity.md @@ -90,9 +90,9 @@ - `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkConverter.java` - `src/main/java/com/techfork/activity/bookmark/presentation/BookmarkController.java` - `src/main/java/com/techfork/domain/useraccount/service/UserLookupService.java` -- `src/main/java/com/techfork/post/application/query/PostLookupService.java` +- `src/main/java/com/techfork/post/application/query/lookup/PostLookupService.java` - `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java` -- `src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java` +- `src/main/java/com/techfork/post/application/query/lookup/PostKeywordLookupService.java` - `src/main/java/com/techfork/activity/readhistory/application/command/ReadHistoryCommandService.java` - `src/main/java/com/techfork/activity/readhistory/application/command/SaveSearchHistoryCommand.java` - `src/main/java/com/techfork/activity/readhistory/presentation/SearchHistoryRequest.java` diff --git a/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java b/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java index 6292acf4..f4a154ea 100644 --- a/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java +++ b/src/main/java/com/techfork/activity/bookmark/application/command/BookmarkCommandService.java @@ -4,7 +4,7 @@ import com.techfork.activity.bookmark.domain.BookmarkErrorCode; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.post.domain.Post; -import com.techfork.post.application.query.PostLookupService; +import com.techfork.post.application.query.lookup.PostLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.exception.GeneralException; diff --git a/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java b/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java index 88c8d5e2..6fb89b11 100644 --- a/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java +++ b/src/main/java/com/techfork/activity/bookmark/application/query/BookmarkQueryService.java @@ -2,7 +2,7 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkQueryRow; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.query.PostKeywordLookupService; +import com.techfork.post.application.query.lookup.PostKeywordLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; diff --git a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java index 7d30ea84..d3b2c99b 100644 --- a/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java +++ b/src/main/java/com/techfork/activity/readpost/application/command/ReadPostCommandService.java @@ -6,7 +6,7 @@ import com.techfork.activity.readpost.infrastructure.ReadPostRepository; import com.techfork.post.domain.Post; import com.techfork.post.application.command.PostViewCountCommandService; -import com.techfork.post.application.query.PostLookupService; +import com.techfork.post.application.query.lookup.PostLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.service.UserLookupService; import com.techfork.global.exception.GeneralException; diff --git a/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java b/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java index 44095026..dedb4ca6 100644 --- a/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java +++ b/src/main/java/com/techfork/activity/readpost/application/query/ReadPostQueryService.java @@ -3,7 +3,7 @@ import com.techfork.activity.bookmark.application.query.lookup.BookmarkLookupService; import com.techfork.activity.readpost.infrastructure.ReadPostQueryRow; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.post.application.query.PostKeywordLookupService; +import com.techfork.post.application.query.lookup.PostKeywordLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import java.util.List; import java.util.Map; diff --git a/src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java b/src/main/java/com/techfork/post/application/query/lookup/PostKeywordLookupService.java similarity index 94% rename from src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java rename to src/main/java/com/techfork/post/application/query/lookup/PostKeywordLookupService.java index a1c8c6a9..d60e9b5a 100644 --- a/src/main/java/com/techfork/post/application/query/PostKeywordLookupService.java +++ b/src/main/java/com/techfork/post/application/query/lookup/PostKeywordLookupService.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.query; +package com.techfork.post.application.query.lookup; import com.techfork.post.domain.PostKeyword; import com.techfork.post.infrastructure.PostKeywordRepository; diff --git a/src/main/java/com/techfork/post/application/query/PostLookupService.java b/src/main/java/com/techfork/post/application/query/lookup/PostLookupService.java similarity index 93% rename from src/main/java/com/techfork/post/application/query/PostLookupService.java rename to src/main/java/com/techfork/post/application/query/lookup/PostLookupService.java index b9c18a2b..401401f8 100644 --- a/src/main/java/com/techfork/post/application/query/PostLookupService.java +++ b/src/main/java/com/techfork/post/application/query/lookup/PostLookupService.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.query; +package com.techfork.post.application.query.lookup; import com.techfork.post.domain.Post; import com.techfork.post.domain.exception.PostErrorCode; diff --git a/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java b/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java index e492a313..9bb9b33c 100644 --- a/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java +++ b/src/test/java/com/techfork/activity/bookmark/application/command/BookmarkCommandServiceTest.java @@ -5,7 +5,7 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; import com.techfork.post.domain.Post; import com.techfork.post.domain.exception.PostErrorCode; -import com.techfork.post.application.query.PostLookupService; +import com.techfork.post.application.query.lookup.PostLookupService; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; diff --git a/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java b/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java index e027fa4d..c8cd0c81 100644 --- a/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java +++ b/src/test/java/com/techfork/activity/bookmark/application/query/BookmarkQueryServiceTest.java @@ -2,7 +2,7 @@ import com.techfork.activity.bookmark.infrastructure.BookmarkQueryRow; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.query.PostKeywordLookupService; +import com.techfork.post.application.query.lookup.PostKeywordLookupService; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; import com.techfork.domain.useraccount.service.UserLookupService; diff --git a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java index ecc4fa79..7f55238b 100644 --- a/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java +++ b/src/test/java/com/techfork/activity/readpost/application/command/ReadPostCommandServiceTest.java @@ -7,7 +7,7 @@ import com.techfork.post.application.command.PostViewCountCommandService; import com.techfork.post.domain.Post; import com.techfork.post.domain.exception.PostErrorCode; -import com.techfork.post.application.query.PostLookupService; +import com.techfork.post.application.query.lookup.PostLookupService; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.useraccount.entity.User; import com.techfork.domain.useraccount.exception.UserErrorCode; diff --git a/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java b/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java index d7c03959..f1c91274 100644 --- a/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java +++ b/src/test/java/com/techfork/activity/readpost/application/query/ReadPostQueryServiceTest.java @@ -3,7 +3,7 @@ import com.techfork.activity.bookmark.application.query.lookup.BookmarkLookupService; import com.techfork.activity.readpost.infrastructure.ReadPostQueryRow; import com.techfork.activity.readpost.infrastructure.ReadPostRepository; -import com.techfork.post.application.query.PostKeywordLookupService; +import com.techfork.post.application.query.lookup.PostKeywordLookupService; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; import java.time.LocalDateTime; import java.util.Arrays; diff --git a/src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java b/src/test/java/com/techfork/post/application/query/lookup/PostKeywordLookupServiceTest.java similarity index 97% rename from src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java rename to src/test/java/com/techfork/post/application/query/lookup/PostKeywordLookupServiceTest.java index 92e36adf..2facb3a7 100644 --- a/src/test/java/com/techfork/post/application/query/PostKeywordLookupServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/lookup/PostKeywordLookupServiceTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.query; +package com.techfork.post.application.query.lookup; import com.techfork.post.domain.Post; import com.techfork.post.domain.PostKeyword; diff --git a/src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java b/src/test/java/com/techfork/post/application/query/lookup/PostLookupServiceTest.java similarity index 97% rename from src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java rename to src/test/java/com/techfork/post/application/query/lookup/PostLookupServiceTest.java index c214261e..a9352e42 100644 --- a/src/test/java/com/techfork/post/application/query/PostLookupServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/lookup/PostLookupServiceTest.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.query; +package com.techfork.post.application.query.lookup; import com.techfork.post.domain.Post; import com.techfork.post.domain.exception.PostErrorCode; From bb32e0bfb87bb80eb1bb70c5586c9560e3285b43 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:50:39 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20DTO=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RecommendationListResponse.java | 1 - .../application/query/PostQueryService.java | 38 +-- .../post/infrastructure/PostRepository.java | 38 +-- .../post/infrastructure/row/CompanyRow.java | 11 + .../infrastructure/row/PostDetailRow.java | 21 ++ .../post/infrastructure/row/PostInfoRow.java | 22 ++ .../dto => presentation}/CompanyDto.java | 2 +- .../CompanyListResponse.java | 2 +- .../post/presentation/PostController.java | 6 +- .../post/presentation/PostControllerV2.java | 4 +- .../post/presentation/PostConverter.java | 38 ++- .../dto => presentation}/PostDetailDto.java | 2 +- .../dto => presentation}/PostInfoDto.java | 2 +- .../PostListResponse.java | 2 +- .../query/PostQueryServiceTest.java | 236 ++++++++++-------- .../infrastructure/PostRepositoryTest.java | 82 +++--- 16 files changed, 314 insertions(+), 193 deletions(-) create mode 100644 src/main/java/com/techfork/post/infrastructure/row/CompanyRow.java create mode 100644 src/main/java/com/techfork/post/infrastructure/row/PostDetailRow.java create mode 100644 src/main/java/com/techfork/post/infrastructure/row/PostInfoRow.java rename src/main/java/com/techfork/post/{application/dto => presentation}/CompanyDto.java (85%) rename src/main/java/com/techfork/post/{application/dto => presentation}/CompanyListResponse.java (88%) rename src/main/java/com/techfork/post/{application/dto => presentation}/PostDetailDto.java (91%) rename src/main/java/com/techfork/post/{application/dto => presentation}/PostInfoDto.java (92%) rename src/main/java/com/techfork/post/{application/dto => presentation}/PostListResponse.java (89%) diff --git a/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java b/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java index 59e20fb8..db1e3a53 100644 --- a/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java +++ b/src/main/java/com/techfork/domain/recommendation/dto/RecommendationListResponse.java @@ -1,6 +1,5 @@ package com.techfork.domain.recommendation.dto; -import com.techfork.post.application.dto.PostInfoDto; import lombok.Builder; import java.util.List; diff --git a/src/main/java/com/techfork/post/application/query/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java index 8cee54e0..bd0405e3 100644 --- a/src/main/java/com/techfork/post/application/query/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -6,6 +6,12 @@ import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; import com.techfork.post.infrastructure.PostRepository; +import com.techfork.post.infrastructure.row.CompanyRow; +import com.techfork.post.infrastructure.row.PostDetailRow; +import com.techfork.post.infrastructure.row.PostInfoRow; +import com.techfork.post.presentation.CompanyListResponse; +import com.techfork.post.presentation.PostDetailDto; +import com.techfork.post.presentation.PostListResponse; import com.techfork.post.presentation.PostConverter; import com.techfork.global.exception.CommonErrorCode; import com.techfork.global.exception.GeneralException; @@ -40,14 +46,14 @@ public CompanyListResponse getCompanies() { } public CompanyListResponse getCompaniesV2() { - List companies = postRepository.findCompaniesWithDetails(); + List companies = postRepository.findCompaniesWithDetails(); return postConverter.toCompanyListResponseV2(companies); } public PostListResponse getPostsByCompany(String company, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts = postRepository.findByCompanyWithCursor(company, lastPostId, pageRequest); - List postsWithKeywords = attachKeywordsToPostInfoList(posts); + List posts = postRepository.findByCompanyWithCursor(company, lastPostId, pageRequest); + List postsWithKeywords = attachKeywordsToPostInfoList(posts); if (userId != null) { postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); @@ -58,8 +64,8 @@ public PostListResponse getPostsByCompany(String company, Long lastPostId, int s public PostListResponse getPostsByCompanyV2(List companies, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts = postRepository.findByCompanyNamesWithCursor(companies, lastPublishedAt, lastPostId, pageRequest); - List postsWithKeywords = attachKeywordsToPostInfoList(posts); + List posts = postRepository.findByCompanyNamesWithCursor(companies, lastPublishedAt, lastPostId, pageRequest); + List postsWithKeywords = attachKeywordsToPostInfoList(posts); if (userId != null) { postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); @@ -70,7 +76,7 @@ public PostListResponse getPostsByCompanyV2(List companies, LocalDateTim public PostListResponse getRecentPosts(EPostSortType sortBy, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts; + List posts; if (sortBy == EPostSortType.POPULAR) { posts = postRepository.findPopularPostsWithCursor(lastPostId, pageRequest); @@ -78,7 +84,7 @@ public PostListResponse getRecentPosts(EPostSortType sortBy, Long lastPostId, in posts = postRepository.findRecentPostsWithCursor(lastPostId, pageRequest); } - List postsWithKeywords = attachKeywordsToPostInfoList(posts); + List postsWithKeywords = attachKeywordsToPostInfoList(posts); if (userId != null) { postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); @@ -89,7 +95,7 @@ public PostListResponse getRecentPosts(EPostSortType sortBy, Long lastPostId, in public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewCount, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts; + List posts; if (sortBy == EPostSortType.POPULAR) { posts = postRepository.findPopularPostsWithCursorV2(lastViewCount, lastPostId, pageRequest); @@ -97,7 +103,7 @@ public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewC posts = postRepository.findRecentPostsWithCursorV2(lastPublishedAt, lastPostId, pageRequest); } - List postsWithKeywords = attachKeywordsToPostInfoList(posts); + List postsWithKeywords = attachKeywordsToPostInfoList(posts); if (userId != null) { postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); @@ -107,7 +113,7 @@ public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewC } public PostDetailDto getPostDetail(Long postId, Long userId) { - PostDetailDto postDetail = postRepository.findByIdWithTechBlog(postId) + PostDetailRow postDetail = postRepository.findByIdWithTechBlog(postId) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND)); List keywords = postKeywordRepository.findByPostIdIn(List.of(postId)) @@ -123,13 +129,13 @@ public PostDetailDto getPostDetail(Long postId, Long userId) { return postConverter.toPostDetailDto(postDetail, keywords, isBookmarked); } - private List attachKeywordsToPostInfoList(List posts) { + private List attachKeywordsToPostInfoList(List posts) { if (posts.isEmpty()) { return posts; } List postIds = posts.stream() - .map(PostInfoDto::id) + .map(PostInfoRow::id) .toList(); Map> keywordMap = postKeywordRepository.findByPostIdIn(postIds) @@ -140,7 +146,7 @@ private List attachKeywordsToPostInfoList(List posts) )); return posts.stream() - .map(post -> PostInfoDto.builder() + .map(post -> PostInfoRow.builder() .id(post.id()) .title(post.title()) .shortSummary(post.shortSummary()) @@ -156,13 +162,13 @@ private List attachKeywordsToPostInfoList(List posts) .toList(); } - private List attachBookmarksToPostInfoList(List posts, Long userId) { + private List attachBookmarksToPostInfoList(List posts, Long userId) { if (posts.isEmpty()) { return posts; } List postIds = posts.stream() - .map(PostInfoDto::id) + .map(PostInfoRow::id) .toList(); Set bookmarkedPostIds = bookmarkRepository.findBookmarkedPostIds(userId, postIds) @@ -170,7 +176,7 @@ private List attachBookmarksToPostInfoList(List posts, .collect(Collectors.toSet()); return posts.stream() - .map(post -> PostInfoDto.builder() + .map(post -> PostInfoRow.builder() .id(post.id()) .title(post.title()) .shortSummary(post.shortSummary()) diff --git a/src/main/java/com/techfork/post/infrastructure/PostRepository.java b/src/main/java/com/techfork/post/infrastructure/PostRepository.java index 848b91cf..23593af7 100644 --- a/src/main/java/com/techfork/post/infrastructure/PostRepository.java +++ b/src/main/java/com/techfork/post/infrastructure/PostRepository.java @@ -1,9 +1,9 @@ package com.techfork.post.infrastructure; -import com.techfork.post.application.dto.CompanyDto; -import com.techfork.post.application.dto.PostDetailDto; -import com.techfork.post.application.dto.PostInfoDto; import com.techfork.post.domain.Post; +import com.techfork.post.infrastructure.row.CompanyRow; +import com.techfork.post.infrastructure.row.PostDetailRow; +import com.techfork.post.infrastructure.row.PostInfoRow; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -48,7 +48,7 @@ AND LENGTH(TRIM(p.summary)) > 0 List findDistinctCompanies(); @Query(""" - SELECT new com.techfork.post.application.dto.CompanyDto( + SELECT new com.techfork.post.infrastructure.row.CompanyRow( p.company, (COUNT(CASE WHEN p.publishedAt >= CURRENT_DATE THEN 1 END) > 0), MAX(p.logoUrl) @@ -57,24 +57,24 @@ AND LENGTH(TRIM(p.summary)) > 0 GROUP BY p.company ORDER BY MAX(p.publishedAt) DESC """) - List findCompaniesWithDetails(); + List findCompaniesWithDetails(); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE (:company IS NULL OR p.company = :company) AND (:lastPostId IS NULL OR p.id < :lastPostId) ORDER BY p.publishedAt DESC """) - List findByCompanyWithCursor( + List findByCompanyWithCursor( @Param("company") String company, @Param("lastPostId") Long lastPostId, Pageable pageable ); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE (:companies IS NULL OR p.company IN :companies) @@ -85,22 +85,22 @@ List findByCompanyWithCursor( ) ORDER BY p.publishedAt DESC, p.id DESC """) - List findByCompanyNamesWithCursor(List companies, LocalDateTime lastPublishedAt, Long lastPostId, PageRequest pageRequest); + List findByCompanyNamesWithCursor(List companies, LocalDateTime lastPublishedAt, Long lastPostId, PageRequest pageRequest); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE :lastPostId IS NULL OR p.id < :lastPostId ORDER BY p.publishedAt DESC """) - List findRecentPostsWithCursor( + List findRecentPostsWithCursor( @Param("lastPostId") Long lastPostId, Pageable pageable ); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE ( @@ -110,26 +110,26 @@ List findRecentPostsWithCursor( ) ORDER BY p.publishedAt DESC, p.id DESC """) - List findRecentPostsWithCursorV2( + List findRecentPostsWithCursorV2( @Param("lastPublishedAt") LocalDateTime lastPublishedAt, @Param("lastPostId") Long lastPostId, Pageable pageable ); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE :lastPostId IS NULL OR p.id < :lastPostId ORDER BY p.viewCount DESC, p.publishedAt DESC """) - List findPopularPostsWithCursor( + List findPopularPostsWithCursor( @Param("lastPostId") Long lastPostId, Pageable pageable ); @Query(""" - SELECT new com.techfork.post.application.dto.PostInfoDto( + SELECT new com.techfork.post.infrastructure.row.PostInfoRow( p.id, p.title, p.shortSummary, p.company, p.url, p.logoUrl, p.thumbnailUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE ( @@ -139,17 +139,17 @@ List findPopularPostsWithCursor( ) ORDER BY p.viewCount DESC, p.id DESC """) - List findPopularPostsWithCursorV2( + List findPopularPostsWithCursorV2( @Param("lastViewCount") Integer lastViewCount, @Param("lastPostId") Long lastPostId, Pageable pageable ); @Query(""" - SELECT new com.techfork.post.application.dto.PostDetailDto( + SELECT new com.techfork.post.infrastructure.row.PostDetailRow( p.id, p.title, p.summary, p.company, p.url, p.logoUrl, p.publishedAt, p.viewCount, null, null) FROM Post p WHERE p.id = :id """) - Optional findByIdWithTechBlog(@Param("id") Long id); + Optional findByIdWithTechBlog(@Param("id") Long id); } diff --git a/src/main/java/com/techfork/post/infrastructure/row/CompanyRow.java b/src/main/java/com/techfork/post/infrastructure/row/CompanyRow.java new file mode 100644 index 00000000..5b72c660 --- /dev/null +++ b/src/main/java/com/techfork/post/infrastructure/row/CompanyRow.java @@ -0,0 +1,11 @@ +package com.techfork.post.infrastructure.row; + +import lombok.Builder; + +@Builder +public record CompanyRow( + String company, + boolean hasNewPost, + String logoUrl +) { +} diff --git a/src/main/java/com/techfork/post/infrastructure/row/PostDetailRow.java b/src/main/java/com/techfork/post/infrastructure/row/PostDetailRow.java new file mode 100644 index 00000000..a6158a52 --- /dev/null +++ b/src/main/java/com/techfork/post/infrastructure/row/PostDetailRow.java @@ -0,0 +1,21 @@ +package com.techfork.post.infrastructure.row; + +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record PostDetailRow( + Long id, + String title, + String summary, + String company, + String url, + String logoUrl, + LocalDateTime publishedAt, + Long viewCount, + List keywords, + Boolean isBookmarked +) { +} diff --git a/src/main/java/com/techfork/post/infrastructure/row/PostInfoRow.java b/src/main/java/com/techfork/post/infrastructure/row/PostInfoRow.java new file mode 100644 index 00000000..3e730a82 --- /dev/null +++ b/src/main/java/com/techfork/post/infrastructure/row/PostInfoRow.java @@ -0,0 +1,22 @@ +package com.techfork.post.infrastructure.row; + +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder(toBuilder = true) +public record PostInfoRow( + Long id, + String title, + String shortSummary, + String company, + String url, + String logoUrl, + String thumbnailUrl, + LocalDateTime publishedAt, + Long viewCount, + List keywords, + Boolean isBookmarked +) { +} diff --git a/src/main/java/com/techfork/post/application/dto/CompanyDto.java b/src/main/java/com/techfork/post/presentation/CompanyDto.java similarity index 85% rename from src/main/java/com/techfork/post/application/dto/CompanyDto.java rename to src/main/java/com/techfork/post/presentation/CompanyDto.java index 891e7c11..6b5f1795 100644 --- a/src/main/java/com/techfork/post/application/dto/CompanyDto.java +++ b/src/main/java/com/techfork/post/presentation/CompanyDto.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.dto; +package com.techfork.post.presentation; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/application/dto/CompanyListResponse.java b/src/main/java/com/techfork/post/presentation/CompanyListResponse.java similarity index 88% rename from src/main/java/com/techfork/post/application/dto/CompanyListResponse.java rename to src/main/java/com/techfork/post/presentation/CompanyListResponse.java index c49b4028..6c8fe221 100644 --- a/src/main/java/com/techfork/post/application/dto/CompanyListResponse.java +++ b/src/main/java/com/techfork/post/presentation/CompanyListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.dto; +package com.techfork.post.presentation; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/presentation/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java index 51a00b0c..21421051 100644 --- a/src/main/java/com/techfork/post/presentation/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -1,8 +1,8 @@ package com.techfork.post.presentation; -import com.techfork.post.application.dto.CompanyListResponse; -import com.techfork.post.application.dto.PostDetailDto; -import com.techfork.post.application.dto.PostListResponse; +import com.techfork.post.presentation.CompanyListResponse; +import com.techfork.post.presentation.PostDetailDto; +import com.techfork.post.presentation.PostListResponse; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; diff --git a/src/main/java/com/techfork/post/presentation/PostControllerV2.java b/src/main/java/com/techfork/post/presentation/PostControllerV2.java index 77f7b55d..58575aa1 100644 --- a/src/main/java/com/techfork/post/presentation/PostControllerV2.java +++ b/src/main/java/com/techfork/post/presentation/PostControllerV2.java @@ -1,7 +1,7 @@ package com.techfork.post.presentation; -import com.techfork.post.application.dto.CompanyListResponse; -import com.techfork.post.application.dto.PostListResponse; +import com.techfork.post.presentation.CompanyListResponse; +import com.techfork.post.presentation.PostListResponse; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; diff --git a/src/main/java/com/techfork/post/presentation/PostConverter.java b/src/main/java/com/techfork/post/presentation/PostConverter.java index 9d72de2e..dec86139 100644 --- a/src/main/java/com/techfork/post/presentation/PostConverter.java +++ b/src/main/java/com/techfork/post/presentation/PostConverter.java @@ -1,6 +1,8 @@ package com.techfork.post.presentation; -import com.techfork.post.application.dto.*; +import com.techfork.post.infrastructure.row.CompanyRow; +import com.techfork.post.infrastructure.row.PostDetailRow; +import com.techfork.post.infrastructure.row.PostInfoRow; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -15,30 +17,50 @@ public CompanyListResponse toCompanyListResponse(List companies) { .build(); } - public CompanyListResponse toCompanyListResponseV2(List companies) { + public CompanyListResponse toCompanyListResponseV2(List companies) { return CompanyListResponse.builder() .totalNumber(companies.size()) - .companies(companies) + .companies(companies.stream() + .map(company -> CompanyDto.builder() + .company(company.company()) + .hasNewPost(company.hasNewPost()) + .logoUrl(company.logoUrl()) + .build()) + .toList()) .build(); } - public PostListResponse toPostListResponse(List posts, int requestedSize) { + public PostListResponse toPostListResponse(List posts, int requestedSize) { boolean hasNext = posts.size() > requestedSize; - List content = hasNext ? posts.subList(0, requestedSize) : posts; + List content = hasNext ? posts.subList(0, requestedSize) : posts; Long lastPostId = null; Long lastViewCount = null; LocalDateTime lastPublishedAt = null; if (!content.isEmpty()) { - PostInfoDto lastPost = content.get(content.size() - 1); + PostInfoRow lastPost = content.get(content.size() - 1); lastPostId = lastPost.id(); lastViewCount = lastPost.viewCount(); lastPublishedAt = lastPost.publishedAt(); } return PostListResponse.builder() - .posts(content) + .posts(content.stream() + .map(post -> PostInfoDto.builder() + .id(post.id()) + .title(post.title()) + .shortSummary(post.shortSummary()) + .company(post.company()) + .url(post.url()) + .logoUrl(post.logoUrl()) + .thumbnailUrl(post.thumbnailUrl()) + .publishedAt(post.publishedAt()) + .viewCount(post.viewCount()) + .keywords(post.keywords()) + .isBookmarked(post.isBookmarked()) + .build()) + .toList()) .lastPostId(lastPostId) .lastViewCount(lastViewCount) .lastPublishedAt(lastPublishedAt) @@ -46,7 +68,7 @@ public PostListResponse toPostListResponse(List posts, int requeste .build(); } - public PostDetailDto toPostDetailDto(PostDetailDto baseDto, List keywords, Boolean isBookmarked) { + public PostDetailDto toPostDetailDto(PostDetailRow baseDto, List keywords, Boolean isBookmarked) { return PostDetailDto.builder() .id(baseDto.id()) .title(baseDto.title()) diff --git a/src/main/java/com/techfork/post/application/dto/PostDetailDto.java b/src/main/java/com/techfork/post/presentation/PostDetailDto.java similarity index 91% rename from src/main/java/com/techfork/post/application/dto/PostDetailDto.java rename to src/main/java/com/techfork/post/presentation/PostDetailDto.java index 8fe2a5c1..aeaaa9e3 100644 --- a/src/main/java/com/techfork/post/application/dto/PostDetailDto.java +++ b/src/main/java/com/techfork/post/presentation/PostDetailDto.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.dto; +package com.techfork.post.presentation; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/application/dto/PostInfoDto.java b/src/main/java/com/techfork/post/presentation/PostInfoDto.java similarity index 92% rename from src/main/java/com/techfork/post/application/dto/PostInfoDto.java rename to src/main/java/com/techfork/post/presentation/PostInfoDto.java index 2da79c0e..427af3a4 100644 --- a/src/main/java/com/techfork/post/application/dto/PostInfoDto.java +++ b/src/main/java/com/techfork/post/presentation/PostInfoDto.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.dto; +package com.techfork.post.presentation; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/application/dto/PostListResponse.java b/src/main/java/com/techfork/post/presentation/PostListResponse.java similarity index 89% rename from src/main/java/com/techfork/post/application/dto/PostListResponse.java rename to src/main/java/com/techfork/post/presentation/PostListResponse.java index f38db0ee..aa91f758 100644 --- a/src/main/java/com/techfork/post/application/dto/PostListResponse.java +++ b/src/main/java/com/techfork/post/presentation/PostListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.application.dto; +package com.techfork.post.presentation; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index 1252125b..49083433 100644 --- a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -1,7 +1,14 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.dto.*; +import com.techfork.post.infrastructure.row.CompanyRow; +import com.techfork.post.infrastructure.row.PostDetailRow; +import com.techfork.post.infrastructure.row.PostInfoRow; +import com.techfork.post.presentation.CompanyDto; +import com.techfork.post.presentation.CompanyListResponse; +import com.techfork.post.presentation.PostDetailDto; +import com.techfork.post.presentation.PostInfoDto; +import com.techfork.post.presentation.PostListResponse; import com.techfork.post.presentation.PostConverter; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; @@ -90,26 +97,28 @@ void getCompanies_Success() { @DisplayName("getCompaniesV2() - 회사 상세 정보 포함 목록 조회 성공") void getCompaniesV2_Success() { // Given - List mockCompanies = List.of( - CompanyDto.builder() + List mockCompanyRows = List.of( + CompanyRow.builder() .company("카카오") .hasNewPost(true) .logoUrl("https://test.com/kakao-logo.png") .build(), - CompanyDto.builder() + CompanyRow.builder() .company("네이버") .hasNewPost(false) .logoUrl("https://test.com/naver-logo.png") .build() ); + List mockCompanies = mockCompanyRows.stream().map(this::toCompanyDto).toList(); + CompanyListResponse expectedResponse = CompanyListResponse.builder() .totalNumber(2) .companies(mockCompanies) .build(); - given(postRepository.findCompaniesWithDetails()).willReturn(mockCompanies); - given(postConverter.toCompanyListResponseV2(mockCompanies)).willReturn(expectedResponse); + given(postRepository.findCompaniesWithDetails()).willReturn(mockCompanyRows); + given(postConverter.toCompanyListResponseV2(mockCompanyRows)).willReturn(expectedResponse); // When CompanyListResponse result = postQueryService.getCompaniesV2(); @@ -126,7 +135,7 @@ void getCompaniesV2_Success() { assertThat(resultCompanies.get(1).hasNewPost()).isFalse(); verify(postRepository, times(1)).findCompaniesWithDetails(); - verify(postConverter, times(1)).toCompanyListResponseV2(mockCompanies); + verify(postConverter, times(1)).toCompanyListResponseV2(mockCompanyRows); } @Test @@ -136,7 +145,7 @@ void getPostDetail_WithoutAuth_Success() { Long postId = 1L; Long userId = null; - PostDetailDto mockPostDetail = PostDetailDto.builder() + PostDetailRow mockPostDetailRow = PostDetailRow.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -164,15 +173,15 @@ void getPostDetail_WithoutAuth_Success() { .company("카카오") .url("https://test.com/1") .logoUrl("https://test.com/logo.png") - .publishedAt(mockPostDetail.publishedAt()) + .publishedAt(mockPostDetailRow.publishedAt()) .viewCount(100L) .keywords(keywordStrings) .isBookmarked(null) .build(); - given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetail)); + given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); - given(postConverter.toPostDetailDto(mockPostDetail, keywordStrings, null)).willReturn(expectedResponse); + given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, null)).willReturn(expectedResponse); // When PostDetailDto result = postQueryService.getPostDetail(postId, userId); @@ -188,7 +197,7 @@ void getPostDetail_WithoutAuth_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetail, keywordStrings, null); + verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, null); verify(bookmarkRepository, never()).findBookmarkedPostIds(any(), any()); } @@ -199,7 +208,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { Long postId = 1L; Long userId = 100L; - PostDetailDto mockPostDetail = PostDetailDto.builder() + PostDetailRow mockPostDetailRow = PostDetailRow.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -224,16 +233,16 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { .company("카카오") .url("https://test.com/1") .logoUrl("https://test.com/logo.png") - .publishedAt(mockPostDetail.publishedAt()) + .publishedAt(mockPostDetailRow.publishedAt()) .viewCount(100L) .keywords(keywordStrings) .isBookmarked(true) .build(); - given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetail)); + given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of(postId)); - given(postConverter.toPostDetailDto(mockPostDetail, keywordStrings, true)).willReturn(expectedResponse); + given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, true)).willReturn(expectedResponse); // When PostDetailDto result = postQueryService.getPostDetail(postId, userId); @@ -246,7 +255,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetail, keywordStrings, true); + verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, true); } @Test @@ -256,7 +265,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { Long postId = 1L; Long userId = 100L; - PostDetailDto mockPostDetail = PostDetailDto.builder() + PostDetailRow mockPostDetailRow = PostDetailRow.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -281,16 +290,16 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { .company("카카오") .url("https://test.com/1") .logoUrl("https://test.com/logo.png") - .publishedAt(mockPostDetail.publishedAt()) + .publishedAt(mockPostDetailRow.publishedAt()) .viewCount(100L) .keywords(keywordStrings) .isBookmarked(false) .build(); - given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetail)); + given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of()); - given(postConverter.toPostDetailDto(mockPostDetail, keywordStrings, false)).willReturn(expectedResponse); + given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, false)).willReturn(expectedResponse); // When PostDetailDto result = postQueryService.getPostDetail(postId, userId); @@ -303,7 +312,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetail, keywordStrings, false); + verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, false); } @Test @@ -330,8 +339,8 @@ void getRecentPosts_Latest_Success() { Long lastPostId = null; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(2L) .title("게시글 2") .company("카카오") @@ -341,7 +350,7 @@ void getRecentPosts_Latest_Success() { .viewCount(50L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(1L) .title("게시글 1") .company("네이버") @@ -353,6 +362,8 @@ void getRecentPosts_Latest_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(1L) @@ -360,7 +371,7 @@ void getRecentPosts_Latest_Success() { .build(); given(postRepository.findRecentPostsWithCursor(eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -384,8 +395,8 @@ void getRecentPosts_Popular_Success() { Long lastPostId = null; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("인기 게시글 1") .company("카카오") @@ -395,7 +406,7 @@ void getRecentPosts_Popular_Success() { .viewCount(1000L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("인기 게시글 2") .company("네이버") @@ -407,6 +418,8 @@ void getRecentPosts_Popular_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(2L) @@ -414,7 +427,7 @@ void getRecentPosts_Popular_Success() { .build(); given(postRepository.findPopularPostsWithCursor(eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -438,8 +451,8 @@ void getPostsByCompany_Success() { Long lastPostId = null; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("카카오 게시글") .company(company) @@ -451,6 +464,8 @@ void getPostsByCompany_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(1L) @@ -458,7 +473,7 @@ void getPostsByCompany_Success() { .build(); given(postRepository.findByCompanyWithCursor(eq(company), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -483,8 +498,8 @@ void getPostsByCompanyV2_Success() { int size = 20; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(2L) .title("네이버 게시글") .company("네이버") @@ -494,7 +509,7 @@ void getPostsByCompanyV2_Success() { .viewCount(100L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(1L) .title("카카오 게시글") .company("카카오") @@ -506,6 +521,8 @@ void getPostsByCompanyV2_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(1L) @@ -514,7 +531,7 @@ void getPostsByCompanyV2_Success() { .build(); given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -543,8 +560,8 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { int size = 20; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(3L) .title("라인 게시글") .company("라인") @@ -554,7 +571,7 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { .viewCount(200L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("네이버 게시글") .company("네이버") @@ -566,6 +583,8 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(2L) @@ -574,7 +593,7 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { .build(); given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -597,8 +616,8 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { Long lastPostId = 100L; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(99L) .title("카카오 게시글 99") .company("카카오") @@ -610,6 +629,8 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(99L) @@ -618,7 +639,7 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { .build(); given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -644,8 +665,8 @@ void getRecentPostsV2_Latest_Success() { int size = 20; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(2L) .title("게시글 2") .company("카카오") @@ -655,7 +676,7 @@ void getRecentPostsV2_Latest_Success() { .viewCount(50L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(1L) .title("게시글 1") .company("네이버") @@ -667,6 +688,8 @@ void getRecentPostsV2_Latest_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(1L) @@ -675,7 +698,7 @@ void getRecentPostsV2_Latest_Success() { .build(); given(postRepository.findRecentPostsWithCursorV2(eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -703,8 +726,8 @@ void getRecentPostsV2_Popular_Success() { int size = 20; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("인기 게시글 1") .company("카카오") @@ -714,7 +737,7 @@ void getRecentPostsV2_Popular_Success() { .viewCount(1000L) .keywords(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("인기 게시글 2") .company("네이버") @@ -726,6 +749,8 @@ void getRecentPostsV2_Popular_Success() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(2L) @@ -734,7 +759,7 @@ void getRecentPostsV2_Popular_Success() { .build(); given(postRepository.findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -760,8 +785,8 @@ void getRecentPostsV2_Popular_WithCursor() { Long lastPostId = 100L; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(99L) .title("인기 게시글 99") .company("카카오") @@ -773,6 +798,8 @@ void getRecentPostsV2_Popular_WithCursor() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(99L) @@ -781,7 +808,7 @@ void getRecentPostsV2_Popular_WithCursor() { .build(); given(postRepository.findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -806,8 +833,8 @@ void getRecentPostsV2_Latest_WithCursor() { Long lastPostId = 100L; int size = 20; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(99L) .title("게시글 99") .company("카카오") @@ -819,6 +846,8 @@ void getRecentPostsV2_Latest_WithCursor() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(99L) @@ -827,7 +856,7 @@ void getRecentPostsV2_Latest_WithCursor() { .build(); given(postRepository.findRecentPostsWithCursorV2(eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -851,8 +880,8 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { int size = 20; Long userId = 1L; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("카카오 게시글 1") .company(company) @@ -863,7 +892,7 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { .keywords(List.of("Java")) .isBookmarked(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("카카오 게시글 2") .company(company) @@ -877,38 +906,19 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( - PostInfoDto.builder() - .id(1L) - .title("카카오 게시글 1") - .company(company) - .url("https://test.com/1") - .logoUrl("https://test.com/logo.png") - .publishedAt(mockPosts.get(0).publishedAt()) - .viewCount(50L) - .keywords(List.of("Java")) - .isBookmarked(true) - .build(), - PostInfoDto.builder() - .id(2L) - .title("카카오 게시글 2") - .company(company) - .url("https://test.com/2") - .logoUrl("https://test.com/logo.png") - .publishedAt(mockPosts.get(1).publishedAt()) - .viewCount(100L) - .keywords(List.of("Spring")) - .isBookmarked(false) - .build() + mockPosts.get(0).toBuilder().isBookmarked(true).build(), + mockPosts.get(1).toBuilder().isBookmarked(false).build() )) .lastPostId(2L) .hasNext(false) .build(); given(postRepository.findByCompanyWithCursor(eq(company), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -935,8 +945,8 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { int size = 20; Long userId = null; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("카카오 게시글 1") .company(company) @@ -949,6 +959,8 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { .build() ); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) .lastPostId(1L) @@ -956,7 +968,7 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { .build(); given(postRepository.findByCompanyWithCursor(eq(company), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -981,8 +993,8 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { int size = 20; Long userId = 1L; - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("게시글 1") .company("카카오") @@ -993,7 +1005,7 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { .keywords(List.of()) .isBookmarked(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("게시글 2") .company("네이버") @@ -1007,6 +1019,7 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(2L); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1018,7 +1031,7 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { .build(); given(postRepository.findRecentPostsWithCursor(eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -1047,8 +1060,8 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { Long userId = 1L; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("카카오 게시글") .company("카카오") @@ -1059,7 +1072,7 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { .keywords(List.of()) .isBookmarked(null) .build(), - PostInfoDto.builder() + PostInfoRow.builder() .id(2L) .title("네이버 게시글") .company("네이버") @@ -1073,6 +1086,7 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L, 2L); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1084,7 +1098,7 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { .build(); given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -1114,8 +1128,8 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { Long userId = 1L; LocalDateTime now = LocalDateTime.now(); - List mockPosts = List.of( - PostInfoDto.builder() + List mockPostRows = List.of( + PostInfoRow.builder() .id(1L) .title("인기 게시글 1") .company("카카오") @@ -1129,6 +1143,7 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1139,7 +1154,7 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { .build(); given(postRepository.findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class))) - .willReturn(mockPosts); + .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); @@ -1155,4 +1170,29 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { verify(postRepository, times(1)).findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(eq(userId), any()); } + + private PostInfoDto toPostInfoDto(PostInfoRow row) { + return PostInfoDto.builder() + .id(row.id()) + .title(row.title()) + .shortSummary(row.shortSummary()) + .company(row.company()) + .url(row.url()) + .logoUrl(row.logoUrl()) + .thumbnailUrl(row.thumbnailUrl()) + .publishedAt(row.publishedAt()) + .viewCount(row.viewCount()) + .keywords(row.keywords()) + .isBookmarked(row.isBookmarked()) + .build(); + } + + private CompanyDto toCompanyDto(CompanyRow row) { + return CompanyDto.builder() + .company(row.company()) + .hasNewPost(row.hasNewPost()) + .logoUrl(row.logoUrl()) + .build(); + } + } diff --git a/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java b/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java index c0288635..e265ab38 100644 --- a/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java +++ b/src/test/java/com/techfork/post/infrastructure/PostRepositoryTest.java @@ -1,8 +1,8 @@ package com.techfork.post.infrastructure; -import com.techfork.post.application.dto.CompanyDto; -import com.techfork.post.application.dto.PostDetailDto; -import com.techfork.post.application.dto.PostInfoDto; +import com.techfork.post.infrastructure.row.CompanyRow; +import com.techfork.post.infrastructure.row.PostDetailRow; +import com.techfork.post.infrastructure.row.PostInfoRow; import com.techfork.post.domain.Post; import com.techfork.domain.source.entity.TechBlog; import com.techfork.domain.source.repository.TechBlogRepository; @@ -117,7 +117,7 @@ void findPopularPostsWithCursor_FirstPage_OrderByViewCountDesc() { Post post3 = createPost("게시글3", techBlog1, LocalDateTime.now().minusDays(1), 300L); postRepository.saveAll(List.of(post1, post2, post3)); - List result = postRepository.findPopularPostsWithCursor(null, PageRequest.of(0, 10)); + List result = postRepository.findPopularPostsWithCursor(null, PageRequest.of(0, 10)); assertThat(result).hasSize(3); assertThat(result.get(0).viewCount()).isEqualTo(500L); @@ -135,7 +135,7 @@ void findPopularPostsWithCursor_NextPage_FilterByLastPostId() { Post post5 = createPost("게시글5", techBlog1, LocalDateTime.now().minusDays(1), 100L); postRepository.saveAll(List.of(post1, post2, post3, post4, post5)); - List result = postRepository.findPopularPostsWithCursor(post3.getId(), PageRequest.of(0, 10)); + List result = postRepository.findPopularPostsWithCursor(post3.getId(), PageRequest.of(0, 10)); assertThat(result).hasSize(2); assertThat(result).allMatch(dto -> dto.id() < post3.getId()); @@ -154,7 +154,7 @@ void findPopularPostsWithCursorV2_CursorPagingWithViewCountAndId() { Post post3 = createPost("게시글3", techBlog1, LocalDateTime.now().minusDays(3), 100L); postRepository.saveAll(List.of(post1, post2, post3)); - List result = postRepository.findPopularPostsWithCursorV2(null, null, PageRequest.of(0, 10)); + List result = postRepository.findPopularPostsWithCursorV2(null, null, PageRequest.of(0, 10)); assertThat(result).hasSize(3); assertThat(result.get(0).viewCount()).isEqualTo(500L); @@ -170,7 +170,7 @@ void findPopularPostsWithCursorV2_SameViewCount_OrderById() { Post post3 = createPost("게시글3", techBlog1, LocalDateTime.now().minusDays(3), 500L); postRepository.saveAll(List.of(post1, post2, post3)); - List result = postRepository.findPopularPostsWithCursorV2(null, null, PageRequest.of(0, 10)); + List result = postRepository.findPopularPostsWithCursorV2(null, null, PageRequest.of(0, 10)); assertThat(result).hasSize(3); assertThat(result.get(0).id()).isGreaterThan(result.get(1).id()); @@ -188,9 +188,9 @@ void findPopularPostsWithCursorV2_NextPageWithCursor() { postRepository.saveAll(List.of(post1, post2, post3, post4, post5)); PageRequest pageRequest = PageRequest.of(0, 2); - List page1 = postRepository.findPopularPostsWithCursorV2(null, null, pageRequest); - PostInfoDto lastPost = page1.get(1); - List page2 = postRepository.findPopularPostsWithCursorV2( + List page1 = postRepository.findPopularPostsWithCursorV2(null, null, pageRequest); + PostInfoRow lastPost = page1.get(1); + List page2 = postRepository.findPopularPostsWithCursorV2( lastPost.viewCount().intValue(), lastPost.id(), pageRequest @@ -215,7 +215,7 @@ void findRecentPostsWithCursor_OrderByPublishedAtDesc() { Post post3 = createPost("게시글3", techBlog1, now.minusDays(2), 300L); postRepository.saveAll(List.of(post1, post2, post3)); - List result = postRepository.findRecentPostsWithCursor(null, PageRequest.of(0, 10)); + List result = postRepository.findRecentPostsWithCursor(null, PageRequest.of(0, 10)); assertThat(result).hasSize(3); assertThat(result.get(0).publishedAt()).isAfter(result.get(1).publishedAt()); @@ -230,7 +230,7 @@ void cursorPaging_SizePlusOne_CanDetermineHasNext() { postRepository.save(post); } - List result = postRepository.findRecentPostsWithCursor(null, PageRequest.of(0, 4)); + List result = postRepository.findRecentPostsWithCursor(null, PageRequest.of(0, 4)); assertThat(result).hasSize(4); boolean hasNext = result.size() > 3; @@ -251,7 +251,7 @@ void findRecentPostsWithCursorV2_CursorPagingWithPublishedAtAndId() { Post post3 = createPost("게시글3", techBlog1, now, 300L); postRepository.saveAll(List.of(post1, post2, post3)); - List page1 = postRepository.findRecentPostsWithCursorV2(null, null, PageRequest.of(0, 10)); + List page1 = postRepository.findRecentPostsWithCursorV2(null, null, PageRequest.of(0, 10)); assertThat(page1).hasSize(3); assertThat(page1.get(0).id()).isGreaterThan(page1.get(1).id()); @@ -270,9 +270,9 @@ void findRecentPostsWithCursorV2_NextPageWithCursor() { postRepository.saveAll(List.of(post1, post2, post3, post4, post5)); PageRequest pageRequest = PageRequest.of(0, 2); - List page1 = postRepository.findRecentPostsWithCursorV2(null, null, pageRequest); - PostInfoDto lastPost = page1.get(1); - List page2 = postRepository.findRecentPostsWithCursorV2( + List page1 = postRepository.findRecentPostsWithCursorV2(null, null, pageRequest); + PostInfoRow lastPost = page1.get(1); + List page2 = postRepository.findRecentPostsWithCursorV2( lastPost.publishedAt(), lastPost.id(), pageRequest @@ -295,10 +295,10 @@ void findByCompanyWithCursor_NullCompany_ReturnsAll() { Post naverPost = createPost("네이버 게시글", techBlog2, LocalDateTime.now(), 200L); postRepository.saveAll(List.of(kakaoPost, naverPost)); - List result = postRepository.findByCompanyWithCursor(null, null, PageRequest.of(0, 10)); + List result = postRepository.findByCompanyWithCursor(null, null, PageRequest.of(0, 10)); assertThat(result).hasSize(2); - assertThat(result).extracting(PostInfoDto::company) + assertThat(result).extracting(PostInfoRow::company) .containsExactlyInAnyOrder("카카오", "네이버"); } @@ -310,7 +310,7 @@ void findByCompanyWithCursor_SpecificCompany_ReturnsFiltered() { Post naverPost = createPost("네이버 게시글", techBlog2, LocalDateTime.now(), 300L); postRepository.saveAll(List.of(kakaoPost1, kakaoPost2, naverPost)); - List result = postRepository.findByCompanyWithCursor("카카오", null, PageRequest.of(0, 10)); + List result = postRepository.findByCompanyWithCursor("카카오", null, PageRequest.of(0, 10)); assertThat(result).hasSize(2); assertThat(result).allMatch(dto -> dto.company().equals("카카오")); @@ -329,10 +329,10 @@ void findByCompanyNames_NullCompanies_ReturnsAll() { Post awsPost = createPost("AWS 게시글", techBlog3, LocalDateTime.now(), 500L); postRepository.saveAll(List.of(kakaoPost, naverPost, awsPost)); - List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); + List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); assertThat(result).hasSize(3); - assertThat(result).extracting(PostInfoDto::company) + assertThat(result).extracting(PostInfoRow::company) .containsExactlyInAnyOrder("카카오", "네이버", "AWS"); } @@ -345,7 +345,7 @@ void findByCompanyNamesWithCursor_SpecificCompanies_ReturnsFiltered() { Post awsPost = createPost("AWS 게시글", techBlog3, LocalDateTime.now(), 500L); postRepository.saveAll(List.of(kakaoPost1, kakaoPost2, naverPost, awsPost)); - List result = postRepository.findByCompanyNamesWithCursor( + List result = postRepository.findByCompanyNamesWithCursor( List.of("카카오", "네이버"), null, null, @@ -353,7 +353,7 @@ void findByCompanyNamesWithCursor_SpecificCompanies_ReturnsFiltered() { ); assertThat(result).hasSize(3); - assertThat(result).extracting(PostInfoDto::company) + assertThat(result).extracting(PostInfoRow::company) .containsOnly("카카오", "네이버") .doesNotContain("AWS"); } @@ -365,10 +365,10 @@ void findByCompanyNames_SortPublishedAtCheck() { Post oldPost = createPost("옛날 글", techBlog1, LocalDateTime.now().minusDays(5), 200L); postRepository.saveAll(List.of(recentPost, oldPost)); - List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); + List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); assertThat(result) - .extracting(PostInfoDto::title) + .extracting(PostInfoRow::title) .containsExactly("최신 글", "옛날 글"); } @@ -381,7 +381,7 @@ void findByCompanyNames_SortPublihedAtEqualsCheck() { Post awsPost = createPost("AWS 게시글", techBlog3, now, 500L); postRepository.saveAll(List.of(kakaoPost, naverPost, awsPost)); - List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); + List result = postRepository.findByCompanyNamesWithCursor(null, null, null, PageRequest.of(0, 10)); assertThat(result) .extracting("id") @@ -399,9 +399,9 @@ void findByCompanyNames_CursorPaging() { postRepository.saveAll(posts); PageRequest pageRequest = PageRequest.of(0, 2); - List page1 = postRepository.findByCompanyNamesWithCursor(null, null, null, pageRequest); - PostInfoDto lastPostOfPage1 = page1.get(1); - List page2 = postRepository.findByCompanyNamesWithCursor( + List page1 = postRepository.findByCompanyNamesWithCursor(null, null, null, pageRequest); + PostInfoRow lastPostOfPage1 = page1.get(1); + List page2 = postRepository.findByCompanyNamesWithCursor( null, lastPostOfPage1.publishedAt(), lastPostOfPage1.id(), @@ -421,13 +421,13 @@ class FindByIdWithTechBlog { @Test @DisplayName("JOIN하여 게시글 상세 정보 조회에 성공한다") - void findByIdWithTechBlog_Success_ReturnsPostDetailDto() { + void findByIdWithTechBlog_Success_ReturnsPostDetailRow() { Post post = postRepository.save(createPost("테스트 게시글", techBlog1, LocalDateTime.now(), 100L)); - Optional result = postRepository.findByIdWithTechBlog(post.getId()); + Optional result = postRepository.findByIdWithTechBlog(post.getId()); assertThat(result).isPresent(); - PostDetailDto dto = result.get(); + PostDetailRow dto = result.get(); assertThat(dto.id()).isEqualTo(post.getId()); assertThat(dto.title()).isEqualTo("테스트 게시글"); assertThat(dto.company()).isEqualTo("카카오"); @@ -438,7 +438,7 @@ void findByIdWithTechBlog_Success_ReturnsPostDetailDto() { @Test @DisplayName("존재하지 않는 ID 조회 시 Empty를 반환한다") void findByIdWithTechBlog_NotFound_ReturnsEmpty() { - Optional result = postRepository.findByIdWithTechBlog(99999L); + Optional result = postRepository.findByIdWithTechBlog(99999L); assertThat(result).isEmpty(); } @@ -475,16 +475,16 @@ void findCompaniesWithDetails_Success() { Post naverPost = createPost("네이버 게시글", techBlog2, LocalDate.now().minusDays(2).atStartOfDay(), 300L); postRepository.saveAll(List.of(kakaoPost1, kakaoPost2, naverPost)); - List result = postRepository.findCompaniesWithDetails(); + List result = postRepository.findCompaniesWithDetails(); assertThat(result).hasSize(2); - CompanyDto firstCompany = result.get(0); + CompanyRow firstCompany = result.get(0); assertThat(firstCompany.company()).isEqualTo("카카오"); assertThat(firstCompany.hasNewPost()).isTrue(); assertThat(firstCompany.logoUrl()).isNotNull(); - CompanyDto secondCompany = result.get(1); + CompanyRow secondCompany = result.get(1); assertThat(secondCompany.company()).isEqualTo("네이버"); assertThat(secondCompany.hasNewPost()).isFalse(); } @@ -496,15 +496,15 @@ void findCompaniesWithDetails_HasNewPost_AccurateDetection() { Post yesterdayPost = createPost("어제 게시글", techBlog2, LocalDate.now().minusDays(1).atStartOfDay(), 200L); postRepository.saveAll(List.of(todayPost, yesterdayPost)); - List result = postRepository.findCompaniesWithDetails(); + List result = postRepository.findCompaniesWithDetails(); - CompanyDto kakaoCompany = result.stream() + CompanyRow kakaoCompany = result.stream() .filter(c -> c.company().equals("카카오")) .findFirst() .orElseThrow(); assertThat(kakaoCompany.hasNewPost()).isTrue(); - CompanyDto naverCompany = result.stream() + CompanyRow naverCompany = result.stream() .filter(c -> c.company().equals("네이버")) .findFirst() .orElseThrow(); @@ -518,7 +518,7 @@ void findCompaniesWithDetails_OrderByLatestPublishedAtDesc() { Post recentPost = createPost("최근 게시글", techBlog2, LocalDate.now().minusDays(1).atStartOfDay(), 200L); postRepository.saveAll(List.of(oldPost, recentPost)); - List result = postRepository.findCompaniesWithDetails(); + List result = postRepository.findCompaniesWithDetails(); assertThat(result).hasSize(2); assertThat(result.get(0).company()).isEqualTo("네이버"); @@ -528,7 +528,7 @@ void findCompaniesWithDetails_OrderByLatestPublishedAtDesc() { @Test @DisplayName("게시글이 없으면 빈 리스트를 반환한다") void findCompaniesWithDetails_NoPosts_ReturnsEmptyList() { - List result = postRepository.findCompaniesWithDetails(); + List result = postRepository.findCompaniesWithDetails(); assertThat(result).isEmpty(); } From 59a48dec75215b4896379c118636e8a4b40766cc Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 15:57:01 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20Summary=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20sum?= =?UTF-8?q?mary=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=95=84=EB=9E=98?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/application/batch/PostSummaryProcessor.java | 4 ++-- .../SummaryExtractionResult.java} | 4 ++-- .../application/summary/SummaryExtractionService.java | 8 ++++---- .../application/batch/PostSummaryProcessorTest.java | 6 +++--- .../summary/SummaryExtractionServiceTest.java | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) rename src/main/java/com/techfork/post/application/{dto/SummaryWithKeywordsDto.java => summary/SummaryExtractionResult.java} (57%) diff --git a/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java index e9ef6eb1..b61cfaff 100644 --- a/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java +++ b/src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java @@ -1,6 +1,6 @@ package com.techfork.post.application.batch; -import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.summary.SummaryExtractionResult; import com.techfork.post.domain.Post; import com.techfork.post.application.summary.SummaryExtractionService; import lombok.RequiredArgsConstructor; @@ -24,7 +24,7 @@ public class PostSummaryProcessor implements ItemProcessor { public Post process(Post post) { log.debug("요약 및 키워드 추출 중: {}", post.getTitle()); - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary( + SummaryExtractionResult result = summaryExtractionService.extractSummary( post.getTitle(), post.getPlainContent() ); diff --git a/src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java b/src/main/java/com/techfork/post/application/summary/SummaryExtractionResult.java similarity index 57% rename from src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java rename to src/main/java/com/techfork/post/application/summary/SummaryExtractionResult.java index fbe12c73..30db53e6 100644 --- a/src/main/java/com/techfork/post/application/dto/SummaryWithKeywordsDto.java +++ b/src/main/java/com/techfork/post/application/summary/SummaryExtractionResult.java @@ -1,8 +1,8 @@ -package com.techfork.post.application.dto; +package com.techfork.post.application.summary; import java.util.List; -public record SummaryWithKeywordsDto( +public record SummaryExtractionResult( String summary, String shortSummary, List keywords diff --git a/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java b/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java index 2bb68783..05b1abb6 100644 --- a/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java +++ b/src/main/java/com/techfork/post/application/summary/SummaryExtractionService.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.summary.SummaryExtractionResult; import com.techfork.global.llm.LlmClient; import com.techfork.global.llm.exception.LlmException; import com.techfork.global.util.ContentCleaner; @@ -37,7 +37,7 @@ public class SummaryExtractionService { } """; - public SummaryWithKeywordsDto extractSummary(String title, String content) { + public SummaryExtractionResult extractSummary(String title, String content) { String processedContent = content; if (content != null && content.length() > 50000) { processedContent = ContentCleaner.cleanAndLimit(content, 50000); @@ -54,7 +54,7 @@ public SummaryWithKeywordsDto extractSummary(String title, String content) { return parseResponse(response.trim()); } - private SummaryWithKeywordsDto parseResponse(String response) { + private SummaryExtractionResult parseResponse(String response) { try { JsonNode jsonNode = objectMapper.readTree(response); String summary = jsonNode.get("summary").asText(); @@ -66,7 +66,7 @@ private SummaryWithKeywordsDto parseResponse(String response) { keywordsNode.forEach(node -> keywords.add(node.asText())); } - return new SummaryWithKeywordsDto(summary, shortSummary, keywords); + return new SummaryExtractionResult(summary, shortSummary, keywords); } catch (Exception e) { log.error("JSON 응답 파싱 실패: {}", response, e); throw new LlmException("LLM summary response parsing failed", e); diff --git a/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java index 98e8f535..f64b070f 100644 --- a/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java +++ b/src/test/java/com/techfork/post/application/batch/PostSummaryProcessorTest.java @@ -1,6 +1,6 @@ package com.techfork.post.application.batch; -import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.summary.SummaryExtractionResult; import com.techfork.post.domain.Post; import com.techfork.post.domain.PostKeyword; import com.techfork.post.fixture.PostFixture; @@ -36,7 +36,7 @@ void updatesSummariesAndRebuildsKeywordsFromExtractionResult() { Post post = createPostWithExistingKeywords(); PostKeyword oldKeyword1 = post.getKeywords().get(0); PostKeyword oldKeyword2 = post.getKeywords().get(1); - SummaryWithKeywordsDto summaryWithKeywordsDto = new SummaryWithKeywordsDto( + SummaryExtractionResult summaryWithKeywordsDto = new SummaryExtractionResult( "새 요약", "새 짧은 요약", List.of("AI", "Batch") @@ -64,7 +64,7 @@ void clearsExistingKeywordsWhenExtractionReturnsNoKeywords() { PostSummaryProcessor postSummaryProcessor = new PostSummaryProcessor(summaryExtractionService); Post post = createPostWithExistingKeywords(); given(summaryExtractionService.extractSummary("요약 대상 글", "평문 본문")) - .willReturn(new SummaryWithKeywordsDto("새 요약", "새 짧은 요약", List.of())); + .willReturn(new SummaryExtractionResult("새 요약", "새 짧은 요약", List.of())); Post result = postSummaryProcessor.process(post); diff --git a/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java b/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java index d23484bc..27504b4c 100644 --- a/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java +++ b/src/test/java/com/techfork/post/application/summary/SummaryExtractionServiceTest.java @@ -1,7 +1,7 @@ package com.techfork.post.application.summary; import com.fasterxml.jackson.databind.ObjectMapper; -import com.techfork.post.application.dto.SummaryWithKeywordsDto; +import com.techfork.post.application.summary.SummaryExtractionResult; import com.techfork.global.llm.LlmClient; import com.techfork.global.llm.exception.LlmException; import com.techfork.global.util.ContentCleaner; @@ -51,7 +51,7 @@ void parsesSummaryShortSummaryAndKeywordsFromJsonResponse() { } """); - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary("제목", "본문"); + SummaryExtractionResult result = summaryExtractionService.extractSummary("제목", "본문"); assertThat(result.summary()).isEqualTo("상세 요약"); assertThat(result.shortSummary()).isEqualTo("짧은 요약"); @@ -69,7 +69,7 @@ void returnsEmptyStringWhenShortSummaryIsMissing() { } """); - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary("제목", "본문"); + SummaryExtractionResult result = summaryExtractionService.extractSummary("제목", "본문"); assertThat(result.summary()).isEqualTo("상세 요약"); assertThat(result.shortSummary()).isEmpty(); @@ -87,7 +87,7 @@ void returnsEmptyListWhenKeywordsAreMissing() { } """); - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary("제목", "본문"); + SummaryExtractionResult result = summaryExtractionService.extractSummary("제목", "본문"); assertThat(result.summary()).isEqualTo("상세 요약"); assertThat(result.shortSummary()).isEqualTo("짧은 요약"); @@ -106,7 +106,7 @@ void returnsEmptyListWhenKeywordsIsNotArray() { } """); - SummaryWithKeywordsDto result = summaryExtractionService.extractSummary("제목", "본문"); + SummaryExtractionResult result = summaryExtractionService.extractSummary("제목", "본문"); assertThat(result.summary()).isEqualTo("상세 요약"); assertThat(result.shortSummary()).isEqualTo("짧은 요약"); From a17f6a268fcca2d41dbfd4f3dc166dbf024781c7 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 16:05:23 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20DTO=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/query/PostQueryService.java | 7 +- .../presentation/CompanyListResponse.java | 2 +- .../{CompanyDto.java => CompanyResponse.java} | 4 +- .../post/presentation/PostController.java | 6 +- .../post/presentation/PostConverter.java | 8 +-- ...DetailDto.java => PostDetailResponse.java} | 2 +- ...PostInfoDto.java => PostInfoResponse.java} | 4 +- .../post/presentation/PostListResponse.java | 2 +- .../query/PostQueryServiceTest.java | 72 +++++++++---------- .../PostControllerIntegrationTest.java | 4 +- .../PostControllerV2IntegrationTest.java | 8 +-- 11 files changed, 59 insertions(+), 60 deletions(-) rename src/main/java/com/techfork/post/presentation/{CompanyDto.java => CompanyResponse.java} (68%) rename src/main/java/com/techfork/post/presentation/{PostDetailDto.java => PostDetailResponse.java} (93%) rename src/main/java/com/techfork/post/presentation/{PostInfoDto.java => PostInfoResponse.java} (87%) diff --git a/src/main/java/com/techfork/post/application/query/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java index bd0405e3..89e733dd 100644 --- a/src/main/java/com/techfork/post/application/query/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -1,7 +1,6 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; -import com.techfork.post.application.dto.*; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; @@ -10,7 +9,7 @@ import com.techfork.post.infrastructure.row.PostDetailRow; import com.techfork.post.infrastructure.row.PostInfoRow; import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailDto; +import com.techfork.post.presentation.PostDetailResponse; import com.techfork.post.presentation.PostListResponse; import com.techfork.post.presentation.PostConverter; import com.techfork.global.exception.CommonErrorCode; @@ -112,7 +111,7 @@ public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewC return postConverter.toPostListResponse(postsWithKeywords, size); } - public PostDetailDto getPostDetail(Long postId, Long userId) { + public PostDetailResponse getPostDetail(Long postId, Long userId) { PostDetailRow postDetail = postRepository.findByIdWithTechBlog(postId) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND)); @@ -126,7 +125,7 @@ public PostDetailDto getPostDetail(Long postId, Long userId) { isBookmarked = !bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId)).isEmpty(); } - return postConverter.toPostDetailDto(postDetail, keywords, isBookmarked); + return postConverter.toPostDetailResponse(postDetail, keywords, isBookmarked); } private List attachKeywordsToPostInfoList(List posts) { diff --git a/src/main/java/com/techfork/post/presentation/CompanyListResponse.java b/src/main/java/com/techfork/post/presentation/CompanyListResponse.java index 6c8fe221..e104abce 100644 --- a/src/main/java/com/techfork/post/presentation/CompanyListResponse.java +++ b/src/main/java/com/techfork/post/presentation/CompanyListResponse.java @@ -9,7 +9,7 @@ @Schema(name = "CompanyListResponse") public record CompanyListResponse( Integer totalNumber, - @Schema(description = "회사 목록 (V1: String, V2: CompanyDto)") + @Schema(description = "회사 목록 (V1: String, V2: CompanyResponse)") List companies ) { } diff --git a/src/main/java/com/techfork/post/presentation/CompanyDto.java b/src/main/java/com/techfork/post/presentation/CompanyResponse.java similarity index 68% rename from src/main/java/com/techfork/post/presentation/CompanyDto.java rename to src/main/java/com/techfork/post/presentation/CompanyResponse.java index 6b5f1795..ba5fb52b 100644 --- a/src/main/java/com/techfork/post/presentation/CompanyDto.java +++ b/src/main/java/com/techfork/post/presentation/CompanyResponse.java @@ -4,8 +4,8 @@ import lombok.Builder; @Builder -@Schema(name = "CompanyDto", description = "회사 정보") -public record CompanyDto( +@Schema(name = "CompanyResponse", description = "회사 정보") +public record CompanyResponse( String company, boolean hasNewPost, String logoUrl diff --git a/src/main/java/com/techfork/post/presentation/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java index 21421051..edb61219 100644 --- a/src/main/java/com/techfork/post/presentation/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -1,7 +1,7 @@ package com.techfork.post.presentation; import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailDto; +import com.techfork.post.presentation.PostDetailResponse; import com.techfork.post.presentation.PostListResponse; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; @@ -81,14 +81,14 @@ public ResponseEntity> getRecentPosts( description = "특정 게시글의 상세 정보를 조회합니다. 로그인 시 북마크 여부가 포함됩니다." ) @GetMapping("/{postId}") - public ResponseEntity> getPostDetail( + public ResponseEntity> getPostDetail( @Parameter(description = "게시글 ID") @PathVariable Long postId, @Parameter(hidden = true) @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostDetailDto response = postQueryService.getPostDetail(postId, userId); + PostDetailResponse response = postQueryService.getPostDetail(postId, userId); return BaseResponse.of(SuccessCode.OK, response); } } diff --git a/src/main/java/com/techfork/post/presentation/PostConverter.java b/src/main/java/com/techfork/post/presentation/PostConverter.java index dec86139..1912867f 100644 --- a/src/main/java/com/techfork/post/presentation/PostConverter.java +++ b/src/main/java/com/techfork/post/presentation/PostConverter.java @@ -21,7 +21,7 @@ public CompanyListResponse toCompanyListResponseV2(List companies) { return CompanyListResponse.builder() .totalNumber(companies.size()) .companies(companies.stream() - .map(company -> CompanyDto.builder() + .map(company -> CompanyResponse.builder() .company(company.company()) .hasNewPost(company.hasNewPost()) .logoUrl(company.logoUrl()) @@ -47,7 +47,7 @@ public PostListResponse toPostListResponse(List posts, int requeste return PostListResponse.builder() .posts(content.stream() - .map(post -> PostInfoDto.builder() + .map(post -> PostInfoResponse.builder() .id(post.id()) .title(post.title()) .shortSummary(post.shortSummary()) @@ -68,8 +68,8 @@ public PostListResponse toPostListResponse(List posts, int requeste .build(); } - public PostDetailDto toPostDetailDto(PostDetailRow baseDto, List keywords, Boolean isBookmarked) { - return PostDetailDto.builder() + public PostDetailResponse toPostDetailResponse(PostDetailRow baseDto, List keywords, Boolean isBookmarked) { + return PostDetailResponse.builder() .id(baseDto.id()) .title(baseDto.title()) .summary(baseDto.summary()) diff --git a/src/main/java/com/techfork/post/presentation/PostDetailDto.java b/src/main/java/com/techfork/post/presentation/PostDetailResponse.java similarity index 93% rename from src/main/java/com/techfork/post/presentation/PostDetailDto.java rename to src/main/java/com/techfork/post/presentation/PostDetailResponse.java index aeaaa9e3..f7034a28 100644 --- a/src/main/java/com/techfork/post/presentation/PostDetailDto.java +++ b/src/main/java/com/techfork/post/presentation/PostDetailResponse.java @@ -8,7 +8,7 @@ @Builder @Schema(name = "PostDetailResponse") -public record PostDetailDto( +public record PostDetailResponse( Long id, String title, String summary, diff --git a/src/main/java/com/techfork/post/presentation/PostInfoDto.java b/src/main/java/com/techfork/post/presentation/PostInfoResponse.java similarity index 87% rename from src/main/java/com/techfork/post/presentation/PostInfoDto.java rename to src/main/java/com/techfork/post/presentation/PostInfoResponse.java index 427af3a4..5c86e9e4 100644 --- a/src/main/java/com/techfork/post/presentation/PostInfoDto.java +++ b/src/main/java/com/techfork/post/presentation/PostInfoResponse.java @@ -7,8 +7,8 @@ import java.util.List; @Builder(toBuilder = true) -@Schema(name = "PostInfoDto") -public record PostInfoDto( +@Schema(name = "PostInfoResponse") +public record PostInfoResponse( Long id, String title, String shortSummary, diff --git a/src/main/java/com/techfork/post/presentation/PostListResponse.java b/src/main/java/com/techfork/post/presentation/PostListResponse.java index aa91f758..9aef3d99 100644 --- a/src/main/java/com/techfork/post/presentation/PostListResponse.java +++ b/src/main/java/com/techfork/post/presentation/PostListResponse.java @@ -9,7 +9,7 @@ @Builder @Schema(name = "PostListResponse") public record PostListResponse( - List posts, + List posts, Long lastPostId, Long lastViewCount, LocalDateTime lastPublishedAt, diff --git a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index 49083433..8590e4dc 100644 --- a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -4,10 +4,10 @@ import com.techfork.post.infrastructure.row.CompanyRow; import com.techfork.post.infrastructure.row.PostDetailRow; import com.techfork.post.infrastructure.row.PostInfoRow; -import com.techfork.post.presentation.CompanyDto; +import com.techfork.post.presentation.CompanyResponse; import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailDto; -import com.techfork.post.presentation.PostInfoDto; +import com.techfork.post.presentation.PostDetailResponse; +import com.techfork.post.presentation.PostInfoResponse; import com.techfork.post.presentation.PostListResponse; import com.techfork.post.presentation.PostConverter; import com.techfork.post.domain.PostKeyword; @@ -110,7 +110,7 @@ void getCompaniesV2_Success() { .build() ); - List mockCompanies = mockCompanyRows.stream().map(this::toCompanyDto).toList(); + List mockCompanies = mockCompanyRows.stream().map(this::toCompanyResponse).toList(); CompanyListResponse expectedResponse = CompanyListResponse.builder() .totalNumber(2) @@ -127,7 +127,7 @@ void getCompaniesV2_Success() { assertThat(result).isNotNull(); assertThat(result.companies()).hasSize(2); - List resultCompanies = (List) result.companies(); + List resultCompanies = (List) result.companies(); assertThat(resultCompanies.get(0).company()).isEqualTo("카카오"); assertThat(resultCompanies.get(0).hasNewPost()).isTrue(); assertThat(resultCompanies.get(0).logoUrl()).isEqualTo("https://test.com/kakao-logo.png"); @@ -166,7 +166,7 @@ void getPostDetail_WithoutAuth_Success() { List mockKeywords = List.of(keyword1, keyword2); List keywordStrings = List.of("Java", "Spring"); - PostDetailDto expectedResponse = PostDetailDto.builder() + PostDetailResponse expectedResponse = PostDetailResponse.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -181,10 +181,10 @@ void getPostDetail_WithoutAuth_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); - given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, null)).willReturn(expectedResponse); + given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, null)).willReturn(expectedResponse); // When - PostDetailDto result = postQueryService.getPostDetail(postId, userId); + PostDetailResponse result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -197,7 +197,7 @@ void getPostDetail_WithoutAuth_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, null); + verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, null); verify(bookmarkRepository, never()).findBookmarkedPostIds(any(), any()); } @@ -226,7 +226,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { List mockKeywords = List.of(keyword1); List keywordStrings = List.of("Java"); - PostDetailDto expectedResponse = PostDetailDto.builder() + PostDetailResponse expectedResponse = PostDetailResponse.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -242,10 +242,10 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of(postId)); - given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, true)).willReturn(expectedResponse); + given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, true)).willReturn(expectedResponse); // When - PostDetailDto result = postQueryService.getPostDetail(postId, userId); + PostDetailResponse result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -255,7 +255,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, true); + verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, true); } @Test @@ -283,7 +283,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { List mockKeywords = List.of(keyword1); List keywordStrings = List.of("Java"); - PostDetailDto expectedResponse = PostDetailDto.builder() + PostDetailResponse expectedResponse = PostDetailResponse.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -299,10 +299,10 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of()); - given(postConverter.toPostDetailDto(mockPostDetailRow, keywordStrings, false)).willReturn(expectedResponse); + given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, false)).willReturn(expectedResponse); // When - PostDetailDto result = postQueryService.getPostDetail(postId, userId); + PostDetailResponse result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -312,7 +312,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailDto(mockPostDetailRow, keywordStrings, false); + verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, false); } @Test @@ -362,7 +362,7 @@ void getRecentPosts_Latest_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -418,7 +418,7 @@ void getRecentPosts_Popular_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -464,7 +464,7 @@ void getPostsByCompany_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -521,7 +521,7 @@ void getPostsByCompanyV2_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -583,7 +583,7 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -629,7 +629,7 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -688,7 +688,7 @@ void getRecentPostsV2_Latest_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -749,7 +749,7 @@ void getRecentPostsV2_Popular_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -798,7 +798,7 @@ void getRecentPostsV2_Popular_WithCursor() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -846,7 +846,7 @@ void getRecentPostsV2_Latest_WithCursor() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -906,7 +906,7 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -959,7 +959,7 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(mockPosts) @@ -1019,7 +1019,7 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(2L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1086,7 +1086,7 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L, 2L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1143,7 +1143,7 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(); - List mockPosts = mockPostRows.stream().map(this::toPostInfoDto).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); PostListResponse expectedResponse = PostListResponse.builder() .posts(List.of( @@ -1171,8 +1171,8 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { verify(bookmarkRepository, times(1)).findBookmarkedPostIds(eq(userId), any()); } - private PostInfoDto toPostInfoDto(PostInfoRow row) { - return PostInfoDto.builder() + private PostInfoResponse toPostInfoResponse(PostInfoRow row) { + return PostInfoResponse.builder() .id(row.id()) .title(row.title()) .shortSummary(row.shortSummary()) @@ -1187,8 +1187,8 @@ private PostInfoDto toPostInfoDto(PostInfoRow row) { .build(); } - private CompanyDto toCompanyDto(CompanyRow row) { - return CompanyDto.builder() + private CompanyResponse toCompanyResponse(CompanyRow row) { + return CompanyResponse.builder() .company(row.company()) .hasNewPost(row.hasNewPost()) .logoUrl(row.logoUrl()) diff --git a/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java b/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java index 69baae1a..dc3b9eca 100644 --- a/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java +++ b/src/test/java/com/techfork/post/presentation/PostControllerIntegrationTest.java @@ -144,7 +144,7 @@ void getPostDetail_WithoutAuth_Success() throws Exception { mockMvc.perform(get("/api/v1/posts/{postId}", testPost1.getId())) .andDo(print()) .andExpect(status().isOk()) - // PostDetailDto의 모든 필드 검증 + // PostDetailResponse의 모든 필드 검증 .andExpect(jsonPath("$.data.id").value(testPost1.getId())) .andExpect(jsonPath("$.data.title").value("테스트 게시글 1")) .andExpect(jsonPath("$.data.summary").exists()) @@ -222,7 +222,7 @@ void getRecentPosts_WithoutAuth() throws Exception { .andExpect(jsonPath("$.data.lastViewCount").exists()) .andExpect(jsonPath("$.data.lastPublishedAt").exists()) .andExpect(jsonPath("$.data.hasNext").value(false)) - // PostInfoDto의 모든 필드 검증 (첫 번째 항목만) + // PostInfoResponse의 모든 필드 검증 (첫 번째 항목만) .andExpect(jsonPath("$.data.posts[0].id").isNumber()) .andExpect(jsonPath("$.data.posts[0].title").isString()) .andExpect(jsonPath("$.data.posts[0].shortSummary").isString()) diff --git a/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java b/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java index abfe5b99..56239037 100644 --- a/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java +++ b/src/test/java/com/techfork/post/presentation/PostControllerV2IntegrationTest.java @@ -145,7 +145,7 @@ void getCompanies_Success() throws Exception { .andExpect(jsonPath("$.data.totalNumber").value(2)) .andExpect(jsonPath("$.data.companies").isArray()) .andExpect(jsonPath("$.data.companies.length()").value(2)) - // CompanyDto의 모든 필드 검증 (첫 번째 항목만) + // CompanyResponse의 모든 필드 검증 (첫 번째 항목만) .andExpect(jsonPath("$.data.companies[0].company").value("카카오")) .andExpect(jsonPath("$.data.companies[0].hasNewPost").value(true)) .andExpect(jsonPath("$.data.companies[0].logoUrl").value("https://kakao.com/logo.png")); @@ -207,7 +207,7 @@ void getPostsByCompany_MultipleCompanies_Success() throws Exception { .andExpect(jsonPath("$.data.lastViewCount").exists()) .andExpect(jsonPath("$.data.lastPublishedAt").exists()) .andExpect(jsonPath("$.data.hasNext").value(false)) - // PostInfoDto의 모든 필드 검증 (첫 번째 항목만) + // PostInfoResponse의 모든 필드 검증 (첫 번째 항목만) .andExpect(jsonPath("$.data.posts[0].id").isNumber()) .andExpect(jsonPath("$.data.posts[0].title").isString()) .andExpect(jsonPath("$.data.posts[0].shortSummary").isString()) @@ -364,7 +364,7 @@ void getRecentPosts_Latest_Success() throws Exception { .andExpect(jsonPath("$.data.lastPostId").exists()) .andExpect(jsonPath("$.data.lastPublishedAt").exists()) .andExpect(jsonPath("$.data.hasNext").value(false)) - // PostInfoDto의 모든 필드 검증 (첫 번째 항목만) + // PostInfoResponse의 모든 필드 검증 (첫 번째 항목만) .andExpect(jsonPath("$.data.posts[0].id").isNumber()) .andExpect(jsonPath("$.data.posts[0].title").value("오늘의 게시글")) .andExpect(jsonPath("$.data.posts[0].shortSummary").value("오늘 짧은 요약")) @@ -400,7 +400,7 @@ void getRecentPosts_Popular_Success() throws Exception { .andExpect(jsonPath("$.data.lastViewCount").exists()) .andExpect(jsonPath("$.data.lastPublishedAt").exists()) .andExpect(jsonPath("$.data.hasNext").value(false)) - // PostInfoDto의 모든 필드 검증 (첫 번째 항목만) + // PostInfoResponse의 모든 필드 검증 (첫 번째 항목만) .andExpect(jsonPath("$.data.posts[0].id").isNumber()) .andExpect(jsonPath("$.data.posts[0].title").value("오늘의 게시글")) .andExpect(jsonPath("$.data.posts[0].shortSummary").value("오늘 짧은 요약")) From f07cb697180c428d82bfa835a39395b8826c97b7 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 16:17:46 +0900 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20QueryService=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Result=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/query/PostQueryService.java | 103 +++++++++-- .../query/result/CompanyListItemResult.java | 11 ++ .../query/result/GetCompanyListResult.java | 12 ++ .../query/result/GetPostDetailResult.java | 21 +++ .../query/result/GetPostListResult.java | 16 ++ .../query/result/PostListItemResult.java | 22 +++ .../post/presentation/PostController.java | 16 +- .../post/presentation/PostControllerV2.java | 12 +- .../post/presentation/PostConverter.java | 71 ++++---- .../query/PostQueryServiceTest.java | 164 +++++++----------- 10 files changed, 282 insertions(+), 166 deletions(-) create mode 100644 src/main/java/com/techfork/post/application/query/result/CompanyListItemResult.java create mode 100644 src/main/java/com/techfork/post/application/query/result/GetCompanyListResult.java create mode 100644 src/main/java/com/techfork/post/application/query/result/GetPostDetailResult.java create mode 100644 src/main/java/com/techfork/post/application/query/result/GetPostListResult.java create mode 100644 src/main/java/com/techfork/post/application/query/result/PostListItemResult.java diff --git a/src/main/java/com/techfork/post/application/query/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java index 89e733dd..257fb08e 100644 --- a/src/main/java/com/techfork/post/application/query/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -1,6 +1,11 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; +import com.techfork.post.application.query.result.CompanyListItemResult; +import com.techfork.post.application.query.result.GetCompanyListResult; +import com.techfork.post.application.query.result.GetPostDetailResult; +import com.techfork.post.application.query.result.GetPostListResult; +import com.techfork.post.application.query.result.PostListItemResult; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; @@ -8,10 +13,6 @@ import com.techfork.post.infrastructure.row.CompanyRow; import com.techfork.post.infrastructure.row.PostDetailRow; import com.techfork.post.infrastructure.row.PostInfoRow; -import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailResponse; -import com.techfork.post.presentation.PostListResponse; -import com.techfork.post.presentation.PostConverter; import com.techfork.global.exception.CommonErrorCode; import com.techfork.global.exception.GeneralException; import com.techfork.global.util.CloudflareThirdPartyThumbnailOptimizer; @@ -36,20 +37,35 @@ public class PostQueryService { private final PostRepository postRepository; private final PostKeywordRepository postKeywordRepository; private final BookmarkRepository bookmarkRepository; - private final PostConverter postConverter; private final CloudflareThirdPartyThumbnailOptimizer thumbnailOptimizer; - public CompanyListResponse getCompanies() { + public GetCompanyListResult getCompanies() { List companies = postRepository.findDistinctCompanies(); - return postConverter.toCompanyListResponse(companies); + return GetCompanyListResult.builder() + .totalNumber(null) + .companies(companies.stream() + .map(company -> CompanyListItemResult.builder() + .company(company) + .build()) + .toList()) + .build(); } - public CompanyListResponse getCompaniesV2() { + public GetCompanyListResult getCompaniesV2() { List companies = postRepository.findCompaniesWithDetails(); - return postConverter.toCompanyListResponseV2(companies); + return GetCompanyListResult.builder() + .totalNumber(companies.size()) + .companies(companies.stream() + .map(company -> CompanyListItemResult.builder() + .company(company.company()) + .hasNewPost(company.hasNewPost()) + .logoUrl(company.logoUrl()) + .build()) + .toList()) + .build(); } - public PostListResponse getPostsByCompany(String company, Long lastPostId, int size, Long userId) { + public GetPostListResult getPostsByCompany(String company, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); List posts = postRepository.findByCompanyWithCursor(company, lastPostId, pageRequest); List postsWithKeywords = attachKeywordsToPostInfoList(posts); @@ -58,10 +74,10 @@ public PostListResponse getPostsByCompany(String company, Long lastPostId, int s postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); } - return postConverter.toPostListResponse(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, size); } - public PostListResponse getPostsByCompanyV2(List companies, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { + public GetPostListResult getPostsByCompanyV2(List companies, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); List posts = postRepository.findByCompanyNamesWithCursor(companies, lastPublishedAt, lastPostId, pageRequest); List postsWithKeywords = attachKeywordsToPostInfoList(posts); @@ -70,10 +86,10 @@ public PostListResponse getPostsByCompanyV2(List companies, LocalDateTim postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); } - return postConverter.toPostListResponse(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, size); } - public PostListResponse getRecentPosts(EPostSortType sortBy, Long lastPostId, int size, Long userId) { + public GetPostListResult getRecentPosts(EPostSortType sortBy, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); List posts; @@ -89,10 +105,10 @@ public PostListResponse getRecentPosts(EPostSortType sortBy, Long lastPostId, in postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); } - return postConverter.toPostListResponse(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, size); } - public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewCount, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { + public GetPostListResult getRecentPostsV2(EPostSortType sortBy, Integer lastViewCount, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { PageRequest pageRequest = PageRequest.of(0, size + 1); List posts; @@ -108,10 +124,10 @@ public PostListResponse getRecentPostsV2(EPostSortType sortBy, Integer lastViewC postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); } - return postConverter.toPostListResponse(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, size); } - public PostDetailResponse getPostDetail(Long postId, Long userId) { + public GetPostDetailResult getPostDetail(Long postId, Long userId) { PostDetailRow postDetail = postRepository.findByIdWithTechBlog(postId) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND)); @@ -125,7 +141,18 @@ public PostDetailResponse getPostDetail(Long postId, Long userId) { isBookmarked = !bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId)).isEmpty(); } - return postConverter.toPostDetailResponse(postDetail, keywords, isBookmarked); + return GetPostDetailResult.builder() + .id(postDetail.id()) + .title(postDetail.title()) + .summary(postDetail.summary()) + .company(postDetail.company()) + .url(postDetail.url()) + .logoUrl(postDetail.logoUrl()) + .publishedAt(postDetail.publishedAt()) + .viewCount(postDetail.viewCount()) + .keywords(keywords) + .isBookmarked(isBookmarked) + .build(); } private List attachKeywordsToPostInfoList(List posts) { @@ -190,4 +217,42 @@ private List attachBookmarksToPostInfoList(List posts, .build()) .toList(); } + + private GetPostListResult toGetPostListResult(List posts, int requestedSize) { + boolean hasNext = posts.size() > requestedSize; + List content = hasNext ? posts.subList(0, requestedSize) : posts; + + Long lastPostId = null; + Long lastViewCount = null; + LocalDateTime lastPublishedAt = null; + + if (!content.isEmpty()) { + PostInfoRow lastPost = content.get(content.size() - 1); + lastPostId = lastPost.id(); + lastViewCount = lastPost.viewCount(); + lastPublishedAt = lastPost.publishedAt(); + } + + return GetPostListResult.builder() + .posts(content.stream() + .map(post -> PostListItemResult.builder() + .id(post.id()) + .title(post.title()) + .shortSummary(post.shortSummary()) + .company(post.company()) + .url(post.url()) + .logoUrl(post.logoUrl()) + .thumbnailUrl(post.thumbnailUrl()) + .publishedAt(post.publishedAt()) + .viewCount(post.viewCount()) + .keywords(post.keywords()) + .isBookmarked(post.isBookmarked()) + .build()) + .toList()) + .lastPostId(lastPostId) + .lastViewCount(lastViewCount) + .lastPublishedAt(lastPublishedAt) + .hasNext(hasNext) + .build(); + } } diff --git a/src/main/java/com/techfork/post/application/query/result/CompanyListItemResult.java b/src/main/java/com/techfork/post/application/query/result/CompanyListItemResult.java new file mode 100644 index 00000000..09316202 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/result/CompanyListItemResult.java @@ -0,0 +1,11 @@ +package com.techfork.post.application.query.result; + +import lombok.Builder; + +@Builder +public record CompanyListItemResult( + String company, + boolean hasNewPost, + String logoUrl +) { +} diff --git a/src/main/java/com/techfork/post/application/query/result/GetCompanyListResult.java b/src/main/java/com/techfork/post/application/query/result/GetCompanyListResult.java new file mode 100644 index 00000000..b57eff7f --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/result/GetCompanyListResult.java @@ -0,0 +1,12 @@ +package com.techfork.post.application.query.result; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record GetCompanyListResult( + Integer totalNumber, + List companies +) { +} diff --git a/src/main/java/com/techfork/post/application/query/result/GetPostDetailResult.java b/src/main/java/com/techfork/post/application/query/result/GetPostDetailResult.java new file mode 100644 index 00000000..9da899ca --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/result/GetPostDetailResult.java @@ -0,0 +1,21 @@ +package com.techfork.post.application.query.result; + +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record GetPostDetailResult( + Long id, + String title, + String summary, + String company, + String url, + String logoUrl, + LocalDateTime publishedAt, + Long viewCount, + List keywords, + Boolean isBookmarked +) { +} diff --git a/src/main/java/com/techfork/post/application/query/result/GetPostListResult.java b/src/main/java/com/techfork/post/application/query/result/GetPostListResult.java new file mode 100644 index 00000000..a34cb728 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/result/GetPostListResult.java @@ -0,0 +1,16 @@ +package com.techfork.post.application.query.result; + +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +public record GetPostListResult( + List posts, + Long lastPostId, + Long lastViewCount, + LocalDateTime lastPublishedAt, + boolean hasNext +) { +} diff --git a/src/main/java/com/techfork/post/application/query/result/PostListItemResult.java b/src/main/java/com/techfork/post/application/query/result/PostListItemResult.java new file mode 100644 index 00000000..c152862a --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/result/PostListItemResult.java @@ -0,0 +1,22 @@ +package com.techfork.post.application.query.result; + +import lombok.Builder; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder(toBuilder = true) +public record PostListItemResult( + Long id, + String title, + String shortSummary, + String company, + String url, + String logoUrl, + String thumbnailUrl, + LocalDateTime publishedAt, + Long viewCount, + List keywords, + Boolean isBookmarked +) { +} diff --git a/src/main/java/com/techfork/post/presentation/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java index edb61219..0dd367c6 100644 --- a/src/main/java/com/techfork/post/presentation/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -3,6 +3,9 @@ import com.techfork.post.presentation.CompanyListResponse; import com.techfork.post.presentation.PostDetailResponse; import com.techfork.post.presentation.PostListResponse; +import com.techfork.post.application.query.result.GetCompanyListResult; +import com.techfork.post.application.query.result.GetPostDetailResult; +import com.techfork.post.application.query.result.GetPostListResult; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; @@ -25,6 +28,7 @@ public class PostController { private final PostQueryService postQueryService; + private final PostConverter postConverter; @Operation( summary = "게시글이 있는 회사 목록 조회", @@ -32,7 +36,8 @@ public class PostController { ) @GetMapping("/companies") public ResponseEntity> getCompanies() { - CompanyListResponse response = postQueryService.getCompanies(); + GetCompanyListResult result = postQueryService.getCompanies(); + CompanyListResponse response = postConverter.toCompanyListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -52,7 +57,8 @@ public ResponseEntity> getPostsByCompany( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -72,7 +78,8 @@ public ResponseEntity> getRecentPosts( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -88,7 +95,8 @@ public ResponseEntity> getPostDetail( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostDetailResponse response = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); + PostDetailResponse response = postConverter.toPostDetailResponse(result); return BaseResponse.of(SuccessCode.OK, response); } } diff --git a/src/main/java/com/techfork/post/presentation/PostControllerV2.java b/src/main/java/com/techfork/post/presentation/PostControllerV2.java index 58575aa1..15b9172a 100644 --- a/src/main/java/com/techfork/post/presentation/PostControllerV2.java +++ b/src/main/java/com/techfork/post/presentation/PostControllerV2.java @@ -2,6 +2,8 @@ import com.techfork.post.presentation.CompanyListResponse; import com.techfork.post.presentation.PostListResponse; +import com.techfork.post.application.query.result.GetCompanyListResult; +import com.techfork.post.application.query.result.GetPostListResult; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; @@ -31,6 +33,7 @@ public class PostControllerV2 { private final PostQueryService postQueryService; + private final PostConverter postConverter; @Operation( summary = "게시글이 있는 회사 목록 조회 (V2)", @@ -43,7 +46,8 @@ public class PostControllerV2 { ) @GetMapping("/companies") public ResponseEntity> getCompanies() { - CompanyListResponse response = postQueryService.getCompaniesV2(); + GetCompanyListResult result = postQueryService.getCompaniesV2(); + CompanyListResponse response = postConverter.toCompanyListResponseV2(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -77,7 +81,8 @@ public ResponseEntity> getPostsByCompany( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); + PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -114,7 +119,8 @@ public ResponseEntity> getRecentPosts( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - PostListResponse response = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } } diff --git a/src/main/java/com/techfork/post/presentation/PostConverter.java b/src/main/java/com/techfork/post/presentation/PostConverter.java index 1912867f..f690b53b 100644 --- a/src/main/java/com/techfork/post/presentation/PostConverter.java +++ b/src/main/java/com/techfork/post/presentation/PostConverter.java @@ -1,26 +1,27 @@ package com.techfork.post.presentation; -import com.techfork.post.infrastructure.row.CompanyRow; -import com.techfork.post.infrastructure.row.PostDetailRow; -import com.techfork.post.infrastructure.row.PostInfoRow; +import com.techfork.post.application.query.result.CompanyListItemResult; +import com.techfork.post.application.query.result.GetCompanyListResult; +import com.techfork.post.application.query.result.GetPostDetailResult; +import com.techfork.post.application.query.result.GetPostListResult; +import com.techfork.post.application.query.result.PostListItemResult; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; -import java.util.List; - @Component public class PostConverter { - public CompanyListResponse toCompanyListResponse(List companies) { + public CompanyListResponse toCompanyListResponse(GetCompanyListResult result) { return CompanyListResponse.builder() - .companies(companies) + .companies(result.companies().stream() + .map(CompanyListItemResult::company) + .toList()) .build(); } - public CompanyListResponse toCompanyListResponseV2(List companies) { + public CompanyListResponse toCompanyListResponseV2(GetCompanyListResult result) { return CompanyListResponse.builder() - .totalNumber(companies.size()) - .companies(companies.stream() + .totalNumber(result.totalNumber()) + .companies(result.companies().stream() .map(company -> CompanyResponse.builder() .company(company.company()) .hasNewPost(company.hasNewPost()) @@ -30,23 +31,9 @@ public CompanyListResponse toCompanyListResponseV2(List companies) { .build(); } - public PostListResponse toPostListResponse(List posts, int requestedSize) { - boolean hasNext = posts.size() > requestedSize; - List content = hasNext ? posts.subList(0, requestedSize) : posts; - - Long lastPostId = null; - Long lastViewCount = null; - LocalDateTime lastPublishedAt = null; - - if (!content.isEmpty()) { - PostInfoRow lastPost = content.get(content.size() - 1); - lastPostId = lastPost.id(); - lastViewCount = lastPost.viewCount(); - lastPublishedAt = lastPost.publishedAt(); - } - + public PostListResponse toPostListResponse(GetPostListResult result) { return PostListResponse.builder() - .posts(content.stream() + .posts(result.posts().stream() .map(post -> PostInfoResponse.builder() .id(post.id()) .title(post.title()) @@ -61,25 +48,25 @@ public PostListResponse toPostListResponse(List posts, int requeste .isBookmarked(post.isBookmarked()) .build()) .toList()) - .lastPostId(lastPostId) - .lastViewCount(lastViewCount) - .lastPublishedAt(lastPublishedAt) - .hasNext(hasNext) + .lastPostId(result.lastPostId()) + .lastViewCount(result.lastViewCount()) + .lastPublishedAt(result.lastPublishedAt()) + .hasNext(result.hasNext()) .build(); } - public PostDetailResponse toPostDetailResponse(PostDetailRow baseDto, List keywords, Boolean isBookmarked) { + public PostDetailResponse toPostDetailResponse(GetPostDetailResult result) { return PostDetailResponse.builder() - .id(baseDto.id()) - .title(baseDto.title()) - .summary(baseDto.summary()) - .company(baseDto.company()) - .url(baseDto.url()) - .logoUrl(baseDto.logoUrl()) - .publishedAt(baseDto.publishedAt()) - .viewCount(baseDto.viewCount()) - .keywords(keywords) - .isBookmarked(isBookmarked) + .id(result.id()) + .title(result.title()) + .summary(result.summary()) + .company(result.company()) + .url(result.url()) + .logoUrl(result.logoUrl()) + .publishedAt(result.publishedAt()) + .viewCount(result.viewCount()) + .keywords(result.keywords()) + .isBookmarked(result.isBookmarked()) .build(); } } diff --git a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index 8590e4dc..2f9e5644 100644 --- a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -4,12 +4,11 @@ import com.techfork.post.infrastructure.row.CompanyRow; import com.techfork.post.infrastructure.row.PostDetailRow; import com.techfork.post.infrastructure.row.PostInfoRow; -import com.techfork.post.presentation.CompanyResponse; -import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailResponse; -import com.techfork.post.presentation.PostInfoResponse; -import com.techfork.post.presentation.PostListResponse; -import com.techfork.post.presentation.PostConverter; +import com.techfork.post.application.query.result.CompanyListItemResult; +import com.techfork.post.application.query.result.GetCompanyListResult; +import com.techfork.post.application.query.result.GetPostDetailResult; +import com.techfork.post.application.query.result.GetPostListResult; +import com.techfork.post.application.query.result.PostListItemResult; import com.techfork.post.domain.PostKeyword; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.infrastructure.PostKeywordRepository; @@ -54,8 +53,6 @@ class PostQueryServiceTest { @Mock private BookmarkRepository bookmarkRepository; - @Mock - private PostConverter postConverter; @Mock private CloudflareThirdPartyThumbnailOptimizer thumbnailOptimizer; @@ -73,24 +70,19 @@ void setUp() { void getCompanies_Success() { // Given List mockCompanies = List.of("카카오", "네이버", "라인"); - CompanyListResponse expectedResponse = new CompanyListResponse(3, mockCompanies); given(postRepository.findDistinctCompanies()).willReturn(mockCompanies); - given(postConverter.toCompanyListResponse(mockCompanies)).willReturn(expectedResponse); // When - CompanyListResponse result = postQueryService.getCompanies(); + GetCompanyListResult result = postQueryService.getCompanies(); // Then assertThat(result).isNotNull(); assertThat(result.companies()).hasSize(3); - @SuppressWarnings("unchecked") - List companies = (List) result.companies(); - assertThat(companies).contains("카카오", "네이버", "라인"); + assertThat(result.companies()).extracting(CompanyListItemResult::company).contains("카카오", "네이버", "라인"); verify(postRepository, times(1)).findDistinctCompanies(); - verify(postConverter, times(1)).toCompanyListResponse(mockCompanies); } @Test @@ -110,24 +102,23 @@ void getCompaniesV2_Success() { .build() ); - List mockCompanies = mockCompanyRows.stream().map(this::toCompanyResponse).toList(); + List mockCompanies = mockCompanyRows.stream().map(this::toCompanyListItemResult).toList(); - CompanyListResponse expectedResponse = CompanyListResponse.builder() + GetCompanyListResult expectedResponse = GetCompanyListResult.builder() .totalNumber(2) .companies(mockCompanies) .build(); given(postRepository.findCompaniesWithDetails()).willReturn(mockCompanyRows); - given(postConverter.toCompanyListResponseV2(mockCompanyRows)).willReturn(expectedResponse); // When - CompanyListResponse result = postQueryService.getCompaniesV2(); + GetCompanyListResult result = postQueryService.getCompaniesV2(); // Then assertThat(result).isNotNull(); assertThat(result.companies()).hasSize(2); - List resultCompanies = (List) result.companies(); + List resultCompanies = result.companies(); assertThat(resultCompanies.get(0).company()).isEqualTo("카카오"); assertThat(resultCompanies.get(0).hasNewPost()).isTrue(); assertThat(resultCompanies.get(0).logoUrl()).isEqualTo("https://test.com/kakao-logo.png"); @@ -135,7 +126,6 @@ void getCompaniesV2_Success() { assertThat(resultCompanies.get(1).hasNewPost()).isFalse(); verify(postRepository, times(1)).findCompaniesWithDetails(); - verify(postConverter, times(1)).toCompanyListResponseV2(mockCompanyRows); } @Test @@ -166,7 +156,7 @@ void getPostDetail_WithoutAuth_Success() { List mockKeywords = List.of(keyword1, keyword2); List keywordStrings = List.of("Java", "Spring"); - PostDetailResponse expectedResponse = PostDetailResponse.builder() + GetPostDetailResult expectedResponse = GetPostDetailResult.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -181,10 +171,9 @@ void getPostDetail_WithoutAuth_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); - given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, null)).willReturn(expectedResponse); // When - PostDetailResponse result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -197,7 +186,6 @@ void getPostDetail_WithoutAuth_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); - verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, null); verify(bookmarkRepository, never()).findBookmarkedPostIds(any(), any()); } @@ -226,7 +214,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { List mockKeywords = List.of(keyword1); List keywordStrings = List.of("Java"); - PostDetailResponse expectedResponse = PostDetailResponse.builder() + GetPostDetailResult expectedResponse = GetPostDetailResult.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -242,10 +230,9 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of(postId)); - given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, true)).willReturn(expectedResponse); // When - PostDetailResponse result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -255,7 +242,6 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, true); } @Test @@ -283,7 +269,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { List mockKeywords = List.of(keyword1); List keywordStrings = List.of("Java"); - PostDetailResponse expectedResponse = PostDetailResponse.builder() + GetPostDetailResult expectedResponse = GetPostDetailResult.builder() .id(postId) .title("테스트 제목") .summary("테스트 요약") @@ -299,10 +285,9 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.of(mockPostDetailRow)); given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of()); - given(postConverter.toPostDetailResponse(mockPostDetailRow, keywordStrings, false)).willReturn(expectedResponse); // When - PostDetailResponse result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); // Then assertThat(result).isNotNull(); @@ -312,7 +297,6 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { verify(postRepository, times(1)).findByIdWithTechBlog(postId); verify(postKeywordRepository, times(1)).findByPostIdIn(List.of(postId)); verify(bookmarkRepository, times(1)).findBookmarkedPostIds(userId, List.of(postId)); - verify(postConverter, times(1)).toPostDetailResponse(mockPostDetailRow, keywordStrings, false); } @Test @@ -362,9 +346,9 @@ void getRecentPosts_Latest_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(1L) .hasNext(false) @@ -373,10 +357,9 @@ void getRecentPosts_Latest_Success() { given(postRepository.findRecentPostsWithCursor(eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -418,9 +401,9 @@ void getRecentPosts_Popular_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(2L) .hasNext(false) @@ -429,10 +412,9 @@ void getRecentPosts_Popular_Success() { given(postRepository.findPopularPostsWithCursor(eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -464,9 +446,9 @@ void getPostsByCompany_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(1L) .hasNext(false) @@ -475,10 +457,9 @@ void getPostsByCompany_Success() { given(postRepository.findByCompanyWithCursor(eq(company), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompany(company, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -521,9 +502,9 @@ void getPostsByCompanyV2_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(1L) .lastPublishedAt(now.minusHours(1)) @@ -533,10 +514,9 @@ void getPostsByCompanyV2_Success() { given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -547,7 +527,6 @@ void getPostsByCompanyV2_Success() { verify(postRepository, times(1)).findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class)); verify(postKeywordRepository, times(1)).findByPostIdIn(any()); - verify(postConverter, times(1)).toPostListResponse(any(), eq(size)); } @Test @@ -583,9 +562,9 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(2L) .lastPublishedAt(now.minusHours(1)) @@ -595,10 +574,9 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -629,9 +607,9 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(99L) .lastPublishedAt(lastPublishedAt.minusMinutes(10)) @@ -641,10 +619,9 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { given(postRepository.findByCompanyNamesWithCursor(eq(companies), eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -688,9 +665,9 @@ void getRecentPostsV2_Latest_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(1L) .lastPublishedAt(now.minusDays(1)) @@ -700,10 +677,9 @@ void getRecentPostsV2_Latest_Success() { given(postRepository.findRecentPostsWithCursorV2(eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -749,9 +725,9 @@ void getRecentPostsV2_Popular_Success() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(2L) .lastViewCount(500L) @@ -761,10 +737,9 @@ void getRecentPostsV2_Popular_Success() { given(postRepository.findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -798,9 +773,9 @@ void getRecentPostsV2_Popular_WithCursor() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(99L) .lastViewCount(400L) @@ -810,10 +785,9 @@ void getRecentPostsV2_Popular_WithCursor() { given(postRepository.findPopularPostsWithCursorV2(eq(lastViewCount), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -846,9 +820,9 @@ void getRecentPostsV2_Latest_WithCursor() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(99L) .lastPublishedAt(lastPublishedAt.minusHours(1)) @@ -858,10 +832,9 @@ void getRecentPostsV2_Latest_WithCursor() { given(postRepository.findRecentPostsWithCursorV2(eq(lastPublishedAt), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); // Then assertThat(result).isNotNull(); @@ -906,9 +879,9 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(List.of( mockPosts.get(0).toBuilder().isBookmarked(true).build(), mockPosts.get(1).toBuilder().isBookmarked(false).build() @@ -921,10 +894,9 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); // Then assertThat(result).isNotNull(); @@ -959,9 +931,9 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { .build() ); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(mockPosts) .lastPostId(1L) .hasNext(false) @@ -970,10 +942,9 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { given(postRepository.findByCompanyWithCursor(eq(company), eq(lastPostId), any(PageRequest.class))) .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); // Then assertThat(result).isNotNull(); @@ -1019,9 +990,9 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(2L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(List.of( mockPosts.get(0).toBuilder().isBookmarked(false).build(), mockPosts.get(1).toBuilder().isBookmarked(true).build() @@ -1034,10 +1005,9 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); // Then assertThat(result).isNotNull(); @@ -1086,9 +1056,9 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(1L, 2L); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(List.of( mockPosts.get(0).toBuilder().isBookmarked(true).build(), mockPosts.get(1).toBuilder().isBookmarked(true).build() @@ -1101,10 +1071,9 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); // Then assertThat(result).isNotNull(); @@ -1143,9 +1112,9 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { ); List bookmarkedPostIds = List.of(); - List mockPosts = mockPostRows.stream().map(this::toPostInfoResponse).toList(); + List mockPosts = mockPostRows.stream().map(this::toPostListItemResult).toList(); - PostListResponse expectedResponse = PostListResponse.builder() + GetPostListResult expectedResponse = GetPostListResult.builder() .posts(List.of( mockPosts.get(0).toBuilder().isBookmarked(false).build() )) @@ -1157,10 +1126,9 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { .willReturn(mockPostRows); given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); - given(postConverter.toPostListResponse(any(), eq(size))).willReturn(expectedResponse); // When - PostListResponse result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); // Then assertThat(result).isNotNull(); @@ -1171,8 +1139,8 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { verify(bookmarkRepository, times(1)).findBookmarkedPostIds(eq(userId), any()); } - private PostInfoResponse toPostInfoResponse(PostInfoRow row) { - return PostInfoResponse.builder() + private PostListItemResult toPostListItemResult(PostInfoRow row) { + return PostListItemResult.builder() .id(row.id()) .title(row.title()) .shortSummary(row.shortSummary()) @@ -1187,8 +1155,8 @@ private PostInfoResponse toPostInfoResponse(PostInfoRow row) { .build(); } - private CompanyResponse toCompanyResponse(CompanyRow row) { - return CompanyResponse.builder() + private CompanyListItemResult toCompanyListItemResult(CompanyRow row) { + return CompanyListItemResult.builder() .company(row.company()) .hasNewPost(row.hasNewPost()) .logoUrl(row.logoUrl()) From d32a70c10d6726140ab3d77ed126643fe43dc132 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 16:25:54 +0900 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20query=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EB=84=98=EA=B8=B8=20=EB=95=8C=20record?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=95=A9=ED=95=B4=EC=84=9C=20=EB=84=98?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/query/GetPostDetailQuery.java | 7 ++ .../query/GetPostsByCompanyQuery.java | 9 +++ .../query/GetPostsByCompanyV2Query.java | 13 ++++ .../query/GetRecentPostsQuery.java | 11 ++++ .../query/GetRecentPostsV2Query.java | 15 +++++ .../application/query/PostQueryService.java | 66 +++++++++---------- .../post/presentation/PostController.java | 12 +++- .../post/presentation/PostControllerV2.java | 8 ++- .../query/PostQueryServiceTest.java | 43 ++++++------ 9 files changed, 127 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/techfork/post/application/query/GetPostDetailQuery.java create mode 100644 src/main/java/com/techfork/post/application/query/GetPostsByCompanyQuery.java create mode 100644 src/main/java/com/techfork/post/application/query/GetPostsByCompanyV2Query.java create mode 100644 src/main/java/com/techfork/post/application/query/GetRecentPostsQuery.java create mode 100644 src/main/java/com/techfork/post/application/query/GetRecentPostsV2Query.java diff --git a/src/main/java/com/techfork/post/application/query/GetPostDetailQuery.java b/src/main/java/com/techfork/post/application/query/GetPostDetailQuery.java new file mode 100644 index 00000000..560f21a4 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/GetPostDetailQuery.java @@ -0,0 +1,7 @@ +package com.techfork.post.application.query; + +public record GetPostDetailQuery( + Long postId, + Long userId +) { +} diff --git a/src/main/java/com/techfork/post/application/query/GetPostsByCompanyQuery.java b/src/main/java/com/techfork/post/application/query/GetPostsByCompanyQuery.java new file mode 100644 index 00000000..9771cdf8 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/GetPostsByCompanyQuery.java @@ -0,0 +1,9 @@ +package com.techfork.post.application.query; + +public record GetPostsByCompanyQuery( + String company, + Long lastPostId, + int size, + Long userId +) { +} diff --git a/src/main/java/com/techfork/post/application/query/GetPostsByCompanyV2Query.java b/src/main/java/com/techfork/post/application/query/GetPostsByCompanyV2Query.java new file mode 100644 index 00000000..e1d96354 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/GetPostsByCompanyV2Query.java @@ -0,0 +1,13 @@ +package com.techfork.post.application.query; + +import java.time.LocalDateTime; +import java.util.List; + +public record GetPostsByCompanyV2Query( + List companies, + LocalDateTime lastPublishedAt, + Long lastPostId, + int size, + Long userId +) { +} diff --git a/src/main/java/com/techfork/post/application/query/GetRecentPostsQuery.java b/src/main/java/com/techfork/post/application/query/GetRecentPostsQuery.java new file mode 100644 index 00000000..2ab12ca1 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/GetRecentPostsQuery.java @@ -0,0 +1,11 @@ +package com.techfork.post.application.query; + +import com.techfork.post.domain.enums.EPostSortType; + +public record GetRecentPostsQuery( + EPostSortType sortBy, + Long lastPostId, + int size, + Long userId +) { +} diff --git a/src/main/java/com/techfork/post/application/query/GetRecentPostsV2Query.java b/src/main/java/com/techfork/post/application/query/GetRecentPostsV2Query.java new file mode 100644 index 00000000..db66e693 --- /dev/null +++ b/src/main/java/com/techfork/post/application/query/GetRecentPostsV2Query.java @@ -0,0 +1,15 @@ +package com.techfork.post.application.query; + +import com.techfork.post.domain.enums.EPostSortType; + +import java.time.LocalDateTime; + +public record GetRecentPostsV2Query( + EPostSortType sortBy, + Integer lastViewCount, + LocalDateTime lastPublishedAt, + Long lastPostId, + int size, + Long userId +) { +} diff --git a/src/main/java/com/techfork/post/application/query/PostQueryService.java b/src/main/java/com/techfork/post/application/query/PostQueryService.java index 257fb08e..755484c5 100644 --- a/src/main/java/com/techfork/post/application/query/PostQueryService.java +++ b/src/main/java/com/techfork/post/application/query/PostQueryService.java @@ -65,80 +65,80 @@ public GetCompanyListResult getCompaniesV2() { .build(); } - public GetPostListResult getPostsByCompany(String company, Long lastPostId, int size, Long userId) { - PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts = postRepository.findByCompanyWithCursor(company, lastPostId, pageRequest); + public GetPostListResult getPostsByCompany(GetPostsByCompanyQuery query) { + PageRequest pageRequest = PageRequest.of(0, query.size() + 1); + List posts = postRepository.findByCompanyWithCursor(query.company(), query.lastPostId(), pageRequest); List postsWithKeywords = attachKeywordsToPostInfoList(posts); - if (userId != null) { - postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); + if (query.userId() != null) { + postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, query.userId()); } - return toGetPostListResult(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, query.size()); } - public GetPostListResult getPostsByCompanyV2(List companies, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { - PageRequest pageRequest = PageRequest.of(0, size + 1); - List posts = postRepository.findByCompanyNamesWithCursor(companies, lastPublishedAt, lastPostId, pageRequest); + public GetPostListResult getPostsByCompanyV2(GetPostsByCompanyV2Query query) { + PageRequest pageRequest = PageRequest.of(0, query.size() + 1); + List posts = postRepository.findByCompanyNamesWithCursor(query.companies(), query.lastPublishedAt(), query.lastPostId(), pageRequest); List postsWithKeywords = attachKeywordsToPostInfoList(posts); - if (userId != null) { - postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); + if (query.userId() != null) { + postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, query.userId()); } - return toGetPostListResult(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, query.size()); } - public GetPostListResult getRecentPosts(EPostSortType sortBy, Long lastPostId, int size, Long userId) { - PageRequest pageRequest = PageRequest.of(0, size + 1); + public GetPostListResult getRecentPosts(GetRecentPostsQuery query) { + PageRequest pageRequest = PageRequest.of(0, query.size() + 1); List posts; - if (sortBy == EPostSortType.POPULAR) { - posts = postRepository.findPopularPostsWithCursor(lastPostId, pageRequest); + if (query.sortBy() == EPostSortType.POPULAR) { + posts = postRepository.findPopularPostsWithCursor(query.lastPostId(), pageRequest); } else { - posts = postRepository.findRecentPostsWithCursor(lastPostId, pageRequest); + posts = postRepository.findRecentPostsWithCursor(query.lastPostId(), pageRequest); } List postsWithKeywords = attachKeywordsToPostInfoList(posts); - if (userId != null) { - postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); + if (query.userId() != null) { + postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, query.userId()); } - return toGetPostListResult(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, query.size()); } - public GetPostListResult getRecentPostsV2(EPostSortType sortBy, Integer lastViewCount, LocalDateTime lastPublishedAt, Long lastPostId, int size, Long userId) { - PageRequest pageRequest = PageRequest.of(0, size + 1); + public GetPostListResult getRecentPostsV2(GetRecentPostsV2Query query) { + PageRequest pageRequest = PageRequest.of(0, query.size() + 1); List posts; - if (sortBy == EPostSortType.POPULAR) { - posts = postRepository.findPopularPostsWithCursorV2(lastViewCount, lastPostId, pageRequest); + if (query.sortBy() == EPostSortType.POPULAR) { + posts = postRepository.findPopularPostsWithCursorV2(query.lastViewCount(), query.lastPostId(), pageRequest); } else { - posts = postRepository.findRecentPostsWithCursorV2(lastPublishedAt, lastPostId, pageRequest); + posts = postRepository.findRecentPostsWithCursorV2(query.lastPublishedAt(), query.lastPostId(), pageRequest); } List postsWithKeywords = attachKeywordsToPostInfoList(posts); - if (userId != null) { - postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, userId); + if (query.userId() != null) { + postsWithKeywords = attachBookmarksToPostInfoList(postsWithKeywords, query.userId()); } - return toGetPostListResult(postsWithKeywords, size); + return toGetPostListResult(postsWithKeywords, query.size()); } - public GetPostDetailResult getPostDetail(Long postId, Long userId) { - PostDetailRow postDetail = postRepository.findByIdWithTechBlog(postId) + public GetPostDetailResult getPostDetail(GetPostDetailQuery query) { + PostDetailRow postDetail = postRepository.findByIdWithTechBlog(query.postId()) .orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND)); - List keywords = postKeywordRepository.findByPostIdIn(List.of(postId)) + List keywords = postKeywordRepository.findByPostIdIn(List.of(query.postId())) .stream() .map(PostKeyword::getKeyword) .toList(); Boolean isBookmarked = null; - if (userId != null) { - isBookmarked = !bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId)).isEmpty(); + if (query.userId() != null) { + isBookmarked = !bookmarkRepository.findBookmarkedPostIds(query.userId(), List.of(query.postId())).isEmpty(); } return GetPostDetailResult.builder() diff --git a/src/main/java/com/techfork/post/presentation/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java index 0dd367c6..eebc5ac0 100644 --- a/src/main/java/com/techfork/post/presentation/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -6,6 +6,9 @@ import com.techfork.post.application.query.result.GetCompanyListResult; import com.techfork.post.application.query.result.GetPostDetailResult; import com.techfork.post.application.query.result.GetPostListResult; +import com.techfork.post.application.query.GetPostDetailQuery; +import com.techfork.post.application.query.GetPostsByCompanyQuery; +import com.techfork.post.application.query.GetRecentPostsQuery; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; @@ -57,7 +60,8 @@ public ResponseEntity> getPostsByCompany( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostsByCompanyQuery query = new GetPostsByCompanyQuery(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(query); PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -78,7 +82,8 @@ public ResponseEntity> getRecentPosts( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + GetRecentPostsQuery query = new GetRecentPostsQuery(sortBy, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPosts(query); PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -95,7 +100,8 @@ public ResponseEntity> getPostDetail( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); + GetPostDetailQuery query = new GetPostDetailQuery(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(query); PostDetailResponse response = postConverter.toPostDetailResponse(result); return BaseResponse.of(SuccessCode.OK, response); } diff --git a/src/main/java/com/techfork/post/presentation/PostControllerV2.java b/src/main/java/com/techfork/post/presentation/PostControllerV2.java index 15b9172a..8ab328c3 100644 --- a/src/main/java/com/techfork/post/presentation/PostControllerV2.java +++ b/src/main/java/com/techfork/post/presentation/PostControllerV2.java @@ -4,6 +4,8 @@ import com.techfork.post.presentation.PostListResponse; import com.techfork.post.application.query.result.GetCompanyListResult; import com.techfork.post.application.query.result.GetPostListResult; +import com.techfork.post.application.query.GetPostsByCompanyV2Query; +import com.techfork.post.application.query.GetRecentPostsV2Query; import com.techfork.post.domain.enums.EPostSortType; import com.techfork.post.application.query.PostQueryService; import com.techfork.global.common.code.SuccessCode; @@ -81,7 +83,8 @@ public ResponseEntity> getPostsByCompany( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); + GetPostsByCompanyV2Query query = new GetPostsByCompanyV2Query(companies, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompanyV2(query); PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } @@ -119,7 +122,8 @@ public ResponseEntity> getRecentPosts( @AuthenticationPrincipal UserPrincipal userPrincipal ) { Long userId = userPrincipal != null ? userPrincipal.getId() : null; - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + GetRecentPostsV2Query query = new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPostsV2(query); PostListResponse response = postConverter.toPostListResponse(result); return BaseResponse.of(SuccessCode.OK, response); } diff --git a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java index 2f9e5644..bb7a6f52 100644 --- a/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java +++ b/src/test/java/com/techfork/post/application/query/PostQueryServiceTest.java @@ -1,6 +1,11 @@ package com.techfork.post.application.query; import com.techfork.activity.bookmark.infrastructure.BookmarkRepository; +import com.techfork.post.application.query.GetPostDetailQuery; +import com.techfork.post.application.query.GetPostsByCompanyQuery; +import com.techfork.post.application.query.GetPostsByCompanyV2Query; +import com.techfork.post.application.query.GetRecentPostsQuery; +import com.techfork.post.application.query.GetRecentPostsV2Query; import com.techfork.post.infrastructure.row.CompanyRow; import com.techfork.post.infrastructure.row.PostDetailRow; import com.techfork.post.infrastructure.row.PostInfoRow; @@ -173,7 +178,7 @@ void getPostDetail_WithoutAuth_Success() { given(postKeywordRepository.findByPostIdIn(List.of(postId))).willReturn(mockKeywords); // When - GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(new GetPostDetailQuery(postId, userId)); // Then assertThat(result).isNotNull(); @@ -232,7 +237,7 @@ void getPostDetail_WithAuth_BookmarkedPost_Success() { given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of(postId)); // When - GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(new GetPostDetailQuery(postId, userId)); // Then assertThat(result).isNotNull(); @@ -287,7 +292,7 @@ void getPostDetail_WithAuth_NotBookmarkedPost_Success() { given(bookmarkRepository.findBookmarkedPostIds(userId, List.of(postId))).willReturn(List.of()); // When - GetPostDetailResult result = postQueryService.getPostDetail(postId, userId); + GetPostDetailResult result = postQueryService.getPostDetail(new GetPostDetailQuery(postId, userId)); // Then assertThat(result).isNotNull(); @@ -308,7 +313,7 @@ void getPostDetail_NotFound_ThrowsException() { given(postRepository.findByIdWithTechBlog(postId)).willReturn(Optional.empty()); // When & Then - assertThatThrownBy(() -> postQueryService.getPostDetail(postId, userId)) + assertThatThrownBy(() -> postQueryService.getPostDetail(new GetPostDetailQuery(postId, userId))) .isInstanceOf(GeneralException.class); verify(postRepository, times(1)).findByIdWithTechBlog(postId); @@ -359,7 +364,7 @@ void getRecentPosts_Latest_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPosts(new GetRecentPostsQuery(sortBy, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -414,7 +419,7 @@ void getRecentPosts_Popular_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPosts(new GetRecentPostsQuery(sortBy, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -459,7 +464,7 @@ void getPostsByCompany_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompany(new GetPostsByCompanyQuery(company, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -516,7 +521,7 @@ void getPostsByCompanyV2_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(new GetPostsByCompanyV2Query(companies, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -576,7 +581,7 @@ void getPostsByCompanyV2_NullCompanies_ReturnsAll() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(new GetPostsByCompanyV2Query(companies, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -621,7 +626,7 @@ void getPostsByCompanyV2_WithCursor_ReturnsNextPage() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getPostsByCompanyV2(new GetPostsByCompanyV2Query(companies, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -679,7 +684,7 @@ void getRecentPostsV2_Latest_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -739,7 +744,7 @@ void getRecentPostsV2_Popular_Success() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -787,7 +792,7 @@ void getRecentPostsV2_Popular_WithCursor() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -834,7 +839,7 @@ void getRecentPostsV2_Latest_WithCursor() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null); + GetPostListResult result = postQueryService.getRecentPostsV2(new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, null)); // Then assertThat(result).isNotNull(); @@ -896,7 +901,7 @@ void getPostsByCompany_WithUserId_IncludesBookmarks() { given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); // When - GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(new GetPostsByCompanyQuery(company, lastPostId, size, userId)); // Then assertThat(result).isNotNull(); @@ -944,7 +949,7 @@ void getPostsByCompany_WithoutUserId_NoBookmarks() { given(postKeywordRepository.findByPostIdIn(any())).willReturn(List.of()); // When - GetPostListResult result = postQueryService.getPostsByCompany(company, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompany(new GetPostsByCompanyQuery(company, lastPostId, size, userId)); // Then assertThat(result).isNotNull(); @@ -1007,7 +1012,7 @@ void getRecentPosts_WithUserId_IncludesBookmarks() { given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); // When - GetPostListResult result = postQueryService.getRecentPosts(sortBy, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPosts(new GetRecentPostsQuery(sortBy, lastPostId, size, userId)); // Then assertThat(result).isNotNull(); @@ -1073,7 +1078,7 @@ void getPostsByCompanyV2_WithUserId_IncludesBookmarks() { given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); // When - GetPostListResult result = postQueryService.getPostsByCompanyV2(companies, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getPostsByCompanyV2(new GetPostsByCompanyV2Query(companies, lastPublishedAt, lastPostId, size, userId)); // Then assertThat(result).isNotNull(); @@ -1128,7 +1133,7 @@ void getRecentPostsV2_WithUserId_Popular_IncludesBookmarks() { given(bookmarkRepository.findBookmarkedPostIds(eq(userId), any())).willReturn(bookmarkedPostIds); // When - GetPostListResult result = postQueryService.getRecentPostsV2(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId); + GetPostListResult result = postQueryService.getRecentPostsV2(new GetRecentPostsV2Query(sortBy, lastViewCount, lastPublishedAt, lastPostId, size, userId)); // Then assertThat(result).isNotNull(); From dd473884fd871c973e2f7147a46264b70ce24cc6 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 16:33:36 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20response=EB=A5=BC=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EC=97=AC=20=EA=B0=80=EB=8F=85=EC=84=B1=20?= =?UTF-8?q?=EC=A6=9D=EC=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/techfork/post/presentation/PostController.java | 6 +++--- .../com/techfork/post/presentation/PostControllerV2.java | 4 ++-- .../java/com/techfork/post/presentation/PostConverter.java | 5 +++++ .../presentation/{ => response}/CompanyListResponse.java | 2 +- .../post/presentation/{ => response}/CompanyResponse.java | 2 +- .../presentation/{ => response}/PostDetailResponse.java | 2 +- .../post/presentation/{ => response}/PostInfoResponse.java | 2 +- .../post/presentation/{ => response}/PostListResponse.java | 2 +- 8 files changed, 15 insertions(+), 10 deletions(-) rename src/main/java/com/techfork/post/presentation/{ => response}/CompanyListResponse.java (86%) rename src/main/java/com/techfork/post/presentation/{ => response}/CompanyResponse.java (84%) rename src/main/java/com/techfork/post/presentation/{ => response}/PostDetailResponse.java (90%) rename src/main/java/com/techfork/post/presentation/{ => response}/PostInfoResponse.java (91%) rename src/main/java/com/techfork/post/presentation/{ => response}/PostListResponse.java (88%) diff --git a/src/main/java/com/techfork/post/presentation/PostController.java b/src/main/java/com/techfork/post/presentation/PostController.java index eebc5ac0..f6bb0a55 100644 --- a/src/main/java/com/techfork/post/presentation/PostController.java +++ b/src/main/java/com/techfork/post/presentation/PostController.java @@ -1,8 +1,8 @@ package com.techfork.post.presentation; -import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostDetailResponse; -import com.techfork.post.presentation.PostListResponse; +import com.techfork.post.presentation.response.CompanyListResponse; +import com.techfork.post.presentation.response.PostDetailResponse; +import com.techfork.post.presentation.response.PostListResponse; import com.techfork.post.application.query.result.GetCompanyListResult; import com.techfork.post.application.query.result.GetPostDetailResult; import com.techfork.post.application.query.result.GetPostListResult; diff --git a/src/main/java/com/techfork/post/presentation/PostControllerV2.java b/src/main/java/com/techfork/post/presentation/PostControllerV2.java index 8ab328c3..c08d57b0 100644 --- a/src/main/java/com/techfork/post/presentation/PostControllerV2.java +++ b/src/main/java/com/techfork/post/presentation/PostControllerV2.java @@ -1,7 +1,7 @@ package com.techfork.post.presentation; -import com.techfork.post.presentation.CompanyListResponse; -import com.techfork.post.presentation.PostListResponse; +import com.techfork.post.presentation.response.CompanyListResponse; +import com.techfork.post.presentation.response.PostListResponse; import com.techfork.post.application.query.result.GetCompanyListResult; import com.techfork.post.application.query.result.GetPostListResult; import com.techfork.post.application.query.GetPostsByCompanyV2Query; diff --git a/src/main/java/com/techfork/post/presentation/PostConverter.java b/src/main/java/com/techfork/post/presentation/PostConverter.java index f690b53b..f1bdd5f9 100644 --- a/src/main/java/com/techfork/post/presentation/PostConverter.java +++ b/src/main/java/com/techfork/post/presentation/PostConverter.java @@ -5,6 +5,11 @@ import com.techfork.post.application.query.result.GetPostDetailResult; import com.techfork.post.application.query.result.GetPostListResult; import com.techfork.post.application.query.result.PostListItemResult; +import com.techfork.post.presentation.response.CompanyListResponse; +import com.techfork.post.presentation.response.CompanyResponse; +import com.techfork.post.presentation.response.PostDetailResponse; +import com.techfork.post.presentation.response.PostInfoResponse; +import com.techfork.post.presentation.response.PostListResponse; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/techfork/post/presentation/CompanyListResponse.java b/src/main/java/com/techfork/post/presentation/response/CompanyListResponse.java similarity index 86% rename from src/main/java/com/techfork/post/presentation/CompanyListResponse.java rename to src/main/java/com/techfork/post/presentation/response/CompanyListResponse.java index e104abce..d838b786 100644 --- a/src/main/java/com/techfork/post/presentation/CompanyListResponse.java +++ b/src/main/java/com/techfork/post/presentation/response/CompanyListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.presentation; +package com.techfork.post.presentation.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/presentation/CompanyResponse.java b/src/main/java/com/techfork/post/presentation/response/CompanyResponse.java similarity index 84% rename from src/main/java/com/techfork/post/presentation/CompanyResponse.java rename to src/main/java/com/techfork/post/presentation/response/CompanyResponse.java index ba5fb52b..1826b2ea 100644 --- a/src/main/java/com/techfork/post/presentation/CompanyResponse.java +++ b/src/main/java/com/techfork/post/presentation/response/CompanyResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.presentation; +package com.techfork.post.presentation.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/presentation/PostDetailResponse.java b/src/main/java/com/techfork/post/presentation/response/PostDetailResponse.java similarity index 90% rename from src/main/java/com/techfork/post/presentation/PostDetailResponse.java rename to src/main/java/com/techfork/post/presentation/response/PostDetailResponse.java index f7034a28..e0b23cc6 100644 --- a/src/main/java/com/techfork/post/presentation/PostDetailResponse.java +++ b/src/main/java/com/techfork/post/presentation/response/PostDetailResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.presentation; +package com.techfork.post.presentation.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/presentation/PostInfoResponse.java b/src/main/java/com/techfork/post/presentation/response/PostInfoResponse.java similarity index 91% rename from src/main/java/com/techfork/post/presentation/PostInfoResponse.java rename to src/main/java/com/techfork/post/presentation/response/PostInfoResponse.java index 5c86e9e4..06f44bb4 100644 --- a/src/main/java/com/techfork/post/presentation/PostInfoResponse.java +++ b/src/main/java/com/techfork/post/presentation/response/PostInfoResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.presentation; +package com.techfork.post.presentation.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/techfork/post/presentation/PostListResponse.java b/src/main/java/com/techfork/post/presentation/response/PostListResponse.java similarity index 88% rename from src/main/java/com/techfork/post/presentation/PostListResponse.java rename to src/main/java/com/techfork/post/presentation/response/PostListResponse.java index 9aef3d99..874205c5 100644 --- a/src/main/java/com/techfork/post/presentation/PostListResponse.java +++ b/src/main/java/com/techfork/post/presentation/response/PostListResponse.java @@ -1,4 +1,4 @@ -package com.techfork.post.presentation; +package com.techfork.post.presentation.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; From 3d0d9ce2d0232161376bcf1e993b924c18f64482 Mon Sep 17 00:00:00 2001 From: Dimo-2562 Date: Fri, 22 May 2026 16:50:49 +0900 Subject: [PATCH 13/13] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/test-gap-analysis.md | 3 ++- docs/ubiquitous-language/README.md | 2 +- docs/ubiquitous-language/admin-ops.md | 2 +- docs/ubiquitous-language/post-content.md | 16 ++++++++-------- docs/ubiquitous-language/search.md | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/test-gap-analysis.md b/docs/test-gap-analysis.md index 427dbc03..e777ffcb 100644 --- a/docs/test-gap-analysis.md +++ b/docs/test-gap-analysis.md @@ -86,7 +86,8 @@ 현재 working tree에는 다음 untracked 테스트 디렉터리가 있다. ```text -src/test/java/com/techfork/domain/post/batch/ +src/test/java/com/techfork/post/application/batch/ +src/test/java/com/techfork/post/infrastructure/batch/ ``` 포함 파일: diff --git a/docs/ubiquitous-language/README.md b/docs/ubiquitous-language/README.md index c9cc4541..955ed559 100644 --- a/docs/ubiquitous-language/README.md +++ b/docs/ubiquitous-language/README.md @@ -25,7 +25,7 @@ | 바운디드 컨텍스트 | 문서 | 현재 owning package | 메모 | |---|---|---|---| | Source / Ingestion | [`source-ingestion.md`](./source-ingestion.md) | `src/main/java/com/techfork/domain/source` | RSS 수집, 소스 블로그, 파이프라인 시작점 | -| Post / Content | [`post-content.md`](./post-content.md) | `src/main/java/com/techfork/domain/post` | 기술 게시글 본문, 요약, 키워드, 검색 projection | +| Post / Content | [`post-content.md`](./post-content.md) | `src/main/java/com/techfork/post` | 기술 게시글 본문, 요약, 키워드, 검색 projection | | User Account | [`user-account.md`](./user-account.md) | `src/main/java/com/techfork/domain/useraccount` | 계정, 온보딩, 관심사, 계정 프로필 | | Personalization Profile | [`personalization-profile.md`](./personalization-profile.md) | `src/main/java/com/techfork/domain/personalization` | 개인화 프로필 생성, 벡터, 핵심 키워드, 재생성 | | Activity | [`activity.md`](./activity.md) | `src/main/java/com/techfork/activity` | 읽기/검색/북마크 행동 기록 | diff --git a/docs/ubiquitous-language/admin-ops.md b/docs/ubiquitous-language/admin-ops.md index 7bb53610..61822c8d 100644 --- a/docs/ubiquitous-language/admin-ops.md +++ b/docs/ubiquitous-language/admin-ops.md @@ -5,7 +5,7 @@ ## Owning packages - `src/main/java/com/techfork/domain/admin` -- 연관 운영 서비스: `src/main/java/com/techfork/domain/source`, `src/main/java/com/techfork/domain/post/batch` +- 연관 운영 서비스: `src/main/java/com/techfork/domain/source`, `src/main/java/com/techfork/post/application/batch`, `src/main/java/com/techfork/post/infrastructure/batch` ## 표준 용어 diff --git a/docs/ubiquitous-language/post-content.md b/docs/ubiquitous-language/post-content.md index c4257db3..f4ab4a61 100644 --- a/docs/ubiquitous-language/post-content.md +++ b/docs/ubiquitous-language/post-content.md @@ -4,7 +4,7 @@ ## Owning packages -- `src/main/java/com/techfork/domain/post` +- `src/main/java/com/techfork/post` ## 표준 용어 @@ -66,11 +66,11 @@ ## 주요 근거 파일 -- `src/main/java/com/techfork/domain/post/entity/Post.java` -- `src/main/java/com/techfork/domain/post/entity/PostKeyword.java` +- `src/main/java/com/techfork/post/domain/Post.java` +- `src/main/java/com/techfork/post/domain/PostKeyword.java` - `src/main/java/com/techfork/post/application/command/PostViewCountCommandService.java` -- `src/main/java/com/techfork/domain/post/repository/PostRepository.java` -- `src/main/java/com/techfork/domain/post/document/PostDocument.java` -- `src/main/java/com/techfork/domain/post/document/ContentChunk.java` -- `src/main/java/com/techfork/domain/post/batch/PostSummaryProcessor.java` -- `src/main/java/com/techfork/domain/post/batch/PostEmbeddingProcessor.java` +- `src/main/java/com/techfork/post/infrastructure/PostRepository.java` +- `src/main/java/com/techfork/post/domain/projection/PostDocument.java` +- `src/main/java/com/techfork/post/domain/projection/ContentChunk.java` +- `src/main/java/com/techfork/post/application/batch/PostSummaryProcessor.java` +- `src/main/java/com/techfork/post/application/batch/PostEmbeddingProcessor.java` diff --git a/docs/ubiquitous-language/search.md b/docs/ubiquitous-language/search.md index b6b77d4d..ad22b8af 100644 --- a/docs/ubiquitous-language/search.md +++ b/docs/ubiquitous-language/search.md @@ -5,7 +5,7 @@ ## Owning packages - `src/main/java/com/techfork/domain/search` -- 관련 read model: `src/main/java/com/techfork/domain/post/document`, `src/main/java/com/techfork/domain/personalization/document` +- 관련 read model: `src/main/java/com/techfork/post/domain/projection`, `src/main/java/com/techfork/domain/personalization/document` ## 표준 용어