From 0cd288eae495015feb8b8a55150746d04ca94b45 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 18:52:23 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CategoryGetService.java | 21 +++++++++++++++++++ .../exception/CategoryNotFoundException.java | 10 +++++++++ .../exception/constant/ExceptionCode.java | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java create mode 100644 src/main/java/com/pinback/pinback_server/domain/category/exception/CategoryNotFoundException.java diff --git a/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java b/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java new file mode 100644 index 00000000..1a909387 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java @@ -0,0 +1,21 @@ +package com.pinback.pinback_server.domain.category.domain.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.category.domain.repository.CategoryRepository; +import com.pinback.pinback_server.domain.category.exception.CategoryNotFoundException; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional +@RequiredArgsConstructor +public class CategoryGetService { + private final CategoryRepository categoryRepository; + + public Category getCategory(long categoryId) { + return categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new); + } +} diff --git a/src/main/java/com/pinback/pinback_server/domain/category/exception/CategoryNotFoundException.java b/src/main/java/com/pinback/pinback_server/domain/category/exception/CategoryNotFoundException.java new file mode 100644 index 00000000..b0454584 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/category/exception/CategoryNotFoundException.java @@ -0,0 +1,10 @@ +package com.pinback.pinback_server.domain.category.exception; + +import com.pinback.pinback_server.global.exception.ApplicationException; +import com.pinback.pinback_server.global.exception.constant.ExceptionCode; + +public class CategoryNotFoundException extends ApplicationException { + public CategoryNotFoundException() { + super(ExceptionCode.CATEGORY_NOT_FOUND); + } +} diff --git a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java index 41187dee..5f557f90 100644 --- a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java +++ b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java @@ -17,7 +17,8 @@ public enum ExceptionCode { //404 NOT_FOUND(HttpStatus.NOT_FOUND, "c40400", "리소스가 존재하지 않습니다."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "c40400", "사용자가 존재하지 않습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "c40401", "사용자가 존재하지 않습니다."), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "c40402", "카테고리가 존재하지 않습니다."), //405 METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "c40500", "잘못된 HTTP method 요청입니다."), From 6ca9093b14a4069b56965bf460321fd892c39059 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 18:53:06 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/ArticleSaveService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java new file mode 100644 index 00000000..6de093d3 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleSaveService.java @@ -0,0 +1,20 @@ +package com.pinback.pinback_server.domain.article.domain.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.repository.ArticleRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class ArticleSaveService { + private final ArticleRepository articleRepository; + + public void save(Article article) { + articleRepository.save(article); + } +} From d214bca4cce4751718855f0b3ec5d88c8ae37261 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 18:53:25 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ArticleManagementUsecase.java | 30 +++++++++++++++++++ .../command/ArticleCreateCommand.java | 11 +++++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/application/command/ArticleCreateCommand.java diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java new file mode 100644 index 00000000..abf2afec --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java @@ -0,0 +1,30 @@ +package com.pinback.pinback_server.domain.article.application; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; +import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.service.ArticleSaveService; +import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.category.domain.service.CategoryGetService; +import com.pinback.pinback_server.domain.user.domain.entity.User; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ArticleManagementUsecase { + + private final CategoryGetService categoryGetService; + private final ArticleSaveService articleSaveService; + + //TODO: 리마인드 로직 추가 필요 + //TODO: 자신의 카테고리가 아닐경우 예외처리 + public void createArticle(User user, ArticleCreateCommand command) { + Category category = categoryGetService.getCategory(command.categoryId()); + Article article = Article.create(command.url(), command.memo(), user, category); + articleSaveService.save(article); + } +} diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/command/ArticleCreateCommand.java b/src/main/java/com/pinback/pinback_server/domain/article/application/command/ArticleCreateCommand.java new file mode 100644 index 00000000..f44ea4f2 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/command/ArticleCreateCommand.java @@ -0,0 +1,11 @@ +package com.pinback.pinback_server.domain.article.application.command; + +import java.time.LocalDateTime; + +public record ArticleCreateCommand( + String url, + long categoryId, + String memo, + LocalDateTime remindTime +) { +} From ddcbf01ac0b110092833d738d030b3930d8d9d16 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 18:53:47 +0900 Subject: [PATCH 04/13] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArticleManagementUsecaseTest.java | 56 +++++++++++++++++++ .../domain/fixture/TestFixture.java | 17 ++++++ 2 files changed, 73 insertions(+) create mode 100644 src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java create mode 100644 src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java diff --git a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java new file mode 100644 index 00000000..80bc79bf --- /dev/null +++ b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java @@ -0,0 +1,56 @@ +package com.pinback.pinback_server.domain.article.application; + +import static com.pinback.pinback_server.domain.fixture.TestFixture.*; + +import java.time.LocalDateTime; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; +import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.repository.ArticleRepository; +import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.category.domain.repository.CategoryRepository; +import com.pinback.pinback_server.domain.user.domain.entity.User; +import com.pinback.pinback_server.domain.user.domain.repository.UserRepository; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class ArticleManagementUsecaseTest { + + @Autowired + private ArticleManagementUsecase articleManagementUsecase; + @Autowired + private UserRepository userRepository; + @Autowired + private CategoryRepository categoryRepository; + @Autowired + private ArticleRepository articleRepository; + + @DisplayName("사용자는 아티클을 생성할 수 있다.") + @Test + void articleSaveService() { + User user = userRepository.save(user()); + Category category = categoryRepository.save(category(user)); + ArticleCreateCommand command = new ArticleCreateCommand("testUrl", category.getId() + , "테스트메모", + LocalDateTime.of(2025, 8, 6, 0, 0, 0)); + //when + articleManagementUsecase.createArticle(user, command); + + //then + Article article = articleRepository.findById(1L).get(); + Assertions.assertThat(article.getUrl()).isEqualTo(command.url()); + Assertions.assertThat(article.getMemo()).isEqualTo(command.memo()); + Assertions.assertThat(article.getCategory()).isEqualTo(category); + Assertions.assertThat(article.getIsRead()).isFalse(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java new file mode 100644 index 00000000..90bd4017 --- /dev/null +++ b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java @@ -0,0 +1,17 @@ +package com.pinback.pinback_server.domain.fixture; + +import java.time.LocalTime; + +import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.user.domain.entity.User; + +public class TestFixture { + + public static User user() { + return User.create("testUser@gmail.com", LocalTime.of(12, 0, 0)); + } + + public static Category category(User user) { + return Category.create("테스트카테고리", user); + } +} From 686a94898493358ea7913a4327ea95a680394118 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 19:18:16 +0900 Subject: [PATCH 05/13] =?UTF-8?q?chore:=20=EC=98=88=EC=99=B8=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=A3=BC=EC=84=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pinback_server/global/exception/constant/ExceptionCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java index 5f557f90..c7a9fc2d 100644 --- a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java +++ b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java @@ -10,7 +10,7 @@ public enum ExceptionCode { //400 INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "c40000", "잘못된 요청입니다."), - //403 + //401 INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "c40101", "유효하지 않은 토큰입니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "c40102", "만료된 토큰입니다."), EMPTY_TOKEN(HttpStatus.UNAUTHORIZED, "c40101", "토큰이 비어있습니다."), From 01fba58cd31e4334bb76645bc669b66a2aa4b0c4 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 19:18:59 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=EC=9E=90=EC=8B=A0=EC=9D=98=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=90=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=EB=B0=9C?= =?UTF-8?q?=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/application/ArticleManagementUsecase.java | 3 +-- .../category/domain/repository/CategoryRepository.java | 4 ++++ .../domain/category/domain/service/CategoryGetService.java | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java index abf2afec..ebc9776e 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java @@ -21,9 +21,8 @@ public class ArticleManagementUsecase { private final ArticleSaveService articleSaveService; //TODO: 리마인드 로직 추가 필요 - //TODO: 자신의 카테고리가 아닐경우 예외처리 public void createArticle(User user, ArticleCreateCommand command) { - Category category = categoryGetService.getCategory(command.categoryId()); + Category category = categoryGetService.getCategoryAndUser(command.categoryId(), user); Article article = Article.create(command.url(), command.memo(), user, category); articleSaveService.save(article); } diff --git a/src/main/java/com/pinback/pinback_server/domain/category/domain/repository/CategoryRepository.java b/src/main/java/com/pinback/pinback_server/domain/category/domain/repository/CategoryRepository.java index 751a3398..5dd78295 100644 --- a/src/main/java/com/pinback/pinback_server/domain/category/domain/repository/CategoryRepository.java +++ b/src/main/java/com/pinback/pinback_server/domain/category/domain/repository/CategoryRepository.java @@ -1,10 +1,14 @@ package com.pinback.pinback_server.domain.category.domain.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.user.domain.entity.User; @Repository public interface CategoryRepository extends JpaRepository { + Optional findByIdAndUser(long categoryId, User user); } diff --git a/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java b/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java index 1a909387..b5eed25d 100644 --- a/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java +++ b/src/main/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetService.java @@ -6,6 +6,7 @@ import com.pinback.pinback_server.domain.category.domain.entity.Category; import com.pinback.pinback_server.domain.category.domain.repository.CategoryRepository; import com.pinback.pinback_server.domain.category.exception.CategoryNotFoundException; +import com.pinback.pinback_server.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; @@ -15,7 +16,7 @@ public class CategoryGetService { private final CategoryRepository categoryRepository; - public Category getCategory(long categoryId) { - return categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new); + public Category getCategoryAndUser(long categoryId, User user) { + return categoryRepository.findByIdAndUser(categoryId, user).orElseThrow(CategoryNotFoundException::new); } } From ba2c229a52b64170ecc7b074084e4d55b9d93fde Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 19:19:17 +0900 Subject: [PATCH 07/13] =?UTF-8?q?test:=20=EC=9E=90=EC=8B=A0=EC=9D=98=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=90=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CategoryGetServiceTest.java | 47 +++++++++++++++++++ .../domain/fixture/TestFixture.java | 4 ++ 2 files changed, 51 insertions(+) create mode 100644 src/test/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetServiceTest.java diff --git a/src/test/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetServiceTest.java b/src/test/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetServiceTest.java new file mode 100644 index 00000000..500a6d56 --- /dev/null +++ b/src/test/java/com/pinback/pinback_server/domain/category/domain/service/CategoryGetServiceTest.java @@ -0,0 +1,47 @@ +package com.pinback.pinback_server.domain.category.domain.service; + +import static com.pinback.pinback_server.domain.fixture.TestFixture.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.category.domain.entity.Category; +import com.pinback.pinback_server.domain.category.domain.repository.CategoryRepository; +import com.pinback.pinback_server.domain.category.exception.CategoryNotFoundException; +import com.pinback.pinback_server.domain.user.domain.entity.User; +import com.pinback.pinback_server.domain.user.domain.repository.UserRepository; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class CategoryGetServiceTest { + @Autowired + private UserRepository userRepository; + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private CategoryGetService categoryGetService; + + @DisplayName("카테고리 소유자가 아닐경우 예외가 발생한다.") + @Test + void throwExceptionIsNotOwner() { + //given + User user = userRepository.save(user()); + User user1 = userRepository.save(userWithEmail("another@gmail.com")); + Category category = categoryRepository.save(category(user)); + + //when & Then + + Assertions.assertThatThrownBy(() -> categoryGetService.getCategoryAndUser(category.getId(), user1)) + .isInstanceOf(CategoryNotFoundException.class); + + } + +} diff --git a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java index 90bd4017..7afc7409 100644 --- a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java +++ b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java @@ -11,6 +11,10 @@ public static User user() { return User.create("testUser@gmail.com", LocalTime.of(12, 0, 0)); } + public static User userWithEmail(String email) { + return User.create(email, LocalTime.of(12, 0, 0)); + } + public static Category category(User user) { return Category.create("테스트카테고리", user); } From 7857bc8adbba19112c1d8b7989fd16a961fcd23c Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 19:19:33 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20EOF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleManagementUsecaseTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java index 80bc79bf..5c5b09cd 100644 --- a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java +++ b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java @@ -52,5 +52,4 @@ void articleSaveService() { Assertions.assertThat(article.getCategory()).isEqualTo(category); Assertions.assertThat(article.getIsRead()).isFalse(); } - -} \ No newline at end of file +} From 7247a3a5e2464b74bd934878e9c049b46151ac70 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 19:20:43 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20Transactional=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/application/ArticleManagementUsecase.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java index ebc9776e..61ed2548 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java @@ -21,6 +21,7 @@ public class ArticleManagementUsecase { private final ArticleSaveService articleSaveService; //TODO: 리마인드 로직 추가 필요 + @Transactional public void createArticle(User user, ArticleCreateCommand command) { Category category = categoryGetService.getCategoryAndUser(command.categoryId(), user); Article article = Article.create(command.url(), command.memo(), user, category); From 823f488914aa21a35f72d97f378579dbb7940551 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 21:30:45 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ArticleController.java | 28 +++++++++++++++++ .../dto/request/ArticleCreateRequest.java | 30 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/request/ArticleCreateRequest.java diff --git a/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java b/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java new file mode 100644 index 00000000..d5e540ff --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java @@ -0,0 +1,28 @@ +package com.pinback.pinback_server.domain.article.presentation; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.pinback.pinback_server.domain.article.application.ArticleManagementUsecase; +import com.pinback.pinback_server.domain.article.presentation.dto.request.ArticleCreateRequest; +import com.pinback.pinback_server.domain.user.domain.entity.User; +import com.pinback.pinback_server.global.common.annotation.CurrentUser; +import com.pinback.pinback_server.global.common.dto.ResponseDto; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/article") +@RequiredArgsConstructor +public class ArticleController { + private final ArticleManagementUsecase articleManagementUsecase; + + @PostMapping + public ResponseDto createArticle(@CurrentUser User user, @Valid @RequestBody ArticleCreateRequest request) { + articleManagementUsecase.createArticle(user, request.toCommand()); + return ResponseDto.created(); + } +} diff --git a/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/request/ArticleCreateRequest.java b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/request/ArticleCreateRequest.java new file mode 100644 index 00000000..455ef9d7 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/request/ArticleCreateRequest.java @@ -0,0 +1,30 @@ +package com.pinback.pinback_server.domain.article.presentation.dto.request; + +import java.time.LocalDateTime; + +import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record ArticleCreateRequest( + @NotEmpty(message = "url을 비어있을 수 없습니다.") + String url, + + @NotNull(message = "카테고리 ID는 비어있을 수 없습니다.") + Long categoryId, + + String memo, + + @NotNull(message = "리마인드 날짜는 비어있을 수 없습니다.") + LocalDateTime remindTime +) { + public ArticleCreateCommand toCommand() { + return new ArticleCreateCommand( + url, + categoryId, + memo, + remindTime + ); + } +} From f1f66a66d8508f0b086b9af9a8534731ecdd14a3 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 21:31:16 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/article/domain/entity/Article.java | 6 +++++- .../domain/repository/ArticleRepository.java | 3 +++ .../domain/service/ArticleGetService.java | 20 +++++++++++++++++++ .../ArticleAlreadyExistException.java | 10 ++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java create mode 100644 src/main/java/com/pinback/pinback_server/domain/article/exception/ArticleAlreadyExistException.java diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java index 2c3b3472..578534b8 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java @@ -13,6 +13,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,7 +22,10 @@ @Getter @Entity -@Table(name = "article") +@Table(name = "article", uniqueConstraints = +@UniqueConstraint( + columnNames = {"user_id", "url"} +)) @Builder(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepository.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepository.java index 6e777a59..4df86d93 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepository.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepository.java @@ -4,7 +4,10 @@ import org.springframework.stereotype.Repository; import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.user.domain.entity.User; @Repository public interface ArticleRepository extends JpaRepository { + + boolean existsByUserAndUrl(User user, String url); } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java new file mode 100644 index 00000000..b79bc453 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java @@ -0,0 +1,20 @@ +package com.pinback.pinback_server.domain.article.domain.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.pinback.pinback_server.domain.article.domain.repository.ArticleRepository; +import com.pinback.pinback_server.domain.user.domain.entity.User; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ArticleGetService { + private final ArticleRepository articleRepository; + + public boolean checkExistsByUserAndUrl(User user, String url) { + return articleRepository.existsByUserAndUrl(user, url); + } +} diff --git a/src/main/java/com/pinback/pinback_server/domain/article/exception/ArticleAlreadyExistException.java b/src/main/java/com/pinback/pinback_server/domain/article/exception/ArticleAlreadyExistException.java new file mode 100644 index 00000000..7cd51830 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/exception/ArticleAlreadyExistException.java @@ -0,0 +1,10 @@ +package com.pinback.pinback_server.domain.article.exception; + +import com.pinback.pinback_server.global.exception.ApplicationException; +import com.pinback.pinback_server.global.exception.constant.ExceptionCode; + +public class ArticleAlreadyExistException extends ApplicationException { + public ArticleAlreadyExistException() { + super(ExceptionCode.ARTICLE_ALREADY_EXIST); + } +} From 889095f9e5cef065feaa9c2911d96627552c5c59 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 21:31:25 +0900 Subject: [PATCH 12/13] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B2=80=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleManagementUsecase.java | 6 ++++++ .../global/exception/constant/ExceptionCode.java | 1 + 2 files changed, 7 insertions(+) diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java index 61ed2548..a462d4d8 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java @@ -5,7 +5,9 @@ import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.service.ArticleGetService; import com.pinback.pinback_server.domain.article.domain.service.ArticleSaveService; +import com.pinback.pinback_server.domain.article.exception.ArticleAlreadyExistException; import com.pinback.pinback_server.domain.category.domain.entity.Category; import com.pinback.pinback_server.domain.category.domain.service.CategoryGetService; import com.pinback.pinback_server.domain.user.domain.entity.User; @@ -19,10 +21,14 @@ public class ArticleManagementUsecase { private final CategoryGetService categoryGetService; private final ArticleSaveService articleSaveService; + private final ArticleGetService articleGetService; //TODO: 리마인드 로직 추가 필요 @Transactional public void createArticle(User user, ArticleCreateCommand command) { + if (articleGetService.checkExistsByUserAndUrl(user, command.url())) { + throw new ArticleAlreadyExistException(); + } Category category = categoryGetService.getCategoryAndUser(command.categoryId(), user); Article article = Article.create(command.url(), command.memo(), user, category); articleSaveService.save(article); diff --git a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java index c7a9fc2d..f6e4ccfc 100644 --- a/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java +++ b/src/main/java/com/pinback/pinback_server/global/exception/constant/ExceptionCode.java @@ -26,6 +26,7 @@ public enum ExceptionCode { //409 DUPLICATE(HttpStatus.CONFLICT, "c40900", "이미 존재하는 리소스입니다."), USER_ALREADY_EXIST(HttpStatus.CONFLICT, "c40901", "이미 존재하는 사용자입니다."), + ARTICLE_ALREADY_EXIST(HttpStatus.CONFLICT, "c40902", "이미 저장한 url 입니다."), //500 INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "s50000", "서버 내부 오류가 발생했습니다."); From f9c40620b786251f67de7e32ab8a795cdd7aaad6 Mon Sep 17 00:00:00 2001 From: rootTiket Date: Mon, 7 Jul 2025 21:31:40 +0900 Subject: [PATCH 13/13] =?UTF-8?q?test:=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B2=80=EC=A6=9D=EB=A1=9C=EC=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ApplicationTest.java | 21 ++++++++++++++ .../ArticleManagementUsecaseTest.java | 29 +++++++++++++++---- .../domain/fixture/CustomRepository.java | 23 +++++++++++++++ .../domain/fixture/TestFixture.java | 10 +++++++ 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/pinback/pinback_server/domain/ApplicationTest.java create mode 100644 src/test/java/com/pinback/pinback_server/domain/fixture/CustomRepository.java diff --git a/src/test/java/com/pinback/pinback_server/domain/ApplicationTest.java b/src/test/java/com/pinback/pinback_server/domain/ApplicationTest.java new file mode 100644 index 00000000..396a8b55 --- /dev/null +++ b/src/test/java/com/pinback/pinback_server/domain/ApplicationTest.java @@ -0,0 +1,21 @@ +package com.pinback.pinback_server.domain; + +import org.junit.jupiter.api.AfterEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import com.pinback.pinback_server.domain.fixture.CustomRepository; + +@SpringBootTest +@ActiveProfiles("test") +public class ApplicationTest { + + @Autowired + CustomRepository customRepository; + + @AfterEach + void tearDown() { + customRepository.clearAndReset(); + } +} diff --git a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java index 5c5b09cd..7cfc708a 100644 --- a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java +++ b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java @@ -1,10 +1,10 @@ package com.pinback.pinback_server.domain.article.application; import static com.pinback.pinback_server.domain.fixture.TestFixture.*; +import static org.assertj.core.api.Assertions.*; import java.time.LocalDateTime; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,9 +12,11 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import com.pinback.pinback_server.domain.ApplicationTest; import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; import com.pinback.pinback_server.domain.article.domain.entity.Article; import com.pinback.pinback_server.domain.article.domain.repository.ArticleRepository; +import com.pinback.pinback_server.domain.article.exception.ArticleAlreadyExistException; import com.pinback.pinback_server.domain.category.domain.entity.Category; import com.pinback.pinback_server.domain.category.domain.repository.CategoryRepository; import com.pinback.pinback_server.domain.user.domain.entity.User; @@ -23,7 +25,7 @@ @SpringBootTest @ActiveProfiles("test") @Transactional -class ArticleManagementUsecaseTest { +class ArticleManagementUsecaseTest extends ApplicationTest { @Autowired private ArticleManagementUsecase articleManagementUsecase; @@ -47,9 +49,24 @@ void articleSaveService() { //then Article article = articleRepository.findById(1L).get(); - Assertions.assertThat(article.getUrl()).isEqualTo(command.url()); - Assertions.assertThat(article.getMemo()).isEqualTo(command.memo()); - Assertions.assertThat(article.getCategory()).isEqualTo(category); - Assertions.assertThat(article.getIsRead()).isFalse(); + assertThat(article.getUrl()).isEqualTo(command.url()); + assertThat(article.getMemo()).isEqualTo(command.memo()); + assertThat(article.getCategory()).isEqualTo(category); + assertThat(article.getIsRead()).isFalse(); + } + + @DisplayName("사용자는 중복된 url을 저장할 수 없다.") + @Test + void articleDuplicate() { + User user = userRepository.save(user()); + Category category = categoryRepository.save(category(user)); + Article article = articleRepository.save(articleWithCategory(user, category)); + ArticleCreateCommand command = new ArticleCreateCommand(article.getUrl(), article.getCategory().getId() + , article.getMemo(), + LocalDateTime.of(2025, 8, 6, 0, 0, 0)); + //when & then + assertThatThrownBy(() -> articleManagementUsecase.createArticle(user, command)) + .isInstanceOf(ArticleAlreadyExistException.class); + } } diff --git a/src/test/java/com/pinback/pinback_server/domain/fixture/CustomRepository.java b/src/test/java/com/pinback/pinback_server/domain/fixture/CustomRepository.java new file mode 100644 index 00000000..4b935714 --- /dev/null +++ b/src/test/java/com/pinback/pinback_server/domain/fixture/CustomRepository.java @@ -0,0 +1,23 @@ +package com.pinback.pinback_server.domain.fixture; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; + +@Repository +public class CustomRepository { + @Autowired + private EntityManager entityManager; + + @Transactional + public void clearAndReset() { + entityManager.createNativeQuery("DELETE FROM article").executeUpdate(); + entityManager.createNativeQuery("DELETE FROM category").executeUpdate(); + entityManager.createNativeQuery("DELETE FROM users").executeUpdate(); + + entityManager.createNativeQuery("ALTER TABLE article ALTER COLUMN article_id RESTART WITH 1").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE category ALTER COLUMN category_id RESTART WITH 1").executeUpdate(); + } +} diff --git a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java index 7afc7409..1c9a6078 100644 --- a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java +++ b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java @@ -2,6 +2,7 @@ import java.time.LocalTime; +import com.pinback.pinback_server.domain.article.domain.entity.Article; import com.pinback.pinback_server.domain.category.domain.entity.Category; import com.pinback.pinback_server.domain.user.domain.entity.User; @@ -18,4 +19,13 @@ public static User userWithEmail(String email) { public static Category category(User user) { return Category.create("테스트카테고리", user); } + + public static Article article(User user) { + Category category = category(user); + return Article.create("test", "testmemo", user, category); + } + + public static Article articleWithCategory(User user, Category category) { + return Article.create("test", "testmemo", user, category); + } }