From 1507407a31c96d17d94a1998fccac0dd33c435fc Mon Sep 17 00:00:00 2001 From: GiJungPark Date: Sun, 28 Dec 2025 15:11:25 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[REFACTOR]=20=EC=84=9C=EC=9E=AC=20=EA=B4=80?= =?UTF-8?q?=EC=8B=AC=20=EB=93=B1=EB=A1=9D/=ED=95=B4=EC=A0=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=8E=B8=EC=9D=98=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - markAsInterested(): 관심 등록 처리 - unmarkAsInterested(): 관심 해제 처리 --- .../WSSServer/library/domain/UserNovel.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java index fca72eb2..225a5778 100644 --- a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java +++ b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java @@ -91,6 +91,20 @@ public void setIsInterest(Boolean isInterest) { this.isInterest = isInterest; } + /** + * 관심 상태 지정 + */ + public void markAsInterested() { + this.isInterest = true; + } + + /** + * 관심 상태 해제 + */ + public void unmarkAsInterested() { + this.isInterest = false; + } + public void updateUserNovel(Float userNovelRating, ReadStatus status, LocalDate startDate, LocalDate endDate) { this.userNovelRating = userNovelRating; this.status = status; From a78dbb953ca777b79c5e1b0939b026c9d621c925 Mon Sep 17 00:00:00 2001 From: GiJungPark Date: Sun, 28 Dec 2025 15:34:40 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[FIX]=20=EC=9E=98=EB=AA=BB=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EB=90=9C=20Genre=20JPA=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필드 명과 메서드 명이 매칭되지 않는 문제 해결 --- .../org/websoso/WSSServer/novel/service/GenreServiceImpl.java | 2 +- .../java/org/websoso/WSSServer/repository/GenreRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java b/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java index a5b0bb24..69e3272a 100644 --- a/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java +++ b/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java @@ -32,7 +32,7 @@ public List getGenresOrException(List names) { List uniqueNames = names.stream().distinct().toList(); - List genres = genreRepository.findByNameIn(uniqueNames); + List genres = genreRepository.findByGenreNameIn(uniqueNames); if (genres.size() != uniqueNames.size()) { throw new CustomGenreException(GENRE_NOT_FOUND, diff --git a/src/main/java/org/websoso/WSSServer/repository/GenreRepository.java b/src/main/java/org/websoso/WSSServer/repository/GenreRepository.java index 6facddcc..b06e6724 100644 --- a/src/main/java/org/websoso/WSSServer/repository/GenreRepository.java +++ b/src/main/java/org/websoso/WSSServer/repository/GenreRepository.java @@ -11,5 +11,5 @@ public interface GenreRepository extends JpaRepository { Optional findByGenreName(String name); - List findByNameIn(List names); + List findByGenreNameIn(List genreNames); } From 6fd81ba5891f4a3fc716315be86f65042a2dff22 Mon Sep 17 00:00:00 2001 From: GiJungPark Date: Sun, 28 Dec 2025 15:46:02 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[REFACTOR]=20=EA=B4=80=EC=8B=AC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=EC=97=90=20Upsert=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20API=20=EB=A9=B1=EB=93=B1=EC=84=B1=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Native Upsert(ON DUPLICATE KEY UPDATE) 적용으로 동시성 이슈 해결 및 성능 최적화 - 중복 요청 시 예외를 던지지 않고 성공 처리하도록 변경하여 멱등성 확보 --- .../LibraryInterestApplication.java | 26 +------------------ .../WSSServer/library/domain/UserNovel.java | 3 +++ .../repository/UserNovelRepository.java | 26 +++++++++++++++++++ .../library/service/LibraryService.java | 11 ++++++++ 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java b/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java index d2e14d3c..f0f6f7b2 100644 --- a/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java +++ b/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java @@ -1,11 +1,8 @@ package org.websoso.WSSServer.application; -import static org.websoso.WSSServer.exception.error.CustomUserNovelError.ALREADY_INTERESTED; import static org.websoso.WSSServer.exception.error.CustomUserNovelError.NOT_INTERESTED; -import static org.websoso.WSSServer.exception.error.CustomUserNovelError.USER_NOVEL_ALREADY_EXISTS; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.user.domain.User; @@ -32,21 +29,7 @@ public class LibraryInterestApplication { public void registerAsInterest(User user, Long novelId) { Novel novel = novelService.getNovelOrException(novelId); - UserNovel userNovel = user == null ? null : libraryService.getLibraryOrNull(user, novel); - - if (userNovel != null && userNovel.getIsInterest()) { - throw new CustomUserNovelException(ALREADY_INTERESTED, "already registered as interested"); - } - - if (userNovel == null) { - try { - userNovel = createUserNovelByInterest(user, novel); - } catch (DataIntegrityViolationException e) { - userNovel = libraryService.getLibraryOrException(user, novelId); - } - } - - userNovel.setIsInterest(true); + libraryService.registerInterest(user, novel); } /** @@ -71,11 +54,4 @@ public void unregisterAsInterest(User user, Long novelId) { } - private UserNovel createUserNovelByInterest(User user, Novel novel) { - if (libraryService.getLibraryOrNull(user, novel) != null) { - throw new CustomUserNovelException(USER_NOVEL_ALREADY_EXISTS, "this novel is already registered"); - } - - return libraryService.createLibrary(null, 0.0f, null, null, user, novel); - } } diff --git a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java index 225a5778..96ab4da1 100644 --- a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java +++ b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java @@ -36,6 +36,9 @@ }) public class UserNovel extends BaseEntity { + public static final Float DEFAULT_RATING = 0.0f; + public static final ReadStatus DEFAULT_STATUS = null; + @Id @GeneratedValue(strategy = IDENTITY) @Column(nullable = false) diff --git a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelRepository.java b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelRepository.java index 1e367ec2..48ff0970 100644 --- a/src/main/java/org/websoso/WSSServer/library/repository/UserNovelRepository.java +++ b/src/main/java/org/websoso/WSSServer/library/repository/UserNovelRepository.java @@ -1,8 +1,10 @@ package org.websoso.WSSServer.library.repository; +import io.lettuce.core.dynamic.annotation.Param; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.websoso.WSSServer.novel.domain.Novel; @@ -29,4 +31,28 @@ public interface UserNovelRepository extends JpaRepository, Use List findUserNovelByUser(User user); Optional findByNovel_NovelIdAndUser(Long novelId, User user); + + @Modifying(clearAutomatically = true) + @Query(value = """ + INSERT INTO user_novel ( + user_id, novel_id, is_interest, + user_novel_rating, status, + created_date, modified_date + ) VALUES ( + :userId, :novelId, true, + :defaultRating, + :#{#defaultStatus?.name()}, + NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + is_interest = true, + modified_date = NOW() + """, nativeQuery = true) + void upsertInterest( + @Param("userId") Long userId, + @Param("novelId") Long novelId, + @Param("defaultRating") Float defaultRating, + @Param("defaultStatus") ReadStatus defaultStatus + ); + } diff --git a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java index aebeb9f0..19252189 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java @@ -56,6 +56,17 @@ public UserNovel createLibrary(ReadStatus status, Float userNovelRating, LocalDa novel)); } + @Transactional + public void registerInterest(User user, Novel novel) { + userNovelRepository.upsertInterest( + user.getUserId(), + novel.getNovelId(), + UserNovel.DEFAULT_RATING, + UserNovel.DEFAULT_STATUS + ); + } + + @Transactional public void delete(UserNovel library) { userNovelRepository.delete(library); } From 486111779f477797603862f190c9bff1826f141b Mon Sep 17 00:00:00 2001 From: GiJungPark Date: Sun, 28 Dec 2025 16:46:05 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[REFACTOR]=20=EA=B4=80=EC=8B=AC=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20=EB=A1=9C=EC=A7=81=EC=97=90=20API=20=EB=A9=B1?= =?UTF-8?q?=EB=93=B1=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관심 있어요 해제 시, 이미 관심 해제된 정보여도 204를 반환하도록 함 --- .../LibraryInterestApplication.java | 13 +++---- .../WSSServer/library/domain/UserNovel.java | 9 ++--- .../library/service/LibraryService.java | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java b/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java index f0f6f7b2..decacb81 100644 --- a/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java +++ b/src/main/java/org/websoso/WSSServer/application/LibraryInterestApplication.java @@ -40,18 +40,13 @@ public void registerAsInterest(User user, Long novelId) { */ @Transactional public void unregisterAsInterest(User user, Long novelId) { - UserNovel userNovel = libraryService.getLibraryOrException(user, novelId); + UserNovel library = libraryService.getLibraryOrNull(user, novelId); - if (!userNovel.getIsInterest()) { - throw new CustomUserNovelException(NOT_INTERESTED, "not registered as interest"); - } - - userNovel.setIsInterest(false); - - if (userNovel.getStatus() == null) { - libraryService.delete(userNovel); + if (library == null) { + return; } + libraryService.unregisterInterest(library); } } diff --git a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java index 96ab4da1..66f340d4 100644 --- a/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java +++ b/src/main/java/org/websoso/WSSServer/library/domain/UserNovel.java @@ -90,10 +90,6 @@ public static UserNovel create(ReadStatus status, Float userNovelRating, LocalDa return new UserNovel(status, userNovelRating, startDate, endDate, user, novel); } - public void setIsInterest(Boolean isInterest) { - this.isInterest = isInterest; - } - /** * 관심 상태 지정 */ @@ -108,6 +104,11 @@ public void unmarkAsInterested() { this.isInterest = false; } + public boolean isSafeToDelete() { + return !this.isInterest + && this.status == null; + } + public void updateUserNovel(Float userNovelRating, ReadStatus status, LocalDate startDate, LocalDate endDate) { this.userNovelRating = userNovelRating; this.status = status; diff --git a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java index 19252189..784c48c4 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java @@ -44,6 +44,15 @@ public UserNovel getLibraryOrNull(User user, Novel novel) { return userNovelRepository.findByNovel_NovelIdAndUser(novel.getNovelId(), user).orElse(null); } + @Transactional(readOnly = true) + public UserNovel getLibraryOrNull(User user, long novelId) { + if (user == null) { + return null; + } + + return userNovelRepository.findByNovel_NovelIdAndUser(novelId, user).orElse(null); + } + @Transactional public UserNovel createLibrary(ReadStatus status, Float userNovelRating, LocalDate startDate, LocalDate endDate, User user, Novel novel) { @@ -56,6 +65,13 @@ public UserNovel createLibrary(ReadStatus status, Float userNovelRating, LocalDa novel)); } + /** + *

관심있어요를 등록한다.

+ * 서재 내역이 없다면, 서재 내역을 생성하면서 등록한다. + * + * @param user 사용자 Entity + * @param novel 서재 Entity + */ @Transactional public void registerInterest(User user, Novel novel) { userNovelRepository.upsertInterest( @@ -66,6 +82,25 @@ public void registerInterest(User user, Novel novel) { ); } + /** + *

관심있어요를 해제한다.

+ * 만약, 서재 정보가 없다면 삭제한다. + * + * @param library 서재 Entity + */ + @Transactional + public void unregisterInterest(UserNovel library) { + if (Boolean.FALSE.equals(library.getIsInterest())) { + return; + } + + library.unmarkAsInterested(); + + if (library.isSafeToDelete()) { + userNovelRepository.delete(library); + } + } + @Transactional public void delete(UserNovel library) { userNovelRepository.delete(library);