From 2e95b150dc9ac86f687e24ace8555cb8ac56c214 Mon Sep 17 00:00:00 2001 From: June <68291395+june-777@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:31:04 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EC=A0=90=EC=A3=BC=20=EC=9D=8C?= =?UTF-8?q?=EC=8B=9D=20=EC=83=81=ED=92=88=20=EA=B2=8C=EC=8B=9C=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] 기능 요구사항에 맞게 Store 도메인 엔티티의 인스턴스 필드 반영 기능 요구사항: 가게 이름, 전화번호, 위치 정보, 카테고리, 이용 시간, 최소 주문 금액을 입력해 가게를 등록 notice: - 카테고리 → StoreCategory 도메인 엔티티 - 주소 → Address 임베디드 타입 - 이용 시간 → StoreTime 임베디드 타입 - 도메인 생성 검증은 StoreValidator 에서 절차지향으로 진행 * [feat] 가게 이름 검증 기능 가게 이름은 2글자 ~ 10글자만 가능 * [feat] 가게 주소 검증 기능 - 가게 주소는 송파구만 가능 - 추후 가게 위치 비즈니스 요구사항을 구체화하면, 주소 검증 로직 수정 필요 * [feat] 가게 최소 주문 금액 검증 기능 - 5,000원 이상 - 1,000원 단위 * [feat] 가게 이용 시간 검증 기능 - 가게 시작/종료 시간은 분 단위까지 가능 - 가게 시작 시간은 종료 시간보다 이전 * [feat] 가게 주소 임베디드 타입 정의 임베디드 타입 사용 이유 - 주소는 도시/구/상세주소 같은 정보들로 그룹핑 됨 - 현재는 프로토타입으로서 구 정보만 사용하지만, 가게 주소 비즈니스 요구사항이 구체화되면 다른 정보들도 활용할 여지가 있음 - 구체화가 완료되면, 엔티티로 분리할 것인지 논의 필요 * [feat] 가게 이용 시간 임베디드 타입 정의 임베디드 타입 사용 이유 - 이용 시간은 시작 시간/ 종료 시간 정보로 그룹핑 됨 - LocalDateTime 과 변수명만으로는 비즈니스 요구사항을 명시적으로 표현하기 제약이 있음 * [feat] 가게 카테고리 도메인 엔티티 정의 도메인 엔티티로 분리한 이유: - 가게 카테고리는 DB 레벨에서 관리해야, 추후 카테고리를 추가/생성/삭제할 때 변경 이상에 대처가 용이할 것으로 판단 * [feat] 시간 정보를 제공하는 DateTimeProvider 인터페이스 정의 인터페이스 설계 이유: 날짜 API 를 직접 참조함으로서 테스트하기 어려워지는 코드를 예방하기 위함 * [feat] 고정 시간을 반환하는 DateTimeProvider 테스트용 구현체 시간 관련 로직을 테스트할 때 사용 사용 방법은 javadocs 로 샘플 코드 기재 * [feat] 현재 시간을 반환하는 DateTimeProvider 프로덕션용 구현체 * [feat] DateTimeProvider 구현체인 CurrentDateTime 를 Bean 객체로 설정 * [test] Store 도메인 객체 생성 테스트 - 가게 최소 주문 가격 * [test] Store 도메인 객체 생성 테스트 - 가게 이용 시간 * [test] Store 도메인 객체 생성 테스트 - 가게 이름 * [feat] Store (가게) 도메인 관련 커스텀 예외 정의 Store 와 관련된 예외임을 명시적으로 나타내고자 함 IllegalArgumentException 등 Java 에서 제공하는 기본 예외는 예외의 명시성이 떨어진다 판단 * [feat] 가게 카테고리 생성자 추가 기본 생성자는 애노테이션 사용 및 protected 접근 제어자로 설정 * [feat] StoreRepository JPA 인터페이스 정의 * [test] StoreRepository 가게 저장 기능 테스트 - repository 단위 테스트는 @DataJpaTest 로 최소한의 Bean 만 주입하여 사용 - @SpringBootTest 는 모든 Bean 을 로드하기 때문에 테스트가 무거워짐 * [feat] StoreCategoryRepository JPA 인터페이스 정의 - 가게 카테고리 이름으로 조회하는 시그니쳐 추가 * [test] 가게 카테고리 이름으로 조회하는 기능 테스트 * [feat] VendorRepository JPA 인터페이스 정의 - StoreRepository 단위 테스트를 위해 정의함 - Store 엔티티를 저장할 때, 참조중인 Vendor(Not Null) 를 저장해야 Not-Null Property Exception 이 발생하지 않음 * [feat] 가게 생성을 담당하는 서비스 클래스 구현 - 발생가능한 RunTimeException 은 javadocs 로 명시 * [feat] 가게 등록 요청 DTO - Not Null, 빈값, 빈값 포함한 공백 에 대한 입력 검증은 Bean Validation 으로 검증함 * [test] 가게 등록 서비스 메서드 단위 테스트 * [fix] Order 엔티티의 테이블 이름을 Orders 로 명시 order 는 DBMS 예약어라서 Auto DDL 과정에서 예외가 발생함 * [feat] 인메모리 H2 DB 및 jpa yaml 설정 - datasource: 인메모리 H2 DB를 사용하도록 설정 - h2: h2 웹 콘솔 사용 가능하도록 설정 - jpa: ddl 자동 생성, sql 로깅 설정 - 설정 내용의 depth 가독성이 용이하여, properties 를 yaml 로 변경함 * [refactor] 불필요한 static 키워드 제거 * [refactor] 가게 등록 요청 DTO 패키지 이동 * [feat] 가게와 관련된 예외를 명시하는 ErrorCode Enum 정의 - 가게 관련한 예외의 범주를 ErrorCode Enum 으로 한정하도록 함 - 예외 메시지를 응집하여, 테스트 코드에서 예외 메시지 검증으로 구체적인 검증이 용이하도록 하기 위함 * [refactor] StoreException 을 생성할 때, 가게 관련 ErrorCode Enum 을 생성자 매개변수로 사용하도록 변경 * [test] 가게 등록 서비스 예외 검증을 예외 메시지로 구체적인 검증 * [test] 가게 생성 예외 검증을 예외 메시지로 구체적인 검증 * [fix] Long, LocalDateTime 데이터 타입에 대한 Bean Validation 수정 @NotBlank 는 String 타입에 적용 가능 * [feat] Store 가게 API 컨트롤러 구현 * [feat] Store 예외 핸들러 구현 * [test] Store 가게 API 컨트롤러 단위 테스트 * [feat] 메뉴카테고리 엔티티 설계 설계 배경: 음식 상품을 등록할 때, 해당 가게에 등록한 음식 카테고리를 조회할 수 있어야함. 이와 연계하여, 매장에 대한 음식 카테고리를 등록할 수 있어야함. Enum 설계도 고려해봤지만, 동적으로 음식 카테고리를 추가하는데 제약이 있고, 메뉴 카테고리 조회에도 제약이 있다고 판단. 연관관계 설명: 메뉴-메뉴카테고리 (다대일): 하나의 메뉴는 하나의 메뉴 카테고리를 갖고, 하나의 메뉴 카테고리는 여러 메뉴에 속한다. 메뉴카테고리-매장 (다대일): 하나의 메뉴카테고리는 하나의 매장에 속하고, 하나의 매장은 여러 메뉴카테고리를 갖는다. * [feat] 음식 상품이 이미지를 반드시 갖도록 Nullable 제약조건 false 로 변경 * [feat] 메뉴 도메인 Null 값 검증 기능 * [feat] 메뉴 도메인 빈 문자열, 공백 문자열 검증 기능 * [feat] 메뉴 이름 길이 검증 기능 * [feat] 메뉴 가격 검증 기능 * [feat] 메뉴 도메인 생성자 및 검증 기능 추가 * [test] Menu 도메인 객체 생성 테스트 - 가게 Null 값 * [test] Menu 도메인 객체 생성 테스트 - 메뉴카테고리 Null 값 * [test] Menu 도메인 객체 생성 테스트 - 메뉴 이름 * [test] Menu 도메인 객체 생성 테스트 - 메뉴 가격 * [test] Menu 도메인 객체 생성 테스트 - 메뉴 사진 url * [feat] 메뉴카테고리 도메인 Null 값 검증 기능 * [feat] 메뉴카테고리 도메인 빈 문자열, 공백 문자열 검증 기능 * [feat] 메뉴카테고리 이름 길이 검증 기능 * [feat] 메뉴카테고리 생성자에 검증 기능 추가 * [feat] 메뉴카테고리 객체 생성 실패 커스텀 예외 * [feat] 메뉴 객체 생성 실패 커스텀 예외 * [test] MenuCategory 도메인 객체 생성 테스트 - 가게 Null 값 * [test] MenuCategory 도메인 객체 생성 테스트 - 이름 * [feat] 점주 ID 기반으로 매장을 조회하는 시그니쳐 추가 점주가 가게 메뉴 등록 할 때, 점주에 대한 가게를 조회하는 로직이 필요함 * [test] 점주 ID 기반으로 매장을 조회하는 기능 테스트 * [feat] MenuCategoryRepository JPA 인터페이스 정의 가게ID와 메뉴카테고리 이름으로 메뉴카테고리를 조회하는 기능 * [test] 가게ID와 메뉴카테고리 이름으로 메뉴카테고리를 조회하는 기능 테스트 * [feat] MenuRepository JPA 인터페이스 정의 * [feat] 메뉴 카테고리를 찾을 수 없는 커스텀 예외 * [feat] 가게를 찾을 수 없는 커스텀 예외 * [feat] @Getter 롬복 추가 * [feat] 가게 메뉴 등록 요청 DTO * [feat] 가게 메뉴 등록 서비스 * [test] 가게 메뉴 등록 서비스 테스트 * [feat] Vendor @Getter 롬복 추가 및 라인 정리 * [test] merge 과정에서 깨지는 테스트코드 수정 및 테스트 * [feat] 가게 메뉴 등록 요청 Bean Validation 예외 메시지 구체화 * [feat] Store 객체 생성 검증 조건 추가 - Null 체크 * [test] 기존 테스트 코드의 Store 객체 생성자에 null 제거 및 테스트 * [feat] 가게 메뉴 등록 요청 DTO에 가게 ID 추가 * [fix] 가게 ID로 가게를 조회하도록 수정 - 점주 - 가게 일대다 관계로, 점주 ID로 조회하면 가게 목록을 조회하게 됨 * [feat] 가게의 점주와 동일한지 검증하는 기능 * [feat] 가게의 점주와 동일하지 않을 때 발생하는 커스텀 예외 --- .../camp/woowak/lab/menu/domain/Menu.java | 31 +++ .../woowak/lab/menu/domain/MenuCategory.java | 36 +++ .../menu/domain/MenuCategoryValidator.java | 38 ++++ .../woowak/lab/menu/domain/MenuValidator.java | 46 ++++ .../InvalidMenuCategoryCreationException.java | 10 + .../InvalidMenuCreationException.java | 10 + .../NotFoundMenuCategoryException.java | 10 + .../repository/MenuCategoryRepository.java | 13 ++ .../lab/menu/repository/MenuRepository.java | 8 + .../camp/woowak/lab/store/domain/Store.java | 13 +- .../lab/store/domain/StoreValidator.java | 14 +- .../exception/NotEqualsOwnerException.java | 9 + .../exception/NotFoundStoreException.java | 10 + .../lab/store/exception/StoreException.java | 1 + .../service/StoreMenuRegistrationService.java | 67 ++++++ .../dto/StoreMenuRegistrationRequest.java | 31 +++ .../camp/woowak/lab/vendor/domain/Vendor.java | 2 + .../lab/menu/domain/MenuCategoryTest.java | 118 ++++++++++ .../camp/woowak/lab/menu/domain/MenuTest.java | 205 ++++++++++++++++++ .../MenuCategoryRepositoryTest.java | 139 ++++++++++++ .../woowak/lab/store/domain/StoreTest.java | 104 ++++++--- .../StoreMenuRegistrationServiceTest.java | 122 +++++++++++ 22 files changed, 1000 insertions(+), 37 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/menu/domain/MenuCategory.java create mode 100644 src/main/java/camp/woowak/lab/menu/domain/MenuCategoryValidator.java create mode 100644 src/main/java/camp/woowak/lab/menu/domain/MenuValidator.java create mode 100644 src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCategoryCreationException.java create mode 100644 src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCreationException.java create mode 100644 src/main/java/camp/woowak/lab/menu/exception/NotFoundMenuCategoryException.java create mode 100644 src/main/java/camp/woowak/lab/menu/repository/MenuCategoryRepository.java create mode 100644 src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java create mode 100644 src/main/java/camp/woowak/lab/store/exception/NotEqualsOwnerException.java create mode 100644 src/main/java/camp/woowak/lab/store/exception/NotFoundStoreException.java create mode 100644 src/main/java/camp/woowak/lab/store/service/StoreMenuRegistrationService.java create mode 100644 src/main/java/camp/woowak/lab/store/service/dto/StoreMenuRegistrationRequest.java create mode 100644 src/test/java/camp/woowak/lab/menu/domain/MenuCategoryTest.java create mode 100644 src/test/java/camp/woowak/lab/menu/domain/MenuTest.java create mode 100644 src/test/java/camp/woowak/lab/menu/repository/MenuCategoryRepositoryTest.java create mode 100644 src/test/java/camp/woowak/lab/store/service/StoreMenuRegistrationServiceTest.java diff --git a/src/main/java/camp/woowak/lab/menu/domain/Menu.java b/src/main/java/camp/woowak/lab/menu/domain/Menu.java index e5985670..6b6f2dae 100644 --- a/src/main/java/camp/woowak/lab/menu/domain/Menu.java +++ b/src/main/java/camp/woowak/lab/menu/domain/Menu.java @@ -1,18 +1,49 @@ package camp.woowak.lab.menu.domain; import camp.woowak.lab.store.domain.Store; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; @Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Menu { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) private Store store; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "menu_category_id", nullable = false) + private MenuCategory menuCategory; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private Integer price; + + @Column(nullable = false) + private String imageUrl; + + public Menu(Store store, MenuCategory menuCategory, String name, Integer price, String imageUrl) { + MenuValidator.validate(store, menuCategory, name, price, imageUrl); + this.store = store; + this.menuCategory = menuCategory; + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + } + } diff --git a/src/main/java/camp/woowak/lab/menu/domain/MenuCategory.java b/src/main/java/camp/woowak/lab/menu/domain/MenuCategory.java new file mode 100644 index 00000000..2e8d10b2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/domain/MenuCategory.java @@ -0,0 +1,36 @@ +package camp.woowak.lab.menu.domain; + +import camp.woowak.lab.store.domain.Store; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MenuCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "store_id", nullable = false) + private Store store; + + @Column(nullable = false) + private String name; + + public MenuCategory(Store store, String name) { + MenuCategoryValidator.validate(store, name); + this.store = store; + this.name = name; + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/domain/MenuCategoryValidator.java b/src/main/java/camp/woowak/lab/menu/domain/MenuCategoryValidator.java new file mode 100644 index 00000000..26f9c71b --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/domain/MenuCategoryValidator.java @@ -0,0 +1,38 @@ +package camp.woowak.lab.menu.domain; + +import camp.woowak.lab.menu.exception.InvalidMenuCategoryCreationException; +import camp.woowak.lab.store.domain.Store; + +public class MenuCategoryValidator { + + private static final int MAX_NAME_LENGTH = 10; + + public static void validate(final Store store, final String name) { + validateNotNull(store, name); + validateNotBlank(name); + validateNameLength(name); + } + + private static void validateNotNull(final Object... targets) { + for (Object target : targets) { + if (target == null) { + throw new InvalidMenuCategoryCreationException(target + "은 Null 이 될 수 없습니다."); + } + } + } + + private static void validateNotBlank(final String... targets) { + for (String target : targets) { + if (target.isBlank()) { + throw new InvalidMenuCategoryCreationException(target + "은 빈 문자열이거나 공백 문자열이 포함될 수 없습니다."); + } + } + } + + private static void validateNameLength(final String name) { + if (name.length() > MAX_NAME_LENGTH) { + throw new InvalidMenuCategoryCreationException("메뉴 카테고리 이름은 " + MAX_NAME_LENGTH + "글자까지 가능합니다."); + } + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/domain/MenuValidator.java b/src/main/java/camp/woowak/lab/menu/domain/MenuValidator.java new file mode 100644 index 00000000..db0bc7e9 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/domain/MenuValidator.java @@ -0,0 +1,46 @@ +package camp.woowak.lab.menu.domain; + +import camp.woowak.lab.menu.exception.InvalidMenuCreationException; +import camp.woowak.lab.store.domain.Store; + +public class MenuValidator { + + private static final int MAX_NAME_LENGTH = 10; + + public static void validate(final Store store, final MenuCategory menuCategory, final String name, + final Integer price, final String imageUrl) { + validateNotNull(store, menuCategory, name, price, imageUrl); + validateNotBlank(name, imageUrl); + validateNameLength(name); + validatePriceNegative(price); + } + + private static void validateNotNull(final Object... targets) { + for (Object target : targets) { + if (target == null) { + throw new InvalidMenuCreationException(target + "은 Null 이 될 수 없습니다."); + } + } + } + + private static void validateNotBlank(final String... targets) { + for (String target : targets) { + if (target.isBlank()) { + throw new InvalidMenuCreationException(target + "은 빈 문자열이거나 공백 문자열이 포함될 수 없습니다."); + } + } + } + + private static void validateNameLength(final String name) { + if (name.length() > MAX_NAME_LENGTH) { + throw new InvalidMenuCreationException("메뉴 이름은 " + MAX_NAME_LENGTH + "글자까지 가능합니다."); + } + } + + private static void validatePriceNegative(final Integer price) { + if (price <= 0) { + throw new InvalidMenuCreationException("메뉴의 가격은 양수만 가능합니다"); + } + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCategoryCreationException.java b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCategoryCreationException.java new file mode 100644 index 00000000..5b3aeba8 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCategoryCreationException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.menu.exception; + +// TODO: extends CustomException +public class InvalidMenuCategoryCreationException extends RuntimeException { + + public InvalidMenuCategoryCreationException(String message) { + super(message); + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCreationException.java b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCreationException.java new file mode 100644 index 00000000..3e7ed471 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuCreationException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.menu.exception; + +// TODO: extends CustomException +public class InvalidMenuCreationException extends RuntimeException { + + public InvalidMenuCreationException(String message) { + super(message); + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/exception/NotFoundMenuCategoryException.java b/src/main/java/camp/woowak/lab/menu/exception/NotFoundMenuCategoryException.java new file mode 100644 index 00000000..4e8ef121 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/NotFoundMenuCategoryException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.menu.exception; + +// TODO: extends CustomException +public class NotFoundMenuCategoryException extends RuntimeException { + + public NotFoundMenuCategoryException(String message) { + super(message); + } + +} diff --git a/src/main/java/camp/woowak/lab/menu/repository/MenuCategoryRepository.java b/src/main/java/camp/woowak/lab/menu/repository/MenuCategoryRepository.java new file mode 100644 index 00000000..708658c5 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/repository/MenuCategoryRepository.java @@ -0,0 +1,13 @@ +package camp.woowak.lab.menu.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.menu.domain.MenuCategory; + +public interface MenuCategoryRepository extends JpaRepository { + + Optional findByStoreIdAndName(Long storeId, String name); + +} diff --git a/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java new file mode 100644 index 00000000..89305b0d --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.menu.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.menu.domain.Menu; + +public interface MenuRepository extends JpaRepository { +} diff --git a/src/main/java/camp/woowak/lab/store/domain/Store.java b/src/main/java/camp/woowak/lab/store/domain/Store.java index d67aeb3b..16fc444a 100644 --- a/src/main/java/camp/woowak/lab/store/domain/Store.java +++ b/src/main/java/camp/woowak/lab/store/domain/Store.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; +import camp.woowak.lab.store.exception.NotEqualsOwnerException; import camp.woowak.lab.vendor.domain.Vendor; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -14,10 +15,12 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter public class Store { @Id @@ -35,8 +38,6 @@ public class Store { @Column(nullable = false) private String name; - // TODO: 위치 정보에 대한 요구사항 논의 후 수정 예정. - // i.g) 송파구로 특정, 도시 정보로 특정 등 요구사항이 정의되어야 엔티티 설계를 진행할 수 있음 @Embedded private StoreAddress storeAddress; @@ -52,7 +53,7 @@ public class Store { public Store(Vendor owner, StoreCategory storeCategory, String name, String address, String phoneNumber, Integer minOrderPrice, LocalDateTime startTime, LocalDateTime endTime ) { - StoreValidator.validate(name, address, minOrderPrice, startTime, endTime); + StoreValidator.validate(owner, storeCategory, name, address, minOrderPrice, startTime, endTime); this.owner = owner; this.storeCategory = storeCategory; this.name = name; @@ -62,4 +63,10 @@ public Store(Vendor owner, StoreCategory storeCategory, String name, String addr this.storeTime = new StoreTime(startTime, endTime); } + public void validateOwner(Vendor owner) { + if (!this.owner.equals(owner)) { + throw new NotEqualsOwnerException("가게의 점주가 일치하지 않습니다." + this.owner + ", " + owner); + } + } + } diff --git a/src/main/java/camp/woowak/lab/store/domain/StoreValidator.java b/src/main/java/camp/woowak/lab/store/domain/StoreValidator.java index d17d0a3d..b8393cc4 100644 --- a/src/main/java/camp/woowak/lab/store/domain/StoreValidator.java +++ b/src/main/java/camp/woowak/lab/store/domain/StoreValidator.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import camp.woowak.lab.store.exception.StoreException; +import camp.woowak.lab.vendor.domain.Vendor; public class StoreValidator { @@ -15,9 +16,11 @@ public class StoreValidator { private static final int MIN_NAME_LENGTH = 2; private static final int MAX_NAME_LENGTH = 10; - public static void validate(final String name, final String address, final Integer minOrderPrice, + public static void validate(final Vendor owner, StoreCategory storeCategory, final String name, + final String address, final Integer minOrderPrice, final LocalDateTime startTime, final LocalDateTime endTime ) { + validateNotNull(owner, storeCategory, name, address, minOrderPrice, startTime, endTime); validateName(name); validateAddress(address); validateMinOrderPrice(minOrderPrice); @@ -25,6 +28,14 @@ public static void validate(final String name, final String address, final Integ validateTime(startTime, endTime); } + private static void validateNotNull(Object... targets) { + for (Object target : targets) { + if (target == null) { + throw new StoreException(NULL_EXIST); + } + } + } + private static void validateName(final String name) { if (MIN_NAME_LENGTH <= name.length() && name.length() <= MAX_NAME_LENGTH) { return; @@ -32,7 +43,6 @@ private static void validateName(final String name) { throw new StoreException(INVALID_NAME_RANGE); } - // TODO: 가게 위치 비즈니스 요구사항 구체화하면, 주소 검증 로직 수정 예정 private static void validateAddress(final String address) { if (StoreAddress.DEFAULT_DISTRICT.equals(address)) { return; diff --git a/src/main/java/camp/woowak/lab/store/exception/NotEqualsOwnerException.java b/src/main/java/camp/woowak/lab/store/exception/NotEqualsOwnerException.java new file mode 100644 index 00000000..f122216c --- /dev/null +++ b/src/main/java/camp/woowak/lab/store/exception/NotEqualsOwnerException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.store.exception; + +public class NotEqualsOwnerException extends RuntimeException { + + public NotEqualsOwnerException(String message) { + super(message); + } + +} diff --git a/src/main/java/camp/woowak/lab/store/exception/NotFoundStoreException.java b/src/main/java/camp/woowak/lab/store/exception/NotFoundStoreException.java new file mode 100644 index 00000000..42848feb --- /dev/null +++ b/src/main/java/camp/woowak/lab/store/exception/NotFoundStoreException.java @@ -0,0 +1,10 @@ +package camp.woowak.lab.store.exception; + +// TODO: 404Exception 상속하도록 수정 +public class NotFoundStoreException extends RuntimeException { + + public NotFoundStoreException(String message) { + super(message); + } + +} diff --git a/src/main/java/camp/woowak/lab/store/exception/StoreException.java b/src/main/java/camp/woowak/lab/store/exception/StoreException.java index 628c6d6c..a108d391 100644 --- a/src/main/java/camp/woowak/lab/store/exception/StoreException.java +++ b/src/main/java/camp/woowak/lab/store/exception/StoreException.java @@ -15,6 +15,7 @@ public StoreException(final ErrorCode errorCode) { @Getter public enum ErrorCode { + NULL_EXIST("NULL 값이 존재합니다."), INVALID_NAME_RANGE("가게 이름은 2글자 ~ 10글자 이어야합니다."), INVALID_ADDRESS("가게 주소는 송파구만 가능합니다."), diff --git a/src/main/java/camp/woowak/lab/store/service/StoreMenuRegistrationService.java b/src/main/java/camp/woowak/lab/store/service/StoreMenuRegistrationService.java new file mode 100644 index 00000000..6b8af6b4 --- /dev/null +++ b/src/main/java/camp/woowak/lab/store/service/StoreMenuRegistrationService.java @@ -0,0 +1,67 @@ +package camp.woowak.lab.store.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.menu.exception.NotFoundMenuCategoryException; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.exception.NotFoundStoreException; +import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.store.service.dto.StoreMenuRegistrationRequest; +import camp.woowak.lab.vendor.domain.Vendor; +import lombok.RequiredArgsConstructor; + +/** + *

Description

+ * 가게 메뉴 등록을 담당 + * + *

User Story

+ * 점주는 음식 상품을 등록할 수 있다. + */ +@Service +@RequiredArgsConstructor +public class StoreMenuRegistrationService { + + private final StoreRepository storeRepository; + private final MenuRepository menuRepository; + private final MenuCategoryRepository menuCategoryRepository; + + public void storeMenuRegistration(final Vendor owner, final StoreMenuRegistrationRequest request) { + Store store = findStoreBy(request.storeId()); + store.validateOwner(owner); + + List menuLineItems = request.menuItems(); + List menus = createMenus(store, menuLineItems); + + menuRepository.saveAll(menus); + } + + private Store findStoreBy(final Long storeId) { + return storeRepository.findById(storeId) + .orElseThrow(() -> new NotFoundStoreException("존재하지 않는 가게입니다.")); + } + + private List createMenus(final Store store, + final List menuLineItems + ) { + return menuLineItems.stream() + .map(menuLineItem -> createMenu(store, menuLineItem)) + .toList(); + } + + private Menu createMenu(final Store store, final StoreMenuRegistrationRequest.MenuLineItem menuLineItem) { + MenuCategory menuCategory = findMenuCategoryBy(store, menuLineItem.categoryName()); + return new Menu(store, menuCategory, menuLineItem.name(), menuLineItem.price(), menuLineItem.imageUrl()); + } + + private MenuCategory findMenuCategoryBy(final Store store, final String manuCategoryName) { + return menuCategoryRepository.findByStoreIdAndName(store.getId(), manuCategoryName) + .orElseThrow(() -> new NotFoundMenuCategoryException(store + ", " + manuCategoryName + " 의 메뉴카테고리가 없습니다.")); + } + +} diff --git a/src/main/java/camp/woowak/lab/store/service/dto/StoreMenuRegistrationRequest.java b/src/main/java/camp/woowak/lab/store/service/dto/StoreMenuRegistrationRequest.java new file mode 100644 index 00000000..5a588aaa --- /dev/null +++ b/src/main/java/camp/woowak/lab/store/service/dto/StoreMenuRegistrationRequest.java @@ -0,0 +1,31 @@ +package camp.woowak.lab.store.service.dto; + +import java.util.List; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record StoreMenuRegistrationRequest( + @NotBlank(message = "가게 ID는 필수값입니다.") + Long storeId, + + @NotNull(message = "등록할 메뉴는 필수값입니다.") + List menuItems +) { + + public record MenuLineItem( + @NotBlank(message = "메뉴 이름은 필수값입니다.") + String name, + + @NotBlank(message = "사진은 필수값입니다.") + String imageUrl, + + @NotBlank(message = "메뉴 카테고리 이름은 필수값입니다.") + String categoryName, + + @NotNull(message = "메뉴 가격은 필수값입니다.") + Integer price + ) { + } + +} diff --git a/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java b/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java index d6f35a9b..0e39e56d 100644 --- a/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java +++ b/src/main/java/camp/woowak/lab/vendor/domain/Vendor.java @@ -10,8 +10,10 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToOne; +import lombok.Getter; @Entity +@Getter public class Vendor { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/test/java/camp/woowak/lab/menu/domain/MenuCategoryTest.java b/src/test/java/camp/woowak/lab/menu/domain/MenuCategoryTest.java new file mode 100644 index 00000000..87febf38 --- /dev/null +++ b/src/test/java/camp/woowak/lab/menu/domain/MenuCategoryTest.java @@ -0,0 +1,118 @@ +package camp.woowak.lab.menu.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.menu.exception.InvalidMenuCategoryCreationException; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.domain.StoreAddress; +import camp.woowak.lab.store.domain.StoreCategory; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +class MenuCategoryTest { + + @Nested + @DisplayName("메뉴 카테고리 생성은") + class MenuCategoryCreateTest { + + Store storeFixture = createValidStore(); + + @Nested + @DisplayName("메뉴 카테고리에 대한 가게가") + class StoreOfMenu { + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCategoryCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new MenuCategory(null, "가게")) + .isInstanceOf(InvalidMenuCategoryCreationException.class); + } + + } + + @Nested + @DisplayName("이름이") + class MenuName { + + @Test + @DisplayName("[Success] 10글자 이하만 가능하다") + void success() { + // given & when & then + assertThatCode(() -> new MenuCategory(storeFixture, "1234567890")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new MenuCategory(storeFixture, null)) + .isInstanceOf(InvalidMenuCategoryCreationException.class); + } + + @Test + @DisplayName("[Exception] 빈 문자열이면 InvalidMenuCreationException 이 발생한다") + void isEmpty() { + // given & when & then + assertThatCode(() -> new MenuCategory(storeFixture, "")) + .isInstanceOf(InvalidMenuCategoryCreationException.class); + } + + @Test + @DisplayName("[Exception] 공백 문자열이면 InvalidMenuCreationException 이 발생한다") + void isBlank() { + // given & when & then + assertThatCode(() -> new MenuCategory(storeFixture, " ")) + .isInstanceOf(InvalidMenuCategoryCreationException.class); + } + + @Test + @DisplayName("[Exception] 10 글자를 초과하면 InvalidMenuCreationException 이 발생한다") + void greaterThanMaxNameLength() { + // given & when & then + assertThatCode(() -> new MenuCategory(storeFixture, "12345678901")) + .isInstanceOf(InvalidMenuCategoryCreationException.class); + } + + } + + } + + private Store createValidStore() { + LocalDateTime validStartDateFixture = LocalDateTime.of(2020, 1, 1, 1, 1); + LocalDateTime validEndDateFixture = LocalDateTime.of(2020, 1, 1, 2, 1); + String validNameFixture = "3K1K 가게"; + String validAddressFixture = StoreAddress.DEFAULT_DISTRICT; + String validPhoneNumberFixture = "02-1234-5678"; + Integer validMinOrderPriceFixture = 5000; + + return new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, + validPhoneNumberFixture, + validMinOrderPriceFixture, + validStartDateFixture, validEndDateFixture); + } + + private Vendor createVendor() { + PayAccount payAccount = new TestPayAccount(1L); + PasswordEncoder passwordEncoder = new NoOpPasswordEncoder(); + + return new Vendor("vendor", + "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder); + } + + private StoreCategory createStoreCategory() { + return new StoreCategory("양식"); + } + +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java b/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java new file mode 100644 index 00000000..79cb2c43 --- /dev/null +++ b/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java @@ -0,0 +1,205 @@ +package camp.woowak.lab.menu.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.menu.exception.InvalidMenuCreationException; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.domain.StoreAddress; +import camp.woowak.lab.store.domain.StoreCategory; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +class MenuTest { + + Store storeFixture = createValidStore(); + MenuCategory menuCategoryFixture = createValidMenuCategory(); + + @Nested + @DisplayName("메뉴 생성은") + class MenuCreateTest { + + @Nested + @DisplayName("메뉴가 속한 가게가") + class MenuOfStore { + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new Menu(null, menuCategoryFixture, "1234", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + } + + @Nested + @DisplayName("메뉴카테고리가") + class MenuCategory { + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, null, "1234", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + } + + @Nested + @DisplayName("메뉴 이름이") + class MenuName { + + @Test + @DisplayName("[Success] 10글자 이하만 가능하다") + void success() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "1234567890", 1000, "image")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, null, 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 빈 문자열이면 InvalidMenuCreationException 이 발생한다") + void isEmpty() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 공백 문자열이면 InvalidMenuCreationException 이 발생한다") + void isBlank() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, " ", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 10 글자를 초과하면 InvalidMenuCreationException 이 발생한다") + void greaterThanMaxNameLength() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "12345678901", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + } + + @Nested + @DisplayName("메뉴 가격이") + class MenuPrice { + + @Test + @DisplayName("[Success] 양수만 가능하다") + void success() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "1234567890", 1, "image")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "메뉴이름", null, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 음수면 InvalidMenuCreationException 이 발생한다") + void isNegative() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "메뉴이름", -1, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 0이면 InvalidMenuCreationException 이 발생한다") + void isZero() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "메뉴이름", 0, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + } + + @Nested + @DisplayName("메뉴 사진 url 이") + class MenuDescription { + + @Test + @DisplayName("[Exception] Null 이면 InvalidMenuCreationException 이 발생한다") + void isNull() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, null, 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 빈 문자열이면 InvalidMenuCreationException 이 발생한다") + void isEmpty() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, "", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + @Test + @DisplayName("[Exception] 공백 문자열이면 InvalidMenuCreationException 이 발생한다") + void isBlank() { + // given & when & then + assertThatCode(() -> new Menu(storeFixture, menuCategoryFixture, " ", 1000, "image")) + .isInstanceOf(InvalidMenuCreationException.class); + } + + } + + } + + private Store createValidStore() { + LocalDateTime validStartDateFixture = LocalDateTime.of(2020, 1, 1, 1, 1); + LocalDateTime validEndDateFixture = LocalDateTime.of(2020, 1, 1, 2, 1); + String validNameFixture = "3K1K 가게"; + String validAddressFixture = StoreAddress.DEFAULT_DISTRICT; + String validPhoneNumberFixture = "02-1234-5678"; + Integer validMinOrderPriceFixture = 5000; + + return new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, + validPhoneNumberFixture, + validMinOrderPriceFixture, + validStartDateFixture, validEndDateFixture); + } + + private MenuCategory createValidMenuCategory() { + return new MenuCategory(storeFixture, "1234567890"); + } + + private Vendor createVendor() { + PayAccount payAccount = new TestPayAccount(1L); + PasswordEncoder passwordEncoder = new NoOpPasswordEncoder(); + + return new Vendor("vendor", + "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder); + } + + private StoreCategory createStoreCategory() { + return new StoreCategory("양식"); + } + +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/menu/repository/MenuCategoryRepositoryTest.java b/src/test/java/camp/woowak/lab/menu/repository/MenuCategoryRepositoryTest.java new file mode 100644 index 00000000..37b7ceb5 --- /dev/null +++ b/src/test/java/camp/woowak/lab/menu/repository/MenuCategoryRepositoryTest.java @@ -0,0 +1,139 @@ +package camp.woowak.lab.menu.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import camp.woowak.lab.infra.date.DateTimeProvider; +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.domain.StoreAddress; +import camp.woowak.lab.store.domain.StoreCategory; +import camp.woowak.lab.store.repository.StoreCategoryRepository; +import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.vendor.repository.VendorRepository; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@DataJpaTest +class MenuCategoryRepositoryTest { + + @Autowired + VendorRepository vendorRepository; + + @Autowired + StoreRepository storeRepository; + + @Autowired + MenuCategoryRepository menuCategoryRepository; + + @Autowired + StoreCategoryRepository storeCategoryRepository; + + @Autowired + PayAccountRepository payAccountRepository; + + @Nested + @DisplayName("가게와 메뉴카테고리 이름으로 메뉴카테고리를 조회하는 기능은") + class FindByStoreAndNameTest { + + @Test + @DisplayName("[Success] 가게와 메뉴카테고리 이름이 있으면 조회를 성공한다") + void success() { + // given + PayAccount payAccount = payAccountRepository.save(new PayAccount()); + + Vendor vendor = createVendor(payAccount); + String categoryName = "돈가스"; + StoreCategory storeCategory = new StoreCategory(categoryName); + + vendorRepository.saveAndFlush(vendor); + storeCategoryRepository.saveAndFlush(storeCategory); + + Store store = createStore(vendor, storeCategory); + storeRepository.saveAndFlush(store); + MenuCategory menuCategory = new MenuCategory(store, categoryName); + menuCategoryRepository.save(menuCategory); + + // when & then + assertThat(menuCategoryRepository.findByStoreIdAndName(store.getId(), categoryName)) + .isPresent() + .containsSame(menuCategory); + } + + @Test + @DisplayName("[Exception] 가게가 없으면 빈 Optional 을 반환한다") + void notExistStore() { + // given + String categoryName = "돈가스"; + + // when & then + assertThat(menuCategoryRepository.findByStoreIdAndName(1234567L, categoryName)).isEmpty(); + } + + @Test + @DisplayName("[Exception] 메뉴카테고리 이름이 없으면 빈 Optional 을 반환한다") + void notExistMenuCategoryName() { + // given + String categoryName = "돈가스"; + String notExistCategoryName = "xxx"; + PayAccount payAccount = payAccountRepository.save(new PayAccount()); + + Vendor vendor = createVendor(payAccount); + vendorRepository.saveAndFlush(vendor); + + StoreCategory storeCategory = new StoreCategory(categoryName); + storeCategoryRepository.saveAndFlush(storeCategory); + + Store store = createStore(vendor, storeCategory); + storeRepository.saveAndFlush(store); + + MenuCategory menuCategory = new MenuCategory(store, categoryName); + menuCategoryRepository.save(menuCategory); + + // when & then + assertThat(menuCategoryRepository.findByStoreIdAndName(store.getId(), notExistCategoryName)) + .isEmpty(); + } + + } + + private Store createStore(Vendor vendor, StoreCategory storeCategory) { + DateTimeProvider fixedStartTime = () -> LocalDateTime.of(2024, 8, 24, 1, 0, 0); + DateTimeProvider fixedEndTime = () -> LocalDateTime.of(2024, 8, 24, 5, 0, 0); + + LocalDateTime validStartTimeFixture = fixedStartTime.now(); + LocalDateTime validEndTimeFixture = fixedEndTime.now(); + + String validNameFixture = "3K1K 가게"; + String validAddressFixture = StoreAddress.DEFAULT_DISTRICT; + String validPhoneNumberFixture = "02-0000-0000"; + Integer validMinOrderPriceFixture = 5000; + + return new Store(vendor, + storeCategory, + validNameFixture, + validAddressFixture, + validPhoneNumberFixture, + validMinOrderPriceFixture, + validStartTimeFixture, + validEndTimeFixture + ); + } + + private Vendor createVendor(PayAccount payAccount) { + PasswordEncoder passwordEncoder = new NoOpPasswordEncoder(); + return new Vendor("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, + passwordEncoder); + } + +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/store/domain/StoreTest.java b/src/test/java/camp/woowak/lab/store/domain/StoreTest.java index 20b96e69..5d59985c 100644 --- a/src/test/java/camp/woowak/lab/store/domain/StoreTest.java +++ b/src/test/java/camp/woowak/lab/store/domain/StoreTest.java @@ -10,7 +10,12 @@ import org.junit.jupiter.api.Test; import camp.woowak.lab.infra.date.DateTimeProvider; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; import camp.woowak.lab.store.exception.StoreException; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; class StoreTest { @@ -41,9 +46,10 @@ void validMinOrderPrice() { int validMinOrderPrice = 5000; // when & then - assertThatCode(() -> new Store(null, null, validNameFixture, validAddressFixture, null, - validMinOrderPrice, - validStartTimeFixture, validEndTimeFixture)) + assertThatCode( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPrice, + validStartTimeFixture, validEndTimeFixture)) .doesNotThrowAnyException(); } @@ -54,9 +60,10 @@ void lessThanMinOrderPrice() { int lessThanMinOrderPrice = 4999; // when & then - assertThatThrownBy(() -> new Store(null, null, validNameFixture, validAddressFixture, null, - lessThanMinOrderPrice, - validStartTimeFixture, validEndTimeFixture)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + lessThanMinOrderPrice, + validStartTimeFixture, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_MIN_ORDER_PRICE.getMessage()); } @@ -68,9 +75,10 @@ void validUnitOrderPrice() { int validMinOrderPrice = 10000; // when & then - assertThatCode(() -> new Store(null, null, validNameFixture, validAddressFixture, null, - validMinOrderPrice, - validStartTimeFixture, validEndTimeFixture)) + assertThatCode( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPrice, + validStartTimeFixture, validEndTimeFixture)) .doesNotThrowAnyException(); } @@ -81,9 +89,10 @@ void inValidUnitOrderPrice() { int inValidUnitOrderPrice = 5001; // when & then - assertThatThrownBy(() -> new Store(null, null, validNameFixture, validAddressFixture, null, - inValidUnitOrderPrice, - validStartTimeFixture, validEndTimeFixture)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + inValidUnitOrderPrice, + validStartTimeFixture, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_UNIT_OF_MIN_ORDER_PRICE.getMessage()); } @@ -102,7 +111,8 @@ void storeStartTimeBeforeThanEndTime() { // when & then assertThatCode( - () -> new Store(null, null, validNameFixture, validAddressFixture, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPriceFixture, validStartTime, validEndTime)) .doesNotThrowAnyException(); } @@ -115,7 +125,8 @@ void endTimeSameWithStartTime() { // when & then assertThatThrownBy( - () -> new Store(null, null, validNameFixture, validAddressFixture, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPriceFixture, validStartTimeFixture, endTimeSameWithStartTime)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME.getMessage()); @@ -129,7 +140,8 @@ void endTimeBeforeThanStartTime() { // when & then assertThatThrownBy( - () -> new Store(null, null, validNameFixture, validAddressFixture, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPriceFixture, validStartTimeFixture, endTimeBeforeThanStartTime)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME.getMessage()); @@ -144,7 +156,8 @@ void validStartTimeUnit() { // when & then assertThatCode( - () -> new Store(null, null, validNameFixture, validAddressFixture, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPriceFixture, validStartTimeUnitMinute, validEndTimeUnitMinute)) .doesNotThrowAnyException(); } @@ -158,7 +171,8 @@ void startTimeWithSeconds() { // when & then assertThatThrownBy( - () -> new Store(null, null, validNameFixture, validAddressFixture, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + validMinOrderPriceFixture, startTimeWithSeconds, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME_UNIT.getMessage()); @@ -172,8 +186,10 @@ void startTimeWithNanoSeconds() { LocalDateTime startTimeWithNanoSeconds = inValidUnitDateTimeProvider.now(); // when & then - assertThatThrownBy(() -> new Store(null, null, validNameFixture, validAddressFixture, null, 5000, - startTimeWithNanoSeconds, validEndTimeFixture)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + 5000, + startTimeWithNanoSeconds, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME_UNIT.getMessage()); } @@ -186,8 +202,10 @@ void endTimeWithSeconds() { LocalDateTime endTimeWithSeconds = inValidUnitDateTimeProvider.now(); // when & then - assertThatThrownBy(() -> new Store(null, null, validNameFixture, validAddressFixture, null, 5000, - validStartTimeFixture, endTimeWithSeconds)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + 5000, + validStartTimeFixture, endTimeWithSeconds)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME_UNIT.getMessage()); } @@ -200,8 +218,10 @@ void endTimeWithNanoSeconds() { LocalDateTime endTimeWithNanoSeconds = inValidUnitDateTimeProvider.now(); // when & then - assertThatThrownBy(() -> new Store(null, null, validNameFixture, validAddressFixture, null, 5000, - validStartTimeFixture, endTimeWithNanoSeconds)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddressFixture, null, + 5000, + validStartTimeFixture, endTimeWithNanoSeconds)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_TIME_UNIT.getMessage()); } @@ -219,9 +239,11 @@ void validStoreName() { String lengthValidStoreName = validNameFixture; // when & then - assertThatCode(() -> new Store(null, null, lengthValidStoreName, validAddressFixture, null, - validMinOrderPriceFixture, - validStartTimeFixture, validEndTimeFixture)) + assertThatCode( + () -> new Store(createVendor(), createStoreCategory(), lengthValidStoreName, validAddressFixture, + null, + validMinOrderPriceFixture, + validStartTimeFixture, validEndTimeFixture)) .doesNotThrowAnyException(); } @@ -232,9 +254,11 @@ void lessThanMinLengthName() { String lessThanMinLengthName = "헤"; // when & then - assertThatThrownBy(() -> new Store(null, null, lessThanMinLengthName, validAddressFixture, null, - validMinOrderPriceFixture, - validStartTimeFixture, validEndTimeFixture)) + assertThatThrownBy( + () -> new Store(createVendor(), createStoreCategory(), lessThanMinLengthName, validAddressFixture, + null, + validMinOrderPriceFixture, + validStartTimeFixture, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_NAME_RANGE.getMessage()); } @@ -247,7 +271,8 @@ void greaterThanMaxLengthName() { // when & then assertThatThrownBy( - () -> new Store(null, null, greaterThanMaxLengthName, validAddressFixture, null, + () -> new Store(createVendor(), createStoreCategory(), greaterThanMaxLengthName, + validAddressFixture, null, validMinOrderPriceFixture, validStartTimeFixture, validEndTimeFixture)) .isInstanceOf(StoreException.class) @@ -267,7 +292,8 @@ void onlySongPa() { String validAddress = "송파"; // when & then assertThatCode( - () -> new Store(null, null, validNameFixture, validAddress, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddress, null, + validMinOrderPriceFixture, validStartTimeFixture, validEndTimeFixture)) .doesNotThrowAnyException(); } @@ -279,7 +305,8 @@ void notSongPa() { String validAddress = "강남"; // when & then assertThatThrownBy( - () -> new Store(null, null, validNameFixture, validAddress, null, validMinOrderPriceFixture, + () -> new Store(createVendor(), createStoreCategory(), validNameFixture, validAddress, null, + validMinOrderPriceFixture, validStartTimeFixture, validEndTimeFixture)) .isInstanceOf(StoreException.class) .hasMessage(INVALID_ADDRESS.getMessage()); @@ -289,4 +316,17 @@ void notSongPa() { } + private Vendor createVendor() { + PayAccount payAccount = new TestPayAccount(1L); + PasswordEncoder passwordEncoder = new NoOpPasswordEncoder(); + + return new Vendor("vendor", + "validEmail@validEmail.com", + "validPassword", "010-0000-0000", payAccount, passwordEncoder); + } + + private StoreCategory createStoreCategory() { + return new StoreCategory("양식"); + } + } \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/store/service/StoreMenuRegistrationServiceTest.java b/src/test/java/camp/woowak/lab/store/service/StoreMenuRegistrationServiceTest.java new file mode 100644 index 00000000..c46fd33b --- /dev/null +++ b/src/test/java/camp/woowak/lab/store/service/StoreMenuRegistrationServiceTest.java @@ -0,0 +1,122 @@ +package camp.woowak.lab.store.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import camp.woowak.lab.menu.domain.MenuCategory; +import camp.woowak.lab.menu.repository.MenuCategoryRepository; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.TestPayAccount; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.domain.StoreAddress; +import camp.woowak.lab.store.domain.StoreCategory; +import camp.woowak.lab.store.exception.NotFoundStoreException; +import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.store.service.dto.StoreMenuRegistrationRequest; +import camp.woowak.lab.vendor.domain.Vendor; +import camp.woowak.lab.web.authentication.NoOpPasswordEncoder; +import camp.woowak.lab.web.authentication.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class StoreMenuRegistrationServiceTest { + + @Mock + StoreRepository storeRepository; + + @Mock + MenuRepository menuRepository; + + @Mock + MenuCategoryRepository menuCategoryRepository; + + @InjectMocks + StoreMenuRegistrationService storeMenuRegistrationService; + + Vendor owner = createVendor(); + Store storeFixture = createValidStore(owner); + + MenuCategory menuCategoryFixture = createValidMenuCategory(); + + @Test + @DisplayName("[Success] 메뉴 등록 성공") + void storeMenuRegistrationSuccess() { + // given + Long storeId = 1L; + + List menuItems = List.of( + new StoreMenuRegistrationRequest.MenuLineItem("메뉴1", "image1.jpg", "카테고리1", 10000) + ); + StoreMenuRegistrationRequest request = new StoreMenuRegistrationRequest(storeId, menuItems); + + when(storeRepository.findById(storeId)).thenReturn(Optional.of(storeFixture)); + when(menuCategoryRepository.findByStoreIdAndName(storeFixture.getId(), "카테고리1")).thenReturn( + Optional.of(menuCategoryFixture)); + + // when + storeMenuRegistrationService.storeMenuRegistration(owner, request); + + // then + verify(storeRepository).findById(storeId); + verify(menuCategoryRepository).findByStoreIdAndName(storeFixture.getId(), "카테고리1"); + verify(menuRepository).saveAll(anyList()); + } + + @Test + @DisplayName("[Exception] 존재하지 않는 가게") + void storeMenuRegistrationStoreNotFound() { + // given + Long storeId = 1L; + + List menuItems = List.of( + new StoreMenuRegistrationRequest.MenuLineItem("메뉴1", "image1.jpg", "카테고리1", 10000) + ); + StoreMenuRegistrationRequest request = new StoreMenuRegistrationRequest(storeId, menuItems); + + when(storeRepository.findById(storeId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> storeMenuRegistrationService.storeMenuRegistration(owner, request)) + .isInstanceOf(NotFoundStoreException.class); + } + + private Store createValidStore(Vendor owner) { + LocalDateTime validStartDateFixture = LocalDateTime.of(2020, 1, 1, 1, 1); + LocalDateTime validEndDateFixture = LocalDateTime.of(2020, 1, 1, 2, 1); + String validNameFixture = "3K1K 가게"; + String validAddressFixture = StoreAddress.DEFAULT_DISTRICT; + String validPhoneNumberFixture = "02-1234-5678"; + Integer validMinOrderPriceFixture = 5000; + + return new Store(owner, createStoreCategory(), validNameFixture, validAddressFixture, + validPhoneNumberFixture, + validMinOrderPriceFixture, + validStartDateFixture, validEndDateFixture); + } + + private MenuCategory createValidMenuCategory() { + return new MenuCategory(storeFixture, "1234567890"); + } + + private Vendor createVendor() { + PayAccount payAccount = new TestPayAccount(1L); + PasswordEncoder passwordEncoder = new NoOpPasswordEncoder(); + return new Vendor("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, + passwordEncoder); + } + + private StoreCategory createStoreCategory() { + return new StoreCategory("양식"); + } +} \ No newline at end of file