From d2ef2e97894b17d8172e5704b1cf7b7f7516a2ee Mon Sep 17 00:00:00 2001 From: ose0221 Date: Fri, 13 Feb 2026 16:00:15 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 15 +++++++++ .../dto/response/ArticleDetailResponseV3.java | 31 +++++++++++++++++++ .../article/port/in/GetArticlePort.java | 3 ++ .../usecase/query/GetArticleUsecase.java | 7 +++++ 4 files changed, 56 insertions(+) create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/ArticleDetailResponseV3.java diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 9f94333..b9caa5d 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -1,12 +1,16 @@ package com.pinback.api.article.controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.pinback.api.article.dto.request.ArticleCreateRequest; +import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; +import com.pinback.application.article.port.in.GetArticlePort; import com.pinback.domain.user.entity.User; import com.pinback.infrastructure.article.service.ArticleUpdateService; import com.pinback.shared.annotation.CurrentUser; @@ -26,6 +30,7 @@ @Tag(name = "ArticleV3", description = "아티클 관리 API V3") public class ArticleControllerV3 { private final CreateArticlePort createArticlePort; + private final GetArticlePort getArticlePort; private final ArticleUpdateService articleMetadataUpdateService; @Operation(summary = "아티클 생성v3", description = "url에서 썸네일과 제목을 추출하여 새로운 아티클을 생성합니다") @@ -38,6 +43,16 @@ public ResponseDto createArticle( return ResponseDto.ok(); } + @Operation(summary = "아티클 상세 조회 V3", description = "아티클의 상세 정보를 조회합니다") + @GetMapping("/{articleId}") + public ResponseDto getArticleDetail( + @Parameter(description = "아티클 ID") @PathVariable Long articleId + ) { + ArticleDetailResponseV3 response = getArticlePort.getArticleDetailWithMetadata(articleId); + return ResponseDto.ok(response); + } + + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { articleMetadataUpdateService.migrateMissingMetadata(); diff --git a/application/src/main/java/com/pinback/application/article/dto/response/ArticleDetailResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/ArticleDetailResponseV3.java new file mode 100644 index 0000000..dd38546 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/ArticleDetailResponseV3.java @@ -0,0 +1,31 @@ +package com.pinback.application.article.dto.response; + +import java.time.LocalDateTime; + +import com.pinback.application.category.dto.response.CategoryResponse; +import com.pinback.domain.article.entity.Article; + +public record ArticleDetailResponseV3( + long articleId, + String url, + String title, + String thumbnailUrl, + String memo, + LocalDateTime remindAt, + LocalDateTime createdAt, + CategoryResponse categoryResponse +) { + public static ArticleDetailResponseV3 from(Article article) { + return new ArticleDetailResponseV3( + article.getId(), + article.getUrl(), + article.getTitle(), + article.getThumbnail(), + article.getMemo(), + article.getRemindAt(), + article.getCreatedAt(), + CategoryResponse.from(article.getCategory()) + ); + } + +} diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 8d6d7f4..dfa5fb6 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -4,6 +4,7 @@ import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleDetailResponse; +import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; @@ -24,4 +25,6 @@ public interface GetArticlePort { TodayRemindResponse getRemindArticles(User user, LocalDateTime now, boolean readStatus, PageQuery query); TodayRemindResponseV2 getRemindArticlesV2(User user, LocalDateTime now, boolean readStatus, PageQuery query); + + ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 778e460..0b96d00 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -15,6 +15,7 @@ import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleDetailResponse; +import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticleResponse; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; @@ -157,6 +158,12 @@ public TodayRemindResponseV2 getRemindArticlesV2( ); } + @Override + public ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId) { + Article article = articleGetServicePort.findById(articleId); + return ArticleDetailResponseV3.from(article); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), From 1aae4596c7d187f7a0b70fba9e51e5000a960dcb Mon Sep 17 00:00:00 2001 From: ose0221 Date: Fri, 13 Feb 2026 16:19:47 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EB=A6=AC=EB=A7=88=EC=9D=B8?= =?UTF-8?q?=EB=93=9C=20=EC=95=84=ED=8B=B0=ED=81=B4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 19 ++++++++++ .../dto/response/RemindArticleResponseV3.java | 34 ++++++++++++++++++ .../dto/response/TodayRemindResponseV3.java | 21 +++++++++++ .../article/port/in/GetArticlePort.java | 3 ++ .../usecase/query/GetArticleUsecase.java | 36 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/RemindArticleResponseV3.java create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/TodayRemindResponseV3.java diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index b9caa5d..969691b 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -1,14 +1,19 @@ package com.pinback.api.article.controller; +import java.time.LocalDateTime; + import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.pinback.api.article.dto.request.ArticleCreateRequest; +import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; +import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; import com.pinback.application.article.port.in.GetArticlePort; import com.pinback.domain.user.entity.User; @@ -52,6 +57,20 @@ public ResponseDto getArticleDetail( return ResponseDto.ok(response); } + @Operation(summary = "리마인드 아티클 조회 v3", description = "오늘 리마인드할 아티클을 읽음/안읽음 상태별로 조회합니다.") + @GetMapping("/remind") + public ResponseDto getRemindArticlesV3( + @Parameter(hidden = true) @CurrentUser User user, + @Parameter(description = "현재 시간", example = "2026-02-13T10:00:00") @RequestParam LocalDateTime now, + @Parameter(description = "읽음 상태 (true: 읽음, false: 안읽음)", example = "true") @RequestParam(name = "read-status") boolean readStatus, + @Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page, + @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "8") int size + ) { + PageQuery query = new PageQuery(page, size); + TodayRemindResponseV3 response = getArticlePort.getRemindArticlesV3(user, now, readStatus, query); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/dto/response/RemindArticleResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/RemindArticleResponseV3.java new file mode 100644 index 0000000..3887e56 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/RemindArticleResponseV3.java @@ -0,0 +1,34 @@ +package com.pinback.application.article.dto.response; + +import java.time.LocalDateTime; + +import com.pinback.application.category.dto.response.CategoryResponse; +import com.pinback.domain.article.entity.Article; + +public record RemindArticleResponseV3( + long articleId, + String url, + String title, + String thumbnailUrl, + String memo, + LocalDateTime createdAt, + boolean isRead, + boolean isReadAfterRemind, + LocalDateTime remindAt, + CategoryResponse category +) { + public static RemindArticleResponseV3 from(Article article) { + return new RemindArticleResponseV3( + article.getId(), + article.getUrl(), + article.getTitle(), + article.getThumbnail(), + article.getMemo(), + article.getCreatedAt(), + article.isRead(), + article.isReadAfterRemind(), + article.getRemindAt(), + CategoryResponse.from(article.getCategory()) + ); + } +} diff --git a/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindResponseV3.java new file mode 100644 index 0000000..edd29c6 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindResponseV3.java @@ -0,0 +1,21 @@ +package com.pinback.application.article.dto.response; + +import java.util.List; + +public record TodayRemindResponseV3( + boolean hasNext, + long totalArticleCount, + long readArticleCount, + long unreadArticleCount, + List articles +) { + public static TodayRemindResponseV3 of( + boolean hasNext, + long totalArticleCount, + long readArticleCount, + long unreadArticleCount, + List articles + ) { + return new TodayRemindResponseV3(hasNext, totalArticleCount, readArticleCount, unreadArticleCount, articles); + } +} diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index dfa5fb6..c0cc196 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -9,6 +9,7 @@ import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; +import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.domain.user.entity.User; public interface GetArticlePort { @@ -27,4 +28,6 @@ public interface GetArticlePort { TodayRemindResponseV2 getRemindArticlesV2(User user, LocalDateTime now, boolean readStatus, PageQuery query); ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId); + + TodayRemindResponseV3 getRemindArticlesV3(User user, LocalDateTime now, boolean readStatus, PageQuery query); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 0b96d00..1cfc5a2 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -21,8 +21,10 @@ import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.RemindArticleResponse; import com.pinback.application.article.dto.response.RemindArticleResponseV2; +import com.pinback.application.article.dto.response.RemindArticleResponseV3; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; +import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.GetArticlePort; import com.pinback.application.article.port.out.ArticleGetServicePort; import com.pinback.application.category.port.in.GetCategoryPort; @@ -164,6 +166,40 @@ public ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId) { return ArticleDetailResponseV3.from(article); } + @Override + public TodayRemindResponseV3 getRemindArticlesV3( + User user, + LocalDateTime now, + boolean readStatus, + PageQuery query + ) { + LocalDateTime endBound = now; + LocalDateTime startBound = now.minusHours(24); + + RemindArticlesWithCountDtoV2 result = articleGetServicePort.findTodayRemindWithCountV2( + user, + startBound, + endBound, + PageRequest.of(query.pageNumber(), query.pageSize()), + readStatus + ); + + List articleResponses = + result.articles() != null ? + result.articles().stream() + .map(RemindArticleResponseV3::from) + .toList() : + Collections.emptyList(); + + return TodayRemindResponseV3.of( + result.hasNext(), + result.totalCount(), + result.readCount(), + result.unreadCount(), + articleResponses + ); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), From 8388a833a5396aa9fd77b873914af526f6cebdcd Mon Sep 17 00:00:00 2001 From: ose0221 Date: Fri, 13 Feb 2026 16:24:16 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EC=9E=90=EC=8B=A0=EC=9D=B4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=9C=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A7=8C=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pinback/api/article/controller/ArticleControllerV3.java | 3 ++- .../pinback/application/article/port/in/GetArticlePort.java | 2 +- .../application/article/usecase/query/GetArticleUsecase.java | 4 ++-- .../infrastructure/article/service/ArticleGetService.java | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 969691b..4e5337f 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -51,9 +51,10 @@ public ResponseDto createArticle( @Operation(summary = "아티클 상세 조회 V3", description = "아티클의 상세 정보를 조회합니다") @GetMapping("/{articleId}") public ResponseDto getArticleDetail( + @Parameter(hidden = true) @CurrentUser User user, @Parameter(description = "아티클 ID") @PathVariable Long articleId ) { - ArticleDetailResponseV3 response = getArticlePort.getArticleDetailWithMetadata(articleId); + ArticleDetailResponseV3 response = getArticlePort.getArticleDetailWithMetadata(user, articleId); return ResponseDto.ok(response); } diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index c0cc196..9d95f89 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -27,7 +27,7 @@ public interface GetArticlePort { TodayRemindResponseV2 getRemindArticlesV2(User user, LocalDateTime now, boolean readStatus, PageQuery query); - ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId); + ArticleDetailResponseV3 getArticleDetailWithMetadata(User user, long articleId); TodayRemindResponseV3 getRemindArticlesV3(User user, LocalDateTime now, boolean readStatus, PageQuery query); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 1cfc5a2..2712b53 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -161,8 +161,8 @@ public TodayRemindResponseV2 getRemindArticlesV2( } @Override - public ArticleDetailResponseV3 getArticleDetailWithMetadata(long articleId) { - Article article = articleGetServicePort.findById(articleId); + public ArticleDetailResponseV3 getArticleDetailWithMetadata(User user, long articleId) { + Article article = articleGetServicePort.findByUserAndId(user, articleId); return ArticleDetailResponseV3.from(article); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index f51963d..2ae35f4 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -13,6 +13,7 @@ import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.port.out.ArticleGetServicePort; import com.pinback.application.common.exception.ArticleNotFoundException; +import com.pinback.application.common.exception.ArticleNotOwnedException; import com.pinback.domain.article.entity.Article; import com.pinback.domain.category.entity.Category; import com.pinback.domain.user.entity.User; @@ -72,7 +73,7 @@ public ArticlesWithUnreadCountDto findUnreadArticles(User user, PageRequest page @Override public Article findByUserAndId(User user, long articleId) { - return articleRepository.findArticleByUserAndId(user, articleId).orElseThrow(ArticleNotFoundException::new); + return articleRepository.findArticleByUserAndId(user, articleId).orElseThrow(ArticleNotOwnedException::new); } @Override From 6ccd326b0217c0da99dca60ff93419b6b8074b05 Mon Sep 17 00:00:00 2001 From: ose0221 Date: Fri, 13 Feb 2026 21:05:46 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=A6=AC=EB=A7=88=EC=9D=B8?= =?UTF-8?q?=EB=93=9C=20=EC=9D=BD=EC=9D=8C/=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 11 ++++++++ .../article/dto/RemindArticleCountDtoV3.java | 8 ++++++ .../response/TodayRemindCountResponse.java | 15 +++++++++++ .../article/port/in/GetArticlePort.java | 3 +++ .../port/out/ArticleGetServicePort.java | 3 +++ .../usecase/query/GetArticleUsecase.java | 20 +++++++++++++++ .../repository/ArticleRepositoryCustom.java | 3 +++ .../ArticleRepositoryCustomImpl.java | 25 +++++++++++++++++++ .../article/service/ArticleGetService.java | 17 +++++++++++++ 9 files changed, 105 insertions(+) create mode 100644 application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 4e5337f..7884e14 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -13,6 +13,7 @@ import com.pinback.api.article.dto.request.ArticleCreateRequest; import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; +import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; import com.pinback.application.article.port.in.GetArticlePort; @@ -72,6 +73,16 @@ public ResponseDto getRemindArticlesV3( return ResponseDto.ok(response); } + @Operation(summary = "리마인드 아티클 읽음/안읽음 개수 조회 v3", description = "오늘 리마인드할 아티클의 읽음/안읽음 개수를 반환합니다.") + @GetMapping("/remind/count") + public ResponseDto getRemindArticlesInfo( + @Parameter(hidden = true) @CurrentUser User user, + @Parameter(description = "현재 시간", example = "2026-02-13T10:00:00") @RequestParam LocalDateTime now + ) { + TodayRemindCountResponse response = getArticlePort.getRemindArticlesInfo(user, now); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java b/application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java new file mode 100644 index 0000000..213da38 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java @@ -0,0 +1,8 @@ +package com.pinback.application.article.dto; + +public record RemindArticleCountDtoV3( + long totalCount, + long readCount, + long unreadCount +) { +} diff --git a/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java b/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java new file mode 100644 index 0000000..0a028b0 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java @@ -0,0 +1,15 @@ +package com.pinback.application.article.dto.response; + +public record TodayRemindCountResponse( + long totalArticleCount, + long readArticleCount, + long unreadArticleCount +) { + public static TodayRemindCountResponse of( + long totalArticleCount, + long readArticleCount, + long unreadArticleCount + ) { + return new TodayRemindCountResponse(totalArticleCount, readArticleCount, unreadArticleCount); + } +} diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 9d95f89..2e4d3fa 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -7,6 +7,7 @@ import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; +import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; import com.pinback.application.article.dto.response.TodayRemindResponseV3; @@ -30,4 +31,6 @@ public interface GetArticlePort { ArticleDetailResponseV3 getArticleDetailWithMetadata(User user, long articleId); TodayRemindResponseV3 getRemindArticlesV3(User user, LocalDateTime now, boolean readStatus, PageQuery query); + + TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime now); } diff --git a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java index 688203f..c792597 100644 --- a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java +++ b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Pageable; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; +import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.domain.article.entity.Article; @@ -39,4 +40,6 @@ RemindArticlesWithCountDto findTodayRemindWithCount(User user, LocalDateTime sta RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2(User user, LocalDateTime startDateTime, LocalDateTime endDateTime, Pageable pageable, Boolean isReadAfterRemind); + + RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 2712b53..53c03d4 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; +import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.dto.query.PageQuery; @@ -22,6 +23,7 @@ import com.pinback.application.article.dto.response.RemindArticleResponse; import com.pinback.application.article.dto.response.RemindArticleResponseV2; import com.pinback.application.article.dto.response.RemindArticleResponseV3; +import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; import com.pinback.application.article.dto.response.TodayRemindResponseV3; @@ -200,6 +202,24 @@ public TodayRemindResponseV3 getRemindArticlesV3( ); } + @Override + public TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime now) { + LocalDateTime endBound = now; + LocalDateTime startBound = now.minusHours(24); + + RemindArticleCountDtoV3 result = articleGetServicePort.findTodayRemindCountV3( + user, + startBound, + endBound + ); + + return TodayRemindCountResponse.of( + result.totalCount(), + result.readCount(), + result.unreadCount() + ); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java index 0344b74..0bcde6e 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.domain.article.entity.Article; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; @@ -28,4 +29,6 @@ RemindArticlesWithCount findTodayRemindWithCount(UUID userId, Pageable pageable, RemindArticlesWithCountV2 findTodayRemindWithCountV2(UUID userId, Pageable pageable, LocalDateTime startAt, LocalDateTime endAt, Boolean isReadAfterRemind); + + RemindArticleCountDtoV3 findTodayRemindCountV3(UUID userId, LocalDateTime startAt, LocalDateTime endAt); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java index 6cd220e..1513d34 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java @@ -14,11 +14,14 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; +import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.domain.article.entity.Article; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -263,4 +266,26 @@ public RemindArticlesWithCountV2 findTodayRemindWithCountV2( PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne) ); } + + @Override + public RemindArticleCountDtoV3 findTodayRemindCountV3( + UUID userId, + LocalDateTime startBound, + LocalDateTime endBound + ) { + BooleanExpression baseConditions = article.user.id.eq(userId) + .and(article.remindAt.gt(startBound).and(article.remindAt.loe(endBound))); + + return queryFactory + .select(Projections.constructor(RemindArticleCountDtoV3.class, + article.count(), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = true THEN 1 ELSE 0 END)", article.isReadAfterRemind).coalesce(0L), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = false THEN 1 ELSE 0 END)", article.isReadAfterRemind).coalesce(0L) + )) + .from(article) + .where(baseConditions) + .fetchOne(); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index 2ae35f4..e051c67 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; +import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.port.out.ArticleGetServicePort; @@ -119,6 +120,22 @@ public RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2( ); } + @Override + public RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, + LocalDateTime endDateTime) { + RemindArticleCountDtoV3 infraResult = articleRepository.findTodayRemindCountV3( + user.getId(), + startDateTime, + endDateTime + ); + return new RemindArticleCountDtoV3( + infraResult.totalCount(), + infraResult.readCount(), + infraResult.unreadCount() + + ); + } + private ArticlesWithUnreadCountDto convertToDto(ArticlesWithUnreadCount infraResult) { return new ArticlesWithUnreadCountDto( infraResult.unReadCount(), From 684d5d3ad3f109a4be55fe2593c4f4a894518657 Mon Sep 17 00:00:00 2001 From: ose0221 Date: Fri, 13 Feb 2026 22:25:19 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=A0=84=EC=B2=B4=20=EC=95=84=ED=8B=B0?= =?UTF-8?q?=ED=81=B4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 14 +++++ .../article/dto/ArticlesWithCountDto.java | 12 +++++ .../dto/response/ArticleResponseV3.java | 30 +++++++++++ .../response/GetAllArticlesResponseV3.java | 17 +++++++ .../article/port/in/GetArticlePort.java | 3 ++ .../port/out/ArticleGetServicePort.java | 3 ++ .../usecase/query/GetArticleUsecase.java | 25 +++++++++ .../exception/InvalidReadStatusException.java | 10 ++++ .../repository/ArticleRepositoryCustom.java | 8 ++- .../ArticleRepositoryCustomImpl.java | 51 +++++++++++++++++-- .../article/repository/dto/ArticleInfoV3.java | 7 +++ .../repository/dto/ArticleWithCountV3.java | 12 +++++ .../repository/dto/RemindArticleCountV3.java | 8 +++ .../article/service/ArticleGetService.java | 19 ++++++- .../shared/constant/ExceptionCode.java | 1 + 15 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 application/src/main/java/com/pinback/application/article/dto/ArticlesWithCountDto.java create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/ArticleResponseV3.java create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/GetAllArticlesResponseV3.java create mode 100644 application/src/main/java/com/pinback/application/common/exception/InvalidReadStatusException.java create mode 100644 infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleInfoV3.java create mode 100644 infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleWithCountV3.java create mode 100644 infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 7884e14..8587982 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -13,6 +13,7 @@ import com.pinback.api.article.dto.request.ArticleCreateRequest; import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; +import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; @@ -83,6 +84,19 @@ public ResponseDto getRemindArticlesInfo( return ResponseDto.ok(response); } + @Operation(summary = "나의 북마크 전체 아티클 조회 V3", description = "사용자의 모든 아티클을 페이징으로 조회합니다.") + @GetMapping + public ResponseDto getAllArticles( + @Parameter(hidden = true) @CurrentUser User user, + @Parameter(description = "읽음 상태 (생략: 읽음, false: 안읽음)", example = "false") @RequestParam(name = "read-status", required = false) Boolean readStatus, + @Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page, + @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "8") int size + ) { + PageQuery query = new PageQuery(page, size); + GetAllArticlesResponseV3 response = getArticlePort.getAllArticlesV3(user, readStatus, query); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/dto/ArticlesWithCountDto.java b/application/src/main/java/com/pinback/application/article/dto/ArticlesWithCountDto.java new file mode 100644 index 0000000..805d042 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/ArticlesWithCountDto.java @@ -0,0 +1,12 @@ +package com.pinback.application.article.dto; + +import org.springframework.data.domain.Page; + +import com.pinback.domain.article.entity.Article; + +public record ArticlesWithCountDto( + long totalCount, + long unreadCount, + Page
article +) { +} diff --git a/application/src/main/java/com/pinback/application/article/dto/response/ArticleResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/ArticleResponseV3.java new file mode 100644 index 0000000..6024032 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/ArticleResponseV3.java @@ -0,0 +1,30 @@ +package com.pinback.application.article.dto.response; + +import java.time.LocalDateTime; + +import com.pinback.application.category.dto.response.CategoryResponse; +import com.pinback.domain.article.entity.Article; + +public record ArticleResponseV3( + long articleId, + String url, + String title, + String thumbnailUrl, + String memo, + LocalDateTime createdAt, + boolean isRead, + CategoryResponse category +) { + public static ArticleResponseV3 from(Article article) { + return new ArticleResponseV3( + article.getId(), + article.getUrl(), + article.getTitle(), + article.getThumbnail(), + article.getMemo(), + article.getCreatedAt(), + article.isRead(), + CategoryResponse.from(article.getCategory()) + ); + } +} diff --git a/application/src/main/java/com/pinback/application/article/dto/response/GetAllArticlesResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/GetAllArticlesResponseV3.java new file mode 100644 index 0000000..fa027cf --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/GetAllArticlesResponseV3.java @@ -0,0 +1,17 @@ +package com.pinback.application.article.dto.response; + +import java.util.List; + +public record GetAllArticlesResponseV3( + long totalArticleCount, + long unreadArticleCount, + List articles +) { + public static GetAllArticlesResponseV3 of( + long totalArticleCount, + long unreadArticleCount, + List articles + ) { + return new GetAllArticlesResponseV3(totalArticleCount, unreadArticleCount, articles); + } +} diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 2e4d3fa..397144e 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -7,6 +7,7 @@ import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; +import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; @@ -33,4 +34,6 @@ public interface GetArticlePort { TodayRemindResponseV3 getRemindArticlesV3(User user, LocalDateTime now, boolean readStatus, PageQuery query); TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime now); + + GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, PageQuery query); } diff --git a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java index c792597..f984ccc 100644 --- a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java +++ b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; @@ -42,4 +43,6 @@ RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2(User user, LocalDateTime LocalDateTime endDateTime, Pageable pageable, Boolean isReadAfterRemind); RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime); + + ArticlesWithCountDto findAllByReadStatus(User user, Boolean readStatus, PageRequest pageRequest); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 53c03d4..83cc34c 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; @@ -18,8 +19,10 @@ import com.pinback.application.article.dto.response.ArticleDetailResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticleResponse; +import com.pinback.application.article.dto.response.ArticleResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; +import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.RemindArticleResponse; import com.pinback.application.article.dto.response.RemindArticleResponseV2; import com.pinback.application.article.dto.response.RemindArticleResponseV3; @@ -30,6 +33,7 @@ import com.pinback.application.article.port.in.GetArticlePort; import com.pinback.application.article.port.out.ArticleGetServicePort; import com.pinback.application.category.port.in.GetCategoryPort; +import com.pinback.application.common.exception.InvalidReadStatusException; import com.pinback.domain.article.entity.Article; import com.pinback.domain.category.entity.Category; import com.pinback.domain.user.entity.User; @@ -220,6 +224,27 @@ public TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime n ); } + @Override + public GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, PageQuery query) { + if (readStatus != null && readStatus) { + throw new InvalidReadStatusException(); + + } + + ArticlesWithCountDto result = articleGetServicePort.findAllByReadStatus( + user, readStatus, PageRequest.of(query.pageNumber(), query.pageSize())); + + List articleResponses = result.article().stream() + .map(ArticleResponseV3::from) + .toList(); + + return GetAllArticlesResponseV3.of( + result.totalCount(), + result.unreadCount(), + articleResponses + ); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), diff --git a/application/src/main/java/com/pinback/application/common/exception/InvalidReadStatusException.java b/application/src/main/java/com/pinback/application/common/exception/InvalidReadStatusException.java new file mode 100644 index 0000000..673f84a --- /dev/null +++ b/application/src/main/java/com/pinback/application/common/exception/InvalidReadStatusException.java @@ -0,0 +1,10 @@ +package com.pinback.application.common.exception; + +import com.pinback.shared.constant.ExceptionCode; +import com.pinback.shared.exception.ApplicationException; + +public class InvalidReadStatusException extends ApplicationException { + public InvalidReadStatusException() { + super(ExceptionCode.INVALID_READSTATUS); + } +} diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java index 0bcde6e..54abbca 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java @@ -4,11 +4,13 @@ import java.util.UUID; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.domain.article.entity.Article; +import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; +import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; @@ -30,5 +32,7 @@ RemindArticlesWithCount findTodayRemindWithCount(UUID userId, Pageable pageable, RemindArticlesWithCountV2 findTodayRemindWithCountV2(UUID userId, Pageable pageable, LocalDateTime startAt, LocalDateTime endAt, Boolean isReadAfterRemind); - RemindArticleCountDtoV3 findTodayRemindCountV3(UUID userId, LocalDateTime startAt, LocalDateTime endAt); + RemindArticleCountV3 findTodayRemindCountV3(UUID userId, LocalDateTime startAt, LocalDateTime endAt); + + ArticleWithCountV3 findAllByReadStatus(UUID userId, Boolean readStatus, PageRequest pageRequest); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java index 1513d34..e5c1334 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java @@ -10,13 +10,16 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; -import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.domain.article.entity.Article; +import com.pinback.infrastructure.article.repository.dto.ArticleInfoV3; +import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; +import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; import com.querydsl.core.types.Projections; @@ -268,7 +271,7 @@ public RemindArticlesWithCountV2 findTodayRemindWithCountV2( } @Override - public RemindArticleCountDtoV3 findTodayRemindCountV3( + public RemindArticleCountV3 findTodayRemindCountV3( UUID userId, LocalDateTime startBound, LocalDateTime endBound @@ -277,7 +280,7 @@ public RemindArticleCountDtoV3 findTodayRemindCountV3( .and(article.remindAt.gt(startBound).and(article.remindAt.loe(endBound))); return queryFactory - .select(Projections.constructor(RemindArticleCountDtoV3.class, + .select(Projections.constructor(RemindArticleCountV3.class, article.count(), Expressions.numberTemplate(Long.class, "SUM(CASE WHEN {0} = true THEN 1 ELSE 0 END)", article.isReadAfterRemind).coalesce(0L), @@ -288,4 +291,46 @@ public RemindArticleCountDtoV3 findTodayRemindCountV3( .where(baseConditions) .fetchOne(); } + + @Override + public ArticleWithCountV3 findAllByReadStatus(UUID userId, Boolean readStatus, PageRequest pageRequest) { + BooleanExpression baseConditions = article.user.id.eq(userId); + + ArticleInfoV3 counts = queryFactory + .select(Projections.constructor(ArticleInfoV3.class, + article.count(), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = false THEN 1 ELSE 0 END)", article.isRead).coalesce(0L) + )) + .from(article) + .where(baseConditions) + .fetchOne(); + + BooleanExpression listConditions = (readStatus == null) + ? baseConditions + : baseConditions.and(article.isRead.isFalse()); + + List
articles = queryFactory + .selectFrom(article) + .join(article.user, user).fetchJoin() + .where(listConditions) + .offset(pageRequest.getOffset()) + .limit(pageRequest.getPageSize()) + .orderBy(article.createdAt.desc()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(article.count()) + .from(article) + .where(listConditions); + + ArticleInfoV3 safeCounts = (counts != null) ? counts : new ArticleInfoV3(0L, 0L); + + return new ArticleWithCountV3( + safeCounts.totalCount(), + safeCounts.unreadCount(), + PageableExecutionUtils.getPage(articles, pageRequest, countQuery::fetchOne) + ); + } + } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleInfoV3.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleInfoV3.java new file mode 100644 index 0000000..cbea9c7 --- /dev/null +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleInfoV3.java @@ -0,0 +1,7 @@ +package com.pinback.infrastructure.article.repository.dto; + +public record ArticleInfoV3( + Long totalCount, + Long unreadCount +) { +} diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleWithCountV3.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleWithCountV3.java new file mode 100644 index 0000000..b0414b2 --- /dev/null +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleWithCountV3.java @@ -0,0 +1,12 @@ +package com.pinback.infrastructure.article.repository.dto; + +import org.springframework.data.domain.Page; + +import com.pinback.domain.article.entity.Article; + +public record ArticleWithCountV3( + Long totalCount, + Long unreadCount, + Page
article +) { +} diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java new file mode 100644 index 0000000..f20c5ac --- /dev/null +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java @@ -0,0 +1,8 @@ +package com.pinback.infrastructure.article.repository.dto; + +public record RemindArticleCountV3( + long totalCount, + long readCount, + long unreadCount +) { +} diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index e051c67..c6d8680 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; @@ -19,7 +20,9 @@ import com.pinback.domain.category.entity.Category; import com.pinback.domain.user.entity.User; import com.pinback.infrastructure.article.repository.ArticleRepository; +import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; +import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; @@ -123,7 +126,7 @@ public RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2( @Override public RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime) { - RemindArticleCountDtoV3 infraResult = articleRepository.findTodayRemindCountV3( + RemindArticleCountV3 infraResult = articleRepository.findTodayRemindCountV3( user.getId(), startDateTime, endDateTime @@ -136,6 +139,20 @@ public RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime s ); } + @Override + public ArticlesWithCountDto findAllByReadStatus(User user, Boolean readStatus, PageRequest pageRequest) { + ArticleWithCountV3 infraResult = articleRepository.findAllByReadStatus( + user.getId(), + readStatus, + pageRequest + ); + return new ArticlesWithCountDto( + infraResult.totalCount(), + infraResult.unreadCount(), + infraResult.article() + ); + } + private ArticlesWithUnreadCountDto convertToDto(ArticlesWithUnreadCount infraResult) { return new ArticlesWithUnreadCountDto( infraResult.unReadCount(), diff --git a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java index b65d1b7..9daf8f3 100644 --- a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java +++ b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java @@ -14,6 +14,7 @@ public enum ExceptionCode { CATEGORY_NAME_INVALID(HttpStatus.BAD_REQUEST, "c40003", "카테고리 이름은 공백을 포함할 수 없습니다."), INVALID_FCM_TOKEN(HttpStatus.BAD_REQUEST, "c40004", "유효하지 않은 FCM 토큰입니다."), INVALID_URL(HttpStatus.BAD_REQUEST, "c40005", "유효하지 않은 URL이거나 접속할 수 없는 사이트입니다."), + INVALID_READSTATUS(HttpStatus.BAD_REQUEST, "c40006", "잘못된 상태 값입니다."), //401 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "c40101", "유효하지 않은 토큰입니다."), From 4ec096d907fb607bc6938a026d7ef63151a488b6 Mon Sep 17 00:00:00 2001 From: ose0221 Date: Sat, 14 Feb 2026 16:01:48 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=A0=84=EC=B2=B4=20=EC=95=84=ED=8B=B0?= =?UTF-8?q?=ED=81=B4=20=EC=A0=84=EC=B2=B4/=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 15 ++++++++--- ...tDtoV3.java => ArticleCountInfoDtoV3.java} | 2 +- ...nse.java => ArticleCountInfoResponse.java} | 6 ++--- .../article/port/in/GetArticlePort.java | 6 +++-- .../port/out/ArticleGetServicePort.java | 6 +++-- .../usecase/query/GetArticleUsecase.java | 22 ++++++++++----- .../repository/ArticleRepositoryCustom.java | 6 +++-- .../ArticleRepositoryCustomImpl.java | 27 ++++++++++++++++--- ...leCountV3.java => ArticleCountInfoV3.java} | 2 +- .../article/service/ArticleGetService.java | 22 +++++++++++---- 10 files changed, 86 insertions(+), 28 deletions(-) rename application/src/main/java/com/pinback/application/article/dto/{RemindArticleCountDtoV3.java => ArticleCountInfoDtoV3.java} (72%) rename application/src/main/java/com/pinback/application/article/dto/response/{TodayRemindCountResponse.java => ArticleCountInfoResponse.java} (63%) rename infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/{RemindArticleCountV3.java => ArticleCountInfoV3.java} (76%) diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 8587982..239ba54 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -12,9 +12,9 @@ import com.pinback.api.article.dto.request.ArticleCreateRequest; import com.pinback.application.article.dto.query.PageQuery; +import com.pinback.application.article.dto.response.ArticleCountInfoResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; -import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; import com.pinback.application.article.port.in.GetArticlePort; @@ -76,11 +76,11 @@ public ResponseDto getRemindArticlesV3( @Operation(summary = "리마인드 아티클 읽음/안읽음 개수 조회 v3", description = "오늘 리마인드할 아티클의 읽음/안읽음 개수를 반환합니다.") @GetMapping("/remind/count") - public ResponseDto getRemindArticlesInfo( + public ResponseDto getRemindArticlesInfo( @Parameter(hidden = true) @CurrentUser User user, @Parameter(description = "현재 시간", example = "2026-02-13T10:00:00") @RequestParam LocalDateTime now ) { - TodayRemindCountResponse response = getArticlePort.getRemindArticlesInfo(user, now); + ArticleCountInfoResponse response = getArticlePort.getRemindArticlesInfo(user, now); return ResponseDto.ok(response); } @@ -97,6 +97,15 @@ public ResponseDto getAllArticles( return ResponseDto.ok(response); } + @Operation(summary = "나의 북마크 아티클 전체보기/안읽음 개수 조회 v3", description = "나의 북마크 아티클의 전체보기/안읽음 개수를 반환합니다.") + @GetMapping("/count") + public ResponseDto getArticlesInfo( + @Parameter(hidden = true) @CurrentUser User user + ) { + ArticleCountInfoResponse response = getArticlePort.getAllArticlesInfo(user); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java b/application/src/main/java/com/pinback/application/article/dto/ArticleCountInfoDtoV3.java similarity index 72% rename from application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java rename to application/src/main/java/com/pinback/application/article/dto/ArticleCountInfoDtoV3.java index 213da38..1900cec 100644 --- a/application/src/main/java/com/pinback/application/article/dto/RemindArticleCountDtoV3.java +++ b/application/src/main/java/com/pinback/application/article/dto/ArticleCountInfoDtoV3.java @@ -1,6 +1,6 @@ package com.pinback.application.article.dto; -public record RemindArticleCountDtoV3( +public record ArticleCountInfoDtoV3( long totalCount, long readCount, long unreadCount diff --git a/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java b/application/src/main/java/com/pinback/application/article/dto/response/ArticleCountInfoResponse.java similarity index 63% rename from application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java rename to application/src/main/java/com/pinback/application/article/dto/response/ArticleCountInfoResponse.java index 0a028b0..61a8153 100644 --- a/application/src/main/java/com/pinback/application/article/dto/response/TodayRemindCountResponse.java +++ b/application/src/main/java/com/pinback/application/article/dto/response/ArticleCountInfoResponse.java @@ -1,15 +1,15 @@ package com.pinback.application.article.dto.response; -public record TodayRemindCountResponse( +public record ArticleCountInfoResponse( long totalArticleCount, long readArticleCount, long unreadArticleCount ) { - public static TodayRemindCountResponse of( + public static ArticleCountInfoResponse of( long totalArticleCount, long readArticleCount, long unreadArticleCount ) { - return new TodayRemindCountResponse(totalArticleCount, readArticleCount, unreadArticleCount); + return new ArticleCountInfoResponse(totalArticleCount, readArticleCount, unreadArticleCount); } } diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 397144e..2f062b1 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -3,12 +3,12 @@ import java.time.LocalDateTime; import com.pinback.application.article.dto.query.PageQuery; +import com.pinback.application.article.dto.response.ArticleCountInfoResponse; import com.pinback.application.article.dto.response.ArticleDetailResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; -import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; import com.pinback.application.article.dto.response.TodayRemindResponseV3; @@ -33,7 +33,9 @@ public interface GetArticlePort { TodayRemindResponseV3 getRemindArticlesV3(User user, LocalDateTime now, boolean readStatus, PageQuery query); - TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime now); + ArticleCountInfoResponse getRemindArticlesInfo(User user, LocalDateTime now); GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, PageQuery query); + + ArticleCountInfoResponse getAllArticlesInfo(User user); } diff --git a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java index f984ccc..384c05c 100644 --- a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java +++ b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java @@ -7,9 +7,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import com.pinback.application.article.dto.ArticleCountInfoDtoV3; import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; -import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.domain.article.entity.Article; @@ -42,7 +42,9 @@ RemindArticlesWithCountDto findTodayRemindWithCount(User user, LocalDateTime sta RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2(User user, LocalDateTime startDateTime, LocalDateTime endDateTime, Pageable pageable, Boolean isReadAfterRemind); - RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime); + ArticleCountInfoDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime); ArticlesWithCountDto findAllByReadStatus(User user, Boolean readStatus, PageRequest pageRequest); + + ArticleCountInfoDtoV3 findAllCountV3(User user); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 83cc34c..a2d55d3 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -10,12 +10,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.pinback.application.article.dto.ArticleCountInfoDtoV3; import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; -import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.dto.query.PageQuery; +import com.pinback.application.article.dto.response.ArticleCountInfoResponse; import com.pinback.application.article.dto.response.ArticleDetailResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticleResponse; @@ -26,7 +27,6 @@ import com.pinback.application.article.dto.response.RemindArticleResponse; import com.pinback.application.article.dto.response.RemindArticleResponseV2; import com.pinback.application.article.dto.response.RemindArticleResponseV3; -import com.pinback.application.article.dto.response.TodayRemindCountResponse; import com.pinback.application.article.dto.response.TodayRemindResponse; import com.pinback.application.article.dto.response.TodayRemindResponseV2; import com.pinback.application.article.dto.response.TodayRemindResponseV3; @@ -207,17 +207,17 @@ public TodayRemindResponseV3 getRemindArticlesV3( } @Override - public TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime now) { + public ArticleCountInfoResponse getRemindArticlesInfo(User user, LocalDateTime now) { LocalDateTime endBound = now; LocalDateTime startBound = now.minusHours(24); - RemindArticleCountDtoV3 result = articleGetServicePort.findTodayRemindCountV3( + ArticleCountInfoDtoV3 result = articleGetServicePort.findTodayRemindCountV3( user, startBound, endBound ); - return TodayRemindCountResponse.of( + return ArticleCountInfoResponse.of( result.totalCount(), result.readCount(), result.unreadCount() @@ -228,7 +228,6 @@ public TodayRemindCountResponse getRemindArticlesInfo(User user, LocalDateTime n public GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, PageQuery query) { if (readStatus != null && readStatus) { throw new InvalidReadStatusException(); - } ArticlesWithCountDto result = articleGetServicePort.findAllByReadStatus( @@ -245,6 +244,17 @@ public GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, ); } + @Override + public ArticleCountInfoResponse getAllArticlesInfo(User user) { + ArticleCountInfoDtoV3 result = articleGetServicePort.findAllCountV3(user); + + return ArticleCountInfoResponse.of( + result.totalCount(), + result.readCount(), + result.unreadCount() + ); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java index 54abbca..e8cbd5e 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java @@ -8,9 +8,9 @@ import org.springframework.data.domain.Pageable; import com.pinback.domain.article.entity.Article; +import com.pinback.infrastructure.article.repository.dto.ArticleCountInfoV3; import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; -import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; @@ -32,7 +32,9 @@ RemindArticlesWithCount findTodayRemindWithCount(UUID userId, Pageable pageable, RemindArticlesWithCountV2 findTodayRemindWithCountV2(UUID userId, Pageable pageable, LocalDateTime startAt, LocalDateTime endAt, Boolean isReadAfterRemind); - RemindArticleCountV3 findTodayRemindCountV3(UUID userId, LocalDateTime startAt, LocalDateTime endAt); + ArticleCountInfoV3 findTodayRemindCountV3(UUID userId, LocalDateTime startAt, LocalDateTime endAt); ArticleWithCountV3 findAllByReadStatus(UUID userId, Boolean readStatus, PageRequest pageRequest); + + ArticleCountInfoV3 findAllCountV3(UUID userId); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java index e5c1334..5ec8b72 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; @@ -16,10 +17,10 @@ import org.springframework.stereotype.Repository; import com.pinback.domain.article.entity.Article; +import com.pinback.infrastructure.article.repository.dto.ArticleCountInfoV3; import com.pinback.infrastructure.article.repository.dto.ArticleInfoV3; import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; -import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; import com.querydsl.core.types.Projections; @@ -271,7 +272,7 @@ public RemindArticlesWithCountV2 findTodayRemindWithCountV2( } @Override - public RemindArticleCountV3 findTodayRemindCountV3( + public ArticleCountInfoV3 findTodayRemindCountV3( UUID userId, LocalDateTime startBound, LocalDateTime endBound @@ -280,7 +281,7 @@ public RemindArticleCountV3 findTodayRemindCountV3( .and(article.remindAt.gt(startBound).and(article.remindAt.loe(endBound))); return queryFactory - .select(Projections.constructor(RemindArticleCountV3.class, + .select(Projections.constructor(ArticleCountInfoV3.class, article.count(), Expressions.numberTemplate(Long.class, "SUM(CASE WHEN {0} = true THEN 1 ELSE 0 END)", article.isReadAfterRemind).coalesce(0L), @@ -333,4 +334,24 @@ public ArticleWithCountV3 findAllByReadStatus(UUID userId, Boolean readStatus, P ); } + @Override + public ArticleCountInfoV3 findAllCountV3(UUID userId) { + BooleanExpression baseConditions = article.user.id.eq(userId); + + // 쿼리 실행 + ArticleCountInfoV3 result = queryFactory + .select(Projections.constructor(ArticleCountInfoV3.class, + article.count(), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = true THEN 1 ELSE 0 END)", article.isRead).coalesce(0L), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = false THEN 1 ELSE 0 END)", article.isRead).coalesce(0L) + )) + .from(article) + .where(baseConditions) + .fetchOne(); + + return Optional.ofNullable(result) + .orElseGet(() -> new ArticleCountInfoV3(0L, 0L, 0L)); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleCountInfoV3.java similarity index 76% rename from infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java rename to infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleCountInfoV3.java index f20c5ac..9de6985 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/RemindArticleCountV3.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticleCountInfoV3.java @@ -1,6 +1,6 @@ package com.pinback.infrastructure.article.repository.dto; -public record RemindArticleCountV3( +public record ArticleCountInfoV3( long totalCount, long readCount, long unreadCount diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index c6d8680..8b7b70c 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -8,9 +8,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import com.pinback.application.article.dto.ArticleCountInfoDtoV3; import com.pinback.application.article.dto.ArticlesWithCountDto; import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; -import com.pinback.application.article.dto.RemindArticleCountDtoV3; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDtoV2; import com.pinback.application.article.port.out.ArticleGetServicePort; @@ -20,9 +20,9 @@ import com.pinback.domain.category.entity.Category; import com.pinback.domain.user.entity.User; import com.pinback.infrastructure.article.repository.ArticleRepository; +import com.pinback.infrastructure.article.repository.dto.ArticleCountInfoV3; import com.pinback.infrastructure.article.repository.dto.ArticleWithCountV3; import com.pinback.infrastructure.article.repository.dto.ArticlesWithUnreadCount; -import com.pinback.infrastructure.article.repository.dto.RemindArticleCountV3; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCount; import com.pinback.infrastructure.article.repository.dto.RemindArticlesWithCountV2; @@ -124,14 +124,14 @@ public RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2( } @Override - public RemindArticleCountDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, + public ArticleCountInfoDtoV3 findTodayRemindCountV3(User user, LocalDateTime startDateTime, LocalDateTime endDateTime) { - RemindArticleCountV3 infraResult = articleRepository.findTodayRemindCountV3( + ArticleCountInfoV3 infraResult = articleRepository.findTodayRemindCountV3( user.getId(), startDateTime, endDateTime ); - return new RemindArticleCountDtoV3( + return new ArticleCountInfoDtoV3( infraResult.totalCount(), infraResult.readCount(), infraResult.unreadCount() @@ -153,6 +153,18 @@ public ArticlesWithCountDto findAllByReadStatus(User user, Boolean readStatus, P ); } + @Override + public ArticleCountInfoDtoV3 findAllCountV3(User user) { + ArticleCountInfoV3 infraResult = articleRepository.findAllCountV3(user.getId()); + return new ArticleCountInfoDtoV3( + infraResult.totalCount(), + infraResult.readCount(), + infraResult.unreadCount() + + ); + + } + private ArticlesWithUnreadCountDto convertToDto(ArticlesWithUnreadCount infraResult) { return new ArticlesWithUnreadCountDto( infraResult.unReadCount(), From c1d128df3d34316c146064ac75d7032208ffb3d9 Mon Sep 17 00:00:00 2001 From: ose0221 Date: Sat, 14 Feb 2026 16:45:23 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EB=B3=84=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 18 ++++++- .../dto/response/ArticlesPageResponseV3.java | 15 ++++++ .../response/CategoryArticleResponseV3.java | 27 +++++++++++ .../article/port/in/GetArticlePort.java | 3 ++ .../port/out/ArticleGetServicePort.java | 3 ++ .../usecase/query/GetArticleUsecase.java | 25 ++++++++++ .../repository/ArticleRepositoryCustom.java | 3 ++ .../ArticleRepositoryCustomImpl.java | 47 +++++++++++++++++++ .../article/service/ArticleGetService.java | 19 ++++++++ .../shared/constant/ExceptionCode.java | 2 +- 10 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/ArticlesPageResponseV3.java create mode 100644 application/src/main/java/com/pinback/application/article/dto/response/CategoryArticleResponseV3.java diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 239ba54..7817785 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -14,6 +14,7 @@ import com.pinback.application.article.dto.query.PageQuery; import com.pinback.application.article.dto.response.ArticleCountInfoResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; +import com.pinback.application.article.dto.response.ArticlesPageResponseV3; import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.TodayRemindResponseV3; import com.pinback.application.article.port.in.CreateArticlePort; @@ -88,7 +89,7 @@ public ResponseDto getRemindArticlesInfo( @GetMapping public ResponseDto getAllArticles( @Parameter(hidden = true) @CurrentUser User user, - @Parameter(description = "읽음 상태 (생략: 읽음, false: 안읽음)", example = "false") @RequestParam(name = "read-status", required = false) Boolean readStatus, + @Parameter(description = "읽음 상태 (생략: 전체보기, false: 안읽음)", example = "false") @RequestParam(name = "read-status", required = false) Boolean readStatus, @Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page, @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "8") int size ) { @@ -106,6 +107,21 @@ public ResponseDto getArticlesInfo( return ResponseDto.ok(response); } + @Operation(summary = "나의 북마크 카테고리별 아티클 조회 V3", description = "특정 카테고리의 아티클 전체 목록을 조회합니다.") + @GetMapping("/category") + public ResponseDto getAllArticlesByCategory( + @Parameter(hidden = true) @CurrentUser User user, + @Parameter(description = "카테고리 ID") @RequestParam(name = "category-id") Long categoryId, + @Parameter(description = "읽음 상태 (생략시: 전체보기, false: 안읽음)", example = "false") @RequestParam(name = "read-status", required = false) Boolean readStatus, + @Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page, + @Parameter(description = "페이지 크기") @RequestParam(defaultValue = "8") int size + ) { + PageQuery query = new PageQuery(page, size); + ArticlesPageResponseV3 response = getArticlePort.getAllArticlesByCategoryV3(user, categoryId, readStatus, + query); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/dto/response/ArticlesPageResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/ArticlesPageResponseV3.java new file mode 100644 index 0000000..e123a5e --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/ArticlesPageResponseV3.java @@ -0,0 +1,15 @@ +package com.pinback.application.article.dto.response; + +import java.util.List; + +public record ArticlesPageResponseV3( + long totalArticleCount, + long unreadArticleCount, + String categoryName, + List articles +) { + public static ArticlesPageResponseV3 of(long totalArticleCount, long unreadArticleCount, String categoryName, + List articles) { + return new ArticlesPageResponseV3(totalArticleCount, unreadArticleCount, categoryName, articles); + } +} diff --git a/application/src/main/java/com/pinback/application/article/dto/response/CategoryArticleResponseV3.java b/application/src/main/java/com/pinback/application/article/dto/response/CategoryArticleResponseV3.java new file mode 100644 index 0000000..1480417 --- /dev/null +++ b/application/src/main/java/com/pinback/application/article/dto/response/CategoryArticleResponseV3.java @@ -0,0 +1,27 @@ +package com.pinback.application.article.dto.response; + +import java.time.LocalDateTime; + +import com.pinback.domain.article.entity.Article; + +public record CategoryArticleResponseV3( + long articleId, + String url, + String title, + String thumbnailUrl, + String memo, + LocalDateTime createdAt, + boolean isRead +) { + public static CategoryArticleResponseV3 from(Article article) { + return new CategoryArticleResponseV3( + article.getId(), + article.getUrl(), + article.getTitle(), + article.getThumbnail(), + article.getMemo(), + article.getCreatedAt(), + article.isRead() + ); + } +} diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 2f062b1..444e53d 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -7,6 +7,7 @@ import com.pinback.application.article.dto.response.ArticleDetailResponse; import com.pinback.application.article.dto.response.ArticleDetailResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; +import com.pinback.application.article.dto.response.ArticlesPageResponseV3; import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.TodayRemindResponse; @@ -38,4 +39,6 @@ public interface GetArticlePort { GetAllArticlesResponseV3 getAllArticlesV3(User user, Boolean readStatus, PageQuery query); ArticleCountInfoResponse getAllArticlesInfo(User user); + + ArticlesPageResponseV3 getAllArticlesByCategoryV3(User user, long categoryId, Boolean readStatus, PageQuery query); } diff --git a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java index 384c05c..b99d62b 100644 --- a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java +++ b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java @@ -47,4 +47,7 @@ RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2(User user, LocalDateTime ArticlesWithCountDto findAllByReadStatus(User user, Boolean readStatus, PageRequest pageRequest); ArticleCountInfoDtoV3 findAllCountV3(User user); + + ArticlesWithCountDto findAllByCategoryAndReadStatus(User user, Category category, Boolean readStatus, + PageRequest pageRequest); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index a2d55d3..7a67461 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -22,6 +22,8 @@ import com.pinback.application.article.dto.response.ArticleResponse; import com.pinback.application.article.dto.response.ArticleResponseV3; import com.pinback.application.article.dto.response.ArticlesPageResponse; +import com.pinback.application.article.dto.response.ArticlesPageResponseV3; +import com.pinback.application.article.dto.response.CategoryArticleResponseV3; import com.pinback.application.article.dto.response.GetAllArticlesResponse; import com.pinback.application.article.dto.response.GetAllArticlesResponseV3; import com.pinback.application.article.dto.response.RemindArticleResponse; @@ -255,6 +257,29 @@ public ArticleCountInfoResponse getAllArticlesInfo(User user) { ); } + @Override + public ArticlesPageResponseV3 getAllArticlesByCategoryV3(User user, long categoryId, Boolean readStatus, + PageQuery query) { + if (readStatus != null && readStatus) { + throw new InvalidReadStatusException(); + } + Category category = getCategoryPort.getCategoryAndUser(categoryId, user); + + ArticlesWithCountDto result = articleGetServicePort.findAllByCategoryAndReadStatus( + user, category, readStatus, PageRequest.of(query.pageNumber(), query.pageSize())); + + List articleResponses = result.article().stream() + .map(CategoryArticleResponseV3::from) + .toList(); + + return ArticlesPageResponseV3.of( + result.totalCount(), + result.unreadCount(), + category.getName(), + articleResponses + ); + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java index e8cbd5e..2983ffe 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java @@ -37,4 +37,7 @@ RemindArticlesWithCountV2 findTodayRemindWithCountV2(UUID userId, Pageable pagea ArticleWithCountV3 findAllByReadStatus(UUID userId, Boolean readStatus, PageRequest pageRequest); ArticleCountInfoV3 findAllCountV3(UUID userId); + + ArticleWithCountV3 findAllByCategoryAndReadStatus(UUID userId, long categoryId, Boolean readStatus, + PageRequest pageRequest); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java index 5ec8b72..97aff55 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java @@ -354,4 +354,51 @@ public ArticleCountInfoV3 findAllCountV3(UUID userId) { return Optional.ofNullable(result) .orElseGet(() -> new ArticleCountInfoV3(0L, 0L, 0L)); } + + @Override + public ArticleWithCountV3 findAllByCategoryAndReadStatus( + UUID userId, + long categoryId, + Boolean readStatus, + PageRequest pageRequest + ) { + BooleanExpression baseConditions = article.user.id.eq(userId) + .and(article.category.id.eq(categoryId)); + + ArticleInfoV3 counts = queryFactory + .select(Projections.constructor(ArticleInfoV3.class, + article.count(), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = false THEN 1 ELSE 0 END)", article.isRead).coalesce(0L) + )) + .from(article) + .where(baseConditions) + .fetchOne(); + + BooleanExpression listConditions = (readStatus == null) + ? baseConditions + : baseConditions.and(article.isRead.isFalse()); + + List
articles = queryFactory + .selectFrom(article) + .join(article.user, user).fetchJoin() + .where(listConditions) + .offset(pageRequest.getOffset()) + .limit(pageRequest.getPageSize()) + .orderBy(article.createdAt.desc()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(article.count()) + .from(article) + .where(listConditions); + + ArticleInfoV3 safeCounts = (counts != null) ? counts : new ArticleInfoV3(0L, 0L); + + return new ArticleWithCountV3( + safeCounts.totalCount(), + safeCounts.unreadCount(), + PageableExecutionUtils.getPage(articles, pageRequest, countQuery::fetchOne) + ); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index 8b7b70c..b19a8fa 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -162,7 +162,26 @@ public ArticleCountInfoDtoV3 findAllCountV3(User user) { infraResult.unreadCount() ); + } + @Override + public ArticlesWithCountDto findAllByCategoryAndReadStatus( + User user, + Category category, + Boolean readStatus, + PageRequest pageRequest + ) { + ArticleWithCountV3 infraResult = articleRepository.findAllByCategoryAndReadStatus( + user.getId(), + category.getId(), + readStatus, + pageRequest + ); + return new ArticlesWithCountDto( + infraResult.totalCount(), + infraResult.unreadCount(), + infraResult.article() + ); } private ArticlesWithUnreadCountDto convertToDto(ArticlesWithUnreadCount infraResult) { diff --git a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java index 9daf8f3..48d8530 100644 --- a/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java +++ b/shared/src/main/java/com/pinback/shared/constant/ExceptionCode.java @@ -14,7 +14,7 @@ public enum ExceptionCode { CATEGORY_NAME_INVALID(HttpStatus.BAD_REQUEST, "c40003", "카테고리 이름은 공백을 포함할 수 없습니다."), INVALID_FCM_TOKEN(HttpStatus.BAD_REQUEST, "c40004", "유효하지 않은 FCM 토큰입니다."), INVALID_URL(HttpStatus.BAD_REQUEST, "c40005", "유효하지 않은 URL이거나 접속할 수 없는 사이트입니다."), - INVALID_READSTATUS(HttpStatus.BAD_REQUEST, "c40006", "잘못된 상태 값입니다."), + INVALID_READSTATUS(HttpStatus.BAD_REQUEST, "c40006", "잘못된 read-status 상태 값입니다.(전체보기: 생략/안읽음: false)"), //401 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "c40101", "유효하지 않은 토큰입니다."), From 965ca23c71ba780bbdafebd9f6011bea07f6b3f3 Mon Sep 17 00:00:00 2001 From: ose0221 Date: Sat, 14 Feb 2026 16:59:03 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EB=B3=84=20=EC=95=84=ED=8B=B0=ED=81=B4=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0/=EC=95=88=EC=9D=BD=EC=9D=8C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerV3.java | 11 ++++++++++ .../article/port/in/GetArticlePort.java | 2 ++ .../port/out/ArticleGetServicePort.java | 2 ++ .../usecase/query/GetArticleUsecase.java | 14 +++++++++++++ .../repository/ArticleRepositoryCustom.java | 2 ++ .../ArticleRepositoryCustomImpl.java | 21 +++++++++++++++++++ .../article/service/ArticleGetService.java | 10 +++++++++ 7 files changed, 62 insertions(+) diff --git a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java index 7817785..2721bbe 100644 --- a/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java +++ b/api/src/main/java/com/pinback/api/article/controller/ArticleControllerV3.java @@ -122,6 +122,17 @@ public ResponseDto getAllArticlesByCategory( return ResponseDto.ok(response); } + @Operation(summary = "나의 북마크 카테고리별 아티클 전체보기/안읽음 개수 조회 v3", description = "나의 북마크에서 특정 카테고리 아티클의 전체보기/안읽음 개수를 반환합니다.") + @GetMapping("/category/count") + public ResponseDto getArticlesInfoByCategory( + @Parameter(hidden = true) @CurrentUser User user, + @Parameter(description = "카테고리 ID") @RequestParam(name = "category-id") Long categoryId + + ) { + ArticleCountInfoResponse response = getArticlePort.getAllArticlesInfoByCategoryV3(user, categoryId); + return ResponseDto.ok(response); + } + // 기존 아티클 메타데이터 처리 후 삭제 예정 @PostMapping("/metadata") public ResponseDto migrateMetadata() { diff --git a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java index 444e53d..df508c3 100644 --- a/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java +++ b/application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java @@ -41,4 +41,6 @@ public interface GetArticlePort { ArticleCountInfoResponse getAllArticlesInfo(User user); ArticlesPageResponseV3 getAllArticlesByCategoryV3(User user, long categoryId, Boolean readStatus, PageQuery query); + + ArticleCountInfoResponse getAllArticlesInfoByCategoryV3(User user, long categoryId); } diff --git a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java index b99d62b..d7bbf0c 100644 --- a/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java +++ b/application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java @@ -50,4 +50,6 @@ RemindArticlesWithCountDtoV2 findTodayRemindWithCountV2(User user, LocalDateTime ArticlesWithCountDto findAllByCategoryAndReadStatus(User user, Category category, Boolean readStatus, PageRequest pageRequest); + + ArticleCountInfoDtoV3 findAllCountByCategoryV3(User user, Category category); } diff --git a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java index 7a67461..2bff2fe 100644 --- a/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java +++ b/application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java @@ -280,6 +280,20 @@ public ArticlesPageResponseV3 getAllArticlesByCategoryV3(User user, long categor ); } + @Override + public ArticleCountInfoResponse getAllArticlesInfoByCategoryV3(User user, long categoryId) { + Category category = getCategoryPort.getCategoryAndUser(categoryId, user); + + ArticleCountInfoDtoV3 result = articleGetServicePort.findAllCountByCategoryV3(user, category); + + return ArticleCountInfoResponse.of( + result.totalCount(), + result.readCount(), + result.unreadCount() + ); + + } + private LocalDateTime getRemindDateTime(LocalDateTime now, LocalTime remindDefault) { return LocalDateTime.of( now.getYear(), diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java index 2983ffe..d3289b3 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java @@ -40,4 +40,6 @@ RemindArticlesWithCountV2 findTodayRemindWithCountV2(UUID userId, Pageable pagea ArticleWithCountV3 findAllByCategoryAndReadStatus(UUID userId, long categoryId, Boolean readStatus, PageRequest pageRequest); + + ArticleCountInfoV3 findAllCountByCategoryV3(UUID userId, long categoryId); } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java index 97aff55..850d19f 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java @@ -401,4 +401,25 @@ public ArticleWithCountV3 findAllByCategoryAndReadStatus( PageableExecutionUtils.getPage(articles, pageRequest, countQuery::fetchOne) ); } + + @Override + public ArticleCountInfoV3 findAllCountByCategoryV3(UUID userId, long categoryId) { + BooleanExpression baseConditions = article.user.id.eq(userId) + .and(article.category.id.eq(categoryId)); + + ArticleCountInfoV3 result = queryFactory + .select(Projections.constructor(ArticleCountInfoV3.class, + article.count(), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = true THEN 1 ELSE 0 END)", article.isRead).coalesce(0L), + Expressions.numberTemplate(Long.class, + "SUM(CASE WHEN {0} = false THEN 1 ELSE 0 END)", article.isRead).coalesce(0L) + )) + .from(article) + .where(baseConditions) + .fetchOne(); + + return Optional.ofNullable(result) + .orElseGet(() -> new ArticleCountInfoV3(0L, 0L, 0L)); + } } diff --git a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java index b19a8fa..6113a4c 100644 --- a/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java +++ b/infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java @@ -184,6 +184,16 @@ public ArticlesWithCountDto findAllByCategoryAndReadStatus( ); } + @Override + public ArticleCountInfoDtoV3 findAllCountByCategoryV3(User user, Category category) { + ArticleCountInfoV3 infraResult = articleRepository.findAllCountByCategoryV3(user.getId(), category.getId()); + return new ArticleCountInfoDtoV3( + infraResult.totalCount(), + infraResult.readCount(), + infraResult.unreadCount() + ); + } + private ArticlesWithUnreadCountDto convertToDto(ArticlesWithUnreadCount infraResult) { return new ArticlesWithUnreadCountDto( infraResult.unReadCount(), From 206a0a175b2a80fb32666d27d9c8b9686bcbee8b Mon Sep 17 00:00:00 2001 From: ose0221 Date: Sat, 14 Feb 2026 17:07:16 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor:=20=EB=8B=A4=EB=A5=B8=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EC=95=84=ED=8B=B0=ED=81=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20NotOwnedException=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/article/service/ArticleGetServiceTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/test/java/com/pinback/infrastructure/article/service/ArticleGetServiceTest.java b/infrastructure/src/test/java/com/pinback/infrastructure/article/service/ArticleGetServiceTest.java index c46a23e..618b617 100644 --- a/infrastructure/src/test/java/com/pinback/infrastructure/article/service/ArticleGetServiceTest.java +++ b/infrastructure/src/test/java/com/pinback/infrastructure/article/service/ArticleGetServiceTest.java @@ -18,6 +18,7 @@ import com.pinback.application.article.dto.ArticlesWithUnreadCountDto; import com.pinback.application.article.dto.RemindArticlesWithCountDto; import com.pinback.application.common.exception.ArticleNotFoundException; +import com.pinback.application.common.exception.ArticleNotOwnedException; import com.pinback.domain.article.entity.Article; import com.pinback.domain.category.entity.Category; import com.pinback.domain.user.entity.User; @@ -219,7 +220,7 @@ void findByUserAndIdNotOwnedTest() { //when & then assertThatThrownBy(() -> articleGetService.findByUserAndId(user2, article.getId())) - .isInstanceOf(ArticleNotFoundException.class); + .isInstanceOf(ArticleNotOwnedException.class); } @DisplayName("읽지 않은 아티클만 조회하면 읽지 않은 아티클만 반환한다.")