Skip to content

Commit

Permalink
Merge pull request #24 from taco-official/KL-87/카테고리-소분류-목록조회-api-구현
Browse files Browse the repository at this point in the history
feat(Kl-87) : add Subcategory Api
min3m authored Jul 30, 2024
2 parents 284bb24 + c5db3e4 commit 5e3d529
Showing 13 changed files with 446 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package taco.klkl.domain.subcategory.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import taco.klkl.domain.subcategory.dto.response.SubcategoryResponseDto;
import taco.klkl.domain.subcategory.sevice.SubcategoryService;

@Slf4j
@Tag(name = "7. 서브카테고리", description = "서브카테고리 관련 API")
@RestController
@RequestMapping("/v1/categories/{id}/subcategories")
@RequiredArgsConstructor
public class SubcategoryController {
private final SubcategoryService subcategoryService;

@GetMapping
public ResponseEntity<List<SubcategoryResponseDto>> getSubcategory(@PathVariable Long id) {
List<SubcategoryResponseDto> subcategories = subcategoryService.getSubcategories(id);
return ResponseEntity.ok(subcategories);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package taco.klkl.domain.subcategory.convert;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import taco.klkl.domain.subcategory.domain.SubcategoryName;

@Converter(autoApply = true)
public class SubcategoryNameConverter implements AttributeConverter<SubcategoryName, String> {

@Override
public String convertToDatabaseColumn(SubcategoryName subcategoryName) {
return subcategoryName.getName();
}

@Override
public SubcategoryName convertToEntityAttribute(String name) {
return SubcategoryName.getByName(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package taco.klkl.domain.subcategory.dao;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import taco.klkl.domain.subcategory.domain.Subcategory;

@Repository
public interface SubcategoryRepository extends JpaRepository<Subcategory, Long> {
List<Subcategory> findAllByCategoryId(Long id);
}
39 changes: 39 additions & 0 deletions src/main/java/taco/klkl/domain/subcategory/domain/Subcategory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package taco.klkl.domain.subcategory.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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.Getter;
import lombok.NoArgsConstructor;
import taco.klkl.domain.category.domain.Category;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Subcategory {
@Id
@Column(name = "subcategory_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@ManyToOne
@JoinColumn(name = "category_id")
private Category category;

@Column(name = "name")
private SubcategoryName name;

private Subcategory(Category category, SubcategoryName name) {
this.category = category;
this.name = name;
}

public static Subcategory of(Category category, SubcategoryName name) {
return new Subcategory(category, name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package taco.klkl.domain.subcategory.domain;

import java.util.Arrays;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum SubcategoryName {
//식품
INSTANT_FOOD("라면 및 즉석식품"),
SNACK("스낵 및 과자"),
SAUCE("조미료 및 소스"),
HEALTH_FOOD("보충제 및 건강식품"),
BEVERAGE("음료 및 차"),
DRINKS("주류"),
//의류
TOP("상의"),
BOTTOM("하의"),
OUTER("아우터"),
DRESS("원피스"),
SHOES("신발"),
ACCESSORY("액세사리"),
JEWELRY("쥬얼리"),
//잡화
DRUG("일반의약품"),
KITCHEN_SUPPLIES("주방잡화"),
BATHROOM_SUPPLIES("욕실잡화"),
STATIONERY("문구 및 완구"),
//화장품
SKIN_CARE("스킨케어"),
MAKEUP("메이크업"),
HAIR_CARE("헤어케어"),
BODY_CARE("바디케어"),
HYGIENE_PRODUCT("위생용품"),
//None
NONE(""),
;

private final String name;

public static SubcategoryName getByName(String name) {
return Arrays.stream(values())
.filter(subcategoryName -> subcategoryName.getName().equals(name))
.findFirst()
.orElse(NONE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package taco.klkl.domain.subcategory.dto.response;

import taco.klkl.domain.subcategory.domain.SubcategoryName;

public record SubcategoryResponseDto(
Long id,
String name
) {
public static SubcategoryResponseDto of(Long id, SubcategoryName subcategoryName) {
return new SubcategoryResponseDto(id, subcategoryName.getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package taco.klkl.domain.subcategory.exception;

import taco.klkl.global.error.exception.CustomException;
import taco.klkl.global.error.exception.ErrorCode;

public class CategoryNotFoundException extends CustomException {
public CategoryNotFoundException() {
super(ErrorCode.CATEGORY_ID_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package taco.klkl.domain.subcategory.sevice;

import java.util.List;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import taco.klkl.domain.subcategory.dao.SubcategoryRepository;
import taco.klkl.domain.subcategory.domain.Subcategory;
import taco.klkl.domain.subcategory.dto.response.SubcategoryResponseDto;
import taco.klkl.domain.subcategory.exception.CategoryNotFoundException;

@Slf4j
@Service
@RequiredArgsConstructor
public class SubcategoryService {
private final SubcategoryRepository subcategoryRepository;

public List<SubcategoryResponseDto> getSubcategories(Long id) {
List<Subcategory> subcategories = subcategoryRepository.findAllByCategoryId(id);
if (subcategories.isEmpty()) {
throw new CategoryNotFoundException();
}
return subcategories.stream()
.map(subcategory -> SubcategoryResponseDto.of(subcategory.getId(), subcategory.getName()))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ public enum ErrorCode {
// Region

// Category

CATEGORY_ID_NOT_FOUND(HttpStatus.NOT_FOUND, "C060", "존재하지 않는 카테고리 ID 입니다."),
// Filter

// Notification
31 changes: 31 additions & 0 deletions src/main/resources/database/data.sql
Original file line number Diff line number Diff line change
@@ -21,6 +21,37 @@ VALUES (300, '식품'),
(302, '잡화'),
(303, '화장품');

INSERT INTO Subcategory (subcategory_id, name, category_id)
VALUES
-- 식품 카테고리 (category_id: 300)
(310, '라면 및 즉석식품', 300),
(311, '스낵 및 과자', 300),
(312, '조미료 및 소스', 300),
(313, '보충제 및 건강식품', 300),
(314, '음료 및 차', 300),
(315, '주류', 300),

-- 의류 카테고리 (category_id: 301)
(320, '상의', 301),
(321, '하의', 301),
(322, '아우터', 301),
(323, '원피스', 301),
(324, '신발', 301),
(325, '액세사리', 301),
(326, '쥬얼리', 301),

-- 잡화 카테고리 (category_id: 302)
(330, '일반의약품', 302),
(331, '주방잡화', 302),
(332, '욕실잡화', 302),
(333, '문구 및 완구', 302),

-- 화장품 카테고리 (category_id: 303)
(340, '스킨케어', 303),
(341, '메이크업', 303),
(342, '헤어케어', 303),
(343, '바디케어', 303),
(344, '위생용품', 303);
/* Filter */

/* Notification */
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package taco.klkl.domain.subcategory.controller;

import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import taco.klkl.domain.subcategory.dto.response.SubcategoryResponseDto;
import taco.klkl.domain.subcategory.exception.CategoryNotFoundException;
import taco.klkl.domain.subcategory.sevice.SubcategoryService;
import taco.klkl.global.error.exception.ErrorCode;

@WebMvcTest(SubcategoryController.class)
public class SubcategoryControllerTest {
@Autowired
private MockMvc mockMvc;

@MockBean
private SubcategoryService subcategoryService;

@Test
@DisplayName("valid id가 들어올 경우 GlobalResponse로 Wrapping되어 나오는지 Test")
public void testGetSubcategoryWithValidId() throws Exception {
// given
List<SubcategoryResponseDto> subcategoryResponseDto = Arrays.asList(
new SubcategoryResponseDto(1L, "즉석식품"),
new SubcategoryResponseDto(2L, "스낵")
);

// when
when(subcategoryService.getSubcategories(anyLong())).thenReturn(subcategoryResponseDto);

// then
mockMvc.perform(get("/v1/categories/1/subcategories")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isSuccess", is(true)))
.andExpect(jsonPath("$.code", is("C000")))
.andExpect(jsonPath("$.data", hasSize(2)))
.andExpect(jsonPath("$.data[0].id", is(1)))
.andExpect(jsonPath("$.data[0].name", is("즉석식품")))
.andExpect(jsonPath("$.data[1].id", is(2)))
.andExpect(jsonPath("$.data[1].name", is("스낵")))
.andExpect(jsonPath("$.timestamp", notNullValue()));

verify(subcategoryService, times(1)).getSubcategories(anyLong());
}

@Test
@DisplayName("invalid한 id가 들어올 경우 GlobalException으로 Wrapping되어 나오는지 Test")
public void testGetSubcategoryWithInvalidId() throws Exception {
// given
when(subcategoryService.getSubcategories(anyLong())).thenThrow(new CategoryNotFoundException());

// when & then
mockMvc.perform(get("/v1/categories/999/subcategories")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.isSuccess", is(false)))
.andExpect(jsonPath("$.code", is(ErrorCode.CATEGORY_ID_NOT_FOUND.getCode())))
.andExpect(jsonPath("$.data.message", is(ErrorCode.CATEGORY_ID_NOT_FOUND.getMessage())))
.andExpect(jsonPath("$.timestamp", notNullValue()));

verify(subcategoryService, times(1)).getSubcategories(anyLong());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package taco.klkl.domain.subcategory.integration;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import jakarta.transaction.Transactional;
import taco.klkl.domain.subcategory.dto.response.SubcategoryResponseDto;
import taco.klkl.domain.subcategory.sevice.SubcategoryService;
import taco.klkl.global.error.exception.ErrorCode;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class SubcategoryIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private SubcategoryService subcategoryService;

@Test
@DisplayName("valid한 id값이 들어왔을 때 반환값이 제대로 전달되는지 테스트")
public void testGetCategoriesWithValidId() throws Exception {
//given
List<SubcategoryResponseDto> subcategoryResponseDto = subcategoryService.getSubcategories(300L);

//when, then
mockMvc.perform(get("/v1/categories/300/subcategories")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(subcategoryResponseDto.size())))
.andExpect(jsonPath("$.isSuccess", is(true)))
.andExpect(jsonPath("$.code", is("C000")));
}

@Test
@DisplayName("invalid한 id값이 들어왔을 때 오류가 전달되는지 테스트")
public void testGetCategoriesWithInvalidId() throws Exception {
//given

//when, then
mockMvc.perform(get("/v1/categories/999/subcategories")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.isSuccess", is(false)))
.andExpect(jsonPath("$.code", is(ErrorCode.CATEGORY_ID_NOT_FOUND.getCode())))
.andExpect(jsonPath("$.data.message", is(ErrorCode.CATEGORY_ID_NOT_FOUND.getMessage())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package taco.klkl.domain.subcategory.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

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 jakarta.transaction.Transactional;
import taco.klkl.domain.category.domain.Category;
import taco.klkl.domain.subcategory.dao.SubcategoryRepository;
import taco.klkl.domain.subcategory.domain.Subcategory;
import taco.klkl.domain.subcategory.domain.SubcategoryName;
import taco.klkl.domain.subcategory.dto.response.SubcategoryResponseDto;
import taco.klkl.domain.subcategory.exception.CategoryNotFoundException;
import taco.klkl.domain.subcategory.sevice.SubcategoryService;

@ExtendWith(MockitoExtension.class)
@Transactional
public class SubcategoryServiceTest {

@Mock
private SubcategoryRepository subcategoryRepository;

@InjectMocks
private SubcategoryService subcategoryService;

@Test
@DisplayName("Valid한 카테고리ID 입력시 해당하는 서브카테고리를 반환하는지 테스트")
void testGetSubcategoriesWithValidCategoryId() {
//given
Category category = Category.of("Category1");
Subcategory subcategory1 = Subcategory.of(category, SubcategoryName.DRESS);
Subcategory subcategory2 = Subcategory.of(category, SubcategoryName.HAIR_CARE);
List<Subcategory> subcategories = Arrays.asList(subcategory1, subcategory2);

//when
when(subcategoryRepository.findAllByCategoryId(category.getId())).thenReturn(subcategories);
List<SubcategoryResponseDto> response = subcategoryService.getSubcategories(category.getId());

//then
assertNotNull(response);
assertEquals(2, response.size());
assertEquals(SubcategoryName.DRESS.getName(), response.get(0).name());
assertEquals(SubcategoryName.HAIR_CARE.getName(), response.get(1).name());

verify(subcategoryRepository, times(1)).findAllByCategoryId(category.getId());
}

@Test
@DisplayName("Invalid한 카테고리 ID 입력시 CategoryNotFoundException을 반환하는지 테스트")
void testGetSubcategoriesWithInvalidCategoryId() {
//given
Long categoryId = 1L;

//when
when(subcategoryRepository.findAllByCategoryId(categoryId)).thenReturn(Collections.emptyList());

//then
assertThrows(CategoryNotFoundException.class, () -> {
subcategoryService.getSubcategories(categoryId);
});

verify(subcategoryRepository, times(1)).findAllByCategoryId(categoryId);
}
}

0 comments on commit 5e3d529

Please sign in to comment.