점프투스프링부트 개선 | 카테고리

Park JeaHyun·2023년 2월 9일

목표

Category 엔티티 생성

ERD

질문 엔티티에 다대일 관계로 카테고리 엔티티를 추가하자. 아래와 같은 ERD가 그려질 것이다.

보다 간단한 방법으로 질문 엔티티에 카테고리 컬럼을 추가해도 상관없을 듯하다.

Entity

엔티티는 아래와 같이 작성

@Entity
@Getter
@Setter
public class Category {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	@Column(unique = true)
	private String name;
	
	@OneToMany(mappedBy = "category")
    private List<Question> questionList;
}
@Getter
@Setter
@Entity
public class Question {
	
    ...(생략)...
    
    @ManyToOne
    private Category category; 
}

게시물 조회 로직 변경

이제 게시물들을 카테고리 별로 조회해야 한다. QuestionService에서 사용하던 조회 조건을 조금 수정하면 쉽게 해결할 수 있다.

  • 기존 조회 조건
public class QuestionService {

    private final QuestionRepository questionRepository;

    private Specification<Question> search(String kw) {
        return new Specification<>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
                query.distinct(true);  // 중복을 제거 
                Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
                Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
                Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);
                return cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // 제목 
                        cb.like(q.get("content"), "%" + kw + "%"),      // 내용 
                        cb.like(u1.get("username"), "%" + kw + "%"),    // 질문 작성자 
                        cb.like(a.get("content"), "%" + kw + "%"),      // 답변 내용 
                        cb.like(u2.get("username"), "%" + kw + "%"));   // 답변 작성자 
            }
        };
    }
    
    public Page<Question> getList(int page, String kw) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        Specification<Question> spec = search(kw);
        return this.questionRepository.findAll(spec, pageable);
    }

    (... 생략 ...)
}
  • 수정된 조회 조건
    기존 or 조건(검색어)에서 and 조건(원하는 카테고리만 조회)을 추가
    즉, 검색 조건이 일치하고 카테고리도 일치하는 게시물들만 조회됨
public class QuestionService {

    private final QuestionRepository questionRepository;

    private Specification<Question> search(String kw, String categoryName) {
        return new Specification<>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
                query.distinct(true);  // 중복을 제거 
                Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
                Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
                Join<Question, Category> c = q.join("category", JoinType.LEFT);
                Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);
                return cb.and(cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // 제목 
                        cb.like(q.get("content"), "%" + kw + "%"),      // 내용 
                        cb.like(u1.get("username"), "%" + kw + "%"),    // 질문 작성자 
                        cb.like(a.get("content"), "%" + kw + "%"),      // 답변 내용 
                        cb.like(u2.get("username"), "%" + kw + "%")),		// 답변 작성자 
                		// and
                		cb.like(c.get("name"), "%" + categoryName + "%"));		// 카테고리 이름
            }
        };
    }

    public Page<Question> getList(int page, String kw, String categoryName) {
    	List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        Specification<Question> spec = search(kw, categoryName);
        return this.questionRepository.findAll(spec, pageable);
    }

    (... 생략 ...)
}

카테고리는 질문 게시판과 자유 게시판으로 나눴다.

  • @GetMapping("/question/list")
  • @GetMapping("/freepost/list")

질문 컨트롤러에 자유 게시판용 메서드를 추가하자. (질문 게시판용 메서드도 비슷하게 수정)

@GetMapping("/freepost/list")
public String freepostList(Model model, @RequestParam(value="page", defaultValue="0") int page,
	@RequestParam(value = "kw", defaultValue = "") String kw) {
    log.info("page:{}, kw:{}", page, kw);
    Page<Question> paging = this.questionService.getList(page, kw, "자유");
    model.addAttribute("paging", paging);
    model.addAttribute("kw", kw);
    return "question_list";
}

기존 레이아웃의 네비게이션 바 수정

<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">SBB</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
				
              	...(생략)...
              
                <li>
					<a class="nav-link" th:href="@{/question/list}">질문 게시판</a>
				</li>
				 <li>
					<a class="nav-link" th:href="@{/freepost/list}">자유 게시판</a>
				</li>
            </ul>
        </div>
    </div>
</nav>

게시물 수정 로직 변경

게시물을 작성할 때도 카테고리를 선택해서 게시물을 생성해야 한다. 따라서 전체 카테고리 중에서 알맞는 카테고리를 선택할 수 있어야 한다. 즉, 아래와 같이 게시물 등록 폼에서 전체 카테고리를 조회한다.

@PreAuthorize("isAuthenticated()")
@GetMapping("/question/create")
public String questionCreate(Model model, QuestionForm questionForm) {
	model.addAttribute("categoryList", categoryService.getList()); 
    return "question_form";
}
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">게시물 등록</h5>
    <form th:object="${questionForm}" method="post">
		
      	...(생략)...
      
        <div class="mb-3">
			<label for="category" class="form-label">카테고리</label>
			<select class="form-select" th:field="*{category}">
				<option th:each="category : ${categoryList}" th:value="${category.name}" th:text="${category.name}"></option>
			</select>
		</div>
        <input type="submit" value="저장하기" class="btn btn-primary my-2">
    </form>
</div>
</html>

게시물을 작성하면서 선택된 카테고리는 QuestionForm 클래스에 담겨서 컨트롤러에 넘어오게 수정한다.

@Getter
@Setter
public class QuestionForm {
    @NotEmpty(message="제목은 필수항목입니다.")
    @Size(max=200)
    private String subject;

    @NotEmpty(message="내용은 필수항목입니다.")
    private String content;
    
    @NotEmpty(message="카테고리는 필수항목입니다.")
    private String category; 
}

이제 컨트롤러에 넘어온 카테고리 이름으로 카테고리 엔티티를 조회하고, 조회한 카테고리 엔티티를 질문 엔티티에 넣어주고 저장하면 질문에 카테고리가 생기게 된다.

@PreAuthorize("isAuthenticated()")
@PostMapping("/question/create")
public String questionCreate(Model model, @Valid QuestionForm questionForm, 
	BindingResult bindingResult, Principal principal) {
    if (bindingResult.hasErrors()) {
    	model.addAttribute("categoryList", categoryService.getList());
        return "question_form";
    }
    SiteUser siteUser = this.userService.getUser(principal.getName());
    Category category = this.categoryService.getCategory(questionForm.getCategory());
    this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser, category);
    return "redirect:/question/list";
    }

1개의 댓글

comment-user-thumbnail
2025년 6월 2일

좋은 글 감사합니다!! 근데 네비게이션 바 수정에서 자유게시판 href를 /question/freepost/list 로 해야할 거 같아요. 오타나신거 같은데 다른 분들 참고하세요

답글 달기