Skip to content

Commit

Permalink
[#20] 음식 등록 기능을 구현한다 (#36)
Browse files Browse the repository at this point in the history
* feat: Food 도메인 작성

- Food 도메인 작성
- 하위 VO 작성
- 필요한 예외 등록

* feat: Food 컨트롤러/서비스/리포지터리 작성

- Food 컨트롤러 코드 작성
- Food 서비스 코드 작성
- Food 리포지터리 코드 작성
- 인터셉터 등록
- FoodServiceTest 코드 문제 수정
- FakeFoodRepository 초안 작성
- 요청 dto, 응답 dto 작성
- createdAt 추가

* test: Food 테스트 작성

- Food 단위 테스트 작성
- 하위 VO 테스트 작성

* test: FoodService 음식 생성 테스트 작성

- FoodService 음식 생성 테스트 작성
- FakeFoodRepository 기능 작성
- SuperBuilder 적용

* test: FoodController 통합 테스트 추가 작성

- FoodController 음식 생성 테스트 작성
- API 문서화 반영
- 필요한 fixture 등록

* test: FoodJpaRepository 테스트 작성

- FoodJpaRepository 테스트 작성

* docs: food.sql 작성

- Food DDL 작성

* fix: url 정보 추가

- 음식 url 정보 추가

* refactor: FoodServiceTest 중복 제거

- FoodServiceTest 중복 제거 및 fixture 등록

* refactor: FoodControllerWebMvcTest 픽스처 이용

- FoodControllerWebMvcTest에 fixture 이용되도록 수정

* refactor: FoodCreateRequestFixture 중복 제거

- FoodCreateRequestFixture 중복 제거하고 다른 방식으로 작성
  • Loading branch information
devholic22 authored Oct 6, 2024
1 parent d397a87 commit 24209e7
Show file tree
Hide file tree
Showing 26 changed files with 834 additions and 5 deletions.
12 changes: 12 additions & 0 deletions src/docs/asciidoc/food.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@

== Food

=== 음식 생성 (POST /api/foods)

==== 요청
include::{snippets}/food-controller-web-mvc-test/음식 생성/http-request.adoc[]
include::{snippets}/food-controller-web-mvc-test/음식 생성/request-headers.adoc[]
include::{snippets}/food-controller-web-mvc-test/음식 생성/request-fields.adoc[]

==== 응답
include::{snippets}/food-controller-web-mvc-test/음식 생성/http-response.adoc[]
include::{snippets}/food-controller-web-mvc-test/음식 생성/response-headers.adoc[]
include::{snippets}/food-controller-web-mvc-test/음식 생성/response-fields.adoc[]

=== 음식 검색 (GET /api/foods/search)

==== 요청
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.flab.eattofit.food.application;

import com.flab.eattofit.food.application.dto.FoodCreateRequest;
import com.flab.eattofit.food.domain.Food;
import com.flab.eattofit.food.domain.FoodRepository;
import com.flab.eattofit.food.domain.FoodSearchManager;
import com.flab.eattofit.food.domain.vo.FoodNutrient;
import com.flab.eattofit.food.domain.vo.FoodWeight;
import com.flab.eattofit.food.infrastructure.dto.PredictFoodSearchResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -12,12 +17,31 @@ public class FoodService {

private static final String FOOD_SEARCH_MANAGER = "doinglabFoodSearchManager";

private final FoodRepository foodRepository;
private final FoodSearchManager foodSearchManager;

public FoodService(@Qualifier(value = FOOD_SEARCH_MANAGER) final FoodSearchManager foodSearchManager) {
public FoodService(
final FoodRepository foodRepository,
@Qualifier(value = FOOD_SEARCH_MANAGER) final FoodSearchManager foodSearchManager
) {
this.foodRepository = foodRepository;
this.foodSearchManager = foodSearchManager;
}

public Food createFood(final FoodCreateRequest request, final Long memberId) {
FoodWeight weight = FoodWeight.createWith(request.servingSize(), request.unit());
FoodNutrient nutrient = FoodNutrient.builder()
.kcal(request.kcal())
.carbohydrate(request.carbohydrate())
.protein(request.protein())
.fat(request.fat())
.sodium(request.sodium())
.build();
Food food = Food.createWith(request.name(), weight, nutrient, request.url(), memberId);

return foodRepository.save(food);
}

public PredictFoodSearchResponse foodSearch(final Long memberId, final String url) {
log.info("{} 회원이 {}로 음식 검색 요청", memberId, url);
return foodSearchManager.search(url);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.flab.eattofit.food.application.dto;

import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.math.BigDecimal;

public record FoodCreateRequest(
@NotEmpty(message = "음식 이름이 있어야 합니다.")
String name,

@NotNull(message = "음식 사이즈가 있어야 합니다.")
@DecimalMin(value = "0", inclusive = false, message = "음식 사이즈는 0보다 커야 합니다.")
BigDecimal servingSize,

@NotEmpty(message = "음식 사이즈 단위가 있어야 합니다.")
String unit,

@NotNull(message = "음식 칼로리가 있어야 합니다.")
@DecimalMin(value = "0", message = "음식 칼로리는 0 이상이어야 합니다.")
BigDecimal kcal,

@NotNull(message = "음식 탄수화물이 있어야 합니다.")
@DecimalMin(value = "0", message = "음식 탄수화물은 0 이상이어야 합니다.")
BigDecimal carbohydrate,

@NotNull(message = "음식 단백질이 있어야 합니다.")
@DecimalMin(value = "0", message = "음식 단백질은 0 이상이어야 합니다.")
BigDecimal protein,

@NotNull(message = "음식 지방이 있어야 합니다.")
@DecimalMin(value = "0", message = "음식 지방은 0 이상이어야 합니다.")
BigDecimal fat,

@NotNull(message = "음식 나트륨이 있어야 합니다.")
@DecimalMin(value = "0", message = "음식 나트륨은 0 이상이어야 합니다.")
BigDecimal sodium,

@NotEmpty(message = "음식 이미지 주소가 있어야 합니다.")
String url
) {
}
59 changes: 59 additions & 0 deletions src/main/java/com/flab/eattofit/food/domain/Food.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.flab.eattofit.food.domain;

import com.flab.eattofit.food.domain.vo.FoodNutrient;
import com.flab.eattofit.food.domain.vo.FoodWeight;
import com.flab.eattofit.global.domain.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
@Entity
public class Food extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

@Embedded
private FoodWeight weight;

@Embedded
private FoodNutrient nutrient;

@Column(nullable = false)
private String url;

@Column(nullable = false)
private Long memberId;

private Food(final String name, final FoodWeight weight, final FoodNutrient nutrient, final String url, final Long memberId) {
this.name = name;
this.weight = weight;
this.nutrient = nutrient;
this.url = url;
this.memberId = memberId;
}

public static Food createWith(
final String name,
final FoodWeight weight,
final FoodNutrient nutrient,
final String url,
final Long memberId
) {
return new Food(name, weight, nutrient, url, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.flab.eattofit.food.domain;

public interface FoodRepository {

Food save(Food food);
}
50 changes: 50 additions & 0 deletions src/main/java/com/flab/eattofit/food/domain/vo/FoodNutrient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.flab.eattofit.food.domain.vo;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Embeddable
public class FoodNutrient {

@Column(nullable = false)
private BigDecimal kcal;

@Column(nullable = false)
private BigDecimal carbohydrate;

@Column(nullable = false)
private BigDecimal protein;

@Column(nullable = false)
private BigDecimal fat;

@Column(nullable = false)
private BigDecimal sodium;

public static FoodNutrient createWith(
final BigDecimal kcal,
final BigDecimal carbohydrate,
final BigDecimal protein,
final BigDecimal fat,
final BigDecimal sodium
) {
return FoodNutrient.builder()
.kcal(kcal)
.carbohydrate(carbohydrate)
.protein(protein)
.fat(fat)
.sodium(sodium)
.build();
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/flab/eattofit/food/domain/vo/FoodUnit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.flab.eattofit.food.domain.vo;

import com.flab.eattofit.food.exception.FoodUnitNotFoundException;
import lombok.Getter;

import java.util.Arrays;

@Getter
public enum FoodUnit {
GRAM("g"),
KILOGRAM("kg");

private final String name;

FoodUnit(final String name) {
this.name = name;
}

public static FoodUnit findByName(final String name) {
return Arrays.stream(values())
.filter(unit -> unit.isSame(name))
.findAny()
.orElseThrow(FoodUnitNotFoundException::new);
}

private boolean isSame(final String name) {
return name.equals(this.name);
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/flab/eattofit/food/domain/vo/FoodWeight.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.flab.eattofit.food.domain.vo;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class FoodWeight {

@Column(nullable = false)
private BigDecimal servingSize;

@Enumerated(value = EnumType.STRING)
@Column(nullable = false)
private FoodUnit unit;

public static FoodWeight createWith(final BigDecimal servingSize, final String unit) {
return new FoodWeight(servingSize, FoodUnit.findByName(unit));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.flab.eattofit.food.exception;

import com.flab.eattofit.global.exception.GlobalException;
import org.springframework.http.HttpStatus;

public class FoodUnitNotFoundException extends GlobalException {

public FoodUnitNotFoundException() {
super(HttpStatus.NOT_FOUND, "FOOD_UNIT_NOT_FOUND", "이용 가능한 음식 단위 타입이 아닙니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flab.eattofit.food.infrastructure;

import com.flab.eattofit.food.domain.Food;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FoodJpaRepository extends JpaRepository<Food, Long> {

Food save(Food food);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.flab.eattofit.food.infrastructure;

import com.flab.eattofit.food.domain.Food;
import com.flab.eattofit.food.domain.FoodRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class FoodRepositoryImpl implements FoodRepository {

private final FoodJpaRepository foodJpaRepository;

@Override
public Food save(final Food food) {
return foodJpaRepository.save(food);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/flab/eattofit/food/ui/FoodController.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.flab.eattofit.food.ui;

import com.flab.eattofit.food.application.FoodService;
import com.flab.eattofit.food.application.dto.FoodCreateRequest;
import com.flab.eattofit.food.domain.Food;
import com.flab.eattofit.food.infrastructure.dto.PredictFoodSearchResponse;
import com.flab.eattofit.food.ui.dto.FoodCreateResponse;
import com.flab.eattofit.member.ui.auth.support.annotations.AuthMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;

@RequiredArgsConstructor
@RequestMapping("/api/foods")
@RestController
Expand All @@ -19,6 +27,16 @@ public class FoodController {

private final FoodService foodService;

@PostMapping
public ResponseEntity<FoodCreateResponse> createFood(
@RequestBody @Valid final FoodCreateRequest request,
@AuthMember final Long memberId
) {
Food food = foodService.createFood(request, memberId);
return ResponseEntity.created(URI.create("/foods/" + food.getId()))
.body(FoodCreateResponse.from(food));
}

@GetMapping("/search")
public ResponseEntity<PredictFoodSearchResponse> search(
final @AuthMember Long memberId,
Expand Down
Loading

0 comments on commit 24209e7

Please sign in to comment.