From 08aadd61d66b2b916bebfab9780137c35c57ff39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:50:18 +0900 Subject: [PATCH 01/30] =?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, 2 insertions(+), 1 deletion(-) 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 f39c53cb7..e9a929611 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -129,7 +129,8 @@ public enum ErrorCode implements ResponseCode { FEED_NOT_FOUND(HttpStatus.NOT_FOUND, 160000, "존재하지 않는 FEED 입니다."), TAG_NAME_NOT_MATCH(HttpStatus.BAD_REQUEST, 160001, "일치하는 태그 이름이 없습니다."), TAG_NOT_FOUND(HttpStatus.NOT_FOUND, 160002, "존재하지 않는 TAG 입니다."), - INVALID_FEED_CREATE(HttpStatus.BAD_REQUEST, 160003, "유효하지 않은 FEED 생성 요청 입니다."), + INVALID_FEED_COMMAND(HttpStatus.BAD_REQUEST, 160003, "유효하지 않은 FEED 생성/수정 요청 입니다."), + FEED_UPDATE_FORBIDDEN(HttpStatus.FORBIDDEN, 160004, "피드 수정 권한이 없습니다."), /** * 170000 : Image File error From 1d596e39987188a7fa8380f148b89d3fd583147a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:50:30 +0900 Subject: [PATCH 02/30] =?UTF-8?q?[feat]=20feed=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B7=9C=EC=B9=99=20=EC=B1=85=EC=9E=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/feed/domain/Feed.java | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/domain/Feed.java b/src/main/java/konkuk/thip/feed/domain/Feed.java index 3fe0be1e5..6f6eae08e 100644 --- a/src/main/java/konkuk/thip/feed/domain/Feed.java +++ b/src/main/java/konkuk/thip/feed/domain/Feed.java @@ -6,7 +6,9 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -36,7 +38,8 @@ public class Feed extends BaseDomainEntity { private List tagList; - private List contentList; + @Builder.Default + private List contentList = new ArrayList<>(); public static Feed withoutId(String content, Long creatorId, Boolean isPublic, Long targetBookId, List tagValues, List imageUrls) { @@ -56,42 +59,67 @@ public static Feed withoutId(String content, Long creatorId, Boolean isPublic, L } private static List convertToContentList(List imageUrls) { - if (imageUrls == null) return List.of(); - + if (imageUrls == null) return new ArrayList<>(); return imageUrls.stream() .filter(url -> url != null && !url.isBlank()) .map(url -> Content.builder().contentUrl(url).build()) .collect(Collectors.toList()); } - public static void validateCategoryAndTags(String category, List tagList) { - - // 둘 다 없으면 카테고리도 태그도 없는 새 게시글 (예외 상황 아님) - boolean categoryEmpty = (category == null || category.trim().isEmpty()); + public static void validateTags(List tagList) { boolean tagListEmpty = (tagList == null || tagList.isEmpty()); - // 둘 중 하나만 입력된 경우 - if (categoryEmpty ^ tagListEmpty) { - throw new InvalidStateException(INVALID_FEED_CREATE, new IllegalArgumentException("카테고리와 태그는 모두 입력되거나 모두 비워져야 합니다.")); - } - // 태그가 있는 경우, 개수 최대 5개 제한 if (!tagListEmpty && tagList.size() > 5) { - throw new InvalidStateException(INVALID_FEED_CREATE, new IllegalArgumentException("태그는 최대 5개까지 입력할 수 있습니다.")); + throw new InvalidStateException(INVALID_FEED_COMMAND, new IllegalArgumentException("태그는 최대 5개까지 입력할 수 있습니다.")); } // 태그 중복 체크 if (!tagListEmpty) { long distinctCount = tagList.stream().distinct().count(); if (distinctCount != tagList.size()) { - throw new InvalidStateException(INVALID_FEED_CREATE, new IllegalArgumentException("태그는 중복 될 수 없습니다.")); + throw new InvalidStateException(INVALID_FEED_COMMAND, new IllegalArgumentException("태그는 중복 될 수 없습니다.")); } } } public static void validateImageCount(int imageSize) { if (imageSize > 3) { - throw new InvalidStateException(INVALID_FEED_CREATE, new IllegalArgumentException("이미지는 최대 3개까지 업로드할 수 있습니다.")); + throw new InvalidStateException(INVALID_FEED_COMMAND, new IllegalArgumentException("이미지는 최대 3개까지 업로드할 수 있습니다.")); + } + } + + public void validateCreator(Long userId) { + if (!this.creatorId.equals(userId)) { + throw new InvalidStateException(FEED_UPDATE_FORBIDDEN); + } + } + + public void updateContent(String newContent) { + this.content = newContent; + } + + public void updateVisibility(Boolean isPublic) { + this.isPublic = isPublic; + } + + public void updateTags(List tagValues) { + this.tagList = Tag.fromList(tagValues); + } + + public void updateImages(List imageUrls) { + this.contentList = convertToContentList(imageUrls); + } + + public void validateOwnsImages(List candidateImageUrls) { + Set myImageUrls = this.getContentList().stream() + .map(Content::getContentUrl) + .filter(url -> url != null && !url.isBlank()) + .collect(Collectors.toSet()); + for (String url : candidateImageUrls) { + if (!myImageUrls.contains(url)) { + throw new InvalidStateException(INVALID_FEED_COMMAND, new IllegalArgumentException("해당 이미지는 이 피드에 존재하지 않습니다: " + url)); + } } } From 24dced56ecab06ed3ab6c07304fe368cc52c2fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:50:56 +0900 Subject: [PATCH 03/30] [feat] FeedCommandController.updateFeed (#86) --- .../adapter/in/web/FeedCommandController.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 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 9e6c8bb6e..2a6b0e7d6 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,13 +4,13 @@ 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.response.FeedCreateResponse; +import konkuk.thip.feed.adapter.in.web.request.FeedUpdateRequest; +import konkuk.thip.feed.adapter.in.web.response.FeedIdResponse; import konkuk.thip.feed.application.port.in.FeedCreateUseCase; +import konkuk.thip.feed.application.port.in.FeedUpdateUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -21,11 +21,23 @@ public class FeedCommandController { private final FeedCreateUseCase feedCreateUseCase; + private final FeedUpdateUseCase feedUpdateUseCase; + //피드 작성 @PostMapping("/feeds") - public BaseResponse createFeed(@RequestPart("request") @Valid final FeedCreateRequest request, - @RequestPart(value = "images", required = false) final List images, - @UserId final Long userId) { - return BaseResponse.ok(FeedCreateResponse.of(feedCreateUseCase.createFeed(request.toCommand(userId),images))); + public BaseResponse createFeed(@RequestPart("request") @Valid final FeedCreateRequest request, + @RequestPart(value = "images", required = false) final List images, + @UserId final Long userId) { + return BaseResponse.ok(FeedIdResponse.of(feedCreateUseCase.createFeed(request.toCommand(userId),images))); + } + + // 피드 수정 (책 빼고 변경가능) + @PatchMapping("/feeds/{feedId}") + public BaseResponse updateFeed(@RequestBody @Valid FeedUpdateRequest request, + @PathVariable("feedId") final Long feedId, + @UserId Long userId) { + + return BaseResponse.ok(FeedIdResponse.of(feedUpdateUseCase.updateFeed(request.toCommand(userId,feedId)))); + } } From 6435b25a1176e073ce2197c9bd8d0f572e128b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:51:19 +0900 Subject: [PATCH 04/30] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=8B=9C=20=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BB=A4?= =?UTF-8?q?=EB=A7=A8=EB=93=9C=20=EC=96=B4=EB=8C=91=ED=84=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedCommandPersistenceAdapter.java | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) 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 2bc104b3d..adbc244d3 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 @@ -18,14 +18,12 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import java.util.List; import static konkuk.thip.common.exception.code.ErrorCode.*; -@Slf4j @Repository @RequiredArgsConstructor public class FeedCommandPersistenceAdapter implements FeedCommandPort { @@ -38,6 +36,16 @@ 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 Long save(Feed feed) { @@ -60,19 +68,36 @@ public Long save(Feed feed) { return savedFeed.getPostId(); } - private void saveContents(Feed feed, FeedJpaEntity feedJpaEntity) { - if (feed.getContentList().isEmpty()) return; + @Override + public Long update(Feed feed) { + FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) + .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); + feedJpaEntity.updateFrom(feed); + updateContents(feed, feedJpaEntity); + updateFeedTags(feed, feedJpaEntity); + + return feedJpaEntity.getPostId(); + } - List contentJpaEntities = feed.getContentList().stream() + private void addAllContents(Feed feed, FeedJpaEntity feedJpaEntity) { + if (feed.getContentList().isEmpty()) return; + List contents = feed.getContentList().stream() .map(content -> contentMapper.toJpaEntity(content, feedJpaEntity)) .toList(); + contents.forEach(feedJpaEntity.getContentList()::add); + } - contentJpaEntities.forEach(feedJpaEntity.getContentList()::add); + private void saveContents(Feed feed, FeedJpaEntity feedJpaEntity) { + addAllContents(feed, feedJpaEntity); } - private void saveFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { - if (feed.getTagList().isEmpty()) return; + private void updateContents(Feed feed, FeedJpaEntity feedJpaEntity) { + feedJpaEntity.getContentList().clear(); // 피드 수정시 기존 영속성 컨텍스트 내 엔티티 연결 제거 + addAllContents(feed, feedJpaEntity); + } + private void addAllTags(Feed feed, FeedJpaEntity feedJpaEntity) { + if (feed.getTagList().isEmpty()) return; for (Tag tag : feed.getTagList()) { TagJpaEntity tagJpaEntity = tagJpaRepository.findByValue(tag.getValue()) .orElseThrow(() -> new EntityNotFoundException(TAG_NOT_FOUND)); @@ -85,6 +110,12 @@ private void saveFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { feedTagJpaRepository.save(feedTagJpaEntity); } } - + private void saveFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { + addAllTags(feed, feedJpaEntity); + } + private void updateFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { + feedTagJpaRepository.deleteAllByFeedJpaEntity(feedJpaEntity); // 피드 수정시 기존 피드의 모든 FeedTag 매핑 row 삭제 + addAllTags(feed, feedJpaEntity); + } } From 315c2b0b6ef6f412d35935ec4f5781fe504818cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:51:34 +0900 Subject: [PATCH 05/30] [feat] FeedCommandPort.update (#86) --- .../konkuk/thip/feed/application/port/out/FeedCommandPort.java | 2 ++ 1 file changed, 2 insertions(+) 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 a6a9c1930..ffbdc3621 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 @@ -5,4 +5,6 @@ public interface FeedCommandPort { Long save(Feed feed); + Long update(Feed feed); + Feed findById(Long id); } From 40656b73735f378634df2580a995db48613150ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:52:27 +0900 Subject: [PATCH 06/30] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=EC=8B=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B2=80=EC=A6=9D=20=EC=95=88?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?s3=20=EC=82=AD=EC=A0=9C=EA=B4=80=EB=A0=A8=20todo=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/request/FeedCreateRequest.java | 3 --- .../thip/feed/application/port/in/dto/FeedCreateCommand.java | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java index f695ea8bf..a4a518638 100644 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedCreateRequest.java @@ -17,8 +17,6 @@ public record FeedCreateRequest( @NotNull(message = "방 공개 설정 여부는 필수입니다.") Boolean isPublic, - String category, - List tagList ) { public FeedCreateCommand toCommand(Long userId) { @@ -26,7 +24,6 @@ public FeedCreateCommand toCommand(Long userId) { isbn, contentBody, isPublic, - category, tagList, userId ); diff --git a/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java index 6d0ba81c9..a5922a6fa 100644 --- a/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java +++ b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedCreateCommand.java @@ -10,8 +10,6 @@ public record FeedCreateCommand( Boolean isPublic, - String category, - List tagList, Long userId From e8df46c3cf793a683620c9af3dd99f31824fa2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:52:42 +0900 Subject: [PATCH 07/30] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=20response=20dto=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/response/FeedIdResponse.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIdResponse.java diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIdResponse.java b/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIdResponse.java new file mode 100644 index 000000000..22e68ccba --- /dev/null +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedIdResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.feed.adapter.in.web.response; + +public record FeedIdResponse(Long feedId) { + public static FeedIdResponse of(Long feedId) { + return new FeedIdResponse(feedId); + } +} From 9540bf94b90f02e435ab9df3208353fd2bda6bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:52:50 +0900 Subject: [PATCH 08/30] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=20response=20dto=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feed/adapter/in/web/response/FeedCreateResponse.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java b/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java deleted file mode 100644 index 5c8dce218..000000000 --- a/src/main/java/konkuk/thip/feed/adapter/in/web/response/FeedCreateResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.feed.adapter.in.web.response; - -public record FeedCreateResponse(Long feedId) { - public static FeedCreateResponse of(Long feedId) { - return new FeedCreateResponse(feedId); - } -} From 26c43eda6f004dc3c8fd96254b57c747972d730b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:53:03 +0900 Subject: [PATCH 09/30] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=EC=8B=9C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B2=80=EC=A6=9D=20=EC=95=88?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?s3=20=EC=82=AD=EC=A0=9C=EA=B4=80=EB=A0=A8=20todo=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FeedCreateService.java | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java index 10edae5cd..e6fd35c16 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java @@ -10,7 +10,6 @@ import konkuk.thip.feed.application.port.out.FeedCommandPort; import konkuk.thip.feed.application.port.out.S3CommandPort; import konkuk.thip.feed.domain.Feed; -import konkuk.thip.room.application.port.out.RoomCommandPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,33 +22,30 @@ public class FeedCreateService implements FeedCreateUseCase { private final S3CommandPort s3CommandPort; - private final RoomCommandPort roomCommandPort; private final BookCommandPort bookCommandPort; private final FeedCommandPort feedCommandPort; private final BookApiQueryPort bookApiQueryPort; @Override @Transactional + //TODO 추후 예외 발생시 이미 s3에 업로드된 이미지 삭제 방식 논의 public Long createFeed(FeedCreateCommand command, List images) { // 1. 피드 생성 비지니스 정책 검증 - Feed.validateCategoryAndTags(command.category(), command.tagList()); + Feed.validateTags(command.tagList()); Feed.validateImageCount(images != null ? images.size() : 0); - - // 2. Category 검증 및 조회 - validateCategoryAndTagList(command.category(), command.tagList()); - - // 3. Book 검증 및 조회 + // 2. Book 검증 및 조회 Long targetBookId = findOrCreateBookByIsbn(command.isbn()); - // 4. 이미지 업로드 - List imageUrls = (images == null || images.isEmpty()) - ? List.of() - : s3CommandPort.uploadImages(images); - - // 5. Feed 생성 및 저장 (Content도 함께 생성 및 저장 애그리거트 루트인 Feed가 생성책임 가지고있음) + // 3. 이미지 업로드 + List imageUrls = null; try { + imageUrls = (images == null || images.isEmpty()) + ? List.of() + : s3CommandPort.uploadImages(images); + + // 4. Feed 생성 및 저장 (Content도 함께 생성 및 저장 애그리거트 루트인 Feed가 생성책임 가지고있음) Feed feed = Feed.withoutId( command.contentBody(), command.userId(), @@ -61,25 +57,13 @@ public Long createFeed(FeedCreateCommand command, List images) { return feedCommandPort.save(feed); } catch (Exception e) { - if (imageUrls != null) { + if (imageUrls != null && !imageUrls.isEmpty()) { s3CommandPort.deleteImages(imageUrls); } throw e; } } - // TODO: 카테고리, 태그 관계가 명확해지면 카테고리 내의 도메인에서 검증하도록 리팩토링 예정 - private void validateCategoryAndTagList(String categoryValue, List tagList) { - - boolean hasCategoryAndTags = categoryValue != null && !categoryValue.trim().isEmpty() - && tagList != null && !tagList.isEmpty(); - - // Category 검증 및 조회 - if(hasCategoryAndTags) { roomCommandPort.findCategoryByValue(categoryValue); } - - // TODO: Category로 tagList 검증 - } - /** * ISBN으로 책을 조회하고, 없으면 외부 API(Naver)에서 상세 정보를 조회해 새로 저장 후 ID 반환 */ From b09b5058ebfbd0767993004512610a9f52328f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:53:39 +0900 Subject: [PATCH 10/30] =?UTF-8?q?[feat]=20jpa=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=ED=95=9C=EB=B2=88=EC=97=90=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=ED=95=98=EB=8A=94=20updateFrom=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index 26e140844..cd184408e 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.feed.domain.Feed; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.AccessLevel; @@ -40,4 +41,12 @@ public FeedJpaEntity(String content, Integer likeCount, Integer commentCount, Us this.bookJpaEntity = bookJpaEntity; this.contentList = contentList; } + + public void updateFrom(Feed feed) { + this.content = feed.getContent(); + this.isPublic = feed.getIsPublic(); + this.reportCount = feed.getReportCount(); + this.likeCount = feed.getLikeCount(); + this.commentCount = feed.getCommentCount(); + } } \ No newline at end of file From 310cb9f03c06b111100565f8826f3698b04cd330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:54:35 +0900 Subject: [PATCH 11/30] =?UTF-8?q?[fix]=20feed<->content=20=EC=96=91?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EB=A7=A4=ED=95=91=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=9B=84,=20=ED=94=BC=EB=93=9C=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=EC=8B=9C=20=EB=88=84=EB=9D=BD=EB=90=98=EC=97=88?= =?UTF-8?q?=EB=8D=98=20content=20=EB=A7=A4=ED=95=91=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/feed/adapter/out/mapper/FeedMapper.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java b/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java index dfb48382f..15581ae27 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/mapper/FeedMapper.java @@ -6,14 +6,18 @@ import konkuk.thip.feed.domain.Feed; import konkuk.thip.feed.domain.Tag; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Component +@RequiredArgsConstructor public class FeedMapper { + private final ContentMapper contentMapper; + public FeedJpaEntity toJpaEntity(Feed feed, UserJpaEntity userJpaEntity, BookJpaEntity bookJpaEntity) { return FeedJpaEntity.builder() .content(feed.getContent()) @@ -41,6 +45,9 @@ public Feed toDomainEntity(FeedJpaEntity feedJpaEntity, List tagJp .map(TagJpaEntity::getValue) .map(Tag::from) .toList()) + .contentList(feedJpaEntity.getContentList().stream() + .map(contentMapper::toDomainEntity) + .toList()) .createdAt(feedJpaEntity.getCreatedAt()) .modifiedAt(feedJpaEntity.getModifiedAt()) .status(feedJpaEntity.getStatus()) From 7abfbdf72feb06f508eed8e6d3f8158c5f14edb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:55:43 +0900 Subject: [PATCH 12/30] =?UTF-8?q?[test]=20=ED=94=BC=EB=93=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/FeedCreateControllerTest.java | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java index 33ed9fa47..b755cdf51 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedCreateControllerTest.java @@ -18,7 +18,7 @@ import java.util.Map; import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; -import static konkuk.thip.common.exception.code.ErrorCode.INVALID_FEED_CREATE; +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_FEED_COMMAND; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -55,7 +55,7 @@ private void assertBadRequest_InvalidFeedCreate(Map request, Str .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) .requestAttr("userId", 1L)) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(INVALID_FEED_CREATE.getCode())) + .andExpect(jsonPath("$.code").value(INVALID_FEED_COMMAND.getCode())) .andExpect(jsonPath("$.message", containsString(message))); } @@ -102,24 +102,8 @@ void missing_is_public() throws Exception { } @Nested - @DisplayName("카테고리/태그 입력 불일치 검증") - class CategoryTagValidation { - - @Test - @DisplayName("카테고리만 있고 태그가 없을 때 400 반환") - void onlyCategory() throws Exception { - Map req = buildValidRequest(); - req.put("tagList", List.of()); - assertBadRequest_InvalidFeedCreate(req, "카테고리와 태그는 모두 입력되거나 모두 비워져야 합니다."); - } - - @Test - @DisplayName("태그만 있고 카테고리가 없을 때 400 반환") - void onlyTags() throws Exception { - Map req = buildValidRequest(); - req.put("category", null); - assertBadRequest_InvalidFeedCreate(req, "카테고리와 태그는 모두 입력되거나 모두 비워져야 합니다."); - } + @DisplayName("태그 입력 불일치 검증") + class TagValidation { @Test @DisplayName("태그가 6개 이상이면 400 반환") @@ -171,7 +155,7 @@ void tooManyImages() throws Exception { ); result.andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(INVALID_FEED_CREATE.getCode())) + .andExpect(jsonPath("$.code").value(INVALID_FEED_COMMAND.getCode())) .andExpect(jsonPath("$.message",containsString("이미지는 최대 3개까지 업로드할 수 있습니다."))); } From 2e5b31e556a9d629bd6ad22331abe5a1c758d982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:55:54 +0900 Subject: [PATCH 13/30] =?UTF-8?q?[test]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/FeedUpdateAPITest.java | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateAPITest.java diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateAPITest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateAPITest.java new file mode 100644 index 000000000..91dbb2406 --- /dev/null +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateAPITest.java @@ -0,0 +1,216 @@ +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.util.TestEntityFactory; +import konkuk.thip.config.TestS3MockConfig; +import konkuk.thip.feed.adapter.out.jpa.ContentJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.FeedTagJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.Content.ContentJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedTag.FeedTagJpaRepository; +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.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.context.annotation.Import; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@Import(TestS3MockConfig.class) +@DisplayName("[통합] 피드 수정 api 통합 테스트") +class FeedUpdateAPITest { + + @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 FeedTagJpaRepository feedTagJpaRepository; + @Autowired private ContentJpaRepository contentJpaRepository; + + private UserJpaEntity user; + private BookJpaEntity book; + private FeedJpaEntity feed; + private TagJpaEntity tag1; + private TagJpaEntity tag2; + private TagJpaEntity tag3; + + @BeforeEach + void setUp() { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); + user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createLiteratureCategory(alias)); + + tag1 = tagJpaRepository.save(TestEntityFactory.createTag(category, "소설추천")); + tag2 = tagJpaRepository.save(TestEntityFactory.createTag(category, "책추천")); + tag3 = tagJpaRepository.save(TestEntityFactory.createTag(category, "오늘의책")); + book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + feed = feedJpaRepository.save(TestEntityFactory.createFeed(user,book, true)); + } + + @Test + @DisplayName("여러 태그가 있는 피드에서 태그를 수정하면 최종 태그로 반영되어 저장된다.") + void updateTaggedFeed_shouldUpdateTagsCorrectly() throws Exception { + + // given + Long feedId = feed.getPostId(); + + // 기존 태그 3개 연관 + List existingTags = List.of(tag1, tag2, tag3); + List mappings = existingTags.stream() + .map(tag -> TestEntityFactory.createFeedTagMapping(feed, tag)) + .toList(); + + feedTagJpaRepository.saveAll(mappings); + + // 수정 요청 + Map request = new HashMap<>(); + request.put("contentBody", "태그 갱신 테스트"); + request.put("isPublic", false); + request.put("tagList", List.of("소설추천", "오늘의책")); // 하나 제거됨 + + // when + ResultActions result = mockMvc.perform(patch("/feeds/{feedId}", feedId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()); + long tagCount = feedTagJpaRepository.findAll().stream() + .filter(ft -> ft.getFeedJpaEntity().getPostId().equals(feedId)) + .count(); + assertThat(tagCount).isEqualTo(2); + } + + @Test + @DisplayName("이미지가 여러 개인 피드에서 이미지 일부만 유지하도록 수정할 수 있다.") + void updateImageFeed_shouldRetainSomeImagesOnly() throws Exception { + + // given + List originalImages = List.of( + "https://s3-mock/image-1.jpg", + "https://s3-mock/image-2.jpg", + "https://s3-mock/image-3.jpg" + ); + + FeedJpaEntity feed = feedJpaRepository.save(TestEntityFactory.createFeedWithContents(user, book, originalImages, true)); + Long feedId = feed.getPostId(); + + // 수정 요청: 이미지 1개만 유지 + Map request = new HashMap<>(); + request.put("contentBody", "이미지 삭제 테스트"); + request.put("isPublic", false); + request.put("remainImageUrls", List.of("https://s3-mock/image-2.jpg")); // 나머지 삭제 + + // when + ResultActions result = mockMvc.perform(patch("/feeds/{feedId}", feedId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()); + FeedJpaEntity updatedFeed = feedJpaRepository.findById(feedId).orElseThrow(); + assertThat(updatedFeed.getContentList()).hasSize(1); + assertThat(updatedFeed.getContentList().get(0).getContentUrl()).isEqualTo("https://s3-mock/image-2.jpg"); + List contentRows = contentJpaRepository.findAll().stream() + .filter(c -> c.getPostJpaEntity().getPostId().equals(feedId)) + .toList(); + assertThat(contentRows).hasSize(1); + assertThat(contentRows.get(0).getContentUrl()).isEqualTo("https://s3-mock/image-2.jpg"); + } + + @Test + @DisplayName("피드의 내용, 공개 여부, 태그, 이미지 전체를 모두 수정할 수 있다.") + void updateFeedWithAllFields_shouldModifyEverythingCorrectly() throws Exception { + + // given + // 기존 이미지 3개 + List originalImages = List.of( + "https://s3-mock/image-1.jpg", + "https://s3-mock/image-2.jpg", + "https://s3-mock/image-3.jpg" + ); + FeedJpaEntity feed = feedJpaRepository.save(TestEntityFactory.createFeedWithContents(user, book, originalImages, true)); + Long feedId = feed.getPostId(); + + // 기존 태그 3개 매핑 + List existingTags = List.of(tag1, tag2, tag3); + List tagMappings = existingTags.stream() + .map(tag -> TestEntityFactory.createFeedTagMapping(feed, tag)) + .collect(Collectors.toList()); + feedTagJpaRepository.saveAll(tagMappings); + + // 수정 요청: 태그 일부 삭제 & 이미지 일부 삭제 & 본문 변경 & 공개 여부 변경 + Map request = new HashMap<>(); + request.put("contentBody", "전부 수정되는 피드 테스트"); + request.put("isPublic", false); + request.put("remainImageUrls", List.of("https://s3-mock/image-2.jpg")); // 이미지 1개 유지 + request.put("tagList", List.of("소설추천", "오늘의책")); // 태그 2개만 남김 + + // when + ResultActions result = mockMvc.perform(patch("/feeds/{feedId}", feedId) + .requestAttr("userId", user.getUserId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()); + + FeedJpaEntity updated = feedJpaRepository.findById(feedId).orElseThrow(); + // 1. 본문 + assertThat(updated.getContent()).isEqualTo("전부 수정되는 피드 테스트"); + // 2. 공개 여부 + assertThat(updated.getIsPublic()).isFalse(); + // 3. 이미지 + assertThat(updated.getContentList()).hasSize(1); + assertThat(updated.getContentList().get(0).getContentUrl()).isEqualTo("https://s3-mock/image-2.jpg"); + List contentRows = contentJpaRepository.findAll().stream() + .filter(c -> c.getPostJpaEntity().getPostId().equals(feedId)) + .toList(); + assertThat(contentRows).hasSize(1); + assertThat(contentRows.get(0).getContentUrl()).isEqualTo("https://s3-mock/image-2.jpg"); + // 4. 태그 갯수 + long tagCount = feedTagJpaRepository.findAll().stream() + .filter(tag -> tag.getFeedJpaEntity().getPostId().equals(feedId)) + .count(); + assertThat(tagCount).isEqualTo(2); + } + +} From 7cc67d1d56e1bc91e9f835016baa45f45f788cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:56:12 +0900 Subject: [PATCH 14/30] =?UTF-8?q?[test]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/FeedUpdateControllerTest.java | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java new file mode 100644 index 000000000..f9127c804 --- /dev/null +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java @@ -0,0 +1,208 @@ +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.util.TestEntityFactory; +import konkuk.thip.feed.adapter.out.persistence.repository.Content.ContentJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedTag.FeedTagJpaRepository; +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.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.Nested; +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.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@DisplayName("[단위] 피드 생성 api controller 단위 테스트") +class FeedUpdateControllerTest { + + @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 FeedTagJpaRepository feedTagJpaRepository; + @Autowired private ContentJpaRepository contentJpaRepository; + + private Long savedFeedId; + private Long creatorUserId; + + @BeforeEach + void setUp() { + + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); + UserJpaEntity 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, "오늘의책")); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBookWithISBN("9788954682152")); + savedFeedId = feedJpaRepository.save(TestEntityFactory.createFeed(user,book, true)).getPostId(); + creatorUserId = user.getUserId(); + } + + private Map buildValidUpdateRequest() { + Map request = new HashMap<>(); + request.put("contentBody", "수정된 테스트 콘텐츠"); + request.put("isPublic", true); + request.put("tagList", List.of("책추천", "소설추천")); + request.put("remainImageUrls", List.of()); + return request; + } + + private void assertBadRequest(int expectedCode, Map request, String message) throws Exception { + mockMvc.perform(patch("/feeds/1") + .requestAttr("userId", 100L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(request)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(expectedCode)) + .andExpect(jsonPath("$.message", containsString(message))); + } + + @Nested + @DisplayName("피드 관련 검증") + class BasicValidation { + + @Test + @DisplayName("존재하지 않는 피드를 삭제(수정)하려는 경우 404 반환") + void deleteNonExistentFeed() throws Exception { + Map req = buildValidUpdateRequest(); + mockMvc.perform(patch("/feeds/99999") + .requestAttr("userId", 100L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(req)) + ) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(FEED_NOT_FOUND.getCode())) + .andExpect(jsonPath("$.message", containsString("존재하지 않는 FEED 입니다."))); + } + + @Test + @DisplayName("피드 생성자가 아닌 유저가 수정하려는 경우 400 반환") + void unauthorizedFeedEdit() throws Exception { + Map req = buildValidUpdateRequest(); + mockMvc.perform(patch("/feeds/" + savedFeedId) + .requestAttr("userId",100L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(req)) + ) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(FEED_UPDATE_FORBIDDEN.getCode())) + .andExpect(jsonPath("$.message", containsString("피드 수정 권한이 없습니다."))); + } + + } + + @Nested + @DisplayName("태그 검증") + class TagValidation { + + @Test + @DisplayName("태그리스트 중 존재하지 않는 태그가 있을 때 400 반환") + void invalidTagNames() throws Exception { + Map req = buildValidUpdateRequest(); + req.put("tagList", List.of("에세이", "휴식", "힐링")); + mockMvc.perform(patch("/feeds/" + savedFeedId) + .requestAttr("userId",creatorUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(req)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(TAG_NAME_NOT_MATCH.getCode())) + .andExpect(jsonPath("$.message", containsString("일치하는 태그 이름이 없습니다"))) + .andExpect(jsonPath("$.message", containsString("에세이"))) + .andExpect(jsonPath("$.message", containsString("휴식"))) + .andExpect(jsonPath("$.message", containsString("힐링"))); + } + + @Test + @DisplayName("태그가 5개 초과일 경우 400 반환") + void tooManyTags() throws Exception { + Map req = buildValidUpdateRequest(); + req.put("tagList", List.of("t1","t2","t3","t4","t5","t6")); + assertBadRequest(INVALID_FEED_COMMAND.getCode(), req, "태그는 최대 5개까지 입력할 수 있습니다."); + } + + @Test + @DisplayName("태그가 중복되어 있을 경우 400 반환") + void duplicatedTags() throws Exception { + Map req = buildValidUpdateRequest(); + req.put("tagList", List.of("중복", "중복")); + assertBadRequest(INVALID_FEED_COMMAND.getCode(), req, "태그는 중복 될 수 없습니다."); + } + + } + + @Nested + @DisplayName("이미지 검증") + class ImageValidation { + + @Test + @DisplayName("이미지가 3개 초과되면 400 반환") + void tooManyImages() throws Exception { + Map req = buildValidUpdateRequest(); + req.put("remainImageUrls", + List.of("https://s3.../profile1.png", "https://s3.../profile2.png", + "https://s3.../profile3.png", "https://s3.../profile4.png")); + mockMvc.perform(patch("/feeds/" + savedFeedId) + .requestAttr("userId",creatorUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(req)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(INVALID_FEED_COMMAND.getCode())) + .andExpect(jsonPath("$.message",containsString("이미지는 최대 3개까지 업로드할 수 있습니다."))); + } + + @Test + @DisplayName("이미지 url이 잘못되었을 때 400 반환") + void invalidImageUrl() throws Exception { + Map req = buildValidUpdateRequest(); + req.put("remainImageUrls", List.of("https://s3.../profile1.png")); + mockMvc.perform(patch("/feeds/" + savedFeedId) + .requestAttr("userId",creatorUserId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsBytes(req)) + ) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(INVALID_FEED_COMMAND.getCode())) + .andExpect(jsonPath("$.message", containsString("해당 이미지는 이 피드에 존재하지 않습니다"))); + } + } + +} From e84d924c7f2c8beac9e4f2b77c39395ca4b28b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:56:45 +0900 Subject: [PATCH 15/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A6=AC=EC=97=90=20=ED=94=BC=EB=93=9C/?= =?UTF-8?q?=ED=94=BC=EB=93=9C=ED=83=9C=EA=B7=B8/=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=ED=8F=AC=ED=95=A8=ED=94=BC=EB=93=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/TestEntityFactory.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 08a38c86e..e03ddb30a 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -2,6 +2,9 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.ContentJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.jpa.FeedTagJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; @@ -16,6 +19,8 @@ import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; public class TestEntityFactory { @@ -169,4 +174,52 @@ public static TagJpaEntity createTag(CategoryJpaEntity category,String value) { .value(value) .build(); } + + public static FeedJpaEntity createFeed(UserJpaEntity user, BookJpaEntity book, boolean isPublic) { + + return FeedJpaEntity.builder() + .content("기본 피드 본문입니다.") + .isPublic(isPublic) + .likeCount(0) + .commentCount(0) + .reportCount(0) + .userJpaEntity(user) + .bookJpaEntity(book) + .contentList(new ArrayList<>()) + .build(); + } + + public static FeedTagJpaEntity createFeedTagMapping(FeedJpaEntity feed, TagJpaEntity tag) { + return FeedTagJpaEntity.builder() + .feedJpaEntity(feed) + .tagJpaEntity(tag) + .build(); + } + + + public static FeedJpaEntity createFeedWithContents(UserJpaEntity user, BookJpaEntity book, List imageUrls, boolean isPublic) { + + FeedJpaEntity feed = FeedJpaEntity.builder() + .content("이미지 포함 피드") + .isPublic(isPublic) + .likeCount(0) + .commentCount(0) + .reportCount(0) + .userJpaEntity(user) + .bookJpaEntity(book) + .contentList(new ArrayList<>()) + .build(); + + List contents = imageUrls.stream() + .map(url -> ContentJpaEntity.builder() + .contentUrl(url) + .postJpaEntity(feed) + .build()) + .toList(); + + feed.getContentList().addAll(contents); + return feed; + } + + } \ No newline at end of file From 88211fdf28c40a0d9f5e1489beb8d2df8687ad93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:57:14 +0900 Subject: [PATCH 16/30] [feat] TagJpaRepository.findAllByFeedId (#86) --- .../out/persistence/repository/Tag/TagJpaRepository.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java index 88c778a9b..176244ecd 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/Tag/TagJpaRepository.java @@ -2,9 +2,16 @@ import konkuk.thip.feed.adapter.out.jpa.TagJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface TagJpaRepository extends JpaRepository{ Optional findByValue(String value); + + @Query("SELECT ft.tagJpaEntity FROM FeedTagJpaEntity ft WHERE ft.feedJpaEntity.postId = :feedId") + List findAllByFeedId(@Param("feedId") Long feedId); + } From 41213827f16b31735d2bef3976b10e0a6c5fa41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:57:52 +0900 Subject: [PATCH 17/30] [feat] FeedTagJpaRepository.deleteAllByFeedJpaEntity (#86) --- .../repository/FeedTag/FeedTagJpaRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 16c932218..f8d37dcbd 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 @@ -1,7 +1,16 @@ package konkuk.thip.feed.adapter.out.persistence.repository.FeedTag; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.feed.adapter.out.jpa.FeedTagJpaEntity; 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; public interface FeedTagJpaRepository extends JpaRepository{ + + @Modifying + @Query("DELETE FROM FeedTagJpaEntity ft WHERE ft.feedJpaEntity = :feedJpaEntity") + void deleteAllByFeedJpaEntity(@Param("feedJpaEntity") FeedJpaEntity feedJpaEntity); + } From cbb46c897386f255b75e32c51c92bfe48a562649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:58:02 +0900 Subject: [PATCH 18/30] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/dto/FeedUpdateCommand.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/port/in/dto/FeedUpdateCommand.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedUpdateCommand.java b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedUpdateCommand.java new file mode 100644 index 000000000..758a3e91f --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/port/in/dto/FeedUpdateCommand.java @@ -0,0 +1,20 @@ +package konkuk.thip.feed.application.port.in.dto; + +import java.util.List; + +public record FeedUpdateCommand( + + String contentBody, + + Boolean isPublic, + + List tagList, + + List remainImageUrls, + + Long userId, + + Long feedId +) +{ +} From 0431eb773059c6584e25af0bbb59712fbfde012c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:58:11 +0900 Subject: [PATCH 19/30] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20request=20dto=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/request/FeedUpdateRequest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java diff --git a/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java new file mode 100644 index 000000000..d18433731 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/adapter/in/web/request/FeedUpdateRequest.java @@ -0,0 +1,27 @@ +package konkuk.thip.feed.adapter.in.web.request; + +import konkuk.thip.feed.application.port.in.dto.FeedUpdateCommand; + +import java.util.List; + +public record FeedUpdateRequest( + + String contentBody, + + Boolean isPublic, + + List tagList, + + List remainImageUrls +) { + public FeedUpdateCommand toCommand(Long userId, Long feedId) { + return new FeedUpdateCommand( + contentBody, + isPublic, + tagList, + remainImageUrls, + userId, + feedId + ); + } +} From 5af7c9ce3a82f4e5070dbac29ca5da9323c42b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:58:26 +0900 Subject: [PATCH 20/30] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FeedUpdateService.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java new file mode 100644 index 000000000..ef1282c71 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java @@ -0,0 +1,72 @@ +package konkuk.thip.feed.application.service; + +import konkuk.thip.feed.application.port.in.FeedUpdateUseCase; +import konkuk.thip.feed.application.port.in.dto.FeedUpdateCommand; +import konkuk.thip.feed.application.port.out.FeedCommandPort; +import konkuk.thip.feed.application.port.out.S3CommandPort; +import konkuk.thip.feed.domain.Content; +import konkuk.thip.feed.domain.Feed; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FeedUpdateService implements FeedUpdateUseCase { + + private final S3CommandPort s3CommandPort; + private final FeedCommandPort feedCommandPort; + + @Override + @Transactional + public Long updateFeed(FeedUpdateCommand command) { + + // 1. 유효성 검증 + Feed.validateTags(command.tagList()); + Feed.validateImageCount(command.remainImageUrls() != null ? command.remainImageUrls().size() : 0); + + // 2. 피드 조회 및 유효성 검증 + Feed feed = feedCommandPort.findById(command.feedId()); + feed.validateCreator(command.userId()); + + // 3. 도메인 내부 상태 변경 + applyPartialFeedUpdate(feed, command); + + // 4. 업데이트 + return feedCommandPort.update(feed); + } + + private void applyPartialFeedUpdate(Feed feed, FeedUpdateCommand command) { + + if (command.remainImageUrls() != null) { + feed.validateOwnsImages(command.remainImageUrls()); + feed.updateImages(command.remainImageUrls()); + } + if (command.contentBody() != null) { + feed.updateContent(command.contentBody()); + } + if (command.isPublic() != null) { + feed.updateVisibility(command.isPublic()); + } + if (command.tagList() != null) { + feed.updateTags(command.tagList()); + } + } + + //TODO 추후 이벤트 기반으로 트랜잭션 커밋후 S3 삭제하도록 리펙토링 or 사용하지 않는 이미지 배치 삭제방식 논의 + private void handleFeedImageDelete(Feed feed, List remainImageUrls) { + List oldImageUrls = feed.getContentList().stream() + .map(Content::getContentUrl) + .filter(url -> url != null && !url.isBlank()) + .toList(); + + List toDelete = oldImageUrls.stream() + .filter(url -> !remainImageUrls.contains(url)) + .toList(); + if (!toDelete.isEmpty()) { + s3CommandPort.deleteImages(toDelete); + } + } +} From b4f35f1057333814a0b6fb19ab34e91fac60659a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:58:33 +0900 Subject: [PATCH 21/30] =?UTF-8?q?[feat]=20=ED=94=BC=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/application/port/in/FeedUpdateUseCase.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/konkuk/thip/feed/application/port/in/FeedUpdateUseCase.java diff --git a/src/main/java/konkuk/thip/feed/application/port/in/FeedUpdateUseCase.java b/src/main/java/konkuk/thip/feed/application/port/in/FeedUpdateUseCase.java new file mode 100644 index 000000000..d94530a60 --- /dev/null +++ b/src/main/java/konkuk/thip/feed/application/port/in/FeedUpdateUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.feed.application.port.in; + +import konkuk.thip.feed.application.port.in.dto.FeedUpdateCommand; + +public interface FeedUpdateUseCase { + Long updateFeed(FeedUpdateCommand command); +} \ No newline at end of file From b4464d65c3124a019cac3dbdcf6ece2f094278d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:59:10 +0900 Subject: [PATCH 22/30] =?UTF-8?q?[refactor]=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=8B=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=EA=B0=80=20=EB=B6=80=EB=AA=A8=20=ED=95=84=EB=93=9C=20=EA=B0=92?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=9C=EC=96=B4=EC=9E=90=20protected=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java index 35fce5ada..ccb8710c8 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java @@ -21,11 +21,11 @@ public abstract class PostJpaEntity extends BaseJpaEntity { private Long postId; @Column(length = 6100, nullable = false) - private String content; + protected String content; - private Integer likeCount = 0; + protected Integer likeCount = 0; - private Integer commentCount = 0; + protected Integer commentCount = 0; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) From c8e4b61f7cff0b90ba1fcb6cacab17531ead6a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 13:59:25 +0900 Subject: [PATCH 23/30] =?UTF-8?q?[refactor]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=82=AC?= =?UTF-8?q?=EB=9D=BC=EC=A7=80=EB=A9=B4=EC=84=9C=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/RoomCommandPersistenceAdapter.java | 8 -------- .../thip/room/application/port/out/RoomCommandPort.java | 2 -- 2 files changed, 10 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java index 97801088d..b2f14f44e 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java @@ -9,7 +9,6 @@ import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.application.port.out.RoomCommandPort; -import konkuk.thip.room.domain.Category; import konkuk.thip.room.domain.Room; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -48,13 +47,6 @@ public Long save(Room room) { return roomJpaRepository.save(roomJpaEntity).getRoomId(); } - @Override - public Category findCategoryByValue(String value) { - CategoryJpaEntity categoryJpaEntity = categoryJpaRepository.findByValue(value).orElseThrow( - () -> new EntityNotFoundException(CATEGORY_NOT_FOUND)); - return Category.from(categoryJpaEntity.getValue()); - } - @Override public void updateMemberCount(Room room) { RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(room.getId()).orElseThrow( diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java index 878aa8dc8..cd517429e 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java @@ -9,7 +9,5 @@ public interface RoomCommandPort { Long save(Room room); - Category findCategoryByValue(String value); - void updateMemberCount(Room room); } From 7311c5851e20958323b44b049c089e13d357472b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 14:06:54 +0900 Subject: [PATCH 24/30] =?UTF-8?q?[refactor]=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/room/application/port/out/RoomCommandPort.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java index 2d4128421..75071b2df 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java @@ -19,8 +19,5 @@ default Room getByIdOrThrow(Long id) { Long save(Room room); - void updateMemberCount(Room room); - Category findCategoryByValue(String value); - void update(Room room); } From ce71420273820157279d4802e730cca2db50938d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 14:37:35 +0900 Subject: [PATCH 25/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/FeedUpdateControllerTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java index f9127c804..98af7d644 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java @@ -4,9 +4,7 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; import konkuk.thip.common.util.TestEntityFactory; -import konkuk.thip.feed.adapter.out.persistence.repository.Content.ContentJpaRepository; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; -import konkuk.thip.feed.adapter.out.persistence.repository.FeedTag.FeedTagJpaRepository; 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; @@ -40,7 +38,7 @@ @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @Transactional -@DisplayName("[단위] 피드 생성 api controller 단위 테스트") +@DisplayName("[단위] 피드 수정 api controller 단위 테스트") class FeedUpdateControllerTest { @Autowired @@ -53,8 +51,6 @@ class FeedUpdateControllerTest { @Autowired private BookJpaRepository bookJpaRepository; @Autowired private TagJpaRepository tagJpaRepository; @Autowired private FeedJpaRepository feedJpaRepository; - @Autowired private FeedTagJpaRepository feedTagJpaRepository; - @Autowired private ContentJpaRepository contentJpaRepository; private Long savedFeedId; private Long creatorUserId; From fc79d91e5c61b6e5323a91a8af2d32232e0ee98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 14:39:47 +0900 Subject: [PATCH 26/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/FeedUpdateControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java index 98af7d644..f8f52c6d7 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java @@ -95,8 +95,8 @@ private void assertBadRequest(int expectedCode, Map request, Str class BasicValidation { @Test - @DisplayName("존재하지 않는 피드를 삭제(수정)하려는 경우 404 반환") - void deleteNonExistentFeed() throws Exception { + @DisplayName("존재하지 않는 피드를 수정하려는 경우 404 반환") + void updateNonExistentFeed() throws Exception { Map req = buildValidUpdateRequest(); mockMvc.perform(patch("/feeds/99999") .requestAttr("userId", 100L) From 17f44087549def185056b56a5c4ff20d537b21d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 14:50:59 +0900 Subject: [PATCH 27/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/FeedUpdateControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java index f8f52c6d7..1b466ed7b 100644 --- a/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/in/web/FeedUpdateControllerTest.java @@ -109,7 +109,7 @@ void updateNonExistentFeed() throws Exception { } @Test - @DisplayName("피드 생성자가 아닌 유저가 수정하려는 경우 400 반환") + @DisplayName("피드 생성자가 아닌 유저가 수정하려는 경우 403 반환") void unauthorizedFeedEdit() throws Exception { Map req = buildValidUpdateRequest(); mockMvc.perform(patch("/feeds/" + savedFeedId) From bd8adf8a435933754491c91adc7a5234d7608e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 21 Jul 2025 17:35:12 +0900 Subject: [PATCH 28/30] =?UTF-8?q?[refactor]=20final=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/feed/adapter/in/web/FeedCommandController.java | 4 ++-- 1 file changed, 2 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..713a0e429 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 @@ -33,9 +33,9 @@ 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)))); From 89e73f79da24824f306f4aeef60032bc1b5aba03 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 04:25:19 +0900 Subject: [PATCH 29/30] =?UTF-8?q?[refactor]=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=99=20=EC=B6=94=EA=B0=80=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FeedUpdateService.java | 16 ++++++------- .../java/konkuk/thip/feed/domain/Feed.java | 23 ++++++++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) 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..182f236c8 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedUpdateService.java @@ -23,15 +23,14 @@ public class FeedUpdateService implements FeedUpdateUseCase { @Transactional public Long updateFeed(FeedUpdateCommand command) { - // 1. 유효성 검증 + //1. 유효성 검증 Feed.validateTags(command.tagList()); Feed.validateImageCount(command.remainImageUrls() != null ? command.remainImageUrls().size() : 0); - // 2. 피드 조회 및 유효성 검증 + // 2. 피드 조회 Feed feed = feedCommandPort.findById(command.feedId()); - feed.validateCreator(command.userId()); - // 3. 도메인 내부 상태 변경 + // 3. 도메인 내에서 내부 상태 변경 및 검증 applyPartialFeedUpdate(feed, command); // 4. 업데이트 @@ -41,17 +40,16 @@ public Long updateFeed(FeedUpdateCommand command) { private void applyPartialFeedUpdate(Feed feed, FeedUpdateCommand command) { if (command.remainImageUrls() != null) { - feed.validateOwnsImages(command.remainImageUrls()); - feed.updateImages(command.remainImageUrls()); + feed.updateImages(command.userId(), command.remainImageUrls()); } if (command.contentBody() != null) { - feed.updateContent(command.contentBody()); + feed.updateContent(command.userId(), command.contentBody()); } if (command.isPublic() != null) { - feed.updateVisibility(command.isPublic()); + feed.updateVisibility(command.userId(), command.isPublic()); } if (command.tagList() != null) { - feed.updateTags(command.tagList()); + feed.updateTags(command.userId(), command.tagList()); } } diff --git a/src/main/java/konkuk/thip/feed/domain/Feed.java b/src/main/java/konkuk/thip/feed/domain/Feed.java index 6f6eae08e..1f50c9e5c 100644 --- a/src/main/java/konkuk/thip/feed/domain/Feed.java +++ b/src/main/java/konkuk/thip/feed/domain/Feed.java @@ -44,6 +44,9 @@ public class Feed extends BaseDomainEntity { public static Feed withoutId(String content, Long creatorId, Boolean isPublic, Long targetBookId, List tagValues, List imageUrls) { + validateTags(tagValues); + validateImageCount(imageUrls != null ? imageUrls.size() : 0); + return Feed.builder() .id(null) .content(content) @@ -95,20 +98,28 @@ public void validateCreator(Long userId) { } } - public void updateContent(String newContent) { + public void updateContent(Long userId, String newContent) { + validateCreator(userId); this.content = newContent; } - public void updateVisibility(Boolean isPublic) { + public void updateVisibility(Long userId, Boolean isPublic) { + validateCreator(userId); this.isPublic = isPublic; } - public void updateTags(List tagValues) { - this.tagList = Tag.fromList(tagValues); + public void updateTags(Long userId, List newTagValues) { + validateCreator(userId); + validateTags(newTagValues); + this.tagList = Tag.fromList(newTagValues); // Tag.from(...) 등으로 변환 } - public void updateImages(List imageUrls) { - this.contentList = convertToContentList(imageUrls); + public void updateImages(Long userId, List newImageUrls) { + validateCreator(userId); + validateImageCount(newImageUrls.size()); + validateOwnsImages(newImageUrls); + + this.contentList = convertToContentList(newImageUrls); } public void validateOwnsImages(List candidateImageUrls) { From f6601333b60623cd5c75c3203f7bf7cc498df671 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 04:25:32 +0900 Subject: [PATCH 30/30] =?UTF-8?q?[refactor]=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedCommandPersistenceAdapter.java | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) 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..3ff52382b 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 @@ -61,9 +61,9 @@ public Long save(Feed feed) { FeedJpaEntity savedFeed = feedJpaRepository.save(feedJpaEntity); // Content가 존재하면 ContentJpaEntity 생성 및 Feed 연관관계 설정 - saveContents(feed, savedFeed); + applyFeedContents(feed, savedFeed); // 태그가 존재하면 태그 피드 매핑 생성 및 저장 - saveFeedTags(feed, savedFeed); + applyFeedTags(feed, savedFeed); return savedFeed.getPostId(); } @@ -73,30 +73,25 @@ public Long update(Feed feed) { FeedJpaEntity feedJpaEntity = feedJpaRepository.findById(feed.getId()) .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); feedJpaEntity.updateFrom(feed); - updateContents(feed, feedJpaEntity); - updateFeedTags(feed, feedJpaEntity); + + feedJpaEntity.getContentList().clear(); // 피드 수정시 기존 영속성 컨텍스트 내 엔티티 연결 제거 + applyFeedContents(feed, feedJpaEntity); + + feedTagJpaRepository.deleteAllByFeedJpaEntity(feedJpaEntity); // 피드 수정시 기존 피드의 모든 FeedTag 매핑 row 삭제 + applyFeedTags(feed, feedJpaEntity); return feedJpaEntity.getPostId(); } - private void addAllContents(Feed feed, FeedJpaEntity feedJpaEntity) { + private void applyFeedContents(Feed feed, FeedJpaEntity feedJpaEntity) { if (feed.getContentList().isEmpty()) return; List contents = feed.getContentList().stream() .map(content -> contentMapper.toJpaEntity(content, feedJpaEntity)) .toList(); - contents.forEach(feedJpaEntity.getContentList()::add); - } - - private void saveContents(Feed feed, FeedJpaEntity feedJpaEntity) { - addAllContents(feed, feedJpaEntity); + feedJpaEntity.getContentList().addAll(contents); } - private void updateContents(Feed feed, FeedJpaEntity feedJpaEntity) { - feedJpaEntity.getContentList().clear(); // 피드 수정시 기존 영속성 컨텍스트 내 엔티티 연결 제거 - addAllContents(feed, feedJpaEntity); - } - - private void addAllTags(Feed feed, FeedJpaEntity feedJpaEntity) { + private void applyFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { if (feed.getTagList().isEmpty()) return; for (Tag tag : feed.getTagList()) { TagJpaEntity tagJpaEntity = tagJpaRepository.findByValue(tag.getValue()) @@ -110,12 +105,5 @@ private void addAllTags(Feed feed, FeedJpaEntity feedJpaEntity) { feedTagJpaRepository.save(feedTagJpaEntity); } } - private void saveFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { - addAllTags(feed, feedJpaEntity); - } - private void updateFeedTags(Feed feed, FeedJpaEntity feedJpaEntity) { - feedTagJpaRepository.deleteAllByFeedJpaEntity(feedJpaEntity); // 피드 수정시 기존 피드의 모든 FeedTag 매핑 row 삭제 - addAllTags(feed, feedJpaEntity); - } }