-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 아티클 생성로직 구현 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0cd288e
6ca9093
d214bca
ddcbf01
686a948
01fba58
ba2c229
7857bc8
7247a3a
823f488
f1f66a6
889095f
f9c4062
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| 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.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; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Void> createArticle(@CurrentUser User user, @Valid @RequestBody ArticleCreateRequest request) { | ||
| articleManagementUsecase.createArticle(user, request.toCommand()); | ||
| return ResponseDto.created(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Category, Long> { | ||
| Optional<Category> findByIdAndUser(long categoryId, User user); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 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 com.pinback.pinback_server.domain.user.domain.entity.User; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Service | ||
| @Transactional | ||
| @RequiredArgsConstructor | ||
| public class CategoryGetService { | ||
| private final CategoryRepository categoryRepository; | ||
|
|
||
| public Category getCategoryAndUser(long categoryId, User user) { | ||
| return categoryRepository.findByIdAndUser(categoryId, user).orElseThrow(CategoryNotFoundException::new); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||||||||||||||
| 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.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.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; | ||||||||||||||||||||||||||||||||||
| import com.pinback.pinback_server.domain.user.domain.repository.UserRepository; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @SpringBootTest | ||||||||||||||||||||||||||||||||||
| @ActiveProfiles("test") | ||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||
| class ArticleManagementUsecaseTest extends ApplicationTest { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||
| private ArticleManagementUsecase articleManagementUsecase; | ||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||
| private UserRepository userRepository; | ||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||
| private CategoryRepository categoryRepository; | ||||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||||
| private ArticleRepository articleRepository; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @DisplayName("사용자는 아티클을 생성할 수 있다.") | ||||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||||
| void articleSaveService() { | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Fix misleading test method name. The method name -@DisplayName("사용자는 아티클을 생성할 수 있다.")
-@Test
-void articleSaveService() {
+@DisplayName("사용자는 아티클을 생성할 수 있다.")
+@Test
+void createArticle_Success() {🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||
| assertThat(article.getUrl()).isEqualTo(command.url()); | ||||||||||||||||||||||||||||||||||
| assertThat(article.getMemo()).isEqualTo(command.memo()); | ||||||||||||||||||||||||||||||||||
| assertThat(article.getCategory()).isEqualTo(category); | ||||||||||||||||||||||||||||||||||
| assertThat(article.getIsRead()).isFalse(); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add validation for ignored remindTime parameter. The test doesn't verify that assertThat(article.getUrl()).isEqualTo(command.url());
assertThat(article.getMemo()).isEqualTo(command.memo());
assertThat(article.getCategory()).isEqualTo(category);
assertThat(article.getIsRead()).isFalse();
+assertThat(article.getRemindTime()).isEqualTo(command.remindTime());📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid hard-coded ID assumptions. Using -//then
-Article article = articleRepository.findById(1L).get();
+//then
+List<Article> articles = articleRepository.findByUser(user);
+assertThat(articles).hasSize(1);
+Article article = articles.get(0);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @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); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Critical: RemindTime parameter is completely ignored.
The method accepts
remindTimethrough the command but doesn't use it anywhere. This creates a significant disconnect between the API contract and the actual implementation.Consider one of these approaches:
Verify if the
Article.create()method supports remindTime parameter:🏁 Script executed:
Length of output: 90
I couldn’t locate the
Article.createsignature with AST grep. Let’s fall back to a file search and regex:🏁 Script executed:
Length of output: 666
Include
remindTimein article creation or document its omissionThe
remindTimevalue provided inArticleCreateCommandis never used because theArticle.create(...)factory method only accepts(url, memo, user, category)and doesn’t support a reminder timestamp. You need to either wire it through the domain model or explicitly note that it’s pending implementation.• In
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:createArticleyou call:but never use
command.remindTime().• In
src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java:55the signature is:Consider one of these fixes:
Option 1: Extend the
createsignature and passremindTimethrough:Option 2: Leave
remindTimeunhandled for now but document it:🤖 Prompt for AI Agents