From c655f8779ea05f0e3278353d7ca02d7d13291077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:22:36 +0900 Subject: [PATCH 01/22] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A0=A8=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 378c98a14..0da903803 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -132,6 +132,9 @@ public enum ErrorCode implements ResponseCode { TAG_NOT_FOUND(HttpStatus.NOT_FOUND, 160002, "존재하지 않는 TAG 입니다."), INVALID_FEED_COMMAND(HttpStatus.BAD_REQUEST, 160003, "유효하지 않은 FEED 생성/수정 요청 입니다."), FEED_UPDATE_FORBIDDEN(HttpStatus.FORBIDDEN, 160004, "피드 수정 권한이 없습니다."), + DUPLICATED_FEEDS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 160005, "중복된 피드가 존재합니다."), + FEED_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 160006, "사용자가 이미 저장한 피드입니다."), + FEED_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 160007, "사용자가 저장하지 않은 피드는 저장삭제 할 수 없습니다."), /** * 170000 : Image File error From 75d010644d0416e4c99eb7115b97c5a20ea84eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:23:02 +0900 Subject: [PATCH 02/22] =?UTF-8?q?[feat]=20equals,hashcode=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=94=A9=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/feed/domain/Feed.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/konkuk/thip/feed/domain/Feed.java b/src/main/java/konkuk/thip/feed/domain/Feed.java index 6f6eae08e..2e19b3b2b 100644 --- a/src/main/java/konkuk/thip/feed/domain/Feed.java +++ b/src/main/java/konkuk/thip/feed/domain/Feed.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -41,6 +42,19 @@ public class Feed extends BaseDomainEntity { @Builder.Default private List contentList = new ArrayList<>(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Feed feed = (Feed) o; + return Objects.equals(id, feed.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + public static Feed withoutId(String content, Long creatorId, Boolean isPublic, Long targetBookId, List tagValues, List imageUrls) { From a0d55eb5c6c02a12fbb78ed1bdcf39ccb00193e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:23:27 +0900 Subject: [PATCH 03/22] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/FeedCommandController.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java index 2a6b0e7d6..c43488005 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/FeedCommandController.java @@ -4,9 +4,12 @@ import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.feed.adapter.in.web.request.FeedCreateRequest; +import konkuk.thip.feed.adapter.in.web.request.FeedIsSavedRequest; import konkuk.thip.feed.adapter.in.web.request.FeedUpdateRequest; import konkuk.thip.feed.adapter.in.web.response.FeedIdResponse; +import konkuk.thip.feed.adapter.in.web.response.FeedIsSavedResponse; import konkuk.thip.feed.application.port.in.FeedCreateUseCase; +import konkuk.thip.feed.application.port.in.FeedSavedUseCase; import konkuk.thip.feed.application.port.in.FeedUpdateUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,6 +25,7 @@ public class FeedCommandController { private final FeedCreateUseCase feedCreateUseCase; private final FeedUpdateUseCase feedUpdateUseCase; + private final FeedSavedUseCase feedSavedUseCase; //피드 작성 @PostMapping("/feeds") @@ -33,11 +37,20 @@ public BaseResponse createFeed(@RequestPart("request") @Valid fi // 피드 수정 (책 빼고 변경가능) @PatchMapping("/feeds/{feedId}") - public BaseResponse updateFeed(@RequestBody @Valid FeedUpdateRequest request, + public BaseResponse updateFeed(@RequestBody @Valid final FeedUpdateRequest request, @PathVariable("feedId") final Long feedId, - @UserId Long userId) { + @UserId final Long userId) { return BaseResponse.ok(FeedIdResponse.of(feedUpdateUseCase.updateFeed(request.toCommand(userId,feedId)))); } + + //피드 저장상태 변경: true -> 저장, false -> 저장해제(삭제) + @PostMapping("/feeds/{feedId}/saved") + public BaseResponse changeSavedFeed(@RequestBody final FeedIsSavedRequest request, + @PathVariable("feedId") final Long feedId, + @UserId final Long userId) { + return BaseResponse.ok(FeedIsSavedResponse.of(feedSavedUseCase.changeSavedFeed(FeedIsSavedRequest.toCommand(userId,feedId,request.type())))); + } + } From 97e9b2a9f62b3f617286d0c6a1c779b8fbc2b64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:24:11 +0900 Subject: [PATCH 04/22] =?UTF-8?q?[feat]=20FeedIsSavedRequest=20dto=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/request/FeedIsSavedRequest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java new file mode 100644 index 000000000..efb6c0f50 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedIsSavedRequest.java @@ -0,0 +1,13 @@ +package konkuk.thip.feed.adapter.in.web.request; + +import jakarta.validation.constraints.NotNull; +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedCommand; + +public record FeedIsSavedRequest( + @NotNull(message = "type은 필수입니다.") + boolean type +) { + public static FeedIsSavedCommand toCommand(Long userId, Long feedId, Boolean type) { + return new FeedIsSavedCommand(userId, feedId, type); + } +} From 2b1b30a1273912cc739125c153daaede42fa385b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:24:19 +0900 Subject: [PATCH 05/22] =?UTF-8?q?[feat]=20FeedIsSavedResponse=20dto=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/response/FeedIsSavedResponse.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIsSavedResponse.java diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIsSavedResponse.java b/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIsSavedResponse.java new file mode 100644 index 000000000..df2559e66 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIsSavedResponse.java @@ -0,0 +1,12 @@ +package konkuk.thip.feed.adapter.in.web.response; + +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedResult; + +public record FeedIsSavedResponse( + Long feedId, + boolean isSaved +) { + public static FeedIsSavedResponse of(FeedIsSavedResult feedIsSavedResult) { + return new FeedIsSavedResponse(feedIsSavedResult.feedId(), feedIsSavedResult.isSaved()); + } +} From 84073a9df0034fe2f64565ef509c9444fa2426df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:24:29 +0900 Subject: [PATCH 06/22] =?UTF-8?q?[feat]=20FeedIsSavedResult=20dto=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/dto/FeedIsSavedResult.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedResult.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedResult.java b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedResult.java new file mode 100644 index 000000000..5b91f418b --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedResult.java @@ -0,0 +1,11 @@ +package konkuk.thip.feed.application.port.in.dto; + +public record FeedIsSavedResult( + Long feedId, + boolean isSaved +) +{ + public static FeedIsSavedResult of(Long feedId, boolean isSaved) { + return new FeedIsSavedResult(feedId, isSaved); + } +} \ No newline at end of file From 8fe39995cc7d435f4585b7f1d1a77c67afdebff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:24:38 +0900 Subject: [PATCH 07/22] =?UTF-8?q?[feat]=20FeedIsSavedCommand=20dto=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/dto/FeedIsSavedCommand.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedCommand.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedCommand.java b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedCommand.java new file mode 100644 index 000000000..11fc6b5aa --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedIsSavedCommand.java @@ -0,0 +1,12 @@ +package konkuk.thip.feed.application.port.in.dto; + +public record FeedIsSavedCommand( + + Long userId, + + Long feedId, + + Boolean isSaved +) +{ +} From 559f40bb66ec4c34c0618279a300677c867ecfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:24:53 +0900 Subject: [PATCH 08/22] [feat] FeedSavedService.changeSavedFeed (#86) --- .../application/service/FeedSavedService.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java b/src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java new file mode 100644 index 000000000..9825e8d78 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/service/FeedSavedService.java @@ -0,0 +1,45 @@ +package konkuk.thip.feed.application.service; + +import jakarta.transaction.Transactional; +import konkuk.thip.feed.application.port.in.FeedSavedUseCase; +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedCommand; +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedResult; +import konkuk.thip.feed.application.port.out.FeedCommandPort; +import konkuk.thip.feed.domain.Feed; +import konkuk.thip.feed.domain.SavedFeeds; +import konkuk.thip.saved.application.port.out.SavedCommandPort; +import konkuk.thip.saved.application.port.out.SavedQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FeedSavedService implements FeedSavedUseCase { + + private final FeedCommandPort feedCommandPort; + private final SavedCommandPort savedCommandPort; + private final SavedQueryPort savedQueryPort; + + @Override + @Transactional + public FeedIsSavedResult changeSavedFeed(FeedIsSavedCommand feedIsSavedCommand) { + + // 1. 피드 검증 및 조회 + Feed feed = feedCommandPort.getByIdOrThrow(feedIsSavedCommand.feedId()); + + // 2. 유저가 저장한 피드 목록 조회 + SavedFeeds savedFeeds = savedQueryPort.findSavedFeedsByUserId(feedIsSavedCommand.userId()); + + if (feedIsSavedCommand.isSaved()) { + // 저장 요청 시 이미 저장되어 있으면 예외 발생 + savedFeeds.validateNotAlreadySaved(feed); + savedCommandPort.saveFeed(feedIsSavedCommand.userId(), feed.getId()); + } else { + // 삭제 요청 시 저장되어 있지 않으면 예외 발생 + savedFeeds.validateCanDelete(feed); + savedCommandPort.deleteFeed(feedIsSavedCommand.userId(), feed.getId()); + } + + return FeedIsSavedResult.of(feed.getId(), feedIsSavedCommand.isSaved()); + } +} From a1cf21585c03d18efc0dc7cdd9f1e6f31fe97ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:25:05 +0900 Subject: [PATCH 09/22] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=9C=A0?= =?UTF-8?q?=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=9E=91=EC=84=B1=20=20?= =?UTF-8?q?(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/application/port/in/FeedSavedUseCase.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/port/in/FeedSavedUseCase.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/FeedSavedUseCase.java b/src/main/java/konkuk/thip/feed/application/port/in/FeedSavedUseCase.java new file mode 100644 index 000000000..8f2951f62 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/port/in/FeedSavedUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.feed.application.port.in; + +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedCommand; +import konkuk.thip.feed.application.port.in.dto.FeedIsSavedResult; + +public interface FeedSavedUseCase { + FeedIsSavedResult changeSavedFeed(FeedIsSavedCommand feedIsSavedCommand); +} From cc24e321ce167582472ad0140598422aa6dbe353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:25:18 +0900 Subject: [PATCH 10/22] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=96=B4?= =?UTF-8?q?=EB=8C=91=ED=84=B0=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SavedCommandPersistenceAdapter.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java index 108e7be9c..67d3aeed6 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java @@ -3,7 +3,10 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; +import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.saved.adapter.out.mapper.SavedBookMapper; import konkuk.thip.saved.adapter.out.mapper.SavedFeedMapper; import konkuk.thip.saved.adapter.out.persistence.repository.SavedBookJpaRepository; @@ -22,6 +25,7 @@ public class SavedCommandPersistenceAdapter implements SavedCommandPort { private final UserJpaRepository userJpaRepository; private final BookJpaRepository bookJpaRepository; + private final FeedJpaRepository feedJpaRepository; private final SavedBookJpaRepository savedBookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; private final SavedBookMapper savedBookMapper; @@ -40,10 +44,28 @@ public void saveBook(Long userId, Long bookId) { savedBookJpaRepository.save(entity); } - - //삭제 전략 도입 전 @Override public void deleteBook(Long userId, Long bookId) { savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId); } + + @Override + public void saveFeed(Long userId, Long feedId) { + UserJpaEntity user = userJpaRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + FeedJpaEntity feed = feedJpaRepository.findById(feedId) + .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); + SavedFeedJpaEntity entity = SavedFeedJpaEntity.builder() + .userJpaEntity(user) + .feedJpaEntity(feed) + .build(); + savedFeedJpaRepository.save(entity); + } + + @Override + public void deleteFeed(Long userId, Long feedId) { + savedFeedJpaRepository.deleteByUserJpaEntity_UserIdAndFeedJpaEntity_PostId(userId, feedId); + } + + } \ No newline at end of file From 179ebb206883469c3b3cfd7c31cf21bac1df4945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:25:21 +0900 Subject: [PATCH 11/22] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=96=B4?= =?UTF-8?q?=EB=8C=91=ED=84=B0=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/saved/application/port/out/SavedCommandPort.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java index a4a12157f..21e26934f 100644 --- a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java +++ b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java @@ -4,4 +4,6 @@ public interface SavedCommandPort { void saveBook(Long userId, Long bookId); void deleteBook(Long userId, Long bookId); + void saveFeed(Long userId, Long feedId); + void deleteFeed(Long userId, Long feedId); } From 280eeaf44e84ee921a9d4d6fae06d4e717a83cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:25:40 +0900 Subject: [PATCH 12/22] =?UTF-8?q?[feat]=20SavedFeedJpaRepository=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/repository/SavedFeedJpaRepository.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java index be10fae92..a5cf1bb84 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java @@ -3,5 +3,9 @@ import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface SavedFeedJpaRepository extends JpaRepository { + void deleteByUserJpaEntity_UserIdAndFeedJpaEntity_PostId(Long userId, Long feedId); + List findByUserJpaEntity_UserId(Long userId); } From 153b85a3c4a6fd61b54e76f6b0931bd4a8d4905e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:26:00 +0900 Subject: [PATCH 13/22] =?UTF-8?q?[feat]=20SavedFeeds=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=A0=80=EC=9E=A5=ED=95=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=9D=BC=EA=B8=89=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/feed/domain/SavedFeeds.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/domain/SavedFeeds.java diff --git a/src/main/java/konkuk/thip/feed/domain/SavedFeeds.java b/src/main/java/konkuk/thip/feed/domain/SavedFeeds.java new file mode 100644 index 000000000..e5c739477 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/domain/SavedFeeds.java @@ -0,0 +1,42 @@ +package konkuk.thip.feed.domain; + +import konkuk.thip.common.exception.InvalidStateException; +import lombok.Getter; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static konkuk.thip.common.exception.code.ErrorCode.*; + +@Getter +public class SavedFeeds { + + private final Set feeds; + + public SavedFeeds(List feeds) { + Set feedSet = new HashSet<>(feeds); + if (feedSet.size() != feeds.size()) { + throw new InvalidStateException(DUPLICATED_FEEDS_IN_COLLECTION); + } + this.feeds = Collections.unmodifiableSet(feedSet); + } + + // 중복 저장 검증 + public void validateNotAlreadySaved(Feed feed) { + if (feeds.contains(feed)) { + throw new InvalidStateException(FEED_ALREADY_SAVED); + } + } + + // 삭제 가능 여부 검증 + public void validateCanDelete(Feed feed) { + if (!feeds.contains(feed)) { + throw new InvalidStateException(FEED_NOT_SAVED_CANNOT_DELETE); + } + } + +} + + From e7d18a3c277270ef784d10118e5e9f994c4e7323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:26:30 +0900 Subject: [PATCH 14/22] =?UTF-8?q?[feat]=20SavedQueryPersistenceAdapter.fin?= =?UTF-8?q?dSavedFeedsByUserId=20=EC=9E=91=EC=84=B1=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SavedQueryPersistenceAdapter.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java index ec563a0e9..fb524cd8a 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java @@ -3,9 +3,14 @@ import konkuk.thip.book.adapter.out.mapper.BookMapper; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; +import konkuk.thip.feed.adapter.out.mapper.FeedMapper; +import konkuk.thip.feed.adapter.out.persistence.repository.Tag.TagJpaRepository; +import konkuk.thip.feed.domain.Feed; +import konkuk.thip.feed.domain.SavedFeeds; import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; -import konkuk.thip.saved.adapter.out.mapper.SavedBookMapper; -import konkuk.thip.saved.adapter.out.mapper.SavedFeedMapper; +import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.saved.adapter.out.persistence.repository.SavedBookJpaRepository; import konkuk.thip.saved.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.saved.application.port.out.SavedQueryPort; @@ -27,9 +32,9 @@ public class SavedQueryPersistenceAdapter implements SavedQueryPort { private final SavedBookJpaRepository savedBookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; private final UserJpaRepository userJpaRepository; - private final SavedBookMapper savedBookMapper; + private final TagJpaRepository tagJpaRepository; private final BookMapper bookMapper; - private final SavedFeedMapper savedFeedMapper; + private final FeedMapper feedMapper; @Override public boolean existsByUserIdAndBookId(Long userId, Long bookId) { @@ -37,7 +42,7 @@ public boolean existsByUserIdAndBookId(Long userId, Long bookId) { } @Override - public SavedBooks findByUserId(Long userId) { + public SavedBooks findSavedBooksByUserId(Long userId) { UserJpaEntity user = userJpaRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); @@ -51,5 +56,22 @@ public SavedBooks findByUserId(Long userId) { return new SavedBooks(books); } + @Override + public SavedFeeds findSavedFeedsByUserId(Long userId) { + UserJpaEntity user = userJpaRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + List savedFeedEntities = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + + List feeds = savedFeedEntities.stream() + .map(entity -> { + FeedJpaEntity feedJpa = entity.getFeedJpaEntity(); + List tags = tagJpaRepository.findAllByFeedId(feedJpa.getPostId()); + return feedMapper.toDomainEntity(feedJpa, tags); + }) + .toList(); + + return new SavedFeeds(feeds); + } + } From 28a745f8951bbba727b994013b1668cc63f8e2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:26:42 +0900 Subject: [PATCH 15/22] =?UTF-8?q?[feat]=20SavedQueryPort=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/saved/application/port/out/SavedQueryPort.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java b/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java index 1ed5ae9e6..2c77fa3d7 100644 --- a/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java +++ b/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java @@ -1,8 +1,10 @@ package konkuk.thip.saved.application.port.out; import konkuk.thip.book.domain.SavedBooks; +import konkuk.thip.feed.domain.SavedFeeds; public interface SavedQueryPort { boolean existsByUserIdAndBookId(Long userId, Long bookId); - SavedBooks findByUserId(Long userId); + SavedBooks findSavedBooksByUserId(Long userId); + SavedFeeds findSavedFeedsByUserId(Long userId); } From 9944eb02b16e341b6e0136c10f2f73b8462c7152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:27:07 +0900 Subject: [PATCH 16/22] =?UTF-8?q?[test]=20SavedFeed=20=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/util/TestEntityFactory.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index e03ddb30a..240df2cd5 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -12,6 +12,7 @@ import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; @@ -222,4 +223,10 @@ public static FeedJpaEntity createFeedWithContents(UserJpaEntity user, BookJpaEn } + public static SavedFeedJpaEntity createSavedFeed(UserJpaEntity user, FeedJpaEntity feed) { + return SavedFeedJpaEntity.builder() + .feedJpaEntity(feed) + .userJpaEntity(user) + .build(); + } } \ No newline at end of file From 8bab3a5d15a4c8043b4f426c0938f4fa4be8bdd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:27:27 +0900 Subject: [PATCH 17/22] =?UTF-8?q?[test]=20=ED=94=BC=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/FeedChangeSavedAPITest.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java new file mode 100644 index 000000000..2a3848970 --- /dev/null +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java @@ -0,0 +1,179 @@ +package konkuk.thip.feed.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.feed.adapter.in.web.request.FeedIsSavedRequest; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.Tag.TagJpaRepository; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository; +import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; +import konkuk.thip.saved.adapter.out.persistence.repository.SavedFeedJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 피드 저장 상태 변경 api 통합 테스트") +class FeedChangeSavedAPITest { + + @Autowired + private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + @Autowired private AliasJpaRepository aliasJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private CategoryJpaRepository categoryJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private TagJpaRepository tagJpaRepository; + @Autowired private FeedJpaRepository feedJpaRepository; + @Autowired private SavedFeedJpaRepository savedFeedJpaRepository; + + private UserJpaEntity user; + private BookJpaEntity book; + private FeedJpaEntity feed; + + @BeforeEach + void setUp() { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); + + tagJpaRepository.save(TestEntityFactory.createTag(category, "소설추천")); + tagJpaRepository.save(TestEntityFactory.createTag(category, "책추천")); + tagJpaRepository.save(TestEntityFactory.createTag(category, "오늘의책")); + book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + feed = feedJpaRepository.save(TestEntityFactory.createFeed(user,book, true)); + } + + @Test + @DisplayName("피드를 처음 저장하면 [저장 성공]") + void saveFeed_success() throws Exception { + // given + FeedIsSavedRequest request = new FeedIsSavedRequest(true); + Long feedId = feed.getPostId(); + + // when + ResultActions result = mockMvc.perform(post("/feeds/{feedId}/saved", feedId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.feedId").value(feedId)) + .andExpect(jsonPath("$.data.isSaved").value(true)); + + // 실제 저장되었는지 검증 + List savedFeeds = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + boolean exists = savedFeeds.stream() + .anyMatch(entity -> entity.getFeedJpaEntity().getPostId().equals(feed.getPostId())); + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("이미 저장한 피드를 다시 저장하려하면 [400 에러 발생]") + void saveFeed_alreadySaved_fail() throws Exception { + // given + savedFeedJpaRepository.save(TestEntityFactory.createSavedFeed(user, feed)); + FeedIsSavedRequest request = new FeedIsSavedRequest(true); + + // when + ResultActions result = mockMvc.perform(post("/feeds/{feedId}/saved", feed.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.FEED_ALREADY_SAVED.getCode())); + } + + @Test + @DisplayName("피드를 저장한 이후 삭제 요청하면 [피드 저장 삭제 성공]") + void deleteFeed_success() throws Exception { + // given + savedFeedJpaRepository.save(TestEntityFactory.createSavedFeed(user, feed)); + FeedIsSavedRequest request = new FeedIsSavedRequest(false); + + // when + ResultActions result = mockMvc.perform(post("/feeds/{feedId}/saved", feed.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.feedId").value(feed.getPostId())) + .andExpect(jsonPath("$.data.isSaved").value(false)); + + List savedFeeds = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + boolean exists = savedFeeds.stream() + .anyMatch(entity -> entity.getFeedJpaEntity().getPostId().equals(feed.getPostId())); + assertThat(exists).isFalse(); + + } + + @Test + @DisplayName("저장하지 않은 피드를 삭제하려고 하면 [400 에러 발생]") + void deleteFeed_notSaved_fail() throws Exception { + // given + FeedIsSavedRequest request = new FeedIsSavedRequest(false); + + // when + ResultActions result = mockMvc.perform(post("/feeds/{feedId}/saved", feed.getPostId()) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.FEED_NOT_SAVED_CANNOT_DELETE.getCode())); + } + + @Test + @DisplayName("존재하지 않는 피드를 저장/삭제 요청시 [404 에러 발생]") + void changeFeedSaveStatus_whenFeedNotExist_thenFail() throws Exception { + + // given + Long invalidFeedId = 99999L; + FeedIsSavedRequest request = new FeedIsSavedRequest(true); + + // when + ResultActions result = mockMvc.perform(post("/feeds/{feedId}/saved", invalidFeedId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ErrorCode.FEED_NOT_FOUND.getCode())); + } + +} From 4f0b485eeaf13ee7dc40603c270ec1122b10dfd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:28:01 +0900 Subject: [PATCH 18/22] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=95=A8=EC=88=98=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/BookSavedService.java | 2 +- .../FeedCommandPersistenceAdapter.java | 16 +++++++++------- .../application/port/out/FeedCommandPort.java | 11 ++++++++++- .../application/service/FeedUpdateService.java | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 9b41d751c..7ac3e3572 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -61,7 +61,7 @@ public BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userI } // 유저가 저장한 책 목록 조회 - SavedBooks savedBooks = savedQueryPort.findByUserId(userId); + SavedBooks savedBooks = savedQueryPort.findSavedBooksByUserId(userId); if (isSave) { // 저장 요청 시 이미 저장되어 있으면 예외 발생 diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java index adbc244d3..b9f36d79f 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -36,16 +37,17 @@ public class FeedCommandPersistenceAdapter implements FeedCommandPort { private final FeedMapper feedMapper; private final ContentMapper contentMapper; - @Override - public Feed findById(Long id) { - FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); - - List tagJpaEntityList = tagJpaRepository.findAllByFeedId(feedJpaEntity.getPostId()); - return feedMapper.toDomainEntity(feedJpaEntity, tagJpaEntityList); + @Override + public Optional findById(Long id) { + return feedJpaRepository.findById(id) + .map(feedJpaEntity -> { + List tagJpaEntityList = tagJpaRepository.findAllByFeedId(feedJpaEntity.getPostId()); + return feedMapper.toDomainEntity(feedJpaEntity, tagJpaEntityList); + }); } + @Override public Long save(Feed feed) { diff --git a/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java b/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java index ffbdc3621..31c86b5c2 100644 --- a/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java +++ b/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java @@ -1,10 +1,19 @@ package konkuk.thip.feed.application.port.out; +import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.feed.domain.Feed; +import java.util.Optional; + +import static konkuk.thip.common.exception.code.ErrorCode.FEED_NOT_FOUND; + public interface FeedCommandPort { Long save(Feed feed); Long update(Feed feed); - Feed findById(Long id); + Optional findById(Long id); + default Feed getByIdOrThrow(Long id) { + return findById(id) + .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); + } } diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java index ef1282c71..e54ebe23c 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java @@ -28,7 +28,7 @@ public Long updateFeed(FeedUpdateCommand command) { Feed.validateImageCount(command.remainImageUrls() != null ? command.remainImageUrls().size() : 0); // 2. 피드 조회 및 유효성 검증 - Feed feed = feedCommandPort.findById(command.feedId()); + Feed feed = feedCommandPort.getByIdOrThrow(command.feedId()); feed.validateCreator(command.userId()); // 3. 도메인 내부 상태 변경 From 7e8ecbf9b17af4ce9406a9625315c0b204a1df81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 02:28:11 +0900 Subject: [PATCH 19/22] =?UTF-8?q?[remove]=20=EB=8D=94=EB=AF=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/application/port/in/dto/DummyResult.java | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/main/java/konkuk/thip/feed/application/port/in/dto/DummyResult.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/dto/DummyResult.java b/src/main/java/konkuk/thip/feed/application/port/in/dto/DummyResult.java deleted file mode 100644 index b260ba6b2..000000000 --- a/src/main/java/konkuk/thip/feed/application/port/in/dto/DummyResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.feed.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class DummyResult { - -} From b2489cd0ed4b46b2cd66f85a723879103f458490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 22 Jul 2025 03:12:40 +0900 Subject: [PATCH 20/22] =?UTF-8?q?[refactor]=20=EB=B0=B0=EC=B9=98=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20n+1=20=EB=AC=B8=EC=A0=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedTag/FeedTagJpaRepository.java | 10 +++++++ .../SavedQueryPersistenceAdapter.java | 26 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java index f8d37dcbd..8dd449199 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java @@ -7,10 +7,20 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; + public interface FeedTagJpaRepository extends JpaRepository{ @Modifying @Query("DELETE FROM FeedTagJpaEntity ft WHERE ft.feedJpaEntity = :feedJpaEntity") void deleteAllByFeedJpaEntity(@Param("feedJpaEntity") FeedJpaEntity feedJpaEntity); + @Query(""" + SELECT ft.feedJpaEntity.postId, ft.tagJpaEntity + FROM FeedTagJpaEntity ft + WHERE ft.feedJpaEntity.postId IN :feedIds + """) + List findFeedIdAndTagsByFeedIds(@Param("feedIds") List feedIds); + + } diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java index fb524cd8a..9c4c21a47 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java @@ -6,7 +6,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; import konkuk.thip.feed.adapter.out.mapper.FeedMapper; -import konkuk.thip.feed.adapter.out.persistence.repository.Tag.TagJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedTag.FeedTagJpaRepository; import konkuk.thip.feed.domain.Feed; import konkuk.thip.feed.domain.SavedFeeds; import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; @@ -21,6 +21,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; @@ -32,9 +33,9 @@ public class SavedQueryPersistenceAdapter implements SavedQueryPort { private final SavedBookJpaRepository savedBookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; private final UserJpaRepository userJpaRepository; - private final TagJpaRepository tagJpaRepository; private final BookMapper bookMapper; private final FeedMapper feedMapper; + private final FeedTagJpaRepository feedTagJpaRepository; @Override public boolean existsByUserIdAndBookId(Long userId, Long bookId) { @@ -60,12 +61,28 @@ public SavedBooks findSavedBooksByUserId(Long userId) { public SavedFeeds findSavedFeedsByUserId(Long userId) { UserJpaEntity user = userJpaRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); - List savedFeedEntities = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + + List savedFeedEntities = + savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + + List feedIds = savedFeedEntities.stream() + .map(entity -> entity.getFeedJpaEntity().getPostId()) + .toList(); + + // 한 번의 쿼리로 Feed ID에 대한 Tag 전체 조회 + List results = feedTagJpaRepository.findFeedIdAndTagsByFeedIds(feedIds); + + // 결과 데이터를 feedId → List 형태로 그룹핑 + Map> feedTagsMap = results.stream() + .collect(Collectors.groupingBy( + row -> (Long) row[0], + Collectors.mapping(row -> (TagJpaEntity) row[1], Collectors.toList()) + )); List feeds = savedFeedEntities.stream() .map(entity -> { FeedJpaEntity feedJpa = entity.getFeedJpaEntity(); - List tags = tagJpaRepository.findAllByFeedId(feedJpa.getPostId()); + List tags = feedTagsMap.getOrDefault(feedJpa.getPostId(), List.of()); return feedMapper.toDomainEntity(feedJpa, tags); }) .toList(); @@ -73,5 +90,4 @@ public SavedFeeds findSavedFeedsByUserId(Long userId) { return new SavedFeeds(feeds); } - } From e6f9e21d38e4d8fed3d7ce7f86cde56dc4783f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Wed, 23 Jul 2025 03:27:34 +0900 Subject: [PATCH 21/22] =?UTF-8?q?[refactor]=20=EB=B0=B0=EC=B9=98=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20Projection=20=EA=B8=B0=EB=B0=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8E=99?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/FeedTag/FeedTagJpaRepository.java | 7 +++---- .../persistence/SavedCommandPersistenceAdapter.java | 2 +- .../persistence/SavedQueryPersistenceAdapter.java | 12 ++++++------ .../port/out/dto/FeedIdAndTagProjection.java | 8 ++++++++ .../feed/adapter/in/web/FeedChangeSavedAPITest.java | 4 ++-- 5 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 src/main/java/konkuk/thip/saved/application/port/out/dto/FeedIdAndTagProjection.java diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java index 8dd449199..cf5519abc 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedTag/FeedTagJpaRepository.java @@ -2,6 +2,7 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.jpa.FeedTagJpaEntity; +import konkuk.thip.saved.application.port.out.dto.FeedIdAndTagProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -16,11 +17,9 @@ public interface FeedTagJpaRepository extends JpaRepository findFeedIdAndTagsByFeedIds(@Param("feedIds") List feedIds); - - + List findFeedIdAndTagsByFeedIds(@Param("feedIds") List feedIds); } diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java index 67d3aeed6..cc61dd5cf 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java @@ -64,7 +64,7 @@ public void saveFeed(Long userId, Long feedId) { @Override public void deleteFeed(Long userId, Long feedId) { - savedFeedJpaRepository.deleteByUserJpaEntity_UserIdAndFeedJpaEntity_PostId(userId, feedId); + savedFeedJpaRepository.deleteByUserIdAndFeedId(userId, feedId); } diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java index 9c4c21a47..b7aff7ee7 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java @@ -15,6 +15,7 @@ import konkuk.thip.saved.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.saved.application.port.out.SavedQueryPort; import konkuk.thip.book.domain.SavedBooks; +import konkuk.thip.saved.application.port.out.dto.FeedIdAndTagProjection; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; @@ -63,20 +64,19 @@ public SavedFeeds findSavedFeedsByUserId(Long userId) { .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); List savedFeedEntities = - savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + savedFeedJpaRepository.findAllByUserId(user.getUserId()); List feedIds = savedFeedEntities.stream() .map(entity -> entity.getFeedJpaEntity().getPostId()) .toList(); - // 한 번의 쿼리로 Feed ID에 대한 Tag 전체 조회 - List results = feedTagJpaRepository.findFeedIdAndTagsByFeedIds(feedIds); + // Projection 기반 조회 + List results = feedTagJpaRepository.findFeedIdAndTagsByFeedIds(feedIds); - // 결과 데이터를 feedId → List 형태로 그룹핑 Map> feedTagsMap = results.stream() .collect(Collectors.groupingBy( - row -> (Long) row[0], - Collectors.mapping(row -> (TagJpaEntity) row[1], Collectors.toList()) + FeedIdAndTagProjection::getFeedId, + Collectors.mapping(FeedIdAndTagProjection::getTagJpaEntity, Collectors.toList()) )); List feeds = savedFeedEntities.stream() diff --git a/src/main/java/konkuk/thip/saved/application/port/out/dto/FeedIdAndTagProjection.java b/src/main/java/konkuk/thip/saved/application/port/out/dto/FeedIdAndTagProjection.java new file mode 100644 index 000000000..ab1ef2c5d --- /dev/null +++ b/src/main/java/konkuk/thip/saved/application/port/out/dto/FeedIdAndTagProjection.java @@ -0,0 +1,8 @@ +package konkuk.thip.saved.application.port.out.dto; + +import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; + +public interface FeedIdAndTagProjection { + Long getFeedId(); + TagJpaEntity getTagJpaEntity(); +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java index 2a3848970..76b976d60 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedChangeSavedAPITest.java @@ -91,7 +91,7 @@ void saveFeed_success() throws Exception { .andExpect(jsonPath("$.data.isSaved").value(true)); // 실제 저장되었는지 검증 - List savedFeeds = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + List savedFeeds = savedFeedJpaRepository.findAllByUserId(user.getUserId()); boolean exists = savedFeeds.stream() .anyMatch(entity -> entity.getFeedJpaEntity().getPostId().equals(feed.getPostId())); assertThat(exists).isTrue(); @@ -133,7 +133,7 @@ void deleteFeed_success() throws Exception { .andExpect(jsonPath("$.data.feedId").value(feed.getPostId())) .andExpect(jsonPath("$.data.isSaved").value(false)); - List savedFeeds = savedFeedJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + List savedFeeds = savedFeedJpaRepository.findAllByUserId(user.getUserId()); boolean exists = savedFeeds.stream() .anyMatch(entity -> entity.getFeedJpaEntity().getPostId().equals(feed.getPostId())); assertThat(exists).isFalse(); From e8779bca23b0f8dd445f0667248cbc3f3f4d35aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Wed, 23 Jul 2025 03:28:02 +0900 Subject: [PATCH 22/22] =?UTF-8?q?[refactor]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=EC=9D=B8=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/SavedFeedJpaRepository.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java index a5cf1bb84..36e02a0fe 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/repository/SavedFeedJpaRepository.java @@ -2,10 +2,17 @@ import konkuk.thip.saved.adapter.out.jpa.SavedFeedJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; public interface SavedFeedJpaRepository extends JpaRepository { - void deleteByUserJpaEntity_UserIdAndFeedJpaEntity_PostId(Long userId, Long feedId); - List findByUserJpaEntity_UserId(Long userId); + @Modifying + @Query(value = "DELETE FROM saved_feeds WHERE user_id = :userId AND post_id = :feedId", nativeQuery = true) + void deleteByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId); + + @Query(value = "SELECT * FROM saved_feeds WHERE user_id = :userId", nativeQuery = true) + List findAllByUserId(@Param("userId") Long userId); }