이번에는 계층형 카테고리를 추가하였다.
예를 들어 현재 내 티스토리 블로그와 같은 모양을 생각하고 만들었다.
고민한 점은 모든 게시글이 카테고리를 가지고 있는 것이 맞을까 라는 생각이었다.
따라서 굳이 카테고리에 넣지 않아도 되도록 만들었다.
중요한 부분위주로 설명하겠다.
Category
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_id")
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Category parent;
public Category(String name, Category parent) {
this.name = name;
this.parent = parent;
}
}
ManyToOne을 사용하여 부모 카테고리와 연결을 시켰다.
또한 부모 카테고리가 삭제되면, 아래 있던 카테고리도 삭제되도록 설정하였다.
CategoryController
@Api(value = "Category Controller", tags = "Category")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/categories")
public class CategoryController {
private final CategoryService categoryService;
@ApiOperation(value = "모든 카테고리 조회", notes = "모든 카테고리를 조회합니다.")
@GetMapping
@ResponseStatus(HttpStatus.OK)
public Response findAllCategories() {
return Response.success(categoryService.findAllCategory());
}
@ApiOperation(value = "카테고리 생성", notes = "카테고리를 생성합니다.")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Response createCategory(@Valid @RequestBody CategoryCreateRequestDto req) {
categoryService.createCategory(req);
return Response.success();
}
@ApiOperation(value = "카테고리 삭제", notes = "카테고리를 삭제합니다.")
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public Response deleteCategory(@ApiParam(value = "카테고리 id", required = true) @PathVariable Long id) {
categoryService.deleteCategory(id);
return Response.success();
}
}
간단하게 조회 생성 삭제를 만들었다.
CategoryService
@Service
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;
@Transactional(readOnly = true)
public List<CategoryResponseDto> findAllCategory() {
List<Category> categories = categoryRepository.findAllOrderByParent();
return CategoryResponseDto.toDtoList(categories);
}
@Transactional
public void createCategory(CategoryCreateRequestDto req) {
Category parent = Optional.ofNullable(req.getParentId())
.map(id -> categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new))
.orElse(null);
categoryRepository.save(new Category(req.getName(), parent));
}
@Transactional
public void deleteCategory(Long id) {
Category category = categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new);
categoryRepository.delete(category);
}
}
카테고리를 만들었으니, 이를 게시글에 적용해 보자.
카테고리가 추가되어서 게시판 필드에 카테고리가 추가되었다.
Board
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
@OnDelete(action = OnDeleteAction.NO_ACTION)
private Category category;
public Board(String title, String content, Member member,Category category, List<Image> images) {
this.title = title;
this.content = content;
this.member = member;
this.likeCount = 0;
this.viewCount = 0;
this.category = category;
this.images = new ArrayList<>();
isReported = false;
addImages(images);
}
카테고리 필드가 추가되었고, 그로 인해 생성자에 카테고리가 추가되었다.
카테고리는 필수값이 아니다.
BoardController
@ApiOperation(value = "게시글 생성", notes = "게시글을 작성합니다.")
@PostMapping("/boards")
@ResponseStatus(HttpStatus.CREATED)
public Response createBoard(@Valid @ModelAttribute BoardCreateRequestDto req,
@RequestParam(value = "category",required = false) Long categoryId){
boardService.createBoard(req, getPrincipal(),categoryId);
return Response.success();
}
@ApiOperation(value = "게시글 목록 조회", notes = "게시글 목록을 조회합니다.")
@GetMapping("/boards/all/{categoryId}")
@ResponseStatus(HttpStatus.OK)
public Response findAllBoardsWithCategory(@ApiParam(value = "카테고리 id", required = true) @PathVariable Long categoryId, @RequestParam(defaultValue = "0") Integer page) {
return Response.success(boardService.findAllBoardsWithCategory(page, categoryId));
}
게시글 생성 시 쿼리 파라미터로 게시글을 지정할 수 있도록 수정하였다.
필수값이 아니므로 required = false로 설정해 두었다.
게시글 목록 조회가 새로 생겼다.
BoardService
@Transactional
public void createBoard(BoardCreateRequestDto req, Member member,Long categoryId){
List<Image> images = req.getImages().stream()
.map(i -> new Image(i.getOriginalFilename()))
.collect(toList());
Category category = getCategory(categoryId);
Board board = new Board(req.getTitle(), req.getContent(), member,category, images);
boardRepository.save(board);
uploadImages(board.getImages(), req.getImages());
}
private Category getCategory(Long categoryId){
if (categoryId==null) return null;
return categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new);
}
@Transactional(readOnly = true)
public BoardFindAllWithPagingResponseDto findAllBoardsWithCategory(Integer page, Long categoryId) {
PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending());
Page<Board> boards = boardRepository.findAllByCategoryId(categoryId,pageRequest);
List<BoardFindAllResponseDto> boardsWithDto = boards.stream().map(BoardFindAllResponseDto::toDto)
.collect(toList());
return BoardFindAllWithPagingResponseDto.toDto(boardsWithDto, new PageInfoDto(boards));
}
사실 게시판 서비스 코드에 중복이 좀 많고, 더 깔끔하게 수정해야 할 필요성을 느꼈다.
일단 지금의 내용을 설명하자면,
기존에 게시판을 생성하는 과정에서 카테고리가 추가되었기 때문에, 카테고리를 생성자에 넣어준다.
다만 카테고리가 존재하는지와 카테고리 분류를 하지 않은 경우를 판단한다.
카테고리별 게시글 조회는 이전에 전체조회에서 단순하게 카테고리에 일치하는 게시글만을 조회하는 역학을 한다.
이제 테스트 코드를 작성해 보자.
CategoryControllerTest
@ExtendWith(MockitoExtension.class)
public class CategoryControllerTest {
@InjectMocks
CategoryController categoryController;
@Mock
CategoryService categoryService;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(categoryController).build();
}
@Test
public void 전체카테고리_조회_테스트() throws Exception {
// given
// when, then
mockMvc.perform(get("/api/categories"))
.andExpect(status().isOk());
verify(categoryService).findAllCategory();
}
@Test
public void 카테고리생성_테스트() throws Exception {
// given
CategoryCreateRequestDto req = new CategoryCreateRequestDto("category1", 1l);
// when, then
mockMvc.perform(
post("/api/categories")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated());
verify(categoryService).createCategory(req);
}
@Test
public void 카테고리삭제_테스트() throws Exception {
// given
Long id = 1L;
// when, then
mockMvc.perform(
delete("/api/categories/{id}", id))
.andExpect(status().isOk());
verify(categoryService).deleteCategory(id);
}
}
CategoryServiceTest
@ExtendWith(MockitoExtension.class)
public class CategoryServiceTest {
@InjectMocks
CategoryService categoryService;
@Mock
CategoryRepository categoryRepository;
@Mock
MemberRepository memberRepository;
@Test
public void 카테고리전체_조회_테스트() {
// given
List<Category> categories = new ArrayList<>();
categories.add(new Category("categoty",null));
given(categoryRepository.findAllOrderByParent()).willReturn(categories);
// when
List<CategoryResponseDto> result = categoryService.findAllCategory();
// then
assertThat(result.size()).isEqualTo(1);
}
@Test
public void 카테고리생성_테스트() {
// given
Category category = new Category("s", null);
CategoryCreateRequestDto req = new CategoryCreateRequestDto("name", 1l);
given(categoryRepository.findById(anyLong())).willReturn(Optional.of(category));
// when
categoryService.createCategory(req);
// then
verify(categoryRepository).save(any());
}
@Test
public void 카테고리삭제_테스트() {
// given
Category category = new Category("s", null);
given(categoryRepository.findById(anyLong())).willReturn(Optional.of(category));
// when
categoryService.deleteCategory(anyLong());
// then
verify(categoryRepository).delete(any());
}
}
보드테스트는 따로 올리지 않았다. 자세한 건 아래 깃허브를 통해 볼 수 있다.
테스트가 잘 통과하는 것을 볼 수 있다.
이제 포스트맨을 사용하여 확인해 보자.
우선 회원 1번으로 로그인하자.
회원 1번의 경우 권한을 ADMIN으로 설정해줘야 한다.
최상위 카테고리를 만들어보자.
이후 자식 카테고리를 추가할 수 있다.
여기서 1번을 삭제하면 전부 삭제되어야 한다.
이제 아무 카테고리를 만든 후에 게시글을 생성할 때 카테고리를 지정하고 조회해 보자.
카테고리 5번에 게시글을 작성해 보자.
잘 조회되는 것을 볼 수 있다.
이제 스웨거에 추가된 모습을 보자.
자세한 코드는 아래 깃허브를 통해 볼 수 있다.
https://github.com/kimtaesoo99/community
'프로젝트 > 커뮤니티' 카테고리의 다른 글
[프로젝트] 커뮤니티 REST API 서버만들기 #10 도메인 테스트 작성 (0) | 2023.02.14 |
---|---|
[프로젝트] 커뮤니티 REST API 서버만들기 #8 어드민 페이지 만들기 (4) | 2023.01.28 |
[프로젝트] 커뮤니티 REST API 서버만들기 #7 Report API 만들기 (0) | 2023.01.27 |
[프로젝트] 커뮤니티 REST API 서버만들기 #6 Comment API만들기 (0) | 2023.01.25 |
[프로젝트] 커뮤니티 REST API 서버만들기 #5 게시판 부가기능 추가 (0) | 2023.01.24 |