Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.starchive.springapp.category.controller;

import com.starchive.springapp.category.dto.CategoryCreateRequest;
import com.starchive.springapp.category.dto.CategoryDto;
import com.starchive.springapp.category.dto.CategoryUpdateRequest;
import com.starchive.springapp.category.dto.CategoryUpdateResponse;
import com.starchive.springapp.category.service.CategoryService;
import com.starchive.springapp.global.dto.ResponseDto;
import com.starchive.springapp.hashtag.dto.HashTagDto;
import com.starchive.springapp.hashtag.service.HashTagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Null;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -36,4 +44,28 @@ public ResponseEntity<ResponseDto<List<HashTagDto>>> showHashTags(@PathVariable(
ResponseDto<List<HashTagDto>> listResponseDto = new ResponseDto<>(categories);
return ResponseEntity.ok(listResponseDto);
}

@PostMapping("/categories")
@Operation(summary = "카테고리 생성")
public ResponseEntity<Null> createCategory(@Valid CategoryCreateRequest categoryCreateRequest) {
categoryService.create(categoryCreateRequest);

return ResponseEntity.noContent().build();
}

@PutMapping("/categories")
@Operation(summary = "카테고리 수정")
public ResponseEntity<ResponseDto<CategoryUpdateResponse>> updateCategory(
@Valid CategoryUpdateRequest categoryUpdateRequest) {
CategoryUpdateResponse updateResponse = categoryService.update(categoryUpdateRequest);

return ResponseEntity.ok(new ResponseDto<>(updateResponse));
}

@DeleteMapping("/categories/{categoryId}")
@Operation(summary = "카테고리 삭제")
public ResponseEntity<Null> deleteCategory(@PathVariable("categoryId") Long categoryId) {
categoryService.delete(categoryId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static jakarta.persistence.FetchType.LAZY;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -33,7 +34,7 @@ public class Category {
@Column(length = 100)
String name;

@OneToMany(mappedBy = "parent", fetch = LAZY)
@OneToMany(mappedBy = "parent", fetch = LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Category> children = new ArrayList<>();

public Category(String name, Category parent) {
Expand All @@ -44,9 +45,15 @@ public Category(String name, Category parent) {
}
}

private void changeParent(Category parent) {
public void changeName(String name) {
this.name = name;
}

public void changeParent(Category parent) {
this.parent = parent;
parent.getChildren().add(this);
if (parent != null) {
parent.getChildren().add(this);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.starchive.springapp.category.dto;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryCreateRequest {
@Size(max = 100)
@NotNull
private String name;

@Nullable
private Long parentId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.starchive.springapp.category.dto;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryUpdateRequest {
@NotNull
private Long categoryId;

@Size(max = 100)
@NotNull
private String name;

@Nullable
private Long parentId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.starchive.springapp.category.dto;

import com.starchive.springapp.category.domain.Category;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryUpdateResponse {
private Long categoryId;

private String name;

private Long parentId;

public static CategoryUpdateResponse from(Category category) {
CategoryUpdateResponse categoryUpdateResponse = new CategoryUpdateResponse();
categoryUpdateResponse.categoryId = category.getId();
categoryUpdateResponse.name = category.getName();
if (category.getParent() == null) {
categoryUpdateResponse.parentId = null;
} else {
categoryUpdateResponse.parentId = category.getParent().getId();
}
return categoryUpdateResponse;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.starchive.springapp.category.exception;

import static com.starchive.springapp.global.ErrorMessage.ALREADY_EXISTS_CATEGORY;

public class CategoryAlreadyExistsException extends RuntimeException {
public CategoryAlreadyExistsException() {
super(ALREADY_EXISTS_CATEGORY);
}

public CategoryAlreadyExistsException(String message) {
super(message);
}

public CategoryAlreadyExistsException(String message, Throwable cause) {
super(message, cause);
}

public CategoryAlreadyExistsException(Throwable cause) {
super(cause);
}

protected CategoryAlreadyExistsException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@

public interface CategoryRepository extends JpaRepository<Category, Long> {
@Query("SELECT c FROM Category c LEFT JOIN FETCH c.children left join fetch c.parent WHERE c.id = :id")
Optional<Category> findByIdWithChildren(@Param("id") Long id);
Optional<Category> findByIdWithParentAndChildren(@Param("id") Long id);

@Query("SELECT DISTINCT c FROM Category c LEFT JOIN FETCH c.children WHERE c.parent IS NULL")
List<Category> findRootCategoriesWithChildren();

Optional<Category> findByName(@Param("name") String name);

@Query("select c from Category c left join fetch c.parent where c.id = :id")
Optional<Category> findByIdWithParent(@Param("id") Long id);

}
Original file line number Diff line number Diff line change
@@ -1,26 +1,126 @@
package com.starchive.springapp.category.service;

import com.starchive.springapp.category.domain.Category;
import com.starchive.springapp.category.dto.CategoryCreateRequest;
import com.starchive.springapp.category.dto.CategoryDto;
import com.starchive.springapp.category.dto.CategoryUpdateRequest;
import com.starchive.springapp.category.dto.CategoryUpdateResponse;
import com.starchive.springapp.category.exception.CategoryAlreadyExistsException;
import com.starchive.springapp.category.exception.CategoryNotFoundException;
import com.starchive.springapp.category.repository.CategoryRepository;
import com.starchive.springapp.post.repository.PostRepository;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;
private final PostRepository postRepository;

public List<CategoryDto> findAll() {
List<Category> rootCateGories = categoryRepository.findRootCategoriesWithChildren();
return rootCateGories.stream().map(CategoryDto::from).toList();
}

public Category findOne(Long id) {
return categoryRepository.findByIdWithChildren(id).orElseThrow(CategoryNotFoundException::new);
return categoryRepository.findByIdWithParentAndChildren(id).orElseThrow(CategoryNotFoundException::new);
}


public void create(CategoryCreateRequest categoryCreateRequest) {
categoryRepository.findByName(categoryCreateRequest.getName()).ifPresent(category -> {
throw new CategoryAlreadyExistsException();
});

if (categoryCreateRequest.getParentId() == null) {
Category category = new Category(categoryCreateRequest.getName(), null);
categoryRepository.save(category);
return;
}

Category parentCategory = categoryRepository.findById(categoryCreateRequest.getParentId())
.orElseThrow(() -> new CategoryNotFoundException("존재하지 않는 부모 카테고리입니다."));

Category category = new Category(categoryCreateRequest.getName(), parentCategory);
categoryRepository.save(category);

}

public CategoryUpdateResponse update(CategoryUpdateRequest categoryUpdateRequest) {
Category category = categoryRepository.findByIdWithParent(categoryUpdateRequest.getCategoryId())
.orElseThrow(CategoryNotFoundException::new);

if (!category.getName().equals(categoryUpdateRequest.getName())) {
categoryRepository.findByName(categoryUpdateRequest.getName()).ifPresent(findOne -> {
throw new CategoryAlreadyExistsException(categoryUpdateRequest.getName() + " 은 이미 존재하는 카테고리 이름입니다.");
});

category.changeName(categoryUpdateRequest.getName());
}

updateParentCategory(categoryUpdateRequest, category);

return CategoryUpdateResponse.from(category);
}

public void delete(Long id) {
if (id == 0) {
throw new RuntimeException("삭제할 수 없는 카테고리입니다.");
}
Category category = categoryRepository.findByIdWithParentAndChildren(id)
.orElseThrow(CategoryNotFoundException::new);

List<Long> categoryIds = new ArrayList<>();
categoryIds.add(category.getId());

if (!category.getChildren().isEmpty()) {
for (Category child : category.getChildren()) {
categoryIds.add(child.getId());
}
}

postRepository.bulkUpdateToNoneCategory(categoryIds);

categoryRepository.deleteById(category.getId());
}

private Category updateParentCategory(CategoryUpdateRequest categoryUpdateRequest, Category category) {
Long newParentId = categoryUpdateRequest.getParentId();
if (category.getParent() != null && newParentId == null) {
category.changeParent(null);
return category;
}

if (category.getParent() != null && newParentId != null) {

Category parentCategory = findParentCategoryById(newParentId);

if (category.getParent().getId() != newParentId) {
category.changeParent(parentCategory);
}

return category;
}

if (category.getParent() == null && newParentId != null) {

Category parentCategory = findParentCategoryById(newParentId);

category.changeParent(parentCategory);

return category;
}

return category;
}

private Category findParentCategoryById(Long parentId) {
return categoryRepository.findById(parentId)
.orElseThrow(() -> new CategoryNotFoundException("존재하지 않는 부모 카테고리입니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class ErrorMessage {
final public static String CATEGORY_NOT_FOUND = "카테고리가 존재하지 않습니다.";
final public static String HASHTAG_NOT_FOUND = "해쉬태그가 존재하지 않습니다.";
final public static String POST_NOT_FOUND = "게시글이 존재하지 않습니다.";
final public static String ALREADY_EXISTS_CATEGORY = "이미 존재하는 카테고리 이름입니다.";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.starchive.springapp.global.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;

@Data
@AllArgsConstructor
public class ErrorResult {
private HttpStatus code;
private String message;
}
Loading
Loading