diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index a491a5f0e..1474cd71c 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -30,6 +30,14 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x backend/bang-ggood/gradlew + - name: Write application.yml + env: + APPLICATION_YML: ${{ secrets.APPLICATION_YML }} + APPLICATION_TEST_YML: ${{ secrets.APPLICATION_TEST_YML }} + run: | + echo "${APPLICATION_YML}" > src/main/resources/application.yml + echo "${APPLICATION_TEST_YML}" > src/test/resources/application-test.yml + - name: Build with Gradle run: ./gradlew clean build working-directory: backend/bang-ggood diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index f04c9ad9f..7b37e809d 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -31,6 +31,14 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + + - name: Write application.yml + env: + APPLICATION_YML: ${{ secrets.APPLICATION_YML }} + APPLICATION_TEST_YML: ${{ secrets.APPLICATION_TEST_YML }} + run: | + echo "${APPLICATION_YML}" > src/main/resources/application.yml + echo "${APPLICATION_TEST_YML}" > src/test/resources/application-test.yml - name: Build with Gradle run: ./gradlew clean build diff --git a/backend/bang-ggood/.gitignore b/backend/bang-ggood/.gitignore index 06da7f656..cb8bf25be 100644 --- a/backend/bang-ggood/.gitignore +++ b/backend/bang-ggood/.gitignore @@ -19,6 +19,7 @@ bin/ ### IntelliJ IDEA ### src/main/resources/application.yml +src/test/resources/application-test.yml .idea *.iws *.iml diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/controller/CategoryController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/controller/CategoryController.java index 91800937d..bfc0669b2 100644 --- a/backend/bang-ggood/src/main/java/com/bang_ggood/category/controller/CategoryController.java +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/controller/CategoryController.java @@ -1,9 +1,12 @@ package com.bang_ggood.category.controller; import com.bang_ggood.category.dto.CategoriesReadResponse; +import com.bang_ggood.category.dto.CategoryPriorityCreateRequest; import com.bang_ggood.category.service.CategoryService; 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.RestController; @RestController @@ -15,6 +18,13 @@ public CategoryController(CategoryService categoryService) { this.categoryService = categoryService; } + @PostMapping("/categories/priority") + public ResponseEntity createCategoriesPriority(@RequestBody CategoryPriorityCreateRequest request) { + // TODO: List 요소 null check 필요 + categoryService.createCategoriesPriority(request); + return ResponseEntity.noContent().build(); + } + @GetMapping("/categories") public ResponseEntity readCategories() { return ResponseEntity.ok(categoryService.readCategories()); diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/Category.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/Category.java index 91f278674..14f547670 100644 --- a/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/Category.java +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/Category.java @@ -1,5 +1,7 @@ package com.bang_ggood.category.domain; +import java.util.Arrays; + public enum Category { CLEAN(1, "청결"), @@ -18,6 +20,11 @@ public enum Category { this.description = description; } + public static boolean contains(int id) { + return Arrays.stream(values()) + .anyMatch(category -> category.id == id); + } + public int getId() { return id; } diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/CategoryPriority.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/CategoryPriority.java new file mode 100644 index 000000000..cdd36c60f --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/domain/CategoryPriority.java @@ -0,0 +1,60 @@ +package com.bang_ggood.category.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.user.domain.User; +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.ManyToOne; +import java.util.Objects; + +@Entity +public class CategoryPriority extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Integer categoryId; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + protected CategoryPriority() { + } + + public CategoryPriority(Integer categoryId, User user) { + this.categoryId = categoryId; + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CategoryPriority that = (CategoryPriority) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "CategoryPriority{" + + "id=" + id + + ", categoryId=" + categoryId + + ", user=" + user + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/dto/CategoryPriorityCreateRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/dto/CategoryPriorityCreateRequest.java new file mode 100644 index 000000000..543324b60 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/dto/CategoryPriorityCreateRequest.java @@ -0,0 +1,6 @@ +package com.bang_ggood.category.dto; + +import java.util.List; + +public record CategoryPriorityCreateRequest(List categoryIds) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/repository/CategoryPriorityRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/repository/CategoryPriorityRepository.java new file mode 100644 index 000000000..d37dda882 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/repository/CategoryPriorityRepository.java @@ -0,0 +1,7 @@ +package com.bang_ggood.category.repository; + +import com.bang_ggood.category.domain.CategoryPriority; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CategoryPriorityRepository extends JpaRepository { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/category/service/CategoryService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/category/service/CategoryService.java index 5bcd37c85..c588d7bff 100644 --- a/backend/bang-ggood/src/main/java/com/bang_ggood/category/service/CategoryService.java +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/category/service/CategoryService.java @@ -1,15 +1,71 @@ package com.bang_ggood.category.service; import com.bang_ggood.category.domain.Category; +import com.bang_ggood.category.domain.CategoryPriority; import com.bang_ggood.category.dto.CategoriesReadResponse; +import com.bang_ggood.category.dto.CategoryPriorityCreateRequest; import com.bang_ggood.category.dto.CategoryReadResponse; +import com.bang_ggood.category.repository.CategoryPriorityRepository; +import com.bang_ggood.exception.BangggoodException; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Arrays; import java.util.List; +import java.util.Set; + +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_DUPLICATED; +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_NOT_FOUND; +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_PRIORITY_INVALID_COUNT; @Service public class CategoryService { + private static final int MAX_CATEGORY_PRIORITY_COUNT = 3; + + private final CategoryPriorityRepository categoryPriorityRepository; + private final UserRepository userRepository; + + public CategoryService(CategoryPriorityRepository categoryPriorityRepository, UserRepository userRepository) { + this.categoryPriorityRepository = categoryPriorityRepository; + this.userRepository = userRepository; + } + + @Transactional + public void createCategoriesPriority(CategoryPriorityCreateRequest request) { + validateDuplication(request); + validateCategoryCount(request); + validateCategoryId(request); + + User user = userRepository.getUserById(1L); + List categoryPriorities = request.categoryIds().stream() + .map(id -> new CategoryPriority(id, user)) + .toList(); + + categoryPriorityRepository.saveAll(categoryPriorities); + } + + private void validateDuplication(CategoryPriorityCreateRequest request) { + if (request.categoryIds().size() != Set.copyOf(request.categoryIds()).size()) { + throw new BangggoodException(CATEGORY_DUPLICATED); + } + } + + private void validateCategoryCount(CategoryPriorityCreateRequest request) { + if (request.categoryIds().size() > MAX_CATEGORY_PRIORITY_COUNT) { + throw new BangggoodException(CATEGORY_PRIORITY_INVALID_COUNT); + } + } + + private void validateCategoryId(CategoryPriorityCreateRequest request) { + for (Integer id : request.categoryIds()) { + if (!Category.contains(id)) { + throw new BangggoodException(CATEGORY_NOT_FOUND); + } + } + } + public CategoriesReadResponse readCategories() { List categoryReadResponses = Arrays.stream(Category.values()) .map(CategoryReadResponse::from) diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/exception/ExceptionCode.java b/backend/bang-ggood/src/main/java/com/bang_ggood/exception/ExceptionCode.java index 51ddfe69b..6caa1dcb0 100644 --- a/backend/bang-ggood/src/main/java/com/bang_ggood/exception/ExceptionCode.java +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/exception/ExceptionCode.java @@ -4,7 +4,12 @@ public enum ExceptionCode { - INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 인자입니다."); + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "잘못된 인자입니다."), + USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "유저가 존재하지 않습니다."), + CATEGORY_PRIORITY_INVALID_COUNT(HttpStatus.BAD_REQUEST, "카테고리 개수가 유효하지 않습니다."), + CATEGORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "카테코리가 존재하지 않습니다."), + CATEGORY_DUPLICATED(HttpStatus.BAD_REQUEST, "중복된 카테고리가 존재합니다."), + ; private final HttpStatus httpStatus; private final String message; diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java new file mode 100644 index 000000000..a5796317c --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java @@ -0,0 +1,54 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Objects; + +@Table(name = "users") +@Entity +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + protected User() { + } + + public User(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java new file mode 100644 index 000000000..65cadda49 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.bang_ggood.user.repository; + +import com.bang_ggood.exception.BangggoodException; +import com.bang_ggood.exception.ExceptionCode; +import com.bang_ggood.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + default User getUserById(Long id) { + return findById(id).orElseThrow(() -> new BangggoodException(ExceptionCode.USER_NOT_FOUND)); + } +} diff --git a/backend/bang-ggood/src/main/resources/data.sql b/backend/bang-ggood/src/main/resources/data.sql new file mode 100644 index 000000000..31aaed9e4 --- /dev/null +++ b/backend/bang-ggood/src/main/resources/data.sql @@ -0,0 +1,2 @@ +INSERT INTO users(id, name, created_at, modified_at) +VALUES (1, '방방이', '2024-07-22 07:56:42', '2024-07-22 07:56:42'); diff --git a/backend/bang-ggood/src/main/resources/schema.sql b/backend/bang-ggood/src/main/resources/schema.sql new file mode 100644 index 000000000..a318875f2 --- /dev/null +++ b/backend/bang-ggood/src/main/resources/schema.sql @@ -0,0 +1,19 @@ +CREATE TABLE users +( + id bigint generated by default as identity, + name varchar(255) not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + primary key (id) +); + +CREATE TABLE category_priority +( + id bigint generated by default as identity, + category_id tinyint not null, + user_id bigint not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + primary key (id), + foreign key (user_id) references users +); diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java b/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java new file mode 100644 index 000000000..903186620 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java @@ -0,0 +1,11 @@ +package com.bang_ggood; + + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public abstract class IntegrationTestSupport { +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java index a484db970..f2b231d7c 100644 --- a/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java @@ -4,20 +4,16 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Table; 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.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.annotation.DirtiesContext; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -class JpaAuditingTest { +class JpaAuditingTest extends IntegrationTestSupport{ @Autowired private TestRepository testRepository; @@ -56,6 +52,7 @@ void jpaAuditing_modifyEntity() { ); } + @Table(name = "test_entity") @Entity static class TestEntity extends BaseEntity { diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/category/service/CategoryServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/category/service/CategoryServiceTest.java index c4155d48b..5ed9a9f46 100644 --- a/backend/bang-ggood/src/test/java/com/bang_ggood/category/service/CategoryServiceTest.java +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/category/service/CategoryServiceTest.java @@ -1,20 +1,96 @@ package com.bang_ggood.category.service; -import com.bang_ggood.category.domain.Category; +import com.bang_ggood.IntegrationTestSupport; import com.bang_ggood.category.dto.CategoriesReadResponse; -import org.assertj.core.api.Assertions; +import com.bang_ggood.category.dto.CategoryPriorityCreateRequest; +import com.bang_ggood.exception.BangggoodException; 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.boot.test.context.SpringBootTest.WebEnvironment; +import java.util.List; -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class CategoryServiceTest { +import static com.bang_ggood.category.domain.Category.AMENITY; +import static com.bang_ggood.category.domain.Category.CLEAN; +import static com.bang_ggood.category.domain.Category.ECONOMIC; +import static com.bang_ggood.category.domain.Category.SECURITY; +import static com.bang_ggood.category.domain.Category.values; +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_DUPLICATED; +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_NOT_FOUND; +import static com.bang_ggood.exception.ExceptionCode.CATEGORY_PRIORITY_INVALID_COUNT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CategoryServiceTest extends IntegrationTestSupport { @Autowired private CategoryService categoryService; + @DisplayName("카테고리 우선순위 생성 성공") + @Test + void createCategoriesPriority() { + // given + CategoryPriorityCreateRequest request = new CategoryPriorityCreateRequest(List.of( + CLEAN.getId(), + AMENITY.getId(), + ECONOMIC.getId() + )); + + // when & then + assertThatCode(() -> categoryService.createCategoriesPriority(request)) + .doesNotThrowAnyException(); + // TODO: 추후 우선순위 조회 API로 예외 검증 + } + + @DisplayName("카테고리 우선순위 생성 실패 : 카테고리 개수가 유효하지 않을 때") + @Test + void createCategoriesPriority_invalidCount_exception() { + // given + CategoryPriorityCreateRequest request = new CategoryPriorityCreateRequest(List.of( + CLEAN.getId(), + AMENITY.getId(), + ECONOMIC.getId(), + SECURITY.getId() + )); + + // when & then + assertThatThrownBy(() -> categoryService.createCategoriesPriority(request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(CATEGORY_PRIORITY_INVALID_COUNT.getMessage()); + } + + @DisplayName("카테고리 우선순위 생성 실패 : 카테고리가 존재하지 않을 때") + @Test + void createCategoriesPriority_notFound_exception() { + // given + CategoryPriorityCreateRequest request = new CategoryPriorityCreateRequest(List.of( + CLEAN.getId(), + AMENITY.getId(), + 0 + )); + + // when & then + assertThatThrownBy(() -> categoryService.createCategoriesPriority(request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(CATEGORY_NOT_FOUND.getMessage()); + } + + @DisplayName("카테고리 우선순위 생성 실패 : 중복된 카테고리가 존재할 때") + @Test + void createCategoriesPriority_duplicated_exception() { + // given + CategoryPriorityCreateRequest request = new CategoryPriorityCreateRequest(List.of( + CLEAN.getId(), + AMENITY.getId(), + AMENITY.getId() + )); + + // when & then + assertThatThrownBy(() -> categoryService.createCategoriesPriority(request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(CATEGORY_DUPLICATED.getMessage()); + } + @DisplayName("카테고리 조회 성공") @Test void readCategories() { @@ -22,7 +98,7 @@ void readCategories() { CategoriesReadResponse categoriesReadResponse = categoryService.readCategories(); // then - Assertions.assertThat(categoriesReadResponse.categories()) - .hasSize(Category.values().length); + assertThat(categoriesReadResponse.categories()) + .hasSize(values().length); } } diff --git a/backend/bang-ggood/src/test/resources/data-test.sql b/backend/bang-ggood/src/test/resources/data-test.sql new file mode 100644 index 000000000..31aaed9e4 --- /dev/null +++ b/backend/bang-ggood/src/test/resources/data-test.sql @@ -0,0 +1,2 @@ +INSERT INTO users(id, name, created_at, modified_at) +VALUES (1, '방방이', '2024-07-22 07:56:42', '2024-07-22 07:56:42'); diff --git a/backend/bang-ggood/src/test/resources/schema-test.sql b/backend/bang-ggood/src/test/resources/schema-test.sql new file mode 100644 index 000000000..a4583caf7 --- /dev/null +++ b/backend/bang-ggood/src/test/resources/schema-test.sql @@ -0,0 +1,30 @@ +CREATE TABLE if not exists users +( + id bigint generated by default as identity, + name varchar(255) not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + primary key (id) +); + +CREATE TABLE if not exists category_priority +( + id bigint generated by default as identity, + category_id tinyint not null, + user_id bigint not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + primary key (id), + foreign key (user_id) references users +); + + +CREATE TABLE test_entity +( + id bigint generated by default as identity, + name varchar(255) not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + primary key (id) +); +