-
Notifications
You must be signed in to change notification settings - Fork 0
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
✨ implement ingredient registration #78
Changes from 15 commits
2eb269b
33bbf53
d15ab42
3ef41a0
273dfc0
9fe985d
7d5a4f9
ccf192e
635ffd2
ee69b11
2a21e11
2779356
44aadea
67627e2
8a99729
63da917
29ca71f
3576f2a
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 |
---|---|---|
@@ -1,16 +1,29 @@ | ||
package net.pengcook.ingredient.domain; | ||
|
||
import static lombok.AccessLevel.PRIVATE; | ||
import static lombok.AccessLevel.PROTECTED; | ||
|
||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@NoArgsConstructor(access = PROTECTED) | ||
@AllArgsConstructor(access = PRIVATE) | ||
@Getter | ||
public class Ingredient { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private long id; | ||
|
||
private String name; | ||
|
||
public Ingredient(String name) { | ||
this(0L, name); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package net.pengcook.ingredient.dto; | ||
|
||
import java.util.List; | ||
import net.pengcook.ingredient.domain.Requirement; | ||
import net.pengcook.ingredient.exception.InvalidNameException; | ||
|
||
public record IngredientCreateRequest( | ||
String name, | ||
Requirement requirement, | ||
List<String> substitutions | ||
) { | ||
|
||
public IngredientCreateRequest { | ||
validateName(name); | ||
if (requirement == Requirement.ALTERNATIVE) { | ||
validateSubstitutions(substitutions); | ||
} | ||
} | ||
|
||
private void validateName(String name) { | ||
if (name == null || name.isBlank()) { | ||
throw new InvalidNameException("name cannot be null or empty"); | ||
} | ||
} | ||
|
||
private void validateSubstitutions(List<String> substitutions) { | ||
if (substitutions == null) { | ||
throw new InvalidNameException("substitutions cannot be null"); | ||
} | ||
for (String substitution : substitutions) { | ||
validateName(substitution); | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package net.pengcook.ingredient.exception; | ||
|
||
import org.springframework.http.HttpStatus; | ||
|
||
public class InvalidNameException extends IngredientException { | ||
|
||
public InvalidNameException(String message) { | ||
super(HttpStatus.BAD_REQUEST, message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package net.pengcook.ingredient.repository; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.stereotype.Repository; | ||
|
||
import net.pengcook.ingredient.domain.IngredientRecipe; | ||
|
||
@Repository | ||
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. 이거 어노테이션 없어도 된대요 |
||
public interface IngredientRecipeRepository extends JpaRepository<IngredientRecipe, Long> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package net.pengcook.ingredient.repository; | ||
|
||
import java.util.Optional; | ||
import net.pengcook.ingredient.domain.Ingredient; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
public interface IngredientRepository extends JpaRepository<Ingredient, Long> { | ||
|
||
Optional<Ingredient> findByName(String name); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package net.pengcook.ingredient.repository; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.stereotype.Repository; | ||
|
||
import net.pengcook.ingredient.domain.IngredientSubstitution; | ||
|
||
@Repository | ||
public interface IngredientSubstitutionRepository extends JpaRepository<IngredientSubstitution, Long> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package net.pengcook.ingredient.service; | ||
|
||
import org.springframework.stereotype.Service; | ||
|
||
import net.pengcook.ingredient.domain.Ingredient; | ||
import net.pengcook.ingredient.domain.IngredientRecipe; | ||
import net.pengcook.ingredient.domain.Requirement; | ||
import net.pengcook.ingredient.repository.IngredientRecipeRepository; | ||
import net.pengcook.recipe.domain.Recipe; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class IngredientRecipeService { | ||
|
||
private final IngredientRecipeRepository ingredientRecipeRepository; | ||
|
||
public IngredientRecipe save(Ingredient ingredient, Recipe recipe, Requirement requirement) { | ||
IngredientRecipe ingredientRecipe = new IngredientRecipe(ingredient, recipe, requirement); | ||
return ingredientRecipeRepository.save(ingredientRecipe); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package net.pengcook.ingredient.service; | ||
|
||
|
||
import jakarta.transaction.Transactional; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
import net.pengcook.ingredient.domain.Ingredient; | ||
import net.pengcook.ingredient.domain.IngredientRecipe; | ||
import net.pengcook.ingredient.domain.Requirement; | ||
import net.pengcook.ingredient.dto.IngredientCreateRequest; | ||
import net.pengcook.ingredient.exception.InvalidNameException; | ||
import net.pengcook.ingredient.repository.IngredientRepository; | ||
import net.pengcook.recipe.domain.Recipe; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@Transactional | ||
@RequiredArgsConstructor | ||
@Getter | ||
public class IngredientService { | ||
|
||
private final IngredientRepository ingredientRepository; | ||
private final IngredientRecipeService ingredientRecipeService; | ||
private final IngredientSubstitutionService ingredientSubstitutionService; | ||
|
||
public void register(List<IngredientCreateRequest> requests, Recipe recipe) { | ||
validateDuplicateNames(getIngredientNames(requests)); | ||
for (IngredientCreateRequest request : requests) { | ||
registerOne(request, recipe); | ||
} | ||
} | ||
|
||
private void registerOne(IngredientCreateRequest request, Recipe recipe) { | ||
Ingredient ingredient = registerOrGetIngredient(request.name()); | ||
IngredientRecipe ingredientRecipe = registerIngredientRecipe(recipe, request, ingredient); | ||
|
||
if (request.requirement() == Requirement.ALTERNATIVE) { | ||
registerSubstitution(request, ingredientRecipe); | ||
} | ||
} | ||
|
||
private Ingredient registerOrGetIngredient(String ingredientName) { | ||
String name = ingredientName.toLowerCase(); | ||
|
||
return ingredientRepository.findByName(name) | ||
.orElseGet(() -> ingredientRepository.save(new Ingredient(name))); | ||
} | ||
|
||
private IngredientRecipe registerIngredientRecipe( | ||
Recipe recipe, | ||
IngredientCreateRequest request, | ||
Ingredient ingredient | ||
) { | ||
return ingredientRecipeService.save(ingredient, recipe, request.requirement()); | ||
} | ||
|
||
private void registerSubstitution(IngredientCreateRequest request, IngredientRecipe ingredientRecipe) { | ||
List<String> substitutionNames = request.substitutions(); | ||
validateDuplicateNames(substitutionNames); | ||
|
||
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. registerSubstitution 메서드와 registerIngredientSubstitution 이 메서드를 private 보다 IngredientRecipeService와 IngredientSubstitutionService의 public 메서드로 해보는것은 어떨까요? IngredientSubstitution을 저장하는것은 IngredientSubstitutionService의 책임인데 이걸 IngredientService가 대신 해주고 있는것 같아요. 또한 Private 메서드라 테스트가 어려워요. |
||
for (String substitutionName : substitutionNames) { | ||
Ingredient substitution = registerOrGetIngredient(substitutionName); | ||
registerIngredientSubstitution(ingredientRecipe, substitution); | ||
} | ||
} | ||
|
||
private void registerIngredientSubstitution(IngredientRecipe ingredientRecipe, Ingredient substitution) { | ||
ingredientSubstitutionService.save(ingredientRecipe, substitution); | ||
} | ||
Comment on lines
+69
to
+71
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. 이 메서드가... 꼭 필요한가요....??? 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. 가독성을 위해 생성하였습니다. |
||
|
||
private List<String> getIngredientNames(List<IngredientCreateRequest> requests) { | ||
return requests.stream() | ||
.map(IngredientCreateRequest::name) | ||
.map(String::toLowerCase) | ||
.toList(); | ||
} | ||
|
||
private void validateDuplicateNames(List<String> names) { | ||
if (hasDuplicateName(names)) { | ||
throw new InvalidNameException("ingredient name duplicated"); | ||
} | ||
} | ||
|
||
private boolean hasDuplicateName(List<String> names) { | ||
HashSet<String> nonDuplicate = new HashSet<>(names); | ||
return (names.size() != nonDuplicate.size()); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package net.pengcook.ingredient.service; | ||
|
||
import org.springframework.stereotype.Service; | ||
|
||
import net.pengcook.ingredient.domain.Ingredient; | ||
import net.pengcook.ingredient.domain.IngredientRecipe; | ||
import net.pengcook.ingredient.domain.IngredientSubstitution; | ||
import net.pengcook.ingredient.repository.IngredientSubstitutionRepository; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class IngredientSubstitutionService { | ||
|
||
private final IngredientSubstitutionRepository ingredientSubstitutionRepository; | ||
|
||
public void save(IngredientRecipe ingredientRecipe, Ingredient substitution) { | ||
IngredientSubstitution ingredientSubstitution = new IngredientSubstitution(ingredientRecipe, substitution); | ||
ingredientSubstitutionRepository.save(ingredientSubstitution); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package net.pengcook.ingredient.dto; | ||
|
||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.stream.Stream; | ||
import net.pengcook.ingredient.domain.Requirement; | ||
import net.pengcook.ingredient.exception.InvalidNameException; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.CsvSource; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
||
class IngredientCreateRequestTest { | ||
|
||
static Stream<Arguments> substitutions() { | ||
List<String> nullSubstitution = new ArrayList<>(); | ||
nullSubstitution.add(null); | ||
|
||
return Stream.of( | ||
Arguments.arguments(List.of("", "wheat")), | ||
Arguments.arguments(List.of(" ", "wheat")), | ||
Arguments.arguments(nullSubstitution) | ||
); | ||
} | ||
|
||
@ParameterizedTest | ||
@CsvSource(value = { | ||
"''", | ||
"' '", | ||
}) | ||
@DisplayName("재료 이름에 공백 또는 null은 허용하지 않는다.") | ||
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. ✅ 테스트 코드 네이밍
재료 이름에 공백 또는 null이 입력되면 예외가 발생한다. |
||
void validateName(String name) { | ||
assertThatThrownBy(() -> new IngredientCreateRequest(name, Requirement.ALTERNATIVE, List.of("wheat"))) | ||
.isInstanceOf(InvalidNameException.class); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("substitutions") | ||
@DisplayName("ALTERNATIVE 상태일 때, 대체 재료 이름에 공백 또는 null은 허용하지 않는다.") | ||
void validateSubstitutionName(List<String> substitutions) { | ||
assertThatThrownBy(() -> new IngredientCreateRequest("rice", Requirement.ALTERNATIVE, substitutions)) | ||
.isInstanceOf(InvalidNameException.class); | ||
} | ||
|
||
@Test | ||
@DisplayName("ALTERNATIVE 상태일 때, 대체 재료 리스트에 null은 허용하지 않는다.") | ||
void validateSubstitutionIsNull() { | ||
assertThatThrownBy(() -> new IngredientCreateRequest("rice", Requirement.ALTERNATIVE, null)) | ||
.isInstanceOf(InvalidNameException.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.
@AllArgumentConstructor 사용후 this(0L, ...) 으로 초기화 해도 좋을것 같아요.
Category 도메인 참고 해보시죠