본문 바로가기
개발일지_development diary/YSit

YSit [11] - 게시물 작성 / 목록 기능 구현

by YES_developNewbie 2022. 12. 28.

유저 기능은 어느정도 구현이 끝났으므로 게시판 기능을 구현을 시작했다.

먼저 게시판 카테고리가 테이블로 만드는 것보다 Enum타입으로 구현하는게 더 편해보이고, 추가적으로 카테고리를 생성할 수 있어서 테이블이 아닌 Enum타입으로 바꿨다. 

 

[ 게시물Repsitory 생성 ]

 

@Repository@RequiredArgsConstructor 어노테이션을 선언해준다. ( 자세한 설명은 이전 글들 참고 )

엔티티 저장을 위해 엔티티를 파라미터로 받고 엔티티매니저가 persist해주는 메서드를 만들어준다. 그리고 전체 리스트 구하는 메서드, 게시물Id 기준으로 게시물 하나 구하기, 제목 기준, 내용 기준으로 검색하기 메서드를 만들어준다. 

제목 기준, 내용 기준으로 검색은 유사한 형태로 검색해도 게시물이 나와야 한다. 그렇기 때문에 where절에 like문을 써야한다. JPQL에서 Like문을 쓸 때에는 파라미터로 넣는 값 옆에 %를 추가해서 넣으면 된다.

public List<Article> findByTitle(String title) {
    String paramTitle = "%"+title+"%";
    return em.createQuery("select a from Article a where a.title like :title", Article.class)
            .setParameter("title", paramTitle)
            .getResultList();
}

(Param)% : 파라미터 뒤에 어떤 글자가 있으면 조회

%(Param) : 파라미터 앞에 어떤 글자가 있으면 조회

%(Param)% : 데이터에 파라미터가 들어가면 조회

 

[ 게시물Service 생성 ]

 

@Service, @RequiredArgsConstructor 어노테이션을 추가해준다. ( 자세한 설명은 이전 글들 참고 )

게시물 저장 비즈니스 로직을 짠다. 먼저 제목 기준으로 검색하는 findByTitle 메서드를 호출해서 파라미터로 받은 엔티티의 제목이 있는지 확인한다. 만약 검색한 리스트가 비어있다면 Repository의 저장 메서드로 저장을 해준다. 그리고 당연히 데이터 변화가 있기 때문에 @Transactional을 선언해준다.

@Transactional(readOnly = false)
public Long save(Article article) {
    List<Article> articles = articleRepository.findByTitle(article.getTitle());
    if (!articles.isEmpty()){
        throw new IllegalStateException("이미 있는 제목입니다");
    }
    articleRepository.save(article);
    return article.getId();
}

그 외 조회메서드들은 위임만 시켜준다.

 

[ 게시물 테스트 작성 ]

 

먼저 저장메서드를 테스트한다. 빌더로 유저와 게시물 엔티티를 만들어주고, 저장을 한 후, findOne 메서드로 아이디를 조회하여 받은 엔티티가 Null이 아니면 성공이도록 작성했다.

@Test
public void saveTest() {
    String title = "게시물1";
    String body = "내용1";
    Board category = Board.자유;
    User user = User.builder()
            .name("TEST")
            .loginId("TEST")
            .loginPw("TEST")
            .build();
    userService.register(user);
    ArticleStatus status = ArticleStatus.PUBLIC;
    Article article = Article.builder()
            .title(title)
            .body(body)
            .category(category)
            .user(user)
            .status(status)
            .build();
    articleService.save(article);
    Article article1 = articleRepository.findOne(article.getId());
    if (Objects.isNull(article1)) {
        fail("실패");
    }
}

두번째로 저장 테스트와 같이 유저와 게시물 엔티티를 만들어주고 저장을 한 후, 제목 기준으로 리스트를 받아 비어있지 않으면 성공시킨다.

@Test
public void findByTitle() {
    String title = "게시물1";
    String body = "내용1";
    Board category = Board.자유;
    ArticleStatus status = ArticleStatus.PUBLIC;
    User user = User.builder()
            .name("TEST")
            .loginId("TEST")
            .loginPw("TEST")
            .build();
    userService.register(user);
    Article article = Article.builder()
            .title(title)
            .body(body)
            .category(category)
            .user(user)
            .status(status)
            .build();
    articleService.save(article);

    List<Article> articleList = articleRepository.findByTitle("게시물");
    if (articleList.isEmpty()) {
        fail("실패");
    }
}

 

[ . . . ]

 

Home.html에 a태그로 게시물 작성으로 가는 링크를 걸어준다. 

 

[ Enum타입을 select태그에 사용하기 ]

 

<div class="col-md-4">
  <label class="col-form-label">Category:</label>
  <select class="form-control" th:field="*{category}">
    <option th:each="categoryValue : ${T(YSIT.YSit.domain.Board).values()}"
            th:value="${categoryValue.name()}"
            th:text="${categoryValue.name()}">
      val
    </option>
  </select>
</div>

th:field로 id, name을 설정해주고, 타임리프의 th:each로 반복을 돌려준다. ${T(Enum타입의 경로).values()}를 사용해 Enum의 값들을 넣어준다. 그 후에 th:valueth:text로 select 태그의 값을 설정해준다.

 

[ 게시물 작성 컨트롤러 ]

 

ArticleForm을 만들어 엔티티를 대체할 DTO를 만들어준다.

Get방식으로 통신을 읽어서 메서드에 들어오면 세션에 Id값이 있는지 확인한다. 그리고 model에 Form과 세션에서 받은 유저 아이디값을 넣어 반환한다. 

@GetMapping("/article/write")
public String writeForm(Model model, HttpServletRequest request) {
    HttpSession session = request.getSession();
    Long id = (Long) session.getAttribute("Id");
    if (Objects.isNull(session.getAttribute("Id"))){
        return "redirect:/";
    }
    User user = userService.findOne(id);
    model.addAttribute("loginId", user.getLoginId());
    model.addAttribute("articleForm", new ArticleForm());
    return "/article/Write";
}

Post방식으로 통신이 들어오면 세션이 비어있지 않은지, 제목이 중복되지 않은지, 제목과 내용, 카테고리 설정이 비어있지 않은지 확인하고 만약 비어있다면 다시 HTML로 반환한다. 만약 그렇지 않다면 세션에 있는 ID 값을 받아오고, 비공개 체크박스가 체크되어 있는지 여부로 PUBLIC과 PRIVATE로 게시물status를 확인한다. 그리고 세션에서 받은 ID값으로 유저 엔티티를 조회하여 Article빌더로 엔티티를 생성한다. 그 후 저장한다.

@PostMapping("/article/write")
public String write(@ModelAttribute ArticleForm form, BindingResult result,
                    HttpServletRequest request) {
    HttpSession session = request.getSession();

    if (Objects.isNull(session.getAttribute("Id"))){
        return "redirect:/";
    }
    if (!articleRepository.findByTitle(form.getTitle()).isEmpty()) {
        result.rejectValue("title", "sameTitle");
    }
    if (form.getTitle().isBlank()) {
        result.rejectValue("title", "required");
    }
    if (form.getBody().isBlank()) {
        result.rejectValue("body", "required");
    }
    if (form.getCategory() == null) {
        result.rejectValue("category", "required");
    }
    if (result.hasErrors()) {
        return "/article/write";
    }

    Long id = (Long) session.getAttribute("Id");

    ArticleStatus articleStatus;
    if (form.getStatus()) {
        articleStatus = ArticleStatus.PRIVATE;
    } else {
        articleStatus = ArticleStatus.PUBLIC;
    }
    User user = userService.findOne(id);
    Article article = Article.builder()
            .title(form.getTitle())
            .body(form.getBody())
            .status(articleStatus)
            .category(form.getCategory())
            .user(user)
            .regDate(LocalDateTime.now())
            .build();
    articleService.save(article);

    return "redirect:/";
}

 

[ . . . ]

 

나중에 게시물 목록을 구현할 때, 유저 식별자 이외에 작성자 컬럼이 필요할 것 같아 Article도메인에 생성해주었다. 그리고 빌더에도 user엔티티를 파라미터로 받으면 그 엔티티의 로그인아이디를 해당 객체의 작성자로 설정하게 만들었다.

@Builder
public Article (String title, String body, Board category, User user, ArticleStatus status, LocalDateTime regDate) {
    this.title = title;
    this.body = body;
    this.category = category;
    this.user = user;
    this.writeUser = user.getLoginId();
    this.status = status;
    this.regDate = regDate;
}

 

[ 게시물 목록 컨트롤러 ]

 

여느 때와 같이 ArticleListForm을 만들어서 bool타입의 title, body로 제목 기준 검색인지, 내용 기준 검색인지 확인하고, 검색 내용을 search로 받게 구성했다.

@GetMapping의 게시물 목록 컨트롤러는 ArticleListForm을 model에 넣어 반환한다.

@GetMapping("/article/articleList")
public String articleListForm(Model model) {
    model.addAttribute("articleList", new ArticleListForm());
    return "article/ArticleList";
}

 Post로 통신이 들어오면 title, body 중 어떤 곳이 체크가 되어있는지 확인하고 리스트를 받는다. 만약 둘다 체크가 되어있거나 안되어 있을 경우 모든 게시물을 불러온다. 그리고 받은 리스트가 없다면 빌더로 null값을 model에 넣어 반환하고, 아닐 경우 해당 리스트를 model에 넣어 반환한다

@PostMapping("/article/articleList")
public String articleList(@ModelAttribute ArticleListForm form, Model model) {
    int nullCheck = 0;
    List<Article> findList = null;

    if (form.getTitle()) {
        nullCheck += 1;
        findList = articleService.findByTitle(form.getSearch());
    }
    if (form.getBody()) {
        nullCheck += 1;
        findList = articleService.findByBody(form.getSearch());
    }
    if (nullCheck >= 2 || nullCheck <= 0) {
         findList = articleService.findAll();
    }

    if (findList != null) {
        model.addAttribute("articles", findList);
    } else {
        Article article = Article.builder()
                .title(null)
                .body(null)
                .status(null)
                .user(null)
                .regDate(null)
                .build();
        model.addAttribute("articles", article);
    }
    model.addAttribute("articleList", new ArticleListForm());
    return "article/articleList";
}

 

[ 게시물 리스트 HTML 및 타임리프 ]

 

타임리프의 th:each으로 데이터를 받아서 목록을 출력한다. 거기서 제목을 클릭하면 해당 게시물로 들어갈 수 있도록 구성해야 하기 때문에 a 태그를 사용하여 링크를 걸어준다. th:href를 사용하면 값에 따라 다른 url을 걸어줄 수 있다.

<tr th:each="article : ${articles}">
  <td th:text="${article.id}"></td>
  <td>
    <a href="#" th:href="@{/article/articlePage/{id}/view (id=${article.id})}" th:text="${article.title}"></a>
  </td>
  <td th:text="${article.writeUser}"></td>
  <td th:text="${article.category}"></td>
  <td th:text="${article.regDate}"></td>
</tr>

 

[ 게시물 페이지 컨트롤러 ]

 

HTML에서 링크로 통신을 걸어줬기 때문에 Get방식으로 받아서 HTML 코드의 링크를 복사하여 붙여준다. ( article.id 대신 articleId로 교체 )

@PathVariable은 url에서 주는 데이터를 컨트롤러에서 받을 수 있게 하는 어노테이션이다. 이 @PathVariable을 이용해 게시물의 Id값을 받아준다. 그 후 게시물Id값을 이용해 엔티티를 조회한 후 model에 넣어 ArticlePage.html로 반환한다.

 

[ ArticlePage.HTML ]

 

대략적으로 성공한 것만 보여지면 되기 때문에 사용자가 제일 필요로 하는 요소인 제목, 내용, 작성자만 출력하게 코드를 구성했다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
  <div>
      <h1 th:text="${article.title}"></h1>
      <div>
        <label th:for="body">[내용]</label>
        <p th:text="${article.body}"></p>
      </div>
      <div>
        <label th:for="writeUser">작성자</label>
        <p th:text="${article.writeUser}"></p>
      </div>
  </div>
</div> <!-- /container -->
</body>
</html>

 

후기

유저에서 했던 부분들과 겹치는 기능들이 많아서 시간이 오래 걸리지 않았고, 덕분에 하루만에 많은 기능들을 만들었다. 나중에 어느정도 다 완성을 하면 리팩토링을 하면서 조금 더 간편하게, 간단한 코드를 구성해보고 싶다. 이 프로젝트를 하기 전에 했던 코딩과는 다른 느낌이라 매번 하면서 새로운 느낌을 받는다. 내일은 남은 게시물 기능을 모두 끝내보고 싶다.