Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Comment on lines +26 to +35
Copy link

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 remindTime through 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:

// Option 1: Use remindTime in Article creation
-Article article = Article.create(command.url(), command.memo(), user, category);
+Article article = Article.create(command.url(), command.memo(), user, category, command.remindTime());

// Option 2: If reminder logic is not ready, document the limitation
+// TODO: remindTime is currently ignored until reminder logic is implemented

Verify if the Article.create() method supports remindTime parameter:


🏁 Script executed:

#!/bin/bash
# Search for Article.create method signatures
ast-grep --pattern 'class Article {
  $$$
  static $_ create($$$) {
    $$$
  }
  $$$
}'

Length of output: 90


I couldn’t locate the Article.create signature with AST grep. Let’s fall back to a file search and regex:


🏁 Script executed:

#!/bin/bash
# Locate Article.java
file=$(fd Article.java)
echo "Located file: $file"

# Show the class declaration
rg -n 'class Article' "$file"

# Show all static create method signatures
rg -n 'static .*\bcreate\b' "$file"

Length of output: 666


Include remindTime in article creation or document its omission

The remindTime value provided in ArticleCreateCommand is never used because the Article.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:createArticle you call:

Article.create(command.url(), command.memo(), user, category);

but never use command.remindTime().

• In src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java:55 the signature is:

public static Article create(String url, String memo, User user, Category category) { … }

Consider one of these fixes:

Option 1: Extend the create signature and pass remindTime through:

// ArticleManagementUsecase.java
- Article article = Article.create(command.url(), command.memo(), user, category);
+ Article article = Article.create(
+     command.url(),
+     command.memo(),
+     user,
+     category,
+     command.remindTime()
+ );

// Article.java
- public static Article create(String url, String memo, User user, Category category) {
+ public static Article create(
+     String url,
+     String memo,
+     User user,
+     Category category,
+     LocalDateTime remindTime
+ ) {
    Article article = new Article(…);
+   article.setRemindTime(remindTime);
    return article;
}

Option 2: Leave remindTime unhandled for now but document it:

// TODO: `remindTime` is ignored until reminder scheduling logic is implemented
@Transactional
public void createArticle(User user, ArticleCreateCommand command) {
    …
}
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java
around lines 26 to 35, the remindTime from ArticleCreateCommand is not used in
the Article.create call because the factory method does not accept it. To fix
this, either extend the Article.create method signature in the domain entity to
include remindTime and pass it through here, or add a clear TODO comment in
createArticle noting that remindTime is currently ignored until reminder
scheduling is implemented.

}
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
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Article, Long> {

boolean existsByUserAndUrl(User user, String 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.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
Expand Up @@ -10,21 +10,23 @@ 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", "토큰이 비어있습니다."),

//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 요청입니다."),

//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", "서버 내부 오류가 발생했습니다.");
Expand Down
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix misleading test method name.

The method name articleSaveService() is misleading as it tests the use case, not the save service specifically.

-@DisplayName("사용자는 아티클을 생성할 수 있다.")
-@Test
-void articleSaveService() {
+@DisplayName("사용자는 아티클을 생성할 수 있다.")
+@Test
+void createArticle_Success() {
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
around lines 39 to 41, rename the test method from articleSaveService() to a
name that clearly reflects it is testing the article management use case, such
as articleManagementUsecaseTest() or testArticleCreationUsecase(), to avoid
confusion with a specific save service.

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
Copy link

Choose a reason for hiding this comment

The 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 remindTime is properly stored, which would catch the issue where this parameter is currently ignored in the implementation.

 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertThat(article.getUrl()).isEqualTo(command.url());
assertThat(article.getMemo()).isEqualTo(command.memo());
assertThat(article.getCategory()).isEqualTo(category);
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();
assertThat(article.getRemindTime()).isEqualTo(command.remindTime());
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
around lines 52 to 55, the test is missing validation for the remindTime
parameter. Add an assertion to verify that the article's remindTime matches the
command's remindTime to ensure this parameter is correctly stored and handled in
the implementation.

}
Comment on lines +50 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid hard-coded ID assumptions.

Using findById(1L) assumes specific ID generation behavior, making the test brittle. Consider using repository queries or storing the created article's ID.

-//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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//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();
}
//then
List<Article> articles = articleRepository.findByUser(user);
assertThat(articles).hasSize(1);
Article article = articles.get(0);
assertThat(article.getUrl()).isEqualTo(command.url());
assertThat(article.getMemo()).isEqualTo(command.memo());
assertThat(article.getCategory()).isEqualTo(category);
assertThat(article.getIsRead()).isFalse();
}
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
around lines 50 to 56, avoid using a hard-coded ID (1L) in findById as it
assumes specific ID generation and makes the test brittle. Instead, capture the
ID of the article when it is created or saved, then use that stored ID to
retrieve the article for assertions. Alternatively, use repository queries that
do not rely on fixed IDs to fetch the article.


@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);

}
}
Loading