카테고리 도메인 리아키텍쳐링

This commit is contained in:
jinia91
2022-03-24 23:54:34 +09:00
parent 47ef05cebf
commit 1f2efe2da3
27 changed files with 401 additions and 356 deletions

View File

@@ -13,14 +13,13 @@ import myblog.blog.article.application.port.response.ArticleResponseForCardBox;
import myblog.blog.article.application.port.response.ArticleResponseForDetail;
import myblog.blog.article.application.port.response.ArticleResponseForEdit;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.dto.*;
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import myblog.blog.member.auth.PrincipalDetails;
import myblog.blog.member.dto.MemberVo;
import myblog.blog.shared.queries.LayoutRenderingQueries;
import org.jsoup.Jsoup;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -33,7 +32,6 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.stream.Collectors;
import static myblog.blog.shared.utils.MarkdownUtils.*;
@@ -45,14 +43,13 @@ public class ArticleController {
private final ArticleQueriesUseCase articleQueriesUseCase;
private final TempArticleUseCase tempArticleUseCase;
private final TagsQueriesUseCase tagsQueriesUseCase;
private final CategoryService categoryService;
private final CategoryUseCase categoryUseCase;
private final LayoutRenderingQueries layoutRenderingQueries;
private final ModelMapper modelMapper;
@GetMapping("article/write")
String getArticleWriteForm(Model model) {
layoutRenderingQueries.AddLayoutTo(model);
model.addAttribute("categoryInput", getCategoryDtosForForm());
model.addAttribute("categoryInput", categoryUseCase.findCategoryByTier(2));
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
model.addAttribute("articleDto", new ArticleForm());
return "article/articleWriteForm";
@@ -80,12 +77,11 @@ public class ArticleController {
String updateArticle(@RequestParam Long articleId, Model model) {
ArticleResponseForEdit articleDto = articleQueriesUseCase.getArticleForEdit(articleId);
layoutRenderingQueries.AddLayoutTo(model);
model.addAttribute("categoryInput", getCategoryDtosForForm());
model.addAttribute("categoryInput", categoryUseCase.findCategoryByTier(2));
model.addAttribute("tagsInput", tagsQueriesUseCase.findAllTagDtos());
model.addAttribute("articleDto", articleDto);
return "article/articleEditForm";
}
/*
- 아티클 수정 요청
*/
@@ -96,7 +92,6 @@ public class ArticleController {
articleUseCase.editArticle(ArticleEditRequest.from(articleId, articleForm));
return "redirect:/article/view?articleId=" + articleId;
}
/*
- 아티클 삭제 요청
*/
@@ -106,7 +101,6 @@ public class ArticleController {
articleUseCase.deleteArticle(articleId);
return "redirect:/";
}
/*
- 카테고리별 게시물 조회하기
*/
@@ -117,7 +111,7 @@ public class ArticleController {
@RequestParam Integer page,
Model model) {
PagingBoxHandler pagingBoxHandler =
PagingBoxHandler.createOf(page, getTotalArticleCntByCategory(category, categoryService.getCategoryForView()));
PagingBoxHandler.createOf(page, getTotalArticleCntByCategory(category, categoryUseCase.getCategoryViewForLayout()));
Slice<ArticleResponseForCardBox> articleDtoList =
articleQueriesUseCase.getArticlesByCategory(category, tier, pagingBoxHandler.getCurPageNum());
@@ -132,7 +126,23 @@ public class ArticleController {
return "article/articleList";
}
private int getTotalArticleCntByCategory(String category, CategoryViewForLayout categorys) {
if (categorys.getTitle().equals(category)) {
return categorys.getCount();
} else {
for (CategoryViewForLayout categoryCnt :
categorys.getCategoryTCountList()) {
if (categoryCnt.getTitle().equals(category))
return categoryCnt.getCount();
for (CategoryViewForLayout categoryCntSub : categoryCnt.getCategoryTCountList()) {
if (categoryCntSub.getTitle().equals(category))
return categoryCntSub.getCount();
}
}
}
throw new IllegalArgumentException("'"+category+"' 라는 카테고리는 존재하지 않습니다.");
}
/*
- 태그별 게시물 조회하기
*/
@@ -157,7 +167,6 @@ public class ArticleController {
return "article/articleListByTag";
}
/*
- 검색어별 게시물 조회하기
*/
@@ -183,7 +192,6 @@ public class ArticleController {
return "article/articleListByKeyword";
}
/*
- 아티클 상세 조회
1. 로그인여부 검토
@@ -240,7 +248,6 @@ public class ArticleController {
return "article/articleView";
}
/*
- 쿠키 추가 / 조회수 증가 검토
*/
@@ -269,36 +276,4 @@ public class ArticleController {
return addHitAvailable;
}
}
/*
- 카테고리별 아티클 갯수 구하기
*/
private int getTotalArticleCntByCategory(String category, CategoryForView categorys) {
if (categorys.getTitle().equals(category)) {
return categorys.getCount();
} else {
for (CategoryForView categoryCnt :
categorys.getCategoryTCountList()) {
if (categoryCnt.getTitle().equals(category))
return categoryCnt.getCount();
for (CategoryForView categoryCntSub : categoryCnt.getCategoryTCountList()) {
if (categoryCntSub.getTitle().equals(category))
return categoryCntSub.getCount();
}
}
}
throw new IllegalArgumentException("'"+category+"' 라는 카테고리는 존재하지 않습니다.");
}
/*
- 아티클 폼에 필요한 카테고리 dtos
*/
private List<CategorySimpleView> getCategoryDtosForForm() {
return categoryService
.findCategoryByTier(2)
.stream()
.map(category -> modelMapper.map(category, CategorySimpleView.class))
.collect(Collectors.toList());
}
}

View File

@@ -24,7 +24,7 @@ public class MainController {
- 메인 화면 제어용 컨트롤러
*/
@GetMapping("/")
public String main(Model model) {
String main(Model model) {
// Dto 전처리
List<ArticleResponseForCardBox> popularArticles = articleQueriesUseCase.getPopularArticles();
//
@@ -37,13 +37,11 @@ public class MainController {
- 최신 아티클 무한스크롤로 조회
*/
@GetMapping("/main/article/{lastArticleId}")
public @ResponseBody
List<ArticleResponseForCardBox> mainNextPage(@PathVariable(required = false) Long lastArticleId) {
@ResponseBody List<ArticleResponseForCardBox> mainNextPage(@PathVariable(required = false) Long lastArticleId) {
// Entity to Dto
List<ArticleResponseForCardBox> articles = articleQueriesUseCase.getRecentArticles(lastArticleId);
// 화면렌더링을 위한 파싱
for(ArticleResponseForCardBox article : articles){
String content = Jsoup.parse(getHtmlRenderer().render(getParser().parse(article.getContent()))).text();
@@ -52,7 +50,6 @@ public class MainController {
}
article.setContent(content);
}
return articles;
}
}

View File

@@ -24,7 +24,6 @@ public class TempArticleController {
public String autoSaveTemp(@RequestBody TempArticleResponse tempArticleResponse){
tempArticleService.saveTemp(new TempArticle(tempArticleResponse.getContent()));
return "저장성공";
}
@@ -36,10 +35,8 @@ public class TempArticleController {
TempArticleResponse getTempArticle(){
Optional<TempArticle> tempArticle = tempArticleService.getTempArticle();
TempArticleResponse tempArticleResponse = new TempArticleResponse();
tempArticleResponse.setContent(tempArticle.orElse(new TempArticle()).getContent());
return tempArticleResponse;
}
}

View File

@@ -51,7 +51,7 @@ public class ArticleRepositoryAdapter implements ArticleRepositoryPort {
}
@Override
public Slice<Article> findBySupCategoryOrderByIdDesc(Pageable pageable, String category) {
public Slice<Article> findBySuperCategoryOrderByIdDesc(Pageable pageable, String category) {
return jpaArticleRepository.findBySupCategoryOrderByIdDesc(pageable,category);
}

View File

@@ -12,7 +12,7 @@ import myblog.blog.article.application.port.response.ArticleResponseByCategory;
import myblog.blog.article.application.port.response.ArticleResponseForDetail;
import myblog.blog.article.application.port.response.ArticleResponseForEdit;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.appliacation.CategoryService;
import org.modelmapper.ModelMapper;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
@@ -78,7 +78,7 @@ public class ArticleQueries implements ArticleQueriesUseCase {
}
if (tier.equals(1)) {
articles = articleRepositoryPort
.findBySupCategoryOrderByIdDesc(
.findBySuperCategoryOrderByIdDesc(
PageRequest.of(pageResolve(page), 5), category);
}
if (tier.equals(2)) {

View File

@@ -13,7 +13,7 @@ import myblog.blog.article.domain.Article;
import myblog.blog.category.domain.Category;
import myblog.blog.member.doamin.Member;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.appliacation.CategoryService;
import myblog.blog.member.service.Oauth2MemberService;
import org.springframework.cache.annotation.CacheEvict;

View File

@@ -16,7 +16,7 @@ public interface ArticleRepositoryPort {
List<Article> findByOrderByIdDescWithList(Pageable pageable);
List<Article> findByOrderByIdDesc(Long articleId, Pageable pageable);
Slice<Article> findBySubCategoryOrderByIdDesc(Pageable pageable, String category);
Slice<Article> findBySupCategoryOrderByIdDesc(Pageable pageable, String category);
Slice<Article> findBySuperCategoryOrderByIdDesc(Pageable pageable, String category);
Article findArticleByIdFetchCategoryAndTags(Long articleId);
Page<Article> findAllByArticleTagsOrderById(Pageable pageable, String tag);
Page<Article> findAllByKeywordOrderById(Pageable pageable, String keyword);

View File

@@ -83,6 +83,14 @@ public class Article extends BasicEntity {
this.hit = 0L;
}
private String makeDefaultThumbOf(String thumbnailUrl) {
String defaultThumbUrl = "https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg";
if (thumbnailUrl == null || thumbnailUrl.equals("")) {
thumbnailUrl = defaultThumbUrl;
}
return thumbnailUrl;
}
/*
- 아티클 수정을 위한 로직
*/
@@ -95,22 +103,8 @@ public class Article extends BasicEntity {
this.thumbnailUrl = getThumbnailUrl();
}
}
/*
- 아티클 조회수 증가
*/
public void addHit(){
this.hit++;
}
/*
- 썸네일 기본 작성
*/
private String makeDefaultThumbOf(String thumbnailUrl) {
String defaultThumbUrl = "https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg";
if (thumbnailUrl == null || thumbnailUrl.equals("")) {
thumbnailUrl = defaultThumbUrl;
}
return thumbnailUrl;
}
}

View File

@@ -0,0 +1,55 @@
package myblog.blog.category.adapter.imcomming;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import myblog.blog.comment.dto.CommentDtoForLayout;
import myblog.blog.comment.service.CommentService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@RequiredArgsConstructor
public class CategoryController {
private final CategoryUseCase categoryUseCase;
private final CommentService commentService;
private final CategoryListValidator categorylistValidator;
/*
- 카테고리 수정폼 조회
*/
@GetMapping("/category/edit")
public String editCategoryForm(Model model) {
List<CategorySimpleDto> categoryList = categoryUseCase.getCategorytCountList();
List<CategorySimpleDto> copyList = new ArrayList<>(List.copyOf(categoryList));
copyList.remove(0);
CategoryViewForLayout categoryViewForLayout = CategoryViewForLayout.from(categoryList);
List<CommentDtoForLayout> comments = commentService.recentCommentList();
model.addAttribute("categoryForEdit", copyList);
model.addAttribute("category", categoryViewForLayout);
model.addAttribute("commentsList", comments);
return "admin/categoryEdit";
}
/*
- 카테고리 수정 요청
*/
@PostMapping("/category/edit")
public @ResponseBody
String editCategory(@RequestBody List<CategorySimpleDto> categoryList, Errors errors) {
// List DTO 검증을 위한 커스텀 validator
categorylistValidator.validate(categoryList, errors);
categoryUseCase.changeCategory(categoryList);
return "변경 성공";
}
}

View File

@@ -1,12 +1,14 @@
package myblog.blog.shared.exception;
package myblog.blog.category.adapter.imcomming;
import lombok.RequiredArgsConstructor;
import myblog.blog.shared.exception.CustomFormException;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import java.util.List;
import java.util.Objects;
/*
@@ -14,7 +16,7 @@ import java.util.List;
*/
@Component
@RequiredArgsConstructor
public class ListValidator implements Validator {
public class CategoryListValidator implements Validator {
private final SpringValidatorAdapter springValidatorAdapter;
@@ -28,5 +30,8 @@ public class ListValidator implements Validator {
for(Object object : (List)target){
springValidatorAdapter.validate(object,errors);
}
if (errors.hasErrors()) {
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
}
}
}

View File

@@ -0,0 +1,54 @@
package myblog.blog.category.adapter.outgoing.persistence;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.appliacation.port.outgoing.CategoryRepositoryPort;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import myblog.blog.category.domain.Category;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
@RequiredArgsConstructor
public class CategoryRepositoryAdapter implements CategoryRepositoryPort {
private final JpaCategoryRepository jpaCategoryRepository;
private final MybatisCategoryRepository mybatisCategoryRepository;
@Override
public Optional<Category> findByTitle(String title) {
return jpaCategoryRepository.findByTitle(title);
}
@Override
public List<Category> findAll() {
return jpaCategoryRepository.findAll();
}
@Override
public List<CategorySimpleDto> getCategoryCount() {
return mybatisCategoryRepository.getCategoryCount();
}
@Override
public List<Category> findAllByTierIs(int tier) {
return jpaCategoryRepository.findAllByTierIs(tier);
}
@Override
public List<Category> findAllWithoutDummy() {
return jpaCategoryRepository.findAllWithoutDummy();
}
@Override
public void deleteAll(List<Category> categoryListFromDb) {
jpaCategoryRepository.deleteAll(categoryListFromDb);
}
@Override
public void save(Category category) {
jpaCategoryRepository.save(category);
}
}

View File

@@ -1,4 +1,4 @@
package myblog.blog.category.repository;
package myblog.blog.category.adapter.outgoing.persistence;
import myblog.blog.category.domain.Category;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface CategoryRepository extends JpaRepository<Category, Long> {
public interface JpaCategoryRepository extends JpaRepository<Category, Long> {
/*
- 카테고리 이름으로 카테고리 찾기

View File

@@ -1,6 +1,6 @@
package myblog.blog.category.repository;
package myblog.blog.category.adapter.outgoing.persistence;
import myblog.blog.category.dto.CategorySimpleView;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@@ -9,7 +9,7 @@ import java.util.List;
@Mapper
@Repository
public interface NaCategoryRepository {
public interface MybatisCategoryRepository {
/*
- 카테고리별 아티클 갯수 통계 쿼리
@@ -23,6 +23,6 @@ public interface NaCategoryRepository {
" group by c.title, b.title with rollup) e\n" +
" right join category f on (e.title = f.title)\n" +
" order by pOrder, cOrder ")
List<CategorySimpleView> getCategoryCount();
List<CategorySimpleDto> getCategoryCount();
}

View File

@@ -1,65 +1,74 @@
package myblog.blog.category.service;
package myblog.blog.category.appliacation;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.domain.Category;
import myblog.blog.category.dto.CategorySimpleView;
import myblog.blog.category.dto.CategoryForView;
import myblog.blog.category.repository.CategoryRepository;
import myblog.blog.category.repository.NaCategoryRepository;
import myblog.blog.category.appliacation.port.incomming.CategoryUseCase;
import myblog.blog.category.appliacation.port.outgoing.CategoryRepositoryPort;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import org.modelmapper.ModelMapper;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional
@RequiredArgsConstructor
public class CategoryService {
public class CategoryService implements CategoryUseCase {
private final CategoryRepository categoryRepository;
private final NaCategoryRepository naCategoryRepository;
private final CategoryRepositoryPort categoryRepositoryPort;
private final ModelMapper modelMapper;
/*
- 카테고리 이름으로 카테고리 찾기
*/
@Override
public Category findCategory(String title) {
return categoryRepository.findByTitle(title)
return categoryRepositoryPort.findByTitle(title)
.orElseThrow(() -> new IllegalArgumentException("NotFoundCategoryException"));
}
/*
- 카테고리 이름으로 카테고리 찾기
*/
@Override
public List<Category> getAllCategories() {
return categoryRepository.findAll();
return categoryRepositoryPort.findAll();
}
/*
- 카테고리와 카테고리별 아티클 찾기
*/
public List<CategorySimpleView> getCategorytCountList() {
return naCategoryRepository.getCategoryCount();
@Override
public List<CategorySimpleDto> getCategorytCountList() {
return categoryRepositoryPort.getCategoryCount();
}
/*
- getCategorytCountList() 캐싱을 위한 전처리 매핑 로직
- 본래는 컨트롤러단에서 존재해야할 dto 매핑코드지만 캐싱을 위해 서비스단으로 이동
- 레이아웃 렌더링 성능 향상을 위해 캐싱작업
카테고리 변경 / 아티클 변경이 존재할경우 레이아웃 캐시 초기화
*/
@Cacheable(value = "layoutCaching", key = "0")
public CategoryForView getCategoryForView() {
return CategoryForView.createCategory(naCategoryRepository.getCategoryCount());
@Override
public CategoryViewForLayout getCategoryViewForLayout() {
return CategoryViewForLayout.from(categoryRepositoryPort.getCategoryCount());
}
/*
- 티어별 카테고리 목록 찾기
*/
public List<Category> findCategoryByTier(int tier) {
return categoryRepository.findAllByTierIs(tier);
@Override
public List<CategorySimpleDto> findCategoryByTier(int tier) {
return categoryRepositoryPort.findAllByTierIs(tier)
.stream()
.map(category -> modelMapper.map(category, CategorySimpleDto.class))
.collect(Collectors.toList());
}
/*
@@ -71,138 +80,114 @@ public class CategoryService {
3-2 DB에 존재하지 않는경우 새로 생성
3-3 DB에만 존재하는 카테고리는 삭제처리
*/
@Override
@Transactional
@CacheEvict(value = {"layoutCaching", "seoCaching"}, allEntries = true)
public void changeCategory(List<CategorySimpleView> categoryList) {
public void changeCategory(List<CategorySimpleDto> categoryList) {
// 1.카테고리 리스트 순서 작성
sortingOrder(categoryList);
CategorySimpleDto.sortByOrder(categoryList);
// 2. 기존 DB 저장된 카테고리 리스트 불러오기
List<Category> categoryListFromDb = categoryRepository.findAllWithoutDummy();
List<Category> categoryListFromDb = categoryRepositoryPort.findAllWithoutDummy();
// 3. 카테고리 변경 루프
// 3. 카테고리 변경
while (!categoryList.isEmpty()) {
CategorySimpleView categorySimpleView = categoryList.get(0);
CategorySimpleDto categorySimpleDto = categoryList.get(0);
categoryList.remove(0);
// 부모카테고리인경우
if (categorySimpleView.getTier() == 1) {
if (categorySimpleDto.getTier() == 1) {
Category pCategory = null;
// 부모카테고리가 기존에 존재 x
if (categorySimpleView.getId() == null) {
pCategory = createNewCategory(categorySimpleView, null);
// 신규 부모인경우
if (categorySimpleDto.getId() == null) {
pCategory = createNewCategory(categorySimpleDto, null);
}
// 부모카테고리가 기존 존재 o
// 기존 부모인경우
else {
for (int i = 0; i < categoryListFromDb.size(); i++) {
if (categoryListFromDb.get(i).getId().equals(categorySimpleView.getId())) {
if (categoryListFromDb.get(i).getId().equals(categorySimpleDto.getId())) {
pCategory = categoryListFromDb.get(i);
categoryListFromDb.remove(i);
break;
}
}
pCategory.updateCategory(
categorySimpleView.getTitle(),
categorySimpleView.getTier(),
categorySimpleView.getPOrder(),
categorySimpleView.getCOrder(),
categorySimpleDto.getTitle(),
categorySimpleDto.getTier(),
categorySimpleDto.getPOrder(),
categorySimpleDto.getCOrder(),
null
);
}
while (!categoryList.isEmpty()) {
CategorySimpleView subCategorySimpleView = categoryList.get(0);
if (subCategorySimpleView.getTier() == 1) break;
CategorySimpleDto subCategorySimpleDto = categoryList.get(0);
if (subCategorySimpleDto.getTier() == 1) break;
categoryList.remove(0);
// 자식 카테고리인경우
Category cCategory = null;
// 카테고리가 기존에 존재 x
if (subCategorySimpleView.getId() == null) {
cCategory = createNewCategory(subCategorySimpleView, pCategory.getTitle());
if (subCategorySimpleDto.getId() == null) {
cCategory = createNewCategory(subCategorySimpleDto, pCategory.getTitle());
}
// 카테고리가 기존에 존재 o
else {
for (int i = 0; i < categoryListFromDb.size(); i++) {
if (categoryListFromDb.get(i).getId().equals(subCategorySimpleView.getId())) {
if (categoryListFromDb.get(i).getId().equals(subCategorySimpleDto.getId())) {
cCategory = categoryListFromDb.get(i);
categoryListFromDb.remove(i);
break;
}
}
cCategory.updateCategory(
subCategorySimpleView.getTitle(),
subCategorySimpleView.getTier(),
subCategorySimpleView.getPOrder(),
subCategorySimpleView.getCOrder(),
subCategorySimpleDto.getTitle(),
subCategorySimpleDto.getTier(),
subCategorySimpleDto.getPOrder(),
subCategorySimpleDto.getCOrder(),
pCategory);
}
}
}
}
// 3-3 불일치 카테고리 전부 삭제
categoryRepository.deleteAll(categoryListFromDb);
categoryRepositoryPort.deleteAll(categoryListFromDb);
}
/*
- 새로운 카테고리 생성하기
- 상위 카테고리 존재 유무 분기
*/
private Category createNewCategory(CategorySimpleView categorySimpleView, String parent) {
private Category createNewCategory(CategorySimpleDto categorySimpleDto, String parent) {
Category parentCategory = null;
if (parent != null) {
parentCategory = categoryRepository.findByTitle(parent)
parentCategory = categoryRepositoryPort.findByTitle(parent)
.orElseThrow(() -> new IllegalArgumentException("NotFoundCategoryException"));
}
Category category = Category.builder()
.title(categorySimpleView.getTitle())
.pSortNum(categorySimpleView.getPOrder())
.cSortNum(categorySimpleView.getCOrder())
.tier(categorySimpleView.getTier())
.title(categorySimpleDto.getTitle())
.pSortNum(categorySimpleDto.getPOrder())
.cSortNum(categorySimpleDto.getCOrder())
.tier(categorySimpleDto.getTier())
.parents(parentCategory)
.build();
categoryRepository.save(category);
categoryRepositoryPort.save(category);
return category;
}
/*
- 카테고리 변경을 위해 카테고리 순번을 작성하는 로직
*/
private void sortingOrder(List<CategorySimpleView> categoryList) {
int pOrderIndex = 0;
int cOrderIndex = 0;
//티어별 트리구조로 순서 작성 로직
for (CategorySimpleView categoryDto : categoryList) {
if (categoryDto.getTier() == 1) {
cOrderIndex = 0;
categoryDto.setPOrder(++pOrderIndex);
categoryDto.setCOrder(cOrderIndex);
} else {
categoryDto.setPOrder(pOrderIndex);
categoryDto.setCOrder(++cOrderIndex);
}
}
}
/*
- 최초 필수 더미 카테고리 추가 코드
*/
// /*
// - 최초 필수 더미 카테고리 추가 코드
// */
// @PostConstruct
private void insertDummyCategory() {
if(categoryRepository.findByTitle("total")==null) {
Category category0 = Category.builder()
.tier(0)
.title("total")
.pSortNum(0)
.cSortNum(0)
.build();
categoryRepository.save(category0);
}
}
// private void insertDummyCategory() {
// if(categoryRepositoryPort.findByTitle("total")==null) {
// Category category0 = Category.builder()
// .tier(0)
// .title("total")
// .pSortNum(0)
// .cSortNum(0)
// .build();
// categoryRepositoryPort.save(category0);
// }
// }
}

View File

@@ -0,0 +1,16 @@
package myblog.blog.category.appliacation.port.incomming;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import myblog.blog.category.domain.Category;
import java.util.List;
public interface CategoryUseCase {
Category findCategory(String title);
List<Category> getAllCategories();
List<CategorySimpleDto> getCategorytCountList();
CategoryViewForLayout getCategoryViewForLayout();
List<CategorySimpleDto> findCategoryByTier(int tier);
void changeCategory(List<CategorySimpleDto> categoryList);
}

View File

@@ -0,0 +1,17 @@
package myblog.blog.category.appliacation.port.outgoing;
import myblog.blog.category.appliacation.port.response.CategorySimpleDto;
import myblog.blog.category.domain.Category;
import java.util.List;
import java.util.Optional;
public interface CategoryRepositoryPort {
Optional<Category> findByTitle(String title);
List<Category> findAll();
List<CategorySimpleDto> getCategoryCount();
List<Category> findAllByTierIs(int tier);
List<Category> findAllWithoutDummy();
void deleteAll(List<Category> categoryListFromDb);
void save(Category category);
}

View File

@@ -0,0 +1,53 @@
package myblog.blog.category.appliacation.port.response;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
import java.util.List;
/*
- 범용 카테고리 DTO
*/
@Getter
@Setter
@ToString
public class CategorySimpleDto implements Cloneable {
private Long id;
@NotBlank(message = "카테고리명은 공백일 수 없습니다.")
private String title;
private int tier;
private int count;
private int pOrder;
private int cOrder;
@Override
public CategorySimpleDto clone() {
try {
return (CategorySimpleDto) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
/*
- 카테고리 변경을 위해 카테고리의 순번을 작성하는 로직
*/
static public void sortByOrder(List<CategorySimpleDto> categoryList) {
int pOrderIndex = 0;
int cOrderIndex = 0;
//티어별 트리구조로 순서 작성 로직
for (CategorySimpleDto categorySimpleDto : categoryList) {
if (categorySimpleDto.getTier() == 1) {
cOrderIndex = 0;
categorySimpleDto.setPOrder(++pOrderIndex);
categorySimpleDto.setCOrder(cOrderIndex);
} else {
categorySimpleDto.setPOrder(pOrderIndex);
categorySimpleDto.setCOrder(++cOrderIndex);
}
}
}
}

View File

@@ -0,0 +1,67 @@
package myblog.blog.category.appliacation.port.response;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/*
- 레이아웃용 트리구조 카테고리 리스트
*/
@Getter
@Setter
public class CategoryViewForLayout {
private int count;
private String title;
private Long id;
private int pOrder;
private int cOrder;
// 트리구조를 갖기 위한 리스트
private List<CategoryViewForLayout> categoryTCountList = new ArrayList<>();
/*
- 스태틱 생성 메서드
*/
public static CategoryViewForLayout from(List<CategorySimpleDto> crList) {
return recursiveBuildFromCategoryDto(0, crList);
}
/*
- 재귀호출로 트리구조 생성
1. DTO객체 생성후 소스를 큐처리로 순차적 매핑
2. Depth 변화시 재귀 호출 / 재귀 탈출
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
*/
private static CategoryViewForLayout recursiveBuildFromCategoryDto(int tier, List<CategorySimpleDto> source) {
CategoryViewForLayout categoryViewForLayout = new CategoryViewForLayout();
while (!source.isEmpty()) {
CategorySimpleDto cSource = source.get(0);
if (cSource.getTier() == tier) {
if(categoryViewForLayout.getTitle() != null
&& !categoryViewForLayout.getTitle().equals(cSource.getTitle())){
return categoryViewForLayout;
}
categoryViewForLayout.setTitle(cSource.getTitle());
categoryViewForLayout.setCount(cSource.getCount());
categoryViewForLayout.setId(cSource.getId());
categoryViewForLayout.setCOrder(cSource.getCOrder());
categoryViewForLayout.setPOrder(cSource.getPOrder());
source.remove(0);
} else if (cSource.getTier() > tier) {
CategoryViewForLayout sub = recursiveBuildFromCategoryDto(tier + 1, source);
categoryViewForLayout.getCategoryTCountList().add(sub);
} else {
return categoryViewForLayout;
}
}
return categoryViewForLayout;
}
private CategoryViewForLayout() {
}
}

View File

@@ -1,77 +0,0 @@
package myblog.blog.category.controller;
import lombok.RequiredArgsConstructor;
import myblog.blog.shared.exception.CustomFormException;
import myblog.blog.shared.exception.ListValidator;
import myblog.blog.category.dto.CategoryForView;
import myblog.blog.category.dto.CategorySimpleView;
import myblog.blog.category.service.CategoryService;
import myblog.blog.comment.dto.CommentDtoForLayout;
import myblog.blog.comment.service.CommentService;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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.ResponseBody;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Controller
@RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;
private final CommentService commentService;
private final ModelMapper modelMapper;
private final ListValidator listValidator;
/*
- 카테고리 수정폼 조회
*/
@GetMapping("/edit/category")
public String editCategoryForm(Model model) {
// DTO 매핑 전처리
List<CategorySimpleView> categoryList = categoryService.getCategorytCountList();
List<CategorySimpleView> copyList = cloneList(categoryList);
copyList.remove(0);
CategoryForView categoryForView = CategoryForView.createCategory(categoryList);
List<CommentDtoForLayout> comments = commentService.recentCommentList();
//
model.addAttribute("categoryForEdit", copyList);
model.addAttribute("category", categoryForView);
model.addAttribute("commentsList", comments);
return "admin/categoryEdit";
}
/*
- 카테고리 수정 요청
*/
@PostMapping("/category/edit")
public @ResponseBody
String editCategory(@RequestBody List<CategorySimpleView> categoryList, Errors errors) {
// List DTO 검증을 위한 커스텀 validator
listValidator.validate(categoryList, errors);
// 유효성 검사
if (errors.hasErrors()) {
throw new CustomFormException(Objects.requireNonNull(errors.getFieldError()).getDefaultMessage());
}
categoryService.changeCategory(categoryList);
return "변경 성공";
}
private List<CategorySimpleView> cloneList(List<CategorySimpleView> categoryList) {
return categoryList
.stream()
.map(categoryNormalDto ->
modelMapper.map(categoryNormalDto, CategorySimpleView.class))
.collect(Collectors.toList());
}
}

View File

@@ -3,8 +3,9 @@ package myblog.blog.category.domain;
import lombok.Builder;
import lombok.Getter;
import myblog.blog.article.domain.Article;
import myblog.blog.shared.BasicEntity;
import myblog.blog.article.domain.Article;
import javax.persistence.*;
import java.util.ArrayList;

View File

@@ -1,67 +0,0 @@
package myblog.blog.category.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/*
- 레이아웃용 트리구조 카테고리 리스트
*/
@Getter
@Setter
public class CategoryForView {
private int count;
private String title;
private Long id;
private int pOrder;
private int cOrder;
// 트리구조를 갖기 위한 리스트
private List<CategoryForView> categoryTCountList = new ArrayList<>();
/*
- 스태틱 생성 메서드
*/
public static CategoryForView createCategory(List<CategorySimpleView> crList) {
return recursiveBuildFromCategoryDto(0, crList);
}
/*
- 재귀호출로 트리구조 생성
1. DTO객체 생성후 소스를 큐처리로 순차적 매핑
2. Depth 변화시 재귀 호출 / 재귀 탈출
3. 탈출시 상위 카테고리 list로 삽입하여 트리구조 작성
*/
private static CategoryForView recursiveBuildFromCategoryDto(int tier, List<CategorySimpleView> source) {
CategoryForView categoryForView = new CategoryForView();
while (!source.isEmpty()) {
CategorySimpleView cSource = source.get(0);
if (cSource.getTier() == tier) {
if(categoryForView.getTitle() != null
&& !categoryForView.getTitle().equals(cSource.getTitle())){
return categoryForView;
}
categoryForView.setTitle(cSource.getTitle());
categoryForView.setCount(cSource.getCount());
categoryForView.setId(cSource.getId());
categoryForView.setCOrder(cSource.getCOrder());
categoryForView.setPOrder(cSource.getPOrder());
source.remove(0);
} else if (cSource.getTier() > tier) {
CategoryForView sub = recursiveBuildFromCategoryDto(tier + 1, source);
categoryForView.getCategoryTCountList().add(sub);
} else {
return categoryForView;
}
}
return categoryForView;
}
private CategoryForView() {
}
}

View File

@@ -1,25 +0,0 @@
package myblog.blog.category.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
/*
- 범용 카테고리 DTO
*/
@Getter
@Setter
@ToString
public class CategorySimpleView {
private Long id;
@NotBlank(message = "카테고리명은 공백일 수 없습니다.")
private String title;
private int tier;
private int count;
private int pOrder;
private int cOrder;
}

View File

@@ -1,9 +1,8 @@
package myblog.blog.member.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import myblog.blog.category.dto.CategoryForView;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import myblog.blog.category.appliacation.CategoryService;
import myblog.blog.comment.dto.CommentDtoForLayout;
import myblog.blog.comment.service.CommentService;
import org.springframework.stereotype.Controller;
@@ -12,7 +11,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import java.util.stream.Collectors;
@Controller
@RequiredArgsConstructor
@@ -34,11 +32,11 @@ public class MemberController {
}
// 레이아웃 DTO 전처리
CategoryForView categoryForView = categoryService.getCategoryForView();
CategoryViewForLayout categoryViewForLayout = categoryService.getCategoryViewForLayout();
List<CommentDtoForLayout> comments = commentService.recentCommentList();
//
model.addAttribute("category",categoryForView);
model.addAttribute("category", categoryViewForLayout);
model.addAttribute("commentsList", comments);
return "login";

View File

@@ -9,7 +9,7 @@ import lombok.RequiredArgsConstructor;
import myblog.blog.article.domain.Article;
import myblog.blog.category.domain.Category;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.appliacation.CategoryService;
import org.jdom2.*;
import org.jdom2.output.*;

View File

@@ -1,8 +1,8 @@
package myblog.blog.shared.queries;
import lombok.RequiredArgsConstructor;
import myblog.blog.category.dto.CategoryForView;
import myblog.blog.category.service.CategoryService;
import myblog.blog.category.appliacation.port.response.CategoryViewForLayout;
import myblog.blog.category.appliacation.CategoryService;
import myblog.blog.comment.dto.CommentDtoForLayout;
import myblog.blog.comment.service.CommentService;
import org.springframework.stereotype.Component;
@@ -21,9 +21,9 @@ public class LayoutRenderingQueries {
- 레이아웃에 필요한 모델 담기
*/
public void AddLayoutTo(Model model) {
CategoryForView categoryForView = categoryService.getCategoryForView();
CategoryViewForLayout categoryViewForLayout = categoryService.getCategoryViewForLayout();
List<CommentDtoForLayout> comments = commentService.recentCommentList();
model.addAttribute("category", categoryForView);
model.addAttribute("category", categoryViewForLayout);
model.addAttribute("commentsList", comments);
}
}

View File

@@ -47,7 +47,7 @@
<li class="border-top my-3"></li>
<li class="mb-1 superCategory btn fs-5"><a th:href="@{/article/list(category=total,tier=0,page=1)}"
th:text="|전체글(${category.getCount()})|">토탈</a></li>
<a th:href="@{/edit/category}" sec:authorize="hasRole('ADMIN')" class="me-0 ps-0 btn"><i class="fas fa-pen"></i></a>
<a th:href="@{/category/edit}" sec:authorize="hasRole('ADMIN')" class="me-0 ps-0 btn"><i class="fas fa-pen"></i></a>
<li class="mb-1" th:each="superCategory : ${category.getCategoryTCountList()}">
<div class="btn-group">

View File

@@ -4,7 +4,7 @@ import myblog.blog.article.domain.Article
import myblog.blog.article.application.ArticleService
import myblog.blog.seo.application.SiteMapService
import myblog.blog.category.domain.Category
import myblog.blog.category.service.CategoryService
import myblog.blog.category.appliacation.CategoryService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
//import org.junit.Test